Compare commits

..

No commits in common. "main" and "screenshot-0.90.0" have entirely different histories.

326 changed files with 0 additions and 71244 deletions

18
.flake8
View File

@ -1,18 +0,0 @@
[flake8]
max-line-length = 128
extend-ignore = E203
per-file-ignores =
cambalache/__init__.py:F401,E402
cambalache/app/__init__.py:F401,E402
cambalache/control/__init__.py:F401,E402
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
exclude =
.flatpak-builder
build
_build

28
.gitignore vendored
View File

@ -1,28 +0,0 @@
*.pyc
.pytest_cache
__pycache__
.flatpak-builder
.catalogs
.local
.lc_messages
.vscode
.env.local
.coverage*
build
repo
cambalache.flatpak
cambalache/cambalache.gresource
cambalache/merengue.gresource
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
tools/CmbUtils-4.0.typelib
tools/libcmbutils3.so
tools/libcmbutils4.so
data/gschemas.compiled
data/mime

3
.gitmodules vendored
View File

@ -1,3 +0,0 @@
[submodule "tests/images"]
path = tests/images
url = https://gitlab.gnome.org/jpu/cambalache-test-images.git

View File

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

459
COPYING
View File

@ -1,459 +0,0 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS

View File

@ -1,280 +0,0 @@
GNU GENERAL PUBLIC LICENSE
Version 2, June 1991
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
License is intended to guarantee your freedom to share and change free
software--to make sure the software is free for all its users. This
General Public License applies to most of the Free Software
Foundation's software and to any other program whose authors commit to
using it. (Some other Free Software Foundation software is covered by
the GNU Lesser General Public License instead.) You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
this service if you wish), that you receive source code or can get it
if you want it, that you can change the software or use pieces of it
in new free programs; and that you know you can do these things.
To protect your rights, we need to make restrictions that forbid
anyone to deny you these rights or to ask you to surrender the rights.
These restrictions translate to certain responsibilities for you if you
distribute copies of the software, or if you modify it.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must give the recipients all the rights that
you have. You must make sure that they, too, receive or can get the
source code. And you must show them these terms so they know their
rights.
We protect your rights with two steps: (1) copyright the software, and
(2) offer you this license which gives you legal permission to copy,
distribute and/or modify the software.
Also, for each author's protection and ours, we want to make certain
that everyone understands that there is no warranty for this free
software. If the software is modified by someone else and passed on, we
want its recipients to know that what they have is not the original, so
that any problems introduced by others will not reflect on the original
authors' reputations.
Finally, any free program is threatened constantly by software
patents. We wish to avoid the danger that redistributors of a free
program will individually obtain patent licenses, in effect making the
program proprietary. To prevent this, we have made it clear that any
patent must be licensed for everyone's free use or not licensed at all.
The precise terms and conditions for copying, distribution and
modification follow.
GNU GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License applies to any program or other work which contains
a notice placed by the copyright holder saying it may be distributed
under the terms of this General Public License. The "Program", below,
refers to any such program or work, and a "work based on the Program"
means either the Program or any derivative work under copyright law:
that is to say, a work containing the Program or a portion of it,
either verbatim or with modifications and/or translated into another
language. (Hereinafter, translation is included without limitation in
the term "modification".) Each licensee is addressed as "you".
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running the Program is not restricted, and the output from the Program
is covered only if its contents constitute a work based on the
Program (independent of having been made by running the Program).
Whether that is true depends on what the Program does.
1. You may copy and distribute verbatim copies of the Program's
source code as you receive it, in any medium, provided that you
conspicuously and appropriately publish on each copy an appropriate
copyright notice and disclaimer of warranty; keep intact all the
notices that refer to this License and to the absence of any warranty;
and give any other recipients of the Program a copy of this License
along with the Program.
You may charge a fee for the physical act of transferring a copy, and
you may at your option offer warranty protection in exchange for a fee.
2. You may modify your copy or copies of the Program or any portion
of it, thus forming a work based on the Program, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) You must cause the modified files to carry prominent notices
stating that you changed the files and the date of any change.
b) You must cause any work that you distribute or publish, that in
whole or in part contains or is derived from the Program or any
part thereof, to be licensed as a whole at no charge to all third
parties under the terms of this License.
c) If the modified program normally reads commands interactively
when run, you must cause it, when started running for such
interactive use in the most ordinary way, to print or display an
announcement including an appropriate copyright notice and a
notice that there is no warranty (or else, saying that you provide
a warranty) and that users may redistribute the program under
these conditions, and telling the user how to view a copy of this
License. (Exception: if the Program itself is interactive but
does not normally print such an announcement, your work based on
the Program is not required to print an announcement.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Program,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Program, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Program.
In addition, mere aggregation of another work not based on the Program
with the Program (or with a work based on the Program) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may copy and distribute the Program (or a work based on it,
under Section 2) in object code or executable form under the terms of
Sections 1 and 2 above provided that you also do one of the following:
a) Accompany it with the complete corresponding machine-readable
source code, which must be distributed under the terms of Sections
1 and 2 above on a medium customarily used for software interchange; or,
b) Accompany it with a written offer, valid for at least three
years, to give any third party, for a charge no more than your
cost of physically performing source distribution, a complete
machine-readable copy of the corresponding source code, to be
distributed under the terms of Sections 1 and 2 above on a medium
customarily used for software interchange; or,
c) Accompany it with the information you received as to the offer
to distribute corresponding source code. (This alternative is
allowed only for noncommercial distribution and only if you
received the program in object code or executable form with such
an offer, in accord with Subsection b above.)
The source code for a work means the preferred form of the work for
making modifications to it. For an executable work, complete source
code means all the source code for all modules it contains, plus any
associated interface definition files, plus the scripts used to
control compilation and installation of the executable. However, as a
special exception, the source code distributed need not include
anything that is normally distributed (in either source or binary
form) with the major components (compiler, kernel, and so on) of the
operating system on which the executable runs, unless that component
itself accompanies the executable.
If distribution of executable or object code is made by offering
access to copy from a designated place, then offering equivalent
access to copy the source code from the same place counts as
distribution of the source code, even though third parties are not
compelled to copy the source along with the object code.
4. You may not copy, modify, sublicense, or distribute the Program
except as expressly provided under this License. Any attempt
otherwise to copy, modify, sublicense or distribute the Program is
void, and will automatically terminate your rights under this License.
However, parties who have received copies, or rights, from you under
this License will not have their licenses terminated so long as such
parties remain in full compliance.
5. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Program or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Program (or any work based on the
Program), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Program or works based on it.
6. Each time you redistribute the Program (or any work based on the
Program), the recipient automatically receives a license from the
original licensor to copy, distribute or modify the Program subject to
these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties to
this License.
7. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Program at all. For example, if a patent
license would not permit royalty-free redistribution of the Program by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Program.
If any portion of this section is held invalid or unenforceable under
any particular circumstance, the balance of the section is intended to
apply and the section as a whole is intended to apply in other
circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system, which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
8. If the distribution and/or use of the Program is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Program under this License
may add an explicit geographical distribution limitation excluding
those countries, so that distribution is permitted only in or among
countries not thus excluded. In such case, this License incorporates
the limitation as if written in the body of this License.
9. The Free Software Foundation may publish revised and/or new versions
of the General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the Program
specifies a version number of this License which applies to it and "any
later version", you have the option of following the terms and conditions
either of that version or of any later version published by the Free
Software Foundation. If the Program does not specify a version number of
this License, you may choose any version ever published by the Free Software
Foundation.
10. If you wish to incorporate parts of the Program into other free
programs whose distribution conditions are different, write to the author
to ask for permission. For software which is copyrighted by the Free
Software Foundation, write to the Free Software Foundation; we sometimes
make exceptions for this. Our decision will be guided by the two goals
of preserving the free status of all derivatives of our free software and
of promoting the sharing and reuse of software generally.
NO WARRANTY
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
REPAIR OR CORRECTION.
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGES.
END OF TERMS AND CONDITIONS

View File

@ -1,60 +0,0 @@
FROM debian:sid-slim
RUN apt-get update && apt-get install -y \
desktop-file-utils \
gettext \
gir1.2-adw-1 \
gir1.2-gtk-3.0 \
gir1.2-gtk-4.0 \
gir1.2-gtksource-5 \
gir1.2-handy-1 \
gir1.2-webkit2-4.1 \
gir1.2-webkit-6.0 \
git \
libadwaita-1-dev \
libgirepository-1.0-dev \
libgtk-3-dev \
libgtk-4-dev \
libhandy-1-dev \
libwlroots-dev \
meson \
ninja-build \
python3-gi \
python3-lxml \
python-gi-dev
RUN useradd -ms /bin/bash discepolo
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 && 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"]

View File

@ -1,19 +0,0 @@
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
flatpak build-bundle repo cambalache.flatpak ar.xjuan.Cambalache
.PHONY: install clean veryclean
install: cambalache.flatpak
flatpak install --user cambalache.flatpak
clean:
rm -rf repo cambalache.flatpak
veryclean: clean
rm -rf .flatpak-builder

View File

@ -1,16 +0,0 @@
## README Mac OS
There is an on going effort to run Cambalache using dependencies from mac port.
See issue [#161](https://gitlab.gnome.org/jpu/cambalache/-/issues/161)
In the mean time you can run Cambalache building a docker image and installing
a X server.
Steps:
- Install [Docker](https://www.docker.com/) and [Xquarts](https://www.xquartz.org/)
- Build cambalache docker image
- `docker build -t cambalache .`
- Make sure docker can connect to the server
- `xhost +localhost`
- Run docker image
- `docker run -e DISPLAY=host.docker.internal:0 cambalache`

196
README.md
View File

@ -1,196 +0,0 @@
![Cambalache](cambalache/app/images/logo-horizontal.svg)
Cambalache is a RAD tool for Gtk 4 and 3 with a clear MVC design and data model first philosophy.
This translates to a wide feature coverage with minimal/none developer intervention for basic support.
![Data Model Diagram](datamodel.svg)
To support multiple Gtk versions it renders the workspace out of process using
a custom wayland compositor widget based on wlroots.
![Merengue Diagram](merengue.svg)
## License
Cambalache is distributed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html),
version 2.1 (LGPL) as described in the COPYING file.
Tools are distributed under the [GNU General Public License](https://www.gnu.org/licenses/gpl-2.0.en.html),
version 2 (GPL) as described in the COPYING.GPL file.
## Source code
Source code lives on GNOME gitlab [here](https://gitlab.gnome.org/jpu/cambalache)
`git clone https://gitlab.gnome.org/jpu/cambalache.git`
## Dependencies
* Python 3 - Cambalache is written in Python
* [Meson](http://mesonbuild.com) build system
* [GTK](http://www.gtk.org) 3 and 4
* python-gi - Python GTK bindings
* python3-lxml - Python libxml2 bindings
* [casilda](https://gitlab.gnome.org/jpu/casilda) - Workspace custom compositor
## Flathub
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.
Instructions on how to install flatpak can be found [here](https://flatpak.org/setup/).
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
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
flatpak install --user cambalache.flatpak
```
Or if you have `make` installed in your host
```
make install
```
Will create the flatpak repository, then the bundle and install it
Run as:
```
flatpak run --user ar.xjuan.Cambalache//master
```
## Manual installation
This is a regular meson package and can be installed the usual way.
```
# Configure project in _build directory
meson setup --wipe --prefix=~/.local _build .
# Build and install in ~/.local
ninja -C _build install
```
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
While docker is not meant for UI applications it is possible to build an image
with Cambalache and run it.
Build the image with:
```
docker build -t cambalache .
```
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)
and/or a merge request [here](https://gitlab.gnome.org/jpu/cambalache/-/merge_requests)
## Contact
You can hang with us and ask us questions on Matrix at #cambalache:gnome.org
[Matrix](https://matrix.to/#/#cambalache:gnome.org)
## Financial support
You can financially support Cambalache development on Liberapay or Patreon
like all these [people](./SUPPORTERS.md) did.
[Liberapay](https://liberapay.com/xjuan)
- Liberapay is a recurrent donations platform
- Run by a non-profit organization
- Source code is public
- No commission fee
- ~5% payment processing fee
[Patreon](https://www.patreon.com/cambalache)
- Patreon is a membership platform for creators
- Run by private company
- No source code available
- ~8% commission fee
- ~8% payment processing fee
## cmb-catalog-gen
This tool is used to generate Cambalache catalogs from Gir files.
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.

View File

@ -1,127 +0,0 @@
## README MS Windows
These instructions have been tested on Windows 10.
They should also work on Windows 11.
## 1. Install WSL2
1. Start -> type `cmd` -> Right-click `Command Prompt` -> select `Run as administrator`.
2. Type this command:
```
wsl --install
```
By default, this will install Ubuntu 20.
## 2. Add Ubuntu 22 to WSL
1. Start -> type `store` -> Select `Microsoft Store`.
2. Click the search bar at the top. Type: `ubuntu 22`
3. Click the result: `Ubuntu 22.04.x LTS`. Click `Install`.
- Troubleshooting: If it says "There has been an error.", try rebooting Windows.
## 3. Configure a Linux user
1. New window appears with message:
> Installing, this may take a few minutes...
2. Then:
> Please create a default UNIX user account. The username does not need to match your Windows username.
>
> For more information visit: https://aka.ms/wslusers
>
> Enter new UNIX username:
3. Type a username you want to use, e.g., `jmoore`.
4. Type a password for the user, e.g. " ". (1 space character)
5. Repeat the password.
- You should now be at a Linux command prompt.
## 4. Install software in Ubuntu 22
- Start -> type `ubu` -> Select `Ubuntu 22.04.x`.
- Paste these commands, one at a time. There will be interactive user prompts for password and Yes/No questions.
```
sudo apt update
sudo apt install flatpak
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
```
## 5. Install the `GWSL` X Windows Server for Microsoft Windows.
1. Start -> type `store` -> Select `Microsoft Store`.
2. Click the search bar at the top. Type: `gwsl`
3. Click the result: `GWSL`. Click `Install`.
4. Start -> type `gwsl` -> Select `GWSL`.
5. Set `Public` to Checked. This allows X Windows clients from Ubuntu 22 WSL2 installation to connect to the GWSL X-Windows server.
- The System Tray notification area should have a new orange icon, the GWSL X server running.
## 6. Install and Run Cambalache as a flatpak
1. Create a startup script for needed daemon, environment variables:
```
vi ~/start_cam.sh
```
You can put these commands on the command-line at first as a test, put them in another script, `~/.bashrc`, etc. Making a script helps when you want to start it from a MS Windows icon.
2. Paste these lines as the script:
```
#!/bin/sh
export WEBKIT_DISABLE_COMPOSITING_MODE=1
flatpak run --user ar.xjuan.Cambalache
```
- The middle line forces the Workspace area of Cambalache, displayed with `webkit`, to not try to use hardware acceleration. If it does try such acceleration in `WSL2`, the Workspace area cannot be drawn and remains blank, even though it is being rendered OK in `broadwayd`.
- The last line runs the flatpak for Cambalache.
Save and quit (`Esc :wq`).
3. Make the script executable:
```
chmod +x start_cam.sh
```
4. Install Cambalache from flatpak.
```
sudo service dbus start
flatpak install --user flathub ar.xjuan.Cambalache
```
- The first line starts the dbus service, required for flatpak to work. Modern Linux installations start dbus at boot, but WSL2 does not.
5. Run Cambalache from the command-line.
```
export DISPLAY="`grep nameserver /etc/resolv.conf | sed 's/nameserver //'`:0"
./start_cam.sh
```
- The first line defines how to find the X windows server. In WSL2, it is at the IP address of the host Windows computer. The $DISPLAY environment variable lets X clients in WSL get to the X Windows server running in MS Windows. It ends up being a value like:
```
$ echo $DISPLAY
192.168.144.1:0
```
`GWSL` was installed, started, and confirmed running in the previous section. So, it is at that IP address, running in MS Windows, waiting for X windows clients to connect to it.
## 7. Starting Cambalache from Windows
1. Click the Windows taskbar Notification area (near the Clock) expand arrow -> (orange GWSL icon) -> `Dashboard`.
2. Click `Shortcut Creator`.
Enter the following settings:
- Shortcut Label: Cambalache
- Shortcut Command:
```
/home/(your WSL2 username)/start_cam.sh
```
e.g.
```
/home/jmoore/start_cam.sh
```
- Run in: `Ubuntu-22.04` <-- IMPORTANT
- Click `More Options`.
- Select `Color Mode`: `Light Mode`. <-- especially if you run Windows in Dark Mode
- Select `Use DBus (Sudo Required)`: `True`.
When the icon is run, this will start dbus, required by flatpak - but will prompt for the user's password each time.
WSL2 also has the dubiously helpful feature of closing all WSL2 processes when there are no WSL2 terminal sessions open for 15 seconds. So it would kill Cambalache.
To defeat this, select this option. It keeps the WSL2 session running when there are no WSL2 console windows open.
- Click `Add to Start Menu`.
3. Start -> type `cam` -> `Right-click` `Cambalache on Ubuntu 22.04`.
4. Select `Pin to Start`.
5. Click `Start` again.
- Note how Cambalache is now featured prominently and can be started easily from Windows.
6. Click `Cambalache on Ubuntu 22.04` to start it.

View File

@ -1,26 +0,0 @@
# Cambalache supporters
Many thanks to all the people that support the project
- Stephan McCormick
- Willo Vincent
- Javier Jardón
- Franz Gratzer
- David
- Sonny Piers
- Patrick Griffis
- Aemilia Scott
- Jonathan K.
- Luis Barron
- Mitch 4J
- JustRyan
- Platon workaccount
- ~1826340
- Mula Gabriel
- Felipe Borges
- Johannes Deutsch
- Patrick
- 2 kojix
- Coleman
- Muasim
- Shogo Takata

View File

@ -1,110 +0,0 @@
{
"app-id" : "ar.xjuan.Cambalache",
"runtime" : "org.gnome.Platform",
"runtime-version" : "48",
"sdk" : "org.gnome.Sdk",
"separate-locales" : false,
"command" : "cambalache",
"finish-args" : [
"--share=ipc",
"--share=network",
"--socket=fallback-x11",
"--socket=wayland",
"--filesystem=home",
"--device=dri"
],
"cleanup" : [
"/include",
"/lib/pkgconfig",
"/man",
"/share/doc",
"/share/gtk-doc",
"/share/man",
"/share/pkgconfig",
"*.la",
"*.a"
],
"modules" : [
{
"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" : [
{
"type" : "file",
"url" : "https://files.pythonhosted.org/packages/80/61/d3dc048cd6c7be6fe45b80cedcbdd4326ba4d550375f266d9f4246d0f4bc/lxml-5.3.2.tar.gz",
"sha256" : "773947d0ed809ddad824b7b14467e1a481b8976e87278ac4a730c2f7c7fcddc1"
}
]
},
{
"name" : "libseat",
"buildsystem" : "meson",
"config-opts" : [
"-Dserver=disabled",
"-Dman-pages=disabled"
],
"sources" : [
{
"type" : "archive",
"url" : "https://git.sr.ht/~kennylevinsen/seatd/archive/0.8.0.tar.gz",
"sha256" : "a562a44ee33ccb20954a1c1ec9a90ecb2db7a07ad6b18d0ac904328efbcf65a0",
"x-checker-data" : {
"type" : "anitya",
"project-id" : 234932,
"stable-only" : true,
"url-template" : "https://git.sr.ht/~kennylevinsen/seatd/archive/$version.tar.gz"
}
}
]
},
{
"name" : "wlroots",
"builddir" : true,
"buildsystem" : "meson",
"config-opts" : [],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.freedesktop.org/wlroots/wlroots.git",
"tag" : "0.18.1",
"commit" : "5bc39071d173301eb8b2cd652c711075526dfbd9"
}
]
},
{
"name" : "casilda",
"builddir" : true,
"buildsystem" : "meson",
"config-opts" : [],
"sources" : [
{
"type" : "git",
"url" : "https://gitlab.gnome.org/jpu/casilda.git",
"tag" : "0.9.0",
"commit" : "4f7b1be321cf76832b12bda11fd91897257377e2"
}
]
},
{
"name" : "cambalache",
"builddir" : true,
"buildsystem" : "meson",
"sources" : [
{
"type" : "git",
"path" : ".",
"branch" : "HEAD"
}
],
"config-opts" : [
"--libdir=lib"
]
}
],
"build-options" : {
"env" : { }
}
}

BIN
cambalache.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -1,107 +0,0 @@
# Cambalache
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
import gi
import logging
import locale
import builtins
from . import config
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__:
_ = locale.gettext
if "N_" not in builtins.__dict__:
def N_(s, p, n):
return _(p) if n > 1 else _(s)
# noqa: E402,E401
from gi.repository import Gio, Gdk, Gtk
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")
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):
formatter = logging.Formatter("%(levelname)s:%(name)s %(message)s")
ch = logging.StreamHandler()
ch.setFormatter(formatter)
logger = logging.getLogger(name)
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
from .cmb_property_label import CmbPropertyLabel
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_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

View File

@ -1 +0,0 @@
../../SUPPORTERS.md

View File

@ -1,34 +0,0 @@
# Cambalache Application
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
from cambalache import config
from gi.repository import Gio
resource = Gio.Resource.load(os.path.join(config.pkgdatadir, "app.gresource"))
resource._register()
from .cmb_application import CmbApplication
from .cmb_scrolled_window import CmbScrolledWindow

View File

@ -1,16 +0,0 @@
<?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>
<file>cambalache.css</file>
<file>images/logo-symbolic.svg</file>
<file>images/gtk3.svg</file>
<file>images/gtk4.svg</file>
<file>images/lp-logo.svg</file>
<file>images/patreon-logo.svg</file>
</gresource>
</gresources>

View File

@ -1,140 +0,0 @@
/*
* cambalache.css
*
* 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
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Author: Juan Pablo Ugarte <juanpablougarte@gmail.com>
*
*/
window.cmb-window .logo {
background: url('resource:///ar/xjuan/Cambalache/app/images/logo-symbolic.svg') no-repeat 50% 35% / 40%;
}
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%;
}
window.cmb-window label.message {
padding: 1ex 1em;
border-radius: 1ex 1ex 0 0;
color: white;
background-color: rgba(0, 0, 0, .6);
}
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);
}
window.cmb-window stackswitcher.compact > button {
min-width: unset;
}
window.cmb-window box.donate {
padding: 1em;
}
popover.cmb-tutor > * {
padding: 1em;
}
popover.cmb-tutor label {
font-size: 18px;
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,
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;
}

View File

@ -1,46 +0,0 @@
#!@PYTHON@
#
# Cambalache UI Maker
#
# Copyright (C) 2021 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 sys
import signal
import locale
import builtins
pkgdatadir = '@pkgdatadir@'
localedir = '@localedir@'
sys.path.insert(1, pkgdatadir)
signal.signal(signal.SIGINT, signal.SIG_DFL)
locale.bindtextdomain("cambalache", localedir)
locale.textdomain("cambalache")
from cambalache.app import CmbApplication
if __name__ == '__main__':
app = CmbApplication()
app.run(sys.argv)

View File

@ -1,264 +0,0 @@
#
# Cambalache Application
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
import sys
import gi
gi.require_version('Adw', '1')
from gi.repository import GLib, Gdk, Gtk, Gio, Adw
from .cmb_window import CmbWindow
from cambalache import CmbProject, utils, config, _
basedir = os.path.dirname(__file__) or "."
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, _("Deprecated: Export project"), None
)
def add_new_window(self):
window = CmbWindow(application=self)
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):
window = None
for win in self.get_windows():
if win.project and win.project.filename == path:
window = win
if window is None:
window = self.add_new_window()
if path is not None:
window.open_project(path, target_tk=target_tk)
window.present()
def import_file(self, path):
window = self.add_new_window() if self.props.active_window is None else self.props.active_window
window.import_file(path)
window.present()
def check_can_quit(self, window=None):
windows = self.__get_windows() if window is None else [window]
unsaved_windows = []
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:
if win.project is None:
continue
if win.actions["save"].get_enabled():
unsaved_windows.append(win)
unsaved_windows_len = len(unsaved_windows)
if unsaved_windows_len == 0:
do_quit()
return
# Create Dialog
window = windows[0]
dialog = window._close_project_dialog_new()
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.append(Gtk.Label(label=_("Select which files:"), halign=Gtk.Align.START))
home = GLib.get_home_dir()
untitled = 0
for win in unsaved_windows:
if win.project.filename is None:
untitled += 1
# Find Unique name
while os.path.exists(f"Untitled {untitled}.cmb"):
untitled += 1
check = Gtk.CheckButton(active=True, margin_start=8, can_focus=False)
entry = Gtk.Entry(text=f"Untitled {untitled}")
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:
win.save_project()
elif response == Gtk.ResponseType.CANCEL:
return
do_quit()
dialog.connect("response", callback, window)
dialog.present()
def __get_windows(self):
retval = []
for win in self.get_windows():
if win.props.application is not None:
retval.append(win)
return retval
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()
if len(windows) == 0:
self.activate_action("quit")
def do_handle_local_options(self, options):
if options.contains("version"):
print(config.VERSION)
return 0
if options.contains("export-all"):
print("Export has been deprecated and does nothing. Every UI file is updated on project save.")
return 0
return -1
if __name__ == "__main__":
app = CmbApplication()
app.run(sys.argv)

View File

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

View File

@ -1,111 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_shortcuts.ui -->
<!-- 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>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Project</property>
<property name="view">shortcuts</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;n</property>
<property name="title" translatable="yes">Create new project</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;o</property>
<property name="title" translatable="yes">Open a project</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;i</property>
<property name="title" translatable="yes">Import file</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;s</property>
<property name="title" translatable="yes">Save project</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;s</property>
<property name="title" translatable="yes">Save project as</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;w</property>
<property name="title" translatable="yes">Close the project</property>
</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">&lt;primary&gt;question</property>
<property name="title" translatable="yes">Keyboard Shortcuts</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;q</property>
<property name="title" translatable="yes">Quit application</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsGroup">
<property name="title" translatable="yes">Workspace</property>
<property name="view">shortcuts</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">Delete</property>
<property name="title" translatable="yes">Delete object</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;Insert</property>
<property name="title" translatable="yes">Add slot/column</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;Delete</property>
<property name="title" translatable="yes">Remove slot/column</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;Insert</property>
<property name="title" translatable="yes">Add row</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="accelerator">&lt;primary&gt;&lt;shift&gt;Delete</property>
<property name="title" translatable="yes">Remove row</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>

View File

@ -1,230 +0,0 @@
#
# Cambalache Tutor
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# 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):
NULL = 1
PLAYING = 2
PAUSED = 3
class CmbTutorPosition(Enum):
BOTTOM = 1
LEFT = 2
RIGHT = 3
CENTER = 4
ScriptNode = namedtuple("ScriptNode", "widget text delay name position")
class CmbTutor(GObject.GObject):
__gsignals__ = {
"show-node": (GObject.SignalFlags.RUN_LAST, None, (str, Gtk.Widget)),
"hide-node": (GObject.SignalFlags.RUN_LAST, None, (str, Gtk.Widget)),
}
window = GObject.Property(type=Gtk.Window, flags=GObject.ParamFlags.READWRITE)
def __init__(self, script, **kwargs):
# List of ScriptNode
self.script = []
# Popover to show the script text
self.popover = None
# Timeout id for running the script
self.timeout_id = None
# Current script node index
self.current = None
self.hiding_node = None
super().__init__(**kwargs)
for node in script:
self.__add(*node)
@GObject.Property(type=int, flags=GObject.ParamFlags.READABLE)
def state(self):
if self.timeout_id:
return CmbTutorState.PLAYING
elif self.current:
return CmbTutorState.PAUSED
return CmbTutorState.NULL
def __add(self, text, widget_name, delay, name=None, position=CmbTutorPosition.BOTTOM):
def find_by_css_name_or_buildable_id(widget, name):
retval = None
css_name = widget.get_name()
# Get css name first
if css_name and css_name != GObject.type_name(widget) and css_name == name:
return widget
# then GtkBuildable name
if isinstance(widget, Gtk.Buildable) and Gtk.Buildable.get_buildable_id(widget) == name:
return widget
# or ModelButton name
if GObject.type_name(widget) == "GtkModelButton" and widget.props.text == name:
return widget
for child in utils.widget_get_children(widget):
retval = find_by_css_name_or_buildable_id(child, name)
if retval:
return retval
return retval
widget = find_by_css_name_or_buildable_id(self.window, widget_name)
if widget:
self.script.append(ScriptNode(widget, text, delay, name, position))
def play(self):
if len(self.script) == 0:
return
if self.current is None:
self.current = 0
self.__script_play()
self.notify("state")
def pause(self):
if self.timeout_id:
GLib.source_remove(self.timeout_id)
self.timeout_id = None
self.__hide_node(self.current)
self.notify("state")
def stop(self):
self.pause()
self.current = None
self.notify("state")
def __popover_new(self, text):
popover = Gtk.Popover(autohide=False)
popover.add_css_class("cmb-tutor")
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, hexpand=True)
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
def __script_transition(self):
self.timeout_id = GLib.timeout_add(250, self.__script_play)
self.__hide_current_node()
# Set next node
if self.current is not None:
self.current = self.current + 1 if self.current < (len(self.script) - 1) else None
return GLib.SOURCE_REMOVE
def __hide_node(self, index):
if self.popover:
self.popover.popdown()
self.popover = None
if index is not None:
node = self.script[index]
if node.widget:
node.widget.get_style_context().remove_class("cmb-tutor-highlight")
def __hide_current_node(self):
if self.hiding_node:
return
self.hiding_node = True
self.__hide_node(self.current)
if self.current is not None:
node = self.script[self.current]
self.emit("hide-node", node.name, node.widget)
self.hiding_node = False
def __script_play(self):
self.timeout_id = None
if self.current is None:
return GLib.SOURCE_REMOVE
node = self.script[self.current]
if node and node.text:
# Ensure the widget is visible
if not node.widget.is_visible():
# if the widget is inside a popover pop it up
parent = node.widget.get_ancestor(Gtk.Popover)
if parent:
parent.popup()
node.widget.add_css_class("cmb-tutor-highlight")
# Create popover
self.popover = self.__popover_new(node.text)
self.popover.set_parent(node.widget)
if node.position == CmbTutorPosition.BOTTOM:
self.popover.set_position(Gtk.PositionType.BOTTOM)
elif node.position == CmbTutorPosition.LEFT:
self.popover.set_position(Gtk.PositionType.LEFT)
elif node.position == CmbTutorPosition.RIGHT:
self.popover.set_position(Gtk.PositionType.RIGHT)
elif node.position == CmbTutorPosition.CENTER:
rect = Gdk.Rectangle()
rect.x = node.widget.get_allocated_width() / 2
rect.y = node.widget.get_allocated_height() / 2
self.popover.set_pointing_to(rect)
self.popover.set_position(Gtk.PositionType.TOP)
self.emit("show-node", node.name, node.widget)
if self.popover:
self.popover.set_sensitive(True)
self.popover.popup()
self.timeout_id = GLib.timeout_add(node.delay * 1000, self.__script_transition)
return GLib.SOURCE_REMOVE

View File

@ -1,90 +0,0 @@
#
# CmbTutorial
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# 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 and find recently used"), "open_button", 5),
(_("Common actions like Undo"), "undo_button", 4),
(_("Redo"), "redo_button", 2),
(_("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),
(
_("This is the project workspace, where you can see and select the widgets to edit"),
"view",
6,
None,
CmbTutorPosition.CENTER,
),
(_("Project tree, with multiple UI support"), "tree_view", 4, None, CmbTutorPosition.CENTER),
(
_("Class selector bar"),
"type_chooser_box",
3,
),
(_("And the object editor"), "editor_stack", 3, None, CmbTutorPosition.CENTER),
(_("You can search all supported classes here"), "type_chooser_all", 4, "show-type-popover", CmbTutorPosition.LEFT),
(_("or investigate what is in each group"), "type_chooser_gtk", 4, "show-type-popover-gtk", CmbTutorPosition.LEFT),
(_("Now let's add a new UI file"), "add_button", 5, "add-ui"),
(_("Good, now try to create a window"), "intro_button", 4, "add-window"),
(_("Excellent!"), "intro_button", 2),
(_("BTW, did you know you can double click on any placeholder to create widgets?"), "intro_button", 5),
(_("Try adding a grid"), "intro_button", 3, "add-grid"),
(_("and a button"), "intro_button", 3, "add-button"),
(_("Quite easy! Isn't it?"), "intro_button", 3),
(
_("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"),
7,
"donate",
CmbTutorPosition.LEFT,
),
(_("Have a nice day!"), "intro_button", 3, "intro-end"),
]

File diff suppressed because it is too large Load Diff

View File

@ -1,877 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.97.1 -->
<interface>
<!-- interface-name cmb_window.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gio" version="2.0"/>
<requires lib="gtk" version="4.14"/>
<requires lib="libadwaita" version="1.5"/>
<menu id="main_menu">
<item>
<attribute name="action">win.create_new</attribute>
<attribute name="label" translatable="yes">New Project</attribute>
</item>
<section>
<submenu>
<attribute name="label" translatable="yes">Add file</attribute>
<item>
<attribute name="action">win.add_ui</attribute>
<attribute name="label" translatable="yes">Add UI</attribute>
</item>
<item>
<attribute name="action">win.add_css</attribute>
<attribute name="label" translatable="yes">Add CSS</attribute>
</item>
<item>
<attribute name="action">win.add_gresource</attribute>
<attribute name="label" translatable="yes">Add GResource</attribute>
</item>
</submenu>
<item>
<attribute name="action">win.import</attribute>
<attribute name="label" translatable="yes">Import file</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">win.save</attribute>
<attribute name="label" translatable="yes">Save</attribute>
</item>
<item>
<attribute name="action">win.save_as</attribute>
<attribute name="label" translatable="yes">Save As</attribute>
</item>
<item>
<attribute name="action">win.close</attribute>
<attribute name="label" translatable="yes">Close Project</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">win.intro</attribute>
<attribute name="label" translatable="yes">Interactive intro</attribute>
</item>
<item>
<attribute name="action">win.notification</attribute>
<attribute name="label" translatable="yes">Notifications</attribute>
</item>
<item>
<attribute name="action">win.contact</attribute>
<attribute name="label" translatable="yes">Contact</attribute>
</item>
<item>
<attribute name="action">win.donate</attribute>
<attribute name="label" translatable="yes">Donate</attribute>
</item>
</section>
<section>
<item>
<attribute name="action">win.show-help-overlay</attribute>
<attribute name="label" translatable="yes">Keyboard Shortcuts</attribute>
</item>
<item>
<attribute name="action">win.about</attribute>
<attribute name="label" translatable="yes">About</attribute>
</item>
</section>
</menu>
<menu id="recent_menu"/>
<object class="GtkFileFilter" id="open_filter">
<mime-types>
<mime-type>application/x-cambalache-project</mime-type>
</mime-types>
</object>
<template class="CmbWindow" parent="AdwApplicationWindow">
<child>
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar" id="headerbar">
<property name="title-widget">
<object class="AdwWindowTitle" id="title">
<property name="title">Cambalache</property>
</object>
</property>
<child>
<object class="AdwSplitButton" id="open_button">
<property name="action-name">win.open</property>
<property name="dropdown-tooltip">Recent Projects</property>
<property name="label" translatable="yes">_Open</property>
<property name="menu-model">recent_menu</property>
<property name="use-underline">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="undo_button">
<property name="action-name">win.undo</property>
<property name="focusable">1</property>
<property name="has-frame">False</property>
<property name="receives-default">1</property>
<child>
<object class="GtkImage">
<property name="icon-name">edit-undo-symbolic</property>
</object>
</child>
</object>
</child>
<child>
<object class="GtkButton" id="redo_button">
<property name="action-name">win.redo</property>
<property name="focusable">1</property>
<property name="has-frame">False</property>
<property name="receives-default">1</property>
<property name="use-underline">1</property>
<child>
<object class="GtkImage">
<property name="icon-name">edit-redo-symbolic</property>
</object>
</child>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="menu_button">
<property name="focusable">1</property>
<property name="has-frame">False</property>
<property name="menu-model">main_menu</property>
<property name="receives-default">1</property>
<child>
<object class="GtkImage">
<property name="icon-name">open-menu-symbolic</property>
</object>
</child>
</object>
</child>
<child type="end">
<object class="GtkButton" id="add_button">
<property name="action-name">win.add_ui</property>
<property name="has-frame">False</property>
<property name="tooltip-text" translatable="yes">Add new UI to project</property>
<child>
<object class="GtkImage">
<property name="icon-name">list-add-symbolic</property>
</object>
</child>
</object>
</child>
<child type="end">
<object class="GtkButton" id="intro_button">
<property name="action-name">win.intro</property>
<property name="has-frame">False</property>
<property name="tooltip-text" translatable="yes">Start interactive introduction</property>
<child>
<object class="GtkImage">
<property name="icon-name">start-here-symbolic</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkOverlay">
<property name="child">
<object class="GtkStack" id="stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="CmbNotificationListView" id="front_notification_list_view">
<property name="halign">start</property>
<property name="valign">center</property>
<property name="vexpand">True</property>
</object>
</child>
<child>
<object class="GtkLabel" id="version_label">
<property name="halign">center</property>
<property name="margin-bottom">4</property>
<property name="name">version</property>
<property name="valign">end</property>
<attributes>
<attribute name="size" value="9000"/>
</attributes>
</object>
</child>
<style>
<class name="logo"/>
</style>
</object>
</property>
<property name="name">cambalache</property>
<property name="title" translatable="yes">Cambalache</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkLabel">
<property name="justify">center</property>
<property name="label" translatable="yes">&lt;b&gt;
&lt;span size="18000"&gt;New project&lt;/span&gt;
&lt;/b&gt;</property>
<property name="use-markup">1</property>
</object>
</child>
<child>
<object class="GtkGrid">
<property name="column-homogeneous">1</property>
<property name="column-spacing">8</property>
<property name="halign">center</property>
<property name="row-spacing">8</property>
<property name="valign">start</property>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">Name</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">Toolkit target</property>
<layout>
<property name="column">0</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">Location</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkEntry" id="np_name_entry">
<property name="focusable">1</property>
<property name="input-hints">lowercase|none</property>
<property name="input-purpose">alpha</property>
<property name="placeholder-text" translatable="yes">&lt;project basename&gt;</property>
<property name="width-chars">32</property>
<signal name="changed" handler="on_np_name_entry_changed"/>
<layout>
<property name="column">1</property>
<property name="column-span">3</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="np_create_button">
<property name="action-name">win.new</property>
<property name="focusable">1</property>
<property name="label" translatable="yes">Create</property>
<property name="margin-top">16</property>
<property name="receives-default">1</property>
<style>
<class name="suggested-action"/>
</style>
<layout>
<property name="column">3</property>
<property name="row">4</property>
</layout>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">start</property>
<property name="homogeneous">True</property>
<child>
<object class="GtkToggleButton" id="np_gtk3_radiobutton">
<property name="group">np_gtk4_radiobutton</property>
<property name="halign">center</property>
<property name="tooltip-text" translatable="yes">Old stable version</property>
<child>
<object class="GtkBox">
<property name="spacing">8</property>
<child>
<object class="GtkImage">
<property name="resource">/ar/xjuan/Cambalache/app/images/gtk3.svg</property>
<style>
<class name="icon-size-32"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="justify">center</property>
<property name="label">&lt;b&gt;Gtk 3&lt;/b&gt;</property>
<property name="use-markup">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkToggleButton" id="np_gtk4_radiobutton">
<property name="active">1</property>
<property name="halign">center</property>
<property name="tooltip-text" translatable="yes">Recommended for new projects</property>
<child>
<object class="GtkBox">
<property name="spacing">8</property>
<child>
<object class="GtkImage">
<property name="resource">/ar/xjuan/Cambalache/app/images/gtk4.svg</property>
<style>
<class name="icon-size-32"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="justify">center</property>
<property name="label">&lt;b&gt;Gtk 4&lt;/b&gt;</property>
<property name="use-markup">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="linked"/>
</style>
<layout>
<property name="column">1</property>
<property name="column-span">3</property>
<property name="row">2</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="np_location_chooser">
<property name="action-name">win.select_project_location</property>
<property name="icon-name">folder-symbolic</property>
<property name="sensitive">False</property>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<child>
<object class="GtkImage">
<property name="icon-name">folder-open-symbolic</property>
</object>
</child>
<child>
<object class="GtkLabel" id="np_location_chooser_label">
<property name="halign">start</property>
<property name="hexpand">True</property>
</object>
</child>
</object>
</child>
<layout>
<property name="column">1</property>
<property name="column-span">3</property>
<property name="row">1</property>
</layout>
</object>
</child>
<child>
<object class="GtkButton" id="np_cancel_button">
<property name="action-name">win.show_workspace</property>
<property name="focusable">1</property>
<property name="label" translatable="yes">Cancel</property>
<property name="margin-top">16</property>
<property name="receives-default">1</property>
<layout>
<property name="column">0</property>
<property name="row">4</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="halign">end</property>
<property name="label" translatable="yes">UI Filename</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkEntry" id="np_ui_entry">
<property name="focusable">1</property>
<property name="input-hints">lowercase|none</property>
<property name="input-purpose">alpha</property>
<property name="sensitive">0</property>
<property name="width-chars">32</property>
<layout>
<property name="column">1</property>
<property name="column-span">3</property>
<property name="row">3</property>
</layout>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="name">new_project</property>
<property name="title" translatable="yes">New Project</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkPaned">
<property name="end-child">
<object class="GtkStack" id="editor_stack">
<property name="transition-type">crossfade</property>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkStackSwitcher">
<property name="halign">center</property>
<property name="stack">object_stack</property>
<style>
<class name="compact"/>
<class name="property-pane"/>
</style>
</object>
</child>
<child>
<object class="GtkStack" id="object_stack">
<property name="transition-type">crossfade</property>
<property name="vexpand">1</property>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbScrolledWindow">
<property name="child">
<object class="CmbObjectEditor" id="object_editor">
<property name="vexpand">True</property>
</object>
</property>
</object>
</property>
<property name="name">properties</property>
<property name="title" translatable="yes">Properties</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbScrolledWindow">
<property name="child">
<object class="CmbObjectEditor" id="object_layout_editor">
<property name="layout">True</property>
</object>
</property>
</object>
</property>
<property name="name">layout</property>
<property name="title" translatable="yes">Layout</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbSignalEditor" id="signal_editor"/>
</property>
<property name="name">signals</property>
<property name="title" translatable="yes">Signals</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkScrolledWindow">
<property name="child">
<object class="CmbAccessibleEditor" id="accessible_editor"/>
</property>
</object>
</property>
<property name="name">accessible</property>
<property name="title">a11y</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbFragmentEditor" id="fragment_editor"/>
</property>
<property name="name">fragment</property>
<property name="title">&lt;/&gt;</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="name">object</property>
<property name="title" translatable="yes">Object Editor</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkStackSwitcher">
<property name="halign">center</property>
<property name="stack">ui_stack</property>
<style>
<class name="compact"/>
<class name="property-pane"/>
</style>
</object>
</child>
<child>
<object class="GtkStack" id="ui_stack">
<property name="vexpand">1</property>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbUIEditor" id="ui_editor">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
</property>
<property name="name">properties</property>
<property name="title" translatable="yes">Properties</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbUIRequiresEditor" id="ui_requires_editor">
<property name="orientation">vertical</property>
</object>
</property>
<property name="name">requires</property>
<property name="title" translatable="yes">Requires</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbFragmentEditor" id="ui_fragment_editor">
<property name="orientation">vertical</property>
</object>
</property>
<property name="name">fragment</property>
<property name="title">&lt;/&gt;</property>
</object>
</child>
</object>
</child>
</object>
</property>
<property name="name">ui</property>
<property name="title" translatable="yes">UI Editor</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbCSSEditor" id="css_editor">
<property name="hexpand">True</property>
<property name="vexpand">True</property>
</object>
</property>
<property name="name">css</property>
<property name="title" translatable="yes">CSS Editor</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="CmbGResourceEditor" id="gresource_editor"/>
</property>
<property name="name">gresource</property>
<property name="title">GResource Editor</property>
</object>
</child>
</object>
</property>
<property name="focusable">1</property>
<property name="height-request">380</property>
<property name="resize-end-child">0</property>
<property name="shrink-end-child">0</property>
<property name="shrink-start-child">0</property>
<property name="start-child">
<object class="GtkPaned">
<property name="end-child">
<object class="GtkStack" id="workspace_stack">
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="CmbTypeChooser" id="type_chooser">
<property name="spacing">2</property>
<property name="valign">start</property>
<signal name="chooser-popdown" handler="on_type_chooser_chooser_popdown"/>
<signal name="chooser-popup" handler="on_type_chooser_chooser_popup"/>
<signal name="type-selected" handler="on_type_chooser_type_selected"/>
</object>
</child>
<child>
<object class="CmbView" id="view">
<property name="vexpand">1</property>
<signal name="placeholder-activated" handler="on_view_placeholder_activated"/>
<signal name="placeholder-selected" handler="on_view_placeholder_selected"/>
</object>
</child>
</object>
</property>
<property name="name">ui</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkScrolledWindow">
<property name="child">
<object class="CmbSourceView" id="source_view">
<property name="editable">False</property>
<property name="lang">xml</property>
</object>
</property>
</object>
</property>
<property name="name">gresource</property>
</object>
</child>
</object>
</property>
<property name="focusable">1</property>
<property name="resize-start-child">0</property>
<property name="shrink-end-child">0</property>
<property name="shrink-start-child">0</property>
<property name="start-child">
<object class="GtkBox" id="inspector">
<property name="focusable">1</property>
<child>
<object class="CmbScrolledWindow">
<property name="min-content-width">200</property>
<property name="propagate-natural-height">True</property>
<property name="propagate-natural-width">True</property>
<child>
<object class="CmbListView" id="list_view"/>
</child>
</object>
</child>
</object>
</property>
</object>
</property>
</object>
</property>
<property name="name">workspace</property>
<property name="title" translatable="yes">Workspace</property>
</object>
</child>
<child>
<object class="GtkStackPage">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkButton">
<property name="action-name">win.show_workspace</property>
<property name="focusable">1</property>
<property name="halign">start</property>
<property name="receives-default">1</property>
<child>
<object class="GtkImage">
<property name="icon-name">go-previous-symbolic</property>
</object>
</child>
<style>
<class name="borderless"/>
</style>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="homogeneous">1</property>
<property name="margin-bottom">64</property>
<property name="margin-top">32</property>
<property name="spacing">196</property>
<property name="valign">start</property>
<property name="vexpand">1</property>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="hexpand">1</property>
<property name="orientation">vertical</property>
<property name="spacing">32</property>
<child>
<object class="GtkImage">
<property name="resource">/ar/xjuan/Cambalache/app/images/lp-logo.svg</property>
<style>
<class name="icon-size-64"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">• Liberapay is a recurrent donations platform
• Run by a non-profit organization
• Source code is public
• No commission fee
• ~5% payment processing fee</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="action-name">win.liberapay</property>
<property name="focusable">1</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Donate</property>
<property name="receives-default">1</property>
<style>
<class name="suggested-action"/>
</style>
</object>
</child>
</object>
</child>
<child>
<object class="GtkBox">
<property name="halign">center</property>
<property name="hexpand">1</property>
<property name="orientation">vertical</property>
<property name="spacing">32</property>
<child>
<object class="GtkImage">
<property name="resource">/ar/xjuan/Cambalache/app/images/patreon-logo.svg</property>
<style>
<class name="icon-size-64"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">• Patreon is a membership platform for creators
• Run by private company
• No source code available
• ~8% commission fee
• ~8% payment processing fee</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="action-name">win.patreon</property>
<property name="focusable">1</property>
<property name="halign">center</property>
<property name="label" translatable="yes">Donate</property>
<property name="receives-default">1</property>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="donate"/>
</style>
</object>
</property>
<property name="name">donate</property>
</object>
</child>
</object>
</property>
<child type="overlay">
<object class="GtkRevealer" id="message_revealer">
<property name="child">
<object class="GtkLabel" id="message_label">
<style>
<class name="message"/>
</style>
</object>
</property>
<property name="halign">center</property>
<property name="transition-type">slide-up</property>
<property name="valign">end</property>
<style/>
</object>
</child>
</object>
</child>
</object>
</child>
<style>
<class name="cmb-window"/>
</style>
</template>
<object class="GtkFileFilter" id="gtk_builder_filter">
<property name="mime-types">application/x-gtk-builder</property>
</object>
<object class="GtkFileFilter" id="glade_filter">
<property name="mime-types">application/x-glade</property>
</object>
<object class="GtkFileFilter" id="blueprint_filter">
<property name="mime-types">text/x-blueprint</property>
</object>
<object class="GtkFileFilter" id="css_filter">
<property name="mime-types">text/css</property>
</object>
<object class="GtkFileFilter" id="gresource_filter">
<property name="suffixes">gresource.xml</property>
</object>
<object class="GtkFileFilter" id="gtk4_filter">
<property name="mime-types">application/x-gtk-builder
text/x-blueprint
text/css</property>
<property name="name">All supported files</property>
<property name="suffixes">gresource.xml</property>
</object>
<object class="GtkFileFilter" id="gtk3_filter">
<property name="mime-types">application/x-gtk-builder
application/x-glade
text/css</property>
<property name="name">All supported files</property>
<property name="suffixes">gresource.xml</property>
</object>
<object class="AdwDialog" id="notification_dialog">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="AdwHeaderBar"/>
</child>
<child>
<object class="CmbNotificationListView" id="notification_list_view"/>
</child>
</object>
</property>
<property name="follows-content-size">True</property>
<property name="title" translatable="yes">Notifications</property>
</object>
</interface>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 32 33.336" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="c" x1="-143" x2="-129.5" y1="207.36" y2="108.86" gradientTransform="matrix(.12809 0 0 .12809 33.844 -12.218)" gradientUnits="userSpaceOnUse">
<stop stop-color="#92cbe2" offset="0"/>
<stop stop-color="#7f95fe" offset="1"/>
</linearGradient>
<linearGradient id="b" x1="-133.5" x2="-50" y1="221.86" y2="279.36" gradientTransform="matrix(.12809 0 0 .12809 33.844 -12.218)" gradientUnits="userSpaceOnUse">
<stop stop-color="#bff872" offset="0"/>
<stop stop-color="#7de567" offset="1"/>
</linearGradient>
<linearGradient id="a" x1="-157.5" x2="-247.5" y1="222.86" y2="276.86" gradientTransform="matrix(.12809 0 0 .12809 33.844 -12.218)" gradientUnits="userSpaceOnUse">
<stop stop-color="#fe798e" offset="0"/>
<stop stop-color="#9c2219" offset="1"/>
</linearGradient>
</defs>
<path d="m14.587 15.159-13.341-7.9466 16.043-6.575 13.469 7.4425z" fill="url(#c)"/>
<path d="m15.427 15.181 15.832-6.9539-2.2718 15.91-14.915 8.5469z" fill="url(#b)"/>
<path d="m14.402 32.294-13.385-8.2263 0.057475-15.837 13.889 7.1944z" fill="url(#a)"/>
<path d="m13.5 32.9c-0.4-0.2-2.4-1.6-4.6-3-5-3.3-6.5-4.3-7.6-4.9-1.4-0.7-1.4 0.1-1.4-8.4 0-8-0.1-9.4 0.6-9.2-0.6-0.6 2.2-1.4 6.5-3.4 4.3-1.9 8.9-3.8 10-4.1 0.3 0 0.9 0.2 3.5 1.5 5.5 2.7 10.1 5.3 10.7 5.9 0.1 0.1 0.3 0.3 0.4 0.3 0.3 0 0.3 0.5 0.2 1.5-0.1 0.5-0.6 3.7-1.2 7.1s-1.1 6.5-1.1 6.9c-0.2 1.5-0.1 1.4-2.4 2.9-1.1 0.7-4 2.4-6.4 3.8-2.4 1.4-4.8 2.7-5.3 3-0.5 0.3-1 0.6-1.1 0.5-0.1 0-0.5-0.2-0.9-0.4zm0.6-3.3c0-1.1 0.1-4.4 0.2-7.3 0.1-6.4 0.2-6.2-1.2-7.1-1.7-1.1-3.4-2.1-6.5-3.7-1.7-0.9-3.5-1.9-4-2.2-0.7-0.4-1.1-0.6-1.2-0.5-0.2 0.2-0.2 6.8 0 11.5l0.1 3.3 0.9 0.5c0.8 0.5 5.4 3.5 9.8 6.6 0.7 0.5 1.4 0.9 1.6 0.9 0.2 0 0.2-0.3 0.3-2.1zm-7.9-5.7 0.2-6.4-2.4-1.3 0.3-2.6 7.7 3.7-0.1 2.5-2.8-1.2c-0.4 1.4-0.4 4.3-0.6 6.5zm14.6 4.5c5.6-3.2 7.5-4.4 7.6-4.7 0-0.2 0.2-0.9 0.3-1.7 0.1-0.8 0.6-3.8 1.1-6.8 0.8-5 1-6.2 0.8-6.2-0.2 0-3.2 1.4-7 3.1-2.4 1.1-5 2.3-5.8 2.7-0.8 0.4-1.6 0.8-1.7 0.9-0.3 0.2-0.9 15-0.6 15.5 0.1 0.2 0.2 0.2 0.8-0.2 0.4-0.2 2.5-1.4 4.6-2.6zm-2.8-2.2c0-3.4-0.3-6.8 0.5-10.3l1.6 0 0.1 4.9 5.5-6.8 2.6-0.7-4.9 6.7 4.5 2.2-1.9 1.2-4.6-2-1.7 0.8-0.1 3.3zm-1.7-12c0.4-0.2 3.1-1.5 6.1-2.9 3-1.4 5.8-2.7 6.2-2.9 1.2-0.5 1.3-0.6 0.7-1-0.9-0.6-5.5-3-8.7-4.6l-3.2-1.6-1.1 0.4c-1.5 0.5-5.8 2.3-9.5 3.9-3.9 1.7-4.4 2-4.4 2.2 0 0.1 0.9 0.6 1.9 1.2 3.3 1.9 10.6 5.6 10.9 5.6 0.2 0 0.6-0.1 1-0.3zm-2-2.1c-1.9-0.5-4-1.5-4.5-2.3-0.4-0.6-0.8-1.6-0.8-2.1 0-0.7 0.4-1.6 0.9-2.1 0.7-0.7 1.6-1 4.1-1.3 2.4-0.3 4.6-0.2 5.8 0.2 1.2 0.4 1.7 0.7 1.9 1 0.6 0.9 1.1 1.7 1.1 1.8 0 0.2-0.6 0.5-1 0.4-0.2 0-0.5-0.1-0.8-0.1-0.4-0.1-0.5-0.2-0.8-0.6-0.3-0.6-1.2-1.2-2-1.4-1.3-0.3-4.8-0.1-5.9 0.5-0.6 0.3-0.9 1-1 2-0.1 0.7 0 0.9 0.2 1.2 0.4 0.4 1.4 0.9 2.6 1.3 0.6 0.2 1 0.3 1.5 0.2 1-0.1 1.2-0.4 0.5-1.2-0.5-0.7-0.7-1-0.5-1.4 0.1-0.2 0.2-0.3 0.8-0.2 0.8 0.1 2.6 0.7 3 1.1 0.4 0.3 0.4 1.1 0.1 1.5-0.4 0.4-2 1.2-2.6 1.4-0.7 0.2-1.8 0.2-2.7 0z" stroke-width="4"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="31.997" height="34.706" version="1.0" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(.34561 0 0 .34685 -6.1578 -4.8943)" stroke="#fff">
<g fill-rule="evenodd" stroke-linejoin="round" stroke-width="6.1337">
<path d="m20.884 30.827 32.933 24.701 53.516-16.467-36.746-21.883z" fill="#729fcf"/>
<path d="m22.942 82.287-2.0583-51.46 32.933 24.701v55.577z" fill="#e40000"/>
<path d="m53.817 111.1 49.399-20.584 4.1166-51.46-53.516 16.467z" fill="#7fe719"/>
</g>
<path d="m23.217 81.319 47.269-13.958 32.898 23.083" fill="none" stroke-width="3.6104"/>
<path d="m70.435 17.876v49.109" fill="#babdb6" fill-rule="evenodd" stroke-width="3.6104"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 728 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 40 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" viewBox="0 0 80 80" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(.83012 0 0 .83012-135.4-247.7)">
<path d="m259.55 385.57c0 5.145-4.169 9.318-9.318 9.318h-77.74c-5.144 0-9.318-4.174-9.318-9.318v-77.74c0-5.145 4.174-9.318 9.318-9.318h77.74c5.149 0 9.318 4.173 9.318 9.318v77.74" fill="#f6c915"/>
<g fill="#fff">
<path d="m202.45 366.03c-3.104 0-5.541-.405-7.311-1.213-1.77-.809-3.039-1.912-3.803-3.313-.766-1.398-1.137-3-1.115-4.818.021-1.814.272-3.748.754-5.803l8.327-34.817 10.164-1.573-9.114 37.768c-.175.786-.273 1.508-.295 2.163-.023.655.098 1.235.36 1.737.262.504.71.908 1.344 1.213.633.307 1.519.504 2.656.591l-1.967 8.06"/>
<path d="m239.16 344.33c0 3.19-.525 6.108-1.574 8.753-1.049 2.646-2.503 4.929-4.36 6.852-1.858 1.925-4.087 3.421-6.688 4.491-2.601 1.07-5.432 1.607-8.49 1.607-1.487 0-2.973-.132-4.459-.395l-2.951 11.869h-9.704l10.884-45.37c1.748-.524 3.748-.994 5.999-1.41 2.252-.415 4.689-.622 7.312-.622 2.448 0 4.558.371 6.327 1.114 1.771.743 3.224 1.76 4.361 3.049 1.136 1.29 1.977 2.798 2.523 4.524.546 1.726.82 3.574.82 5.542m-23.802 13.442c.743.175 1.661.262 2.754.262 1.704 0 3.256-.316 4.655-.951 1.398-.633 2.59-1.518 3.574-2.655.982-1.136 1.747-2.501 2.294-4.098.546-1.595.819-3.354.819-5.278 0-1.879-.416-3.475-1.245-4.787-.831-1.311-2.273-1.967-4.327-1.967-1.4 0-2.711.131-3.935.394l-4.589 19.08"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,2 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80" height="76.8" version="1.1" viewBox="0 0 80 76.8" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(.13338 0 0 -.13338 0 76.8)"><g transform="scale(.1)" fill="#ff424d" fill-rule="evenodd"><path d="m3844.9 5757.8c-1190.8 0-2159.5-969.65-2159.5-2161.6 0-1188.3 968.78-2155.1 2159.5-2155.1 1187.1 0 2152.8 966.79 2152.8 2155.1 0 1191.9-965.74 2161.6-2152.8 2161.6"/><path d="M 0,0 H 1054.41 V 5757.81 H 0 V 0"/></g></g></svg>

Before

Width:  |  Height:  |  Size: 505 B

View File

@ -1,33 +0,0 @@
moduledir = join_paths(modulesdir, 'cambalache', 'app')
gnome.compile_resources('app',
'app.gresource.xml',
gresource_bundle: true,
install: true,
install_dir: pkgdatadir,
)
conf = configuration_data()
conf.set('VERSION', meson.project_version())
conf.set('PYTHON', python_bin.full_path())
conf.set('localedir', localedir)
conf.set('pkgdatadir', pkgdatadir)
configure_file(
input: 'cambalache.in',
output: 'cambalache',
configuration: conf,
install: true,
install_dir: get_option('bindir')
)
install_data([
'__init__.py',
'cmb_application.py',
'cmb_scrolled_window.py',
'cmb_window.py',
'cmb_tutor.py',
'cmb_tutorial.py',
],
install_dir: moduledir)

View File

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

View File

@ -1,290 +0,0 @@
<?xml version='1.0' encoding='UTF-8' standalone='no'?>
<!DOCTYPE cambalache-project SYSTEM "cambalache-project.dtd">
<!-- Created with Cambalache 0.97.1 -->
<cambalache-project version="0.96.0" target_tk="gtk-4.0">
<gresources filename="cambalache.gresource.xml" sha256="fdcf4cd517493f548aa4b4fe206ff7762cee9cdda7ec5a85a718b46eb1c4731b"/>
<gresources filename="app/app.gresource.xml" sha256="3684aa78fce08d8e81d0907317214aeb179c5aea091dd0df405476b43e286941"/>
<css filename="cambalache.css" priority="400" is_global="1"/>
<css filename="app/cambalache.css" is_global="0"/>
<ui template-class="CmbChildTypeComboBox">
<content><![CDATA[<interface>
<!-- interface-name CmbChildTypeComboBox -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbChildTypeComboBox" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbColorEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbColorEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbColorEntry" parent="GtkBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbEnumComboBox">
<content><![CDATA[<interface>
<!-- interface-name CmbEnumComboBox -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbEnumComboBox" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFileEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbFileEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbFileEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFlagsEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbFlagsEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbFlagsEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbIconNameEntry">
<content><![CDATA[<interface>
<!-- interface-name CmbIconNameEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbIconNameEntry" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbObjectChooser">
<content><![CDATA[<interface>
<!-- interface-name CmbObjectChooser -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbObjectChooser" parent="GtkEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSourceView">
<property id="lang" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbSourceView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSourceView" parent="GtkTextView"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSpinButton">
<content><![CDATA[<interface>
<!-- interface-name CmbSpinButton -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSpinButton" parent="GtkSpinButton"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbSwitch">
<content><![CDATA[<interface>
<!-- interface-name CmbSwitch -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbSwitch" parent="GtkSwitch"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTextBuffer">
<content><![CDATA[<interface>
<!-- interface-name CmbTextBuffer -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTextBuffer" parent="GtkTextBuffer"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTextView">
<content><![CDATA[<interface>
<!-- interface-name CmbTextView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTextView" parent="GtkScrolledWindow"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbToplevelChooser">
<property id="derivable-only" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbToplevelChooser -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbToplevelChooser" parent="GtkComboBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTranslatablePopover">
<content><![CDATA[<interface>
<!-- interface-name CmbTranslatablePopover -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTranslatablePopover" parent="GtkPopover"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTranslatableWidget" filename="control/cmb_translatable_widget.ui" sha256="b3178157210f93308b92d99f933cb8d04f2de0278ad75680c7460e2c520b1684"/>
<ui template-class="CasildaCompositor">
<signal id="context-menu"/>
<content><![CDATA[<interface>
<!-- interface-name CmbCompositor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CasildaCompositor" parent="GtkDrawingArea"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTypeChooserPopover">
<property id="category" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="derived-type-id" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="parent-type-id" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="show-categories" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<property id="uncategorized-only" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbTypeChooserPopover -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbTypeChooserPopover" parent="GtkPopover"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbAccessibleEditor">
<content><![CDATA[<interface>
<!-- interface-name CmbAccessibleEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbAccessibleEditor" parent="GtkGrid"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbUIRequiresEditor">
<content><![CDATA[<interface>
<!-- interface-name CmbUIRequiresEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbUIRequiresEditor" parent="GtkGrid"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbScrolledWindow">
<content><![CDATA[<interface>
<!-- interface-name CmbScrolledWindow -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbScrolledWindow" parent="GtkScrolledWindow"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbObjectEditor">
<property id="lang" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="layout" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
<content><![CDATA[<interface>
<!-- interface-name CmbObjectEditor -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbObjectEditor" parent="GtkBox"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbListView">
<content><![CDATA[<interface>
<!-- interface-name CmbListView -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbListView" parent="GtkListView"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbTypeChooserWidget" filename="cmb_type_chooser_widget.ui" sha256="15dedacc452656abdb9d245ec876370705c610422201213d944c1f6329c5c8f6"/>
<ui template-class="CmbSignalEditor" filename="cmb_signal_editor.ui" sha256="df7ca002e39a3a8e16a7334e48d8b1a7847972b0a3f5213f71ba154a70194a1d"/>
<ui template-class="CmbObjectDataEditor" filename="cmb_object_data_editor.ui" sha256="4a590fc58d66e781f731134214a9fdeefabd8b8c11edcc50f6530cac81f796d1"/>
<ui template-class="CmbContextMenu" filename="cmb_context_menu.ui" sha256="81eba3adf715348a5c03ef4cbc151eebd5d9aa8b5a14c5968232f68a61ae573c"/>
<ui template-class="CmbDBInspector" filename="cmb_db_inspector.ui" sha256="4451cdb08d24bd4a802ea692c0ebb4ef46af13152984c0b435d29bf4eb7dab55"/>
<ui filename="app/cmb_shortcuts.ui" sha256="d7ac37fd2430788a9e210ed4bc84dcfeba5609bdcc801afb192bfd900c7a8883"/>
<ui template-class="CmbFileButton" filename="control/cmb_file_button.ui" sha256="f859b4f85d7c80c1fef69b68ebb9129423d9c72fdb38d304132784f7361cbbfd">
<property id="dialog-title" type-id="gchararray" disable-inline-object="0" required="0" disabled="0"/>
<property id="use-open" type-id="gboolean" disable-inline-object="0" required="0" disabled="0"/>
</ui>
<ui template-class="CmbNotificationListView" filename="cmb_notification_list_view.ui" sha256="13622645038ef2aaa154f74cd300f9c0fa0dccf69d45d6c9376f9034e6ee57fb"/>
<ui template-class="CmbVersionNotificationView" filename="cmb_version_notification_view.ui" sha256="9a3ced46b90eb7e425d1c345853c4e8e908870c61c75475f7e20ce3c9ee8cec6"/>
<ui template-class="CmbMessageNotificationView" filename="cmb_message_notification_view.ui" sha256="debeffd184e225d82ed29ac590654b8160363e8d5606366dc8acb3ff9840fee3"/>
<ui template-class="CmbPollNotificationView" filename="cmb_poll_notification_view.ui" sha256="8f47a1e503b85eb5ac3ac54962a40fc2237588e1216afba859a3016a1dcfc121"/>
<ui template-class="CmbPollOptionCheck" filename="cmb_poll_option_check.ui" sha256="aa433f201dc1863f3727e1baa2c4cc239192a1ae4c53553de69d529ba2cc6fed"/>
<ui template-class="CmbNotificationListRow" filename="cmb_notification_list_row.ui" sha256="5ef66fcc24e10d40a91ff0eada84f6aa8a595e368961364d98fde1800755edfc"/>
<ui template-class="CmbPixbufEntry">
<requires>CmbFileEntry</requires>
<content><![CDATA[<interface>
<!-- interface-name CmbPixbufEntry -->
<!-- interface-copyright Juan Pablo Ugarte -->
<template class="CmbPixbufEntry" parent="CmbFileEntry"/>
</interface>
]]></content>
</ui>
<ui template-class="CmbFragmentEditor" filename="cmb_fragment_editor.ui" sha256="0c2de873c689cd60558a835a39edaf4e1d5e68ef3afeb157628e13fd57282057">
<requires>CmbSourceView</requires>
</ui>
<ui template-class="CmbTypeChooser" filename="cmb_type_chooser.ui" sha256="1b4be880ede6d6d6e88e452418c435640d91f5f61cad97b3f82f44429247e5e6">
<requires>CmbTypeChooserPopover</requires>
<signal id="chooser-popdown"/>
<signal id="chooser-popup"/>
<signal id="type-selected"/>
</ui>
<ui template-class="CmbView" filename="cmb_view.ui" sha256="6b9251d30d6e4751b8b04b2af923eec9a8f00d590a322d96bf080fb200b58424">
<requires>CasildaCompositor</requires>
<requires>CmbSourceView</requires>
<requires>CmbDBInspector</requires>
<signal id="placeholder-activated"/>
<signal id="placeholder-selected"/>
</ui>
<ui template-class="CmbGResourceEditor" filename="cmb_gresource_editor.ui" sha256="2050887ef1c45facb6ebff14500214bb035e6808ca61d2a7d661e696d79026ca">
<requires>CmbFileButton</requires>
<requires>CmbEntry</requires>
</ui>
<ui template-class="CmbCSSEditor" filename="cmb_css_editor.ui" sha256="6f39c9cb2f112dac9da415322792a717b8c0a80845dcbb2edacd7695388cba63">
<requires>CmbFileButton</requires>
<requires>CmbSourceView</requires>
</ui>
<ui template-class="CmbUIEditor" filename="cmb_ui_editor.ui" sha256="70e272e2c6c499a5424c6019154cd8338d9edde1fa111ad592a1c104019bb7ee">
<requires>CmbTextBuffer</requires>
<requires>CmbFileButton</requires>
<requires>CmbEntry</requires>
<requires>CmbToplevelChooser</requires>
</ui>
<ui template-class="CmbWindow" filename="app/cmb_window.ui" sha256="df07e3e03b88f9b097ad7d65efa15923d339db83b9d4a7c015a312a71f8c9685">
<requires>CmbNotificationListView</requires>
<requires>CmbScrolledWindow</requires>
<requires>CmbObjectEditor</requires>
<requires>CmbSignalEditor</requires>
<requires>CmbAccessibleEditor</requires>
<requires>CmbFragmentEditor</requires>
<requires>CmbUIEditor</requires>
<requires>CmbUIRequiresEditor</requires>
<requires>CmbCSSEditor</requires>
<requires>CmbGResourceEditor</requires>
<requires>CmbTypeChooser</requires>
<requires>CmbView</requires>
<requires>CmbSourceView</requires>
<requires>CmbListView</requires>
<css-provider>app/cambalache.css</css-provider>
</ui>
</cambalache-project>

View File

@ -1,131 +0,0 @@
/*
* cambalache.css
*
* 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
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Author: Juan Pablo Ugarte <juanpablougarte@gmail.com>
*
*/
CmbView {
background-color: @theme_base_color;
}
popover.cmb-icon-chooser iconview:not(:selected) {
background-color: unset;
}
button.hidden,
CmbPropertyLabel.hidden > box > image {
opacity: 0;
transition: opacity 200ms ease-in-out 0;
}
button.hidden:hover,
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;
padding: unset;
margin: unset;
border: unset;
background: unset;
box-shadow: unset;
outline: unset;
}
CmbPropertyLabel > box > label {
padding: 2px 4px;
}
CmbPropertyLabel:focus > box > label {
/*
FIXME: use focus_border_color
$focus_border_color: if($variant == 'light', transparentize($selected_bg_color, 0.5), transparentize($selected_bg_color, 0.3));
*/
outline-color: color-mix(in srgb, var(--accent-bg-color) 60%, transparent);
outline-offset: -2px;
outline-width: 2px;
outline-style: solid;
border-radius: 6px;
}
CmbPropertyLabel.modified > box > label {
font-style: italic;
}
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;
}

View File

@ -1,31 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<gresources>
<gresource prefix="/ar/xjuan/Cambalache">
<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_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>

View File

@ -1,236 +0,0 @@
#
# CmbAccessibleEditor - Cambalache Accessible Editor
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk
from .cmb_object import CmbObject
from .control import cmb_create_editor, CmbEnumComboBox
from .cmb_property_label import CmbPropertyLabel
from . import utils, _
class CmbAccessibleEditor(Gtk.Box):
__gtype_name__ = "CmbAccessibleEditor"
def __init__(self, **kwargs):
self.__object = None
self.__bindings = []
self.__accessibility_metadata = None
self.__role_filter_model = None
super().__init__(**kwargs)
self.props.orientation = Gtk.Orientation.VERTICAL
self.__role_box = Gtk.Box(spacing=6)
self.__role_box.append(Gtk.Label(label="accessible-role"))
self.__role_combobox = CmbEnumComboBox()
self.__role_box.append(self.__role_combobox)
self.append(self.__role_box)
self.__box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
self.append(self.__box)
def bind_property(self, *args):
binding = GObject.Object.bind_property(*args)
self.__bindings.append(binding)
return binding
def __on_expander_expanded(self, expander, pspec, revealer):
expanded = expander.props.expanded
if expanded:
revealer.props.transition_type = Gtk.RevealerTransitionType.SLIDE_DOWN
else:
revealer.props.transition_type = Gtk.RevealerTransitionType.SLIDE_UP
revealer.props.reveal_child = expanded
def __update_view(self):
for child in utils.widget_get_children(self.__box):
self.__box.remove(child)
if self.__object is None or not self.__object.info.is_a("GtkWidget"):
return
obj = self.__object
if obj.project.target_tk == "gtk-4.0":
if not self.__object.info.is_a("GtkWidget"):
self.__role_box.hide()
return
self.__role_box.show()
prop = self.__object.properties_dict["accessible-role"]
if prop.value in ["none", "presentation"]:
# No need to set properties if role none or presentation
return
role_data = self.__object.project.db.accessibility_metadata.get(prop.value, None)
if role_data:
role_properties = role_data["properties"]
role_states = role_data["states"]
else:
role_properties = None
role_states = None
a11y_data = [
("CmbAccessibleProperty", _("Properties"), len("cmb-a11y-properties"), role_properties),
("CmbAccessibleState", _("States"), len("cmb-a11y-states"), role_states),
("CmbAccessibleRelation", _("Relations"), None, None),
]
else:
type_actions = self.__object.project.db.accessibility_metadata.get(obj.type_id, [])
a11y_data = [
("CmbAccessibleAction", _("Actions"), len("cmb-a11y-actions"), type_actions),
("CmbAccessibleProperty", _("Properties"), None, None),
("CmbAccessibleRelation", _("Relations"), None, None),
]
self.__role_box.hide()
properties = obj.properties_dict
for owner_id, title, prefix_len, allowed_ids in a11y_data:
info = obj.project.type_info.get(owner_id, None)
if info is None:
continue
# Editor count
i = 0
# Grid for all editors
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4)
# Accessible iface properties
for property_id in info.properties:
# Ignore properties or status not for this role
if prefix_len and allowed_ids is not None and property_id[prefix_len:] not in allowed_ids:
continue
prop = properties.get(property_id, None)
if prop is None or prop.info is None:
continue
editor = cmb_create_editor(prop.project, prop.info.type_id, prop=prop)
if editor is None:
return None, None
self.bind_property(
prop,
"value",
editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
label = CmbPropertyLabel(prop=prop, bindable=False)
grid.attach(label, 0, i, 1, 1)
grid.attach(editor, 1, i, 1, 1)
i += 1
if i == 0:
continue
# Create expander/revealer to pack editor grid
expander = Gtk.Expander(label=f"<b>{title}</b>", use_markup=True, expanded=True)
revealer = Gtk.Revealer(reveal_child=True)
expander.connect("notify::expanded", self.__on_expander_expanded, revealer)
revealer.set_child(grid)
self.__box.append(expander)
self.__box.append(revealer)
self.show()
def __on_object_property_changed_notify(self, obj, prop):
if prop.property_id == "accessible-role":
self.__update_view()
def __visible_func(self, model, iter, data):
if self.__accessibility_metadata is None:
return False
name, nick, value = model[iter]
role_data = self.__accessibility_metadata.get(nick, None)
if role_data:
# Ignore abstract roles
if nick != "none" and role_data.get("is_abstract", False):
return False
return True
@GObject.Property(type=CmbObject)
def object(self):
return self.__object
@object.setter
def _set_object(self, obj):
if obj == self.__object:
return
if self.__object and self.__object.info.is_a("GtkWidget"):
self.__object.disconnect_by_func(self.__on_object_property_changed_notify)
self.__role_combobox.props.model = None
self.__accessibility_metadata = None
for binding in self.__bindings:
binding.unbind()
self.__bindings = []
self.__object = obj
if self.__object and self.__object.info.is_a("GtkWidget"):
self.__object.connect("property-changed", self.__on_object_property_changed_notify)
self.__accessibility_metadata = self.__object.project.db.accessibility_metadata
a11y_info = self.__object.project.type_info.get("GtkAccessibleRole", None)
if a11y_info:
a11y_info = self.__object.project.type_info.get("GtkAccessibleRole", None)
self.__role_filter_model = Gtk.TreeModelFilter(child_model=a11y_info.enum)
self.__role_filter_model.set_visible_func(self.__visible_func)
self.__role_combobox.info = a11y_info
self.__role_combobox.props.model = self.__role_filter_model
prop = self.__object.properties_dict.get("accessible-role", None)
self.bind_property(
prop,
"value",
self.__role_combobox,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self.__update_view()
Gtk.WidgetClass.set_css_name(CmbAccessibleEditor, "CmbAccessibleEditor")

View File

@ -1,43 +0,0 @@
#
# Cambalache Object wrappers base class
#
# Copyright (C) 2021 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
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)
def db_get(self, query, pk):
c = self.project.db.execute(query, pk)
row = c.fetchone()
c.close()
return row[0] if row is not None else None
def db_set(self, query, pk, value):
self.project.db.execute(query, (value,) + pk)

View File

@ -1,86 +0,0 @@
#
# Blueprint compiler integration functions
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import io
try:
import blueprintcompiler as bp
from blueprintcompiler import parser, tokenizer
from blueprintcompiler.decompiler import decompile_string
from blueprintcompiler.outputs import XmlOutput
except Exception:
bp = None
class CmbBlueprintError(Exception):
def __init__(self, message, errors=[]):
super().__init__(message)
self.errors = errors
class CmbBlueprintUnsupportedError(CmbBlueprintError):
pass
class CmbBlueprintMissingError(CmbBlueprintError):
def __init__(self):
super().__init__("blueprintcompiler is not available")
def cmb_blueprint_decompile(data: str) -> str:
if bp is None:
raise CmbBlueprintMissingError()
try:
retval = decompile_string(data)
except bp.decompiler.UnsupportedError as e:
raise CmbBlueprintUnsupportedError(str(e))
except Exception as e:
raise CmbBlueprintError(str(e))
return retval
def cmb_blueprint_compile(data: str) -> str:
if bp is None:
raise CmbBlueprintMissingError()
tokens = tokenizer.tokenize(data)
ast, errors, warnings = parser.parse(tokens)
if errors:
f = io.StringIO("")
errors.pretty_print("temp", data, f)
f.seek(0)
raise CmbBlueprintError(f.read(), errors=errors)
if ast is None:
raise CmbBlueprintError("AST is None")
# Ignore warnings
retval = XmlOutput().emit(ast)
return retval.encode()

View File

@ -1,141 +0,0 @@
#
# CmbContextMenu - Cambalache UI Editor
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
from gi.repository import GObject, GLib, Gio, Gdk, Gtk
from cambalache import _
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_context_menu.ui")
class CmbContextMenu(Gtk.PopoverMenu):
__gtype_name__ = "CmbContextMenu"
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_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", self.__on_target_tk_notify)
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 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"]
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 = []
dirs += GLib.get_system_data_dirs()
dirs.append(GLib.get_user_data_dir())
# Add /themes to every dir
dirs = list(map(lambda d: os.path.join(d, "themes"), dirs))
# Append ~/.themes
dirs.append(os.path.join(GLib.get_home_dir(), ".themes"))
for path in dirs:
if not os.path.isdir(path):
continue
for theme in os.listdir(path):
tpath = os.path.join(path, theme, gtk_path, "gtk.css")
if os.path.exists(tpath):
themes.append(theme)
# Dedup and sort
themes = list(dict.fromkeys(themes))
for theme in sorted(themes):
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 = (x, y)
r.width = r.height = 0
self.set_pointing_to(r)
self.popup()

View File

@ -1,57 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- 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="menu-model">menu_model</property>
</template>
</interface>

View File

@ -1,161 +0,0 @@
#
# Cambalache CSS wrapper
#
# Copyright (C) 2022 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 gi.repository import GObject, Gio
from .cmb_path import CmbPath
from .cmb_objects_base import CmbBaseCSS
from cambalache import _
class CmbCSS(CmbBaseCSS):
__gsignals__ = {
"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):
self._path = None
self._monitor = None
self.__saving = False
super().__init__(**kwargs)
self.connect("notify", self.__on_notify)
self.load_css()
def __on_notify(self, obj, pspec):
if pspec.name not in ["css"]:
self.project._css_changed(self, pspec.name)
if pspec.name == "filename":
self.load_css()
@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):
retval = self.db_get("SELECT priority FROM css WHERE css_id=?;", (self.css_id,))
return retval if retval is not None else 0
@priority.setter
def _set_priority(self, value):
self.db_set("UPDATE css SET priority=? WHERE css_id=?;", (self.css_id,), value if value != 0 else None)
@GObject.Property(type=object)
def provider_for(self):
c = self.project.db.cursor()
retval = []
for row in c.execute("SELECT ui_id FROM css_ui WHERE css_id=? ORDER BY ui_id;", (self.css_id,)):
retval.append(row[0])
c.close()
return retval
def __on_css_file_changed(self, file_monitor, file, other_file, event_type):
if event_type != Gio.FileMonitorEvent.CHANGES_DONE_HINT:
return
if self.__saving:
self.__saving = False
return
else:
self.emit("file-changed")
def load_css(self):
if not self.project or not self.filename:
return False
dirname = os.path.dirname(self.project.filename)
path = os.path.join(dirname, self.filename)
if os.path.exists(path):
self._path = path
with open(path) as fd:
self.css = fd.read()
fd.close()
if self._monitor:
self._monitor.cancel()
gfile = Gio.File.new_for_path(path)
self._monitor = gfile.monitor(Gio.FileMonitorFlags.NONE, None)
self._monitor.connect("changed", self.__on_css_file_changed)
return True
else:
self._path = None
return False
def save_css(self):
if not self.project or not self.filename:
return
needs_load = False
if self._path is None:
dirname = os.path.dirname(self.project.filename)
self._path = os.path.join(dirname, self.filename)
needs_load = True
self.__saving = True
with open(self._path, "w") as fd:
fd.write(self.css)
if needs_load:
self.notify("filename")
def add_ui(self, ui):
c = self.project.db.cursor()
# Do not use REPLACE INTO, to make sure both INSERT and UPDATE triggers are used
count = self.db_get("SELECT count(css_id) FROM css_ui WHERE css_id=? AND ui_id=?;", (self.css_id, ui.ui_id))
if count == 0:
c.execute("INSERT INTO css_ui (css_id, ui_id) VALUES (?, ?);", (self.css_id, ui.ui_id))
c.close()
self.notify("provider_for")
def remove_ui(self, ui):
c = self.project.db.cursor()
c.execute("DELETE FROM css_ui WHERE css_id=? AND ui_id=?;", (self.css_id, ui.ui_id))
c.close()
self.notify("provider_for")

View File

@ -1,200 +0,0 @@
#
# CmbCSSEditor - Cambalache CSS Editor
#
# 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
# 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 utils, _
from .cmb_css import CmbCSS
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_css_editor.ui")
class CmbCSSEditor(Gtk.Grid):
__gtype_name__ = "CmbCSSEditor"
filename = Gtk.Template.Child()
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()
view = Gtk.Template.Child()
fields = [("filename", "cmb-value"), ("priority", "value"), ("is_global", "active")]
def __init__(self, **kwargs):
self._object = None
self._bindings = []
super().__init__(**kwargs)
self.save_button.set_sensitive(False)
self.priority.set_range(0, 10000)
self.priority.set_increments(10, 100)
@GObject.Property(type=CmbCSS)
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()
if self._object:
self._object.project.disconnect_by_func(self.__on_ui_added_removed)
self._object.disconnect_by_func(self.__on_provider_for_notify)
self._object.disconnect_by_func(self.__on_css_notify)
self._object.disconnect_by_func(self.__on_file_changed)
self._bindings = []
self._object = obj
if obj is None:
self.set_sensitive(False)
return
self.filename.dirname = obj.project.dirname
self.set_sensitive(True)
for field, target in self.fields:
binding = GObject.Object.bind_property(
obj,
field,
getattr(self, field),
target,
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self._bindings.append(binding)
binding = GObject.Object.bind_property(
obj, "css", self.view, "text", GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL
)
self._bindings.append(binding)
obj.project.connect("ui-added", self.__on_ui_added_removed)
obj.project.connect("ui-removed", self.__on_ui_added_removed)
obj.connect("notify::provider-for", self.__on_provider_for_notify)
obj.connect("notify::css", self.__on_css_notify)
obj.connect("file-changed", self.__on_file_changed)
self.__update_provider_for()
self.__update_ui_button_label()
@Gtk.Template.Callback("on_save_button_clicked")
def __on_save_button_clicked(self, button):
self._object.save_css()
self.infobar.set_revealed(False)
self.save_button.set_sensitive(False)
@Gtk.Template.Callback("on_infobar_response")
def __on_infobar_response(self, infobar, response_id):
if response_id == Gtk.ResponseType.OK:
self.__load_filename()
self.infobar.set_revealed(False)
def __update_provider_for(self):
# Remove all css_ui check buttons
for child in utils.widget_get_children(self.ui_box):
self.ui_box.remove(child)
if self._object is None:
return
ui_list = self._object.project.get_ui_list()
provider_for = self._object.provider_for
# Generate a check button for each UI
for ui in ui_list:
check = Gtk.CheckButton(
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.append(check)
def __on_file_changed(self, obj):
self.infobar.set_revealed(True)
self.save_button.set_sensitive(True)
def __load_filename(self):
if not self.object or not self.object.load_css():
self.save_button.set_sensitive(False)
def __on_check_button_toggled(self, button, ui):
if button.props.active:
self.object.add_ui(ui)
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()
def __on_provider_for_notify(self, obj, pspec):
self.__update_provider_for()
def __on_css_notify(self, obj, pspec):
self.save_button.set_sensitive(True)
@GObject.Signal(
flags=GObject.SignalFlags.RUN_LAST,
return_type=bool,
arg_types=(),
accumulator=GObject.signal_accumulator_true_handled,
)
def remove_css(self):
if self.object:
self.object.project.remove_css(self.object)
return True
Gtk.WidgetClass.set_css_name(CmbCSSEditor, "CmbCSSEditor")

View File

@ -1,203 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_css_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbCSSEditor" parent="GtkGrid">
<property name="column-spacing">3</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">Priority:</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">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>
</child>
<child>
<object class="CmbFileButton" id="filename">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="GtkMenuButton" id="ui_menu_button">
<property name="halign">start</property>
<property name="popover">
<object class="GtkPopover">
<child>
<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>
</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="halign">center</property>
<property name="label" translatable="yes">&lt;small&gt;Note: CSS files need to be loaded at runtime&lt;/small&gt;</property>
<property name="use-markup">1</property>
<property name="valign">center</property>
</object>
</child>
<child>
<object class="GtkButton" id="save_button">
<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="valign">end</property>
<signal name="clicked" handler="on_save_button_clicked"/>
<child>
<object class="GtkImage">
<property name="icon-name">document-save-symbolic</property>
</object>
</child>
</object>
</child>
<layout>
<property name="column">0</property>
<property name="column-span">2</property>
<property name="row">5</property>
</layout>
</object>
</child>
<child>
<object class="GtkFrame">
<property name="child">
<object class="GtkBox">
<property name="orientation">vertical</property>
<child>
<object class="GtkInfoBar" id="infobar">
<property name="message-type">warning</property>
<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>
</child>
<child>
<object class="GtkBox">
<property name="hexpand">1</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="hexpand">1</property>
<property name="label" translatable="yes">The file changed on disk.</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="focusable">1</property>
<property name="vexpand">1</property>
<child>
<object class="CmbSourceView" id="view">
<property name="can-focus">True</property>
<property name="lang">css</property>
<property name="visible">True</property>
</object>
</child>
</object>
</child>
</object>
</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>
</child>
</template>
</interface>

File diff suppressed because it is too large Load Diff

View File

@ -1,294 +0,0 @@
#
# CmbDBInspector
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GLib, GObject, Gio, Gtk
from cambalache import CmbProject
class CmbDBTable(GObject.Object):
def __init__(self, **kwargs):
self.__properties = {}
super().__init__(**kwargs)
def do_get_property(self, prop):
# TODO: read from DB directly
if not prop.name.startswith("cmb-int-") and prop.name not in self.__properties_set__:
raise AttributeError('unknown property %s' % prop.name)
return self.__properties[prop.name]
def do_set_property(self, prop, value):
# TODO: only store PK values when using DB
if not prop.name.startswith("cmb-int-") and prop.name not in self.__properties_set__:
raise AttributeError('unknown property %s' % prop.name)
self.__properties[prop.name] = value
self.notify(prop.name)
class CmbDBStore(GObject.GObject, Gio.ListModel):
__gtype_name__ = 'CmbDBStore'
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, ItemClass, **kwargs):
super().__init__(**kwargs)
self.__item_class = ItemClass
self.__history_index = None
self._objects = []
def do_get_item(self, position):
self.__check_refresh()
return self._objects[position] if position < len(self._objects) else None
def do_get_item_type(self):
return self.__item_class
def do_get_n_items(self):
self.__check_refresh()
return len(self._objects)
def __check_refresh(self):
history_index = self.project.history_index
# Nothing to update if history did not changed
if history_index == self.__history_index:
return
ItemClass = self.__item_class
properties = ItemClass.__properties__
int_properties = ItemClass.__int_properties__
table = ItemClass.__table__
needs_update = False
# Basic optimization, only update if something changed in this table
# TODO: this could be optimized more by check command to know exactly which row changed
if self.__history_index is None or table.startswith("history") or table in ["global", "__profile__"]:
needs_update = True
else:
change_table = table[7:] if table.startswith("history_") else table
# TODO: detect command compression
for row in self.project.db.execute(
"SELECT table_name FROM history WHERE history_id >= ? ORDER BY history_id;", (self.__history_index, )
):
table_name, = row
if table_name == change_table:
needs_update = True
break
self.__history_index = history_index
if not needs_update:
return
# Emit signal to clear model
n_items = len(self._objects)
if n_items:
self._objects = []
self.items_changed(0, n_items, 0)
if len(ItemClass.__pk__):
pk_columns = ",".join(ItemClass.__pk__)
else:
pk_columns = "rowid"
for row in self.project.db.execute(f"SELECT * FROM {table} ORDER BY {pk_columns};"):
item = ItemClass()
for i, val in enumerate(row):
property_id = properties[i]
if property_id in int_properties:
item.set_property(f"cmb-int-{property_id}", val if val is not None else 0)
item.set_property(property_id, val)
self._objects.append(item)
# Emit signal to populate model
self.items_changed(0, 0, len(self._objects))
class TableView(Gtk.ColumnView):
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, ItemClass, **kwargs):
super().__init__(**kwargs)
self.props.show_row_separators = True
self.props.show_column_separators = True
self.props.reorderable = False
self.__model = None
self.__filter_model = None
self.__item_class = ItemClass
for property_id in ItemClass.__properties__:
factory = Gtk.SignalListItemFactory()
factory.connect("setup", self._on_factory_setup)
factory.connect("bind", self._on_factory_bind, property_id)
factory.connect("unbind", self._on_factory_unbind)
col = Gtk.ColumnViewColumn(title=property_id, factory=factory)
if property_id in ItemClass.__int_properties__:
property_expression = Gtk.PropertyExpression.new(ItemClass, None, f"cmb-int-{property_id}")
sorter = Gtk.NumericSorter()
else:
property_expression = Gtk.PropertyExpression.new(ItemClass, None, property_id)
sorter = Gtk.StringSorter()
col.props.resizable = True
col.props.expand = True
sorter.set_expression(property_expression)
col.set_sorter(sorter)
self.append_column(col)
# TODO: keep track of project changes only while we are showing this model
self.connect("map", self.__on_map)
self.project.connect("changed", self.__on_project_changed)
def __update_label(self, item, label, property_id):
val = str(item.get_property(property_id))
label.set_text(val if val else "")
def __on_item_notify(self, item, pspec, label):
self.__update_label(item, label, pspec.name)
def _on_factory_setup(self, factory, list_item):
label = Gtk.Inscription()
list_item.set_child(label)
def _on_factory_bind(self, factory, list_item, property_id):
label = list_item.get_child()
item = list_item.get_item()
self.__update_label(item, label, property_id)
item.connect(f"notify::{property_id}", self.__on_item_notify, label)
def _on_factory_unbind(self, factory, list_item):
item = list_item.get_item()
item.disconnect_by_func(self.__on_item_notify)
def __on_map(self, w):
# Trigger check refresh
if self.__model is not None:
self.__model.get_n_items()
return
# Load model when widget is shown
self.__model = CmbDBStore(self.__item_class, project=self.project)
self.__filter_model = Gtk.SortListModel(model=self.__model, sorter=self.get_sorter())
self.set_model(Gtk.NoSelection(model=self.__filter_model))
def __on_project_changed(self, project):
# Trigger check refresh
if self.__model is not None and self.is_visible():
self.__model.get_n_items()
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_db_inspector.ui")
class CmbDBInspector(Gtk.Box):
__gtype_name__ = "CmbDBInspector"
stack = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__project = None
self.__table_classes = None
super().__init__(**kwargs)
self.connect("map", self.__on_map)
@GObject.Property(type=CmbProject)
def project(self):
return self.__project
@project.setter
def _set_project(self, project):
self.__project = project
def __init_tables(self):
db = self.project.db
self.__table_classes = {}
for row in db.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"):
table, = row
if table.startswith("sqlite_"):
continue
klass = self.__class_from_table(table)
self.__table_classes[table] = klass
def _metadata_from_table(self, table):
db = self.project.db
properties = []
gproperties = {}
int_properties = set()
pk_list = []
for row in db.execute(f"PRAGMA table_info({table});"):
col = row[1]
col_type = row[2]
pk = row[5]
name = col.replace("_", "-")
properties.append(name)
if col_type == "INTEGER":
int_properties.add(name)
gproperties[f"cmb-int-{name}"] = (int, "", "", GLib.MININT, GLib.MAXINT, 0, GObject.ParamFlags.READWRITE)
gproperties[name] = (str, "", "", None, GObject.ParamFlags.READWRITE)
if pk:
pk_list.append(col)
return properties, gproperties, int_properties, pk_list
def __class_from_table(self, table):
class_name = f"CmbDBTable_{table}"
properties, gproperties, int_properties, pk = self._metadata_from_table(table)
klass = type(class_name, (CmbDBTable,), dict(
__table__=table,
__gproperties__=gproperties,
__properties__=properties,
__properties_set__=set(properties),
__int_properties__=int_properties,
__pk__=pk)
)
return klass
def __populate_stack(self):
for table, klass in self.__table_classes.items():
sw = Gtk.ScrolledWindow(
hexpand=True,
vexpand=True,
propagate_natural_width=True,
propagate_natural_height=True)
view = TableView(klass, project=self.__project)
sw.set_child(view)
self.stack.add_titled(sw, table, table)
def __on_map(self, w):
if self.__table_classes is None and self.__project is not None:
self.__init_tables()
self.__populate_stack()

View File

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

View File

@ -1,163 +0,0 @@
#
# CmbDBmigration - Cambalache DataBase Migration functions
#
# 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
#
def ensure_columns_for_0_7_5(table, data):
if table == "object":
# Append position column
return [row + (None,) for row in data]
elif table in ["object_property", "object_layout_property"]:
# Append translation_context, translation_comments columns
return [row + (None, None) for row in data]
return data
def migrate_table_data_to_0_7_5(c, table, data):
if table == "object":
c.execute(
"""
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 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;
"""
)
def ensure_columns_for_0_9_0(table, data):
if table == "object_property":
# Append inline_object_id column
return [row + (None,) for row in data]
return data
def migrate_table_data_to_0_9_0(c, table, data):
if table == "object_property":
# Remove all object properties with a 0 as value
c.execute(
"""
DELETE FROM temp.object_property AS op
WHERE value = 0 AND
(SELECT property_id FROM temp.property WHERE owner_id=op.owner_id AND property_id=op.property_id AND is_object)
IS NOT NULL;
"""
)
def ensure_columns_for_0_11_2(table, data):
if table in ["object", "ui"]:
# Append custom_text column
return [row + (None,) for row in data]
return data
def ensure_columns_for_0_11_4(table, data):
if table == "object_property":
# Append bind_[source_id owner_id property_id flags] column
return [row + (None, None, None, None) for row in data]
return data
def ensure_columns_for_0_13_1(table, data):
if table == "object_data":
# Append translatable, translation_context, translation_comments columns
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;
"""
)

View File

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

View File

@ -1,86 +0,0 @@
#
# CmbFragmentEditor - Cambalache CSS Editor
#
# 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
# 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
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_fragment_editor.ui")
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.__bindings = []
super().__init__(**kwargs)
@GObject.Property(type=GObject.Object)
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:
return
binding = GObject.Object.bind_property(
obj,
"custom-fragment",
self.view,
"text",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
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")

View File

@ -1,61 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_fragment_editor.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<template class="CmbFragmentEditor" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkLabel">
<property name="halign">start</property>
<property name="label" translatable="yes">Extra fragments:</property>
</object>
</child>
<child>
<object class="GtkStack" id="fragment_stack">
<property name="vexpand">True</property>
<child>
<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>
</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>

View File

@ -1,151 +0,0 @@
#
# Cambalache GResource wrapper
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gio
from .cmb_path import CmbPath
from .cmb_objects_base import CmbBaseGResource
from .cmb_list_error import CmbListError
from cambalache import _
class CmbGResource(CmbBaseGResource, Gio.ListModel):
path_parent = GObject.Property(type=CmbPath, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
self._last_known = None
super().__init__(**kwargs)
self.connect("notify", self.__on_notify)
def __bool__(self):
return True
def __str__(self):
return f"CmbGResource<{self.resource_type}> id={self.gresource_id}"
def __on_notify(self, obj, pspec):
resource_type = self.resource_type
if (resource_type == "gresources" and pspec.name == "gresources-filename") or \
(resource_type == "gresource" and pspec.name == "gresource-prefix") or \
(resource_type == "file" and pspec.name == "file-filename"):
obj.notify("display-name")
self.project._gresource_changed(self, pspec.name)
@GObject.Property(type=CmbBaseGResource)
def parent(self):
if self.resource_type in ["gresource", "file"]:
return self.project.get_gresource_by_id(self.parent_id)
return None
@GObject.Property(type=CmbBaseGResource)
def gresources_bundle(self):
resource_type = self.resource_type
if resource_type == "gresource":
return self.parent
elif resource_type == "file":
return self.parent.parent
return self
@GObject.Property(type=str)
def display_name(self):
resource_type = self.resource_type
if resource_type == "gresources":
gresources_filename = self.gresources_filename
if gresources_filename:
basename, relpath = self.project._get_basename_relpath(self.gresources_filename)
return basename
return _("Unnamed GResource {id}").format(id=self.gresource_id)
elif resource_type == "gresource":
gresource_prefix = self.gresource_prefix
return gresource_prefix if gresource_prefix else _("Unprefixed resource {id}").format(id=self.gresource_id)
elif resource_type == "file":
file_filename = self.file_filename
return file_filename if file_filename else _("Unnamed file {id}").format(id=self.gresource_id)
# GListModel helpers
def _save_last_known_parent_and_position(self):
self._last_known = (self.parent, self.position)
def _update_new_parent(self):
parent = self.parent
position = self.position
# Emit GListModel signal to update model
if parent:
parent.items_changed(position, 0, 1)
parent.notify("n-items")
self._last_known = None
def _remove_from_old_parent(self):
if self._last_known is None:
return
parent, position = self._last_known
# Emit GListModel signal to update model
if parent:
parent.items_changed(position, 1, 0)
parent.notify("n-items")
self._last_known = None
# GListModel iface
def do_get_item(self, position):
gresource_id = self.gresource_id
key = self.db_get(
"SELECT gresource_id FROM gresource WHERE parent_id=? AND position=?;",
(gresource_id, position)
)
if key is not None:
return self.project.get_gresource_by_id(key)
# This should not happen
return CmbListError()
def do_get_item_type(self):
return CmbBaseGResource
@GObject.Property(type=int)
def n_items(self):
if self.resource_type in ["gresources", "gresource"]:
retval = self.db_get("SELECT COUNT(gresource_id) FROM gresource WHERE parent_id=?;", (self.gresource_id, ))
return retval if retval is not None else 0
else:
return 0
def do_get_n_items(self):
return self.n_items

View File

@ -1,117 +0,0 @@
#
# CmbGResourceEditor - Cambalache GResource Editor
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk
from .cmb_gresource import CmbGResource
from cambalache import getLogger
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_gresource_editor.ui")
class CmbGResourceEditor(Gtk.Box):
__gtype_name__ = "CmbGResourceEditor"
stack = Gtk.Template.Child()
gresources_filename = Gtk.Template.Child()
gresource_prefix = Gtk.Template.Child()
file_filename = Gtk.Template.Child()
file_compressed = Gtk.Template.Child()
file_preprocess = Gtk.Template.Child()
file_alias = Gtk.Template.Child()
fields = [
("gresources", "gresources_filename", "cmb-value"),
("gresource", "gresource_prefix", "cmb-value"),
("file", "file_filename", "cmb-value"),
("file", "file_compressed", "active"),
("file", "file_preprocess", "cmb-value"),
("file", "file_alias", "cmb-value"),
]
def __init__(self, **kwargs):
self._object = None
self._bindings = []
super().__init__(**kwargs)
@GObject.Property(type=CmbGResource)
def object(self):
return self._object
@object.setter
def _set_object(self, obj):
if obj == self._object:
return
for binding in self._bindings:
binding.unbind()
self._bindings = []
self._object = obj
if obj is None:
self.set_sensitive(False)
for for_type, field, target in self.fields:
widget = getattr(self, field)
target_prop = getattr(widget, target)
if isinstance(target_prop, int):
setattr(widget, target, 0)
else:
setattr(widget, target, None)
return
resource_type = obj.resource_type
self.set_sensitive(True)
self.stack.set_visible_child_name(resource_type)
self.gresources_filename.dirname = obj.project.dirname
self.file_filename.dirname = obj.project.dirname
for for_type, field, target in self.fields:
if resource_type != for_type:
continue
binding = GObject.Object.bind_property(
obj,
field,
getattr(self, field),
target,
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self._bindings.append(binding)
@Gtk.Template.Callback("on_add_gresource_button_clicked")
def __on_add_gresource_button_clicked(self, button):
self._object.project.add_gresource("gresource", parent_id=self._object.gresource_id)
@Gtk.Template.Callback("on_add_file_button_clicked")
def __on_add_file_button_clicked(self, button):
self._object.project.add_gresource("file", parent_id=self._object.gresource_id)
Gtk.WidgetClass.set_css_name(CmbGResourceEditor, "CmbGResourceEditor")

View File

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

View File

@ -1,113 +0,0 @@
#
# Cambalache Layout Property wrapper
#
# Copyright (C) 2021 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 CmbBaseLayoutProperty
from .cmb_property_info import CmbPropertyInfo
from . import utils
class CmbLayoutProperty(CmbBaseLayoutProperty):
object = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs):
super().__init__(**kwargs)
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(
"""
SELECT value
FROM object_layout_property
WHERE ui_id=? AND object_id=? AND child_id=? AND owner_id=? AND property_id=?;
""",
(self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id),
)
row = c.fetchone()
return row[0] if row is not None else self.info.default_value
@value.setter
def _set_value(self, value):
c = self.project.db.cursor()
if value is None or value == self.info.default_value:
c.execute(
"""
DELETE FROM object_layout_property
WHERE ui_id=? AND object_id=? AND child_id=? AND owner_id=? AND property_id=?;
""",
(self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id),
)
value = None
else:
# Do not use REPLACE INTO, to make sure both INSERT and UPDATE triggers are used
count = self.db_get(
"""
SELECT count(value)
FROM object_layout_property
WHERE ui_id=? AND object_id=? AND child_id=? AND owner_id=? AND property_id=?;
""",
(self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id),
)
if count:
c.execute(
"""
UPDATE object_layout_property
SET value=?
WHERE ui_id=? AND object_id=? AND child_id=? AND owner_id=? AND property_id=?;
""",
(value, self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id),
)
else:
c.execute(
"""
INSERT INTO object_layout_property (ui_id, object_id, child_id, owner_id, property_id, value)
VALUES (?, ?, ?, ?, ?, ?);
""",
(self.ui_id, self.object_id, self.child_id, self.owner_id, self.property_id, value),
)
c.close()
def _update_version_warning(self):
target = self.object.ui.get_target(self.library_id)
return utils.get_version_warning(target, self.info.version, self.info.deprecated_version, self.property_id)

View File

@ -1,57 +0,0 @@
#
# Cambalache Library Info wrapper
#
# 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
# 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 CmbBaseLibraryInfo
class CmbLibraryInfo(CmbBaseLibraryInfo):
third_party = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.object_types = self.__init_object_types()
self.min_version = self.__init_min_version()
def __init_object_types(self):
prefix = self.prefix
prefix_len = len(prefix)
retval = []
for row in self.project.db.execute("SELECT type_id FROM type WHERE library_id=?", (self.library_id,)):
(type_id,) = row
if type_id.startswith(prefix):
# Remove Prefix from type name
retval.append(type_id[prefix_len:])
return retval
def __init_min_version(self):
row = self.project.db.execute(
"SELECT MIN_VERSION(version) FROM library_version WHERE library_id=?;", (self.library_id,)
).fetchone()
return row[0] if row is not None else None

View File

@ -1,42 +0,0 @@
#
# Cambalache GListModel error item
#
# 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_base import CmbBase
# 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)
@GObject.Property(type=str)
def display_name(self):
return "list error"
@GObject.Property(type=int)
def n_items(self):
return 0

View File

@ -1,260 +0,0 @@
#
# CmbColumnView
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GLib, GObject, Gdk, Gtk
from .cmb_ui import CmbUI
from .cmb_object import CmbObject
from .cmb_gresource import CmbGResource
from .cmb_context_menu import CmbContextMenu
from .cmb_path import CmbPath
from .cmb_project import CmbProject
from .cmb_tree_expander import CmbTreeExpander
class CmbListView(Gtk.ListView):
__gtype_name__ = "CmbListView"
def __init__(self, **kwargs):
self.__project = None
self.__tree_model = None
self.__in_selection_change = False
self.single_selection = Gtk.SingleSelection()
super().__init__(**kwargs)
self.props.has_tooltip = True
self.props.hexpand = True
factory = Gtk.SignalListItemFactory()
factory.connect("setup", self._on_factory_setup)
factory.connect("bind", self._on_factory_bind)
factory.connect("unbind", self._on_factory_unbind)
self.props.factory = factory
self.single_selection.connect("notify::selected-item", self.__on_selected_item_notify)
self.set_model(self.single_selection)
gesture = Gtk.GestureClick(button=Gdk.BUTTON_SECONDARY)
gesture.connect("pressed", self.__on_button_press)
self.add_controller(gesture)
self.connect("activate", self.__on_activate)
self.add_css_class("navigation-sidebar")
self.add_css_class("cmb-list-view")
def __on_button_press(self, gesture, npress, x, y):
expander = self.__get_tree_expander(x, y)
if expander is None or npress != 1:
return False
# Select row at x,y
list_row = expander.get_list_row()
self.single_selection.set_selected(list_row.get_position())
menu = CmbContextMenu()
if self.__project:
menu.target_tk = self.__project.target_tk
menu.set_parent(self)
menu.popup_at(x, y)
return True
def __get_path_parent(self, obj):
if isinstance(obj, CmbObject):
parent = obj.parent
return parent if parent else obj.ui
elif isinstance(obj, CmbGResource):
return obj.path_parent if obj.resource_type == "gresources" else obj.parent
elif isinstance(obj, CmbProject):
return None
return obj.path_parent
def __get_object_ancestors(self, obj):
ancestors = set()
parent = self.__get_path_parent(obj)
while parent:
ancestors.add(parent)
parent = self.__get_path_parent(parent)
return ancestors
def __object_ancestor_expand(self, obj):
ancestors = self.__get_object_ancestors(obj)
i = 0
# Iterate over tree model
# NOTE: only visible/expanded rows are returned
list_row = self.__tree_model.get_row(i)
while list_row:
item = list_row.get_item()
# Return position if we reached the object row
if item == obj:
return i
elif item in ancestors:
# Expand row if its part of the hierarchy
list_row.set_expanded(True)
i += 1
list_row = self.__tree_model.get_row(i)
return None
def __on_project_selection_changed(self, p):
list_row = self.single_selection.get_selected_item()
current_selection = [list_row.get_item()] if list_row else []
selection = self.__project.get_selection()
if selection == current_selection:
return
self.__in_selection_change = True
if len(selection) > 0:
position = self.__object_ancestor_expand(selection[0])
if position is not None:
self.single_selection.select_item(position, True)
else:
self.single_selection.unselect_all()
else:
self.single_selection.unselect_all()
self.__in_selection_change = False
@GObject.Property(type=CmbProject)
def project(self):
return self.__project
@project.setter
def _set_project(self, project):
if self.__project:
self.__project.disconnect_by_func(self.__on_project_selection_changed)
self.__project = project
if project:
self.__tree_model = Gtk.TreeListModel.new(
project,
False,
False,
self.__tree_model_create_func,
None
)
self.single_selection.props.model = self.__tree_model
self.__project.connect("selection-changed", self.__on_project_selection_changed)
else:
self.single_selection.props.model = None
def __tree_model_create_func(self, item, data):
if isinstance(item, CmbObject):
return item
elif isinstance(item, CmbUI):
return item
elif isinstance(item, CmbGResource):
return item
elif isinstance(item, CmbPath):
return item
return None
def __on_selected_item_notify(self, single_selection, pspec):
if self.__in_selection_change or self.__project is None:
return
list_item = single_selection.get_selected_item()
position = single_selection.get_selected()
if list_item is None:
self.__project.set_selection([])
return
item = list_item.get_item()
self.activate_action("list.activate-item", GLib.Variant("u", position))
if item and not isinstance(item, CmbPath):
item = list_item.get_item()
self.__project.set_selection([item])
else:
self.__project.set_selection([])
def _on_factory_setup(self, factory, list_item):
expander = CmbTreeExpander()
list_item.set_child(expander)
def _on_factory_bind(self, factory, list_item):
row = list_item.get_item()
expander = list_item.get_child()
expander.set_list_row(row)
expander.update_bind()
def _on_factory_unbind(self, factory, list_item):
expander = list_item.get_child()
expander.clear_bind()
def __get_tree_expander(self, x, y):
pick = self.pick(x, y, Gtk.PickFlags.DEFAULT)
if pick is None:
return None
if isinstance(pick, Gtk.TreeExpander):
return pick
child = pick.get_first_child()
if child and isinstance(child, Gtk.TreeExpander):
return child
parent = pick.props.parent
if parent and isinstance(parent, Gtk.TreeExpander):
return parent
return None
def __on_activate(self, column_view, position):
item = self.__tree_model.get_item(position)
item.set_expanded(not item.get_expanded())
def do_query_tooltip(self, x, y, keyboard_mode, tooltip):
expander = self.__get_tree_expander(x, y)
if expander is None:
return False
obj = expander.get_item()
if isinstance(obj, CmbObject):
msg = obj.version_warning
if msg:
tooltip.set_text(msg)
return True
return False

View File

@ -1,50 +0,0 @@
#
# CmbMessageNotificationView
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from cambalache import getLogger
from gi.repository import GObject, Gtk
from .cmb_notification import CmbMessageNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_message_notification_view.ui")
class CmbMessageNotificationView(Gtk.Box):
__gtype_name__ = "CmbMessageNotificationView"
notification = GObject.Property(
type=CmbMessageNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Message
title_label = Gtk.Template.Child()
message_label = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
notification = self.notification
self.title_label.props.label = f"<b>{notification.title}</b>"
self.message_label.props.label = notification.message

View File

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

View File

@ -1,378 +0,0 @@
#
# Cambalache notification system
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
import json
import threading
import http.client
import time
import platform
from uuid import uuid4
from urllib.parse import urlparse
from .config import VERSION
from gi.repository import GObject, GLib, Gio, Gdk, Gtk, Adw, HarfBuzz
from cambalache import getLogger
from . import utils
logger = getLogger(__name__)
class CmbBaseData(GObject.GObject):
def __init__(self, **kwargs):
for prop in self.list_properties():
name = prop.name.replace("-", "_")
if name not in kwargs:
continue
value = kwargs[name]
if isinstance(value, dict) and prop.value_type in GTYPE_PTYHON:
Klass = GTYPE_PTYHON[prop.value_type]
kwargs[name] = Klass(**value)
super().__init__(**kwargs)
def dict(self):
retval = {}
for prop in self.list_properties():
name = prop.name.replace("-", "_")
value = self.get_property(prop.name)
if prop.value_type in GTYPE_PTYHON:
retval[name] = value.dict() if value else None
elif not isinstance(value, GObject.Object):
retval[name] = value
return retval
class CmbPollData(CmbBaseData):
id = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
title = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
description = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
options = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
allowed_votes = GObject.Property(type=int, default=1, flags=GObject.ParamFlags.READWRITE)
start_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
end_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
class CmbPollResult(CmbBaseData):
votes = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
total = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
GTYPE_PTYHON = {CmbPollData.__gtype__: CmbPollData, CmbPollResult.__gtype__: CmbPollResult}
class CmbNotification(CmbBaseData):
center = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
type = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
start_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
end_date = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
class CmbVersionNotification(CmbNotification):
version = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
release_notes = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
read_more_url = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
class CmbMessageNotification(CmbNotification):
title = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
message = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
class CmbPollNotification(CmbNotification):
poll = GObject.Property(type=CmbPollData, flags=GObject.ParamFlags.READWRITE)
results = GObject.Property(type=CmbPollResult, flags=GObject.ParamFlags.READWRITE)
my_votes = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
class CmbNotificationCenter(GObject.GObject):
__gsignals__ = {
"new-notification": (GObject.SignalFlags.RUN_FIRST, None, (CmbNotification,)),
}
# Settings
enabled = GObject.Property(type=bool, default=True, flags=GObject.ParamFlags.READWRITE)
uuid = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
next_request = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
notifications = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
store = GObject.Property(type=Gio.ListStore, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.retry_interval = 2
self.user_agent = self.__get_user_agent()
self.store = Gio.ListStore(item_type=CmbNotification)
self.settings = Gio.Settings(schema_id="ar.xjuan.Cambalache.notification")
for prop in ["enabled", "uuid", "next-request", "notifications"]:
self.settings.bind(prop, self, prop.replace("-", "_"), Gio.SettingsBindFlags.DEFAULT)
# Disable notifications if settings backend is ephemeral
if GObject.type_name(Gio.SettingsBackend.get_default()) == "GMemorySettingsBackend":
logger.info("Disabling notifications")
self.enabled = False
self.__load_notifications()
backend = urlparse(os.environ.get("CMB_NOTIFICATION_URL", "https://xjuan.ar:1934"))
self.REQUEST_INTERVAL = 4 if backend.hostname == "localhost" else 24 * 60 * 60
if backend.scheme == "https":
logger.info(f"Backend: {backend.scheme}://{backend.hostname}:{backend.port}")
self.connection = http.client.HTTPSConnection(backend.hostname, backend.port, timeout=8)
else:
self.connection = None
logger.warning(f"{backend.scheme} is not supported, only HTTPS")
return
# Ensure we have a UUID
if not self.uuid:
self.uuid = str(uuid4())
logger.info(f"User Agent: {self.user_agent}")
logger.info(f"UUID: {self.uuid}")
self._get_notification()
def __get_container(self):
if "FLATPAK_ID" in os.environ:
return "flatpak"
elif "APPIMAGE" in os.environ:
return "appimage"
elif "SNAP" in os.environ:
return "snap"
return None
def __get_user_agent(self):
u = platform.uname()
platform_strings = []
table = str.maketrans({",": "\\,"})
if u.system == "Linux":
release = platform.freedesktop_os_release()
system = f"Linux {release['ID']}"
if "VERSION_ID" in release:
system += f" {release['VERSION_ID']}"
if "VERSION_CODENAME" in release:
system += f" {release['VERSION_CODENAME']}"
else:
system = u.system
display_type = GObject.type_name(Gdk.Display.get_default())
backend = display_type.removeprefix("Gdk").removesuffix("Display")
lang = HarfBuzz.language_to_string(HarfBuzz.language_get_default())
extra = []
# Container type
container = self.__get_container()
if container:
extra.append(f"container {container}")
# GSettings backend
settings_backend = Gio.SettingsBackend.get_default()
gsettings_backend = GObject.type_name(settings_backend).removesuffix("SettingsBackend")
for name, lib in [("GLib", GLib), ("Gtk", Gtk), ("Adw", Adw)]:
extra.append(f"{name} {lib.MAJOR_VERSION}.{lib.MINOR_VERSION}.{lib.MICRO_VERSION}")
# Ignore node name as that is private and irrelevant information
for string in [system, u.release, u.version, u.machine, backend, gsettings_backend]:
if not string:
continue
platform_strings.append(string.translate(table))
return f"Cambalache/{VERSION} ({', '.join(platform_strings)}; {'; '.join(extra)}; {lang})"
def __load_notifications(self):
self.store.remove_all()
if not self.notifications:
return
notifications = json.loads(self.notifications)
now = utils.utcnow()
for data in notifications:
if "end_date" in data and now > data["end_date"]:
continue
self.store.append(self.__notification_from_dict(data))
def __save_notifications(self):
notifications = []
for notification in self.store:
notifications.append(notification.dict())
# Store in GSettings
self.notifications = json.dumps(notifications, indent=2, sort_keys=True)
def __notification_from_dict(self, data):
ntype = data.get("type", None)
if ntype == "version":
return CmbVersionNotification(center=self, **data)
elif ntype == "message":
return CmbMessageNotification(center=self, **data)
elif ntype == "poll":
return CmbPollNotification(center=self, **data)
def __get_notification_idle(self, data):
logger.debug(f"Got notification response {json.dumps(data, indent=2, sort_keys=True)}")
if "notification" in data:
notification = self.__notification_from_dict(data["notification"])
self.store.insert(0, notification)
self.__save_notifications()
self.emit("new-notification", notification)
now = int(time.time())
self.next_request = now + self.REQUEST_INTERVAL
self._get_notification()
return GLib.SOURCE_REMOVE
def __get_notification_thread(self):
headers = {
"User-Agent": self.user_agent,
"x-cambalache-uuid": self.uuid,
}
try:
logger.info(f"GET /notification {headers=}")
self.connection.request("GET", "/notification", headers=headers)
response = self.connection.getresponse()
assert response.status == 200
# Reset retry interval
self.retry_interval = 8
data = response.read().decode()
logger.info(f"response={data}")
if data:
GLib.idle_add(self.__get_notification_idle, json.loads(data))
except Exception as e:
# If it fails we just wait a bit before retrying
self.retry_interval *= 2
self.retry_interval = min(self.retry_interval, 256)
logger.info(f"Request error {e}, retrying in {self.retry_interval}s")
GLib.timeout_add_seconds(self.retry_interval, self._get_notification)
self.connection.close()
def __run_in_thread(self, function, *args, **kwargs):
if not self.connection:
logger.warning("No connection defined")
return
if not self.enabled:
logger.info("Notifications disabled")
return
thread = threading.Thread(target=function, args=args, kwargs=kwargs, daemon=True)
thread.start()
def _get_notification(self):
now = int(time.time())
if now >= self.next_request:
self.__run_in_thread(self.__get_notification_thread)
else:
GLib.timeout_add_seconds(self.next_request - now, self._get_notification)
def __poll_vote_idle(self, data):
logger.debug(f"Got vote response {data}")
poll_uuid = data["uuid"]
results = data["results"]
for notification in self.store:
if isinstance(notification, CmbPollNotification) and notification.poll.id == poll_uuid:
notification.results = CmbPollResult(**results)
self.__save_notifications()
break
return GLib.SOURCE_REMOVE
def __poll_vote_exception_idle(self, poll_uuid):
for notification in self.store:
if isinstance(notification, CmbPollNotification) and notification.poll.id == poll_uuid:
notification.my_votes = []
break
return GLib.SOURCE_REMOVE
def __poll_vote_thread(self, method, poll_uuid, votes=None):
headers = {"User-Agent": self.user_agent, "x-cambalache-uuid": self.uuid, "Content-type": "application/json"}
try:
payload = json.dumps({"votes": votes}) if method == "POST" else None
self.connection.request(method, f"/poll/{poll_uuid}", payload, headers)
response = self.connection.getresponse()
assert response.status == 200
data = response.read().decode()
GLib.idle_add(self.__poll_vote_idle, json.loads(data))
except Exception as e:
logger.warning(f"Error voting {e}")
GLib.idle_add(self.__poll_vote_exception_idle, poll_uuid)
self.connection.close()
def poll_vote(self, notification: CmbPollNotification, votes: list[int]):
if self.uuid is None:
return
notification.my_votes = votes
self.__run_in_thread(self.__poll_vote_thread, "POST", notification.poll.id, votes)
def poll_refresh_results(self, notification: CmbPollNotification):
if self.uuid is None:
return
self.__run_in_thread(self.__poll_vote_thread, "GET", notification.poll.id)
def remove(self, notification: CmbNotification):
valid, position = self.store.find(notification)
if valid:
self.store.remove(position)
self.__save_notifications()
notification_center = CmbNotificationCenter()

View File

@ -1,60 +0,0 @@
#
# CmbNotificationListRow
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import datetime
from cambalache import getLogger
from gi.repository import GObject, Gtk
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_notification_list_row.ui")
class CmbNotificationListRow(Gtk.ListBoxRow):
__gtype_name__ = "CmbNotificationListRow"
view = GObject.Property(type=Gtk.Widget, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
box = Gtk.Template.Child()
date_label = Gtk.Template.Child()
close_button = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.props.activatable = False
notification = self.view.notification
start_date = datetime.datetime.utcfromtimestamp(notification.start_date).strftime("%x")
self.date_label.set_label(f"<small>{start_date}</small>")
self.box.append(self.view)
@Gtk.Template.Callback("on_map")
def __on_map(self, w):
self.props.child.props.reveal_child = True
@Gtk.Template.Callback("on_close_button_clicked")
def __on_close_button_clicked(self, button):
notification = self.view.notification
notification.center.remove(notification)

View File

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

View File

@ -1,67 +0,0 @@
#
# CmbNotificationListView
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from cambalache import getLogger
from gi.repository import GObject, Gtk
from .cmb_version_notification_view import CmbVersionNotificationView
from .cmb_message_notification_view import CmbMessageNotificationView
from .cmb_poll_notification_view import CmbPollNotificationView
from .cmb_notification_list_row import CmbNotificationListRow
from .cmb_notification import CmbNotificationCenter, CmbVersionNotification, CmbMessageNotification, CmbPollNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_notification_list_view.ui")
class CmbNotificationListView(Gtk.Box):
__gtype_name__ = "CmbNotificationListView"
notification_center = GObject.Property(type=CmbNotificationCenter, flags=GObject.ParamFlags.READWRITE)
list_box = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.connect("notify::notification-center", self.__on_notification_center_notify)
def __on_notification_center_notify(self, obj, pspec):
self.list_box.bind_model(self.notification_center.store, self.__create_widget_func)
GObject.Object.bind_property(
self.notification_center.store,
"n-items",
self.list_box,
"visible",
GObject.BindingFlags.SYNC_CREATE,
)
def __create_widget_func(self, item):
if isinstance(item, CmbVersionNotification):
view = CmbVersionNotificationView(notification=item)
elif isinstance(item, CmbMessageNotification):
view = CmbMessageNotificationView(notification=item)
elif isinstance(item, CmbPollNotification):
view = CmbPollNotificationView(notification=item)
return CmbNotificationListRow(view=view)

View File

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

View File

@ -1,746 +0,0 @@
#
# Cambalache Object wrapper
#
# Copyright (C) 2021 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_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, Gio.ListModel):
info = GObject.Property(type=CmbTypeInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
__gsignals__ = {
"property-changed": (GObject.SignalFlags.RUN_FIRST, None, (CmbProperty,)),
"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 = 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)
self.connect("notify", self.__on_notify)
if self.project is None:
return
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.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, 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,
project=self.project,
ui_id=self.ui_id,
object_id=self.object_id,
owner_id=name,
property_id=info.property_id,
info=info,
)
# List of property
self.__properties.append(prop)
# Dictionary of properties
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:
return
# parent_id is stored in the DB so its better to cache it
parent_id = self.parent_id
for property_name in property_info:
info = property_info[property_name]
prop = CmbLayoutProperty(
object=self,
project=self.project,
ui_id=self.ui_id,
object_id=parent_id,
child_id=self.object_id,
owner_id=name,
property_id=info.property_id,
info=info,
)
self.__layout.append(prop)
# Dictionary of properties
self.__layout_dict[property_name] = prop
def _property_changed(self, prop):
self.emit("property-changed", prop)
self.project._object_property_changed(self, prop)
def _layout_property_changed(self, prop):
parent = self.project.get_object_by_id(self.ui_id, self.parent_id)
self.emit("layout-property-changed", parent, prop)
self.project._object_layout_property_changed(parent, self, prop)
def __add_signal_object(self, 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.get_id_string() in self.data_dict:
return
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
for row in c.execute("SELECT * FROM object_signal WHERE ui_id=? AND object_id=?;", (self.ui_id, self.object_id)):
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
for row in c.execute(
"SELECT * FROM object_data WHERE ui_id=? AND object_id=? AND parent_id IS NULL;",
(self.ui_id, self.object_id),
):
self.__add_data_object(CmbObjectData.from_row(self.project, *row))
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)
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):
retval = self.db_get(
"SELECT parent_id FROM object WHERE (ui_id, object_id) IS (?, ?);",
(
self.ui_id,
self.object_id,
),
)
return retval if retval is not None else 0
@parent_id.setter
def _set_parent_id(self, value):
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)
def ui(self):
return self.project.get_object_by_id(self.ui_id)
@GObject.Property(type=GObject.Object)
def parent(self):
return self.project.get_object_by_id(self.ui_id, self.parent_id)
def _add_signal(self, signal_pk, owner_id, signal_id, handler, detail=None, user_data=0, swap=False, after=False):
signal = CmbSignal(
project=self.project,
signal_pk=signal_pk,
ui_id=self.ui_id,
object_id=self.object_id,
owner_id=owner_id,
signal_id=signal_id,
handler=handler,
detail=detail,
user_data=user_data if user_data is not None else 0,
swap=swap,
after=after,
)
self.__add_signal_object(signal)
return signal
def add_signal(self, owner_id, signal_id, handler, detail=None, user_data=0, swap=False, after=False):
try:
c = self.project.db.cursor()
c.execute(
"""
INSERT INTO object_signal (ui_id, object_id, owner_id, signal_id, handler, detail, user_data, swap, after)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
""",
(self.ui_id, self.object_id, owner_id, signal_id, handler, detail, user_data, swap, after),
)
signal_pk = c.lastrowid
c.close()
self.project.db.commit()
except Exception as e:
logger.warning(
f"Error adding signal handler {owner_id}:{signal_id} {handler} to object {self.ui_id}.{{self.object_id}} {e}"
)
return None
else:
return self._add_signal(
signal_pk,
owner_id,
signal_id,
handler,
detail=detail,
user_data=user_data if user_data is not None else 0,
swap=swap,
after=after,
)
def _remove_signal(self, signal):
self.__signals.remove(signal)
del self.__signals_dict[signal.signal_pk]
self.emit("signal-removed", signal)
self.project._object_signal_removed(self, signal)
def remove_signal(self, signal):
try:
self.project.db.execute("DELETE FROM object_signal WHERE signal_pk=?;", (signal.signal_pk,))
self.project.db.commit()
except Exception as e:
handler = f"{signal.owner_id}:{signal.signal_id} {signal.handler}"
logger.warning(f"Error removing signal handler {handler} from object {self.ui_id}.{{self.object_id}} {e}")
return False
else:
self._remove_signal(signal)
return True
def _add_data(self, owner_id, data_id, id, info=None):
data = CmbObjectData(
project=self.project,
object=self,
info=info,
ui_id=self.ui_id,
object_id=self.object_id,
owner_id=owner_id,
data_id=data_id,
id=id,
)
self.__add_data_object(data)
return data
def add_data(self, data_key, value=None, comment=None):
try:
value = str(value) if value is not None else None
taginfo = self.info.get_data_info(data_key)
owner_id = taginfo.owner_id
data_id = taginfo.data_id
id = self.project.db.object_add_data(self.ui_id, self.object_id, owner_id, data_id, value, None, comment)
except Exception as e:
logger.warning(f"Error adding data {data_key} {e}")
return None
else:
return self._add_data(owner_id, data_id, id, info=taginfo)
def _remove_data(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.emit("data-removed", data)
self.project._object_data_removed(self, data)
def remove_data(self, data):
try:
assert data.get_id_string() in self.data_dict
self.project.history_push(
_("Remove {key} from {name}").format(key=data.info.key, name=self.display_name_type)
)
self.project.db.execute(
"DELETE FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=?;",
(self.ui_id, self.object_id, data.owner_id, data.data_id, data.id),
)
self.project.db.commit()
self.project.history_pop()
except Exception as e:
logger.warning(f"{self} Error removing data {data}: {e}")
return False
else:
self._remove_data(data)
return True
def reorder_child(self, child, position):
if child is None:
logger.warning("child has to be a CmbObject")
return
if self.ui_id != child.ui_id or self.object_id != child.parent_id:
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=old_position, new=position)
)
db = self.project.db
# Consider this children
#
# label 0
# button 1
# entry 2
# switch 3
# toggle 4
# 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))
# 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))
# 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
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()
name = self.name
name = name if name is not None else self.type_id
self.project.history_push(_("Clear object {name} properties").format(name=name))
properties = []
for row in c.execute(
"SELECT property_id FROM object_property WHERE ui_id=? AND object_id=?;", (self.ui_id, self.object_id)
):
properties.append(row[0])
# Remove all properties from this object
c.execute("DELETE FROM object_property WHERE ui_id=? AND object_id=?;", (self.ui_id, self.object_id))
self.project.history_pop()
c.close()
for prop_id in properties:
prop = self.__properties_dict[prop_id]
prop.notify("value")
self._property_changed(prop)
@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 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

View File

@ -1,213 +0,0 @@
#
# Cambalache Object Data wrapper
#
# Copyright (C) 2022-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
#
from gi.repository import GObject
from .cmb_objects_base import CmbBaseObjectData
from .cmb_type_info import CmbTypeDataInfo
from cambalache import getLogger, _
logger = getLogger(__name__)
class CmbObjectData(CmbBaseObjectData):
__gsignals__ = {
"data-added": (GObject.SignalFlags.RUN_FIRST, None, (CmbBaseObjectData,)),
"data-removed": (GObject.SignalFlags.RUN_FIRST, None, (CmbBaseObjectData,)),
"arg-changed": (GObject.SignalFlags.RUN_FIRST, None, (str,)),
}
parent = GObject.Property(type=CmbBaseObjectData, flags=GObject.ParamFlags.READWRITE)
object = GObject.Property(type=GObject.Object, flags=GObject.ParamFlags.READWRITE)
info = GObject.Property(type=CmbTypeDataInfo, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
self.args = []
self.children = []
super().__init__(**kwargs)
if self.project is None:
return
if self.info is None:
type_info = self.project.type_info.get(self.owner_id, None)
if type_info:
self.info = type_info.find_data_info(self.data_id)
if self.object is None:
self.object = self.project.get_object_by_id(self.ui_id, self.object_id)
self.__populate_children()
self.connect("notify", self._on_notify)
def __str__(self):
return f"CmbObjectData<{self.owner_id}:{self.info.key}> obj={self.ui_id}:{self.object_id} data={self.data_id}:{self.id}"
def get_id_string(self):
return f"{self.owner_id}.{self.id}"
def get_arg(self, key):
c = self.project.db.execute(
"SELECT value FROM object_data_arg WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=? AND key=?;",
(self.ui_id, self.object_id, self.owner_id, self.data_id, self.id, key),
)
row = c.fetchone()
return row[0] if row is not None else None
def _on_notify(self, obj, pspec):
if pspec.name in ["value"]:
self.project._object_data_changed(self)
def _arg_changed(self, key):
self.emit("arg-changed", key)
self.project._object_data_arg_changed(self, key)
def set_arg(self, key, value):
# Prevent potential infinite recursion
val = self.get_arg(key)
if val == value:
return
c = self.project.db.cursor()
try:
if value is None:
c.execute(
"""
DELETE FROM object_data_arg
WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=? AND key=?;
""",
(self.ui_id, self.object_id, self.owner_id, self.data_id, self.id, key),
)
else:
# Do not use REPLACE INTO, to make sure both INSERT and UPDATE triggers are used
count = self.db_get(
"""
SELECT count(value) FROM object_data_arg
WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=? AND key=?;
""",
(self.ui_id, self.object_id, self.owner_id, self.data_id, self.id, key),
)
if count:
c.execute(
"""
UPDATE object_data_arg SET value=?
WHERE ui_id=? AND object_id=? AND owner_id=? AND data_id=? AND id=? AND key=?;
""",
(str(value), self.ui_id, self.object_id, self.owner_id, self.data_id, self.id, key),
)
else:
c.execute(
"""
INSERT INTO object_data_arg (ui_id, object_id, owner_id, data_id, id, key, value)
VALUES (?, ?, ?, ?, ?, ?, ?);
""",
(self.ui_id, self.object_id, self.owner_id, self.data_id, self.id, key, str(value)),
)
self._arg_changed(key)
except Exception as e:
logger.warning(f"{self} Error setting arg {key}={value}: {e}")
c.close()
def __add_child(self, child):
if child in self.children:
return
self.children.append(child)
self.object.data_dict[child.get_id_string()] = child
self.emit("data-added", child)
self.project._object_data_data_added(self, child)
def _remove_child(self, child):
self.children.remove(child)
del self.object.data_dict[child.get_id_string()]
self.emit("data-removed", child)
self.project._object_data_data_removed(self, child)
def _add_child(self, owner_id, data_id, id, info=None):
new_data = CmbObjectData(
project=self.project,
object=self.object,
ui_id=self.ui_id,
object_id=self.object_id,
owner_id=owner_id,
data_id=data_id,
id=id,
parent=self,
info=info,
)
self.__add_child(new_data)
return new_data
def __populate_children(self):
c = self.project.db.cursor()
# Populate children
for row in c.execute(
"SELECT * FROM object_data WHERE ui_id=? AND object_id=? AND owner_id=? AND parent_id=?;",
(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):
try:
value = str(value) if value is not None else None
taginfo = self.info.children.get(data_key)
owner_id = taginfo.owner_id
data_id = taginfo.data_id
id = self.project.db.object_add_data(self.ui_id, self.object_id, owner_id, data_id, value, self.id, comment)
except Exception as e:
logger.warning(f"{self} Error adding child data {data_key}: {e}")
return None
else:
return self._add_child(owner_id, data_id, id, taginfo)
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
else:
self._remove_child(data)
return True

View File

@ -1,293 +0,0 @@
#
# CmbObjectDataEditor - Cambalache Object Data Editor
#
# 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
# 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_type_info import CmbTypeDataInfo
from .cmb_object_data import CmbObjectData
from .control import cmb_create_editor
from cambalache import _
# Everyone knows that debugging is twice as hard as writing a program in the first place.
# So if youre as clever as you can be when you write it, how will you ever debug it?
# -- Brian Kernighan, 1974
#
# TODO: rewrite this!
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_object_data_editor.ui")
class CmbObjectDataEditor(Gtk.Box):
__gtype_name__ = "CmbObjectDataEditor"
info = GObject.Property(type=CmbTypeDataInfo, flags=GObject.ParamFlags.READWRITE)
label = Gtk.Template.Child()
top_box = Gtk.Template.Child()
add_child = Gtk.Template.Child()
add_only_child = Gtk.Template.Child()
remove_button = Gtk.Template.Child()
grid = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__object = None
self.__data = None
self.__size_group = None
self.__value_editor = None
self.__arg_editors = {}
self.__editors = []
super().__init__(**kwargs)
self.__update_view()
@Gtk.Template.Callback("on_add_only_child_clicked")
def __on_add_only_child_clicked(self, button):
info = self.data.info if self.data else self.info
# Do not add a menu if there is only one child type
child_info = list(info.children.values())[0]
self.__on_child_button_clicked(button, child_info)
@Gtk.Template.Callback("on_remove_clicked")
def __on_remove_clicked(self, button):
if self.info:
self.object.remove_data(self.__data)
elif self.__data:
self.__data.parent.remove_data(self.__data)
@GObject.Property(type=GObject.Object)
def object(self):
return self.__object
@object.setter
def _set_object(self, value):
if self.__object:
self.__object.disconnect_by_func(self.__on_data_added)
self.__object.disconnect_by_func(self.__on_data_removed)
self.__object = value
if self.__object:
self.__object.connect("data-added", self.__on_data_added)
self.__object.connect("data-removed", self.__on_data_removed)
@GObject.Property(type=CmbObjectData)
def data(self):
return self.__data
@data.setter
def _set_data(self, value):
if self.__data:
self.__data.disconnect_by_func(self.__on_data_data_added)
self.__data.disconnect_by_func(self.__on_data_data_removed)
self.__data.disconnect_by_func(self.__on_data_arg_changed)
# Clear old editors
for editor in self.__editors:
self.grid.remove(editor)
self.__editors = []
self.__data = value
if self.__data:
self.__data.connect("data-added", self.__on_data_data_added)
self.__data.connect("data-removed", self.__on_data_data_removed)
self.__data.connect("arg-changed", self.__on_data_arg_changed)
def __update_arg(self, key):
if not self.data:
return
editor = self.__arg_editors.get(key, None)
if editor:
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)
def __on_data_data_removed(self, parent, data):
self.__remove_data_editor(data)
def __on_data_arg_changed(self, data, key):
self.__update_arg(key)
def __on_data_added(self, obj, data):
if self.info and self.data is None and self.info == data.info:
self.data = data
self.__update_view()
def __on_data_removed(self, obj, data):
self.__remove_data_editor(data)
def __ensure_object_data(self, history_message):
if self.data:
return False
self.object.project.history_push(history_message)
self.data = self.object.add_data(self.info.key)
if self.__value_editor:
GObject.Object.bind_property(
self.data,
"value",
self.__value_editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
return True
def __on_child_button_clicked(self, button, info):
msg = _("Add {key}").format(key=info.key)
history_pushed = self.__ensure_object_data(msg)
self.data.add_data(info.key)
if history_pushed:
self.object.project.history_pop()
def __context_menu_new(self, info):
popover = Gtk.Popover(position=Gtk.PositionType.BOTTOM)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, visible=True, spacing=4, border_width=4)
# Add children types
for child in info.children:
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.append(button)
popover.set_child(box)
return popover
def __on_arg_notify(self, obj, pspec, info):
msg = _("Set {key} to {value}").format(key=info.key, value=obj.cmb_value)
history_pushed = self.__ensure_object_data(msg)
self.data.set_arg(info.key, obj.cmb_value)
if history_pushed:
self.object.project.history_pop()
def __add(self, editor, label=None):
neditors = len(self.__editors)
if label:
self.grid.attach(label, 0, neditors, 1, 1)
self.__size_group.add_widget(label)
self.grid.attach(editor, 1, neditors, 1, 1)
self.__editors.append(editor)
def __add_data_editor(self, data):
editor = CmbObjectDataEditor(visible=True, hexpand=True, margin_start=16, object=self.object, data=data)
self.__add(editor)
def __remove_data_editor(self, data):
if self.__data == data:
self.data = None
return
for editor in self.__editors:
if data == editor.data:
self.grid.remove(editor)
self.__editors.remove(editor)
break
def __update_view(self):
if self.data is None and self.info is None:
return
info = self.data.info if self.data else self.info
project = self.__object.project
if info is None:
return
nchildren = len(info.children)
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:
self.add_child.props.popover = self.__context_menu_new(info)
self.add_child.set_visible(True)
elif nchildren:
key = list(info.children.keys())[0]
self.add_only_child.props.tooltip_text = _("Add {key}").format(key=key)
self.add_only_child.set_visible(True)
# Item name
self.label.props.label = info.key
self.__size_group = Gtk.SizeGroup(mode=Gtk.SizeGroupMode.HORIZONTAL)
self.__size_group.add_widget(self.label)
# Value
if info.type_id:
editor = cmb_create_editor(project, info.type_id, data=self.data, parent=self.object)
self.__value_editor = editor
if self.data:
GObject.Object.bind_property(
self.data,
"value",
self.__value_editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self.top_box.append(editor)
nargs = len(info.args)
# Arguments
for arg in info.args:
arg_info = info.args[arg]
editor = cmb_create_editor(project, arg_info.type_id, parent=self.object)
self.__arg_editors[arg_info.key] = editor
# Initialize value
self.__update_arg(arg_info.key)
# Listen for editor value changes and update argument
editor.connect("notify::cmb-value", self.__on_arg_notify, arg_info)
# 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.append(editor)
else:
label = Gtk.Label(visible=True, label=arg_info.key, xalign=1)
self.__add(editor, label)
# Current children
if self.data:
for child in self.data.children:
self.__add_data_editor(child)

View File

@ -1,76 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- 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="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkBox">
<property name="spacing">4</property>
<child>
<object class="GtkLabel" id="label"/>
</child>
<child>
<object class="GtkBox" id="top_box">
<property name="halign">start</property>
<property name="hexpand">True</property>
</object>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="focusable">1</property>
<property name="halign">end</property>
<signal name="clicked" handler="on_remove_clicked"/>
<child>
<object class="GtkImage">
<property name="icon-name">user-trash-symbolic</property>
</object>
</child>
<style>
<class name="borderless"/>
</style>
<style>
<class name="hidden"/>
</style>
</object>
</child>
<child>
<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>
</child>
</object>
</child>
<child>
<object class="GtkGrid" id="grid">
<property name="column-spacing">4</property>
<property name="row-spacing">4</property>
</object>
</child>
</template>
</interface>

View File

@ -1,318 +0,0 @@
#
# CmbObjectEditor - Cambalache Object Editor
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk
from .cmb_object import CmbObject
from .cmb_object_data_editor import CmbObjectDataEditor
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.Box):
__gtype_name__ = "CmbObjectEditor"
layout = GObject.Property(type=bool, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY, default=False)
def __init__(self, **kwargs):
self.__object = None
self.__id_label = None
self.__template_switch = None
self.__bindings = []
super().__init__(**kwargs)
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)
# Label
self.__id_label = Gtk.Label(label=_("Object Id"), halign=Gtk.Align.START)
# Id/Class entry
entry = CmbEntry()
self.bind_property(
self.__object,
"name",
entry,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
grid.attach(self.__id_label, 0, 0, 1, 1)
grid.attach(entry, 1, 0, 1, 1)
# Template check
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
if not derivable:
tooltip_text = _("{type} is not derivable.").format(type=self.__object.info.type_id)
label = Gtk.Label(label=_("Template"), halign=Gtk.Align.START, tooltip_text=tooltip_text, sensitive=derivable)
self.__template_switch = Gtk.Switch(
active=is_template, halign=Gtk.Align.START, tooltip_text=tooltip_text, sensitive=derivable
)
self.__template_switch.connect("notify::active", self.__on_template_switch_notify)
self.__update_template_label()
grid.attach(label, 0, 1, 1, 1)
grid.attach(self.__template_switch, 1, 1, 1, 1)
return grid
def __on_shortcut_button_clicked(self, button, type_id):
obj = self.__object
obj.project.add_object(obj.ui_id, type_id, parent_id=obj.object_id)
def __create_child_shortcuts(self, info):
box = Gtk.FlowBox(visible=True, hexpand=True, selection_mode=Gtk.SelectionMode.NONE)
label = Gtk.Label(label=_("Add"), xalign=0, visible=True)
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.append(button)
return box
def __update_template_label(self):
istmpl = self.__object.ui.template_id == self.__object.object_id
self.__id_label.props.label = _("Type Name") if istmpl else _("Object Id")
def __on_template_switch_notify(self, switch, pspec):
self.__object.ui.template_id = self.__object.object_id if switch.props.active else 0
self.__update_template_label()
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 __create_child_type_editor(self):
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=6)
box.append(Gtk.Label(label=_("Child Type"), width_chars=8))
combo = CmbChildTypeComboBox(object=self.__object)
self.bind_property(
self.__object,
"type",
combo,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
box.append(combo)
return box
def __update_view(self):
for child in utils.widget_get_children(self):
self.remove(child)
if self.__object is None:
return
obj = self.__object
parent = obj.parent
is_builtin = obj.info.is_builtin
if self.layout:
if parent is None or is_builtin:
return
# Child Type input
if parent.info.has_child_types():
self.append(self.__create_child_type_editor())
else:
# ID
self.append(self.__create_id_editor())
if obj.type_id == EXTERNAL_TYPE:
label = Gtk.Label(
label=_(
"This object will not be exported, it is only used to make a reference to it. \
It has to be exposed by your application with GtkBuilder expose_object method."
),
halign=Gtk.Align.START,
margin_top=8,
xalign=0,
wrap=True,
)
self.append(label)
self.show()
return
info = parent.info if self.layout and parent else obj.info
for owner_id in [info.type_id] + info.hierarchy:
if self.layout:
owner_id = f"{owner_id}LayoutChild"
info = obj.project.type_info.get(owner_id, None)
if info is None:
continue
# Editor count
i = 0
# Grid for all properties and custom data editors
grid = Gtk.Grid(hexpand=True, row_spacing=4, column_spacing=4)
# Add shortcuts
if not self.layout and len(info.child_type_shortcuts):
shortcuts = self.__create_child_shortcuts(info)
shortcuts.props.margin_start = 14
grid.attach(shortcuts, 0, i, 2, 1)
i += 1
# Properties
properties = obj.layout_dict if self.layout else obj.properties_dict
for property_id in info.properties:
prop = properties.get(property_id, None)
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
self.bind_property(
prop,
"value",
editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
if self.layout:
label = CmbPropertyLabel(layout_prop=prop)
else:
label = CmbPropertyLabel(prop=prop, bindable=not is_builtin)
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)
i += 1
for data_key in info.data:
data = None
# Find data
for d in obj.data:
if d.info.key == data_key:
data = d
break
editor = CmbObjectDataEditor(
visible=True,
hexpand=True,
object=obj,
data=data,
info=info.data[data_key],
)
grid.attach(editor, 0, i, 2, 1)
i += 1
# Continue if class had no editors to add
if i == 0:
continue
# Create expander/revealer to pack editor grid
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.set_child(grid)
self.append(expander)
self.append(revealer)
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
@object.setter
def _set_object(self, obj):
if obj == self.__object:
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:
obj.connect("notify", self.__on_object_notify)
obj.ui.connect("notify", self.__on_object_ui_notify)
self.__update_view()
Gtk.WidgetClass.set_css_name(CmbObjectEditor, "CmbObjectEditor")

File diff suppressed because it is too large Load Diff

View File

@ -1,118 +0,0 @@
#
# CmbPath
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gio
from .cmb_base import CmbBase
from cambalache import _, getLogger
logger = getLogger(__name__)
class CmbPath(CmbBase, Gio.ListModel):
__gtype_name__ = "CmbPath"
path_parent = GObject.Property(type=CmbBase, flags=GObject.ParamFlags.READWRITE)
path = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
# GListModel
self.__items = []
self.__path_items = {}
def __bool__(self):
return True
def __str__(self):
return f"CmbPath<{self.path}>"
@GObject.Property(type=str)
def display_name(self):
return self.path or _("{n} unsaved files").format(n=self.n_items)
def get_path_item(self, directory):
return self.__path_items.get(directory, None)
def add_item(self, item):
if item in self.__items:
return
display_name = item.display_name
is_path = isinstance(item, CmbPath)
if is_path:
self.__path_items[item.path] = item
i = 0
for list_item in self.__items:
if is_path:
if not isinstance(list_item, CmbPath):
break
if display_name < list_item.display_name:
break
elif not isinstance(list_item, CmbPath) and display_name < list_item.display_name:
break
i += 1
item.path_parent = self
self.__items.insert(i, item)
self.items_changed(i, 0, 1)
if not self.path:
self.notify("display-name")
def remove_item(self, item):
if item not in self.__items:
return
if isinstance(item, CmbPath) and item.path in self.__path_items:
del self.__path_items[item.path]
item.path_parent = None
i = self.__items.index(item)
self.__items.pop(i)
self.items_changed(i, 1, 0)
if not self.path:
self.notify("display-name")
# GListModel iface
@GObject.Property(type=int)
def n_items(self):
return len(self.__items)
def do_get_item(self, position):
return self.__items[position] if position < len(self.__items) else None
def do_get_item_type(self):
return CmbBase
def do_get_n_items(self):
return self.n_items

View File

@ -1,142 +0,0 @@
#
# CmbPollNotificationView
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import datetime
from cambalache import _, getLogger
from gi.repository import GObject, Gtk
from . import utils
from .cmb_notification import CmbPollNotification
from .cmb_poll_option_check import CmbPollOptionCheck
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_poll_notification_view.ui")
class CmbPollNotificationView(Gtk.Box):
__gtype_name__ = "CmbPollNotificationView"
notification = GObject.Property(
type=CmbPollNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Poll
title_label = Gtk.Template.Child()
description_label = Gtk.Template.Child()
option_box = Gtk.Template.Child()
total_label = Gtk.Template.Child()
end_date_label = Gtk.Template.Child()
refresh_button = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__option_checks = []
self.__updating = False
super().__init__(**kwargs)
notification = self.notification
poll = notification.poll
active = utils.utcnow() < poll.end_date
self.title_label.props.label = f"<b>{poll.title}</b>"
self.description_label.props.label = poll.description
close_msg = _("<small>• Closes on {date}</small>") if active else _("<small>• Closed on {date}</small>")
end_date = datetime.datetime.fromtimestamp(poll.end_date)
self.end_date_label.props.label = close_msg.format(date=end_date.strftime("%x"))
self.end_date_label.props.tooltip_text = end_date.strftime("%c")
first_check = None
n_option = 0
for option in poll.options:
button = CmbPollOptionCheck(option=option, sensitive=active)
if poll.allowed_votes == 1:
if first_check is None:
first_check = button
else:
button.set_group(first_check)
button.connect("toggled", self.__on_check_button_toggled, n_option)
self.__option_checks.append(button)
self.option_box.append(button)
n_option += 1
self.__update_results()
notification.connect("notify", self.__on_poll_notify)
def __on_check_button_toggled(self, button, n_option):
allowed_votes = self.notification.poll.allowed_votes
if self.__updating or (allowed_votes == 1 and not button.props.active):
return
votes = []
for i, check in enumerate(self.__option_checks):
if check.props.active:
votes.append(i)
if allowed_votes > 1:
not_done = len(votes) < allowed_votes
for i, check in enumerate(self.__option_checks):
if not_done:
check.set_sensitive(True)
elif not check.props.active:
check.set_sensitive(False)
self.notification.center.poll_vote(self.notification, votes)
def __on_poll_notify(self, notification, pspec):
if pspec.name in ["my-votes", "results"]:
self.__update_results()
def __update_results(self):
notification = self.notification
results = notification.results
my_votes = notification.my_votes
if not results or not my_votes:
self.total_label.props.label = ""
for check in self.__option_checks:
check.fraction = -1
return
self.__updating = True
votes = results.votes
total = results.total
for i, check in enumerate(self.__option_checks):
check.set_active(i in my_votes)
check.fraction = votes[i] / total if total else 0
self.total_label.props.label = _("<small>• {total} vote</small>").format(total=results.total)
self.__updating = False
@Gtk.Template.Callback("on_refresh_button_clicked")
def __on_refresh_button_clicked(self, button):
self.notification.center.poll_refresh_results(self.notification)

View File

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

View File

@ -1,76 +0,0 @@
#
# CmbPollOptionCheck
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from cambalache import getLogger
from gi.repository import GLib, GObject, Gtk
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_poll_option_check.ui")
class CmbPollOptionCheck(Gtk.CheckButton):
__gtype_name__ = "CmbPollOptionCheck"
label = Gtk.Template.Child()
bar = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__fraction = None
self.__tick_id = None
super().__init__(**kwargs)
@GObject.Property(type=str)
def option(self):
return self.label.props.label
@option.setter
def _set_option(self, option):
self.label.props.label = option
@GObject.Property(type=float)
def fraction(self):
return self.__fraction
@fraction.setter
def _set_fraction(self, fraction):
self.__fraction = fraction
if fraction < 0:
self.bar.props.visible = False
else:
self.bar.props.visible = True
if self.__tick_id is None:
self.__tick_id = self.add_tick_callback(self.__update_fraction)
def __update_fraction(self, widget, frame_clock):
if self.bar.props.fraction < self.__fraction:
self.bar.props.fraction = min(self.__fraction, self.bar.props.fraction + 0.08)
elif self.bar.props.fraction > self.__fraction:
self.bar.props.fraction = max(self.__fraction, self.bar.props.fraction - 0.08)
else:
self.__tick_id = None
return GLib.SOURCE_REMOVE
return GLib.SOURCE_CONTINUE

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,299 +0,0 @@
#
# Cambalache Property wrapper
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject
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):
object = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs):
self._init = True
super().__init__(**kwargs)
self._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"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(
"SELECT value 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),
)
row = c.fetchone()
return row[0] if row is not None else self.info.default_value
@value.setter
def _set_value(self, value):
self.__update_values(value, self.translatable, self.translation_context, self.translation_comments, self.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)
if bind_property:
bind_source_id = bind_property.object.object_id
bind_owner_id = bind_property.owner_id
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 and
not translatable and
translation_context is None and
translation_comments is None
):
self.reset()
else:
if (
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
# Do not use REPLACE INTO, to make sure both INSERT and UPDATE triggers are used
count = self.db_get(
"SELECT count(ui_id) 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),
)
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=?,
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,
self.ui_id,
self.object_id,
self.owner_id,
self.property_id,
),
)
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,
translatable, translation_context, translation_comments,
bind_source_id, bind_owner_id, bind_property_id
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
""",
(
self.ui_id,
self.object_id,
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)
c.close()
@GObject.Property(type=CmbBaseProperty)
def bind_property(self):
c = self.project.db.cursor()
row = c.execute(
"""
SELECT bind_source_id, bind_property_id
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),
).fetchone()
if row:
bind_source_id, bind_property_id = row
source = self.project.get_object_by_id(self.ui_id, bind_source_id) if bind_property_id else None
return source.properties_dict.get(bind_property_id, None) if source else None
return None
@bind_property.setter
def _set_bind_property(self, 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)
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

View File

@ -1,66 +0,0 @@
#
# Cambalache Property Type Info wrapper
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject
from .cmb_objects_base import CmbBasePropertyInfo
from cambalache import getLogger
logger = getLogger(__name__)
class CmbPropertyInfo(CmbBasePropertyInfo):
internal_child = GObject.Property(type=GObject.GObject, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.is_a11y = CmbPropertyInfo.type_is_accessible(self.owner_id)
self.a11y_property_id = CmbPropertyInfo.accessible_property_remove_prefix(self.owner_id, self.property_id)
@classmethod
def type_is_accessible(cls, owner_id):
return owner_id in [
"CmbAccessibleProperty",
"CmbAccessibleRelation",
"CmbAccessibleState",
"CmbAccessibleAction"
]
@classmethod
def accessible_property_remove_prefix(cls, owner_id, property_id):
prefix = {
"CmbAccessibleProperty": "cmb-a11y-property-",
"CmbAccessibleRelation": "cmb-a11y-relation-",
"CmbAccessibleState": "cmb-a11y-state-",
"CmbAccessibleAction": "cmb-a11y-action-"
}.get(owner_id, None)
if prefix is None:
return None
# A11y property name without prefix
return property_id.removeprefix(prefix)

View File

@ -1,270 +0,0 @@
#
# CmbPropertyLabel
#
# 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
# 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 .cmb_property import CmbProperty
from .cmb_layout_property import CmbLayoutProperty
from .cmb_property_info import CmbPropertyInfo
from .control import CmbObjectChooser, CmbFlagsEntry
from cambalache import _
class CmbPropertyLabel(Gtk.Button):
__gtype_name__ = "CmbPropertyLabel"
prop = GObject.Property(type=CmbProperty, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
layout_prop = GObject.Property(
type=CmbLayoutProperty, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
bindable = GObject.Property(type=bool, default=True, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY)
def __init__(self, **kwargs):
super().__init__(**kwargs)
if not self.prop and not self.layout_prop:
raise Exception("CmbPropertyLabel requires prop or layout_prop to be set")
return
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.NORMAL)
box.append(self.bind_icon)
# 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())
if self.bindable:
self.connect("clicked", self.__on_bind_button_clicked)
if self.layout_prop:
self.bind_icon = None
self.label.props.label = self.layout_prop.property_id
self.__update_layout_property_label()
self.layout_prop.connect("notify::value", lambda o, p: self.__update_layout_property_label())
box.append(self.label)
self.set_child(box)
def __update_label(self, prop):
if prop.value != prop.info.default_value:
self.add_css_class("modified")
else:
self.remove_css_class("modified")
msg = prop.version_warning
self.set_tooltip_text(msg)
if msg:
self.add_css_class("warning")
else:
self.remove_css_class("warning")
def __update_layout_property_label(self):
self.__update_label(self.layout_prop)
def __update_property_label(self):
self.__update_label(self.prop)
if not self.bindable:
return
if self.prop.bind_property_id:
self.bind_icon.props.icon_name = "binded-symbolic"
self.remove_css_class("hidden")
else:
self.bind_icon.props.icon_name = "bind-symbolic"
self.add_css_class("hidden")
def __on_object_editor_notify(self, object_editor, pspec, property_editor):
object_id = object_editor.cmb_value
if object_id:
property_editor.object = self.prop.project.get_object_by_id(self.prop.ui_id, int(object_id))
def __on_property_editor_changed(self, combo):
bind_source, bind_property = self.__find_bind_source_property(combo.object.object_id, combo.props.active_id)
self.prop.bind_property = bind_property
self.__update_property_label()
def __find_bind_source_property(self, bind_source_id, bind_property_id):
bind_source = self.prop.project.get_object_by_id(self.prop.ui_id, bind_source_id) if bind_source_id else None
bind_property = bind_source.properties_dict.get(bind_property_id, None) if bind_source else None
return bind_source, bind_property
def __on_clear_clicked(self, button, popover):
self.prop.bind_property = None
self.prop.bind_flags = None
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(position=Gtk.PositionType.LEFT, css_classes=["cmb-binding-popover"])
popover.set_parent(self)
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)
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)
# Create Property editor
property_editor = CmbPropertyChooser(object=bind_source, target_info=self.prop.info)
property_editor.connect("changed", self.__on_property_editor_changed)
# Update active_id after letting the object populate the properties
if bind_property:
property_editor.props.active_id = bind_property.property_id
# Object editor (it does not set the object directly to CmbProperty, just choose the object in the prop chooser)
object_editor = CmbObjectChooser(parent=self.prop.object, cmb_value=bind_source.object_id if bind_source else 0)
object_editor.connect("notify::cmb-value", self.__on_object_editor_notify, property_editor)
# Flags editor
binding_flags_info = self.prop.project.type_info.get("GBindingFlags", None)
flags_editor = CmbFlagsEntry(info=binding_flags_info)
GObject.Object.bind_property(
self.prop,
"bind_flags",
flags_editor,
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
i = 1
for prop_label, editor in [("source", object_editor), ("property", property_editor), ("flags", flags_editor)]:
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"), halign=Gtk.Align.END)
clear.connect("clicked", self.__on_clear_clicked, popover)
grid.attach(clear, 0, i, 2, 1)
object_editor.grab_focus()
popover.set_child(grid)
popover.popup()
Gtk.WidgetClass.set_css_name(CmbPropertyLabel, "CmbPropertyLabel")
class CmbPropertyChooser(Gtk.ComboBoxText):
__gtype_name__ = "CmbPropertyChooser"
object = GObject.Property(type=CmbObject, flags=GObject.ParamFlags.READWRITE)
target_info = GObject.Property(type=CmbPropertyInfo, flags=GObject.ParamFlags.READWRITE)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__populate()
self.connect("notify::object", self.__on_object_notify)
def __on_object_notify(self, obj, pspec):
self.__populate()
def __populate(self):
self.remove_all()
if self.object is None:
return
target_info = self.target_info
target_type = target_info.type_id
target_type_info = self.object.project.type_info.get(target_type, None)
target_is_object = target_info.is_object
target_is_iface = target_type_info.parent_id == "interface" if target_type_info else False
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
source_type_info = self.object.project.type_info.get(info.type_id, None)
source_is_object = info.is_object
source_is_iface = source_type_info.parent_id == "interface" if source_type_info else False
if target_is_object or target_is_iface:
# Ignore non object properties
if not source_is_object and not source_is_iface:
continue
# Ignore object properties of a different type
if source_type_info and not source_type_info.is_a(target_info.type_id):
continue
elif source_is_object or source_is_iface:
continue
# Enums and Flags has to be the same type
if target_type_info and target_type_info.parent_id in ["flags", "enum"] and info.type_id != target_type:
continue
if source_type_info and source_type_info.parent_id in ["flags", "enum"] and info.type_id != target_type:
continue
compatible = info.type_id == target_type
if not compatible:
try:
gtype_id = GObject.type_from_name(info.type_id)
gtarget_id = GObject.type_from_name(target_type)
if gtype_id and gtarget_id:
compatible = GObject.Value.type_compatible(gtype_id, gtarget_id)
if not compatible:
compatible = GObject.Value.type_transformable(gtype_id, gtarget_id)
except Exception as e: # noqa F841
self.append(prop.property_id, prop.property_id + "*")
continue
if compatible:
self.append(prop.property_id, prop.property_id)

View File

@ -1,282 +0,0 @@
#
# CmbSignalEditor - Cambalache Signal Editor
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk, Pango
from .cmb_object import CmbObject
from . import utils
from enum import Enum
class Col(Enum):
SIGNAL = 0
OWNER_ID = 1
SIGNAL_ID = 2
DETAIL = 3
HANDLER = 4
USER_DATA = 5
SWAP = 6
AFTER = 7
INFO = 8
WARNING_MESSAGE = 9
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_signal_editor.ui")
class CmbSignalEditor(Gtk.Box):
__gtype_name__ = "CmbSignalEditor"
treeview = Gtk.Template.Child()
treestore = Gtk.Template.Child()
signal_id_column = Gtk.Template.Child()
signal_id = Gtk.Template.Child()
handler_column = Gtk.Template.Child()
handler = Gtk.Template.Child()
user_data_column = Gtk.Template.Child()
user_data = Gtk.Template.Child()
swap_column = Gtk.Template.Child()
swap = Gtk.Template.Child()
after_column = Gtk.Template.Child()
after = Gtk.Template.Child()
def __init__(self, **kwargs):
self._object = None
super().__init__(**kwargs)
self.signal_id_column.set_cell_data_func(self.signal_id, self.__signal_id_data_func, None)
self.handler_column.set_cell_data_func(self.handler, self.__data_func, Col.HANDLER.value)
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:
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:
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):
iter_ = self.treestore.get_iter(path)
signal = self.treestore[iter_][Col.SIGNAL.value]
if signal is None:
if len(new_text) > 0:
owner_id = self.treestore[iter_][Col.OWNER_ID.value]
signal_id = self.treestore[iter_][Col.SIGNAL_ID.value]
self._object.add_signal(owner_id, signal_id, new_text)
else:
self.treestore[iter_][Col.HANDLER.value] = new_text
if len(new_text) > 0:
signal.handler = new_text
else:
self._object.remove_signal(signal)
@Gtk.Template.Callback("on_detail_edited")
def __on_detail_edited(self, renderer, path, new_text):
iter_ = self.treestore.get_iter(path)
signal = self.treestore[iter_][Col.SIGNAL.value]
if signal is not None:
if len(new_text) > 0:
tokens = new_text.split("::")
if len(tokens) == 2 and len(tokens[1]) > 0:
signal.detail = tokens[1]
else:
signal.detail = None
else:
signal.detail = None
self.treestore[iter_][Col.DETAIL.value] = signal.detail
@Gtk.Template.Callback("on_user_data_edited")
def __on_user_data_edited(self, renderer, path, new_text):
iter_ = self.treestore.get_iter(path)
signal = self.treestore[iter_][Col.SIGNAL.value]
if signal is not None:
if len(new_text) > 0:
data_obj = self._object.project.get_object_by_name(signal.ui_id, new_text)
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")
def __on_swap_toggled(self, renderer, path):
iter_ = self.treestore.get_iter(path)
signal = self.treestore[iter_][Col.SIGNAL.value]
if signal is not None:
signal.swap = not self.treestore[iter_][Col.SWAP.value]
self.treestore[iter_][Col.SWAP.value] = signal.swap
@Gtk.Template.Callback("on_after_toggled")
def __on_after_toggled(self, renderer, path):
iter_ = self.treestore.get_iter(path)
signal = self.treestore[iter_][Col.SIGNAL.value]
if signal is not None:
signal.after = not self.treestore[iter_][Col.AFTER.value]
self.treestore[iter_][Col.AFTER.value] = signal.after
def __on_signal_added(self, obj, signal):
for row in self.treestore:
if row[Col.OWNER_ID.value] == signal.owner_id:
for child in row.iterchildren():
if child[Col.SIGNAL.value] is None and child[Col.SIGNAL_ID.value] == signal.signal_id:
self.treestore.insert_before(
row.iter,
child.iter,
(
signal,
signal.owner_id,
signal.signal_id,
signal.detail,
signal.handler,
str(signal.user_data),
signal.swap,
signal.after,
child[Col.INFO.value],
None,
),
)
break
def __on_signal_removed(self, obj, signal):
for row in self.treestore:
for child in row.iterchildren():
if child[Col.SIGNAL.value] == signal:
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
parent = self.treestore.append(None, (None, info.type_id, info.type_id, None, None, None, False, False, None, None))
for signal_id in info.signals:
signal = info.signals[signal_id]
msg = utils.get_version_warning(target, signal.version, signal.deprecated_version, signal.signal_id)
self.treestore.append(parent, (None, info.type_id, signal.signal_id, None, None, None, False, False, signal, msg))
return parent
def __populate_treestore(self):
target = self._object.ui.get_target(self._object.info.library_id)
# Populate object type signals
parent = self.__populate_from_type(self._object.info, target)
# Expand object type signals
if parent:
self.treeview.expand_row(self.treestore.get_path(parent), True)
# Populate all hierarchy signals
for type_id in self._object.info.hierarchy:
info = self._object.project.type_info.get(type_id, None)
if info:
self.__populate_from_type(info, target)
# Populate object signals
for signal in self._object.signals:
self.__on_signal_added(self._object, signal)
def __signal_id_data_func(self, tree_column, cell, tree_model, iter_, column):
info = tree_model[iter_][Col.INFO.value]
signal_id = tree_model[iter_][Col.SIGNAL_ID.value]
warning = tree_model[iter_][Col.WARNING_MESSAGE.value]
if info and info.detailed:
detail = tree_model[iter_][Col.DETAIL.value]
signal = tree_model[iter_][Col.SIGNAL.value]
cell.props.editable = False if signal is None else True
cell.props.text = f"{signal_id}::{detail}" if detail is not None else signal_id
else:
cell.props.editable = False
cell.props.text = signal_id
cell.props.underline = Pango.Underline.ERROR if warning else Pango.Underline.NONE
def __data_func(self, tree_column, cell, tree_model, iter_, column):
info = tree_model[iter_][Col.INFO.value]
signal = tree_model[iter_][Col.SIGNAL.value]
if info is None:
cell.props.visible = False
return
cell.props.visible = True
if signal is None and column != Col.HANDLER.value:
cell.props.sensitive = False
else:
cell.props.sensitive = True
if signal and column == Col.USER_DATA.value:
user_data = signal.user_data
if user_data:
data_obj = self._object.project.get_object_by_id(signal.ui_id, user_data)
cell.props.text = data_obj.name if data_obj else ""
else:
cell.props.text = ""

View File

@ -1,126 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- 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>
</child>
</object>
<object class="GtkTreeStore" id="treestore">
<columns>
<column type="GObject"/>
<column type="gchararray"/>
<column type="gchararray"/>
<column type="gchararray"/>
<column type="gchararray"/>
<column type="gchararray"/>
<column type="gboolean"/>
<column type="gboolean"/>
<column type="GObject"/>
<column type="gchararray"/>
</columns>
</object>
<template class="CmbSignalEditor" parent="GtkBox">
<property name="orientation">vertical</property>
<property name="spacing">4</property>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTreeView" id="treeview">
<property name="focusable">1</property>
<property name="model">treestore</property>
<property name="tooltip-column">9</property>
<child>
<object class="GtkTreeViewColumn" id="signal_id_column">
<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"/>
</object>
<!-- Custom child fragments -->
<attributes>
<attribute name="text">2</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="handler_column">
<property name="expand">1</property>
<property name="min-width">64</property>
<property name="resizable">1</property>
<property name="title" translatable="yes">Handler</property>
<child>
<object class="GtkCellRendererText" id="handler">
<property name="editable">1</property>
<property name="placeholder-text">&lt;Enter callback&gt;</property>
<signal name="edited" handler="on_handler_edited"/>
</object>
<!-- Custom child fragments -->
<attributes>
<attribute name="text">4</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="user_data_column">
<property name="title" translatable="yes">Data</property>
<child>
<object class="GtkCellRendererText" id="user_data">
<property name="editable">1</property>
<property name="placeholder-text">&lt;object&gt;</property>
<signal name="edited" handler="on_user_data_edited"/>
</object>
<!-- Custom child fragments -->
<attributes>
<attribute name="text">5</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="swap_column">
<property name="title" translatable="yes">Swap</property>
<child>
<object class="GtkCellRendererToggle" id="swap">
<signal name="toggled" handler="on_swap_toggled"/>
</object>
<!-- Custom child fragments -->
<attributes>
<attribute name="active">6</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="after_column">
<property name="title" translatable="yes">After</property>
<child>
<object class="GtkCellRendererToggle" id="after">
<signal name="toggled" handler="on_after_toggled"/>
</object>
<!-- Custom child fragments -->
<attributes>
<attribute name="active">7</attribute>
</attributes>
</child>
</object>
</child>
</object>
</property>
<property name="focusable">1</property>
<property name="vexpand">1</property>
</object>
</child>
</template>
</interface>

View File

@ -1,266 +0,0 @@
#
# CmbTreeExpander
#
# Copyright (C) 2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gdk, Gtk, Graphene
from .cmb_ui import CmbUI
from .cmb_object import CmbObject
from .cmb_css import CmbCSS
from .cmb_path import CmbPath
from cambalache import _
class CmbTreeExpander(Gtk.TreeExpander):
__gtype_name__ = "CmbTreeExpander"
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.__project = None
self.__drop_before = None
self.label = Gtk.Inscription(hexpand=True)
self.set_child(self.label)
self.__parent = None
self.__drag_source = None
self.__drop_target = None
def update_bind(self):
item = self.props.item
self.__parent = self.props.parent
# Drag source
self.__drag_source = Gtk.DragSource()
self.__drag_source.connect("prepare", self.__on_drag_prepare)
self.__drag_source.connect("drag-begin", self.__on_drag_begin)
self.__parent.add_controller(self.__drag_source)
# Drop target
self.__drop_target = Gtk.DropTarget.new(type=GObject.TYPE_NONE, actions=Gdk.DragAction.COPY)
self.__drop_target.set_gtypes([CmbObject, CmbUI])
self.__drop_target.connect("accept", self.__on_drop_accept)
self.__drop_target.connect("motion", self.__on_drop_motion)
self.__drop_target.connect("drop", self.__on_drop_drop)
self.__parent.add_controller(self.__drop_target)
# Handle label
self.__update_label(item)
item.connect("notify::display-name", self.__on_item_display_name_notify)
self.__project = item.project
if isinstance(item, CmbCSS):
self.props.hide_expander = True
return
elif isinstance(item, CmbPath):
self.props.hide_expander = False
self.add_css_class("cmb-path" if item.path else "cmb-unsaved-path")
return
self.props.hide_expander = item.props.n_items == 0
item.connect("notify::n-items", self.__on_item_n_items_notify)
def clear_bind(self):
item = self.props.item
if self.__parent:
self.__parent.remove_controller(self.__drag_source)
self.__parent.remove_controller(self.__drop_target)
self.__parent = None
self.__drag_source = None
self.__drop_target = None
self.remove_css_class("cmb-path")
self.remove_css_class("cmb-unsaved-path")
item.disconnect_by_func(self.__on_item_display_name_notify)
if isinstance(item, CmbCSS) or isinstance(item, CmbPath):
return
item.disconnect_by_func(self.__on_item_n_items_notify)
def __on_item_n_items_notify(self, item, pspec):
self.props.hide_expander = item.props.n_items == 0
def __on_item_display_name_notify(self, item, pspec):
self.__update_label(item)
def __update_label(self, item):
self.label.set_markup(item.props.display_name or "")
# Drop target callbacks
def __get_drop_before(self, widget, x, y):
h = widget.get_height()
if y < h/4:
return True
if y > h * 0.8:
return False
return None
def __on_object_drop_accept(self, drop, item):
origin_item = drop.get_drag()._item
if origin_item == item:
return None
if not isinstance(item, CmbObject):
return None
# Ignore if its the same parent
if origin_item.parent_id == item.object_id:
return None
return origin_item
def __on_drop_accept(self, target, drop):
item = self.props.item
origin_item = drop.get_drag()._item
if isinstance(item, CmbObject):
origin_item = self.__on_object_drop_accept(drop, item)
self.__drop_before = None
if origin_item is None or item.parent is None:
return False
return self.__project._check_can_add(origin_item.type_id, item.parent.type_id)
elif isinstance(item, CmbUI):
if origin_item == item:
return False
# Ignore if its the same UI and item is already a toplevel
if origin_item.ui_id == item.ui_id and origin_item.parent_id is None:
return False
return True
def __on_drop_motion(self, target, x, y):
item = self.props.item
if isinstance(item, CmbObject):
row_widget = target.get_widget()
drop_before = self.__get_drop_before(row_widget, x, y)
if self.__drop_before == drop_before:
return Gdk.DragAction.COPY
row_widget.remove_css_class("drop-before")
row_widget.remove_css_class("drop-after")
self.__drop_before = drop_before
if drop_before is not None:
row_widget.add_css_class("drop-before" if drop_before else "drop-after")
return Gdk.DragAction.COPY
def __on_drop_drop(self, target, origin_item, x, y):
row_widget = target.get_widget()
item = self.props.item
if isinstance(item, CmbObject):
drop_before = self.__get_drop_before(row_widget, x, y)
if drop_before is None:
# TODO: handle dragging from one UI to another
if origin_item.ui_id != item.ui_id:
return
if origin_item.parent_id != item.object_id:
self.__project.history_push(
_("Move {name} to {target}").format(name=origin_item.display_name, target=item.display_name)
)
origin_item.parent_id = item.object_id
self.__project.history_pop()
return
if origin_item.parent_id != item.parent_id:
if drop_before:
msg = _("Move {name} before {target}").format(name=origin_item.display_name, target=item.display_name)
else:
msg = _("Move {name} after {target}").format(name=origin_item.display_name, target=item.display_name)
self.__project.history_push(msg)
origin_item.parent_id = item.parent_id
else:
msg = None
parent = item.parent
origin_position = origin_item.position
target_position = item.position
if origin_position > target_position:
if drop_before:
position = item.position
else:
position = item.position + 1
else:
if drop_before:
position = item.position - 1
else:
position = item.position
parent.reorder_child(origin_item, position)
if msg:
self.__project.history_pop()
self.__project.set_selection([origin_item])
elif isinstance(item, CmbUI):
if origin_item.ui_id == item.ui_id:
self.__project.history_push(_("Move {name} as toplevel").format(name=origin_item.display_name))
origin_item.parent_id = 0
self.__project.history_pop()
else:
# TODO: Use copy/paste to move across UI files
pass
# Drag Source callbacks
def __on_drag_prepare(self, drag_source, x, y):
item = self.props.item
# Only CmbObject start a drag
if not isinstance(item, CmbObject):
return None
self.__drag_point = Graphene.Point()
self.__drag_point.x = x
self.__drag_point.y = y
return Gdk.ContentProvider.new_for_value(self.props.item)
def __on_drag_begin(self, drag_source, drag):
drag._item = self.props.item
valid, p = self.__parent.compute_point(self.label, self.__drag_point)
if valid:
drag_source.set_icon(Gtk.WidgetPaintable.new(self.label), p.x, p.y)

View File

@ -1,91 +0,0 @@
#
# CmbTypeChooserBar - Cambalache Type Chooser Bar
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk
from .cmb_project import CmbProject
from .cmb_type_info import CmbTypeInfo
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_type_chooser.ui")
class CmbTypeChooser(Gtk.Box):
__gtype_name__ = "CmbTypeChooser"
__gsignals__ = {
"type-selected": (GObject.SignalFlags.RUN_LAST, None, (CmbTypeInfo,)),
"chooser-popup": (GObject.SignalFlags.RUN_LAST, None, (GObject.Object,)),
"chooser-popdown": (GObject.SignalFlags.RUN_LAST, None, (GObject.Object,)),
}
project = GObject.Property(type=CmbProject, flags=GObject.ParamFlags.READWRITE)
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()
control = Gtk.Template.Child()
display = Gtk.Template.Child()
model = Gtk.Template.Child()
extra = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._choosers = [self.all, self.toplevel, self.layout, self.control, self.display, self.model, self.extra]
self.connect("notify::project", self.__on_project_notify)
self.connect("notify::selected-type", self.__on_selected_type_notify)
for chooser in self._choosers:
chooser.connect("type-selected", lambda o, t: self.emit("type-selected", t))
chooser.connect("notify::visible", self.__on_chooser_visible_notify)
def __on_project_notify(self, object, pspec):
project = self.project
self.selected_type = None
for chooser in self._choosers:
chooser.project = project
def __on_selected_type_notify(self, object, pspec):
project_target = self.project.target_tk if self.project else ""
self.type_label.props.label = self.selected_type.type_id if self.selected_type else project_target
def __on_chooser_visible_notify(self, obj, pspec):
if obj.props.visible:
self.emit("chooser-popup", obj)
else:
self.emit("chooser-popdown", obj)
def select_type_id(self, type_id):
info = self.project.type_info.get(type_id, None)
if info:
self.selected_type = info
self.emit("type-selected", info)
Gtk.WidgetClass.set_css_name(CmbTypeChooser, "CmbTypeChooser")

View File

@ -1,147 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_type_chooser.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.0"/>
<object class="CmbTypeChooserPopover" id="all">
<property name="show-categories">True</property>
</object>
<object class="CmbTypeChooserPopover" id="control">
<property name="category">control</property>
</object>
<object class="CmbTypeChooserPopover" id="display">
<property name="category">display</property>
</object>
<object class="CmbTypeChooserPopover" id="extra">
<property name="uncategorized-only">True</property>
</object>
<object class="CmbTypeChooserPopover" id="layout">
<property name="category">layout</property>
</object>
<object class="CmbTypeChooserPopover" id="model">
<property name="category">model</property>
</object>
<object class="CmbTypeChooserPopover" id="toplevel">
<property name="category">toplevel</property>
</object>
<template class="CmbTypeChooser" parent="GtkBox">
<property name="spacing">4</property>
<child>
<object class="GtkBox" id="content">
<child>
<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="icon-name">edit-find-symbolic</property>
</object>
</child>
</object>
</child>
<style>
<class name="linked"/>
</style>
</object>
</child>
<child>
<object class="GtkLabel" id="type_label">
<property name="halign">start</property>
<property name="hexpand">1</property>
<property name="sensitive">0</property>
<attributes>
<attribute name="style" value="italic"/>
</attributes>
</object>
</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>

View File

@ -1,68 +0,0 @@
#
# CmbTypeChooserPopover - Cambalache Type Chooser Popover
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GObject, Gtk
from .cmb_project import CmbProject
from .cmb_type_info import CmbTypeInfo
from .cmb_type_chooser_widget import CmbTypeChooserWidget
class CmbTypeChooserPopover(Gtk.Popover):
__gtype_name__ = "CmbTypeChooserPopover"
__gsignals__ = {
"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)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._chooser = CmbTypeChooserWidget()
self._chooser.connect("type-selected", self.__on_type_selected)
self.set_child(self._chooser)
self.set_default_widget(self._chooser)
for prop in [
"project",
"category",
"uncategorized_only",
"show_categories",
"parent_type_id",
"derived_type_id",
]:
GObject.Object.bind_property(self, prop, self._chooser, prop, GObject.BindingFlags.SYNC_CREATE)
def __on_type_selected(self, chooser, info):
self.emit("type-selected", info)
self.popdown()

View File

@ -1,208 +0,0 @@
#
# CmbTypeChooserWidget - Cambalache Type Chooser Widget
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from gi.repository import GLib, GObject, Gio, Gtk
from .cmb_project import CmbProject
from .cmb_type_info import CmbTypeInfo
from . import constants
from cambalache import getLogger, _
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_type_chooser_widget.ui")
class CmbTypeChooserWidget(Gtk.Box):
__gtype_name__ = "CmbTypeChooserWidget"
__gsignals__ = {
"type-selected": (GObject.SignalFlags.RUN_LAST, None, (CmbTypeInfo,)),
}
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)
scrolledwindow = Gtk.Template.Child()
listview = Gtk.Template.Child()
def __init__(self, **kwargs):
self.__project = None
self._search_text = ""
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
if not info.instantiable or info.layout not in [None, "container"]:
return False
if info.category == "hidden":
return False
if self.parent_type_id != "":
retval = self.project._check_can_add(info.type_id, self.parent_type_id)
else:
retval = (
info.category is None
if self.uncategorized_only
else (self.category != "" and info.category == self.category) or self.category == ""
)
if retval and self.derived_type_id != "":
retval = info.is_a(self.derived_type_id)
return retval
def __model_from_project(self, project):
if project is None:
return None
categories = {
"toplevel": _("Toplevel"),
"layout": _("Layout"),
"control": _("Control"),
"display": _("Display"),
"model": _("Model"),
}
order = {"toplevel": 0, "layout": 1, "control": 2, "display": 3, "model": 4}
# type_id, type_id.lower(), CmbTypeInfo, sensitive
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:
# Ignore types with no name, just in case
if key:
infos.append(project.type_info[key])
else:
logger.warning("Tried to create a TypeInfo without a name")
infos = sorted(infos, key=lambda i: (order.get(i.category, 99), i.type_id))
show_categories = self.show_categories
last_category = None
for i in infos:
if not self.__type_info_should_append(i):
continue
# Append category
if show_categories and last_category != i.category:
last_category = i.category
category = categories.get(i.category, _("Others"))
store.append(CmbTypeInfo(type_id=f"<i><b>▾ {category}</b></i>"))
store.append(i)
# Special case External object type
if show_categories or self.uncategorized_only:
store.append(project.type_info[constants.EXTERNAL_TYPE])
return filter_model
@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_type_info_added)
self.__project.disconnect_by_func(self.__on_type_info_removed)
self.__project = project
self.__model = self.__model_from_project(project)
self.listview.set_model(Gtk.NoSelection(model=self.__model))
if project:
project.connect("type-info-added", self.__on_type_info_added)
project.connect("type-info-removed", self.__on_type_info_removed)
@Gtk.Template.Callback("on_searchentry_activate")
def __on_searchentry_activate(self, entry):
search_text = entry.props.text
info = self.project.type_info.get(search_text, None)
if info:
self.emit("type-selected", info)
@Gtk.Template.Callback("on_searchentry_search_changed")
def __on_searchentry_search_changed(self, entry):
self._search_text = entry.props.text.lower()
self.__model.props.filter.changed(Gtk.FilterChange.DIFFERENT)
@Gtk.Template.Callback("on_listview_activate")
def __on_listview_activate(self, listview, position):
info = self.__model.get_item(position)
if info is not None and info.project:
self.emit("type-selected", info)
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
if self.__type_info_should_append(info):
self.__model.props.model.insert_sorted(info, lambda a, b, d: GLib.strcmp0(a.type_id, b.type_id), None)
def __on_type_info_removed(self, project, info):
if self.__model is None:
return
# Find info and remove it from model
found, position = self.__model.props.model.find(info)
if found:
self.__model.props.model.remove(position)
Gtk.WidgetClass.set_css_name(CmbTypeChooserWidget, "CmbTypeChooserWidget")

View File

@ -1,52 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- 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="orientation">vertical</property>
<property name="spacing">6</property>
<child>
<object class="GtkSearchEntry" id="searchentry">
<signal name="activate" handler="on_searchentry_activate"/>
<signal name="search-changed" handler="on_searchentry_search_changed"/>
</object>
</child>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow">
<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="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>
</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>
</child>
</template>
</interface>

View File

@ -1,377 +0,0 @@
#
# Cambalache Type Info wrapper
#
# Copyright (C) 2021-2022 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_objects_base import (
CmbBaseTypeInfo,
CmbBaseTypeDataInfo,
CmbBaseTypeDataArgInfo,
CmbBaseTypeInternalChildInfo,
CmbTypeChildInfo,
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):
super().__init__(**kwargs)
def __str__(self):
return f"CmbTypeDataArgInfo<{self.owner_id}>::{self.key}"
class CmbTypeDataInfo(CmbBaseTypeDataInfo):
def __init__(self, **kwargs):
self.args = {}
self.children = {}
super().__init__(**kwargs)
def __str__(self):
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":
self.enum = self.__init_enum_flags("enum")
elif self.parent_id == "flags":
self.flags = self.__init_enum_flags("flags")
self.child_types = self.__init_child_type()
self.is_object = self.is_a("GObject")
self.instantiable = self.is_object and not self.abstract
self.is_menu_builtin = self.type_id in [GMENU_TYPE, GMENU_SECTION_TYPE, GMENU_SUBMENU_TYPE, GMENU_ITEM_TYPE]
self.is_builtin = self.is_menu_builtin or self.type_id in [EXTERNAL_TYPE]
def __str__(self):
return f"CmbTypeInfo<{self.type_id}>"
def __init_hierarchy(self):
retval = []
c = self.project.db.cursor()
for row in c.execute(
"""
WITH RECURSIVE ancestor(type_id, generation, parent_id) AS (
SELECT type_id, 1, parent_id
FROM type
WHERE parent_id IS NOT NULL AND
parent_id != 'interface' AND
parent_id != 'enum' AND
parent_id != 'flags' AND
type_id=?
UNION ALL
SELECT ancestor.type_id, generation + 1, type.parent_id
FROM type JOIN ancestor ON type.type_id = ancestor.parent_id
WHERE type.parent_id IS NOT NULL AND type.parent_id != 'object' AND ancestor.type_id=?
)
SELECT parent_id, generation FROM ancestor
UNION
SELECT type_iface.iface_id, 0
FROM ancestor JOIN type_iface
WHERE ancestor.type_id = type_iface.type_id
ORDER BY generation;
""",
(self.type_id, self.type_id),
):
retval.append(row[0])
c.close()
return retval
def __init_interfaces(self):
retval = []
c = self.project.db.cursor()
for row in c.execute("SELECT iface_id FROM type_iface WHERE type_id=? ORDER BY iface_id;", (self.type_id,)):
retval.append(row[0])
c.close()
return retval
def __init_properties_signals(self, Klass, table):
retval = {}
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.project, *row)
c.close()
return retval
def __type_get_data(self, owner_id, data_id, parent_id, key, type_id, translatable):
args = {}
children = {}
parent_id = parent_id if parent_id is not None else 0
retval = CmbTypeDataInfo.from_row(self.project, owner_id, data_id, parent_id, key, type_id, translatable)
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.project, *row)
# Recurse children
for row in c.execute("SELECT * FROM type_data WHERE owner_id=? AND parent_id=?;", (owner_id, data_id)):
_key = row[3]
children[_key] = self.__type_get_data(*row)
c.close()
retval.args = args
retval.children = children
return retval
def __init_data(self):
retval = {}
c = self.project.db.cursor()
for row in c.execute(
"SELECT * FROM type_data WHERE parent_id IS NULL AND owner_id=? ORDER BY data_id;", (self.type_id,)
):
key = row[3]
retval[key] = self.__type_get_data(*row)
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 = []
c = self.project.db.cursor()
for row in c.execute(
"SELECT child_type_id, allowed, shortcut FROM type_child_constraint WHERE type_id=?;", (self.type_id,)
):
child_type_id, allowed, shortcut = row
retval[child_type_id] = allowed
if shortcut:
shortcuts.append(child_type_id)
c.close()
return retval, shortcuts
def __init_child_type(self):
retval = {}
c = self.project.db.cursor()
for row in c.execute("SELECT * FROM type_child_type WHERE type_id=?;", (self.type_id,)):
type_id, child_type, max_children, linked_property_id = row
retval[child_type] = CmbTypeChildInfo(
project=self.project,
type_id=type_id,
child_type=child_type,
max_children=max_children if max_children else 0,
linked_property_id=linked_property_id,
)
c.close()
return retval
def __init_enum_flags(self, name):
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=? ORDER BY nick;", (self.type_id,)):
retval.append(row)
c.close()
return retval
def is_a(self, type_id):
return self.type_id == type_id or type_id in self.hierarchy
def get_data_info(self, name):
parent = self
while parent:
if name in parent.data:
return parent.data[name]
parent = parent.parent
return None
def find_data_info(self, data_id):
def find_child_info(info, data_id):
for name in info.children:
child_info = info.children[name]
if child_info.data_id == data_id:
return child_info
retval = find_child_info(child_info, data_id)
if retval:
return retval
for name in self.data:
info = self.data[name]
if info.data_id == data_id:
return info
retval = find_child_info(info, data_id)
if retval:
return retval
return None
def has_child_types(self):
parent = self
while parent:
if parent.child_types:
return True
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)

View File

@ -1,222 +0,0 @@
#
# Cambalache UI wrapper
#
# Copyright (C) 2021 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 gi.repository import GObject, Gio
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, 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,))
return retval if retval is not None else 0
@template_id.setter
def _set_template_id(self, value):
self.db_set("UPDATE ui SET template_id=? WHERE (ui_id) IS (?);", (self.ui_id,), value if value != 0 else None)
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 = {}
for row in self.project.db.execute(
"""
SELECT DISTINCT t.library_id, NULL
FROM object AS o, type AS t
WHERE t.library_id IS NOT NULL AND o.ui_id=? AND o.type_id = t.type_id
UNION
SELECT library_id, version FROM ui_library WHERE ui_id=?
""",
(self.ui_id, self.ui_id),
).fetchall():
library_id, version = row
versions = []
for row in self.project.db.execute(
"SELECT version FROM library_version WHERE library_id=? ORDER BY version COLLATE version DESC;", (library_id,)
).fetchall():
versions.append(row[0])
retval[library_id] = {"target": version, "versions": versions}
return retval
def get_library(self, library_id):
c = self.project.db.execute("SELECT version FROM ui_library WHERE ui_id=? AND library_id=?;", (self.ui_id, library_id))
row = c.fetchone()
return row[0] if row is not None else None
def _library_changed(self, lib):
self.emit("library-changed", lib)
self.project._ui_library_changed(self, lib)
def set_library(self, library_id, version, comment=None):
c = self.project.db.cursor()
try:
if version is None:
c.execute("DELETE FROM ui_library WHERE ui_id=? AND library_id=?;", (self.ui_id, library_id))
else:
# Do not use REPLACE INTO, to make sure both INSERT and UPDATE triggers are used
count = self.db_get(
"SELECT count(version) FROM ui_library WHERE ui_id=? AND library_id=?;", (self.ui_id, library_id)
)
if count:
c.execute(
"UPDATE ui_library SET version=?, comment=? WHERE ui_id=? AND library_id=?;",
(str(version), comment, self.ui_id, library_id),
)
else:
c.execute(
"INSERT INTO ui_library (ui_id, library_id, version, comment) VALUES (?, ?, ?, ?);",
(self.ui_id, library_id, str(version), comment),
)
self._library_changed(library_id)
except Exception as e:
logger.warning(f"{self} Error setting library {library_id}={version}: {e}")
c.close()
@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
row = self.project.db.execute(
"""
WITH lib_version(version) AS (
SELECT t.version
FROM object AS o, type AS t
WHERE t.library_id=? AND o.ui_id=? AND o.type_id = t.type_id AND t.version IS NOT NULL
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
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
)
SELECT MAX_VERSION(version) FROM lib_version;
""",
(library_id, ui_id, library_id, ui_id, library_id, ui_id),
).fetchone()
return row[0] if row is not None else None
def get_target(self, library_id):
target = self.get_library(library_id)
if target is None:
target = self.__get_infered_target(library_id)
if target is None:
info = self.project.library_info.get(library_id, None)
if info:
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

View File

@ -1,137 +0,0 @@
#
# CmbUIEditor - Cambalache UI Editor
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import os
from gi.repository import GObject, Gtk
from cambalache import _
from .cmb_ui import CmbUI
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_ui_editor.ui")
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()
authors = Gtk.Template.Child()
translation_domain = Gtk.Template.Child()
comment = Gtk.Template.Child()
fields = ["filename", "template_id", "description", "copyright", "authors", "translation_domain", "comment"]
def __init__(self, **kwargs):
self._object = None
self._bindings = []
super().__init__(**kwargs)
@GObject.Property(type=CmbUI)
def object(self):
return self._object
@object.setter
def _set_object(self, obj):
for binding in self._bindings:
binding.unbind()
self._bindings = []
self._object = obj
if obj is None:
self.set_sensitive(False)
for field in self.fields:
widget = getattr(self, field)
if type(widget.cmb_value) is int:
widget.cmb_value = 0
else:
widget.cmb_value = None
return
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 = obj.bind_property(
field,
getattr(self, field),
"cmb-value",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
)
self._bindings.append(binding)
if obj.project.target_tk == "gtk-4.0":
self.filename.mime_types = "application/x-gtk-builder;text/x-blueprint"
# filename -> format
binding = obj.bind_property(
"filename",
self.format,
"selected",
GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.BIDIRECTIONAL,
transform_to=self.__filename_to_format,
transform_from=self.__format_to_filename,
user_data=obj
)
self._bindings.append(binding)
self.format.show()
self.format.set_sensitive(bool(obj.filename))
else:
self.filename.mime_types = "application/x-gtk-builder;application/x-glade"
self.format.hide()
def __filename_to_format(self, binding, source_value, ui):
if not source_value:
self.format.props.sensitive = False
return 0
self.format.props.sensitive = True
return 1 if source_value.endswith(".blp") else 0
def __format_to_filename(self, binding, target_value, ui):
if not ui.filename:
self.format.props.sensitive = False
return None
self.format.props.sensitive = True
return os.path.splitext(ui.filename)[0] + (".blp" if target_value == 1 else ".ui")
Gtk.WidgetClass.set_css_name(CmbUIEditor, "CmbUIEditor")

View File

@ -1,213 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.97.1 -->
<interface>
<!-- 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"/>
<template class="CmbUIEditor" parent="GtkGrid">
<property name="column-spacing">3</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">Description:</property>
<layout>
<property name="column">0</property>
<property name="row">3</property>
</layout>
</object>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="CmbFileButton" id="filename">
<property name="hexpand">True</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object class="CmbEntry" id="translation_domain">
<property name="can-focus">True</property>
<property name="halign">start</property>
<property name="placeholder-text" translatable="yes">&lt;translation domain&gt;</property>
<property name="visible">True</property>
<layout>
<property name="column">1</property>
<property name="row">6</property>
</layout>
</object>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTextView">
<property name="buffer">description</property>
<property name="focusable">1</property>
</object>
</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>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTextView">
<property name="buffer">authors</property>
<property name="focusable">1</property>
</object>
</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>
</child>
<child>
<object class="CmbToplevelChooser" id="template_id">
<property name="can-focus">False</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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTextView">
<property name="buffer">copyright</property>
<property name="focusable">1</property>
</object>
</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>
</child>
<child>
<object class="GtkLabel">
<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>
</child>
<child>
<object class="GtkScrolledWindow">
<property name="child">
<object class="GtkTextView">
<property name="buffer">comment</property>
<property name="focusable">1</property>
</object>
</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>
</child>
</template>
</interface>

View File

@ -1,113 +0,0 @@
#
# CmbUIRequiresEditor - Cambalache UI Requires Editor
#
# 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
# 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_ui import CmbUI
from . import utils
class CmbUIRequiresEditor(Gtk.Grid):
__gtype_name__ = "CmbUIRequiresEditor"
def __init__(self, **kwargs):
self.__object = None
self.__combos = {}
super().__init__(**kwargs)
self.props.column_spacing = 4
self.props.row_spacing = 4
@GObject.Property(type=CmbUI)
def object(self):
return self.__object
@object.setter
def _set__object(self, obj):
if obj == self.__object:
return
if self.__object:
self.__object.project.disconnect_by_func(self.__on_project_added_removed)
self.__object.disconnect_by_func(self.__on_library_changed)
self.__object = obj
self.set_sensitive(obj is not None)
if obj:
self.__object.project.connect("object-added", self.__on_project_added_removed)
self.__object.project.connect("object-removed", self.__on_project_added_removed)
self.__object.connect("library-changed", self.__on_library_changed)
self.__update()
def __on_project_added_removed(self, project, obj):
self.__update()
def __on_library_changed(self, ui, lib):
combo = self.__combos.get(lib, None)
if combo:
combo.set_active_id(ui.get_library(lib))
def __update(self):
self.__combos = {}
for child in utils.widget_get_children(self):
self.remove(child)
if self.__object is None:
return
i = 0
for library_id, data in self.__object.list_libraries().items():
label = Gtk.Label(label=library_id, visible=True, halign=Gtk.Align.START)
combo = self.__combobox_new(library_id, **data)
# Keep a reference by library_id
self.__combos[library_id] = combo
# Append to grid
self.attach(label, 1, i, 1, 1)
self.attach(combo, 2, i, 1, 1)
i += 1
def __on_combobox_changed(self, combo, library_id):
if self.__object:
self.__object.set_library(library_id, combo.get_active_id())
def __combobox_new(self, library_id, versions=[], target=None):
combo = Gtk.ComboBoxText(visible=True)
combo.append(None, "")
for version in versions:
combo.append(version, version)
if target:
combo.set_active_id(target)
combo.connect("changed", self.__on_combobox_changed, library_id)
return combo

View File

@ -1,57 +0,0 @@
#
# CmbVersionNotificationView
#
# Copyright (C) 2025 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
from cambalache import _, getLogger
from gi.repository import GObject, Gtk
from .cmb_notification import CmbVersionNotification
logger = getLogger(__name__)
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_version_notification_view.ui")
class CmbVersionNotificationView(Gtk.Box):
__gtype_name__ = "CmbVersionNotificationView"
notification = GObject.Property(
type=CmbVersionNotification, flags=GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY
)
# Version
version_label = Gtk.Template.Child()
release_notes_label = Gtk.Template.Child()
read_more_button = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
notification = self.notification
self.version_label.props.label = _("<b>Version {version} is available</b>").format(version=notification.version)
self.release_notes_label.props.label = notification.release_notes
if notification.read_more_url:
self.read_more_button.props.uri = notification.read_more_url
self.read_more_button.show()
else:
self.read_more_button.hide()

View File

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

View File

@ -1,762 +0,0 @@
#
# CmbView - Cambalache View
#
# Copyright (C) 2021-2024 Juan Pablo Ugarte
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation;
# version 2.1 of the License.
#
# library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
# Authors:
# Juan Pablo Ugarte <juanpablougarte@gmail.com>
#
# SPDX-License-Identifier: LGPL-2.1-only
#
import gi
import os
import json
import time
import fcntl
import stat
import atexit
import shutil
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.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(Casilda.Compositor.__gtype__)
class CmbMerengueProcess(GObject.Object):
__gsignals__ = {
"handle-command": (GObject.SignalFlags.RUN_LAST, None, (str,)),
"exit": (GObject.SignalFlags.RUN_LAST, None, ()),
}
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)
@GObject.Property(type=str)
def wayland_display(self):
return self.__wayland_display
@wayland_display.setter
def _set_wayland_display(self, wayland_display):
self.cleanup()
self.__wayland_display = wayland_display
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
for var in env:
envp.append(f"{var}={env[var]}")
pid, stdin, stdout, stderr = GLib.spawn_async(
[self.__file, self.gtk_version, self.__command_socket],
envp=envp,
flags=GLib.SpawnFlags.DO_NOT_REAP_CHILD,
)
self.__pid = pid
GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, self.__on_exit, None)
def __cleanup(self):
self.merengue_started = False
if self.__on_command_in_source:
GLib.source_remove(self.__on_command_in_source)
self.__on_command_in_source = None
if self.__command_in:
self.__command_in = None
if self.__connection:
self.__connection.close()
self.__connection = None
def stop(self):
self.__cleanup()
if self.__pid:
try:
GLib.spawn_close_pid(self.__pid)
os.kill(self.__pid, 9)
except Exception as e:
logger.warning(f"Error stopping {self.__file} {e}")
finally:
self.__pid = 0
def write_command(self, command, payload=None, args=None):
cmd = {"command": command}
if payload is not None:
# Encode to binary first, before calculating lenght
payload = payload.encode()
cmd["payload_length"] = len(payload)
logger.debug(f"write_command {command} {len(payload)}")
if args is not None:
cmd["args"] = args
# Queue command while we are not connected
if self.__connection is None:
self.__command_queue.append((cmd, payload))
return
self.__socket_write_command(cmd, payload)
def __socket_write_command(self, cmd, payload=None):
# Send command in one line as json
output_stream = self.__connection.props.output_stream
def write_data(data):
total_bytes = len(data)
total_sent = 0
while total_sent < total_bytes:
total_sent += output_stream.write(data[total_sent:])
write_data(json.dumps(cmd).encode())
write_data(b"\n")
if payload is not None:
write_data(payload)
# Flush
output_stream.flush()
def __on_exit(self, pid, status, data):
self.__cleanup()
self.__pid = 0
self.emit("exit")
@Gtk.Template(resource_path="/ar/xjuan/Cambalache/cmb_view.ui")
class CmbView(Gtk.Box):
__gtype_name__ = "CmbView"
__gsignals__ = {
"placeholder-selected": (GObject.SignalFlags.RUN_LAST, None, (int, int, object, int, str)),
"placeholder-activated": (GObject.SignalFlags.RUN_LAST, None, (int, int, object, int, str)),
}
show_merengue = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
preview = GObject.Property(type=bool, default=False, flags=GObject.ParamFlags.READWRITE)
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.__ui = None
self.__theme = None
self.menu = self.__create_context_menu()
super().__init__(**kwargs)
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)
# Ensure we delete all socket files when exiting
atexit.register(self.__atexit)
@Gtk.Template.Callback("on_restart_button_clicked")
def __on_restart_button_clicked(self, button):
self.restart_workspace()
def __atexit(self):
dirname = os.path.dirname(self.compositor.props.socket)
self.__merengue_command("quit")
self.__merengue.cleanup()
if os.path.exists(dirname):
shutil.rmtree(dirname)
def __set_dark_mode(self, dark):
valid, bg_color = self.get_style_context().lookup_color('theme_bg_color')
if valid:
self.compositor.props.bg_color = bg_color
return GLib.SOURCE_REMOVE
def _set_dark_mode(self, dark):
# 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.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 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 = None
def __get_ui_dirname(self, ui_id):
dirname = GLib.get_home_dir()
# Use project dir as default base directory
if self.__project.filename:
dirname = os.path.dirname(self.__project.filename)
else:
dirname = os.getcwd()
# Use UI directory
ui = self.__project.get_object_by_id(ui_id)
if ui and ui.filename:
dirname = os.path.join(dirname, os.path.dirname(ui.filename))
return dirname
def __merengue_update_ui(self, ui_id):
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)
self.__merengue_command(
"update_ui",
payload=ui,
args={
"ui_id": ui_id,
"dirname": self.__get_ui_dirname(ui_id),
"toplevels": toplevels,
"selection": objects,
},
)
def __on_changed(self, project):
self.__update_view()
def __on_ui_changed(self, project, ui, field):
if field in ["custom-fragment", "filename"]:
self.__merengue_update_ui(ui.ui_id)
def __on_object_added(self, project, obj):
self.__merengue_update_ui(obj.ui_id)
def __on_object_removed(self, project, obj):
self.__merengue_update_ui(obj.ui_id)
def __on_object_changed(self, project, obj, field):
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):
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
self.__merengue_command(
"object_property_changed",
args={
"ui_id": obj.ui_id,
"object_id": obj.object_id,
"property_id": prop.property_id,
"is_object": prop.info.is_object,
"value": prop.value,
},
)
def __on_object_layout_property_changed(self, project, obj, child, prop):
self.__merengue_command(
"object_layout_property_changed",
args={
"ui_id": obj.ui_id,
"object_id": obj.object_id,
"child_id": child.object_id,
"property_id": prop.property_id,
"value": prop.value,
},
)
def __on_object_property_binding_changed(self, project, obj, prop):
self.__merengue_update_ui(obj.ui_id)
def __get_selection_objects(self, selection, ui_id):
objects = []
for obj in selection:
if type(obj) is CmbObject and obj.ui_id == ui_id:
objects.append(obj.object_id)
return objects
def __on_project_selection_changed(self, project):
selection = project.get_selection()
if len(selection) > 0:
obj = selection[0]
if isinstance(obj, CmbUI):
ui = obj
elif isinstance(obj, CmbObject):
ui = obj.ui
else:
return
ui_id = obj.ui_id
if self.__ui != ui:
self.__ui = ui
self.__merengue_update_ui(ui.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 = None
self.__merengue_update_ui(0)
self.__update_view()
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",
args={
"css_id": obj.css_id,
"filename": filename,
"priority": obj.priority,
"is_global": obj.is_global,
"provider_for": obj.provider_for,
},
)
def __on_css_removed(self, project, obj):
self.__merengue_command("remove_css_provider", args={"css_id": obj.css_id})
def __on_css_changed(self, project, obj, field):
value = obj.get_property(field)
if field == "filename" and value:
dirname = os.path.dirname(self.project.filename)
value = os.path.join(dirname, value)
self.__merengue_command("update_css_provider", args={"css_id": obj.css_id, "field": field, "value": value})
def __on_object_data_changed(self, project, data):
self.__merengue_update_ui(data.ui_id)
def __on_object_data_removed(self, project, obj, data):
self.__merengue_update_ui(data.ui_id)
def __on_object_data_data_removed(self, project, parent, data):
self.__merengue_update_ui(data.ui_id)
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:
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)
self.__project.disconnect_by_func(self.__on_object_removed)
self.__project.disconnect_by_func(self.__on_object_changed)
self.__project.disconnect_by_func(self.__on_object_property_changed)
self.__project.disconnect_by_func(self.__on_object_layout_property_changed)
self.__project.disconnect_by_func(self.__on_object_property_binding_changed)
self.__project.disconnect_by_func(self.__on_object_data_changed)
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.__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.__project = project
self.db_inspector.project = project
self.__update_view()
if project:
project.connect("changed", self.__on_changed)
project.connect("ui-changed", self.__on_ui_changed)
project.connect("object-added", self.__on_object_added)
project.connect("object-removed", self.__on_object_removed)
project.connect("object-changed", self.__on_object_changed)
project.connect("object-property-changed", self.__on_object_property_changed)
project.connect("object-layout-property-changed", self.__on_object_layout_property_changed)
project.connect("object-property-binding-changed", self.__on_object_property_binding_changed)
project.connect("object-data-changed", self.__on_object_data_changed)
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)
# 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"
# Clear any error
self.__set_error_message(None)
self.__merengue.start()
# Update css themes
self.menu.target_tk = project.target_tk
@GObject.Property(type=str)
def gtk_theme(self):
return self.__theme
@gtk_theme.setter
def _set_theme(self, theme):
self.__theme = theme
self.__merengue_command("gtk_settings_set", args={"property": "gtk-theme-name", "value": theme})
def __on_click_gesture_pressed(self, gesture, n_press, x, y):
if gesture.get_current_button() == 3:
self.menu.popup_at(x, y)
def inspect(self):
self.stack.props.visible_child_name = "ui_xml"
self.__update_view()
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(enable_theme=True)
retval.set_parent(self)
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 self.__merengue_last_exit is None:
self.__merengue_last_exit = time.monotonic()
else:
# 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 = []
for key in selection:
obj = self.__project.get_object_by_key(key)
objects.append(obj)
self.__project.set_selection(objects)
def __load_namespaces(self):
if self.project is None:
return
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={
"namespace": info.namespace,
"version": info.version,
"object_types": info.object_types,
},
)
def __on_preview_notify(self, obj, pspec):
self.__merengue_command("set_app_property", args={"property": "preview", "value": self.preview})
def __load_css_providers(self):
providers = self.project.get_css_providers()
for css in providers:
self.__on_css_added(self.project, css)
def __on_merengue_handle_command(self, merengue, payload):
try:
cmd = json.loads(payload)
command = cmd.get("command", None)
args = cmd.get("args", {})
except Exception as e:
logger.warning(f"Merengue command error: {e}")
self.__merengue.stop()
return
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"})
self.__load_namespaces()
self.__load_css_providers()
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:
return
selection = self.project.get_selection()
if len(selection) < 0:
return
obj = selection[0]
self.__merengue_command(command, args={"ui_id": obj.ui_id, "object_id": obj.object_id, "modifier": modifier})
def add_placeholder(self, modifier=False):
self.__add_remove_placeholder("add_placeholder", modifier)
def remove_placeholder(self, modifier=False):
self.__add_remove_placeholder("remove_placeholder", modifier)
Gtk.WidgetClass.set_css_name(CmbView, "CmbView")

View File

@ -1,121 +0,0 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Created with Cambalache 0.95.0 -->
<interface>
<!-- interface-name cmb_view.ui -->
<!-- interface-copyright Juan Pablo Ugarte -->
<requires lib="gtk" version="4.14"/>
<template class="CmbView" parent="GtkBox">
<child>
<object class="GtkStack" id="stack">
<property name="hexpand">true</property>
<property name="transition-duration">300</property>
<property name="transition-type">crossfade</property>
<child>
<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>
</property>
<property name="name">ui_view</property>
<property name="title" translatable="yes">Project View</property>
</object>
</child>
<child>
<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>
</child>
</object>
</child>
</template>
</interface>

View File

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

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