mirror of
https://gitlab.gnome.org/jpu/cambalache.git
synced 2025-06-25 00:02:51 -04:00
Compare commits
434 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 | ||
|
abadecda45 | ||
|
d5c72f6a15 | ||
|
d2e0139b0a | ||
|
f3752d12f9 | ||
|
3900156456 | ||
|
b8ba3bbec0 | ||
|
f6e59fbef4 | ||
|
de2095e29e | ||
|
450a4f00ac | ||
|
1f22224003 | ||
|
a5e03e693b | ||
|
471477b4ce | ||
|
367a6bdf53 | ||
|
17728ace99 | ||
|
705162d571 | ||
|
439418807d | ||
|
9b630afcd5 | ||
|
4b1f60a988 | ||
|
5a6e9f7b46 | ||
|
973c575908 | ||
|
dafe8dbd78 | ||
|
315ca0cfb0 | ||
|
a350cbb143 | ||
|
6adce49744 | ||
|
706d68b476 | ||
|
9c12a62f36 | ||
|
59ab9fae66 | ||
|
964c788a2a | ||
|
6b716b5dfc | ||
|
4b4f79ba49 | ||
|
f9299fb044 | ||
|
5649959c2a | ||
|
e280c283d3 | ||
|
e672787fe3 | ||
|
947462afa0 | ||
|
b09338642d | ||
|
ba726349fb | ||
|
26e87f052c | ||
|
50153ffe76 | ||
|
69ceba6f42 | ||
|
72831b416b | ||
|
cc8a773b40 | ||
|
8081765541 | ||
|
57c34d9050 | ||
|
dad433dc72 | ||
|
1fe0680802 | ||
|
bf0c00739f | ||
|
1123ca8315 | ||
|
2b0f5bc5b7 | ||
|
4962ad41b8 | ||
|
813dbfd986 | ||
|
7a57a3accf | ||
|
0918dbbca5 |
1
.flake8
1
.flake8
@ -8,6 +8,7 @@ per-file-ignores =
|
||||
cambalache/merengue/__init__.py:F401,E402
|
||||
cambalache/merengue/mrg_adw/__init__.py:F401,E402
|
||||
cambalache/merengue/mrg_handy/__init__.py:F401,E402
|
||||
cambalache/merengue/mrg_webkit/__init__.py:F401,E402
|
||||
cambalache/merengue/mrg_webkit2/__init__.py:F401,E402
|
||||
cambalache/merengue/mrg_gtk/__init__.py:F401,E402
|
||||
glade/gladecambalache/__init__.py:F401,E402
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -3,7 +3,7 @@
|
||||
__pycache__
|
||||
.flatpak-builder
|
||||
.catalogs
|
||||
.lib
|
||||
.local
|
||||
.lc_messages
|
||||
.vscode
|
||||
.env.local
|
||||
@ -17,6 +17,7 @@ cambalache/app.gresource
|
||||
cambalache/config.py
|
||||
cambalache/merengue/config.py
|
||||
cambalache/merengue/merengue
|
||||
subprojects/casilda
|
||||
tools/CmbUtils-3.0.gir
|
||||
tools/CmbUtils-3.0.typelib
|
||||
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
|
||||
|
||||
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-4.0 \
|
||||
gir1.2-gtksource-4 \
|
||||
gir1.2-gtksource-5 \
|
||||
gir1.2-handy-1 \
|
||||
gir1.2-adw-1 \
|
||||
gir1.2-webkit2-4.1 \
|
||||
gir1.2-webkit-6.0 \
|
||||
python3-lxml \
|
||||
meson \
|
||||
ninja-build \
|
||||
git \
|
||||
libadwaita-1-dev \
|
||||
libgirepository-1.0-dev \
|
||||
libgtk-3-dev \
|
||||
libgtk-4-dev \
|
||||
libhandy-1-dev \
|
||||
libadwaita-1-dev \
|
||||
gettext \
|
||||
desktop-file-utils
|
||||
libwlroots-dev \
|
||||
meson \
|
||||
ninja-build \
|
||||
python3-gi \
|
||||
python3-lxml \
|
||||
python-gi-dev
|
||||
|
||||
|
||||
RUN useradd -ms /bin/bash discepolo
|
||||
ENV DISPLAY :0
|
||||
@ -25,13 +30,31 @@ ENV DISPLAY :0
|
||||
RUN mkdir -p /src/build
|
||||
|
||||
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
|
||||
|
||||
RUN meson --prefix=/usr
|
||||
RUN ninja
|
||||
RUN ninja install
|
||||
RUN meson --prefix=/usr && ninja && ninja install
|
||||
|
||||
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
|
||||
ENTRYPOINT ["/bin/sh", "-c", "$0 \"$@\"", "cambalache"]
|
3
Makefile
3
Makefile
@ -1,4 +1,7 @@
|
||||
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
|
||||
|
||||
cambalache.flatpak: repo
|
||||
|
107
README.md
107
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
@ -27,27 +27,40 @@ Source code lives on GNOME gitlab [here](https://gitlab.gnome.org/jpu/cambalache
|
||||
## Dependencies
|
||||
|
||||
* Python 3 - Cambalache is written in Python
|
||||
* [Meson](http://mesonbuild.org) build system
|
||||
* [GTK](http://www.gtk.org) 3 and 4 with broadway backend enabled
|
||||
* [Meson](http://mesonbuild.com) build system
|
||||
* [GTK](http://www.gtk.org) 3 and 4
|
||||
* python-gi - Python GTK 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
|
||||
resources and create extra files needed to run.
|
||||
Flathub is the place to get and distribute apps for all of desktop Linux.
|
||||
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
|
||||
|
||||
The preferred way to run Cambalache is using flatpak.
|
||||
Instructions on how to install flatpak can be found [here](https://flatpak.org/setup/).
|
||||
Use the following commands to install build dependencies:
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
flatpak-builder --force-clean --repo=repo build ar.xjuan.Cambalache.json
|
||||
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
|
||||
|
||||
## Flathub
|
||||
|
||||
You can get Cambalache prebuilt bundles [here](https://flathub.org/apps/details/ar.xjuan.Cambalache)
|
||||
|
||||
Use the following to install:
|
||||
Run as:
|
||||
```
|
||||
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak install --user flathub ar.xjuan.Cambalache
|
||||
flatpak run --user ar.xjuan.Cambalache//master
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
```
|
||||
# Create build directory and configure project
|
||||
mkdir _build && cd _build
|
||||
meson --prefix=~/.local
|
||||
# Configure project in _build directory
|
||||
meson setup --wipe --prefix=~/.local _build .
|
||||
|
||||
# Build and install
|
||||
ninja
|
||||
ninja install
|
||||
# Build and install in ~/.local
|
||||
ninja -C _build 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 LD_LIBRARY_PATH=~/.local/lib/x86_64-linux-gnu/
|
||||
export GI_TYPELIB_PATH=~/.local/lib/x86_64-linux-gnu/girepository-1.0/
|
||||
cambalache
|
||||
```
|
||||
|
||||
## Docker
|
||||
@ -102,20 +110,52 @@ Build the image with:
|
||||
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:
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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% payment processing fee
|
||||
|
||||
## Tools
|
||||
## cmb-catalog-gen
|
||||
|
||||
- cambalache-db:
|
||||
Generate Data Model from Gir files
|
||||
This tool is used to generate Cambalache catalogs from Gir files.
|
||||
|
||||
- db-codegen:
|
||||
Generate GObject classes from DB tables
|
||||
A catalog is a XML file with all the necessary data for Cambalache to produce
|
||||
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
|
||||
|
||||
Many thanks to all the people that suppport the project
|
||||
Many thanks to all the people that support the project
|
||||
|
||||
- Stephan McCormick
|
||||
- Willo Vincent
|
||||
- Javier Jardón
|
||||
- Franz Gratzer
|
||||
- David
|
||||
- Sonny Piers
|
||||
- Patrick Griffis
|
||||
- David
|
||||
- Luis Barron
|
||||
- Michel Fodje
|
||||
- Platon workaccount
|
||||
- Aemilia Scott
|
||||
- Jonathan K.
|
||||
- Luis Barron
|
||||
- Mitch 4J
|
||||
- JustRyan
|
||||
- Platon workaccount
|
||||
- ~1826340
|
||||
- Mula Gabriel
|
||||
- Felipe Borges
|
||||
- Johannes Deutsch
|
||||
- Jonathan K.
|
||||
- Patrick
|
||||
- 2 kojix
|
||||
- Coleman
|
||||
- Shogo Takata
|
||||
- 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",
|
||||
"runtime" : "org.gnome.Platform",
|
||||
"runtime-version" : "45",
|
||||
"runtime-version" : "48",
|
||||
"sdk" : "org.gnome.Sdk",
|
||||
"separate-locales": false,
|
||||
"separate-locales" : false,
|
||||
"command" : "cambalache",
|
||||
"finish-args" : [
|
||||
"--share=ipc",
|
||||
@ -26,28 +26,65 @@
|
||||
],
|
||||
"modules" : [
|
||||
{
|
||||
"name": "python3-lxml",
|
||||
"buildsystem": "simple",
|
||||
"build-commands": [
|
||||
"name" : "python3-lxml",
|
||||
"buildsystem" : "simple",
|
||||
"build-commands" : [
|
||||
"pip3 install --exists-action=i --ignore-installed --no-index --find-links=\"file://${PWD}\" --prefix=${FLATPAK_DEST} \"lxml\" --no-build-isolation"
|
||||
],
|
||||
"sources": [
|
||||
"sources" : [
|
||||
{
|
||||
"type": "file",
|
||||
"url": "https://files.pythonhosted.org/packages/30/39/7305428d1c4f28282a4f5bdbef24e0f905d351f34cf351ceb131f5cddf78/lxml-4.9.3.tar.gz",
|
||||
"sha256": "48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c"
|
||||
"type" : "file",
|
||||
"url" : "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz",
|
||||
"sha256" : "773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "gtksourceview4",
|
||||
"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",
|
||||
"sources": [
|
||||
"config-opts" : [],
|
||||
"sources" : [
|
||||
{
|
||||
"type": "archive",
|
||||
"url": "https://download.gnome.org/sources/gtksourceview/4.8/gtksourceview-4.8.4.tar.xz",
|
||||
"sha256": "7ec9d18fb283d1f84a3a3eff3b7a72b09a10c9c006597b3fbabbb5958420a87d"
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -59,9 +96,15 @@
|
||||
{
|
||||
"type" : "git",
|
||||
"path" : ".",
|
||||
"branch": "HEAD"
|
||||
"branch" : "HEAD"
|
||||
}
|
||||
],
|
||||
"config-opts" : [
|
||||
"--libdir=lib"
|
||||
]
|
||||
}
|
||||
]
|
||||
],
|
||||
"build-options" : {
|
||||
"env" : { }
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Cambalache
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -19,6 +19,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
import gi
|
||||
@ -28,10 +30,12 @@ import builtins
|
||||
|
||||
from . import config
|
||||
|
||||
gi.require_version("Gdk", "3.0")
|
||||
gi.require_version("Gtk", "3.0")
|
||||
gi.require_version("GtkSource", "4")
|
||||
gi.require_version("WebKit2", "4.1")
|
||||
gi.require_version("GIRepository", "3.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("GtkSource", "5")
|
||||
gi.require_version("WebKit", "6.0")
|
||||
gi.require_version('Adw', '1')
|
||||
|
||||
# Ensure _() builtin
|
||||
if "_" not in builtins.__dict__:
|
||||
@ -46,16 +50,17 @@ if "N_" not in builtins.__dict__:
|
||||
# noqa: E402,E401
|
||||
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._register()
|
||||
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_resource("/ar/xjuan/Cambalache/cambalache.css")
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
Gtk.IconTheme.get_default().add_resource_path("/ar/xjuan/Cambalache/icons")
|
||||
display = Gdk.Display.get_default()
|
||||
Gtk.StyleContext.add_provider_for_display(display, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION - 1)
|
||||
|
||||
# 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):
|
||||
@ -65,15 +70,17 @@ def getLogger(name):
|
||||
ch.setFormatter(formatter)
|
||||
|
||||
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)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
from .cmb_objects_base import CmbBaseObject
|
||||
from .cmb_css import CmbCSS
|
||||
from .cmb_ui import CmbUI
|
||||
from .cmb_object import CmbObject
|
||||
from .cmb_gresource import CmbGResource
|
||||
|
||||
# from .cmb_object_data import CmbObjectData
|
||||
from .cmb_property import CmbProperty
|
||||
@ -82,14 +89,19 @@ from .cmb_layout_property import CmbLayoutProperty
|
||||
from .cmb_type_info import CmbTypeInfo
|
||||
from .cmb_project import CmbProject
|
||||
|
||||
from .cmb_db_inspector import CmbDBInspector
|
||||
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_signal_editor import CmbSignalEditor
|
||||
from .cmb_ui_editor import CmbUIEditor
|
||||
from .cmb_ui_requires_editor import CmbUIRequiresEditor
|
||||
from .cmb_css_editor import CmbCSSEditor
|
||||
from .cmb_gresource_editor import CmbGResourceEditor
|
||||
from .cmb_fragment_editor import CmbFragmentEditor
|
||||
from .cmb_accessible_editor import CmbAccessibleEditor
|
||||
from .cmb_type_chooser import CmbTypeChooser
|
||||
from .cmb_type_chooser_widget import CmbTypeChooserWidget
|
||||
from .cmb_type_chooser_popover import CmbTypeChooserPopover
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Cambalache Application
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -19,6 +19,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
@ -29,3 +31,4 @@ resource = Gio.Resource.load(os.path.join(config.pkgdatadir, "app.gresource"))
|
||||
resource._register()
|
||||
|
||||
from .cmb_application import CmbApplication
|
||||
from .cmb_scrolled_window import CmbScrolledWindow
|
||||
|
@ -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>
|
||||
<gresource prefix="/ar/xjuan/Cambalache/app">
|
||||
<file>metainfo.xml</file>
|
||||
<file>SUPPORTERS.md</file>
|
||||
<file>cmb_window.ui</file>
|
||||
<file>cmb_shortcuts.ui</file>
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* cambalache.css
|
||||
*
|
||||
* Copyright (C) 2021 Juan Pablo Ugarte
|
||||
* Copyright (C) 2021-2024 Juan Pablo Ugarte
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -21,42 +21,55 @@
|
||||
*
|
||||
*/
|
||||
|
||||
@binding-set WindowBindings {
|
||||
bind "<Control>s" { "cmb-action" ("save") };
|
||||
bind "<Control>w" { "cmb-action" ("close") };
|
||||
bind "<Control>z" { "cmb-action" ("undo") };
|
||||
bind "<Control><shift>z" { "cmb-action" ("redo") };
|
||||
bind "Delete" { "cmb-action" ("delete") };
|
||||
bind "<Control>n" { "cmb-action" ("create_new") };
|
||||
bind "<Control>o" { "cmb-action" ("open") };
|
||||
bind "<Control>Insert" { "cmb-action-bool" ("add_placeholder", 0) };
|
||||
bind "<Control>Delete" { "cmb-action-bool" ("remove_placeholder", 0) };
|
||||
bind "<Control><shift>Insert" { "cmb-action-bool" ("add_placeholder", 1) };
|
||||
bind "<Control><shift>Delete" { "cmb-action-bool" ("remove_placeholder", 1) };
|
||||
|
||||
window.cmb-window .logo {
|
||||
background: url('resource:///ar/xjuan/Cambalache/app/images/logo-symbolic.svg') no-repeat 50% 35% / 40%;
|
||||
}
|
||||
|
||||
CmbWindow .logo {
|
||||
background: url('resource:///ar/xjuan/Cambalache/app/images/logo-symbolic.svg') no-repeat 50% 35% / 40%
|
||||
}
|
||||
|
||||
CmbWindow.dark .logo {
|
||||
window.cmb-window.dark .logo {
|
||||
color: white;
|
||||
background: -gtk-recolor(url('resource:///ar/xjuan/Cambalache/app/images/logo-symbolic.svg'), success #ffcb85, error #1a1a1a) no-repeat 50% 35% / 40%
|
||||
background: -gtk-recolor(url('resource:///ar/xjuan/Cambalache/app/images/logo-symbolic.svg'), success #ffcb85, error #1a1a1a) no-repeat 50% 35% / 40%;
|
||||
}
|
||||
|
||||
CmbWindow label.message {
|
||||
window.cmb-window label.message {
|
||||
padding: 1ex 1em;
|
||||
border-radius: 1ex 1ex 0 0;
|
||||
color: white;
|
||||
background-color: rgba(0, 0, 0, .6);
|
||||
}
|
||||
|
||||
CmbWindow.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;
|
||||
background-color: rgba(255, 255, 255, .6);
|
||||
}
|
||||
|
||||
popover.cmb-tutor {
|
||||
window.cmb-window stackswitcher.compact > button {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
window.cmb-window box.donate {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
popover.cmb-tutor > * {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
@ -65,13 +78,63 @@ popover.cmb-tutor label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
popover.cmb-tutor image {
|
||||
padding-right: 1em;
|
||||
-gtk-icon-size: 48px;
|
||||
}
|
||||
|
||||
button.cmb-tutor-highlight,
|
||||
modelbutton.cmb-tutor-highlight,
|
||||
buttonbox.cmb-tutor-highlight > button,
|
||||
menubutton.cmb-tutor-highlight > button,
|
||||
stackswitcher.cmb-tutor-highlight > button,
|
||||
stack.cmb-tutor-highlight,
|
||||
entry.cmb-tutor-highlight,
|
||||
treeview.cmb-tutor-highlight,
|
||||
box.cmb-tutor-highlight {
|
||||
box.cmb-tutor-highlight,
|
||||
CmbView.cmb-tutor-highlight {
|
||||
box-shadow: inset 0px 0px 6px @theme_selected_bg_color;
|
||||
transition: box-shadow .75s ease;
|
||||
}
|
||||
|
||||
CmbView.cmb-tutor-highlight {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
CmbTypeChooser {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
CmbUIEditor,
|
||||
CmbFragmentEditor,
|
||||
CmbObjectEditor,
|
||||
CmbAccessibleEditor {
|
||||
padding: 0 4px 4px 4px;
|
||||
}
|
||||
|
||||
CmbCSSEditor,
|
||||
CmbGResourceEditor,
|
||||
CmbGResourceFileEditor,
|
||||
stackswitcher.property-pane {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
textview {
|
||||
border: solid @borders 1px;
|
||||
}
|
||||
|
||||
button.borderless {
|
||||
border: unset;
|
||||
}
|
||||
|
||||
image.icon-size-32 {
|
||||
-gtk-icon-size: 32px;
|
||||
}
|
||||
|
||||
image.icon-size-64 {
|
||||
-gtk-icon-size: 64px;
|
||||
}
|
||||
|
||||
windowtitle.changed {
|
||||
font-style: italic;
|
||||
}
|
||||
|
@ -21,6 +21,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Cambalache Application
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,92 +20,69 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
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 cambalache import CmbProject, config, _
|
||||
from cambalache import CmbProject, utils, config, _
|
||||
|
||||
basedir = os.path.dirname(__file__) or "."
|
||||
|
||||
|
||||
class CmbApplication(Gtk.Application):
|
||||
class CmbApplication(Adw.Application):
|
||||
def __init__(self):
|
||||
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("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.connect("open-project", self.__on_open_project)
|
||||
window.connect("delete-event", self.__on_window_delete_event)
|
||||
window.connect("close-request", self.__on_window_close_request)
|
||||
window.connect("project-closed", self.__on_window_project_closed)
|
||||
self.add_window(window)
|
||||
return window
|
||||
|
||||
def open_project(self, path, target_tk=None, uiname=None):
|
||||
def open_project(self, path, target_tk=None):
|
||||
window = None
|
||||
|
||||
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
|
||||
|
||||
if window is None:
|
||||
window = self.__add_window()
|
||||
window = self.add_new_window()
|
||||
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()
|
||||
|
||||
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.present()
|
||||
|
||||
def do_open(self, files, nfiles, hint):
|
||||
for file in files:
|
||||
path = file.get_path()
|
||||
|
||||
content_type, uncertain = Gio.content_type_guess(path, None)
|
||||
if uncertain:
|
||||
with open(path, "rb") as fd:
|
||||
data = fd.read(1024)
|
||||
content_type, uncertain = Gio.content_type_guess(path, data)
|
||||
|
||||
if content_type == "application/x-cambalache-project":
|
||||
self.open_project(path)
|
||||
elif content_type in ["application/x-gtk-builder", "application/x-glade"]:
|
||||
self.import_file(path)
|
||||
|
||||
def do_startup(self):
|
||||
Gtk.Application.do_startup(self)
|
||||
|
||||
for action in ["quit"]:
|
||||
gaction = Gio.SimpleAction.new(action, None)
|
||||
gaction.connect("activate", getattr(self, f"_on_{action}_activate"))
|
||||
self.add_action(gaction)
|
||||
|
||||
provider = Gtk.CssProvider()
|
||||
provider.load_from_resource("/ar/xjuan/Cambalache/app/cambalache.css")
|
||||
Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.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, windows):
|
||||
def check_can_quit(self, window=None):
|
||||
windows = self.__get_windows() if window is None else [window]
|
||||
unsaved_windows = []
|
||||
projects2save = []
|
||||
windows2save = []
|
||||
|
||||
def do_quit():
|
||||
if window is None:
|
||||
self.quit()
|
||||
else:
|
||||
self.remove_window(window)
|
||||
window.destroy()
|
||||
|
||||
# Gather projects that needs saving
|
||||
for win in windows:
|
||||
@ -117,60 +94,65 @@ class CmbApplication(Gtk.Application):
|
||||
|
||||
unsaved_windows_len = len(unsaved_windows)
|
||||
if unsaved_windows_len == 0:
|
||||
return True
|
||||
do_quit()
|
||||
return
|
||||
|
||||
# Create Dialog
|
||||
text = _("Save changes before closing?")
|
||||
dialog = Gtk.MessageDialog(
|
||||
transient_for=windows[0],
|
||||
flags=0,
|
||||
message_type=Gtk.MessageType.QUESTION,
|
||||
text=f"<b><big>{text}</big></b>",
|
||||
use_markup=True,
|
||||
)
|
||||
window = windows[0]
|
||||
dialog = window._close_project_dialog_new()
|
||||
|
||||
# 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:
|
||||
if unsaved_windows_len > 1 or unsaved_windows[0].project.filename is None:
|
||||
# Add checkbox for each unsaved project
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
|
||||
box.add(Gtk.Label(label=_("Select which files:"), halign=Gtk.Align.START))
|
||||
box.append(Gtk.Label(label=_("Select which files:"), halign=Gtk.Align.START))
|
||||
|
||||
home = GLib.get_home_dir()
|
||||
untitled = 0
|
||||
|
||||
for win in unsaved_windows:
|
||||
path = win.project.filename.replace(GLib.get_home_dir(), "~")
|
||||
check = Gtk.CheckButton(label=path, active=True, margin_start=8, can_focus=False)
|
||||
projects2save.append((win.project, check))
|
||||
box.add(check)
|
||||
if win.project.filename is None:
|
||||
untitled += 1
|
||||
|
||||
box.show_all()
|
||||
dialog.props.message_area.add(box)
|
||||
# Find Unique name
|
||||
while os.path.exists(f"Untitled {untitled}.cmb"):
|
||||
untitled += 1
|
||||
|
||||
# Run Dialog
|
||||
response = dialog.run()
|
||||
dialog.destroy()
|
||||
check = Gtk.CheckButton(active=True, margin_start=8, can_focus=False)
|
||||
entry = Gtk.Entry(text=f"Untitled {untitled}")
|
||||
|
||||
# Handle response
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
if unsaved_windows_len > 1:
|
||||
for project, check in projects2save:
|
||||
hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=4)
|
||||
hbox.append(check)
|
||||
hbox.append(entry)
|
||||
|
||||
box.append(hbox)
|
||||
else:
|
||||
path = win.project.filename.replace(home, "~")
|
||||
check = Gtk.CheckButton(label=path, active=True, margin_start=8, can_focus=False)
|
||||
box.append(check)
|
||||
|
||||
windows2save.append((win, check, entry))
|
||||
|
||||
box.show()
|
||||
dialog.props.message_area.append(box)
|
||||
else:
|
||||
windows2save.append((unsaved_windows[0], None, None))
|
||||
|
||||
def callback(dialog, response, window):
|
||||
dialog.destroy()
|
||||
|
||||
if response == Gtk.ResponseType.ACCEPT:
|
||||
for win, check, entry in windows2save:
|
||||
if entry is not None:
|
||||
win.project.filename = entry.props.text
|
||||
if check is None or check.props.active:
|
||||
project.save()
|
||||
elif unsaved_windows_len:
|
||||
unsaved_windows[0].project.save()
|
||||
elif response == Gtk.ResponseType.CANCEL:
|
||||
return False
|
||||
win.save_project()
|
||||
elif response == Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
return True
|
||||
do_quit()
|
||||
|
||||
dialog.connect("response", callback, window)
|
||||
dialog.present()
|
||||
|
||||
def __get_windows(self):
|
||||
retval = []
|
||||
@ -181,8 +163,83 @@ class CmbApplication(Gtk.Application):
|
||||
|
||||
return retval
|
||||
|
||||
def __on_window_delete_event(self, window, event):
|
||||
return not self.__check_can_quit([window])
|
||||
def __on_window_close_request(self, window):
|
||||
self.check_can_quit(window)
|
||||
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):
|
||||
windows = self.__get_windows()
|
||||
@ -190,20 +247,13 @@ class CmbApplication(Gtk.Application):
|
||||
if len(windows) == 0:
|
||||
self.activate_action("quit")
|
||||
|
||||
def _on_quit_activate(self, action, data):
|
||||
if self.__check_can_quit(self.__get_windows()):
|
||||
self.quit()
|
||||
|
||||
def do_handle_local_options(self, options):
|
||||
if options.contains("version"):
|
||||
print(config.VERSION)
|
||||
return 0
|
||||
|
||||
if options.contains("export-all"):
|
||||
filename = options.lookup_value("export-all")
|
||||
filename = "".join([chr(c) for c in filename.unpack()])
|
||||
project = CmbProject(filename=filename)
|
||||
project.export()
|
||||
print("Export has been deprecated and does nothing. Every UI file is updated on project save.")
|
||||
return 0
|
||||
|
||||
return -1
|
||||
|
44
cambalache/app/cmb_scrolled_window.py
Normal file
44
cambalache/app/cmb_scrolled_window.py
Normal file
@ -0,0 +1,44 @@
|
||||
#
|
||||
# CmbScrolledWindow
|
||||
#
|
||||
# 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 Gtk
|
||||
|
||||
|
||||
class CmbScrolledWindow(Gtk.ScrolledWindow):
|
||||
__gtype_name__ = "CmbScrolledWindow"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# Do not let children get scroll events!
|
||||
sroll = Gtk.EventControllerScroll(
|
||||
flags=Gtk.EventControllerScrollFlags.VERTICAL, propagation_phase=Gtk.PropagationPhase.CAPTURE
|
||||
)
|
||||
sroll.connect("scroll", self.handle_scroll_capture)
|
||||
self.add_controller(sroll)
|
||||
|
||||
def handle_scroll_capture(self, ec, dx, dy):
|
||||
self.props.vadjustment.props.value += self.props.vadjustment.props.step_increment * dy
|
||||
return True
|
@ -1,51 +1,70 @@
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.9.0 -->
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<!-- interface-name cmb_shortcuts.ui -->
|
||||
<requires lib="gtk+" version="3.0"/>
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkShortcutsWindow" id="shortcuts">
|
||||
<property name="section-name">shortcuts</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsSection">
|
||||
<property name="section-name">shortcuts</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="title" translatable="yes">Project</property>
|
||||
<property name="view">shortcuts</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="title" translatable="yes">General</property>
|
||||
<property name="view">general</property>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="accelerator"><primary>question</property>
|
||||
<property name="title" translatable="yes">Keyboard Shortcuts</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="accelerator"><Control>s</property>
|
||||
<property name="title" translatable="yes">Save the project</property>
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="accelerator"><Control>e</property>
|
||||
<property name="title" translatable="yes">Save and Export</property>
|
||||
<property name="accelerator"><primary>q</property>
|
||||
<property name="title" translatable="yes">Quit application</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@ -54,33 +73,34 @@
|
||||
<object class="GtkShortcutsGroup">
|
||||
<property name="title" translatable="yes">Workspace</property>
|
||||
<property name="view">shortcuts</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<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="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Cambalache Tutor
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,12 +20,15 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
# Based on glade-intro.c (C) 2017-2018 Juan Pablo Ugarte
|
||||
#
|
||||
|
||||
from gi.repository import GObject, GLib, Gdk, Gtk
|
||||
from enum import Enum
|
||||
from collections import namedtuple
|
||||
from cambalache import utils
|
||||
|
||||
|
||||
class CmbTutorState(Enum):
|
||||
@ -51,7 +54,6 @@ class CmbTutor(GObject.GObject):
|
||||
}
|
||||
|
||||
window = GObject.Property(type=Gtk.Window, flags=GObject.ParamFlags.READWRITE)
|
||||
state = GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
|
||||
|
||||
def __init__(self, script, **kwargs):
|
||||
# List of ScriptNode
|
||||
@ -73,7 +75,7 @@ class CmbTutor(GObject.GObject):
|
||||
for node in script:
|
||||
self.__add(*node)
|
||||
|
||||
@GObject.Property()
|
||||
@GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
|
||||
def state(self):
|
||||
if self.timeout_id:
|
||||
return CmbTutorState.PLAYING
|
||||
@ -83,34 +85,30 @@ class CmbTutor(GObject.GObject):
|
||||
return CmbTutorState.NULL
|
||||
|
||||
def __add(self, text, widget_name, delay, name=None, position=CmbTutorPosition.BOTTOM):
|
||||
retval = {}
|
||||
def find_by_css_name_or_buildable_id(widget, name):
|
||||
retval = None
|
||||
css_name = widget.get_name()
|
||||
|
||||
def find_widget(w, data):
|
||||
if data.get("widget", None):
|
||||
return
|
||||
# Get css name first
|
||||
if css_name and css_name != GObject.type_name(widget) and css_name == name:
|
||||
return widget
|
||||
|
||||
name = None
|
||||
n = w.get_name()
|
||||
# then GtkBuildable name
|
||||
if isinstance(widget, Gtk.Buildable) and Gtk.Buildable.get_buildable_id(widget) == name:
|
||||
return widget
|
||||
|
||||
# Get css name first then GtkBuildable name
|
||||
if n and n != GObject.type_name(w):
|
||||
name = n
|
||||
elif isinstance(w, Gtk.Buildable):
|
||||
n = Gtk.Buildable.get_name(w)
|
||||
if n and not n.startswith("___object"):
|
||||
name = n
|
||||
# or ModelButton name
|
||||
if GObject.type_name(widget) == "GtkModelButton" and widget.props.text == name:
|
||||
return widget
|
||||
|
||||
# Return widget
|
||||
if name == widget_name:
|
||||
data["widget"] = w
|
||||
return
|
||||
for child in utils.widget_get_children(widget):
|
||||
retval = find_by_css_name_or_buildable_id(child, name)
|
||||
if retval:
|
||||
return retval
|
||||
|
||||
if isinstance(w, Gtk.Container):
|
||||
w.forall(find_widget, data)
|
||||
return retval
|
||||
|
||||
self.window.forall(find_widget, retval)
|
||||
|
||||
widget = retval.get("widget", None)
|
||||
widget = find_by_css_name_or_buildable_id(self.window, widget_name)
|
||||
|
||||
if widget:
|
||||
self.script.append(ScriptNode(widget, text, delay, name, position))
|
||||
@ -140,14 +138,13 @@ class CmbTutor(GObject.GObject):
|
||||
self.notify("state")
|
||||
|
||||
def __popover_new(self, text):
|
||||
popover = Gtk.Popover(modal=False)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
popover = Gtk.Popover(autohide=False)
|
||||
popover.add_css_class("cmb-tutor")
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True)
|
||||
|
||||
box.add(Gtk.Image(icon_name="dialog-information-symbolic", icon_size=Gtk.IconSize.DIALOG))
|
||||
box.add(Gtk.Label(label=text, wrap=True, max_width_chars=28))
|
||||
popover.add(box)
|
||||
popover.get_style_context().add_class("cmb-tutor")
|
||||
box.show_all()
|
||||
box.append(Gtk.Image(icon_name="dialog-information-symbolic"))
|
||||
box.append(Gtk.Label(label=text, vexpand=False, hexpand=True, wrap=True, max_width_chars=24))
|
||||
popover.set_child(box)
|
||||
|
||||
return popover
|
||||
|
||||
@ -203,11 +200,11 @@ class CmbTutor(GObject.GObject):
|
||||
if parent:
|
||||
parent.popup()
|
||||
|
||||
node.widget.get_style_context().add_class("cmb-tutor-highlight")
|
||||
node.widget.add_css_class("cmb-tutor-highlight")
|
||||
|
||||
# Create popover
|
||||
self.popover = self.__popover_new(node.text)
|
||||
self.popover.set_relative_to(node.widget)
|
||||
self.popover.set_parent(node.widget)
|
||||
|
||||
if node.position == CmbTutorPosition.BOTTOM:
|
||||
self.popover.set_position(Gtk.PositionType.BOTTOM)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbTutorial
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,25 +20,33 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from .cmb_tutor import CmbTutorPosition
|
||||
from cambalache import _
|
||||
|
||||
intro = [
|
||||
(_("Hi, I will show you around Cambalache"), "intro_button", 5),
|
||||
(_("You can open a project"), "open_button", 3),
|
||||
(
|
||||
_("find recently used"),
|
||||
"recent_button",
|
||||
2,
|
||||
),
|
||||
(_("or create a new one"), "new_button", 4),
|
||||
(_("You can open a project and find recently used"), "open_button", 5),
|
||||
(_("Common actions like Undo"), "undo_button", 4),
|
||||
(_("Redo"), "redo_button", 2),
|
||||
(_("Add new UI file"), "add_button", 3),
|
||||
(_("and Save are directly accessible in the headerbar"), "save_button", 6),
|
||||
(_("just like Save As"), "save_as_button", 2),
|
||||
(_("and the main menu"), "menu_button", 3),
|
||||
(_("and Add new UI are directly accessible in the headerbar"), "add_button", 3),
|
||||
(_("together with the main menu"), "menu_button", 3),
|
||||
(
|
||||
_("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"),
|
||||
(_("Great!"), "intro_button", 2),
|
||||
(
|
||||
@ -65,15 +73,15 @@ intro = [
|
||||
(_("and a button"), "intro_button", 3, "add-button"),
|
||||
(_("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",
|
||||
_("If you have any question, contact us on Matrix!"),
|
||||
_("Contact"),
|
||||
7,
|
||||
None,
|
||||
CmbTutorPosition.LEFT,
|
||||
),
|
||||
(
|
||||
_("That is all for now.\nIf you find Cambalache useful please consider donating"),
|
||||
"donate",
|
||||
_("Donate"),
|
||||
7,
|
||||
"donate",
|
||||
CmbTutorPosition.LEFT,
|
||||
|
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.set('VERSION', meson.project_version())
|
||||
conf.set('PYTHON', python_bin.path())
|
||||
conf.set('PYTHON', python_bin.full_path())
|
||||
conf.set('localedir', localedir)
|
||||
conf.set('pkgdatadir', pkgdatadir)
|
||||
|
||||
@ -24,6 +24,7 @@ configure_file(
|
||||
install_data([
|
||||
'__init__.py',
|
||||
'cmb_application.py',
|
||||
'cmb_scrolled_window.py',
|
||||
'cmb_window.py',
|
||||
'cmb_tutor.py',
|
||||
'cmb_tutorial.py',
|
||||
|
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>
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* cambalache.css
|
||||
*
|
||||
* Copyright (C) 2021 Juan Pablo Ugarte
|
||||
* Copyright (C) 2021-2024 Juan Pablo Ugarte
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
@ -25,14 +25,6 @@ CmbView {
|
||||
background-color: @theme_base_color;
|
||||
}
|
||||
|
||||
CmbObjectEditor viewport {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
CmbWindow stackswitcher.compact > button {
|
||||
min-width: unset;
|
||||
}
|
||||
|
||||
popover.cmb-icon-chooser iconview:not(:selected) {
|
||||
background-color: unset;
|
||||
}
|
||||
@ -48,6 +40,16 @@ CmbPropertyLabel.hidden:hover > box > image {
|
||||
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 {
|
||||
min-width:unset;
|
||||
min-height: unset;
|
||||
@ -59,6 +61,22 @@ CmbPropertyLabel {
|
||||
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 {
|
||||
font-style: italic;
|
||||
}
|
||||
@ -66,3 +84,48 @@ CmbPropertyLabel.modified > box > label {
|
||||
CmbPropertyLabel.warning > box > label {
|
||||
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>
|
||||
<gresource prefix="/ar/xjuan/Cambalache">
|
||||
<file>control/cmb_translatable_widget.ui</file>
|
||||
<file>db/cmb_base.sql</file>
|
||||
<file>db/cmb_project.sql</file>
|
||||
<file>db/cmb_history.sql</file>
|
||||
<file>cmb_view.ui</file>
|
||||
<file>cambalache.css</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_widget.ui</file>
|
||||
<file>cmb_ui_editor.ui</file>
|
||||
<file>cmb_css_editor.ui</file>
|
||||
<file>cmb_fragment_editor.ui</file>
|
||||
<file>cmb_object_data_editor.ui</file>
|
||||
<file>cmb_signal_editor.ui</file>
|
||||
<file>cambalache.css</file>
|
||||
<file>icons/scalable/actions/bind-symbolic.svg</file>
|
||||
<file>cmb_view.ui</file>
|
||||
<file>control/cmb_file_button.ui</file>
|
||||
<file>control/cmb_translatable_widget.ui</file>
|
||||
<file>db/cmb_base.sql</file>
|
||||
<file>db/cmb_history.sql</file>
|
||||
<file>db/cmb_project.sql</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>
|
||||
</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:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
|
||||
class CmbBase(GObject.GObject):
|
||||
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):
|
||||
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()
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbContextMenu - Cambalache UI Editor
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,10 +20,12 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GObject, GLib, Gdk, Gtk
|
||||
from gi.repository import GObject, GLib, Gio, Gdk, Gtk
|
||||
from cambalache import _
|
||||
|
||||
|
||||
@ -31,34 +33,76 @@ from cambalache import _
|
||||
class CmbContextMenu(Gtk.PopoverMenu):
|
||||
__gtype_name__ = "CmbContextMenu"
|
||||
|
||||
gtk_theme = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||
enable_theme = GObject.Property(
|
||||
type=bool,
|
||||
default=False,
|
||||
flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
|
||||
)
|
||||
target_tk = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
main_box = Gtk.Template.Child()
|
||||
separator = Gtk.Template.Child()
|
||||
css_theme = Gtk.Template.Child()
|
||||
css_theme_box = Gtk.Template.Child()
|
||||
main_section = Gtk.Template.Child()
|
||||
add_submenu = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.theme_submenu = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.connect("notify::target-tk", lambda o, p: self.__populate_css_theme_box())
|
||||
self.connect("notify::target-tk", self.__on_target_tk_notify)
|
||||
|
||||
def __on_css_theme_button_toggled(self, button, data):
|
||||
if button.props.active:
|
||||
self.gtk_theme = data
|
||||
def __on_target_tk_notify(self, obj, pspec):
|
||||
self.__populate_css_theme_box()
|
||||
self.__update_add_submenu()
|
||||
|
||||
def __update_add_submenu(self):
|
||||
if self.target_tk not in ["gtk-4.0", "gtk+-3.0"]:
|
||||
return
|
||||
|
||||
types = [
|
||||
"GtkBox",
|
||||
"GtkGrid",
|
||||
"GtkExpander",
|
||||
"GtkRevealer",
|
||||
"GtkOverlay",
|
||||
]
|
||||
|
||||
if self.target_tk == "gtk+-3.0":
|
||||
types += [
|
||||
"GtkAligment",
|
||||
"GtkEventBox"
|
||||
]
|
||||
else:
|
||||
types += [
|
||||
"GtkGraphicsOffload",
|
||||
]
|
||||
|
||||
self.add_submenu.remove_all()
|
||||
|
||||
for gtype in sorted(types):
|
||||
item = Gio.MenuItem()
|
||||
item.set_label(gtype)
|
||||
item.set_action_and_target_value("win.add_parent", GLib.Variant("s", gtype))
|
||||
self.add_submenu.append_item(item)
|
||||
|
||||
def __populate_css_theme_box(self):
|
||||
gtk_path = "gtk-3.0"
|
||||
|
||||
if self.target_tk in [None, ""]:
|
||||
if not self.enable_theme or self.target_tk not in ["gtk-4.0", "gtk+-3.0"]:
|
||||
return
|
||||
|
||||
if self.target_tk == "gtk-4.0":
|
||||
gtk_path = "gtk-4.0"
|
||||
# FIXME: whats the real default theme for gtk4?
|
||||
themes = ["Default"]
|
||||
else:
|
||||
themes = ["Adwaita", "HighContrast", "HighContrastInverse"]
|
||||
|
||||
for child in self.css_theme_box.get_children():
|
||||
self.css_theme_box.remove(child)
|
||||
if self.theme_submenu is None:
|
||||
self.theme_submenu = Gio.Menu()
|
||||
self.main_section.prepend_submenu(_("CSS theme"), self.theme_submenu)
|
||||
|
||||
# Remove all items from theme submenu
|
||||
self.theme_submenu.remove_all()
|
||||
|
||||
dirs = []
|
||||
|
||||
@ -71,9 +115,6 @@ class CmbContextMenu(Gtk.PopoverMenu):
|
||||
# Append ~/.themes
|
||||
dirs.append(os.path.join(GLib.get_home_dir(), ".themes"))
|
||||
|
||||
# Default themes
|
||||
themes = ["Adwaita", "HighContrast", "HighContrastInverse"]
|
||||
|
||||
for path in dirs:
|
||||
if not os.path.isdir(path):
|
||||
continue
|
||||
@ -86,23 +127,15 @@ class CmbContextMenu(Gtk.PopoverMenu):
|
||||
# Dedup and sort
|
||||
themes = list(dict.fromkeys(themes))
|
||||
|
||||
# Add back item
|
||||
button = Gtk.ModelButton(text=_("CSS themes"), menu_name="main", inverted=True, centered=True, visible=True)
|
||||
self.css_theme_box.add(button)
|
||||
|
||||
group = None
|
||||
for theme in sorted(themes):
|
||||
button = Gtk.RadioButton(label=theme, group=group, active=self.gtk_theme == theme, visible=True)
|
||||
if group is None:
|
||||
group = button
|
||||
|
||||
button.connect("toggled", self.__on_css_theme_button_toggled, theme)
|
||||
self.css_theme_box.add(button)
|
||||
|
||||
self.separator.props.visible = self.css_theme.props.visible = len(themes) > 0
|
||||
item = Gio.MenuItem()
|
||||
item.set_label(theme)
|
||||
item.set_action_and_target_value("win.workspace_theme", GLib.Variant("s", theme))
|
||||
self.theme_submenu.append_item(item)
|
||||
|
||||
def popup_at(self, x, y):
|
||||
r = Gdk.Rectangle()
|
||||
r.x, r.y, r.width, r.height = (x, y, 10, 10)
|
||||
r.x, r.y = (x, y)
|
||||
r.width = r.height = 0
|
||||
self.set_pointing_to(r)
|
||||
self.popup()
|
||||
|
@ -1,182 +1,57 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<!-- interface-name cmb_context_menu.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gio" version="2.0"/>
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<menu id="menu_model">
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.cut</attribute>
|
||||
<attribute name="label" translatable="yes">Cut</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.copy</attribute>
|
||||
<attribute name="label" translatable="yes">Copy</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.paste</attribute>
|
||||
<attribute name="label" translatable="yes">Paste</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.delete</attribute>
|
||||
<attribute name="label" translatable="yes">Delete</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section>
|
||||
<item>
|
||||
<attribute name="action">win.add_object</attribute>
|
||||
<attribute name="label" translatable="yes">Add object here</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.add_object_toplevel</attribute>
|
||||
<attribute name="label" translatable="yes">Add object as toplevel</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.remove_parent</attribute>
|
||||
<attribute name="label">Remove parent</attribute>
|
||||
</item>
|
||||
<submenu id="add_submenu">
|
||||
<attribute name="label">Add parent</attribute>
|
||||
</submenu>
|
||||
<item>
|
||||
<attribute name="action">win.clear</attribute>
|
||||
<attribute name="label" translatable="yes">Clear Properties</attribute>
|
||||
</item>
|
||||
<item>
|
||||
<attribute name="action">win.documentation</attribute>
|
||||
<attribute name="label" translatable="yes">Read Documentation</attribute>
|
||||
</item>
|
||||
</section>
|
||||
<section id="main_section"/>
|
||||
</menu>
|
||||
<template class="CmbContextMenu" parent="GtkPopoverMenu">
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="main_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.cut</property>
|
||||
<property name="text" translatable="yes">Cut</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.copy</property>
|
||||
<property name="text" translatable="yes">Copy</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.paste</property>
|
||||
<property name="text" translatable="yes">Paste</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.delete</property>
|
||||
<property name="text" translatable="yes">Delete</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.add_object</property>
|
||||
<property name="text" translatable="yes">Add object here</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.add_object_toplevel</property>
|
||||
<property name="text" translatable="yes">Add object as toplevel</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.clear</property>
|
||||
<property name="text" translatable="yes">Clear Properties</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">7</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="action-name">win.documentation</property>
|
||||
<property name="text" translatable="yes">Read Documentation</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSeparator" id="separator">
|
||||
<property name="can-focus">False</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">9</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkModelButton" id="css_theme">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="text" translatable="yes">CSS theme</property>
|
||||
<property name="menu-name">css-theme</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">10</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="submenu">main</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="css_theme_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">4</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="submenu">css-theme</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<property name="menu-model">menu_model</property>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -20,9 +20,14 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GObject, Gio
|
||||
|
||||
from .cmb_path import CmbPath
|
||||
from .cmb_objects_base import CmbBaseCSS
|
||||
from cambalache import _
|
||||
|
||||
@ -32,6 +37,7 @@ class CmbCSS(CmbBaseCSS):
|
||||
"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)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -51,8 +57,13 @@ class CmbCSS(CmbBaseCSS):
|
||||
if pspec.name == "filename":
|
||||
self.load_css()
|
||||
|
||||
def get_display_name(self):
|
||||
return self.filename if self.filename else _("Unnamed CSS {css_id}").format(css_id=self.css_id)
|
||||
@classmethod
|
||||
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)
|
||||
def priority(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbCSSEditor - Cambalache CSS Editor
|
||||
#
|
||||
# Copyright (C) 2022 Juan Pablo Ugarte
|
||||
# Copyright (C) 2022-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
|
||||
@ -20,8 +20,11 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
from cambalache import utils, _
|
||||
from .cmb_css import CmbCSS
|
||||
|
||||
|
||||
@ -33,6 +36,7 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
priority = Gtk.Template.Child()
|
||||
is_global = Gtk.Template.Child()
|
||||
|
||||
ui_menu_button = Gtk.Template.Child()
|
||||
ui_box = Gtk.Template.Child()
|
||||
infobar = Gtk.Template.Child()
|
||||
save_button = Gtk.Template.Child()
|
||||
@ -77,6 +81,7 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
self.set_sensitive(False)
|
||||
return
|
||||
|
||||
self.filename.dirname = obj.project.dirname
|
||||
self.set_sensitive(True)
|
||||
|
||||
for field, target in self.fields:
|
||||
@ -101,10 +106,7 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
obj.connect("file-changed", self.__on_file_changed)
|
||||
|
||||
self.__update_provider_for()
|
||||
|
||||
@Gtk.Template.Callback("on_remove_button_clicked")
|
||||
def __on_remove_button_clicked(self, button):
|
||||
self.emit("remove-css")
|
||||
self.__update_ui_button_label()
|
||||
|
||||
@Gtk.Template.Callback("on_save_button_clicked")
|
||||
def __on_save_button_clicked(self, button):
|
||||
@ -121,8 +123,7 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
|
||||
def __update_provider_for(self):
|
||||
# Remove all css_ui check buttons
|
||||
ui_box_children = self.ui_box.get_children()
|
||||
for child in ui_box_children:
|
||||
for child in utils.widget_get_children(self.ui_box):
|
||||
self.ui_box.remove(child)
|
||||
|
||||
if self._object is None:
|
||||
@ -134,10 +135,10 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
# Generate a check button for each UI
|
||||
for ui in ui_list:
|
||||
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)
|
||||
self.ui_box.add(check)
|
||||
self.ui_box.append(check)
|
||||
|
||||
def __on_file_changed(self, obj):
|
||||
self.infobar.set_revealed(True)
|
||||
@ -153,6 +154,27 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
else:
|
||||
self.object.remove_ui(ui)
|
||||
|
||||
self.__update_ui_button_label()
|
||||
|
||||
def __update_ui_button_label(self):
|
||||
n = 0
|
||||
first_one = None
|
||||
child = self.ui_box.get_first_child()
|
||||
|
||||
while child is not None:
|
||||
if child.props.active:
|
||||
n += 1
|
||||
|
||||
if first_one is None:
|
||||
first_one = child
|
||||
|
||||
child = child.get_next_sibling()
|
||||
|
||||
if first_one is None:
|
||||
self.ui_menu_button.props.label = _("None")
|
||||
else:
|
||||
self.ui_menu_button.props.label = f"{first_one.props.label} + {n - 1}" if n > 1 else first_one.props.label
|
||||
|
||||
def __on_ui_added_removed(self, project, ui):
|
||||
self.__update_provider_for()
|
||||
|
||||
@ -173,3 +195,6 @@ class CmbCSSEditor(Gtk.Grid):
|
||||
self.object.project.remove_css(self.object)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
Gtk.WidgetClass.set_css_name(CmbCSSEditor, "CmbCSSEditor")
|
||||
|
@ -1,307 +1,203 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="gladecambalache" version="0.0"/>
|
||||
<!-- n-columns=2 n-rows=6 -->
|
||||
<!-- interface-name cmb_css_editor.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="CmbCSSEditor" parent="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">4</property>
|
||||
<property name="column-spacing">3</property>
|
||||
<property name="row-spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<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>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Priority:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">This provider will be used in all UI.</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Global:</property>
|
||||
<property name="tooltip-text" translatable="yes">This provider will be used in all UI.</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CmbEntry" id="filename">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<object class="CmbFileButton" id="filename">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="placeholder-text" translatable="yes"><file name relative to project></property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="priority">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<object class="GtkMenuButton" id="ui_menu_button">
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="is_global">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="ui_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="tooltip-text" translatable="yes">List of UI where this provider will be used</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">start</property>
|
||||
<property name="label" translatable="yes">Provider for:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Remove CSS file from project</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
<signal name="clicked" handler="on_remove_button_clicked" swapped="no"/>
|
||||
<property name="popover">
|
||||
<object class="GtkPopover">
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">app-remove-symbolic</property>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="ui_box">
|
||||
<property name="orientation">vertical</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">3</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSpinButton" id="priority">
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">start</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSwitch" id="is_global">
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">start</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Provider for:</property>
|
||||
<property name="tooltip-text" translatable="yes">List of UI where this provider will be used</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="valign">center</property>
|
||||
<property name="label" translatable="yes"><small>Note: CSS files need to be loaded at runtime</small></property>
|
||||
<property name="use-markup">True</property>
|
||||
<property name="use-markup">1</property>
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="save_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="receives-default">1</property>
|
||||
<property name="sensitive">0</property>
|
||||
<property name="tooltip-text" translatable="yes">Save CSS file</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
<signal name="clicked" handler="on_save_button_clicked" swapped="no"/>
|
||||
<signal name="clicked" handler="on_save_button_clicked"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">document-save-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="column-span">2</property>
|
||||
<property name="row">5</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">5</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkFrame">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label-xalign">0</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkInfoBar" id="infobar">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="message-type">warning</property>
|
||||
<property name="show-close-button">True</property>
|
||||
<property name="revealed">False</property>
|
||||
<signal name="response" handler="on_infobar_response" swapped="no"/>
|
||||
<child internal-child="action_area">
|
||||
<object class="GtkButtonBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">6</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="reload_button">
|
||||
<property name="label" translatable="yes">Reload</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="valign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<property name="revealed">0</property>
|
||||
<property name="show-close-button">1</property>
|
||||
<signal name="response" handler="on_infobar_response"/>
|
||||
<child type="action">
|
||||
<object class="GtkButton" id="reload_button">
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Reload</property>
|
||||
<property name="receives-default">1</property>
|
||||
<property name="valign">end</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child internal-child="content_area">
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="spacing">16</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="label" translatable="yes">The file changed on disk.</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">False</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<action-widgets>
|
||||
<action-widget response="-5">reload_button</action-widget>
|
||||
</action-widgets>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<child>
|
||||
<object class="CmbSourceView" id="view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="lang">css</property>
|
||||
<property name="visible">True</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child type="label_item">
|
||||
<placeholder/>
|
||||
</child>
|
||||
</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="vexpand-set">True</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="column-span">2</property>
|
||||
<property name="row">4</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">4</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
1721
cambalache/cmb_db.py
1721
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:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
|
||||
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":
|
||||
c.execute(
|
||||
"""
|
||||
UPDATE object SET position=new.position - 1
|
||||
UPDATE temp.object SET position=new.position - 1
|
||||
FROM (
|
||||
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
|
||||
) 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(
|
||||
"""
|
||||
UPDATE object SET position=new.position - 1
|
||||
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 object
|
||||
FROM temp.object
|
||||
WHERE parent_id IS NULL
|
||||
) 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
|
||||
c.execute(
|
||||
"""
|
||||
DELETE FROM object_property AS op
|
||||
DELETE FROM temp.object_property AS op
|
||||
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;
|
||||
"""
|
||||
)
|
||||
@ -102,3 +104,60 @@ def ensure_columns_for_0_13_1(table, data):
|
||||
return [row + (None, None, None) for row in data]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def ensure_columns_for_0_17_3(table, data):
|
||||
if table == "object":
|
||||
# Append custom_child_fragment column
|
||||
return [row + (None,) for row in data]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def migrate_table_data_to_0_17_3(c, table, data):
|
||||
if table in ["object_property", "object_layout_property", "object_data"]:
|
||||
c.executescript(
|
||||
f"""
|
||||
UPDATE temp.{table} SET translatable=1
|
||||
WHERE translatable IS NOT NULL AND lower(translatable) IN (1, 'y', 'yes', 't', 'true');
|
||||
UPDATE temp.{table} SET translatable=NULL
|
||||
WHERE translatable IS NOT NULL AND translatable != 1;
|
||||
"""
|
||||
)
|
||||
|
||||
if table == "object_signal":
|
||||
for prop in ["swap", "after"]:
|
||||
c.executescript(
|
||||
f"""
|
||||
UPDATE temp.object_signal SET {prop}=1
|
||||
WHERE {prop} IS NOT NULL AND lower({prop}) IN (1, 'y', 'yes', 't', 'true');
|
||||
UPDATE temp.object_signal SET {prop}=NULL WHERE {prop} IS NOT NULL AND after != 1;
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
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
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbFragmentEditor - Cambalache CSS Editor
|
||||
#
|
||||
# Copyright (C) 2022 Juan Pablo Ugarte
|
||||
# Copyright (C) 2022-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
|
||||
@ -20,8 +20,11 @@
|
||||
# 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
|
||||
|
||||
|
||||
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_fragment_editor.ui")
|
||||
@ -29,10 +32,12 @@ class CmbFragmentEditor(Gtk.Box):
|
||||
__gtype_name__ = "CmbFragmentEditor"
|
||||
|
||||
view = Gtk.Template.Child()
|
||||
child_view = Gtk.Template.Child()
|
||||
switcher = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._object = None
|
||||
self.__binding = None
|
||||
self.__bindings = []
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@ -45,9 +50,10 @@ class CmbFragmentEditor(Gtk.Box):
|
||||
if obj == self._object:
|
||||
return
|
||||
|
||||
if self.__binding:
|
||||
self.__binding.unbind()
|
||||
self.__binding = None
|
||||
for binding in self.__bindings:
|
||||
binding.unbind()
|
||||
|
||||
self.__bindings = []
|
||||
|
||||
self._object = obj
|
||||
|
||||
@ -61,4 +67,20 @@ class CmbFragmentEditor(Gtk.Box):
|
||||
"text",
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
)
|
||||
self.__binding = binding
|
||||
self.__bindings.append(binding)
|
||||
|
||||
# Only objects have child fragments
|
||||
if type(obj) is CmbObject and obj.parent:
|
||||
binding = GObject.Object.bind_property(
|
||||
obj,
|
||||
"custom-child-fragment",
|
||||
self.child_view,
|
||||
"text",
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
)
|
||||
self.__bindings.append(binding)
|
||||
|
||||
self.switcher.set_visible(True)
|
||||
|
||||
|
||||
Gtk.WidgetClass.set_css_name(CmbFragmentEditor, "CmbFragmentEditor")
|
||||
|
@ -1,44 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="gladecambalache" version="0.0"/>
|
||||
<!-- interface-name cmb_fragment_editor.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="CmbFragmentEditor" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Extra fragments:</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<object class="GtkStack" id="fragment_stack">
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="CmbSourceView" id="view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="lang">xml</property>
|
||||
<object class="GtkStackPage">
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<child>
|
||||
<object class="CmbSourceView" id="view">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="lang">xml</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<property name="name">fragment</property>
|
||||
<property name="title" translatable="yes">Fragment</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackPage">
|
||||
<property name="child">
|
||||
<object class="GtkScrolledWindow">
|
||||
<child>
|
||||
<object class="CmbSourceView" id="child_view">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="lang">xml</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<property name="name">child_fragment</property>
|
||||
<property name="title" translatable="yes">Child Fragment</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackSwitcher" id="switcher">
|
||||
<property name="halign">center</property>
|
||||
<property name="stack">fragment_stack</property>
|
||||
<property name="visible">False</property>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
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:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -32,18 +35,22 @@ class CmbLayoutProperty(CmbBaseLayoutProperty):
|
||||
info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__on_init = True
|
||||
super().__init__(**kwargs)
|
||||
self.__on_init = False
|
||||
self.version_warning = None
|
||||
|
||||
owner_info = self.project.type_info.get(self.info.owner_id, None)
|
||||
self.library_id = owner_info.library_id
|
||||
self._update_version_warning()
|
||||
|
||||
self.connect("notify", self.__on_notify)
|
||||
|
||||
def __str__(self):
|
||||
return f"CmbLayoutProperty<{self.object.type_id} {self.info.owner_id}:{self.property_id}>"
|
||||
|
||||
def __on_notify(self, obj, pspec):
|
||||
obj = self.object
|
||||
self.project._object_layout_property_changed(obj.parent, obj, self)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def value(self):
|
||||
c = self.project.db.execute(
|
||||
@ -99,13 +106,6 @@ class CmbLayoutProperty(CmbBaseLayoutProperty):
|
||||
(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()
|
||||
|
||||
def _update_version_warning(self):
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Cambalache Library Info wrapper
|
||||
#
|
||||
# Copyright (C) 2022 Juan Pablo Ugarte
|
||||
# Copyright (C) 2022-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
|
||||
@ -20,11 +20,16 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject
|
||||
from .cmb_objects_base import CmbBaseLibraryInfo
|
||||
|
||||
|
||||
class CmbLibraryInfo(CmbBaseLibraryInfo):
|
||||
third_party = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@ -46,8 +51,7 @@ class CmbLibraryInfo(CmbBaseLibraryInfo):
|
||||
|
||||
def __init_min_version(self):
|
||||
row = self.project.db.execute(
|
||||
"SELECT MIN_VERSION(version) FROM library_version WHERE library_id=?;",
|
||||
(self.library_id, )
|
||||
"SELECT MIN_VERSION(version) FROM library_version WHERE library_id=?;", (self.library_id,)
|
||||
).fetchone()
|
||||
|
||||
return row[0] if row is not None else None
|
||||
|
@ -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
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
@ -20,27 +20,23 @@
|
||||
# Authors:
|
||||
# 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):
|
||||
__gtype_name__ = "CmbListStore"
|
||||
|
||||
table = GObject.Property(type=str)
|
||||
query = GObject.Property(type=str)
|
||||
project = GObject.Property(type=GObject.GObject)
|
||||
|
||||
# This class is used by GListModel implementations when they do not know which item to return
|
||||
class CmbListError(CmbBase):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
data = self.project._get_table_data(self.table)
|
||||
self.set_column_types(data["types"])
|
||||
self.__populate()
|
||||
@GObject.Property(type=str)
|
||||
def display_name(self):
|
||||
return "list error"
|
||||
|
||||
def __populate(self):
|
||||
c = self.project.db.cursor()
|
||||
for row in c.execute(self.query):
|
||||
self.append(row)
|
||||
@GObject.Property(type=int)
|
||||
def n_items(self):
|
||||
return 0
|
||||
|
||||
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:
|
||||
# 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_property import CmbProperty
|
||||
from .cmb_layout_property import CmbLayoutProperty
|
||||
from .cmb_object_data import CmbObjectData
|
||||
from .cmb_type_info import CmbTypeInfo
|
||||
from .cmb_ui import CmbUI
|
||||
from .constants import GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE
|
||||
from . import utils
|
||||
from cambalache import getLogger, _
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class CmbObject(CmbBaseObject):
|
||||
class CmbObject(CmbBaseObject, Gio.ListModel):
|
||||
info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
|
||||
|
||||
__gsignals__ = {
|
||||
@ -43,21 +47,26 @@ class CmbObject(CmbBaseObject):
|
||||
"layout-property-changed": (GObject.SignalFlags.RUN_FIRST, None, (GObject.GObject, CmbLayoutProperty)),
|
||||
"signal-added": (GObject.SignalFlags.RUN_FIRST, None, (CmbSignal,)),
|
||||
"signal-removed": (GObject.SignalFlags.RUN_FIRST, None, (CmbSignal,)),
|
||||
"signal-changed": (GObject.SignalFlags.RUN_FIRST, None, (CmbSignal,)),
|
||||
"data-added": (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):
|
||||
self.properties = []
|
||||
self.properties_dict = {}
|
||||
self.layout = []
|
||||
self.layout_dict = {}
|
||||
self.signals = []
|
||||
self.data = []
|
||||
self.data_dict = {}
|
||||
self.position_layout_property = None
|
||||
self.__properties = None
|
||||
self.__properties_dict = None
|
||||
self.__layout = None
|
||||
self.__layout_dict = None
|
||||
self.__signals = None
|
||||
self.__signals_dict = None
|
||||
self.__data = None
|
||||
self.__data_dict = None
|
||||
self.inline_property_id = None
|
||||
self.version_warning = None
|
||||
self.__is_template = False
|
||||
|
||||
self._last_known = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@ -66,26 +75,79 @@ class CmbObject(CmbBaseObject):
|
||||
if self.project is None:
|
||||
return
|
||||
|
||||
# Append object to project automatically
|
||||
self.project._append_object(self)
|
||||
|
||||
self.__populate_properties()
|
||||
self.__populate_layout_properties()
|
||||
self.__populate_signals()
|
||||
self.__populate_data()
|
||||
self.__update_inline_property_id()
|
||||
self.__update_version_warning()
|
||||
self.ui.connect("notify", self._on_ui_notify)
|
||||
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):
|
||||
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):
|
||||
property_info = self.project.get_type_properties(name)
|
||||
if property_info is None:
|
||||
return
|
||||
|
||||
for property_name in property_info:
|
||||
info = property_info[property_name]
|
||||
for property_name, info in property_info.items():
|
||||
# Check if this property was already installed by a derived class
|
||||
if property_name in self.__properties_dict:
|
||||
continue
|
||||
|
||||
prop = CmbProperty(
|
||||
object=self,
|
||||
@ -98,16 +160,31 @@ class CmbObject(CmbBaseObject):
|
||||
)
|
||||
|
||||
# List of property
|
||||
self.properties.append(prop)
|
||||
self.__properties.append(prop)
|
||||
|
||||
# Dictionary of properties
|
||||
self.properties_dict[property_name] = prop
|
||||
self.__properties_dict[property_name] = prop
|
||||
|
||||
def __populate_properties(self):
|
||||
if self.__properties is not None:
|
||||
return
|
||||
self.__properties = []
|
||||
self.__properties_dict = {}
|
||||
|
||||
self.__populate_type_properties(self.type_id)
|
||||
for parent_id in self.info.hierarchy:
|
||||
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):
|
||||
property_info = self.project.get_type_properties(name)
|
||||
if property_info is None:
|
||||
@ -129,14 +206,10 @@ class CmbObject(CmbBaseObject):
|
||||
info=info,
|
||||
)
|
||||
|
||||
# Keep a reference to the position layout property
|
||||
if info.is_position:
|
||||
self.position_layout_property = prop
|
||||
|
||||
self.layout.append(prop)
|
||||
self.__layout.append(prop)
|
||||
|
||||
# Dictionary of properties
|
||||
self.layout_dict[property_name] = prop
|
||||
self.__layout_dict[property_name] = prop
|
||||
|
||||
def _property_changed(self, prop):
|
||||
self.emit("property-changed", prop)
|
||||
@ -148,23 +221,39 @@ class CmbObject(CmbBaseObject):
|
||||
self.project._object_layout_property_changed(parent, self, prop)
|
||||
|
||||
def __add_signal_object(self, signal):
|
||||
self.signals.append(signal)
|
||||
self.__populate_signals()
|
||||
self.__signals.append(signal)
|
||||
self.__signals_dict[signal.signal_pk] = signal
|
||||
self.emit("signal-added", signal)
|
||||
self.project._object_signal_added(self, signal)
|
||||
|
||||
signal.connect("notify", self.__on_signal_notify)
|
||||
|
||||
def __on_signal_notify(self, signal, pspec):
|
||||
self.emit("signal-changed", signal)
|
||||
self.project._object_signal_changed(self, signal)
|
||||
|
||||
def __add_data_object(self, data):
|
||||
if data in self.data:
|
||||
if data.get_id_string() in self.data_dict:
|
||||
return
|
||||
|
||||
self.data.append(data)
|
||||
self.data_dict[data.get_id_string()] = data
|
||||
self.__data.append(data)
|
||||
self.__data_dict[data.get_id_string()] = data
|
||||
self.emit("data-added", data)
|
||||
self.project._object_data_added(self, data)
|
||||
|
||||
def __on_notify(self, obj, pspec):
|
||||
if pspec.name == "parent-id":
|
||||
self.__populate_layout_properties()
|
||||
|
||||
self.project._object_changed(self, pspec.name)
|
||||
|
||||
def __populate_signals(self):
|
||||
if self.__signals is not None:
|
||||
return
|
||||
self.__signals = []
|
||||
self.__signals_dict = {}
|
||||
|
||||
c = self.project.db.cursor()
|
||||
|
||||
# Populate signals
|
||||
@ -172,6 +261,11 @@ class CmbObject(CmbBaseObject):
|
||||
self.__add_signal_object(CmbSignal.from_row(self.project, *row))
|
||||
|
||||
def __populate_data(self):
|
||||
if self.__data is not None:
|
||||
return
|
||||
self.__data = []
|
||||
self.__data_dict = {}
|
||||
|
||||
c = self.project.db.cursor()
|
||||
|
||||
# Populate data
|
||||
@ -184,11 +278,18 @@ class CmbObject(CmbBaseObject):
|
||||
def __populate_layout_properties(self):
|
||||
parent_id = self.parent_id
|
||||
|
||||
# FIXME: delete is anything is set?
|
||||
self.__layout = []
|
||||
self.__layout_dict = {}
|
||||
|
||||
if parent_id > 0:
|
||||
parent = self.project.get_object_by_id(self.ui_id, parent_id)
|
||||
self.__populate_layout_properties_from_type(f"{parent.type_id}LayoutChild")
|
||||
else:
|
||||
self.layout = []
|
||||
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)
|
||||
def parent_id(self):
|
||||
@ -203,15 +304,42 @@ class CmbObject(CmbBaseObject):
|
||||
|
||||
@parent_id.setter
|
||||
def _set_parent_id(self, value):
|
||||
self.db_set(
|
||||
"UPDATE object SET parent_id=? WHERE (ui_id, object_id) IS (?, ?);",
|
||||
(
|
||||
self.ui_id,
|
||||
self.object_id,
|
||||
),
|
||||
value if value != 0 else None,
|
||||
new_parent_id = value if value != 0 else None
|
||||
old_parent_id = self.parent_id if self.parent_id != 0 else None
|
||||
|
||||
if old_parent_id == new_parent_id:
|
||||
return
|
||||
|
||||
# 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()
|
||||
|
||||
@GObject.Property(type=CmbUI)
|
||||
@ -272,7 +400,9 @@ class CmbObject(CmbBaseObject):
|
||||
)
|
||||
|
||||
def _remove_signal(self, signal):
|
||||
self.signals.remove(signal)
|
||||
self.__signals.remove(signal)
|
||||
del self.__signals_dict[signal.signal_pk]
|
||||
|
||||
self.emit("signal-removed", signal)
|
||||
self.project._object_signal_removed(self, signal)
|
||||
|
||||
@ -316,23 +446,29 @@ class CmbObject(CmbBaseObject):
|
||||
return self._add_data(owner_id, data_id, id, info=taginfo)
|
||||
|
||||
def _remove_data(self, data):
|
||||
if data not in self.data:
|
||||
if data.get_id_string() not in self.data_dict:
|
||||
return
|
||||
|
||||
self.data.remove(data)
|
||||
del self.data_dict[data.get_id_string()]
|
||||
self.__data.remove(data)
|
||||
del self.__data_dict[data.get_id_string()]
|
||||
|
||||
self.emit("data-removed", data)
|
||||
self.project._object_data_removed(self, data)
|
||||
|
||||
def remove_data(self, data):
|
||||
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(
|
||||
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;",
|
||||
(self.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
|
||||
)
|
||||
self.project.db.commit()
|
||||
self.project.history_pop()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self} Error removing data {data}: {e}")
|
||||
return False
|
||||
@ -349,45 +485,76 @@ class CmbObject(CmbBaseObject):
|
||||
logger.warning(f"{child} is not children of {self}")
|
||||
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
|
||||
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
|
||||
c = self.project.db.cursor()
|
||||
for row in c.execute(
|
||||
"""
|
||||
SELECT object_id, position
|
||||
FROM object
|
||||
WHERE ui_id=? AND parent_id=? AND internal IS NULL AND object_id!=? AND object_id NOT IN
|
||||
(SELECT inline_object_id FROM object_property WHERE inline_object_id IS NOT NULL AND ui_id=? AND object_id=?)
|
||||
ORDER BY position;
|
||||
""",
|
||||
(self.ui_id, self.object_id, child.object_id, self.ui_id, self.object_id),
|
||||
):
|
||||
child_id, child_position = row
|
||||
# Consider this children
|
||||
#
|
||||
# label 0
|
||||
# button 1
|
||||
# entry 2
|
||||
# switch 3
|
||||
# toggle 4
|
||||
|
||||
obj = self.project.get_object_by_id(self.ui_id, child_id)
|
||||
if obj:
|
||||
children.append(obj)
|
||||
# Disable check so we can set position temporally to -1
|
||||
db.ignore_check_constraints = True
|
||||
db.execute("UPDATE object SET position=-1 WHERE ui_id=? AND object_id=?;", (self.ui_id, child.object_id))
|
||||
|
||||
# Insert child in new position
|
||||
children.insert(position, child)
|
||||
# Make room for new position
|
||||
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
|
||||
for pos, obj in enumerate(children):
|
||||
# Sync layout property
|
||||
if obj.position_layout_property:
|
||||
obj.position_layout_property.value = pos
|
||||
else:
|
||||
# Or object position
|
||||
obj.position = pos
|
||||
# Set new position
|
||||
db.execute("UPDATE object SET position=? WHERE ui_id=? AND object_id=?;", (position, self.ui_id, child.object_id))
|
||||
|
||||
db.ignore_check_constraints = False
|
||||
|
||||
list_position = child.list_position
|
||||
|
||||
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.emit("child-reordered", child, old_position, position)
|
||||
self.project._object_child_reordered(self, child, old_position, position)
|
||||
|
||||
def clear_properties(self):
|
||||
c = self.project.db.cursor()
|
||||
@ -409,22 +576,171 @@ class CmbObject(CmbBaseObject):
|
||||
c.close()
|
||||
|
||||
for prop_id in properties:
|
||||
prop = self.properties_dict[prop_id]
|
||||
prop = self.__properties_dict[prop_id]
|
||||
prop.notify("value")
|
||||
self._property_changed(prop)
|
||||
|
||||
def get_display_name(self):
|
||||
return self.name if self.name is not None else self.type_id
|
||||
@GObject.Property(type=str)
|
||||
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):
|
||||
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)
|
||||
|
||||
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):
|
||||
self.__update_version_warning()
|
||||
|
||||
self.__populate_properties()
|
||||
self.__populate_layout()
|
||||
|
||||
# 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:
|
||||
if prop.library_id == library_id:
|
||||
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:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject
|
||||
|
||||
from .cmb_objects_base import CmbBaseObjectData
|
||||
from .cmb_type_info import CmbTypeDataInfo
|
||||
from cambalache import getLogger
|
||||
from cambalache import getLogger, _
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@ -59,9 +61,6 @@ class CmbObjectData(CmbBaseObjectData):
|
||||
if self.object is None:
|
||||
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.connect("notify", self._on_notify)
|
||||
|
||||
@ -176,6 +175,7 @@ class CmbObjectData(CmbBaseObjectData):
|
||||
(self.ui_id, self.object_id, self.owner_id, self.id),
|
||||
):
|
||||
obj = CmbObjectData.from_row(self.project, *row)
|
||||
obj.parent = self
|
||||
self.__add_child(obj)
|
||||
|
||||
def add_data(self, data_key, value=None, comment=None):
|
||||
@ -194,11 +194,17 @@ class CmbObjectData(CmbBaseObjectData):
|
||||
def remove_data(self, data):
|
||||
try:
|
||||
assert data in self.children
|
||||
|
||||
self.project.history_push(
|
||||
_("Remove {key} from {name}").format(key=data.info.key, name=self.object.display_name_type)
|
||||
)
|
||||
|
||||
self.project.db.execute(
|
||||
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;",
|
||||
(self.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
|
||||
)
|
||||
self.project.db.commit()
|
||||
self.project.history_pop()
|
||||
except Exception as e:
|
||||
logger.warning(f"{self} Error removing data {data}: {e}")
|
||||
return False
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbObjectDataEditor - Cambalache Object Data Editor
|
||||
#
|
||||
# Copyright (C) 2022-2023 Juan Pablo Ugarte
|
||||
# Copyright (C) 2022-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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
@ -29,6 +31,12 @@ from .control import cmb_create_editor
|
||||
from cambalache import _
|
||||
|
||||
|
||||
# Everyone knows that debugging is twice as hard as writing a program in the first place.
|
||||
# So if 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")
|
||||
class CmbObjectDataEditor(Gtk.Box):
|
||||
__gtype_name__ = "CmbObjectDataEditor"
|
||||
@ -50,7 +58,6 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
self.__value_editor = None
|
||||
self.__arg_editors = {}
|
||||
self.__editors = []
|
||||
self.__editor_margin_end = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@ -68,21 +75,9 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
def __on_remove_clicked(self, button):
|
||||
if self.info:
|
||||
self.object.remove_data(self.__data)
|
||||
else:
|
||||
elif self.__data:
|
||||
self.__data.parent.remove_data(self.__data)
|
||||
|
||||
@Gtk.Template.Callback("on_remove_size_allocate")
|
||||
def __on_remove_size_allocate(self, button, alloc):
|
||||
info = self.data.info if self.data else self.info
|
||||
|
||||
if info is None or info.type_id is None:
|
||||
return
|
||||
|
||||
self.__editor_margin_end = alloc.width + self.top_box.props.spacing
|
||||
|
||||
for editor in self.__editors:
|
||||
editor.props.margin_end = self.__editor_margin_end
|
||||
|
||||
@GObject.Property(type=GObject.Object)
|
||||
def object(self):
|
||||
return self.__object
|
||||
@ -128,7 +123,11 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
|
||||
editor = self.__arg_editors.get(key, None)
|
||||
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):
|
||||
self.__add_data_editor(data)
|
||||
@ -145,8 +144,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
self.__update_view()
|
||||
|
||||
def __on_data_removed(self, obj, data):
|
||||
if self.object and self.info:
|
||||
self.__remove_data_editor(data)
|
||||
self.__remove_data_editor(data)
|
||||
|
||||
def __ensure_object_data(self, history_message):
|
||||
if self.data:
|
||||
@ -182,9 +180,9 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
child_info = info.children[child]
|
||||
button = Gtk.ModelButton(label=_("Add {key}").format(key=child_info.key), visible=True)
|
||||
button.connect("clicked", self.__on_child_button_clicked, child_info)
|
||||
box.add(button)
|
||||
box.append(button)
|
||||
|
||||
popover.add(box)
|
||||
popover.set_child(box)
|
||||
|
||||
return popover
|
||||
|
||||
@ -204,9 +202,6 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
|
||||
self.grid.attach(editor, 1, neditors, 1, 1)
|
||||
|
||||
if self.__editor_margin_end:
|
||||
editor.props.margin_end = self.__editor_margin_end
|
||||
|
||||
self.__editors.append(editor)
|
||||
|
||||
def __add_data_editor(self, data):
|
||||
@ -236,7 +231,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
|
||||
nchildren = len(info.children)
|
||||
|
||||
self.remove_button.set_tooltip_text(_("Remove {key}").format(key=info.key))
|
||||
self.remove_button.props.tooltip_text = _("Remove {key}").format(key=info.key)
|
||||
|
||||
# Add a menu if there is more than one child type
|
||||
if nchildren > 1:
|
||||
@ -244,7 +239,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
self.add_child.set_visible(True)
|
||||
elif nchildren:
|
||||
key = list(info.children.keys())[0]
|
||||
self.add_only_child.set_tooltip_text(_("Add {key}").format(key=key))
|
||||
self.add_only_child.props.tooltip_text = _("Add {key}").format(key=key)
|
||||
self.add_only_child.set_visible(True)
|
||||
|
||||
# Item name
|
||||
@ -255,7 +250,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
|
||||
# Value
|
||||
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
|
||||
|
||||
if self.data:
|
||||
@ -267,7 +262,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
)
|
||||
|
||||
self.top_box.add(editor)
|
||||
self.top_box.append(editor)
|
||||
|
||||
nargs = len(info.args)
|
||||
|
||||
@ -275,7 +270,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
for arg in info.args:
|
||||
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
|
||||
|
||||
# Initialize value
|
||||
@ -287,7 +282,7 @@ class CmbObjectDataEditor(Gtk.Box):
|
||||
# Special case items with one argument and no value (like styles)
|
||||
if nargs == 1 and not info.type_id:
|
||||
self.label.props.label = f"{info.key} {arg_info.key}"
|
||||
self.top_box.add(editor)
|
||||
self.top_box.append(editor)
|
||||
else:
|
||||
label = Gtk.Label(visible=True, label=arg_info.key, xalign=1)
|
||||
self.__add(editor, label)
|
||||
|
@ -1,125 +1,76 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<!-- Created with Cambalache 0.11.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<!-- interface-name cmb_object_data_editor.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="CmbObjectDataEditor" parent="GtkBox">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="top_box">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="no-show-all">True</property>
|
||||
<object class="GtkBox">
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="add_child">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="relief">none</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
<object class="GtkLabel" id="label"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_only_child">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="relief">none</property>
|
||||
<signal name="clicked" handler="on_add_only_child_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<object class="GtkBox" id="top_box">
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">True</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">False</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="relief">none</property>
|
||||
<signal name="clicked" handler="on_remove_clicked" swapped="no"/>
|
||||
<signal name="size-allocate" handler="on_remove_size_allocate" swapped="no"/>
|
||||
<signal name="clicked" handler="on_remove_clicked"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">user-trash-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="borderless"/>
|
||||
</style>
|
||||
<style>
|
||||
<class name="hidden"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="label">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<object class="GtkMenuButton" id="add_child">
|
||||
<property name="halign">end</property>
|
||||
<property name="visible">0</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="add_only_child">
|
||||
<property name="focusable">1</property>
|
||||
<property name="halign">end</property>
|
||||
<property name="visible">0</property>
|
||||
<signal name="clicked" handler="on_add_only_child_clicked"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="borderless"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=2 n-rows=1 -->
|
||||
<object class="GtkGrid" id="grid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">4</property>
|
||||
<property name="column-spacing">4</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
<property name="row-spacing">4</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbObjectEditor - Cambalache Object Editor
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
@ -29,9 +31,10 @@ from .control import CmbEntry, CmbChildTypeComboBox, cmb_create_editor
|
||||
from .cmb_property_label import CmbPropertyLabel
|
||||
from cambalache import _
|
||||
from .constants import EXTERNAL_TYPE
|
||||
from . import utils
|
||||
|
||||
|
||||
class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
class CmbObjectEditor(Gtk.Box):
|
||||
__gtype_name__ = "CmbObjectEditor"
|
||||
|
||||
layout = GObject.Property(type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False)
|
||||
@ -40,13 +43,16 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
self.__object = None
|
||||
self.__id_label = None
|
||||
self.__template_switch = None
|
||||
self.__bindings = []
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True)
|
||||
viewport = Gtk.Viewport(visible=True, shadow_type=Gtk.ShadowType.NONE)
|
||||
viewport.add(self.box)
|
||||
self.add(viewport)
|
||||
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):
|
||||
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4)
|
||||
@ -56,7 +62,7 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
|
||||
# Id/Class entry
|
||||
entry = CmbEntry()
|
||||
GObject.Object.bind_property(
|
||||
self.bind_property(
|
||||
self.__object,
|
||||
"name",
|
||||
entry,
|
||||
@ -68,7 +74,7 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
grid.attach(entry, 1, 0, 1, 1)
|
||||
|
||||
# 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
|
||||
tooltip_text = _("Switch between object and template")
|
||||
derivable = self.__object.info.derivable
|
||||
@ -97,12 +103,12 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
box = Gtk.FlowBox(visible=True, hexpand=True, selection_mode=Gtk.SelectionMode.NONE)
|
||||
|
||||
label = Gtk.Label(label=_("Add"), xalign=0, visible=True)
|
||||
box.add(label)
|
||||
box.append(label)
|
||||
|
||||
for type_id in info.child_type_shortcuts:
|
||||
button = Gtk.Button(label=type_id, visible=True)
|
||||
button.connect("clicked", self.__on_shortcut_button_clicked, type_id)
|
||||
box.add(button)
|
||||
box.append(button)
|
||||
|
||||
return box
|
||||
|
||||
@ -127,23 +133,23 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
def __create_child_type_editor(self):
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
|
||||
|
||||
box.add(Gtk.Label(label=_("Child Type"), width_chars=8))
|
||||
box.append(Gtk.Label(label=_("Child Type"), width_chars=8))
|
||||
|
||||
combo = CmbChildTypeComboBox(object=self.__object)
|
||||
|
||||
GObject.Object.bind_property(
|
||||
self.bind_property(
|
||||
self.__object,
|
||||
"type",
|
||||
combo,
|
||||
"cmb-value",
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
)
|
||||
box.pack_start(combo, True, True, 0)
|
||||
box.append(combo)
|
||||
return box
|
||||
|
||||
def __update_view(self):
|
||||
for child in self.box.get_children():
|
||||
self.box.remove(child)
|
||||
for child in utils.widget_get_children(self):
|
||||
self.remove(child)
|
||||
|
||||
if self.__object is None:
|
||||
return
|
||||
@ -158,10 +164,10 @@ class CmbObjectEditor(Gtk.ScrolledWindow):
|
||||
|
||||
# Child Type input
|
||||
if parent.info.has_child_types():
|
||||
self.box.add(self.__create_child_type_editor())
|
||||
self.append(self.__create_child_type_editor())
|
||||
else:
|
||||
# ID
|
||||
self.box.add(self.__create_id_editor())
|
||||
self.append(self.__create_id_editor())
|
||||
|
||||
if obj.type_id == EXTERNAL_TYPE:
|
||||
label = Gtk.Label(
|
||||
@ -174,8 +180,8 @@ It has to be exposed by your application with GtkBuilder expose_object method."
|
||||
xalign=0,
|
||||
wrap=True,
|
||||
)
|
||||
self.box.add(label)
|
||||
self.show_all()
|
||||
self.append(label)
|
||||
self.show()
|
||||
return
|
||||
|
||||
info = parent.info if self.layout and parent else obj.info
|
||||
@ -209,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:
|
||||
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)
|
||||
|
||||
if editor is None:
|
||||
continue
|
||||
|
||||
GObject.Object.bind_property(
|
||||
self.bind_property(
|
||||
prop,
|
||||
"value",
|
||||
editor,
|
||||
@ -227,7 +237,10 @@ It has to be exposed by your application with GtkBuilder expose_object method."
|
||||
else:
|
||||
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(editor, 1, i, 1, 1)
|
||||
@ -247,7 +260,7 @@ It has to be exposed by your application with GtkBuilder expose_object method."
|
||||
hexpand=True,
|
||||
object=obj,
|
||||
data=data,
|
||||
info=None if data else info.data[data_key],
|
||||
info=info.data[data_key],
|
||||
)
|
||||
|
||||
grid.attach(editor, 0, i, 2, 1)
|
||||
@ -261,16 +274,20 @@ It has to be exposed by your application with GtkBuilder expose_object method."
|
||||
expander = Gtk.Expander(label=f"<b>{owner_id}</b>", use_markup=True, expanded=True)
|
||||
revealer = Gtk.Revealer(reveal_child=True)
|
||||
expander.connect("notify::expanded", self.__on_expander_expanded, revealer)
|
||||
revealer.add(grid)
|
||||
self.box.add(expander)
|
||||
self.box.add(revealer)
|
||||
revealer.set_child(grid)
|
||||
self.append(expander)
|
||||
self.append(revealer)
|
||||
|
||||
self.show_all()
|
||||
self.show()
|
||||
|
||||
def __on_object_ui_notify(self, obj, pspec):
|
||||
if pspec.name == "template-id" and self.__template_switch:
|
||||
self.__template_switch.set_active(obj.props.template_id != 0)
|
||||
|
||||
def __on_object_notify(self, obj, pspec):
|
||||
if pspec.name == "parent-id":
|
||||
self.__update_view()
|
||||
|
||||
@GObject.Property(type=CmbObject)
|
||||
def object(self):
|
||||
return self.__object
|
||||
@ -281,12 +298,19 @@ It has to be exposed by your application with GtkBuilder expose_object method."
|
||||
return
|
||||
|
||||
if self.__object:
|
||||
self.__object.disconnect_by_func(self.__on_object_notify)
|
||||
self.__object.ui.disconnect_by_func(self.__on_object_ui_notify)
|
||||
|
||||
for binding in self.__bindings:
|
||||
binding.unbind()
|
||||
|
||||
self.__bindings = []
|
||||
|
||||
self.__object = obj
|
||||
|
||||
if obj:
|
||||
self.__object.ui.connect("notify", self.__on_object_ui_notify)
|
||||
obj.connect("notify", self.__on_object_notify)
|
||||
obj.ui.connect("notify", self.__on_object_ui_notify)
|
||||
|
||||
self.__update_view()
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
#
|
||||
# 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
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
@ -22,6 +22,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject
|
||||
from .cmb_base import CmbBase
|
||||
@ -55,8 +57,8 @@ class CmbBaseLibraryInfo(CmbBase):
|
||||
)
|
||||
|
||||
|
||||
class CmbPropertyInfo(CmbBase):
|
||||
__gtype_name__ = "CmbPropertyInfo"
|
||||
class CmbBasePropertyInfo(CmbBase):
|
||||
__gtype_name__ = "CmbBasePropertyInfo"
|
||||
|
||||
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)
|
||||
@ -81,13 +83,15 @@ class CmbPropertyInfo(CmbBase):
|
||||
disable_inline_object = GObject.Property(
|
||||
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
|
||||
)
|
||||
is_position = GObject.Property(
|
||||
type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False
|
||||
)
|
||||
deprecated = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
|
||||
required = GObject.Property(
|
||||
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)
|
||||
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):
|
||||
super().__init__(**kwargs)
|
||||
@ -109,9 +113,11 @@ class CmbPropertyInfo(CmbBase):
|
||||
deprecated_version,
|
||||
translatable,
|
||||
disable_inline_object,
|
||||
is_position,
|
||||
deprecated,
|
||||
required,
|
||||
workspace_default,
|
||||
original_owner_id,
|
||||
disabled,
|
||||
):
|
||||
return cls(
|
||||
project=project,
|
||||
@ -128,9 +134,11 @@ class CmbPropertyInfo(CmbBase):
|
||||
deprecated_version=deprecated_version,
|
||||
translatable=translatable,
|
||||
disable_inline_object=disable_inline_object,
|
||||
is_position=is_position,
|
||||
deprecated=deprecated,
|
||||
required=required,
|
||||
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):
|
||||
__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)
|
||||
|
||||
|
||||
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):
|
||||
__gtype_name__ = "CmbBaseProperty"
|
||||
|
||||
@ -976,7 +1099,21 @@ class CmbBaseObject(CmbBase):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def from_row(cls, project, ui_id, object_id, type_id, name, parent_id, internal, type, comment, position, custom_fragment):
|
||||
def from_row(
|
||||
cls,
|
||||
project,
|
||||
ui_id,
|
||||
object_id,
|
||||
type_id,
|
||||
name,
|
||||
parent_id,
|
||||
internal,
|
||||
type,
|
||||
comment,
|
||||
position,
|
||||
custom_fragment,
|
||||
custom_child_fragment,
|
||||
):
|
||||
return cls(project=project, ui_id=ui_id, object_id=object_id)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
@ -1147,6 +1284,27 @@ class CmbBaseObject(CmbBase):
|
||||
value,
|
||||
)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def custom_child_fragment(self):
|
||||
return self.db_get(
|
||||
"SELECT custom_child_fragment FROM object WHERE (ui_id, object_id) IS (?, ?);",
|
||||
(
|
||||
self.ui_id,
|
||||
self.object_id,
|
||||
),
|
||||
)
|
||||
|
||||
@custom_child_fragment.setter
|
||||
def _set_custom_child_fragment(self, value):
|
||||
self.db_set(
|
||||
"UPDATE object SET custom_child_fragment=? WHERE (ui_id, object_id) IS (?, ?);",
|
||||
(
|
||||
self.ui_id,
|
||||
self.object_id,
|
||||
),
|
||||
value,
|
||||
)
|
||||
|
||||
|
||||
class CmbBaseObjectData(CmbBase):
|
||||
__gtype_name__ = "CmbBaseObjectData"
|
||||
|
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
@ -1,7 +1,7 @@
|
||||
#
|
||||
# Cambalache Property wrapper
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,11 +20,17 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
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 cambalache import _, getLogger
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class CmbProperty(CmbBaseProperty):
|
||||
@ -41,9 +47,14 @@ class CmbProperty(CmbBaseProperty):
|
||||
self.library_id = owner_info.library_id
|
||||
self._update_version_warning()
|
||||
|
||||
self.connect("notify", self.__on_notify)
|
||||
|
||||
def __str__(self):
|
||||
return f"CmbProperty<{self.object.type_id} {self.info.owner_id}:{self.property_id}>"
|
||||
|
||||
def __on_notify(self, obj, pspec):
|
||||
self.project._object_property_changed(self.object, self)
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def value(self):
|
||||
c = self.project.db.execute(
|
||||
@ -55,9 +66,96 @@ class CmbProperty(CmbBaseProperty):
|
||||
|
||||
@value.setter
|
||||
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()
|
||||
|
||||
bind_source_id, bind_owner_id, bind_property_id = (None, None, None)
|
||||
@ -67,18 +165,22 @@ class CmbProperty(CmbBaseProperty):
|
||||
bind_property_id = bind_property.property_id
|
||||
|
||||
if (
|
||||
value is None or value == self.info.default_value or (self.info.is_object and value == 0)
|
||||
) and bind_property is None:
|
||||
c.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),
|
||||
)
|
||||
(value is None or value == self.info.default_value or (self.info.is_object and value == 0)) and
|
||||
bind_property is None and
|
||||
not translatable and
|
||||
translation_context is None and
|
||||
translation_comments is None
|
||||
):
|
||||
self.reset()
|
||||
else:
|
||||
if (
|
||||
value is None
|
||||
and bind_source_id == self.bind_source_id
|
||||
and bind_owner_id == self.bind_owner_id
|
||||
and bind_property_id == self.bind_property_id
|
||||
value == self.value and
|
||||
translatable == self.translatable and
|
||||
translation_context == self.translation_context and
|
||||
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
|
||||
|
||||
@ -89,14 +191,23 @@ class CmbProperty(CmbBaseProperty):
|
||||
)
|
||||
|
||||
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(
|
||||
"""
|
||||
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=?;
|
||||
""",
|
||||
(
|
||||
value,
|
||||
translatable,
|
||||
translation_context,
|
||||
translation_comments,
|
||||
bind_source_id,
|
||||
bind_owner_id,
|
||||
bind_property_id,
|
||||
@ -107,11 +218,19 @@ class CmbProperty(CmbBaseProperty):
|
||||
),
|
||||
)
|
||||
else:
|
||||
if self.info.internal_child:
|
||||
self.project.history_push(_("Set {obj} {prop} {prop_type} to {value}").format(**self.__get_msgs(value)))
|
||||
|
||||
c.execute(
|
||||
"""
|
||||
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,
|
||||
@ -119,12 +238,20 @@ class CmbProperty(CmbBaseProperty):
|
||||
self.owner_id,
|
||||
self.property_id,
|
||||
value,
|
||||
translatable,
|
||||
translation_context,
|
||||
translation_comments,
|
||||
bind_source_id,
|
||||
bind_owner_id,
|
||||
bind_property_id,
|
||||
),
|
||||
)
|
||||
|
||||
self.__update_internal_child()
|
||||
|
||||
if self.info.internal_child:
|
||||
self.project.history_pop()
|
||||
|
||||
if self._init is False:
|
||||
self.object._property_changed(self)
|
||||
|
||||
@ -151,9 +278,22 @@ class CmbProperty(CmbBaseProperty):
|
||||
|
||||
@bind_property.setter
|
||||
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)
|
||||
|
||||
def _update_version_warning(self):
|
||||
target = self.object.ui.get_target(self.library_id)
|
||||
self.version_warning = utils.get_version_warning(target, self.info.version, self.info.deprecated_version, self.property_id)
|
||||
warning = utils.get_version_warning(
|
||||
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)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbPropertyLabel
|
||||
#
|
||||
# Copyright (C) 2023 Juan Pablo Ugarte
|
||||
# Copyright (C) 2023-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
|
||||
@ -20,15 +20,17 @@
|
||||
# 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 .cmb_property import CmbProperty
|
||||
from .cmb_layout_property import CmbLayoutProperty
|
||||
from .cmb_objects_base import CmbPropertyInfo
|
||||
from .cmb_property_info import CmbPropertyInfo
|
||||
from .control import CmbObjectChooser, CmbFlagsEntry
|
||||
from . import utils
|
||||
from cambalache import _
|
||||
|
||||
|
||||
class CmbPropertyLabel(Gtk.Button):
|
||||
@ -43,21 +45,21 @@ class CmbPropertyLabel(Gtk.Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.props.relief = Gtk.ReliefStyle.NONE
|
||||
|
||||
if not self.prop and not self.layout_prop:
|
||||
raise Exception("CmbPropertyLabel requires prop or layout_prop to be set")
|
||||
return
|
||||
|
||||
self.label = Gtk.Label(xalign=0, visible=True)
|
||||
box = Gtk.Box(visible=True)
|
||||
self.props.focus_on_click = False
|
||||
self.label = Gtk.Label(halign=Gtk.Align.START, valign=Gtk.Align.CENTER)
|
||||
box = Gtk.Box()
|
||||
|
||||
# Update label status
|
||||
if self.prop:
|
||||
self.bind_icon = Gtk.Image(icon_size=Gtk.IconSize.MENU, visible=True)
|
||||
box.add(self.bind_icon)
|
||||
self.bind_icon = Gtk.Image(icon_size=Gtk.IconSize.NORMAL)
|
||||
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.prop.connect("notify::value", lambda o, p: self.__update_property_label())
|
||||
@ -71,24 +73,22 @@ class CmbPropertyLabel(Gtk.Button):
|
||||
self.__update_layout_property_label()
|
||||
self.layout_prop.connect("notify::value", lambda o, p: self.__update_layout_property_label())
|
||||
|
||||
box.add(self.label)
|
||||
self.add(box)
|
||||
box.append(self.label)
|
||||
self.set_child(box)
|
||||
|
||||
def __update_label(self, prop):
|
||||
style = self.get_style_context()
|
||||
|
||||
if prop.value != prop.info.default_value:
|
||||
style.add_class("modified")
|
||||
self.add_css_class("modified")
|
||||
else:
|
||||
style.remove_class("modified")
|
||||
self.remove_css_class("modified")
|
||||
|
||||
msg = prop.version_warning
|
||||
self.set_tooltip_text(msg)
|
||||
|
||||
if msg:
|
||||
style.add_class("warning")
|
||||
self.add_css_class("warning")
|
||||
else:
|
||||
style.remove_class("warning")
|
||||
self.remove_css_class("warning")
|
||||
|
||||
def __update_layout_property_label(self):
|
||||
self.__update_label(self.layout_prop)
|
||||
@ -101,10 +101,10 @@ class CmbPropertyLabel(Gtk.Button):
|
||||
|
||||
if self.prop.bind_property_id:
|
||||
self.bind_icon.props.icon_name = "binded-symbolic"
|
||||
self.get_style_context().remove_class("hidden")
|
||||
self.remove_css_class("hidden")
|
||||
else:
|
||||
self.bind_icon.props.icon_name = "bind-symbolic"
|
||||
self.get_style_context().add_class("hidden")
|
||||
self.add_css_class("hidden")
|
||||
|
||||
def __on_object_editor_notify(self, object_editor, pspec, property_editor):
|
||||
object_id = object_editor.cmb_value
|
||||
@ -128,12 +128,23 @@ class CmbPropertyLabel(Gtk.Button):
|
||||
self.__update_property_label()
|
||||
popover.popdown()
|
||||
|
||||
def __on_close_clicked(self, button, popover):
|
||||
popover.popdown()
|
||||
|
||||
def __on_bind_button_clicked(self, button):
|
||||
popover = Gtk.Popover(relative_to=button, position=Gtk.PositionType.LEFT)
|
||||
popover = Gtk.Popover(position=Gtk.PositionType.LEFT, css_classes=["cmb-binding-popover"])
|
||||
popover.set_parent(self)
|
||||
|
||||
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4, border_width=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
|
||||
bind_source, bind_property = self.__find_bind_source_property(self.prop.bind_source_id, self.prop.bind_property_id)
|
||||
@ -163,21 +174,19 @@ class CmbPropertyLabel(Gtk.Button):
|
||||
|
||||
i = 1
|
||||
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, visible=True)
|
||||
label = Gtk.Label(label=prop_label, xalign=0)
|
||||
|
||||
grid.attach(label, 0, i, 1, 1)
|
||||
grid.attach(editor, 1, i, 1, 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)
|
||||
|
||||
grid.attach(clear, 0, i, 2, 1)
|
||||
|
||||
object_editor.grab_focus()
|
||||
|
||||
popover.add(grid)
|
||||
popover.set_child(grid)
|
||||
popover.popup()
|
||||
|
||||
|
||||
@ -193,7 +202,6 @@ class CmbPropertyChooser(Gtk.ComboBoxText):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
utils.unset_scroll_event(self)
|
||||
self.__populate()
|
||||
self.connect("notify::object", self.__on_object_notify)
|
||||
|
||||
@ -215,6 +223,9 @@ class CmbPropertyChooser(Gtk.ComboBoxText):
|
||||
for prop in sorted(self.object.properties, key=lambda p: p.property_id):
|
||||
info = prop.info
|
||||
|
||||
if info.is_a11y:
|
||||
continue
|
||||
|
||||
# Ignore construct only properties
|
||||
if info.construct_only:
|
||||
continue
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbSignalEditor - Cambalache Signal Editor
|
||||
#
|
||||
# Copyright (C) 2021-2023 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk, Pango
|
||||
|
||||
@ -69,23 +71,26 @@ class CmbSignalEditor(Gtk.Box):
|
||||
self.user_data_column.set_cell_data_func(self.user_data, self.__data_func, Col.USER_DATA.value)
|
||||
self.swap_column.set_cell_data_func(self.swap, self.__data_func, Col.SWAP.value)
|
||||
self.after_column.set_cell_data_func(self.after, self.__data_func, Col.AFTER.value)
|
||||
|
||||
@GObject.Property(type=CmbObject)
|
||||
def object(self):
|
||||
return self._object
|
||||
|
||||
@object.setter
|
||||
def _set_object(self, obj):
|
||||
if self._object is not None:
|
||||
if self._object:
|
||||
self.treestore.clear()
|
||||
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_changed)
|
||||
|
||||
self._object = obj
|
||||
|
||||
if obj is not None:
|
||||
if obj:
|
||||
self.__populate_treestore()
|
||||
self._object.connect("signal-added", self.__on_signal_added)
|
||||
self._object.connect("signal-removed", self.__on_signal_removed)
|
||||
self._object.connect("signal-changed", self.__on_signal_changed)
|
||||
|
||||
@Gtk.Template.Callback("on_handler_edited")
|
||||
def __on_handler_edited(self, renderer, path, new_text):
|
||||
@ -133,13 +138,17 @@ class CmbSignalEditor(Gtk.Box):
|
||||
if data_obj:
|
||||
signal.user_data = data_obj.object_id
|
||||
name = data_obj.name
|
||||
signal.swap = True
|
||||
else:
|
||||
signal.user_data = 0
|
||||
signal.swap = False
|
||||
self.treestore[iter_][Col.SWAP.value] = signal.swap
|
||||
name = ""
|
||||
|
||||
self.treestore[iter_][Col.USER_DATA.value] = name
|
||||
else:
|
||||
signal.user_data = 0
|
||||
signal.swap = False
|
||||
self.treestore[iter_][Col.USER_DATA.value] = ""
|
||||
|
||||
@Gtk.Template.Callback("on_swap_toggled")
|
||||
@ -178,7 +187,7 @@ class CmbSignalEditor(Gtk.Box):
|
||||
signal.swap,
|
||||
signal.after,
|
||||
child[Col.INFO.value],
|
||||
None
|
||||
None,
|
||||
),
|
||||
)
|
||||
break
|
||||
@ -190,6 +199,17 @@ class CmbSignalEditor(Gtk.Box):
|
||||
self.treestore.remove(child.iter)
|
||||
return
|
||||
|
||||
def __on_signal_changed(self, obj, signal):
|
||||
for row in self.treestore:
|
||||
for child in row.iterchildren():
|
||||
if child[Col.SIGNAL.value] == signal:
|
||||
child[Col.DETAIL.value] = signal.detail
|
||||
child[Col.HANDLER.value] = signal.handler
|
||||
child[Col.USER_DATA.value] = str(signal.user_data)
|
||||
child[Col.SWAP.value] = signal.swap
|
||||
child[Col.AFTER.value] = signal.after
|
||||
return
|
||||
|
||||
def __populate_from_type(self, info, target):
|
||||
if len(info.signals) == 0:
|
||||
return None
|
||||
@ -226,7 +246,7 @@ class CmbSignalEditor(Gtk.Box):
|
||||
signal_id = tree_model[iter_][Col.SIGNAL_ID.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]
|
||||
signal = tree_model[iter_][Col.SIGNAL.value]
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<!-- interface-name cmb_signal_editor.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="GtkEntryCompletion" id="handler_entrycompletion">
|
||||
<child>
|
||||
<object class="GtkCellRendererText"/>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="text">0</attribute>
|
||||
</attributes>
|
||||
@ -12,56 +15,38 @@
|
||||
</object>
|
||||
<object class="GtkTreeStore" id="treestore">
|
||||
<columns>
|
||||
<!-- column-name signal -->
|
||||
<column type="GObject"/>
|
||||
<!-- column-name owner_id -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name signal_id -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name detail -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name handler -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name user_data -->
|
||||
<column type="gchararray"/>
|
||||
<!-- column-name swap -->
|
||||
<column type="gboolean"/>
|
||||
<!-- column-name after -->
|
||||
<column type="gboolean"/>
|
||||
<!-- column-name info -->
|
||||
<column type="GObject"/>
|
||||
<!-- column-name version_warning -->
|
||||
<column type="gchararray"/>
|
||||
</columns>
|
||||
</object>
|
||||
<template class="CmbSignalEditor" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkTreeView" id="treeview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="model">treestore</property>
|
||||
<property name="tooltip-column">9</property>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="signal_id_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="min-width">64</property>
|
||||
<property name="resizable">1</property>
|
||||
<property name="title" translatable="yes">Signal</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="signal_id">
|
||||
<signal name="edited" handler="on_detail_edited" swapped="no"/>
|
||||
<signal name="edited" handler="on_detail_edited"/>
|
||||
</object>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="text">2</attribute>
|
||||
</attributes>
|
||||
@ -70,16 +55,17 @@
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="handler_column">
|
||||
<property name="resizable">True</property>
|
||||
<property name="expand">1</property>
|
||||
<property name="min-width">64</property>
|
||||
<property name="resizable">1</property>
|
||||
<property name="title" translatable="yes">Handler</property>
|
||||
<property name="expand">True</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="handler">
|
||||
<property name="editable">True</property>
|
||||
<property name="editable">1</property>
|
||||
<property name="placeholder-text"><Enter callback></property>
|
||||
<signal name="edited" handler="on_handler_edited" swapped="no"/>
|
||||
<signal name="edited" handler="on_handler_edited"/>
|
||||
</object>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="text">4</attribute>
|
||||
</attributes>
|
||||
@ -91,10 +77,11 @@
|
||||
<property name="title" translatable="yes">Data</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="user_data">
|
||||
<property name="editable">True</property>
|
||||
<property name="editable">1</property>
|
||||
<property name="placeholder-text"><object></property>
|
||||
<signal name="edited" handler="on_user_data_edited" swapped="no"/>
|
||||
<signal name="edited" handler="on_user_data_edited"/>
|
||||
</object>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="text">5</attribute>
|
||||
</attributes>
|
||||
@ -106,8 +93,9 @@
|
||||
<property name="title" translatable="yes">Swap</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="swap">
|
||||
<signal name="toggled" handler="on_swap_toggled" swapped="no"/>
|
||||
<signal name="toggled" handler="on_swap_toggled"/>
|
||||
</object>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="active">6</attribute>
|
||||
</attributes>
|
||||
@ -119,8 +107,9 @@
|
||||
<property name="title" translatable="yes">After</property>
|
||||
<child>
|
||||
<object class="GtkCellRendererToggle" id="after">
|
||||
<signal name="toggled" handler="on_after_toggled" swapped="no"/>
|
||||
<signal name="toggled" handler="on_after_toggled"/>
|
||||
</object>
|
||||
<!-- Custom child fragments -->
|
||||
<attributes>
|
||||
<attribute name="active">7</attribute>
|
||||
</attributes>
|
||||
@ -128,13 +117,10 @@
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
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,172 +0,0 @@
|
||||
#
|
||||
# CmbTreeView - Cambalache Tree View
|
||||
#
|
||||
# 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>
|
||||
#
|
||||
|
||||
from gi.repository import Gdk, 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)
|
||||
self.__right_click = 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)
|
||||
|
||||
self.menu = CmbContextMenu(relative_to=self)
|
||||
|
||||
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.BUTTON_RELEASE_MASK)
|
||||
self.connect("button-press-event", self.__on_button_press_event)
|
||||
self.connect("button-release-event", self.__on_button_release_event)
|
||||
|
||||
self.set_reorderable(True)
|
||||
|
||||
def __on_button_press_event(self, widget, event):
|
||||
if event.window != self.get_bin_window() or event.button != 3:
|
||||
return False
|
||||
|
||||
self.__right_click = True
|
||||
return True
|
||||
|
||||
def __on_button_release_event(self, widget, event):
|
||||
if event.window != self.get_bin_window() or event.button != 3:
|
||||
return False
|
||||
|
||||
if not self.__right_click:
|
||||
return False
|
||||
|
||||
self.__right_click = False
|
||||
|
||||
retval = self.get_path_at_pos(event.x, event.y)
|
||||
|
||||
if retval is None:
|
||||
return False
|
||||
|
||||
path, col, xx, yy = retval
|
||||
self.get_selection().select_path(path)
|
||||
|
||||
self.menu.popup_at(event.x, event.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) == 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, xx, yy, 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) == CmbObject:
|
||||
msg = obj.version_warning
|
||||
if msg:
|
||||
tooltip.set_text(msg)
|
||||
return True
|
||||
|
||||
return False
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbTypeChooserBar - Cambalache Type Chooser Bar
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
@ -41,6 +43,7 @@ class CmbTypeChooser(Gtk.Box):
|
||||
selected_type = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
type_label = Gtk.Template.Child()
|
||||
content = Gtk.Template.Child()
|
||||
all = Gtk.Template.Child()
|
||||
toplevel = Gtk.Template.Child()
|
||||
layout = Gtk.Template.Child()
|
||||
@ -83,3 +86,6 @@ class CmbTypeChooser(Gtk.Box):
|
||||
if info:
|
||||
self.selected_type = info
|
||||
self.emit("type-selected", info)
|
||||
|
||||
|
||||
Gtk.WidgetClass.set_css_name(CmbTypeChooser, "CmbTypeChooser")
|
||||
|
@ -1,216 +1,147 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="gladecambalache" version="0.0"/>
|
||||
<!-- interface-name cmb_type_chooser.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="CmbTypeChooserPopover" id="all">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="show-categories">True</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="control">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="category">control</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="display">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="category">display</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="extra">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="uncategorized-only">True</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="layout">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="category">layout</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="model">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="category">model</property>
|
||||
</object>
|
||||
<object class="CmbTypeChooserPopover" id="toplevel">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="category">toplevel</property>
|
||||
</object>
|
||||
<template class="CmbTypeChooser" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton" id="type_chooser_all">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">all</property>
|
||||
<object class="GtkBox" id="content">
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">edit-find-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox" id="type_chooser_gtk">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">expand</property>
|
||||
<property name="homogeneous">False</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">toplevel</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Widget group for toplevels/windows">Toplevel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">layout</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Widget group for container widgets liek GtkBox grid">Layout</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">control</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Widget group for control wildget like buttons, entries">Control</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">display</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Widget group for display widgets (label, image)">Display</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">model</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="label" translatable="yes" comments="Widget group for model objects (ListStore, TextBuffer)">Model</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="popover">extra</property>
|
||||
<object class="GtkMenuButton" id="type_chooser_all">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">all</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">pan-down-symbolic</property>
|
||||
<property name="icon-name">edit-find-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<style>
|
||||
<class name="linked"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel" id="type_label">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">1</property>
|
||||
<property name="sensitive">0</property>
|
||||
<attributes>
|
||||
<attribute name="style" value="italic"/>
|
||||
</attributes>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="type_chooser_gtk">
|
||||
<property name="homogeneous">False</property>
|
||||
<property name="visible">True</property>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">toplevel</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes" comments="Widget group for toplevels/windows">Toplevel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">layout</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes" comments="Widget group for container widgets like GtkBox grid">Layout</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">control</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes" comments="Widget group for control widget like buttons, entries">Control</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">display</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes" comments="Widget group for display widgets (label, image)">Display</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focus-on-click">0</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">model</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="label" translatable="yes" comments="Widget group for model objects (ListStore, TextBuffer)">Model</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkMenuButton">
|
||||
<property name="focusable">1</property>
|
||||
<property name="popover">extra</property>
|
||||
<property name="receives-default">1</property>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="icon-name">pan-down-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<style>
|
||||
<class name="linked"/>
|
||||
</style>
|
||||
</object>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbTypeChooserPopover - Cambalache Type Chooser Popover
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
@ -47,8 +49,9 @@ class CmbTypeChooserPopover(Gtk.Popover):
|
||||
|
||||
self._chooser = CmbTypeChooserWidget()
|
||||
self._chooser.connect("type-selected", self.__on_type_selected)
|
||||
self._chooser.show_all()
|
||||
self.add(self._chooser)
|
||||
|
||||
self.set_child(self._chooser)
|
||||
self.set_default_widget(self._chooser)
|
||||
|
||||
for prop in [
|
||||
"project",
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbTypeChooserWidget - Cambalache Type Chooser Widget
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,8 +20,10 @@
|
||||
# Authors:
|
||||
# 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_type_info import CmbTypeInfo
|
||||
@ -39,27 +41,35 @@ class CmbTypeChooserWidget(Gtk.Box):
|
||||
"type-selected": (GObject.SignalFlags.RUN_LAST, None, (CmbTypeInfo,)),
|
||||
}
|
||||
|
||||
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE)
|
||||
category = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||
uncategorized_only = GObject.Property(type=bool, flags=GObject.ParamFlags.READWRITE, default=False)
|
||||
show_categories = GObject.Property(type=bool, flags=GObject.ParamFlags.READWRITE, default=False)
|
||||
parent_type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||
derived_type_id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
entrycompletion = Gtk.Template.Child()
|
||||
scrolledwindow = Gtk.Template.Child()
|
||||
treeview = Gtk.Template.Child()
|
||||
listview = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__project = None
|
||||
self.__model = None
|
||||
self._search_text = ""
|
||||
self._filter = None
|
||||
self.__model = None
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.connect("map", self.__on_map)
|
||||
|
||||
def __on_map(self, widget):
|
||||
root = widget.get_root()
|
||||
|
||||
if root is not None:
|
||||
height = root.get_allocated_height() - 100
|
||||
if height > 460:
|
||||
height = height * 0.7
|
||||
|
||||
self.scrolledwindow.set_max_content_height(height)
|
||||
return False
|
||||
|
||||
def __type_info_should_append(self, info):
|
||||
retval = False
|
||||
|
||||
@ -83,10 +93,6 @@ class CmbTypeChooserWidget(Gtk.Box):
|
||||
|
||||
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):
|
||||
if project is None:
|
||||
return None
|
||||
@ -102,7 +108,12 @@ class CmbTypeChooserWidget(Gtk.Box):
|
||||
order = {"toplevel": 0, "layout": 1, "control": 2, "display": 3, "model": 4}
|
||||
|
||||
# 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 = []
|
||||
|
||||
for key in project.type_info:
|
||||
@ -124,41 +135,34 @@ class CmbTypeChooserWidget(Gtk.Box):
|
||||
if show_categories and last_category != i.category:
|
||||
last_category = i.category
|
||||
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
|
||||
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=GObject.GObject)
|
||||
@GObject.Property(type=CmbProject)
|
||||
def project(self):
|
||||
return self.__project
|
||||
|
||||
@project.setter
|
||||
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_removed)
|
||||
self.__project.disconnect_by_func(self.__on_type_info_changed)
|
||||
|
||||
self.__project = project
|
||||
|
||||
self.__model = self.__model_from_project(project)
|
||||
self._filter = Gtk.TreeModelFilter(child_model=self.__model) if self.__model else None
|
||||
if self._filter:
|
||||
self._filter.set_visible_func(self.__visible_func)
|
||||
self.listview.set_model(Gtk.NoSelection(model=self.__model))
|
||||
|
||||
self.entrycompletion.props.model = self.__model
|
||||
self.treeview.props.model = self._filter
|
||||
|
||||
if project is not None:
|
||||
if project:
|
||||
project.connect("type-info-added", self.__on_type_info_added)
|
||||
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")
|
||||
def __on_searchentry_activate(self, entry):
|
||||
@ -171,72 +175,34 @@ class CmbTypeChooserWidget(Gtk.Box):
|
||||
@Gtk.Template.Callback("on_searchentry_search_changed")
|
||||
def __on_searchentry_search_changed(self, entry):
|
||||
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")
|
||||
def __on_treeview_row_activated(self, treeview, path, column):
|
||||
model = treeview.props.model
|
||||
info = model[model.get_iter(path)][2]
|
||||
@Gtk.Template.Callback("on_listview_activate")
|
||||
def __on_listview_activate(self, listview, position):
|
||||
info = self.__model.get_item(position)
|
||||
|
||||
if info is not None:
|
||||
if info is not None and info.project:
|
||||
self.emit("type-selected", info)
|
||||
|
||||
def __visible_func(self, model, iter, data):
|
||||
type_id, type_id_lower, info, sensitive = model[iter]
|
||||
|
||||
# Always show categories if we are not searching
|
||||
if self._search_text == "" and info is None:
|
||||
return True
|
||||
|
||||
return type_id_lower.find(self._search_text) >= 0
|
||||
|
||||
def __on_map(self, widget):
|
||||
toplevel = widget.get_toplevel()
|
||||
|
||||
if toplevel:
|
||||
height = toplevel.get_allocated_height() - 100
|
||||
if height > 460:
|
||||
height = height * 0.7
|
||||
|
||||
self.scrolledwindow.set_max_content_height(height)
|
||||
return False
|
||||
def __custom_filter_func(self, info, data):
|
||||
return info.type_id.lower().find(self._search_text) >= 0
|
||||
|
||||
def __on_type_info_added(self, project, info):
|
||||
if self.__model is None:
|
||||
return
|
||||
|
||||
# Append new type info
|
||||
# TODO: insert in order
|
||||
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):
|
||||
if self.__model is None:
|
||||
return
|
||||
|
||||
# Find info and remove it from model
|
||||
for row in self.__model:
|
||||
if info == row[2]:
|
||||
self.__model.remove(row.iter)
|
||||
|
||||
def __on_type_info_changed(self, project, info):
|
||||
if self.__model is None:
|
||||
return
|
||||
|
||||
info_row = None
|
||||
|
||||
# Find info and update it from model
|
||||
for row in self.__model:
|
||||
if info == row[2]:
|
||||
info_row = row
|
||||
break
|
||||
|
||||
if info_row is None:
|
||||
return
|
||||
|
||||
# Update Type Name
|
||||
info_row[0] = info.type_id
|
||||
info_row[1] = info.type_id.lower()
|
||||
found, position = self.__model.props.model.find(info)
|
||||
if found:
|
||||
self.__model.props.model.remove(position)
|
||||
|
||||
|
||||
Gtk.WidgetClass.set_css_name(CmbTypeChooserWidget, "CmbTypeChooserWidget")
|
||||
|
@ -1,77 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<object class="GtkEntryCompletion" id="entrycompletion">
|
||||
<property name="text-column">0</property>
|
||||
<property name="inline-completion">True</property>
|
||||
<property name="popup-completion">False</property>
|
||||
<property name="popup-single-match">False</property>
|
||||
</object>
|
||||
<!-- interface-name cmb_type_chooser_widget.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<template class="CmbTypeChooserWidget" parent="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="border-width">6</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">6</property>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="searchentry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="completion">entrycompletion</property>
|
||||
<property name="input-hints">GTK_INPUT_HINT_WORD_COMPLETION | GTK_INPUT_HINT_NONE</property>
|
||||
<signal name="activate" handler="on_searchentry_activate" swapped="no"/>
|
||||
<signal name="search-changed" handler="on_searchentry_search_changed" swapped="no"/>
|
||||
<signal name="activate" handler="on_searchentry_activate"/>
|
||||
<signal name="search-changed" handler="on_searchentry_search_changed"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow" id="scrolledwindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="window-placement">bottom-left</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="max-content-height">512</property>
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="propagate-natural-height">True</property>
|
||||
<property name="propagate-natural-width">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<child>
|
||||
<object class="GtkTreeView" id="treeview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="headers-visible">False</property>
|
||||
<property name="enable-search">False</property>
|
||||
<property name="activate-on-single-click">True</property>
|
||||
<signal name="row-activated" handler="on_treeview_row_activated" swapped="no"/>
|
||||
<child internal-child="selection">
|
||||
<object class="GtkTreeSelection" id="treeview-selection"/>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkTreeViewColumn" id="column_adaptor">
|
||||
<child>
|
||||
<object class="GtkCellRendererText" id="adaptor_cell"/>
|
||||
<attributes>
|
||||
<attribute name="markup">0</attribute>
|
||||
<attribute name="sensitive">3</attribute>
|
||||
</attributes>
|
||||
</child>
|
||||
<object class="GtkListView" id="listview">
|
||||
<property name="factory">
|
||||
<object class="GtkBuilderListItemFactory">
|
||||
<property name="bytes"><![CDATA[<?xml version='1.0' encoding='UTF-8'?>
|
||||
<interface>
|
||||
<template class="GtkListItem" parent="GObject">
|
||||
<property name="child">
|
||||
<object class="GtkInscription">
|
||||
<binding name="markup">
|
||||
<lookup name="type_id" type="CmbTypeInfo">
|
||||
<lookup name="item">GtkListItem</lookup>
|
||||
</lookup>
|
||||
</binding>
|
||||
</object>
|
||||
</property>
|
||||
</template>
|
||||
</interface>]]></property>
|
||||
</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>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
@ -27,13 +29,18 @@ from .cmb_objects_base import (
|
||||
CmbBaseTypeInfo,
|
||||
CmbBaseTypeDataInfo,
|
||||
CmbBaseTypeDataArgInfo,
|
||||
CmbBaseTypeInternalChildInfo,
|
||||
CmbTypeChildInfo,
|
||||
CmbPropertyInfo,
|
||||
CmbSignalInfo,
|
||||
)
|
||||
from .cmb_property_info import CmbPropertyInfo
|
||||
|
||||
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):
|
||||
def __init__(self, **kwargs):
|
||||
@ -53,19 +60,33 @@ class CmbTypeDataInfo(CmbBaseTypeDataInfo):
|
||||
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):
|
||||
__gtype_name__ = "CmbTypeInfo"
|
||||
|
||||
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 = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
if self.project is None:
|
||||
return
|
||||
|
||||
self.hierarchy = self.__init_hierarchy()
|
||||
self.interfaces = self.__init_interfaces()
|
||||
self.properties = self.__init_properties_signals(CmbPropertyInfo, "property")
|
||||
self.signals = self.__init_properties_signals(CmbSignalInfo, "signal")
|
||||
self.data = self.__init_data()
|
||||
self.internal_children = self.__init_internal_children()
|
||||
self.child_constraint, self.child_type_shortcuts = self.__init_child_constraint()
|
||||
|
||||
if self.parent_id == "enum":
|
||||
@ -134,7 +155,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
|
||||
|
||||
c = self.project.db.cursor()
|
||||
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()
|
||||
return retval
|
||||
@ -143,14 +164,14 @@ class CmbTypeInfo(CmbBaseTypeInfo):
|
||||
args = {}
|
||||
children = {}
|
||||
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()
|
||||
|
||||
# Collect Arguments
|
||||
for row in c.execute("SELECT * FROM type_data_arg WHERE owner_id=? AND data_id=?;", (owner_id, data_id)):
|
||||
_key = row[2]
|
||||
args[_key] = CmbTypeDataArgInfo.from_row(self, *row)
|
||||
args[_key] = CmbTypeDataArgInfo.from_row(self.project, *row)
|
||||
|
||||
# Recurse children
|
||||
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()
|
||||
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):
|
||||
retval = {}
|
||||
shortcuts = []
|
||||
@ -214,7 +281,7 @@ class CmbTypeInfo(CmbBaseTypeInfo):
|
||||
retval = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_INT)
|
||||
|
||||
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)
|
||||
|
||||
c.close()
|
||||
@ -263,3 +330,48 @@ class CmbTypeInfo(CmbBaseTypeInfo):
|
||||
parent = parent.parent
|
||||
|
||||
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:
|
||||
# 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, _
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class CmbUI(CmbBaseUI):
|
||||
class CmbUI(CmbBaseUI, Gio.ListModel):
|
||||
__gsignals__ = {
|
||||
"library-changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
|
||||
}
|
||||
|
||||
path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
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)
|
||||
def template_id(self):
|
||||
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):
|
||||
self.project._ui_changed(self, pspec.name)
|
||||
|
||||
# Update display name if one of the following properties changed
|
||||
if pspec.name in ["filename", "template-id"]:
|
||||
self.notify("display-name")
|
||||
|
||||
def list_libraries(self):
|
||||
retval = {}
|
||||
|
||||
@ -114,8 +132,21 @@ class CmbUI(CmbBaseUI):
|
||||
|
||||
c.close()
|
||||
|
||||
def get_display_name(self):
|
||||
return self.filename if self.filename else _("Unnamed {ui_id}").format(ui_id=self.ui_id)
|
||||
@classmethod
|
||||
def get_display_name(cls, ui_id, 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
|
||||
|
||||
if filename is None and template_id:
|
||||
template = self.project.get_object_by_id(self.ui_id, template_id)
|
||||
if template:
|
||||
return template.name
|
||||
|
||||
return CmbUI.get_display_name(self.ui_id, filename)
|
||||
|
||||
def __get_infered_target(self, library_id):
|
||||
ui_id = self.ui_id
|
||||
@ -129,11 +160,13 @@ class CmbUI(CmbBaseUI):
|
||||
UNION
|
||||
SELECT p.version
|
||||
FROM object_property AS o, property AS p, type AS t
|
||||
WHERE t.library_id=? AND o.ui_id=? AND o.owner_id = t.type_id AND o.owner_id = p.owner_id AND p.version IS NOT NULL
|
||||
WHERE t.library_id=? AND o.ui_id=? AND o.owner_id = t.type_id AND o.owner_id = p.owner_id
|
||||
AND p.version IS NOT NULL
|
||||
UNION
|
||||
SELECT s.version
|
||||
FROM object_signal AS o, signal AS s, type AS t
|
||||
WHERE t.library_id=? AND o.ui_id=? AND o.owner_id = t.type_id AND o.owner_id = s.owner_id AND s.version IS NOT NULL
|
||||
WHERE t.library_id=? AND o.ui_id=? AND o.owner_id = t.type_id AND o.owner_id = s.owner_id
|
||||
AND s.version IS NOT NULL
|
||||
)
|
||||
SELECT MAX_VERSION(version) FROM lib_version;
|
||||
""",
|
||||
@ -153,3 +186,37 @@ class CmbUI(CmbBaseUI):
|
||||
return info.min_version
|
||||
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbUIEditor - Cambalache UI Editor
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,9 +20,14 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
from cambalache import _
|
||||
from .cmb_ui import CmbUI
|
||||
|
||||
|
||||
@ -31,6 +36,7 @@ class CmbUIEditor(Gtk.Grid):
|
||||
__gtype_name__ = "CmbUIEditor"
|
||||
|
||||
filename = Gtk.Template.Child()
|
||||
format = Gtk.Template.Child()
|
||||
template_id = Gtk.Template.Child()
|
||||
description = Gtk.Template.Child()
|
||||
copyright = Gtk.Template.Child()
|
||||
@ -52,9 +58,6 @@ class CmbUIEditor(Gtk.Grid):
|
||||
|
||||
@object.setter
|
||||
def _set_object(self, obj):
|
||||
if obj == self._object:
|
||||
return
|
||||
|
||||
for binding in self._bindings:
|
||||
binding.unbind()
|
||||
|
||||
@ -67,7 +70,7 @@ class CmbUIEditor(Gtk.Grid):
|
||||
for field in self.fields:
|
||||
widget = getattr(self, field)
|
||||
|
||||
if type(widget.cmb_value) == int:
|
||||
if type(widget.cmb_value) is int:
|
||||
widget.cmb_value = 0
|
||||
else:
|
||||
widget.cmb_value = None
|
||||
@ -75,10 +78,17 @@ class CmbUIEditor(Gtk.Grid):
|
||||
|
||||
self.set_sensitive(True)
|
||||
self.template_id.object = obj
|
||||
self.filename.dirname = obj.project.dirname
|
||||
|
||||
# Set some default name
|
||||
self.filename.unnamed_filename = _("unnamed.ui")
|
||||
if not obj.filename and obj.template_id:
|
||||
template = obj.project.get_object_by_id(obj.ui_id, obj.template_id)
|
||||
if template:
|
||||
self.filename.unnamed_filename = f"{template.name}.ui".lower()
|
||||
|
||||
for field in self.fields:
|
||||
binding = GObject.Object.bind_property(
|
||||
obj,
|
||||
binding = obj.bind_property(
|
||||
field,
|
||||
getattr(self, field),
|
||||
"cmb-value",
|
||||
@ -86,34 +96,42 @@ class CmbUIEditor(Gtk.Grid):
|
||||
)
|
||||
self._bindings.append(binding)
|
||||
|
||||
@Gtk.Template.Callback("on_remove_button_clicked")
|
||||
def __on_remove_button_clicked(self, button):
|
||||
self.emit("remove-ui")
|
||||
if obj.project.target_tk == "gtk-4.0":
|
||||
self.filename.mime_types = "application/x-gtk-builder;text/x-blueprint"
|
||||
|
||||
@Gtk.Template.Callback("on_export_button_clicked")
|
||||
def __on_export_button_clicked(self, button):
|
||||
self.emit("export-ui")
|
||||
# filename -> format
|
||||
binding = obj.bind_property(
|
||||
"filename",
|
||||
self.format,
|
||||
"selected",
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
transform_to=self.__filename_to_format,
|
||||
transform_from=self.__format_to_filename,
|
||||
user_data=obj
|
||||
)
|
||||
self._bindings.append(binding)
|
||||
|
||||
@GObject.Signal(
|
||||
flags=GObject.SignalFlags.RUN_LAST,
|
||||
return_type=bool,
|
||||
arg_types=(),
|
||||
accumulator=GObject.signal_accumulator_true_handled,
|
||||
)
|
||||
def export_ui(self):
|
||||
if self.object:
|
||||
self.object.project.export_ui(self.object)
|
||||
self.format.show()
|
||||
self.format.set_sensitive(bool(obj.filename))
|
||||
else:
|
||||
self.filename.mime_types = "application/x-gtk-builder;application/x-glade"
|
||||
self.format.hide()
|
||||
|
||||
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(
|
||||
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 1 if source_value.endswith(".blp") else 0
|
||||
|
||||
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")
|
||||
|
@ -1,272 +1,213 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.97.1 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="gladecambalache" version="0.0"/>
|
||||
<!-- interface-name cmb_ui_editor.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.0"/>
|
||||
<object class="CmbTextBuffer" id="authors"/>
|
||||
<object class="CmbTextBuffer" id="comment"/>
|
||||
<object class="CmbTextBuffer" id="copyright"/>
|
||||
<object class="CmbTextBuffer" id="description"/>
|
||||
<!-- n-columns=2 n-rows=8 -->
|
||||
<template class="CmbUIEditor" parent="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="row-spacing">4</property>
|
||||
<property name="column-spacing">3</property>
|
||||
<property name="row-spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<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>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Description:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Copyright:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">4</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Authors:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">5</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Domain:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">6</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CmbEntry" id="filename">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<object class="CmbFileButton" id="filename">
|
||||
<property name="hexpand">True</property>
|
||||
<property name="placeholder-text" translatable="yes"><file name relative to project></property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">0</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CmbEntry" id="translation_domain">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="width-chars">16</property>
|
||||
<property name="placeholder-text" translatable="yes"><translation domain></property>
|
||||
<property name="visible">True</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">6</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">5</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkTextView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="buffer">description</property>
|
||||
<property name="focusable">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">3</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkTextView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="buffer">authors</property>
|
||||
<property name="focusable">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">5</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="CmbToplevelChooser" id="template_id">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="derivable-only">True</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="visible">True</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Template:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">2</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="valign">end</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="spacing">4</property>
|
||||
<child>
|
||||
<object class="GtkButton" id="remove_button">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<signal name="clicked" handler="on_remove_button_clicked" swapped="no"/>
|
||||
<child>
|
||||
<object class="GtkImage">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="icon-name">app-remove-symbolic</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="export_button">
|
||||
<property name="label" translatable="yes">Export</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Export</property>
|
||||
<property name="halign">start</property>
|
||||
<signal name="clicked" handler="on_export_button_clicked" swapped="no"/>
|
||||
<style>
|
||||
<class name="suggested-action"/>
|
||||
</style>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="pack-type">end</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">7</property>
|
||||
<property name="width">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkTextView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="buffer">copyright</property>
|
||||
<property name="focusable">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">4</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Comment:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">7</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<child>
|
||||
<property name="child">
|
||||
<object class="GtkTextView">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="buffer">comment</property>
|
||||
<property name="focusable">1</property>
|
||||
</object>
|
||||
</child>
|
||||
</property>
|
||||
<property name="focusable">1</property>
|
||||
<property name="max-content-height">256</property>
|
||||
<property name="min-content-height">96</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="row">7</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkLabel">
|
||||
<property name="halign">start</property>
|
||||
<property name="label" translatable="yes">Format:</property>
|
||||
<layout>
|
||||
<property name="column">0</property>
|
||||
<property name="row">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkDropDown" id="format">
|
||||
<property name="halign">start</property>
|
||||
<property name="model">
|
||||
<object class="GtkStringList">
|
||||
<property name="strings">Gtk Builder
|
||||
Blueprint</property>
|
||||
</object>
|
||||
</property>
|
||||
<layout>
|
||||
<property name="column">1</property>
|
||||
<property name="column-span">1</property>
|
||||
<property name="row">1</property>
|
||||
<property name="row-span">1</property>
|
||||
</layout>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">1</property>
|
||||
<property name="top-attach">6</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbUIRequiresEditor - Cambalache UI Requires Editor
|
||||
#
|
||||
# Copyright (C) 2023 Juan Pablo Ugarte
|
||||
# Copyright (C) 2023-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
|
||||
@ -20,10 +20,13 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
from .cmb_ui import CmbUI
|
||||
from . import utils
|
||||
|
||||
|
||||
class CmbUIRequiresEditor(Gtk.Grid):
|
||||
@ -71,7 +74,7 @@ class CmbUIRequiresEditor(Gtk.Grid):
|
||||
def __update(self):
|
||||
self.__combos = {}
|
||||
|
||||
for child in self.get_children():
|
||||
for child in utils.widget_get_children(self):
|
||||
self.remove(child)
|
||||
|
||||
if self.__object is None:
|
||||
|
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>
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbView - Cambalache View
|
||||
#
|
||||
# Copyright (C) 2021 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,66 +20,157 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import gi
|
||||
import os
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import warnings
|
||||
import fcntl
|
||||
import stat
|
||||
import atexit
|
||||
import shutil
|
||||
|
||||
from gi.repository import GObject, GLib, Gtk, WebKit2
|
||||
gi.require_version('Casilda', '0.1')
|
||||
from gi.repository import GObject, GLib, Gio, Gdk, Gtk, Casilda
|
||||
|
||||
from . import config
|
||||
from .cmb_ui import CmbUI
|
||||
from .cmb_object import CmbObject
|
||||
from .cmb_context_menu import CmbContextMenu
|
||||
from cambalache import getLogger, _
|
||||
from cambalache.cmb_blueprint import cmb_blueprint_decompile
|
||||
from . import utils
|
||||
from cambalache import getLogger, _, N_
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
basedir = os.path.dirname(__file__) or "."
|
||||
|
||||
GObject.type_ensure(WebKit2.Settings.__gtype__)
|
||||
GObject.type_ensure(WebKit2.WebView.__gtype__)
|
||||
GObject.type_ensure(Casilda.Compositor.__gtype__)
|
||||
|
||||
|
||||
class CmbProcess(GObject.Object):
|
||||
class CmbMerengueProcess(GObject.Object):
|
||||
__gsignals__ = {
|
||||
"stdout": (GObject.SignalFlags.RUN_LAST, bool, (GLib.IOCondition,)),
|
||||
"handle-command": (GObject.SignalFlags.RUN_LAST, None, (str,)),
|
||||
"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):
|
||||
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)
|
||||
|
||||
self.pid = 0
|
||||
self.stdin = None
|
||||
self.stdout = None
|
||||
@GObject.Property(type=str)
|
||||
def wayland_display(self):
|
||||
return self.__wayland_display
|
||||
|
||||
def stop(self):
|
||||
if self.stdin:
|
||||
self.stdin.shutdown(False)
|
||||
self.stdin = None
|
||||
@wayland_display.setter
|
||||
def _set_wayland_display(self, wayland_display):
|
||||
self.cleanup()
|
||||
|
||||
if self.stdout:
|
||||
self.stdout.shutdown(False)
|
||||
self.stdout = None
|
||||
self.__wayland_display = wayland_display
|
||||
|
||||
if self.pid:
|
||||
try:
|
||||
GLib.spawn_close_pid(self.pid)
|
||||
os.kill(self.pid, 9)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error stopping {self.file} {e}")
|
||||
|
||||
self.pid = 0
|
||||
|
||||
def run(self, args, env={}):
|
||||
if self.file is None or self.pid > 0:
|
||||
if wayland_display is None:
|
||||
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]
|
||||
|
||||
# Append extra vars
|
||||
@ -87,31 +178,86 @@ class CmbProcess(GObject.Object):
|
||||
envp.append(f"{var}={env[var]}")
|
||||
|
||||
pid, stdin, stdout, stderr = GLib.spawn_async(
|
||||
[self.file] + args,
|
||||
[self.__file, self.gtk_version, self.__command_socket],
|
||||
envp=envp,
|
||||
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)
|
||||
|
||||
def __on_exit(self, pid, status, data):
|
||||
self.stop()
|
||||
self.emit("exit")
|
||||
def __cleanup(self):
|
||||
self.merengue_started = False
|
||||
|
||||
def __on_stdout(self, channel, condition):
|
||||
return self.emit("stdout", condition)
|
||||
if self.__on_command_in_source:
|
||||
GLib.source_remove(self.__on_command_in_source)
|
||||
self.__on_command_in_source = None
|
||||
|
||||
if self.__command_in:
|
||||
self.__command_in = None
|
||||
|
||||
if self.__connection:
|
||||
self.__connection.close()
|
||||
self.__connection = None
|
||||
|
||||
def stop(self):
|
||||
self.__cleanup()
|
||||
|
||||
if self.__pid:
|
||||
try:
|
||||
GLib.spawn_close_pid(self.__pid)
|
||||
os.kill(self.__pid, 9)
|
||||
except Exception as e:
|
||||
logger.warning(f"Error stopping {self.__file} {e}")
|
||||
finally:
|
||||
self.__pid = 0
|
||||
|
||||
def write_command(self, command, payload=None, args=None):
|
||||
cmd = {"command": command}
|
||||
|
||||
if payload is not None:
|
||||
# Encode to binary first, before calculating lenght
|
||||
payload = payload.encode()
|
||||
cmd["payload_length"] = len(payload)
|
||||
logger.debug(f"write_command {command} {len(payload)}")
|
||||
|
||||
if args is not None:
|
||||
cmd["args"] = args
|
||||
|
||||
# Queue command while we are not connected
|
||||
if self.__connection is None:
|
||||
self.__command_queue.append((cmd, payload))
|
||||
return
|
||||
|
||||
self.__socket_write_command(cmd, payload)
|
||||
|
||||
def __socket_write_command(self, cmd, payload=None):
|
||||
# Send command in one line as json
|
||||
output_stream = self.__connection.props.output_stream
|
||||
|
||||
def write_data(data):
|
||||
total_bytes = len(data)
|
||||
total_sent = 0
|
||||
while total_sent < total_bytes:
|
||||
total_sent += output_stream.write(data[total_sent:])
|
||||
|
||||
write_data(json.dumps(cmd).encode())
|
||||
write_data(b"\n")
|
||||
|
||||
if payload is not None:
|
||||
write_data(payload)
|
||||
|
||||
# Flush
|
||||
output_stream.flush()
|
||||
|
||||
def __on_exit(self, pid, status, data):
|
||||
self.__cleanup()
|
||||
self.__pid = 0
|
||||
self.emit("exit")
|
||||
|
||||
|
||||
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui")
|
||||
class CmbView(Gtk.Stack):
|
||||
class CmbView(Gtk.Box):
|
||||
__gtype_name__ = "CmbView"
|
||||
|
||||
__gsignals__ = {
|
||||
@ -119,126 +265,106 @@ class CmbView(Gtk.Stack):
|
||||
"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)
|
||||
|
||||
webview = Gtk.Template.Child()
|
||||
stack = 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()
|
||||
db_inspector = Gtk.Template.Child()
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.__project = None
|
||||
self.__restart_project = None
|
||||
self.__ui_id = 0
|
||||
self.__ui = None
|
||||
self.__theme = None
|
||||
|
||||
self.menu = self.__create_context_menu()
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.__merengue_bin = os.path.join(config.merenguedir, "merengue", "merengue")
|
||||
self.__broadwayd_bin = GLib.find_program_in_path("broadwayd")
|
||||
self.__gtk4_broadwayd_bin = GLib.find_program_in_path("gtk4-broadwayd")
|
||||
|
||||
self.webview.connect("load-changed", self.__on_load_changed)
|
||||
|
||||
self.__merengue = None
|
||||
self.__broadwayd = None
|
||||
self.__port = None
|
||||
self.__merengue_last_exit = None
|
||||
|
||||
context = self.get_style_context()
|
||||
context.connect("changed", lambda ctx: self.__update_webview_bg())
|
||||
|
||||
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.")
|
||||
|
||||
GObject.Object.bind_property(
|
||||
self,
|
||||
"gtk-theme",
|
||||
self.menu,
|
||||
"gtk-theme",
|
||||
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
|
||||
self.__click_gesture = Gtk.GestureClick(
|
||||
propagation_phase=Gtk.PropagationPhase.CAPTURE,
|
||||
button=3
|
||||
)
|
||||
self.__click_gesture.connect("pressed", self.__on_click_gesture_pressed)
|
||||
self.compositor_box.add_controller(self.__click_gesture)
|
||||
|
||||
self.__merengue = CmbMerengueProcess(wayland_display=self.compositor.props.socket)
|
||||
self.__merengue.connect("exit", self.__on_process_exit)
|
||||
self.__merengue_last_exit = None
|
||||
|
||||
self.connect("notify::preview", self.__on_preview_notify)
|
||||
|
||||
def do_destroy(self):
|
||||
if self.__merengue:
|
||||
self.__merengue_command("quit")
|
||||
# Ensure we delete all socket files when exiting
|
||||
atexit.register(self.__atexit)
|
||||
|
||||
if self.__broadwayd:
|
||||
self.__broadwayd.stop()
|
||||
@Gtk.Template.Callback("on_restart_button_clicked")
|
||||
def __on_restart_button_clicked(self, button):
|
||||
self.restart_workspace()
|
||||
|
||||
def __evaluate_js(self, script):
|
||||
self.webview.evaluate_javascript(script, -1, None, None, None, None, None, None)
|
||||
def __atexit(self):
|
||||
dirname = os.path.dirname(self.compositor.props.socket)
|
||||
|
||||
def __update_webview_bg(self):
|
||||
context = self.get_style_context()
|
||||
self.__merengue_command("quit")
|
||||
self.__merengue.cleanup()
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||||
bg = context.get_background_color(Gtk.StateFlags.NORMAL)
|
||||
if os.path.exists(dirname):
|
||||
shutil.rmtree(dirname)
|
||||
|
||||
self.__evaluate_js(f"document.body.style.background = '{bg.to_string()}';")
|
||||
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
|
||||
|
||||
def __on_load_changed(self, webview, event):
|
||||
if event != WebKit2.LoadEvent.FINISHED:
|
||||
return
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
self.__update_webview_bg()
|
||||
|
||||
# Disable alert() function used when broadwayd get disconnected
|
||||
# Monkey patch 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 _set_dark_mode(self, dark):
|
||||
# This needs to be called in an idle because theme_bg_color has not changed at this point
|
||||
GLib.idle_add(self.__set_dark_mode, dark)
|
||||
|
||||
def __merengue_command(self, command, payload=None, args=None):
|
||||
if self.__merengue is None or self.__merengue.stdin is None:
|
||||
return
|
||||
|
||||
cmd = {"command": command, "payload": payload is not None}
|
||||
|
||||
if args is not None:
|
||||
cmd["args"] = args
|
||||
|
||||
# Send command in one line as json
|
||||
self.__merengue.stdin.write(json.dumps(cmd))
|
||||
self.__merengue.stdin.write("\n")
|
||||
|
||||
if payload is not None:
|
||||
self.__merengue.stdin.write(GLib.strescape(payload))
|
||||
self.__merengue.stdin.write("\n")
|
||||
|
||||
# Flush
|
||||
self.__merengue.stdin.flush()
|
||||
if self.__merengue.merengue_started:
|
||||
self.__merengue.write_command(command, payload, args)
|
||||
|
||||
def __get_ui_xml(self, ui_id, merengue=False):
|
||||
if self.show_merengue:
|
||||
merengue = True
|
||||
|
||||
return self.__project.db.tostring(ui_id, merengue=merengue)
|
||||
|
||||
def __update_view(self):
|
||||
if self.__project is not None and self.__ui_id > 0:
|
||||
if self.props.visible_child_name == "ui_xml":
|
||||
ui = self.__get_ui_xml(self.__ui_id)
|
||||
self.text_view.buffer.set_text(ui)
|
||||
if self.__project and self.__ui:
|
||||
if self.stack.props.visible_child_name == "ui_xml":
|
||||
ui_source = self.__get_ui_xml(self.__ui.ui_id)
|
||||
|
||||
if self.__ui.filename and self.__ui.filename.endswith(".blp"):
|
||||
try:
|
||||
ui_source = cmb_blueprint_decompile(ui_source)
|
||||
self.text_view.lang = "blueprint"
|
||||
except Exception as e:
|
||||
ui_source = _("Error exporting project")
|
||||
ui_source += "\n"
|
||||
ui_source += N_(
|
||||
"blueprintcompiler encounter the following error:",
|
||||
"blueprintcompiler encounter the following errors:",
|
||||
len(e.errors)
|
||||
)
|
||||
ui_source += "\n"
|
||||
ui_source += str(e)
|
||||
self.text_view.lang = ""
|
||||
# TODO: forward error to parent to show to user
|
||||
else:
|
||||
self.text_view.lang = "xml"
|
||||
|
||||
self.text_view.buffer.set_text(ui_source)
|
||||
return
|
||||
|
||||
self.text_view.buffer.set_text("")
|
||||
self.__ui_id = 0
|
||||
self.__ui = None
|
||||
|
||||
def __get_ui_dirname(self, ui_id):
|
||||
dirname = GLib.get_home_dir()
|
||||
@ -257,7 +383,7 @@ window.setupDocument = function (document) {
|
||||
return dirname
|
||||
|
||||
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)
|
||||
selection = self.__project.get_selection()
|
||||
objects = self.__get_selection_objects(selection, ui_id)
|
||||
@ -287,11 +413,17 @@ window.setupDocument = function (document) {
|
||||
self.__merengue_update_ui(obj.ui_id)
|
||||
|
||||
def __on_object_changed(self, project, obj, field):
|
||||
if field in ["type", "position", "custom-fragment"]:
|
||||
if field in ["type", "position", "custom-fragment", "parent-id"]:
|
||||
self.__merengue_update_ui(obj.ui_id)
|
||||
|
||||
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)
|
||||
return
|
||||
|
||||
@ -325,7 +457,7 @@ window.setupDocument = function (document) {
|
||||
objects = []
|
||||
|
||||
for obj in selection:
|
||||
if type(obj) == CmbObject and obj.ui_id == ui_id:
|
||||
if type(obj) is CmbObject and obj.ui_id == ui_id:
|
||||
objects.append(obj.object_id)
|
||||
|
||||
return objects
|
||||
@ -336,25 +468,33 @@ window.setupDocument = function (document) {
|
||||
if len(selection) > 0:
|
||||
obj = selection[0]
|
||||
|
||||
if type(obj) not in [CmbUI, CmbObject]:
|
||||
if isinstance(obj, CmbUI):
|
||||
ui = obj
|
||||
elif isinstance(obj, CmbObject):
|
||||
ui = obj.ui
|
||||
else:
|
||||
return
|
||||
|
||||
ui_id = obj.ui_id
|
||||
|
||||
if self.__ui_id != ui_id:
|
||||
self.__ui_id = ui_id
|
||||
self.__merengue_update_ui(ui_id)
|
||||
if self.__ui != ui:
|
||||
self.__ui = ui
|
||||
self.__merengue_update_ui(ui.ui_id)
|
||||
|
||||
objects = self.__get_selection_objects(selection, ui_id)
|
||||
objects = self.__get_selection_objects(selection, ui.ui_id)
|
||||
self.__merengue_command("selection_changed", args={"ui_id": ui_id, "selection": objects})
|
||||
else:
|
||||
self.__ui_id = 0
|
||||
self.__ui = None
|
||||
self.__merengue_update_ui(0)
|
||||
|
||||
def __on_css_added(self, project, obj):
|
||||
dirname = os.path.dirname(self.project.filename)
|
||||
self.__update_view()
|
||||
|
||||
filename = os.path.join(dirname, obj.filename) if obj.filename else None
|
||||
def __on_css_added(self, project, obj):
|
||||
if self.project.filename and obj.filename:
|
||||
dirname = os.path.dirname(self.project.filename)
|
||||
filename = os.path.join(dirname, obj.filename)
|
||||
else:
|
||||
filename = None
|
||||
|
||||
self.__merengue_command(
|
||||
"add_css_provider",
|
||||
@ -391,13 +531,26 @@ window.setupDocument = function (document) {
|
||||
def __on_object_data_arg_changed(self, project, data, value):
|
||||
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)
|
||||
def project(self):
|
||||
return self.__project
|
||||
|
||||
@project.setter
|
||||
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_ui_changed)
|
||||
self.__project.disconnect_by_func(self.__on_object_added)
|
||||
@ -410,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_data_removed)
|
||||
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.__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_removed)
|
||||
self.__project.disconnect_by_func(self.__on_css_changed)
|
||||
self.__merengue.disconnect_by_func(self.__on_merengue_handle_command)
|
||||
self.__merengue.stop()
|
||||
self.__broadwayd.stop()
|
||||
|
||||
self.__project = project
|
||||
self.db_inspector.project = project
|
||||
|
||||
self.__update_view()
|
||||
|
||||
if project is not None:
|
||||
if project:
|
||||
project.connect("changed", self.__on_changed)
|
||||
project.connect("ui-changed", self.__on_ui_changed)
|
||||
project.connect("object-added", self.__on_object_added)
|
||||
@ -435,28 +589,25 @@ window.setupDocument = function (document) {
|
||||
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-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("css-added", self.__on_css_added)
|
||||
project.connect("css-removed", self.__on_css_removed)
|
||||
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)
|
||||
self.__merengue.connect("stdout", self.__on_merengue_stdout)
|
||||
self.__merengue.connect("exit", self.__on_process_exit)
|
||||
# Run view process
|
||||
if project.target_tk == "gtk+-3.0":
|
||||
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)
|
||||
|
||||
broadwayd = self.__gtk4_broadwayd_bin if self.__project.target_tk == "gtk-4.0" else self.__broadwayd_bin
|
||||
self.__broadwayd = CmbProcess(file=broadwayd)
|
||||
self.__broadwayd.connect("stdout", self.__on_broadwayd_stdout)
|
||||
self.__broadwayd.connect("exit", self.__on_process_exit)
|
||||
|
||||
self.__port = self.__find_free_port()
|
||||
display = self.__port - 8080
|
||||
self.__broadwayd.run([f":{display}"])
|
||||
# Clear any error
|
||||
self.__set_error_message(None)
|
||||
self.__merengue.start()
|
||||
|
||||
# Update css themes
|
||||
self.menu.target_tk = self.__project.target_tk
|
||||
self.menu.target_tk = project.target_tk
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def gtk_theme(self):
|
||||
@ -467,72 +618,46 @@ window.setupDocument = function (document) {
|
||||
self.__theme = theme
|
||||
self.__merengue_command("gtk_settings_set", args={"property": "gtk-theme-name", "value": theme})
|
||||
|
||||
@Gtk.Template.Callback("on_context_menu")
|
||||
def __on_context_menu(self, webview, menu, e, hit_test_result):
|
||||
self.menu.popup_at(e.x, e.y)
|
||||
return True
|
||||
def __on_click_gesture_pressed(self, gesture, n_press, x, y):
|
||||
if gesture.get_current_button() == 3:
|
||||
self.menu.popup_at(x, y)
|
||||
|
||||
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 __on_inspect_button_clicked(self, button):
|
||||
self.props.visible_child_name = "ui_xml"
|
||||
def inspect(self):
|
||||
self.stack.props.visible_child_name = "ui_xml"
|
||||
self.__update_view()
|
||||
|
||||
def __on_restart_button_clicked(self, button):
|
||||
self.__restart_project = self.__project
|
||||
self.project = None
|
||||
def restart_workspace(self):
|
||||
# Clear last exit timestamp
|
||||
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):
|
||||
retval = CmbContextMenu(relative_to=self)
|
||||
retval = CmbContextMenu(enable_theme=True)
|
||||
retval.set_parent(self)
|
||||
|
||||
restart = Gtk.ModelButton(text=_("Restart workspace"), visible=True)
|
||||
restart.connect("clicked", self.__on_restart_button_clicked)
|
||||
|
||||
inspect = Gtk.ModelButton(text=_("Inspect UI definition"), visible=True)
|
||||
inspect.connect("clicked", self.__on_inspect_button_clicked)
|
||||
|
||||
retval.main_box.add(restart)
|
||||
retval.main_box.add(inspect)
|
||||
retval.main_section.append(_("Restart workspace"), "win.workspace_restart")
|
||||
retval.main_section.append(_("Inspect UI definition"), "win.inspect")
|
||||
|
||||
return retval
|
||||
|
||||
def __on_process_exit(self, process):
|
||||
if process == self.__merengue:
|
||||
if self.__merengue_last_exit is None:
|
||||
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
|
||||
if self.__merengue_last_exit is None:
|
||||
self.__merengue_last_exit = time.monotonic()
|
||||
else:
|
||||
self.__restart_project = self.__project
|
||||
self.project = None
|
||||
# Stop auto restart if Merengue exited less than 2 seconds ago
|
||||
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):
|
||||
objects = []
|
||||
@ -547,8 +672,11 @@ window.setupDocument = function (document) {
|
||||
if self.project is None:
|
||||
return
|
||||
|
||||
for id in self.project.library_info:
|
||||
info = self.project.library_info[id]
|
||||
for id, info in self.project.library_info.items():
|
||||
# Only load 3rd party libraries, Gtk ones are already loaded
|
||||
if not info.third_party:
|
||||
continue
|
||||
|
||||
self.__merengue_command(
|
||||
"load_namespace",
|
||||
args={
|
||||
@ -567,109 +695,50 @@ window.setupDocument = function (document) {
|
||||
for css in providers:
|
||||
self.__on_css_added(self.project, css)
|
||||
|
||||
def __on_merengue_stdout(self, process, condition):
|
||||
if condition == GLib.IOCondition.HUP:
|
||||
self.__merengue.stop()
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
if self.__merengue.stdout is None:
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
retval = self.__merengue.stdout.readline()
|
||||
cmd = None
|
||||
|
||||
def __on_merengue_handle_command(self, merengue, payload):
|
||||
try:
|
||||
cmd = json.loads(retval)
|
||||
cmd = json.loads(payload)
|
||||
command = cmd.get("command", None)
|
||||
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:
|
||||
logger.warning(f"Merenge output error: {e}")
|
||||
logger.warning(f"Merengue command error: {e}")
|
||||
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):
|
||||
if condition == GLib.IOCondition.HUP:
|
||||
self.__broadwayd.stop()
|
||||
return GLib.SOURCE_REMOVE
|
||||
self.__load_namespaces()
|
||||
|
||||
if self.__broadwayd.stdout is None:
|
||||
return GLib.SOURCE_REMOVE
|
||||
self.__load_css_providers()
|
||||
|
||||
status, retval, length, terminator = self.__broadwayd.stdout.read_line()
|
||||
# path = retval.replace("Listening on ", "").strip()
|
||||
|
||||
# Run view process
|
||||
if self.__project.target_tk == "gtk+-3.0":
|
||||
version = "3.0"
|
||||
elif self.__project.target_tk == "gtk-4.0":
|
||||
version = "4.0"
|
||||
|
||||
display = self.__port - 8080
|
||||
|
||||
env = json.loads(os.environ.get("MERENGUE_DEV_ENV", "{}"))
|
||||
self.__merengue.run(
|
||||
[version],
|
||||
env
|
||||
| {
|
||||
"GDK_BACKEND": "broadway",
|
||||
# 'GTK_DEBUG': 'interactive',
|
||||
"BROADWAY_DISPLAY": f":{display}",
|
||||
},
|
||||
)
|
||||
|
||||
# Load broadway desktop
|
||||
self.webview.load_uri(f"http://127.0.0.1:{self.__port}")
|
||||
|
||||
self.__broadwayd.stdout.shutdown(False)
|
||||
self.__broadwayd.stdout = None
|
||||
return GLib.SOURCE_REMOVE
|
||||
|
||||
def __find_free_port(self):
|
||||
for port in range(8080, 8180):
|
||||
s = socket.socket()
|
||||
retval = s.connect_ex(("127.0.0.1", port))
|
||||
s.close()
|
||||
|
||||
if retval != 0:
|
||||
return port
|
||||
|
||||
return 0
|
||||
self.__ui = None
|
||||
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")
|
||||
|
||||
def __add_remove_placeholder(self, command, modifier):
|
||||
if self.project is None:
|
||||
@ -690,3 +759,4 @@ window.setupDocument = function (document) {
|
||||
|
||||
|
||||
Gtk.WidgetClass.set_css_name(CmbView, "CmbView")
|
||||
|
||||
|
@ -1,87 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.38.2 -->
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<!-- Created with Cambalache 0.95.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="webkit2gtk" version="2.28"/>
|
||||
<object class="WebKitSettings" type-func="webkit_settings_get_type" id="settings">
|
||||
<property name="enable-html5-local-storage">False</property>
|
||||
<property name="enable-html5-database">False</property>
|
||||
<property name="enable-java">False</property>
|
||||
<property name="enable-fullscreen">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>
|
||||
<property name="enable-accelerated-2d-canvas">True</property>
|
||||
<property name="enable-media">False</property>
|
||||
</object>
|
||||
<template class="CmbView" parent="GtkStack">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="events">GDK_BUTTON_RELEASE_MASK | GDK_STRUCTURE_MASK</property>
|
||||
<property name="transition-duration">300</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<!-- interface-name cmb_view.ui -->
|
||||
<!-- interface-copyright Juan Pablo Ugarte -->
|
||||
<requires lib="gtk" version="4.14"/>
|
||||
<template class="CmbView" parent="GtkBox">
|
||||
<child>
|
||||
<object class="WebKitWebView" type-func="webkit_web_view_get_type" id="webview">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="settings">settings</property>
|
||||
<signal name="context-menu" handler="on_context_menu" swapped="no"/>
|
||||
<object class="GtkStack" id="stack">
|
||||
<property name="hexpand">true</property>
|
||||
<property name="transition-duration">300</property>
|
||||
<property name="transition-type">crossfade</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">ui_view</property>
|
||||
<property name="title" translatable="yes">Project View</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="CmbSourceView" id="text_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="cursor-visible">False</property>
|
||||
<property name="lang">xml</property>
|
||||
<object class="GtkStackPage">
|
||||
<property name="child">
|
||||
<object class="GtkBox" id="compositor_box">
|
||||
<child>
|
||||
<object class="GtkGraphicsOffload" id="compositor_offload">
|
||||
<property name="child">
|
||||
<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>
|
||||
</child>
|
||||
</property>
|
||||
<property name="name">ui_view</property>
|
||||
<property name="title" translatable="yes">Project View</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkStackSwitcher">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="halign">center</property>
|
||||
<property name="margin-top">4</property>
|
||||
<property name="margin-bottom">4</property>
|
||||
<property name="stack">CmbView</property>
|
||||
<object class="GtkStackPage">
|
||||
<property name="child">
|
||||
<object class="GtkBox">
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="focusable">1</property>
|
||||
<property name="vexpand">1</property>
|
||||
<child>
|
||||
<object class="CmbSourceView" id="text_view">
|
||||
<property name="can-focus">True</property>
|
||||
<property name="cursor-visible">False</property>
|
||||
<property name="editable">False</property>
|
||||
<property name="lang">xml</property>
|
||||
</object>
|
||||
</child>
|
||||
</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">ui_xml</property>
|
||||
<property name="title" translatable="yes">UI Definition</property>
|
||||
</object>
|
||||
</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>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="name">ui_xml</property>
|
||||
<property name="title" translatable="yes">UI Definition</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</template>
|
||||
</interface>
|
||||
|
@ -1,5 +1,5 @@
|
||||
VERSION = '@VERSION@'
|
||||
FILE_FORMAT_VERSION = '@fileformatversion@'
|
||||
pkgdatadir = '@pkgdatadir@'
|
||||
catalogsdir = '@catalogsdir@'
|
||||
merenguedir = '@merenguedir@'
|
||||
catalogsdir = '@catalogsdir@'
|
||||
|
@ -19,6 +19,8 @@
|
||||
# Authors:
|
||||
# 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()
|
||||
# It is not a valid GType name on purpose since it will never be exported.
|
||||
|
@ -19,9 +19,13 @@
|
||||
# Authors:
|
||||
# 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_entry import CmbEntry
|
||||
from .cmb_file_button import CmbFileButton
|
||||
from .cmb_file_entry import CmbFileEntry
|
||||
from .cmb_icon_name_entry import CmbIconNameEntry
|
||||
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_flags_entry import CmbFlagsEntry
|
||||
from .cmb_object_chooser import CmbObjectChooser
|
||||
from .cmb_object_list_editor import CmbObjectListEditor
|
||||
from .cmb_source_view import CmbSourceView
|
||||
from .cmb_switch import CmbSwitch
|
||||
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
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbChildTypeComboBox
|
||||
#
|
||||
# Copyright (C) 2021-2023 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,8 +20,9 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from cambalache import utils
|
||||
from gi.repository import GObject, Gtk
|
||||
|
||||
|
||||
@ -33,8 +34,6 @@ class CmbChildTypeComboBox(Gtk.ComboBox):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
utils.unset_scroll_event(self)
|
||||
|
||||
self.connect("changed", self.__on_changed)
|
||||
|
||||
# Model, store it in a Python variable to make sure we hold a reference
|
||||
|
@ -20,8 +20,16 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import Gdk, GObject, Gtk
|
||||
from gi.repository import Gdk, GObject, Gtk, Pango
|
||||
|
||||
from .named_colors import named_colors
|
||||
|
||||
|
||||
# Reverse color maps
|
||||
_named_colors = {v: k for k, v in named_colors.items()}
|
||||
|
||||
|
||||
class CmbColorEntry(Gtk.Box):
|
||||
@ -34,52 +42,106 @@ class CmbColorEntry(Gtk.Box):
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.entry = Gtk.Entry(visible=True, width_chars=14, editable=False)
|
||||
self.button = Gtk.ColorButton(visible=True, use_alpha=True)
|
||||
self.entry = Gtk.Entry(width_chars=14)
|
||||
self.button = Gtk.ColorDialogButton(dialog=Gtk.ColorDialog())
|
||||
|
||||
self.__default_color = self.button.props.color
|
||||
self.__default_rgba = self.button.props.rgba
|
||||
self.__default_color = Gdk.RGBA()
|
||||
self.__default_color.parse("black")
|
||||
self.__use_named_color = False
|
||||
self.__in_sync = False
|
||||
|
||||
self.pack_start(self.entry, False, True, 0)
|
||||
self.pack_start(self.button, False, True, 4)
|
||||
self.append(self.entry)
|
||||
self.append(self.button)
|
||||
|
||||
self.button.connect("color-set", self.__on_color_set)
|
||||
self.button.connect("notify::rgba", self.__on_color_set)
|
||||
self.entry.connect("icon-press", self.__on_entry_icon_pressed)
|
||||
self.entry.connect("activate", self.__on_entry_activate)
|
||||
|
||||
def __on_entry_icon_pressed(self, widget, icon_pos, event):
|
||||
def __on_entry_icon_pressed(self, widget, icon_pos):
|
||||
print("__on_entry_icon_pressed")
|
||||
self.cmb_value = None
|
||||
|
||||
def __on_color_set(self, obj):
|
||||
def __on_entry_activate(self, entry):
|
||||
print("__on_entry_activate")
|
||||
self.cmb_value = self.cmb_value
|
||||
|
||||
def __on_color_set(self, obj, pspec):
|
||||
if self.__in_sync:
|
||||
return
|
||||
print("__on_color_set")
|
||||
rgba = self.button.props.rgba
|
||||
color = None
|
||||
|
||||
if rgba and self.__use_named_color:
|
||||
color = self.rgba_to_hex(rgba)
|
||||
|
||||
if color in _named_colors:
|
||||
self.cmb_value = _named_colors[color]
|
||||
return
|
||||
|
||||
if self.use_color:
|
||||
self.cmb_value = self.button.props.color.to_string() if self.button.props.color else None
|
||||
if color is None:
|
||||
color = self.rgba_to_hex(rgba)
|
||||
|
||||
self.cmb_value = color
|
||||
else:
|
||||
self.cmb_value = self.button.props.rgba.to_string() if self.button.props.rgba else None
|
||||
self.cmb_value = rgba.to_string() if rgba else None
|
||||
|
||||
@GObject.Property(type=str)
|
||||
def cmb_value(self):
|
||||
return self.entry.props.text if self.entry.props.text != "" else None
|
||||
|
||||
def rgba_to_hex(self, rgba):
|
||||
if rgba and self.__use_named_color:
|
||||
r = format(int(rgba.red * 255), "X")
|
||||
g = format(int(rgba.green * 255), "X")
|
||||
b = format(int(rgba.blue * 255), "X")
|
||||
|
||||
return f"#{r}{g}{b}"
|
||||
|
||||
return None
|
||||
|
||||
def parse_gdk_color(self, color):
|
||||
c = Pango.Color()
|
||||
|
||||
# Both gdk_rgba_parse() and gdk_color_parse() use pango_color_parse()
|
||||
valid = c.parse(color)
|
||||
|
||||
if valid:
|
||||
rgba = Gdk.RGBA()
|
||||
|
||||
rgba.red = c.red / 65535.0
|
||||
rgba.green = c.green / 65535.0
|
||||
rgba.blue = c.blue / 65535.0
|
||||
rgba.alpha = 1.0
|
||||
|
||||
return (True, rgba)
|
||||
|
||||
return (False, None)
|
||||
|
||||
@cmb_value.setter
|
||||
def _set_cmb_value(self, value):
|
||||
if value:
|
||||
valid = False
|
||||
rgba = None
|
||||
|
||||
self.__in_sync = True
|
||||
|
||||
self.__use_named_color = value in named_colors
|
||||
|
||||
if value is not None:
|
||||
if self.use_color:
|
||||
valid, rgba = self.parse_gdk_color(value)
|
||||
else:
|
||||
rgba = Gdk.RGBA()
|
||||
valid = rgba.parse(value)
|
||||
|
||||
self.button.set_rgba(rgba if valid and rgba is not None else self.__default_color)
|
||||
|
||||
if valid and value:
|
||||
self.entry.props.text = value
|
||||
self.entry.props.secondary_icon_name = "edit-clear-symbolic"
|
||||
else:
|
||||
self.entry.props.text = ""
|
||||
self.entry.props.secondary_icon_name = None
|
||||
|
||||
valid = False
|
||||
|
||||
if self.use_color:
|
||||
color = None
|
||||
if value:
|
||||
valid, color = Gdk.Color.parse(value)
|
||||
|
||||
self.button.set_color(color if valid else self.__default_color)
|
||||
else:
|
||||
rgba = Gdk.RGBA()
|
||||
|
||||
if value:
|
||||
valid = rgba.parse(value)
|
||||
|
||||
self.button.set_rgba(rgba if valid else self.__default_rgba)
|
||||
self.__in_sync = False
|
||||
|
@ -19,11 +19,14 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
import math
|
||||
|
||||
from gi.repository import GLib, Gtk
|
||||
from .cmb_boolean_undefined import CmbBooleanUndefined
|
||||
from .cmb_entry import CmbEntry
|
||||
from .cmb_file_entry import CmbFileEntry
|
||||
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_flags_entry import CmbFlagsEntry
|
||||
from .cmb_object_chooser import CmbObjectChooser
|
||||
from .cmb_object_list_editor import CmbObjectListEditor
|
||||
from .cmb_switch import CmbSwitch
|
||||
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):
|
||||
if type_id == "gchar":
|
||||
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)
|
||||
|
||||
editor = CmbSpinButton(digits=digits, adjustment=adjustment)
|
||||
elif type_id == "GBytes":
|
||||
editor = CmbTextView(hexpand=True)
|
||||
elif type_id == "GStrv":
|
||||
editor = CmbTextView(hexpand=True)
|
||||
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>")
|
||||
elif type_id in ["GtkShortcutTrigger", "GtkShortcutAction"]:
|
||||
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:
|
||||
if info.is_object or info.parent_id == "interface":
|
||||
# TODO: replace prop with project and is_inline
|
||||
editor = CmbObjectChooser(parent=prop.object, prop=prop)
|
||||
if info.parent_id == "enum":
|
||||
if prop is None:
|
||||
editor = CmbObjectChooser(
|
||||
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)
|
||||
elif info.parent_id == "flags":
|
||||
editor = CmbFlagsEntry(info=info)
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbEntry
|
||||
#
|
||||
# Copyright (C) 2021-2023 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,6 +20,8 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
from .cmb_translatable_popover import CmbTranslatablePopover
|
||||
@ -37,8 +39,9 @@ class CmbEntry(Gtk.Entry):
|
||||
self.props.secondary_icon_name = "document-edit-symbolic"
|
||||
self.connect("icon-press", self.__on_icon_pressed)
|
||||
|
||||
def __on_icon_pressed(self, widget, icon_pos, event):
|
||||
popover = CmbTranslatablePopover(relative_to=self)
|
||||
def __on_icon_pressed(self, widget, icon_pos):
|
||||
popover = CmbTranslatablePopover()
|
||||
popover.set_parent(self)
|
||||
popover.bind_properties(self._target)
|
||||
popover.popup()
|
||||
|
||||
@ -51,4 +54,8 @@ class CmbEntry(Gtk.Entry):
|
||||
|
||||
@cmb_value.setter
|
||||
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 ""
|
||||
|
@ -1,7 +1,7 @@
|
||||
#
|
||||
# CmbEnumComboBox
|
||||
#
|
||||
# Copyright (C) 2021-2023 Juan Pablo Ugarte
|
||||
# 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
|
||||
@ -20,16 +20,17 @@
|
||||
# Authors:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
from gi.repository import GObject, Gtk
|
||||
from ..cmb_type_info import CmbTypeInfo
|
||||
from cambalache import utils
|
||||
|
||||
|
||||
class CmbEnumComboBox(Gtk.ComboBox):
|
||||
__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)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@ -41,9 +42,9 @@ class CmbEnumComboBox(Gtk.ComboBox):
|
||||
self.add_attribute(renderer_text, "text", self.text_column)
|
||||
|
||||
self.props.id_column = self.text_column
|
||||
self.props.model = self.info.enum
|
||||
|
||||
utils.unset_scroll_event(self)
|
||||
if self.info:
|
||||
self.props.model = self.info.enum
|
||||
|
||||
def __on_changed(self, obj):
|
||||
self.notify("cmb-value")
|
||||
@ -54,12 +55,13 @@ class CmbEnumComboBox(Gtk.ComboBox):
|
||||
|
||||
@cmb_value.setter
|
||||
def _set_cmb_value(self, value):
|
||||
if self.info is None:
|
||||
return
|
||||
|
||||
self.props.active_id = None
|
||||
active_id = self.info.enum_get_value_as_string(value)
|
||||
|
||||
for row in self.info.enum:
|
||||
enum_name = row[0]
|
||||
enum_nick = row[1]
|
||||
if active_id == self.props.active_id:
|
||||
return
|
||||
|
||||
# Always use nick as value
|
||||
if value == enum_name or value == enum_nick:
|
||||
self.props.active_id = enum_nick
|
||||
self.props.active_id = active_id
|
||||
|
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:
|
||||
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
|
||||
#
|
||||
# SPDX-License-Identifier: LGPL-2.1-only
|
||||
#
|
||||
|
||||
import os
|
||||
|
||||
from cambalache import _
|
||||
from gi.repository import GObject, Gtk
|
||||
from gi.repository import GObject, Gio, Gtk
|
||||
|
||||
|
||||
class CmbFileEntry(Gtk.Entry):
|
||||
@ -35,7 +37,7 @@ class CmbFileEntry(Gtk.Entry):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self.title = (_("Select File"),)
|
||||
self.title = _("Select File")
|
||||
self.filter = None
|
||||
self.props.placeholder_text = "<GFile>"
|
||||
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("icon-press", self.__on_icon_pressed)
|
||||
|
||||
def __on_icon_pressed(self, widget, icon_pos, event):
|
||||
# Create Open Dialog
|
||||
dialog = Gtk.FileChooserNative(
|
||||
title=self.title, transient_for=self.get_toplevel(), action=Gtk.FileChooserAction.OPEN, filter=self.filter
|
||||
def __on_icon_pressed(self, widget, icon_pos):
|
||||
dialog = Gtk.FileDialog(
|
||||
modal=True,
|
||||
title=self.title,
|
||||
default_filter=self.filter,
|
||||
)
|
||||
|
||||
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:
|
||||
self.props.text = os.path.relpath(dialog.get_filename(), start=self.dirname)
|
||||
def dialog_callback(dialog, res):
|
||||
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):
|
||||
self.notify("cmb-value")
|
||||
@ -66,4 +73,6 @@ class CmbFileEntry(Gtk.Entry):
|
||||
|
||||
@cmb_value.setter
|
||||
def _set_cmb_value(self, value):
|
||||
if value == self.props.text:
|
||||
return
|
||||
self.props.text = value if value is not None else ""
|
||||
|
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