From 157b425b5a4dead542d7725c70e15e0bc28dae2b Mon Sep 17 00:00:00 2001 From: DongHun Kwak Date: Tue, 30 Oct 2018 10:29:43 +0900 Subject: [PATCH] Imported Upstream version 3.27.0 --- AUTHORS | 11 - ChangeLog | 574 +++++++++++++++- MANIFEST.in | 17 + Makefile.am | 9 +- Makefile.in | 22 +- NEWS | 27 +- PKG-INFO | 12 +- PKG-INFO.in | 8 +- README => README.rst | 56 +- configure | 52 +- configure.ac | 13 +- examples/Makefile.am | 7 +- examples/Makefile.in | 8 +- examples/demo/demo.py | 356 ++++++++++ examples/demo/demos/Css/__init__.py | 0 examples/demo/demos/Css/css_accordion.py | 80 +++ examples/demo/demos/Css/css_basics.py | 148 ++++ examples/demo/demos/Css/css_multiplebgs.py | 187 +++++ examples/demo/demos/Entry/__init__.py | 0 examples/demo/demos/Entry/entry_buffer.py | 72 ++ examples/demo/demos/Entry/entry_completion.py | 86 +++ examples/demo/demos/Entry/search_entry.py | 254 +++++++ examples/demo/demos/IconView/__init__.py | 0 examples/demo/demos/IconView/iconviewbasics.py | 221 ++++++ examples/demo/demos/IconView/iconviewedit.py | 99 +++ examples/demo/demos/TreeView/__init__.py | 0 examples/demo/demos/TreeView/liststore.py | 212 ++++++ examples/demo/demos/TreeView/treemodel_filelist.py | 235 +++++++ examples/demo/demos/TreeView/treemodel_filetree.py | 280 ++++++++ examples/demo/demos/TreeView/treemodel_large.py | 143 ++++ examples/demo/demos/__init__.py | 0 examples/demo/demos/appwindow.py | 408 +++++++++++ examples/demo/demos/assistant.py | 135 ++++ examples/demo/demos/builder.py | 65 ++ examples/demo/demos/button_box.py | 122 ++++ examples/demo/demos/clipboard.py | 228 +++++++ examples/demo/demos/colorselector.py | 112 +++ examples/demo/demos/combobox.py | 319 +++++++++ examples/demo/demos/data/alphatest.png | Bin 0 -> 26529 bytes examples/demo/demos/data/apple-red.png | Bin 0 -> 3545 bytes examples/demo/demos/data/background.jpg | Bin 0 -> 22219 bytes examples/demo/demos/data/brick.png | Bin 0 -> 5043 bytes examples/demo/demos/data/brick2.png | Bin 0 -> 10713 bytes examples/demo/demos/data/css_accordion.css | 52 ++ examples/demo/demos/data/css_basics.css | 22 + examples/demo/demos/data/css_multiplebgs.css | 136 ++++ examples/demo/demos/data/cssview.css | 41 ++ examples/demo/demos/data/demo.gresource | Bin 0 -> 31110 bytes examples/demo/demos/data/demo.gresource.xml | 18 + examples/demo/demos/data/demo.ui | 258 +++++++ examples/demo/demos/data/floppybuddy.gif | Bin 0 -> 5216 bytes examples/demo/demos/data/gnome-applets.png | Bin 0 -> 3090 bytes examples/demo/demos/data/gnome-calendar.png | Bin 0 -> 2755 bytes examples/demo/demos/data/gnome-foot.png | Bin 0 -> 2916 bytes examples/demo/demos/data/gnome-fs-directory.png | Bin 0 -> 2044 bytes examples/demo/demos/data/gnome-fs-regular.png | Bin 0 -> 1795 bytes examples/demo/demos/data/gnome-gimp.png | Bin 0 -> 3410 bytes examples/demo/demos/data/gnome-gmush.png | Bin 0 -> 3244 bytes examples/demo/demos/data/gnome-gsame.png | Bin 0 -> 4263 bytes examples/demo/demos/data/gnu-keys.png | Bin 0 -> 3852 bytes examples/demo/demos/data/gtk-logo-rgb.gif | Bin 0 -> 6427 bytes examples/demo/demos/data/reset.css | 68 ++ examples/demo/demos/dialogs.py | 156 +++++ examples/demo/demos/drawingarea.py | 207 ++++++ examples/demo/demos/expander.py | 62 ++ examples/demo/demos/flowbox.py | 752 +++++++++++++++++++++ examples/demo/demos/images.py | 305 +++++++++ examples/demo/demos/infobars.py | 103 +++ examples/demo/demos/links.py | 77 +++ examples/demo/demos/menus.py | 134 ++++ examples/demo/demos/pickers.py | 77 +++ examples/demo/demos/pixbuf.py | 184 +++++ examples/demo/demos/printing.py | 180 +++++ examples/demo/demos/rotatedtext.py | 199 ++++++ examples/demo/demos/test.py | 16 + gi/_ossighelper.py | 263 +++++++ gi/overrides/GLib.py | 29 +- gi/overrides/Gio.py | 13 + gi/overrides/Gtk.py | 64 +- m4/ax_code_coverage.m4 | 12 +- m4/ax_compiler_flags_cflags.m4 | 10 +- m4/glib-2.0.m4 | 5 +- pygobject-3.0-uninstalled.pc.in | 12 - pygobject.doap | 7 + setup.py | 409 ++++++++--- tests/Makefile.am | 1 + tests/Makefile.in | 1 + tests/test_everything.py | 8 +- tests/test_gi.py | 25 +- tests/test_ossig.py | 173 +++++ tests/test_overrides_gtk.py | 12 + pygi-convert.sh => tools/pygi-convert.sh | 0 92 files changed, 8411 insertions(+), 258 deletions(-) delete mode 100644 AUTHORS create mode 100644 MANIFEST.in rename README => README.rst (74%) create mode 100755 examples/demo/demo.py create mode 100644 examples/demo/demos/Css/__init__.py create mode 100644 examples/demo/demos/Css/css_accordion.py create mode 100644 examples/demo/demos/Css/css_basics.py create mode 100644 examples/demo/demos/Css/css_multiplebgs.py create mode 100644 examples/demo/demos/Entry/__init__.py create mode 100644 examples/demo/demos/Entry/entry_buffer.py create mode 100644 examples/demo/demos/Entry/entry_completion.py create mode 100644 examples/demo/demos/Entry/search_entry.py create mode 100644 examples/demo/demos/IconView/__init__.py create mode 100644 examples/demo/demos/IconView/iconviewbasics.py create mode 100644 examples/demo/demos/IconView/iconviewedit.py create mode 100644 examples/demo/demos/TreeView/__init__.py create mode 100644 examples/demo/demos/TreeView/liststore.py create mode 100644 examples/demo/demos/TreeView/treemodel_filelist.py create mode 100644 examples/demo/demos/TreeView/treemodel_filetree.py create mode 100644 examples/demo/demos/TreeView/treemodel_large.py create mode 100644 examples/demo/demos/__init__.py create mode 100644 examples/demo/demos/appwindow.py create mode 100644 examples/demo/demos/assistant.py create mode 100644 examples/demo/demos/builder.py create mode 100644 examples/demo/demos/button_box.py create mode 100644 examples/demo/demos/clipboard.py create mode 100644 examples/demo/demos/colorselector.py create mode 100644 examples/demo/demos/combobox.py create mode 100644 examples/demo/demos/data/alphatest.png create mode 100644 examples/demo/demos/data/apple-red.png create mode 100644 examples/demo/demos/data/background.jpg create mode 100644 examples/demo/demos/data/brick.png create mode 100644 examples/demo/demos/data/brick2.png create mode 100644 examples/demo/demos/data/css_accordion.css create mode 100644 examples/demo/demos/data/css_basics.css create mode 100644 examples/demo/demos/data/css_multiplebgs.css create mode 100644 examples/demo/demos/data/cssview.css create mode 100644 examples/demo/demos/data/demo.gresource create mode 100644 examples/demo/demos/data/demo.gresource.xml create mode 100644 examples/demo/demos/data/demo.ui create mode 100644 examples/demo/demos/data/floppybuddy.gif create mode 100644 examples/demo/demos/data/gnome-applets.png create mode 100644 examples/demo/demos/data/gnome-calendar.png create mode 100644 examples/demo/demos/data/gnome-foot.png create mode 100644 examples/demo/demos/data/gnome-fs-directory.png create mode 100644 examples/demo/demos/data/gnome-fs-regular.png create mode 100644 examples/demo/demos/data/gnome-gimp.png create mode 100644 examples/demo/demos/data/gnome-gmush.png create mode 100644 examples/demo/demos/data/gnome-gsame.png create mode 100644 examples/demo/demos/data/gnu-keys.png create mode 100644 examples/demo/demos/data/gtk-logo-rgb.gif create mode 100644 examples/demo/demos/data/reset.css create mode 100644 examples/demo/demos/dialogs.py create mode 100644 examples/demo/demos/drawingarea.py create mode 100644 examples/demo/demos/expander.py create mode 100755 examples/demo/demos/flowbox.py create mode 100644 examples/demo/demos/images.py create mode 100644 examples/demo/demos/infobars.py create mode 100644 examples/demo/demos/links.py create mode 100644 examples/demo/demos/menus.py create mode 100644 examples/demo/demos/pickers.py create mode 100644 examples/demo/demos/pixbuf.py create mode 100644 examples/demo/demos/printing.py create mode 100644 examples/demo/demos/rotatedtext.py create mode 100644 examples/demo/demos/test.py create mode 100644 gi/_ossighelper.py delete mode 100644 pygobject-3.0-uninstalled.pc.in mode change 100755 => 100644 setup.py create mode 100644 tests/test_ossig.py rename pygi-convert.sh => tools/pygi-convert.sh (100%) diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 76b1091..0000000 --- a/AUTHORS +++ /dev/null @@ -1,11 +0,0 @@ -Original Authors: -James Henstridge -Johan Dahlin - -Current Maintainers: -Ignacio Casal Quinteiro -Martin Pitt -Paolo Borelli -Sebastian Pölsterl -Simon Feltman -Tomeu Vizoso diff --git a/ChangeLog b/ChangeLog index 23d0994..c302ac3 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,500 @@ -commit 17b4ba8707a8c6c24cd52e59a1f107dccfa9fd55 +commit eb2ff4362878d0e348c67c606a32e8d332e2454d +Author: Christoph Reiter +Date: Fri Dec 8 16:56:10 2017 +0100 + + demo: pep8 fixes + + The demo app now gets analyzed by pep8 since it was moved to the + examples. + + examples/demo/demos/Css/css_accordion.py | 1 + + examples/demo/demos/Entry/entry_buffer.py | 1 + + examples/demo/demos/Entry/entry_completion.py | 1 + + examples/demo/demos/Entry/search_entry.py | 1 + + examples/demo/demos/IconView/iconviewbasics.py | 1 + + examples/demo/demos/IconView/iconviewedit.py | 1 + + examples/demo/demos/TreeView/liststore.py | 2 + + examples/demo/demos/TreeView/treemodel_filelist.py | 1 + + examples/demo/demos/TreeView/treemodel_filetree.py | 1 + + examples/demo/demos/appwindow.py | 1 + + examples/demo/demos/assistant.py | 1 + + examples/demo/demos/builder.py | 1 + + examples/demo/demos/button_box.py | 1 + + examples/demo/demos/clipboard.py | 1 + + examples/demo/demos/colorselector.py | 1 + + examples/demo/demos/combobox.py | 1 + + examples/demo/demos/dialogs.py | 1 + + examples/demo/demos/drawingarea.py | 11 +- + examples/demo/demos/expander.py | 1 + + examples/demo/demos/flowbox.py | 1332 + ++++++++++---------- + examples/demo/demos/images.py | 1 + + examples/demo/demos/infobars.py | 1 + + examples/demo/demos/links.py | 1 + + examples/demo/demos/menus.py | 1 + + examples/demo/demos/pickers.py | 1 + + examples/demo/demos/pixbuf.py | 1 + + examples/demo/demos/printing.py | 1 + + examples/demo/demos/rotatedtext.py | 1 + + 28 files changed, 700 insertions(+), 670 deletions(-) + +commit af9ddf322f7cf377996b8fa29906bd4b254dc001 +Author: Christoph Reiter +Date: Fri Dec 8 16:40:33 2017 +0100 + + Fix ctypes.PyDLL construction under Windows + + We require the ctypes.pythonapi interface but can't use the global one + since we have to change it which could potentially break other users + of that interface. + + Turns out simply passing None to PyDLL() only works on Unix to + load the + CPython library. Instead copy the logic from the ctypes module in the + stdlib. + + https://bugzilla.gnome.org/show_bug.cgi?id=622084 + + gi/_ossighelper.py | 13 ++++++++++++- + 1 file changed, 12 insertions(+), 1 deletion(-) + +commit d19aca693950c0edb02b226db8bcf81a5304870e +Author: Christoph Reiter +Date: Fri Dec 8 14:40:58 2017 +0100 + + configure.ac: Error out in case autoconf-archive isn't installed + + Check whether the AX_IS_RELEASE macro is defined and if not print + a proper error message. + + https://bugzilla.gnome.org/show_bug.cgi?id=784428 + + configure.ac | 3 +++ + 1 file changed, 3 insertions(+) + +commit 26001a6ae42f8e10ed4c44bc9414e219b946cee0 +Author: Christoph Reiter +Date: Wed Dec 6 18:37:45 2017 +0100 + + Move pygi-convert.sh into tools + + MANIFEST.in | 2 +- + Makefile.am | 2 +- + pygi-convert.sh => tools/pygi-convert.sh | 0 + 3 files changed, 2 insertions(+), 2 deletions(-) + +commit dd1507be6c198319304342b73c8ec1ec2a102145 +Author: Christoph Reiter +Date: Wed Dec 6 18:26:13 2017 +0100 + + README: Convert to reST + + MANIFEST.in | 2 +- + Makefile.am | 3 ++- + README => README.rst | 56 + ++++++++++++++++++++++++++++++++++------------------ + 3 files changed, 40 insertions(+), 21 deletions(-) + +commit e4a52b9f99667df98806d0c4ee78c2909ead27e1 +Author: Christoph Reiter +Date: Wed Dec 6 17:53:51 2017 +0100 + + demo: Move demo into examples and dist it + + We might want to install it in the future, but until then at least put + it into the tarball. + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + MANIFEST.in | 3 +-- + examples/Makefile.am | 7 ++++++- + {demo => examples/demo}/demo.py | 0 + {demo => examples/demo}/demos/Css/__init__.py | 0 + {demo => examples/demo}/demos/Css/css_accordion.py | 0 + {demo => examples/demo}/demos/Css/css_basics.py | 0 + {demo => examples/demo}/demos/Css/css_multiplebgs.py | 0 + {demo => examples/demo}/demos/Entry/__init__.py | 0 + {demo => examples/demo}/demos/Entry/entry_buffer.py | 0 + {demo => examples/demo}/demos/Entry/entry_completion.py | 0 + {demo => examples/demo}/demos/Entry/search_entry.py | 0 + {demo => examples/demo}/demos/IconView/__init__.py | 0 + {demo => examples/demo}/demos/IconView/iconviewbasics.py | 0 + {demo => examples/demo}/demos/IconView/iconviewedit.py | 0 + {demo => examples/demo}/demos/TreeView/__init__.py | 0 + {demo => examples/demo}/demos/TreeView/liststore.py | 0 + .../demo}/demos/TreeView/treemodel_filelist.py | 0 + .../demo}/demos/TreeView/treemodel_filetree.py | 0 + {demo => examples/demo}/demos/TreeView/treemodel_large.py | 0 + {demo => examples/demo}/demos/__init__.py | 0 + {demo => examples/demo}/demos/appwindow.py | 0 + {demo => examples/demo}/demos/assistant.py | 0 + {demo => examples/demo}/demos/builder.py | 0 + {demo => examples/demo}/demos/button_box.py | 0 + {demo => examples/demo}/demos/clipboard.py | 0 + {demo => examples/demo}/demos/colorselector.py | 0 + {demo => examples/demo}/demos/combobox.py | 0 + {demo => examples/demo}/demos/data/alphatest.png | Bin + {demo => examples/demo}/demos/data/apple-red.png | Bin + {demo => examples/demo}/demos/data/background.jpg | Bin + {demo => examples/demo}/demos/data/brick.png | Bin + {demo => examples/demo}/demos/data/brick2.png | Bin + {demo => examples/demo}/demos/data/css_accordion.css | 0 + {demo => examples/demo}/demos/data/css_basics.css | 0 + {demo => examples/demo}/demos/data/css_multiplebgs.css | 0 + {demo => examples/demo}/demos/data/cssview.css | 0 + {demo => examples/demo}/demos/data/demo.gresource | Bin + {demo => examples/demo}/demos/data/demo.gresource.xml | 0 + {demo => examples/demo}/demos/data/demo.ui | 0 + {demo => examples/demo}/demos/data/floppybuddy.gif | Bin + {demo => examples/demo}/demos/data/gnome-applets.png | Bin + {demo => examples/demo}/demos/data/gnome-calendar.png | Bin + {demo => examples/demo}/demos/data/gnome-foot.png | Bin + {demo => examples/demo}/demos/data/gnome-fs-directory.png | Bin + {demo => examples/demo}/demos/data/gnome-fs-regular.png | Bin + {demo => examples/demo}/demos/data/gnome-gimp.png | Bin + {demo => examples/demo}/demos/data/gnome-gmush.png | Bin + {demo => examples/demo}/demos/data/gnome-gsame.png | Bin + {demo => examples/demo}/demos/data/gnu-keys.png | Bin + {demo => examples/demo}/demos/data/gtk-logo-rgb.gif | Bin + {demo => examples/demo}/demos/data/reset.css | 0 + {demo => examples/demo}/demos/dialogs.py | 0 + {demo => examples/demo}/demos/drawingarea.py | 0 + {demo => examples/demo}/demos/expander.py | 0 + {demo => examples/demo}/demos/flowbox.py | 0 + {demo => examples/demo}/demos/images.py | 0 + {demo => examples/demo}/demos/infobars.py | 0 + {demo => examples/demo}/demos/links.py | 0 + {demo => examples/demo}/demos/menus.py | 0 + {demo => examples/demo}/demos/pickers.py | 0 + {demo => examples/demo}/demos/pixbuf.py | 0 + {demo => examples/demo}/demos/printing.py | 0 + {demo => examples/demo}/demos/rotatedtext.py | 0 + {demo => examples/demo}/demos/test.py | 0 + 64 files changed, 7 insertions(+), 3 deletions(-) + +commit f8ff3b5e0e769e6db1509426af28728129780529 +Author: Gian Mario Tagliaretti +Date: Thu Sep 4 08:51:10 2014 +0100 + + demo: Add new Gtk.FlowBox example + + Added a new example for the Gtk.FlowBox class added in GTK+ 3.12. + This example additionally makes use of Gtk.HeaderBar. + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + demo/demos/flowbox.py | 750 + ++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 750 insertions(+) + +commit 3f4a176c77a8026949c04d85d0a822554c4c661c +Author: Simon Feltman +Date: Tue Sep 2 10:53:29 2014 -0700 + + demo: Use HeaderBar for main app window + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + demo/demo.py | 23 +++++++++++------------ + 1 file changed, 11 insertions(+), 12 deletions(-) + +commit 995f1e72eed0970d56d871ed8452c61f92a40696 +Author: Simon Feltman +Date: Tue Sep 2 11:10:31 2014 -0700 + + demo: PyFlakes and PEP8 fixes + + Since PyFlakes and PEP8 are not currently run on the demo due to it + not being packaged, manually check and fix this for now. + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + demo/demo.py | 2 +- + demo/demos/Entry/entry_buffer.py | 2 +- + demo/demos/Entry/entry_completion.py | 2 +- + demo/demos/clipboard.py | 6 +----- + demo/demos/combobox.py | 2 +- + 5 files changed, 5 insertions(+), 9 deletions(-) + +commit 5d2ce69ed51aec62872ebb2a129c2b8a6b4256af +Author: Simon Feltman +Date: Tue Sep 2 11:01:55 2014 -0700 + + demo: Rename gtk-demo.py to demo.py + + Remove GTK+ specificity which also allows the demo app to be used as + one of the demos itself. + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + demo/{gtk-demo.py => demo.py} | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) + +commit 7e48d91a5e1f180cbe4adea41bd41939c26c77b0 +Author: Simon Feltman +Date: Mon Sep 1 22:04:29 2014 -0700 + + demo: Rename demos/gtk-demo to simply demo + + Move the entire "pygobject/demos/gtk-demo" folder to "pygobject/demo". + Since we only have a single demo app which should be used for all + platform demos we can remove the diectory obfuscation. + + https://bugzilla.gnome.org/show_bug.cgi?id=735918 + + {demos/gtk-demo => demo}/demos/Css/__init__.py | 0 + {demos/gtk-demo => demo}/demos/Css/css_accordion.py | 0 + {demos/gtk-demo => demo}/demos/Css/css_basics.py | 0 + {demos/gtk-demo => demo}/demos/Css/css_multiplebgs.py | 0 + {demos/gtk-demo => demo}/demos/Entry/__init__.py | 0 + {demos/gtk-demo => demo}/demos/Entry/entry_buffer.py | 0 + {demos/gtk-demo => demo}/demos/Entry/entry_completion.py | 0 + {demos/gtk-demo => demo}/demos/Entry/search_entry.py | 0 + {demos/gtk-demo => demo}/demos/IconView/__init__.py | 0 + {demos/gtk-demo => demo}/demos/IconView/iconviewbasics.py | 0 + {demos/gtk-demo => demo}/demos/IconView/iconviewedit.py | 0 + {demos/gtk-demo => demo}/demos/TreeView/__init__.py | 0 + {demos/gtk-demo => demo}/demos/TreeView/liststore.py | 0 + .../gtk-demo => demo}/demos/TreeView/treemodel_filelist.py | 0 + .../gtk-demo => demo}/demos/TreeView/treemodel_filetree.py | 0 + {demos/gtk-demo => demo}/demos/TreeView/treemodel_large.py | 0 + {demos/gtk-demo => demo}/demos/__init__.py | 0 + {demos/gtk-demo => demo}/demos/appwindow.py | 0 + {demos/gtk-demo => demo}/demos/assistant.py | 0 + {demos/gtk-demo => demo}/demos/builder.py | 0 + {demos/gtk-demo => demo}/demos/button_box.py | 0 + {demos/gtk-demo => demo}/demos/clipboard.py | 0 + {demos/gtk-demo => demo}/demos/colorselector.py | 0 + {demos/gtk-demo => demo}/demos/combobox.py | 0 + {demos/gtk-demo => demo}/demos/data/alphatest.png | Bin + {demos/gtk-demo => demo}/demos/data/apple-red.png | Bin + {demos/gtk-demo => demo}/demos/data/background.jpg | Bin + {demos/gtk-demo => demo}/demos/data/brick.png | Bin + {demos/gtk-demo => demo}/demos/data/brick2.png | Bin + {demos/gtk-demo => demo}/demos/data/css_accordion.css | 0 + {demos/gtk-demo => demo}/demos/data/css_basics.css | 0 + {demos/gtk-demo => demo}/demos/data/css_multiplebgs.css | 0 + {demos/gtk-demo => demo}/demos/data/cssview.css | 0 + {demos/gtk-demo => demo}/demos/data/demo.gresource | Bin + {demos/gtk-demo => demo}/demos/data/demo.gresource.xml | 0 + {demos/gtk-demo => demo}/demos/data/demo.ui | 0 + {demos/gtk-demo => demo}/demos/data/floppybuddy.gif | Bin + {demos/gtk-demo => demo}/demos/data/gnome-applets.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-calendar.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-foot.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-fs-directory.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-fs-regular.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-gimp.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-gmush.png | Bin + {demos/gtk-demo => demo}/demos/data/gnome-gsame.png | Bin + {demos/gtk-demo => demo}/demos/data/gnu-keys.png | Bin + {demos/gtk-demo => demo}/demos/data/gtk-logo-rgb.gif | Bin + {demos/gtk-demo => demo}/demos/data/reset.css | 0 + {demos/gtk-demo => demo}/demos/dialogs.py | 0 + {demos/gtk-demo => demo}/demos/drawingarea.py | 0 + {demos/gtk-demo => demo}/demos/expander.py | 0 + {demos/gtk-demo => demo}/demos/images.py | 0 + {demos/gtk-demo => demo}/demos/infobars.py | 0 + {demos/gtk-demo => demo}/demos/links.py | 0 + {demos/gtk-demo => demo}/demos/menus.py | 0 + {demos/gtk-demo => demo}/demos/pickers.py | 0 + {demos/gtk-demo => demo}/demos/pixbuf.py | 0 + {demos/gtk-demo => demo}/demos/printing.py | 0 + {demos/gtk-demo => demo}/demos/rotatedtext.py | 0 + {demos/gtk-demo => demo}/demos/test.py | 0 + {demos/gtk-demo => demo}/gtk-demo.py | 0 + 61 files changed, 0 insertions(+), 0 deletions(-) + +commit 1ea36ccfc755693b9ce4cf28be549704bf5370d3 +Author: Christoph Reiter +Date: Wed Dec 6 16:54:54 2017 +0100 + + Remove AUTHORS file + + No need to duplicate that information over and over. + It's in the README and .doap file still + + AUTHORS | 11 ----------- + MANIFEST.in | 1 - + README | 4 ++-- + 3 files changed, 2 insertions(+), 14 deletions(-) + +commit 9462873823a5459276400c0d6cf11e084b681751 +Author: Christoph Reiter +Date: Wed Dec 6 15:55:54 2017 +0100 + + Remove pre-commit.hook + + It was obviously not used in some time as the git command doesn't + exist anymore. + If we ever get per pull/merge request CI we can look into bringing + something + like this back. + + MANIFEST.in | 1 - + pre-commit.hook | 39 --------------------------------------- + 2 files changed, 40 deletions(-) + +commit 3e455944f5835c750911d3178a0607201f23f1a8 +Author: Christoph Reiter +Date: Mon Dec 4 15:33:00 2017 +0100 + + setup.py: Port to distutils/setuptools + + Instead of wrapping autotools add a proper setuptools based build + system. + Compared to the autotools one this does not install .pc files + or headers + and does not allow running tests. + + It uses pkg-config for discovering dependencies and explictely + searches + for .pc files in the Python prefix so that pycairo installations in a + virtualenv are discovered. When using MSVC, pkg-config is skipped and + it is assumend that INCLUDE and LIB is properly set up. + + Version information and requirements are parsed from configure.ac, + package + metadata is parsed from PKG-INFO.in. + + Also adds a "setup.py distcheck" command which makes sure all + tracked files + end up in the tarball and that the tarball builds (no tests are + run atm). + + https://bugzilla.gnome.org/show_bug.cgi?id=789211 + + .gitignore | 1 + + MANIFEST.in | 20 +++ + Makefile.am | 3 +- + PKG-INFO.in | 8 +- + configure.ac | 3 +- + setup.py | 409 + +++++++++++++++++++++++++++++++++++++++++++++++------------ + 6 files changed, 359 insertions(+), 85 deletions(-) + +commit 58f677bfaa0f117465a9e2146c5d83768b5a76ac +Author: Christoph Reiter +Date: Fri Nov 24 13:11:26 2017 +0100 + + Install a default SIGINT handler for functions which start an + event loop + + Currently ctrl+c on a program blocked on Gtk.main() will raise + an exception + but not return control. While it's easy to set up the proper signal + handling and + stop the event loop or execute some other application shutdown code + it's nice to have a good default behaviour for small + prototypes/examples + or when testing some code in an interactive console. + + This adds a context manager which registers a SIGINT handler only + in case + the default Python signal handler is active and restores the + original handle + afterwards. Since signal handlers registered through + g_unix_signal_add() + are not detected by Python's signal module we use PyOS_getsig() + through ctypes + to detect if the signal handler is changed from outside. + + In case of nested event loops, all of them will be aborted. + In case an event loop is started in a thread, nothing will happen. + + The context manager is used in the overrides for Gtk.main(), + Gtk.Dialog.run(), + Gio.Application.run() and GLib.MainLoop.run() + + This also fixes GLib.MainLoop.run() replacing a non-default signal + handler + and not restoring the default one: + https://bugzilla.gnome.org/show_bug.cgi?id=698623 + + https://bugzilla.gnome.org/show_bug.cgi?id=622084 + + gi/_ossighelper.py | 115 + +++++++++++++++++++++++++++++++++++++++++++++++++++ + gi/overrides/GLib.py | 31 +++----------- + gi/overrides/Gio.py | 7 ++-- + gi/overrides/Gtk.py | 12 +++--- + tests/test_ossig.py | 73 +++++++++++++++++++++++++++++++- + 5 files changed, 203 insertions(+), 35 deletions(-) + +commit a321f6e9d8f5b8e779892eab4ce759b60ff98e39 +Author: Christoph Reiter +Date: Fri Nov 17 20:05:24 2017 +0100 + + Make Python OS signal handlers run when an event loop is idling + + When Python receives a signal such as SIGINT it sets a flag and + will execute + the registered signal handler on the next call to + PyErr_CheckSignals(). + In case the main thread is blocked by an idling event loop (say + Gtk.main() + or Gtk.Dialog.run()) the check never happens and the signal handler + will not get executed. + + To work around the issue use signal.set_wakeup_fd() to wake up + the active + event loop when a signal is received, which will invoke a Python + callback + which will lead to the signal handler being executed. + + This patch enables it in overrides for Gtk.main(), Gtk.Dialog.run(), + Gio.Application.run() and GLib.MainLoop.run(). + + Works on Unix, and on Windows with Python 3.5+. + + With this fix in place it is possible to have a cross platform way to + react to SIGINT (GLib.unix_signal_add() worked, but not on Windows), + for example: + + signal.signal(signal.SIGINT, lambda *args: Gtk.main_quit()) + Gtk.main() + + https://bugzilla.gnome.org/show_bug.cgi?id=622084 + + Makefile.am | 3 +- + gi/_ossighelper.py | 137 + +++++++++++++++++++++++++++++++++++++++++++++++++++ + gi/overrides/GLib.py | 4 +- + gi/overrides/Gio.py | 12 +++++ + gi/overrides/Gtk.py | 14 ++++++ + tests/Makefile.am | 1 + + tests/test_ossig.py | 102 ++++++++++++++++++++++++++++++++++++++ + 7 files changed, 271 insertions(+), 2 deletions(-) + +commit 46a9dade170127006df98d44b1a9fb2035ada86b +Author: Christoph Reiter +Date: Fri Nov 24 17:40:11 2017 +0100 + + Drop Python 3.3 support + + It's EOL and not used much (https://hynek.me/articles/python3-2016/) + + https://bugzilla.gnome.org/show_bug.cgi?id=790787 + + configure.ac | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit bd165405754ba44dea12fd3f31b841b5c8ba0f1a +Author: Sander Sweers +Date: Sun Aug 20 22:44:15 2017 +0200 + + Drop set_value usage in Gtk.List/TreeStore.set override + + this causes multiple updates to the store each emitting a signal. + + https://bugzilla.gnome.org/show_bug.cgi?id=790346 + + gi/overrides/Gtk.py | 48 + ++++++++++++++++++++++++--------------------- + tests/test_overrides_gtk.py | 12 ++++++++++++ + 2 files changed, 38 insertions(+), 22 deletions(-) + +commit 5f63b8c626eb7f27de346dac2b66f07794e61b07 Author: Christoph Reiter Date: Thu Oct 26 17:24:02 2017 +0200 @@ -31,7 +527,7 @@ Date: Thu Oct 26 17:24:02 2017 +0200 tests/test_signal.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) -commit b4bf1b9d936e021b1645c069c2e0a3062cfab62b +commit f13b0d6e0d97e21aa2a4553a036362dec9d0c4cb Author: Daniel Colascione Date: Tue Oct 24 14:42:43 2017 +0200 @@ -62,7 +558,7 @@ Date: Tue Oct 24 14:42:43 2017 +0200 gi/gimodule.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) -commit 1136f385d6080297bd57715b749c67f5e7208ba2 +commit 280e80c54e6f2c91638195b92736f5c2b34bbbbd Author: Christoph Reiter Date: Thu Oct 26 09:35:09 2017 +0200 @@ -77,7 +573,7 @@ Date: Thu Oct 26 09:35:09 2017 +0200 tests/test_gi.py | 1 - 1 file changed, 1 deletion(-) -commit a37687d3d8bdc42aea63e551401e6686c926c556 +commit 5f41add624990255dfebf2d726af946599f7bcf6 Author: Christoph Reiter Date: Mon Oct 23 12:41:45 2017 +0200 @@ -94,7 +590,7 @@ Date: Mon Oct 23 12:41:45 2017 +0200 tests/test_gi.py | 5 +++++ 3 files changed, 45 insertions(+) -commit 44a852191a67bc7ef76202412a0102de46eb26f0 +commit 0695d234b0e3e9b0f15a7548816c5c4c96a41ab5 Author: Philippe Renon Date: Thu Aug 31 16:39:08 2017 +0200 @@ -109,7 +605,7 @@ Date: Thu Aug 31 16:39:08 2017 +0200 gi/pygi-enum-marshal.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) -commit fa4330df4e26bb9f77a5cf081d3cc40c342709b9 +commit 1bf7d2cddcd24f619a268d0af85d7919d72bacba Author: Christoph Reiter Date: Sun Oct 22 17:59:17 2017 +0200 @@ -126,7 +622,16 @@ Date: Sun Oct 22 17:59:17 2017 +0200 tests/test_gi.py | 6 ++++++ 4 files changed, 32 insertions(+) -commit 5f0f3b330cfa1eb11db4f376d141445847cb9d16 +commit e502d0097f28e6c65d3d5120230fb428aabbc083 +Author: Christoph Reiter +Date: Fri Oct 20 09:48:07 2017 +0200 + + pygobject.doap: Add myself as maintainer + + pygobject.doap | 7 +++++++ + 1 file changed, 7 insertions(+) + +commit 37db51220668238e4870b7220b496803d942691b Author: James Clarke Date: Fri Oct 13 18:04:45 2017 +0100 @@ -146,7 +651,7 @@ Date: Fri Oct 13 18:04:45 2017 +0100 gi/pygi-closure.c | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) -commit d831decad9e8fdb449518997dee1a5eaa21e0313 +commit 3c791a5d4b17d647a531de04469d04f77fce0548 Author: Christoph Reiter Date: Fri Oct 13 19:24:01 2017 +0200 @@ -161,11 +666,58 @@ Date: Fri Oct 13 19:24:01 2017 +0200 Makefile.am | 7 +++++++ 1 file changed, 7 insertions(+) -commit 5b61ac3f2a66d93110642f43bec4f2a4e656681a +commit c4995493c233225b1fa5a3e5406e78d02e3b86e6 +Author: Christoph Reiter +Date: Fri Oct 13 17:22:02 2017 +0200 + + Drop pygobject-3.0-uninstalled.pc file + + Like glib/gtk+ did in 2011: + https://git.gnome.org/browse/glib/commit/?id=306aa62ea5fa4d3a57bca419afcc159f9509b476 + + configure.ac | 1 - + pygobject-3.0-uninstalled.pc.in | 12 ------------ + 2 files changed, 13 deletions(-) + +commit 3363049be9d8cd277b33127427694588598aa6dd +Author: Christoph Reiter +Date: Fri Oct 13 16:08:01 2017 +0200 + + tests: Windows fix + + some fallout from fd5f2a09ce48329d2df191eca9a0cea926ddfb5b + + tests/test_everything.py | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit fd5f2a09ce48329d2df191eca9a0cea926ddfb5b +Author: Christoph Reiter +Date: Fri Oct 13 15:29:40 2017 +0200 + + tests: some more C locale fixes + + see f40df0a09803ec9c729d842f1f83c6d56aebac22 + + tests/test_everything.py | 8 +++++++- + tests/test_gi.py | 6 ++++-- + 2 files changed, 11 insertions(+), 3 deletions(-) + +commit f40df0a09803ec9c729d842f1f83c6d56aebac22 +Author: Christoph Reiter +Date: Fri Oct 13 14:32:44 2017 +0200 + + tests: Make the test suite pass with the C locale + + Some filename tests assumed a Unicode locale, skip those. + + tests/test_gi.py | 29 +++++++++++++++++++++++++---- + 1 file changed, 25 insertions(+), 4 deletions(-) + +commit 857fb023886476988d99d35c92894cdbc1bbeab2 Author: Christoph Reiter -Date: Thu Oct 12 18:58:04 2017 +0200 +Date: Tue Sep 12 08:35:12 2017 +0200 - configure.ac: version bump to 3.26.1 + configure.ac: post-release version bump to 3.27.0 configure.ac | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..b9f067e --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,17 @@ +include *.am +include autogen.sh +include configure.ac +include COPYING +include HACKING +include *.in +include INSTALL +include m4/introspection.m4 +include m4/python.m4 +include NEWS +include tools/pygi-convert.sh +include pygobject.doap +include README.rst +recursive-include examples *.py *.am *.png *.css *.ui *.gif *.gresource *.jpg *.xml +recursive-include gi *.am *.h +recursive-include pygtkcompat *.am +recursive-include tests *.py *.c *.h *.xml *.supp *nouppera *.am diff --git a/Makefile.am b/Makefile.am index c3ba1a8..643aefc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -17,10 +17,12 @@ EXTRA_DIST = \ pygobject-$(PLATFORM_VERSION).pc.in \ PKG-INFO \ PKG-INFO.in \ - pygi-convert.sh \ + tools/pygi-convert.sh \ m4/python.m4 \ m4/introspection.m4 \ - setup.py + setup.py \ + MANIFEST.in \ + README.rst MAINTAINERCLEANFILES = \ $(srcdir)/INSTALL \ @@ -63,7 +65,8 @@ nobase_pyexec_PYTHON = \ gi/_propertyhelper.py \ gi/_signalhelper.py \ gi/_option.py \ - gi/_error.py + gi/_error.py \ + gi/_ossighelper.py # if we build in a separate tree, we need to symlink the *.py files from the # source tree; Python does not accept the extensions and modules in different diff --git a/Makefile.in b/Makefile.in index 08ce9e1..9190734 100644 --- a/Makefile.in +++ b/Makefile.in @@ -117,8 +117,7 @@ am__CONFIG_DISTCLEAN_FILES = config.status config.cache config.log \ configure.lineno config.status.lineno mkinstalldirs = $(install_sh) -d CONFIG_HEADER = config.h -CONFIG_CLEAN_FILES = pygobject-3.0.pc pygobject-3.0-uninstalled.pc \ - PKG-INFO +CONFIG_CLEAN_FILES = pygobject-3.0.pc PKG-INFO CONFIG_CLEAN_VPATH_FILES = am__vpath_adj_setup = srcdirstrip=`echo "$(srcdir)" | sed 's|.|.|g'`; am__vpath_adj = case $$p in \ @@ -213,11 +212,9 @@ CTAGS = ctags CSCOPE = cscope DIST_SUBDIRS = $(SUBDIRS) am__DIST_COMMON = $(srcdir)/Makefile.in $(srcdir)/PKG-INFO.in \ - $(srcdir)/config.h.in \ - $(srcdir)/pygobject-3.0-uninstalled.pc.in \ - $(srcdir)/pygobject-3.0.pc.in AUTHORS COPYING ChangeLog \ - INSTALL NEWS README compile config.guess config.sub install-sh \ - ltmain.sh missing py-compile + $(srcdir)/config.h.in $(srcdir)/pygobject-3.0.pc.in COPYING \ + ChangeLog INSTALL NEWS compile config.guess config.sub \ + install-sh ltmain.sh missing py-compile DISTFILES = $(DIST_COMMON) $(DIST_SOURCES) $(TEXINFOS) $(EXTRA_DIST) distdir = $(PACKAGE)-$(VERSION) top_distdir = $(distdir) @@ -454,10 +451,12 @@ EXTRA_DIST = \ pygobject-$(PLATFORM_VERSION).pc.in \ PKG-INFO \ PKG-INFO.in \ - pygi-convert.sh \ + tools/pygi-convert.sh \ m4/python.m4 \ m4/introspection.m4 \ - setup.py + setup.py \ + MANIFEST.in \ + README.rst MAINTAINERCLEANFILES = \ $(srcdir)/INSTALL \ @@ -500,7 +499,8 @@ nobase_pyexec_PYTHON = \ gi/_propertyhelper.py \ gi/_signalhelper.py \ gi/_option.py \ - gi/_error.py + gi/_error.py \ + gi/_ossighelper.py # pkg-config files @@ -564,8 +564,6 @@ distclean-hdr: -rm -f config.h stamp-h1 pygobject-3.0.pc: $(top_builddir)/config.status $(srcdir)/pygobject-3.0.pc.in cd $(top_builddir) && $(SHELL) ./config.status $@ -pygobject-3.0-uninstalled.pc: $(top_builddir)/config.status $(srcdir)/pygobject-3.0-uninstalled.pc.in - cd $(top_builddir) && $(SHELL) ./config.status $@ PKG-INFO: $(top_builddir)/config.status $(srcdir)/PKG-INFO.in cd $(top_builddir) && $(SHELL) ./config.status $@ diff --git a/NEWS b/NEWS index 9070fd9..b663a0a 100644 --- a/NEWS +++ b/NEWS @@ -1,13 +1,36 @@ -3.26.1 27-Oct-2017 +3.27.0 08-Dec-2017 + - demo: pep8 fixes (Christoph Reiter) + - Fix ctypes.PyDLL construction under Windows (Christoph Reiter) (#622084) + - configure.ac: Error out in case autoconf-archive isn't installed (Christoph Reiter) (#784428) + - Move pygi-convert.sh into tools (Christoph Reiter) + - README: Convert to reST (Christoph Reiter) + - demo: Move demo into examples and dist it (Christoph Reiter) (#735918) + - demo: Add new Gtk.FlowBox example (Gian Mario Tagliaretti) (#735918) + - demo: Use HeaderBar for main app window (Simon Feltman) (#735918) + - demo: PyFlakes and PEP8 fixes (Simon Feltman) (#735918) + - demo: Rename gtk-demo.py to demo.py (Simon Feltman) (#735918) + - demo: Rename demos/gtk-demo to simply demo (Simon Feltman) (#735918) + - Remove AUTHORS file (Christoph Reiter) + - Remove pre-commit.hook (Christoph Reiter) + - setup.py: Port to distutils/setuptools (Christoph Reiter) (#789211) + - Install a default SIGINT handler for functions which start an event loop (Christoph Reiter) (#622084) + - Make Python OS signal handlers run when an event loop is idling (Christoph Reiter) (#622084) + - Drop Python 3.3 support (Christoph Reiter) (#790787) + - Drop set_value usage in Gtk.List/TreeStore.set override (Sander Sweers) (#790346) - pygobject-object: Fix Python GC collecting a ref cycle too early (Christoph Reiter) (#731501) - Fix potential uninitialized memory access during GC (Daniel Colascione) (#786872) - test: revert parts of the previous test as it's broken on 32 bit builds (Christoph Reiter) (#786948) - flags: Add testcase for bug 786948 (Christoph Reiter) (#786948) - fix potential overflow when marshalling flags from py interface (Philippe Renon) (#786948) - to_py_array: Properly handle enum array items (Christoph Reiter) (#788890) + - pygobject.doap: Add myself as maintainer (Christoph Reiter) - closure: Fix unaligned and out-of-bounds access (James Clarke) (#788894) - build: Fix not installing .egg-info file (Christoph Reiter) (#777719) - - configure.ac: version bump to 3.26.1 (Christoph Reiter) + - Drop pygobject-3.0-uninstalled.pc file (Christoph Reiter) + - tests: Windows fix (Christoph Reiter) + - tests: some more C locale fixes (Christoph Reiter) + - tests: Make the test suite pass with the C locale (Christoph Reiter) + - configure.ac: post-release version bump to 3.27.0 (Christoph Reiter) 3.26.0 12-Sep-2017 - configure.ac: pre-release version bump to 3.26.0 (Christoph Reiter) diff --git a/PKG-INFO b/PKG-INFO index ff24db0..279cc40 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -1,15 +1,15 @@ Metadata-Version: 1.0 -Name: PyGObject -Version: 3.26.1 -Summary: Python bindings for GObject -Home-page: http://www.pygtk.org/ +Name: pygobject +Version: 3.27.0 +Summary: Python bindings for GObject Introspection +Home-page: https://pygobject.readthedocs.io Author: James Henstridge Author-email: james@daa.com.au Maintainer: Simon Feltman Maintainer-email: sfeltman@src.gnome.org License: GNU LGPL -Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/3.26/pygobject-3.26.1.tar.gz -Description: Python bindings for GLib and GObject +Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/3.27/pygobject-3.27.0.tar.gz +Description: Python bindings for GObject Introspection Platform: POSIX, Windows Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Linux diff --git a/PKG-INFO.in b/PKG-INFO.in index 651dabe..bebaf08 100644 --- a/PKG-INFO.in +++ b/PKG-INFO.in @@ -1,15 +1,15 @@ Metadata-Version: 1.0 -Name: PyGObject +Name: pygobject Version: @VERSION@ -Summary: Python bindings for GObject -Home-page: http://www.pygtk.org/ +Summary: Python bindings for GObject Introspection +Home-page: https://pygobject.readthedocs.io Author: James Henstridge Author-email: james@daa.com.au Maintainer: Simon Feltman Maintainer-email: sfeltman@src.gnome.org License: GNU LGPL Download-url: ftp://ftp.gnome.org/pub/GNOME/sources/pygobject/@PYGOBJECT_MAJOR_VERSION@.@PYGOBJECT_MINOR_VERSION@/pygobject-@VERSION@.tar.gz -Description: Python bindings for GLib and GObject +Description: Python bindings for GObject Introspection Platform: POSIX, Windows Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Linux diff --git a/README b/README.rst similarity index 74% rename from README rename to README.rst index eb7ec8a..7eb7c8b 100644 --- a/README +++ b/README.rst @@ -1,15 +1,6 @@ +========= PyGObject -===== -Original authors: James Henstridge - Johan Dahlin - -Current maintainers: Tomeu Vizoso - Martin Pitt - Paolo Borelli - Ignacio Casal Quinteiro - Sebastian Pölsterl - Simon Feltman - +========= This archive contains bindings for the GLib, and GObject, to be used in Python. It is a fairly complete set of bindings, @@ -19,7 +10,8 @@ of the simpler programs you could write). If you have any enhancements or bug reports, please file them in bugzilla at: - http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject + + http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject If you have a patch, file the bug first and then use the "create new attachment" link on the bug's info page. My preferred format for @@ -27,11 +19,13 @@ patches is unified diff format (ie. diff -u). Please don't send me diffs which don't have any context, as these make it very difficult to see what the patch does. + New Versions ============ New versions of this package can be found at: - http://ftp.gnome.org/pub/GNOME/sources/pygobject/ + + http://ftp.gnome.org/pub/GNOME/sources/pygobject/ Mailing list @@ -42,14 +36,17 @@ You can subscribe to it through the web interface: https://mail.gnome.org/mailman/listinfo/python-hackers-list/ + Requirements ============ + * C compiler (GCC and MSVC supported) * Python 2.7 or higher * Glib/Gio 2.38.0 or higher * gobject-introspection 1.46.0 or higher * libffi (optional) + Copyright Information ===================== @@ -57,11 +54,12 @@ This software is covered by the GNU Lesser General Public Licence (version 2.1, or if you choose, a later version). Basically just don't say you wrote bits you didn't. + Compilation =========== PyGObject uses the standard autotools for the build infrastructure. To -build, it should be as simple as running: +build, it should be as simple as running:: $ ./configure --prefix= $ make @@ -71,11 +69,11 @@ By default, configure searches for a few well-known Python interpreter names, such as "python3", "python2", "python2.7", or "python". If your Python interpreter isn't in the path, or is not called "python", you can configure pygobject to build against that with --with-python= or -setting the PYTHON environment variable: +setting the PYTHON environment variable:: - $ ./configure --with-python=python3 - $ PYTHON=python3.2 ./configure - $ ./configure --with-python=~/my-patched-python/python + $ ./configure --with-python=python3 + $ PYTHON=python3.2 ./configure + $ ./configure --with-python=~/my-patched-python/python If configure can't find GTK+, you may need to set the PKG_CONFIG_PATH environment variable to help it find the libraries. @@ -88,6 +86,7 @@ is installed you'll need to set the PYTHONPATH variable to the $prefix/lib/pythonX.Y/site-packages directory created by the PyGObject installation. + Tests ===== @@ -102,7 +101,26 @@ Getting Help If you have questions about programming with PyGObject, you might want to check the documentation on - https://live.gnome.org/PyGObject/ + https://live.gnome.org/PyGObject/ If that does not help, send a message to the mailing list (information on subscribing is above), or join #python on irc.gnome.org. + + +Authors +======= + +Original authors: + * James Henstridge + * Johan Dahlin + +Current maintainers: + * Tomeu Vizoso + * Martin Pitt + * Paolo Borelli + * Ignacio Casal Quinteiro + * Sebastian Pölsterl + * Simon Feltman + * Christoph Reiter + +See the NEWS file and the git history for a list of all contributors. diff --git a/configure b/configure index 752e07a..50ea09e 100755 --- a/configure +++ b/configure @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.69 for pygobject 3.26.1. +# Generated by GNU Autoconf 2.69 for pygobject 3.27.0. # # Report bugs to . # @@ -591,8 +591,8 @@ MAKEFLAGS= # Identity of this package. PACKAGE_NAME='pygobject' PACKAGE_TARNAME='pygobject' -PACKAGE_VERSION='3.26.1' -PACKAGE_STRING='pygobject 3.26.1' +PACKAGE_VERSION='3.27.0' +PACKAGE_STRING='pygobject 3.27.0' PACKAGE_BUGREPORT='http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject' PACKAGE_URL='https://wiki.gnome.org/Projects/PyGObject/' @@ -1419,7 +1419,7 @@ if test "$ac_init_help" = "long"; then # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -\`configure' configures pygobject 3.26.1 to adapt to many kinds of systems. +\`configure' configures pygobject 3.27.0 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1490,7 +1490,7 @@ fi if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of pygobject 3.26.1:";; + short | recursive ) echo "Configuration of pygobject 3.27.0:";; esac cat <<\_ACEOF @@ -1635,7 +1635,7 @@ fi test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -pygobject configure 3.26.1 +pygobject configure 3.27.0 generated by GNU Autoconf 2.69 Copyright (C) 2012 Free Software Foundation, Inc. @@ -1913,7 +1913,7 @@ cat >config.log <<_ACEOF This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by pygobject $as_me 3.26.1, which was +It was created by pygobject $as_me 3.27.0, which was generated by GNU Autoconf 2.69. Invocation command line was $ $0 $@ @@ -2265,6 +2265,9 @@ ac_compiler_gnu=$ac_cv_c_compiler_gnu + + + # $is_release = ($minor_version is even) minor_version=`echo "$PACKAGE_VERSION" | sed 's/[^.][^.]*.\([^.][^.]*\).*/\1/'` if test "$(( $minor_version % 2 ))" -ne 0; then : @@ -2283,14 +2286,14 @@ $as_echo "#define PYGOBJECT_MAJOR_VERSION 3" >>confdefs.h PYGOBJECT_MAJOR_VERSION=3 -$as_echo "#define PYGOBJECT_MINOR_VERSION 26" >>confdefs.h +$as_echo "#define PYGOBJECT_MINOR_VERSION 27" >>confdefs.h -PYGOBJECT_MINOR_VERSION=26 +PYGOBJECT_MINOR_VERSION=27 -$as_echo "#define PYGOBJECT_MICRO_VERSION 1" >>confdefs.h +$as_echo "#define PYGOBJECT_MICRO_VERSION 0" >>confdefs.h -PYGOBJECT_MICRO_VERSION=1 +PYGOBJECT_MICRO_VERSION=0 ac_config_headers="$ac_config_headers config.h" @@ -2810,7 +2813,7 @@ fi # Define the identity of the package. PACKAGE='pygobject' - VERSION='3.26.1' + VERSION='3.27.0' cat >>confdefs.h <<_ACEOF @@ -13365,13 +13368,13 @@ fi # if building for python 3 make sure we have the minimum version supported if test $build_py3k = true ; then - { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $PYTHON >= 3.3" >&5 -$as_echo_n "checking for $PYTHON >= 3.3... " >&6; } + { $as_echo "$as_me:${as_lineno-$LINENO}: checking for $PYTHON >= 3.4" >&5 +$as_echo_n "checking for $PYTHON >= 3.4... " >&6; } prog="import sys # split strings by '.' and convert to numeric. Append some zeros # because we need at least 4 digits for the hex conversion. # map returns an iterator in Python 3.0 and a list in 2.x -minver = list(map(int, '3.3'.split('.'))) + [0, 0, 0] +minver = list(map(int, '3.4'.split('.'))) + [0, 0, 0] minverhex = 0 # xrange is not present in Python 3.0 and range returns an iterator for i in list(range(0, 4)): minverhex = (minverhex << 8) + minver[i] @@ -13892,7 +13895,7 @@ else #include int -main (void) +main () { unsigned int major, minor, micro; @@ -15008,7 +15011,7 @@ done -for flag in -Wall -Wextra -Wundef -Wnested-externs -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wdeclaration-after-statement -Wformat=2 -Wold-style-definition -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wredundant-decls -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wimplicit-function-declaration -Wreturn-type -Wswitch-enum -Wswitch-default -Wduplicated-cond -Wduplicated-branches -Wlogical-op -Wrestrict -Wnull-dereference -Wjump-misses-init -Wdouble-promotion ; do +for flag in -Wall -Wextra -Wundef -Wnested-externs -Wwrite-strings -Wpointer-arith -Wmissing-declarations -Wmissing-prototypes -Wstrict-prototypes -Wredundant-decls -Wno-unused-parameter -Wno-missing-field-initializers -Wdeclaration-after-statement -Wformat=2 -Wold-style-definition -Wcast-align -Wformat-nonliteral -Wformat-security -Wsign-compare -Wstrict-aliasing -Wshadow -Winline -Wpacked -Wmissing-format-attribute -Wmissing-noreturn -Winit-self -Wredundant-decls -Wmissing-include-dirs -Wunused-but-set-variable -Warray-bounds -Wimplicit-function-declaration -Wreturn-type -Wswitch-enum -Wswitch-default ; do as_CACHEVAR=`$as_echo "ax_cv_check_cflags_$ax_compiler_flags_test_$flag" | $as_tr_sh` { $as_echo "$as_me:${as_lineno-$LINENO}: checking whether C compiler accepts $flag" >&5 $as_echo_n "checking whether C compiler accepts $flag... " >&6; } @@ -16330,12 +16333,9 @@ CODE_COVERAGE_LCOV_RMOPTS ?= $(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\ $(if $(CODE_COVERAGE_BRANCH_COVERAGE),\ --rc genhtml_branch_coverage=$(CODE_COVERAGE_BRANCH_COVERAGE)) -CODE_COVERAGE_GENHTML_OPTIONS ?= $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +CODE_COVERAGE_GENHTML_OPTIONS ?= $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULTS) CODE_COVERAGE_IGNORE_PATTERN ?= -GITIGNOREFILES ?= -GITIGNOREFILES += $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_DIRECTORY) - code_coverage_v_lcov_cap = $(code_coverage_v_lcov_cap_$(V)) code_coverage_v_lcov_cap_ = $(code_coverage_v_lcov_cap_$(AM_DEFAULT_VERBOSITY)) code_coverage_v_lcov_cap_0 = @echo " LCOV --capture"\ @@ -16365,6 +16365,9 @@ code-coverage-capture-hook: '"$CODE_COVERAGE_RULES_CLEAN"' +GITIGNOREFILES ?= +GITIGNOREFILES += $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_DIRECTORY) + A''M_DISTCHECK_CONFIGURE_FLAGS ?= A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-code-coverage @@ -16375,7 +16378,7 @@ A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-code-coverage -ac_config_files="$ac_config_files Makefile pygobject-3.0.pc pygobject-3.0-uninstalled.pc gi/Makefile gi/repository/Makefile gi/overrides/Makefile examples/Makefile tests/Makefile pygtkcompat/Makefile PKG-INFO" +ac_config_files="$ac_config_files Makefile pygobject-3.0.pc gi/Makefile gi/repository/Makefile gi/overrides/Makefile examples/Makefile tests/Makefile pygtkcompat/Makefile PKG-INFO" cat >confcache <<\_ACEOF # This file is a shell script that caches the results of configure @@ -16931,7 +16934,7 @@ cat >>$CONFIG_STATUS <<\_ACEOF || ac_write_fail=1 # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by pygobject $as_me 3.26.1, which was +This file was extended by pygobject $as_me 3.27.0, which was generated by GNU Autoconf 2.69. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -16998,7 +17001,7 @@ _ACEOF cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config="`$as_echo "$ac_configure_args" | sed 's/^ //; s/[\\""\`\$]/\\\\&/g'`" ac_cs_version="\\ -pygobject config.status 3.26.1 +pygobject config.status 3.27.0 configured by $0, generated by GNU Autoconf 2.69, with options \\"\$ac_cs_config\\" @@ -17417,7 +17420,6 @@ do "libtool") CONFIG_COMMANDS="$CONFIG_COMMANDS libtool" ;; "Makefile") CONFIG_FILES="$CONFIG_FILES Makefile" ;; "pygobject-3.0.pc") CONFIG_FILES="$CONFIG_FILES pygobject-3.0.pc" ;; - "pygobject-3.0-uninstalled.pc") CONFIG_FILES="$CONFIG_FILES pygobject-3.0-uninstalled.pc" ;; "gi/Makefile") CONFIG_FILES="$CONFIG_FILES gi/Makefile" ;; "gi/repository/Makefile") CONFIG_FILES="$CONFIG_FILES gi/repository/Makefile" ;; "gi/overrides/Makefile") CONFIG_FILES="$CONFIG_FILES gi/overrides/Makefile" ;; diff --git a/configure.ac b/configure.ac index 3ad8da1..cdecad2 100644 --- a/configure.ac +++ b/configure.ac @@ -13,12 +13,12 @@ AC_PREREQ([2.68]) # $ ./configure --with-python=~/my-patched-python/python m4_define(python_min_ver, 2.7) -m4_define(python3_min_ver, 3.3) +m4_define(python3_min_ver, 3.4) dnl the pygobject version number m4_define(pygobject_major_version, 3) -m4_define(pygobject_minor_version, 26) -m4_define(pygobject_micro_version, 1) +m4_define(pygobject_minor_version, 27) +m4_define(pygobject_micro_version, 0) m4_define(pygobject_version, pygobject_major_version.pygobject_minor_version.pygobject_micro_version) dnl versions of packages we require ... @@ -26,10 +26,14 @@ m4_define(introspection_required_version, 1.46.0) m4_define(pycairo_required_version, 1.11.1) m4_define(glib_required_version, 2.38.0) m4_define(gio_required_version, 2.38.0) +m4_define(libffi_required_version, 3.0) AC_INIT([pygobject],[pygobject_version], [http://bugzilla.gnome.org/enter_bug.cgi?product=pygobject], [pygobject],[https://wiki.gnome.org/Projects/PyGObject/]) + +m4_ifndef([AX_IS_RELEASE], [AC_MSG_ERROR(['autoconf-archive' missing])]) + AX_IS_RELEASE([minor-version]) AC_CONFIG_MACRO_DIR([m4]) AC_CONFIG_SRCDIR([gi/gimodule.c]) @@ -131,7 +135,7 @@ PYTHON_VALGRIND_SUPP=`$PYTHON -c "import sys; sys.stdout.write('python' + sys.ve AC_SUBST([PYTHON_VALGRIND_SUPP]) dnl libffi -PKG_CHECK_MODULES(FFI, libffi >= 3.0) +PKG_CHECK_MODULES(FFI, libffi >= libffi_required_version) LIBFFI_PC=libffi AC_SUBST(FFI_CFLAGS) @@ -194,7 +198,6 @@ AX_CODE_COVERAGE() AC_CONFIG_FILES( Makefile pygobject-3.0.pc - pygobject-3.0-uninstalled.pc gi/Makefile gi/repository/Makefile gi/overrides/Makefile diff --git a/examples/Makefile.am b/examples/Makefile.am index af9f3d7..c023cc0 100644 --- a/examples/Makefile.am +++ b/examples/Makefile.am @@ -1 +1,6 @@ -EXTRA_DIST = properties.py signal.py option.py cairo-demo.py +EXTRA_DIST = \ + properties.py \ + signal.py \ + option.py \ + cairo-demo.py \ + demo diff --git a/examples/Makefile.in b/examples/Makefile.in index 7b22613..54843be 100644 --- a/examples/Makefile.in +++ b/examples/Makefile.in @@ -311,7 +311,13 @@ target_alias = @target_alias@ top_build_prefix = @top_build_prefix@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ -EXTRA_DIST = properties.py signal.py option.py cairo-demo.py +EXTRA_DIST = \ + properties.py \ + signal.py \ + option.py \ + cairo-demo.py \ + demo + all: all-am .SUFFIXES: diff --git a/examples/demo/demo.py b/examples/demo/demo.py new file mode 100755 index 0000000..d67935d --- /dev/null +++ b/examples/demo/demo.py @@ -0,0 +1,356 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + + +import codecs +import os +import sys +import textwrap + +from gi.repository import GLib, GObject, Pango, GdkPixbuf, Gtk, Gio + +try: + from gi.repository import GtkSource + GtkSource # PyFlakes +except ImportError: + GtkSource = None + + +DEMOROOTDIR = os.path.abspath(os.path.dirname(__file__)) +DEMOCODEDIR = os.path.join(DEMOROOTDIR, 'demos') +sys.path.insert(0, DEMOROOTDIR) + + +class Demo(GObject.GObject): + __gtype_name__ = 'GtkDemo' + + def __init__(self, title, module, filename): + super(Demo, self).__init__() + + self.title = title + self.module = module + self.filename = filename + + @classmethod + def new_from_file(cls, path): + relpath = os.path.relpath(path, DEMOROOTDIR) + packagename = os.path.dirname(relpath).replace(os.sep, '.') + modulename = os.path.basename(relpath)[0:-3] + + package = __import__(packagename, globals(), locals(), [modulename], 0) + module = getattr(package, modulename) + + try: + return cls(module.title, module, path) + except AttributeError as e: + raise AttributeError('(%s): %s' % (path, e.message)) + + +class DemoTreeStore(Gtk.TreeStore): + __gtype_name__ = 'GtkDemoTreeStore' + + def __init__(self, *args): + super(DemoTreeStore, self).__init__(str, Demo, Pango.Style) + + self._parent_nodes = {} + + for filename in self._list_dir(DEMOCODEDIR): + fullpath = os.path.join(DEMOCODEDIR, filename) + initfile = os.path.join(os.path.dirname(fullpath), '__init__.py') + + if fullpath != initfile and os.path.isfile(initfile) and fullpath.endswith('.py'): + parentname = os.path.dirname(os.path.relpath(fullpath, DEMOCODEDIR)) + + if parentname: + parent = self._get_parent_node(parentname) + else: + parent = None + + demo = Demo.new_from_file(fullpath) + self.append(parent, (demo.title, demo, Pango.Style.NORMAL)) + + def _list_dir(self, path): + demo_file_list = [] + + for filename in os.listdir(path): + fullpath = os.path.join(path, filename) + + if os.path.isdir(fullpath): + demo_file_list.extend(self._list_dir(fullpath)) + elif os.path.isfile(fullpath): + demo_file_list.append(fullpath) + + return sorted(demo_file_list, key=str.lower) + + def _get_parent_node(self, name): + if name not in self._parent_nodes.keys(): + node = self.append(None, (name, None, Pango.Style.NORMAL)) + self._parent_nodes[name] = node + + return self._parent_nodes[name] + + +class GtkDemoApp(Gtk.Application): + __gtype_name__ = 'GtkDemoWindow' + + def __init__(self): + super(GtkDemoApp, self).__init__(application_id='org.gnome.pygobject.gtkdemo') + + # Use a GResource to hold the CSS files. Resource bundles are created by + # the glib-compile-resources program shipped with Glib which takes an xml + # file that describes the bundle, and a set of files that the xml + # references. These are combined into a binary resource bundle. + base_path = os.path.abspath(os.path.dirname(__file__)) + resource_path = os.path.join(base_path, 'demos/data/demo.gresource') + resource = Gio.Resource.load(resource_path) + + # FIXME: method register() should be without the underscore + # FIXME: see https://bugzilla.gnome.org/show_bug.cgi?id=684319 + # Once the resource has been globally registered it can be used + # throughout the application. + resource._register() + + def on_activate(self, app): + self.window = Gtk.ApplicationWindow.new(self) + self.window.set_title('PyGObject GTK+ Code Demos') + self.window.set_default_size(600, 400) + self.setup_default_icon() + + self.header_bar = Gtk.HeaderBar(show_close_button=True, + subtitle='Foobar') + self.window.set_titlebar(self.header_bar) + + stack = Gtk.Stack(transition_type=Gtk.StackTransitionType.SLIDE_LEFT_RIGHT, + homogeneous=True) + switcher = Gtk.StackSwitcher(stack=stack, halign=Gtk.Align.CENTER) + + self.header_bar.set_custom_title(switcher) + + hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, + homogeneous=False, + spacing=0) + self.window.add(hbox) + + tree = self.create_tree() + hbox.pack_start(child=tree, expand=False, fill=False, padding=0) + hbox.pack_start(child=stack, expand=True, fill=True, padding=0) + + text_widget, info_buffer = self.create_text_view() + stack.add_titled(text_widget, name='info', title='Info') + + self.info_buffer = info_buffer + self.info_buffer.create_tag('title', font='Sans 18') + + text_widget, self.source_buffer = self.create_source_view() + stack.add_titled(text_widget, name='source', title='Source') + + self.window.show_all() + + self.selection_cb(self.tree_view.get_selection(), + self.tree_view.get_model()) + + def find_file(self, base=''): + dir = os.path.join(DEMOCODEDIR, 'data') + logo_file = os.path.join(dir, 'gtk-logo-rgb.gif') + base_file = os.path.join(dir, base) + + if (GLib.file_test(logo_file, GLib.FileTest.EXISTS) and + GLib.file_test(base_file, GLib.FileTest.EXISTS)): + return base_file + else: + filename = os.path.join(DEMOCODEDIR, base) + + if GLib.file_test(filename, GLib.FileTest.EXISTS): + return filename + + # can't find the file + raise IOError('Cannot find demo data file "%s"' % base) + + def setup_default_icon(self): + filename = self.find_file('gtk-logo-rgb.gif') + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + transparent = pixbuf.add_alpha(True, 0xff, 0xff, 0xff) + list = [] + list.append(transparent) + Gtk.Window.set_default_icon_list(list) + + def selection_cb(self, selection, model): + sel = selection.get_selected() + if sel == (): + return + + treeiter = sel[1] + title = model.get_value(treeiter, 0) + demo = model.get_value(treeiter, 1) + + if demo is None: + return + + # Split into paragraphs based on double newlines and use + # textwrap to strip out all other formatting whitespace + description = '' + for paragraph in demo.module.description.split('\n\n'): + description += '\n'.join(textwrap.wrap(paragraph, 99999)) + description += '\n\n' # Add paragraphs back in + + f = codecs.open(demo.filename, 'rU', 'utf-8') + code = f.read() + f.close() + + # output and style the title + (start, end) = self.info_buffer.get_bounds() + self.info_buffer.delete(start, end) + (start, end) = self.source_buffer.get_bounds() + self.source_buffer.delete(start, end) + + start = self.info_buffer.get_iter_at_offset(0) + end = start.copy() + self.info_buffer.insert(end, title) + start = end.copy() + start.backward_chars(len(title)) + self.info_buffer.apply_tag_by_name('title', start, end) + self.info_buffer.insert(end, '\n') + + # output the description + self.info_buffer.insert(end, description) + + # output the code + start = self.source_buffer.get_iter_at_offset(0) + end = start.copy() + self.source_buffer.insert(end, code) + + def row_activated_cb(self, view, path, col, store): + iter = store.get_iter(path) + demo = store.get_value(iter, 1) + + if demo is not None: + store.set_value(iter, 2, Pango.Style.ITALIC) + try: + demo.module.main(self) + finally: + store.set_value(iter, 2, Pango.Style.NORMAL) + + def create_tree(self): + tree_store = DemoTreeStore() + tree_view = Gtk.TreeView() + self.tree_view = tree_view + tree_view.set_model(tree_store) + selection = tree_view.get_selection() + selection.set_mode(Gtk.SelectionMode.BROWSE) + tree_view.set_size_request(200, -1) + + cell = Gtk.CellRendererText() + column = Gtk.TreeViewColumn(title='Widget (double click for demo)', + cell_renderer=cell, + text=0, + style=2) + + first_iter = tree_store.get_iter_first() + if first_iter is not None: + selection.select_iter(first_iter) + + selection.connect('changed', self.selection_cb, tree_store) + tree_view.connect('row_activated', self.row_activated_cb, tree_store) + + tree_view.append_column(column) + + tree_view.expand_all() + tree_view.set_headers_visible(False) + scrolled_window = Gtk.ScrolledWindow(hadjustment=None, + vadjustment=None) + scrolled_window.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + + scrolled_window.add(tree_view) + + label = Gtk.Label(label='Widget (double click for demo)') + + box = Gtk.Notebook() + box.append_page(scrolled_window, label) + + tree_view.grab_focus() + + return box + + def create_scrolled_window(self): + scrolled_window = Gtk.ScrolledWindow(hadjustment=None, + vadjustment=None) + scrolled_window.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + scrolled_window.set_shadow_type(Gtk.ShadowType.IN) + return scrolled_window + + def create_text_view(self): + text_view = Gtk.TextView() + buffer = Gtk.TextBuffer() + + text_view.set_buffer(buffer) + text_view.set_editable(False) + text_view.set_cursor_visible(False) + + scrolled_window = self.create_scrolled_window() + scrolled_window.add(text_view) + + text_view.set_wrap_mode(Gtk.WrapMode.WORD) + text_view.set_pixels_above_lines(2) + text_view.set_pixels_below_lines(2) + + return scrolled_window, buffer + + def create_source_view(self): + font_desc = Pango.FontDescription('monospace 11') + + if GtkSource: + lang_mgr = GtkSource.LanguageManager() + lang = lang_mgr.get_language('python') + + buffer = GtkSource.Buffer() + buffer.set_language(lang) + buffer.set_highlight_syntax(True) + + view = GtkSource.View() + view.set_buffer(buffer) + view.set_show_line_numbers(True) + + scrolled_window = self.create_scrolled_window() + scrolled_window.add(view) + + else: + scrolled_window, buffer = self.create_text_view() + view = scrolled_window.get_child() + + view.modify_font(font_desc) + view.set_wrap_mode(Gtk.WrapMode.NONE) + return scrolled_window, buffer + + def run(self, argv): + self.connect('activate', self.on_activate) + return super(GtkDemoApp, self).run(argv) + + +def main(argv): + """Entry point for demo manager""" + app = GtkDemoApp() + return app.run(argv) + + +if __name__ == '__main__': + SystemExit(main(sys.argv)) diff --git a/examples/demo/demos/Css/__init__.py b/examples/demo/demos/Css/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/demo/demos/Css/css_accordion.py b/examples/demo/demos/Css/css_accordion.py new file mode 100644 index 0000000..2b7cddc --- /dev/null +++ b/examples/demo/demos/Css/css_accordion.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2013 Gian Mario Tagliaretti +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "CSS Accordion" +description = """ +A simple accordion demo written using CSS transitions and multiple backgrounds. +""" + + +from gi.repository import Gtk, Gio + + +class CSSAccordionApp: + def __init__(self): + window = Gtk.Window() + window.set_title('CSS Accordion') + window.set_default_size(600, 300) + window.set_border_width(10) + window.connect('destroy', Gtk.main_quit) + + hbox = Gtk.Box(homogeneous=False, spacing=2, + orientation=Gtk.Orientation.HORIZONTAL) + hbox.set_halign(Gtk.Align.CENTER) + hbox.set_valign(Gtk.Align.CENTER) + window.add(hbox) + + for label in ('This', 'Is', 'A', 'CSS', 'Accordion', ':-)'): + hbox.add(Gtk.Button(label=label)) + + bytes = Gio.resources_lookup_data("/css_accordion/css_accordion.css", 0) + + provider = Gtk.CssProvider() + provider.load_from_data(bytes.get_data()) + + self.apply_css(window, provider) + + window.show_all() + + def apply_css(self, widget, provider): + Gtk.StyleContext.add_provider(widget.get_style_context(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + if isinstance(widget, Gtk.Container): + widget.forall(self.apply_css, provider) + + +def main(demoapp=None): + CSSAccordionApp() + Gtk.main() + + +if __name__ == '__main__': + import os + base_path = os.path.abspath(os.path.dirname(__file__)) + resource_path = os.path.join(base_path, '../data/demo.gresource') + resource = Gio.Resource.load(resource_path) + + # FIXME: method register() should be without the underscore + # FIXME: see https://bugzilla.gnome.org/show_bug.cgi?id=684319 + resource._register() + main() diff --git a/examples/demo/demos/Css/css_basics.py b/examples/demo/demos/Css/css_basics.py new file mode 100644 index 0000000..18c3d12 --- /dev/null +++ b/examples/demo/demos/Css/css_basics.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2013 Gian Mario Tagliaretti +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "CSS Basics" +description = """ +Gtk themes are written using CSS. Every widget is build of multiple items +that you can style very similarly to a regular website. +""" + +import os +from gi.repository import Gtk, Gdk, Pango, Gio, GLib + + +class CSSBasicsApp: + def __init__(self, demoapp): + self.demoapp = demoapp + #: Store the last successful parsing of the css so we can revert + #: this in case of an error. + self.last_good_text = '' + #: Set when we receive a parsing-error callback. This is needed + #: to handle logic after a parsing-error callback which does not raise + #: an exception with provider.load_from_data() + self.last_error_code = 0 + + self.window = Gtk.Window() + self.window.set_title('CSS Basics') + self.window.set_default_size(400, 300) + self.window.set_border_width(10) + self.window.connect('destroy', lambda w: Gtk.main_quit()) + + self.infobar = Gtk.InfoBar() + self.infolabel = Gtk.Label() + self.infobar.get_content_area().pack_start(self.infolabel, False, False, 0) + self.infobar.set_message_type(Gtk.MessageType.WARNING) + + scrolled = Gtk.ScrolledWindow() + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.pack_start(scrolled, expand=True, fill=True, padding=0) + box.pack_start(self.infobar, expand=False, fill=True, padding=0) + self.window.add(box) + + provider = Gtk.CssProvider() + + buffer = Gtk.TextBuffer() + buffer.create_tag(tag_name="warning", underline=Pango.Underline.SINGLE) + buffer.create_tag(tag_name="error", underline=Pango.Underline.ERROR) + buffer.connect("changed", self.css_text_changed, provider) + + provider.connect("parsing-error", self.show_parsing_error, buffer) + + textview = Gtk.TextView() + textview.set_buffer(buffer) + scrolled.add(textview) + + bytes = Gio.resources_lookup_data("/css_basics/css_basics.css", 0) + buffer.set_text(bytes.get_data().decode('utf-8')) + + self.apply_css(self.window, provider) + self.window.show_all() + self.infobar.hide() + + def apply_css(self, widget, provider): + Gtk.StyleContext.add_provider(widget.get_style_context(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + if isinstance(widget, Gtk.Container): + widget.forall(self.apply_css, provider) + + def show_parsing_error(self, provider, section, error, buffer): + start = buffer.get_iter_at_line_index(section.get_start_line(), + section.get_start_position()) + + end = buffer.get_iter_at_line_index(section.get_end_line(), + section.get_end_position()) + + if error.code == Gtk.CssProviderError.DEPRECATED: + tag_name = "warning" + else: + tag_name = "error" + self.last_error_code = error.code + + self.infolabel.set_text(error.message) + self.infobar.show_all() + + buffer.apply_tag_by_name(tag_name, start, end) + + def css_text_changed(self, buffer, provider): + start = buffer.get_start_iter() + end = buffer.get_end_iter() + buffer.remove_all_tags(start, end) + + text = buffer.get_text(start, end, False).encode('utf-8') + + # Ignore CSS errors as they are shown by highlighting + try: + provider.load_from_data(text) + except GLib.GError as e: + if e.domain != 'gtk-css-provider-error-quark': + raise e + + # If the parsing-error callback is ever run (even in the case of warnings) + # load the last good css text that ran without any warnings. Otherwise + # we may have a discrepancy in "last_good_text" vs the current buffer + # causing section.get_start_position() to give back an invalid position + # for the editor buffer. + if self.last_error_code: + provider.load_from_data(self.last_good_text) + self.last_error_code = 0 + else: + self.last_good_text = text + self.infobar.hide() + + Gtk.StyleContext.reset_widgets(Gdk.Screen.get_default()) + + +def main(demoapp=None): + CSSBasicsApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + base_path = os.path.abspath(os.path.dirname(__file__)) + resource_path = os.path.join(base_path, '../data/demo.gresource') + resource = Gio.Resource.load(resource_path) + + # FIXME: method register() should be without the underscore + # FIXME: see https://bugzilla.gnome.org/show_bug.cgi?id=684319 + resource._register() + main() diff --git a/examples/demo/demos/Css/css_multiplebgs.py b/examples/demo/demos/Css/css_multiplebgs.py new file mode 100644 index 0000000..9e1b011 --- /dev/null +++ b/examples/demo/demos/Css/css_multiplebgs.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2013 Gian Mario Tagliaretti +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "CSS Theming/Multiple Backgrounds" +description = """ +Gtk themes are written using CSS. Every widget is build of multiple items +that you can style very similarly to a regular website. +""" + +from gi.repository import Gtk, Gdk, Pango, Gio, GLib + + +class CSSMultiplebgsApp: + def __init__(self, demoapp): + self.demoapp = demoapp + #: Store the last successful parsing of the css so we can revert + #: this in case of an error. + self.last_good_text = '' + #: Set when we receive a parsing-error callback. This is needed + #: to handle logic after a parsing-error callback which does not raise + #: an exception with provider.load_from_data() + self.last_error_code = 0 + + self.window = Gtk.Window() + self.window.set_title('CSS Multiplebgs') + self.window.set_default_size(400, 300) + self.window.set_border_width(10) + self.window.connect('destroy', lambda w: Gtk.main_quit()) + + overlay = Gtk.Overlay() + overlay.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + Gdk.EventMask.POINTER_MOTION_MASK) + + self.infobar = Gtk.InfoBar() + self.infolabel = Gtk.Label() + self.infobar.get_content_area().pack_start(self.infolabel, False, False, 0) + self.infobar.set_message_type(Gtk.MessageType.WARNING) + + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + box.pack_start(overlay, expand=True, fill=True, padding=0) + box.pack_start(self.infobar, expand=False, fill=True, padding=0) + self.window.add(box) + + canvas = Gtk.DrawingArea() + canvas.set_name("canvas") + canvas.connect("draw", self.drawing_area_draw) + overlay.add(canvas) + + button = Gtk.Button() + button.add_events(Gdk.EventMask.ENTER_NOTIFY_MASK | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + Gdk.EventMask.POINTER_MOTION_MASK) + button.set_name("bricks-button") + button.set_halign(Gtk.Align.CENTER) + button.set_valign(Gtk.Align.CENTER) + button.set_size_request(250, 84) + overlay.add_overlay(button) + + paned = Gtk.Paned(orientation=Gtk.Orientation.VERTICAL) + overlay.add_overlay(paned) + + # We need a filler so we get a handle + box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + paned.add(box) + + buffer = Gtk.TextBuffer() + buffer.create_tag(tag_name="warning", underline=Pango.Underline.SINGLE) + buffer.create_tag(tag_name="error", underline=Pango.Underline.ERROR) + + provider = Gtk.CssProvider() + + buffer.connect("changed", self.css_text_changed, provider) + provider.connect("parsing-error", self.show_parsing_error, buffer) + + textview = Gtk.TextView() + textview.set_buffer(buffer) + + scrolled = Gtk.ScrolledWindow() + scrolled.add(textview) + paned.add(scrolled) + + bytes = Gio.resources_lookup_data("/css_multiplebgs/css_multiplebgs.css", 0) + buffer.set_text(bytes.get_data().decode('utf-8')) + + self.apply_css(self.window, provider) + self.window.show_all() + self.infobar.hide() + + def drawing_area_draw(self, widget, cairo_t): + context = widget.get_style_context() + Gtk.render_background(context, cairo_t, 0, 0, + widget.get_allocated_width(), + widget.get_allocated_height()) + + Gtk.render_frame(context, cairo_t, 0, 0, + widget.get_allocated_width(), + widget.get_allocated_height()) + + def apply_css(self, widget, provider): + Gtk.StyleContext.add_provider(widget.get_style_context(), + provider, + Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + + if isinstance(widget, Gtk.Container): + widget.forall(self.apply_css, provider) + + def show_parsing_error(self, provider, section, error, buffer): + start = buffer.get_iter_at_line_index(section.get_start_line(), + section.get_start_position()) + + end = buffer.get_iter_at_line_index(section.get_end_line(), + section.get_end_position()) + + if error.code == Gtk.CssProviderError.DEPRECATED: + tag_name = "warning" + else: + tag_name = "error" + self.last_error_code = error.code + + self.infolabel.set_text(error.message) + self.infobar.show_all() + + buffer.apply_tag_by_name(tag_name, start, end) + + def css_text_changed(self, buffer, provider): + start = buffer.get_start_iter() + end = buffer.get_end_iter() + buffer.remove_all_tags(start, end) + + text = buffer.get_text(start, end, False).encode('utf-8') + + # Ignore CSS errors as they are shown by highlighting + try: + provider.load_from_data(text) + except GLib.GError as e: + if e.domain != 'gtk-css-provider-error-quark': + raise e + + # If the parsing-error callback is ever run (even in the case of warnings) + # load the last good css text that ran without any warnings. Otherwise + # we may have a discrepancy in "last_good_text" vs the current buffer + # causing section.get_start_position() to give back an invalid position + # for the editor buffer. + if self.last_error_code: + provider.load_from_data(self.last_good_text) + self.last_error_code = 0 + else: + self.last_good_text = text + self.infobar.hide() + + Gtk.StyleContext.reset_widgets(Gdk.Screen.get_default()) + + +def main(demoapp=None): + CSSMultiplebgsApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + import os + base_path = os.path.abspath(os.path.dirname(__file__)) + resource_path = os.path.join(base_path, '../data/demo.gresource') + resource = Gio.Resource.load(resource_path) + + # FIXME: method register() should be without the underscore + # FIXME: see https://bugzilla.gnome.org/show_bug.cgi?id=684319 + resource._register() + main() diff --git a/examples/demo/demos/Entry/__init__.py b/examples/demo/demos/Entry/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/demo/demos/Entry/entry_buffer.py b/examples/demo/demos/Entry/entry_buffer.py new file mode 100644 index 0000000..f0c04a4 --- /dev/null +++ b/examples/demo/demos/Entry/entry_buffer.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Entry Buffer" +description = """ +Gtk.EntryBuffer provides the text content in a Gtk.Entry. +""" + + +from gi.repository import Gtk + + +class EntryBufferApp: + def __init__(self): + self.window = Gtk.Dialog(title='Gtk.EntryBuffer') + self.window.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.NONE) + self.window.connect('response', self.destroy) + self.window.connect('destroy', lambda x: Gtk.main_quit()) + self.window.set_resizable(False) + + vbox = Gtk.VBox(homogeneous=False, spacing=0) + self.window.get_content_area().pack_start(vbox, True, True, 0) + vbox.set_border_width(5) + + label = Gtk.Label() + label.set_markup('Entries share a buffer. Typing in one is reflected in the other.') + vbox.pack_start(label, False, False, 0) + + # create a buffer + buffer = Gtk.EntryBuffer() + + # create our first entry + entry = Gtk.Entry(buffer=buffer) + vbox.pack_start(entry, False, False, 0) + + # create the second entry + entry = Gtk.Entry(buffer=buffer) + entry.set_visibility(False) + vbox.pack_start(entry, False, False, 0) + + self.window.show_all() + + def destroy(self, *args): + self.window.destroy() + Gtk.main_quit() + + +def main(demoapp=None): + EntryBufferApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/Entry/entry_completion.py b/examples/demo/demos/Entry/entry_completion.py new file mode 100644 index 0000000..107c45a --- /dev/null +++ b/examples/demo/demos/Entry/entry_completion.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Entry Completion" +description = """ +Gtk.EntryCompletion provides a mechanism for adding support for +completion in Gtk.Entry. +""" + + +from gi.repository import Gtk + + +class EntryBufferApp: + def __init__(self): + self.window = Gtk.Dialog(title='Gtk.EntryCompletion') + self.window.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.NONE) + self.window.connect('response', self.destroy) + self.window.connect('destroy', lambda x: Gtk.main_quit()) + self.window.set_resizable(False) + + vbox = Gtk.VBox(homogeneous=False, spacing=0) + self.window.get_content_area().pack_start(vbox, True, True, 0) + vbox.set_border_width(5) + + label = Gtk.Label() + label.set_markup('Completion demo, try writing total or gnome for example.') + vbox.pack_start(label, False, False, 0) + + # create our entry + entry = Gtk.Entry() + vbox.pack_start(entry, False, False, 0) + + # create the completion object + completion = Gtk.EntryCompletion() + + # assign the completion to the entry + entry.set_completion(completion) + + # create tree model and use it as the completion model + completion_model = self.create_completion_model() + completion.set_model(completion_model) + + completion.set_text_column(0) + + self.window.show_all() + + def create_completion_model(self): + store = Gtk.ListStore(str) + + store.append(['GNOME']) + store.append(['total']) + store.append(['totally']) + + return store + + def destroy(self, *args): + self.window.destroy() + Gtk.main_quit() + + +def main(demoapp=None): + EntryBufferApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/Entry/search_entry.py b/examples/demo/demos/Entry/search_entry.py new file mode 100644 index 0000000..793b81a --- /dev/null +++ b/examples/demo/demos/Entry/search_entry.py @@ -0,0 +1,254 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Search Entry" +description = """GtkEntry allows to display icons and progress information. +This demo shows how to use these features in a search entry. +""" + +from gi.repository import Gtk, GObject + +(PIXBUF_COL, + TEXT_COL) = range(2) + + +class SearchboxApp: + def __init__(self, demoapp): + self.demoapp = demoapp + + self.window = Gtk.Dialog(title='Search Entry') + self.window.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.NONE) + + self.window.connect('response', lambda x, y: self.window.destroy()) + self.window.connect('destroy', Gtk.main_quit) + + content_area = self.window.get_content_area() + + vbox = Gtk.VBox(spacing=5) + content_area.pack_start(vbox, True, True, 0) + vbox.set_border_width(5) + + label = Gtk.Label() + label.set_markup('Search entry demo') + vbox.pack_start(label, False, False, 0) + + hbox = Gtk.HBox(homogeneous=False, spacing=10) + hbox.set_border_width(0) + vbox.pack_start(hbox, True, True, 0) + + # Create our entry + entry = Gtk.Entry() + hbox.pack_start(entry, False, False, 0) + + # Create the find and cancel buttons + notebook = Gtk.Notebook() + self.notebook = notebook + notebook.set_show_tabs(False) + notebook.set_show_border(False) + hbox.pack_start(notebook, False, False, 0) + + find_button = Gtk.Button(label='Find') + find_button.connect('clicked', self.start_search, entry) + notebook.append_page(find_button, None) + find_button.show() + + cancel_button = Gtk.Button(label='Cancel') + cancel_button.connect('clicked', self.stop_search, entry) + notebook.append_page(cancel_button, None) + cancel_button.show() + + # Set up the search icon + self.search_by_name(None, entry) + + # Set up the clear icon + entry.set_icon_from_stock(Gtk.EntryIconPosition.SECONDARY, + Gtk.STOCK_CLEAR) + self.text_changed_cb(entry, None, find_button) + + entry.connect('notify::text', self.text_changed_cb, find_button) + + entry.connect('activate', self.activate_cb) + + # Create the menu + menu = self.create_search_menu(entry) + entry.connect('icon-press', self.icon_press_cb, menu) + + # FIXME: this should take None for the detach callback + # but our callback implementation does not allow + # it yet, so we pass in a noop callback + menu.attach_to_widget(entry, self.detach) + + # add accessible alternatives for icon functionality + entry.connect('populate-popup', self.entry_populate_popup) + + self.window.show_all() + + def detach(self, *args): + pass + + def show_find_button(self): + self.notebook.set_current_page(0) + + def show_cancel_button(self): + self.notebook.set_current_page(1) + + def search_progress(self, entry): + entry.progress_pulse() + return True + + def search_progress_done(self, entry): + entry.set_progress_fraction(0.0) + + def finish_search(self, button, entry): + self.show_find_button() + GObject.source_remove(self.search_progress_id) + self.search_progress_done(entry) + self.search_progress_id = 0 + + return False + + def start_search_feedback(self, entry): + self.search_progress_id = GObject.timeout_add(100, + self.search_progress, + entry) + + return False + + def start_search(self, button, entry): + self.show_cancel_button() + self.search_progress_id = GObject.timeout_add_seconds(1, + self.start_search_feedback, + entry) + self.finish_search_id = GObject.timeout_add_seconds(15, + self.finish_search, + button) + + def stop_search(self, button, entry): + GObject.source_remove(self.finish_search_id) + self.finish_search(button, entry) + + def clear_entry_swapped(self, widget, entry): + self.clear_entry(entry) + + def clear_entry(self, entry): + entry.set_text('') + + def search_by_name(self, item, entry): + entry.set_icon_from_stock(Gtk.EntryIconPosition.PRIMARY, + Gtk.STOCK_FIND) + entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, + 'Search by name\n' + + 'Click here to change the search type') + + def search_by_description(self, item, entry): + entry.set_icon_from_stock(Gtk.EntryIconPosition.PRIMARY, + Gtk.STOCK_EDIT) + entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, + 'Search by description\n' + + 'Click here to change the search type') + + def search_by_file(self, item, entry): + entry.set_icon_from_stock(Gtk.EntryIconPosition.PRIMARY, + Gtk.STOCK_OPEN) + entry.set_icon_tooltip_text(Gtk.EntryIconPosition.PRIMARY, + 'Search by file name\n' + + 'Click here to change the search type') + + def create_search_menu(self, entry): + menu = Gtk.Menu() + + item = Gtk.ImageMenuItem.new_with_mnemonic('Search by _name') + image = Gtk.Image.new_from_stock(Gtk.STOCK_FIND, Gtk.IconSize.MENU) + item.set_image(image) + item.set_always_show_image(True) + item.connect('activate', self.search_by_name, entry) + menu.append(item) + + item = Gtk.ImageMenuItem.new_with_mnemonic('Search by _description') + image = Gtk.Image.new_from_stock(Gtk.STOCK_EDIT, Gtk.IconSize.MENU) + item.set_image(image) + item.set_always_show_image(True) + item.connect('activate', self.search_by_description, entry) + menu.append(item) + + item = Gtk.ImageMenuItem.new_with_mnemonic('Search by _file name') + image = Gtk.Image.new_from_stock(Gtk.STOCK_OPEN, Gtk.IconSize.MENU) + item.set_image(image) + item.set_always_show_image(True) + item.connect('activate', self.search_by_name, entry) + menu.append(item) + + menu.show_all() + + return menu + + def icon_press_cb(self, entry, position, event, menu): + if position == Gtk.EntryIconPosition.PRIMARY: + menu.popup(None, None, None, None, + event.button, event.time) + else: + self.clear_entry(entry) + + def text_changed_cb(self, entry, pspec, button): + has_text = entry.get_text_length() > 0 + entry.set_icon_sensitive(Gtk.EntryIconPosition.SECONDARY, has_text) + button.set_sensitive(has_text) + + def activate_cb(self, entry, button): + if self.search_progress_id != 0: + return + self.start_search(button, entry) + + def search_entry_destroyed(self, widget): + if self.finish_search_id != 0: + GObject.source_remove(self.finish_search_id) + if self.search_progress_id != 0: + GObject.source_remove(self.search_progress_id) + + self.window = None + + def entry_populate_popup(self, entry, menu): + has_text = entry.get_text_length() > 0 + + item = Gtk.SeparatorMenuItem() + item.show() + menu.append(item) + + item = Gtk.MenuItem.new_with_mnemonic("C_lear") + item.show() + item.connect('activate', self.clear_entry_swapped, entry) + menu.append(item) + item.set_sensitive(has_text) + + search_menu = self.create_search_menu(entry) + item = Gtk.MenuItem.new_with_label('Search by') + item.show() + item.set_submenu(search_menu) + menu.append(item) + + +def main(demoapp=None): + SearchboxApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/IconView/__init__.py b/examples/demo/demos/IconView/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/demo/demos/IconView/iconviewbasics.py b/examples/demo/demos/IconView/iconviewbasics.py new file mode 100644 index 0000000..8cb71a8 --- /dev/null +++ b/examples/demo/demos/IconView/iconviewbasics.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Icon View Basics" +description = """The GtkIconView widget is used to display and manipulate +icons. It uses a GtkTreeModel for data storage, so the list store example might +be helpful. We also use the Gio.File API to get the icons for each file type. +""" + + +import os + +from gi.repository import GLib, Gio, GdkPixbuf, Gtk + + +class IconViewApp: + (COL_PATH, + COL_DISPLAY_NAME, + COL_PIXBUF, + COL_IS_DIRECTORY, + NUM_COLS) = range(5) + + def __init__(self, demoapp): + self.pixbuf_lookup = {} + + self.demoapp = demoapp + + self.window = Gtk.Window() + self.window.set_title('Gtk.IconView demo') + self.window.set_default_size(650, 400) + self.window.connect('destroy', Gtk.main_quit) + + vbox = Gtk.VBox() + self.window.add(vbox) + + tool_bar = Gtk.Toolbar() + vbox.pack_start(tool_bar, False, False, 0) + + up_button = Gtk.ToolButton(stock_id=Gtk.STOCK_GO_UP) + up_button.set_is_important(True) + up_button.set_sensitive(False) + tool_bar.insert(up_button, -1) + + home_button = Gtk.ToolButton(stock_id=Gtk.STOCK_HOME) + home_button.set_is_important(True) + tool_bar.insert(home_button, -1) + + sw = Gtk.ScrolledWindow() + sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + sw.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + + vbox.pack_start(sw, True, True, 0) + + # create the store and fill it with content + self.parent_dir = '/' + store = self.create_store() + self.fill_store(store) + + icon_view = Gtk.IconView(model=store) + icon_view.set_selection_mode(Gtk.SelectionMode.MULTIPLE) + sw.add(icon_view) + + # connect to the 'clicked' signal of the "Up" tool button + up_button.connect('clicked', self.up_clicked, store) + + # connect to the 'clicked' signal of the "home" tool button + home_button.connect('clicked', self.home_clicked, store) + + self.up_button = up_button + self.home_button = home_button + + # we now set which model columns that correspond to the text + # and pixbuf of each item + icon_view.set_text_column(self.COL_DISPLAY_NAME) + icon_view.set_pixbuf_column(self.COL_PIXBUF) + + # connect to the "item-activated" signal + icon_view.connect('item-activated', self.item_activated, store) + icon_view.grab_focus() + + self.window.show_all() + + def sort_func(self, store, a_iter, b_iter, user_data): + (a_name, a_is_dir) = store.get(a_iter, + self.COL_DISPLAY_NAME, + self.COL_IS_DIRECTORY) + + (b_name, b_is_dir) = store.get(b_iter, + self.COL_DISPLAY_NAME, + self.COL_IS_DIRECTORY) + + if a_name is None: + a_name = '' + + if b_name is None: + b_name = '' + + if (not a_is_dir) and b_is_dir: + return 1 + elif a_is_dir and (not b_is_dir): + return -1 + elif a_name > b_name: + return 1 + elif a_name < b_name: + return -1 + else: + return 0 + + def up_clicked(self, item, store): + self.parent_dir = os.path.split(self.parent_dir)[0] + self.fill_store(store) + # de-sensitize the up button if we are at the root + self.up_button.set_sensitive(self.parent_dir != '/') + + def home_clicked(self, item, store): + self.parent_dir = GLib.get_home_dir() + self.fill_store(store) + + # Sensitize the up button + self.up_button.set_sensitive(True) + + def item_activated(self, icon_view, tree_path, store): + iter_ = store.get_iter(tree_path) + (path, is_dir) = store.get(iter_, self.COL_PATH, self.COL_IS_DIRECTORY) + if not is_dir: + return + + self.parent_dir = path + self.fill_store(store) + + self.up_button.set_sensitive(True) + + def create_store(self): + store = Gtk.ListStore(str, str, GdkPixbuf.Pixbuf, bool) + + # set sort column and function + store.set_default_sort_func(self.sort_func) + store.set_sort_column_id(-1, Gtk.SortType.ASCENDING) + + return store + + def file_to_icon_pixbuf(self, path): + pixbuf = None + + # get the theme icon + f = Gio.file_new_for_path(path) + info = f.query_info(Gio.FILE_ATTRIBUTE_STANDARD_ICON, + Gio.FileQueryInfoFlags.NONE, + None) + gicon = info.get_icon() + + # check to see if it is an image format we support + for format in GdkPixbuf.Pixbuf.get_formats(): + for mime_type in format.get_mime_types(): + content_type = Gio.content_type_from_mime_type(mime_type) + if content_type is not None: + break + + format_gicon = Gio.content_type_get_icon(content_type) + if format_gicon.equal(gicon): + gicon = f.icon_new() + break + + if gicon in self.pixbuf_lookup: + return self.pixbuf_lookup[gicon] + + if isinstance(gicon, Gio.ThemedIcon): + names = gicon.get_names() + icon_theme = Gtk.IconTheme.get_default() + for name in names: + try: + pixbuf = icon_theme.load_icon(name, 64, 0) + break + except GLib.GError: + pass + + self.pixbuf_lookup[gicon] = pixbuf + + elif isinstance(gicon, Gio.FileIcon): + icon_file = gicon.get_file() + path = icon_file.get_path() + pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(path, 72, 72) + self.pixbuf_lookup[gicon] = pixbuf + + return pixbuf + + def fill_store(self, store): + store.clear() + for name in os.listdir(self.parent_dir): + path = os.path.join(self.parent_dir, name) + is_dir = os.path.isdir(path) + pixbuf = self.file_to_icon_pixbuf(path) + store.append((path, name, pixbuf, is_dir)) + + +def main(demoapp=None): + IconViewApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/IconView/iconviewedit.py b/examples/demo/demos/IconView/iconviewedit.py new file mode 100644 index 0000000..85dfa93 --- /dev/null +++ b/examples/demo/demos/IconView/iconviewedit.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Editing and Drag-and-Drop" +description = """The GtkIconView widget supports Editing and Drag-and-Drop. +This example also demonstrates using the generic GtkCellLayout interface to set +up cell renderers in an icon view. +""" + +from gi.repository import Gtk, Gdk, GdkPixbuf + + +class IconviewEditApp: + COL_TEXT = 0 + NUM_COLS = 1 + + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Editing and Drag-and-Drop') + self.window.set_border_width(8) + self.window.connect('destroy', Gtk.main_quit) + + store = Gtk.ListStore(str) + colors = ['Red', 'Green', 'Blue', 'Yellow'] + store.clear() + for c in colors: + store.append([c]) + + icon_view = Gtk.IconView(model=store) + icon_view.set_selection_mode(Gtk.SelectionMode.SINGLE) + icon_view.set_item_orientation(Gtk.Orientation.HORIZONTAL) + icon_view.set_columns(2) + icon_view.set_reorderable(True) + + renderer = Gtk.CellRendererPixbuf() + icon_view.pack_start(renderer, True) + icon_view.set_cell_data_func(renderer, + self.set_cell_color, + None) + + renderer = Gtk.CellRendererText() + icon_view.pack_start(renderer, True) + renderer.props.editable = True + renderer.connect('edited', self.edited, icon_view) + icon_view.add_attribute(renderer, 'text', self.COL_TEXT) + + self.window.add(icon_view) + + self.window.show_all() + + def set_cell_color(self, cell_layout, cell, tree_model, iter_, icon_view): + + # FIXME return single element instead of tuple + text = tree_model.get(iter_, self.COL_TEXT)[0] + color = Gdk.color_parse(text) + pixel = 0 + if color is not None: + pixel = ((color.red >> 8) << 24 | + (color.green >> 8) << 16 | + (color.blue >> 8) << 8) + + pixbuf = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, False, 8, 24, 24) + pixbuf.fill(pixel) + + cell.props.pixbuf = pixbuf + + def edited(self, cell, path_string, text, icon_view): + model = icon_view.get_model() + path = Gtk.TreePath(path_string) + + iter_ = model.get_iter(path) + model.set_row(iter_, [text]) + + +def main(demoapp=None): + IconviewEditApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/TreeView/__init__.py b/examples/demo/demos/TreeView/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/demo/demos/TreeView/liststore.py b/examples/demo/demos/TreeView/liststore.py new file mode 100644 index 0000000..4b3daa1 --- /dev/null +++ b/examples/demo/demos/TreeView/liststore.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "List Store" +description = """ +The GtkListStore is used to store data in list form, to be used later on by a +GtkTreeView to display it. This demo builds a simple GtkListStore and displays +it. See the Stock Browser demo for a more advanced example. +""" + + +from gi.repository import Gtk, GObject, GLib + + +class Bug: + def __init__(self, is_fixed, number, severity, description): + self.is_fixed = is_fixed + self.number = number + self.severity = severity + self.description = description + + +# initial data we use to fill in the store +data = [Bug(False, 60482, "Normal", "scrollable notebooks and hidden tabs"), + Bug(False, 60620, "Critical", "gdk_window_clear_area (gdkwindow-win32.c) is not thread-safe"), + Bug(False, 50214, "Major", "Xft support does not clean up correctly"), + Bug(True, 52877, "Major", "GtkFileSelection needs a refresh method. "), + Bug(False, 56070, "Normal", "Can't click button after setting in sensitive"), + Bug(True, 56355, "Normal", "GtkLabel - Not all changes propagate correctly"), + Bug(False, 50055, "Normal", "Rework width/height computations for TreeView"), + Bug(False, 58278, "Normal", "gtk_dialog_set_response_sensitive () doesn't work"), + Bug(False, 55767, "Normal", "Getters for all setters"), + Bug(False, 56925, "Normal", "Gtkcalender size"), + Bug(False, 56221, "Normal", "Selectable label needs right-click copy menu"), + Bug(True, 50939, "Normal", "Add shift clicking to GtkTextView"), + Bug(False, 6112, "Enhancement", "netscape-like collapsable toolbars"), + Bug(False, 1, "Normal", "First bug :=)")] + + +class ListStoreApp: + (COLUMN_FIXED, + COLUMN_NUMBER, + COLUMN_SEVERITY, + COLUMN_DESCRIPTION, + COLUMN_PULSE, + COLUMN_ICON, + COLUMN_ACTIVE, + COLUMN_SENSITIVE, + NUM_COLUMNS) = range(9) + + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Gtk.ListStore Demo') + self.window.connect('destroy', Gtk.main_quit) + + vbox = Gtk.VBox(spacing=8) + self.window.add(vbox) + + label = Gtk.Label(label='This is the bug list (note: not based on real data, it would be nice to have a nice ODBC interface to bugzilla or so, though).') + vbox.pack_start(label, False, False, 0) + + sw = Gtk.ScrolledWindow() + sw.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + sw.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + vbox.pack_start(sw, True, True, 0) + + self.create_model() + treeview = Gtk.TreeView(model=self.model) + treeview.set_rules_hint(True) + treeview.set_search_column(self.COLUMN_DESCRIPTION) + sw.add(treeview) + + self.add_columns(treeview) + + self.window.set_default_size(280, 250) + self.window.show_all() + + self.window.connect('delete-event', self.window_closed) + self.timeout = GLib.timeout_add(80, self.spinner_timeout) + + def window_closed(self, window, event): + if self.timeout != 0: + GLib.source_remove(self.timeout) + + def spinner_timeout(self): + if self.model is None: + return False + + iter_ = self.model.get_iter_first() + pulse = self.model.get(iter_, self.COLUMN_PULSE)[0] + if pulse == 999999999: + pulse = 0 + else: + pulse += 1 + + self.model.set_value(iter_, self.COLUMN_PULSE, pulse) + self.model.set_value(iter_, self.COLUMN_ACTIVE, True) + + return True + + def create_model(self): + self.model = Gtk.ListStore(bool, + GObject.TYPE_INT, + str, + str, + GObject.TYPE_INT, + str, + bool, + bool) + + col = 0 + for bug in data: + if col == 1 or col == 3: + icon_name = 'battery-critical-charging-symbolic' + else: + icon_name = '' + if col == 3: + is_sensitive = False + else: + is_sensitive = True + + self.model.append([bug.is_fixed, + bug.number, + bug.severity, + bug.description, + 0, + icon_name, + False, + is_sensitive]) + col += 1 + + def add_columns(self, treeview): + model = treeview.get_model() + + # column for is_fixed toggle + renderer = Gtk.CellRendererToggle() + renderer.connect('toggled', self.is_fixed_toggled, model) + + column = Gtk.TreeViewColumn("Fixed?", renderer, + active=self.COLUMN_FIXED) + column.set_fixed_width(50) + column.set_sizing(Gtk.TreeViewColumnSizing.FIXED) + treeview.append_column(column) + + # column for severities + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Severity", renderer, + text=self.COLUMN_SEVERITY) + column.set_sort_column_id(self.COLUMN_SEVERITY) + treeview.append_column(column) + + # column for description + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Description", renderer, + text=self.COLUMN_DESCRIPTION) + column.set_sort_column_id(self.COLUMN_DESCRIPTION) + treeview.append_column(column) + + # column for spinner + renderer = Gtk.CellRendererSpinner() + column = Gtk.TreeViewColumn("Spinning", renderer, + pulse=self.COLUMN_PULSE, + active=self.COLUMN_ACTIVE) + column.set_sort_column_id(self.COLUMN_PULSE) + treeview.append_column(column) + + # column for symbolic icon + renderer = Gtk.CellRendererPixbuf() + renderer.props.follow_state = True + column = Gtk.TreeViewColumn("Symbolic icon", renderer, + icon_name=self.COLUMN_ICON, + sensitive=self.COLUMN_SENSITIVE) + column.set_sort_column_id(self.COLUMN_ICON) + treeview.append_column(column) + + def is_fixed_toggled(self, cell, path_str, model): + # get toggled iter + iter_ = model.get_iter(path_str) + is_fixed = model.get_value(iter_, self.COLUMN_FIXED) + + # do something with value + is_fixed ^= 1 + + model.set_value(iter_, self.COLUMN_FIXED, is_fixed) + + +def main(demoapp=None): + ListStoreApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/TreeView/treemodel_filelist.py b/examples/demo/demos/TreeView/treemodel_filelist.py new file mode 100644 index 0000000..f011a47 --- /dev/null +++ b/examples/demo/demos/TreeView/treemodel_filelist.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python + +title = "File List (GenericTreeModel)" +description = """ +This is a file list demo which makes use of the GenericTreeModel python +implementation of the Gtk.TreeModel interface. This demo shows what methods +need to be overridden to provide a valid TreeModel to a TreeView. +""" + +import os +import stat +import time + +import pygtkcompat +pygtkcompat.enable() +pygtkcompat.enable_gtk('3.0') + +import gtk + + +folderxpm = [ + "17 16 7 1", + " c #000000", + ". c #808000", + "X c yellow", + "o c #808080", + "O c #c0c0c0", + "+ c white", + "@ c None", + "@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@", + "@@+XXXX.@@@@@@@@@", + "@+OOOOOO.@@@@@@@@", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OOOOOOOOOOOOO. ", + "@ ", + "@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@" + ] +folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm) + +filexpm = [ + "12 12 3 1", + " c #000000", + ". c #ffff04", + "X c #b2c0dc", + "X XXX", + "X ...... XXX", + "X ...... X", + "X . ... X", + "X ........ X", + "X . .... X", + "X ........ X", + "X . .. X", + "X ........ X", + "X . .. X", + "X ........ X", + "X X" + ] +filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm) + + +class FileListModel(gtk.GenericTreeModel): + __gtype_name__ = 'DemoFileListModel' + + column_types = (gtk.gdk.Pixbuf, str, int, str, str) + column_names = ['Name', 'Size', 'Mode', 'Last Changed'] + + def __init__(self, dname=None): + gtk.GenericTreeModel.__init__(self) + self._sort_column_id = 0 + self._sort_order = gtk.SORT_ASCENDING + + if not dname: + self.dirname = os.path.expanduser('~') + else: + self.dirname = os.path.abspath(dname) + self.files = ['..'] + [f for f in os.listdir(self.dirname)] + return + + def get_pathname(self, path): + filename = self.files[path[0]] + return os.path.join(self.dirname, filename) + + def is_folder(self, path): + filename = self.files[path[0]] + pathname = os.path.join(self.dirname, filename) + filestat = os.stat(pathname) + if stat.S_ISDIR(filestat.st_mode): + return True + return False + + def get_column_names(self): + return self.column_names[:] + + # + # GenericTreeModel Implementation + # + def on_get_flags(self): + return 0 # gtk.TREE_MODEL_ITERS_PERSIST + + def on_get_n_columns(self): + return len(self.column_types) + + def on_get_column_type(self, n): + return self.column_types[n] + + def on_get_iter(self, path): + return self.files[path[0]] + + def on_get_path(self, rowref): + return self.files.index(rowref) + + def on_get_value(self, rowref, column): + fname = os.path.join(self.dirname, rowref) + try: + filestat = os.stat(fname) + except OSError: + return None + mode = filestat.st_mode + if column is 0: + if stat.S_ISDIR(mode): + return folderpb + else: + return filepb + elif column is 1: + return rowref + elif column is 2: + return filestat.st_size + elif column is 3: + return oct(stat.S_IMODE(mode)) + return time.ctime(filestat.st_mtime) + + def on_iter_next(self, rowref): + try: + i = self.files.index(rowref) + 1 + return self.files[i] + except IndexError: + return None + + def on_iter_children(self, rowref): + if rowref: + return None + return self.files[0] + + def on_iter_has_child(self, rowref): + return False + + def on_iter_n_children(self, rowref): + if rowref: + return 0 + return len(self.files) + + def on_iter_nth_child(self, rowref, n): + if rowref: + return None + try: + return self.files[n] + except IndexError: + return None + + def on_iter_parent(child): + return None + + +class GenericTreeModelExample: + def delete_event(self, widget, event, data=None): + gtk.main_quit() + return False + + def __init__(self): + # Create a new window + self.window = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + + self.window.set_size_request(300, 200) + + self.window.connect("delete_event", self.delete_event) + + self.listmodel = FileListModel() + + # create the TreeView + self.treeview = gtk.TreeView() + + self.tvcolumns = [] + + # create the TreeViewColumns to display the data + for n, name in enumerate(self.listmodel.get_column_names()): + if n == 0: + cellpb = gtk.CellRendererPixbuf() + col = gtk.TreeViewColumn(name, cellpb, pixbuf=0) + cell = gtk.CellRendererText() + col.pack_start(cell, False) + col.add_attribute(cell, 'text', 1) + else: + cell = gtk.CellRendererText() + col = gtk.TreeViewColumn(name, cell, text=n + 1) + if n == 1: + cell.set_property('xalign', 1.0) + + self.treeview.append_column(col) + + self.treeview.connect('row-activated', self.open_file) + + self.scrolledwindow = gtk.ScrolledWindow() + self.scrolledwindow.add(self.treeview) + self.window.add(self.scrolledwindow) + self.treeview.set_model(self.listmodel) + self.window.set_title(self.listmodel.dirname) + self.window.show_all() + + def open_file(self, treeview, path, column): + model = treeview.get_model() + if model.is_folder(path): + pathname = model.get_pathname(path) + new_model = FileListModel(pathname) + self.window.set_title(new_model.dirname) + treeview.set_model(new_model) + return + + +def main(demoapp=None): + demo = GenericTreeModelExample() + demo + gtk.main() + + +if __name__ == "__main__": + main() diff --git a/examples/demo/demos/TreeView/treemodel_filetree.py b/examples/demo/demos/TreeView/treemodel_filetree.py new file mode 100644 index 0000000..3b43190 --- /dev/null +++ b/examples/demo/demos/TreeView/treemodel_filetree.py @@ -0,0 +1,280 @@ +#!/usr/bin/env python + +title = "File Tree (GenericTreeModel)" +description = """ +This is a file list demo which makes use of the GenericTreeModel python +implementation of the Gtk.TreeModel interface. This demo shows what methods +need to be overridden to provide a valid TreeModel to a TreeView. +""" + +import os +import stat +import time +from collections import OrderedDict + +import pygtkcompat +pygtkcompat.enable_gtk('3.0') + +import gtk + + +folderxpm = [ + "17 16 7 1", + " c #000000", + ". c #808000", + "X c yellow", + "o c #808080", + "O c #c0c0c0", + "+ c white", + "@ c None", + "@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@", + "@@+XXXX.@@@@@@@@@", + "@+OOOOOO.@@@@@@@@", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OXOXOXOXOXOXO. ", + "@+XOXOXOXOXOXOX. ", + "@+OOOOOOOOOOOOO. ", + "@ ", + "@@@@@@@@@@@@@@@@@", + "@@@@@@@@@@@@@@@@@" + ] +folderpb = gtk.gdk.pixbuf_new_from_xpm_data(folderxpm) + +filexpm = [ + "12 12 3 1", + " c #000000", + ". c #ffff04", + "X c #b2c0dc", + "X XXX", + "X ...... XXX", + "X ...... X", + "X . ... X", + "X ........ X", + "X . .... X", + "X ........ X", + "X . .. X", + "X ........ X", + "X . .. X", + "X ........ X", + "X X" + ] +filepb = gtk.gdk.pixbuf_new_from_xpm_data(filexpm) + + +class FileTreeModel(gtk.GenericTreeModel): + __gtype_name__ = 'DemoFileTreeModel' + + column_types = (gtk.gdk.Pixbuf, str, int, str, str) + column_names = ['Name', 'Size', 'Mode', 'Last Changed'] + + def __init__(self, dname=None): + gtk.GenericTreeModel.__init__(self) + if not dname: + self.dirname = os.path.expanduser('~') + else: + self.dirname = os.path.abspath(dname) + self.files = self.build_file_dict(self.dirname) + return + + def build_file_dict(self, dirname): + """ + :Returns: + A dictionary containing the files in the given dirname keyed by filename. + If the child filename is a sub-directory, the dict value is a dict. + Otherwise it will be None. + """ + d = OrderedDict() + for fname in os.listdir(dirname): + try: + filestat = os.stat(os.path.join(dirname, fname)) + except OSError: + d[fname] = None + else: + d[fname] = OrderedDict() if stat.S_ISDIR(filestat.st_mode) else None + + return d + + def get_node_from_treepath(self, path): + """ + :Returns: + The node stored at the given tree path in local storage. + """ + # TreePaths are a series of integer indices so just iterate through them + # and index values by each integer since we are using an OrderedDict + if path is None: + path = [] + node = self.files + for index in path: + node = list(node.values())[index] + return node + + def get_node_from_filepath(self, filepath): + """ + :Returns: + The node stored at the given file path in local storage. + """ + if not filepath: + return self.files + node = self.files + for key in filepath.split(os.path.sep): + node = node[key] + return node + + def get_column_names(self): + return self.column_names[:] + + # + # GenericTreeModel Implementation + # + + def on_get_flags(self): + return 0 + + def on_get_n_columns(self): + return len(self.column_types) + + def on_get_column_type(self, n): + return self.column_types[n] + + def on_get_path(self, relpath): + path = [] + node = self.files + for key in relpath.split(os.path.sep): + path.append(list(node.keys()).index(key)) + node = node[key] + return path + + def on_get_value(self, relpath, column): + fname = os.path.join(self.dirname, relpath) + try: + filestat = os.stat(fname) + except OSError: + return None + mode = filestat.st_mode + if column is 0: + if stat.S_ISDIR(mode): + return folderpb + else: + return filepb + elif column is 1: + return os.path.basename(relpath) + elif column is 2: + return filestat.st_size + elif column is 3: + return oct(stat.S_IMODE(mode)) + return time.ctime(filestat.st_mtime) + + def on_get_iter(self, path): + filepath = '' + value = self.files + for index in path: + filepath = os.path.join(filepath, list(value.keys())[index]) + value = list(value.values())[index] + return filepath + + def on_iter_next(self, filepath): + parent_path, child_path = os.path.split(filepath) + parent = self.get_node_from_filepath(parent_path) + + # Index of filepath within its parents child list + sibling_names = list(parent.keys()) + index = sibling_names.index(child_path) + try: + return os.path.join(parent_path, sibling_names[index + 1]) + except IndexError: + return None + + def on_iter_children(self, filepath): + if filepath: + children = list(self.get_node_from_filepath(filepath).keys()) + if children: + return os.path.join(filepath, children[0]) + elif self.files: + return list(self.files.keys())[0] + + return None + + def on_iter_has_child(self, filepath): + return bool(self.get_node_from_filepath(filepath)) + + def on_iter_n_children(self, filepath): + return len(self.get_node_from_filepath(filepath)) + + def on_iter_nth_child(self, filepath, n): + try: + child = list(self.get_node_from_filepath(filepath).keys())[n] + if filepath: + return os.path.join(filepath, child) + else: + return child + except IndexError: + return None + + def on_iter_parent(self, filepath): + return os.path.dirname(filepath) + + def on_ref_node(self, filepath): + value = self.get_node_from_filepath(filepath) + if value is not None: + value.update(self.build_file_dict(os.path.join(self.dirname, filepath))) + + def on_unref_node(self, filepath): + pass + + +class GenericTreeModelExample: + def delete_event(self, widget, event, data=None): + gtk.main_quit() + return False + + def __init__(self): + # Create a new window + self.window = gtk.Window(type=gtk.WINDOW_TOPLEVEL) + self.window.set_size_request(300, 200) + self.window.connect("delete_event", self.delete_event) + + self.listmodel = FileTreeModel() + + # create the TreeView + self.treeview = gtk.TreeView() + + # create the TreeViewColumns to display the data + column_names = self.listmodel.get_column_names() + self.tvcolumn = [None] * len(column_names) + cellpb = gtk.CellRendererPixbuf() + self.tvcolumn[0] = gtk.TreeViewColumn(column_names[0], + cellpb, pixbuf=0) + cell = gtk.CellRendererText() + self.tvcolumn[0].pack_start(cell, False) + self.tvcolumn[0].add_attribute(cell, 'text', 1) + self.treeview.append_column(self.tvcolumn[0]) + for n in range(1, len(column_names)): + cell = gtk.CellRendererText() + if n == 1: + cell.set_property('xalign', 1.0) + self.tvcolumn[n] = gtk.TreeViewColumn(column_names[n], + cell, text=n + 1) + self.treeview.append_column(self.tvcolumn[n]) + + self.scrolledwindow = gtk.ScrolledWindow() + self.scrolledwindow.add(self.treeview) + self.window.add(self.scrolledwindow) + self.treeview.set_model(self.listmodel) + self.window.set_title(self.listmodel.dirname) + self.window.show_all() + + +def main(demoapp=None): + demo = GenericTreeModelExample() + demo + gtk.main() + + +if __name__ == "__main__": + main() diff --git a/examples/demo/demos/TreeView/treemodel_large.py b/examples/demo/demos/TreeView/treemodel_large.py new file mode 100644 index 0000000..b129521 --- /dev/null +++ b/examples/demo/demos/TreeView/treemodel_large.py @@ -0,0 +1,143 @@ +# -*- Mode: Python; py-indent-offset: 4 -*- +# pygobject - Python bindings for the GObject library +# Copyright (C) 2014 Simon Feltman +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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, see . + +title = "Tree Model with Large Data" +description = """ +Implementation of the Gtk.TreeModel interface to create a custom model. +The demo uses a fake data store (it is not backed by a Python list) and is for +the purpose of showing how to override the TreeModel interfaces virtual methods. +""" + +from gi.repository import GObject +from gi.repository import GLib +from gi.repository import Gtk + + +class Model(GObject.Object, Gtk.TreeModel): + columns_types = (str, str) + item_count = 100000 + item_data = 'abcdefghijklmnopqrstuvwxyz' + + def __init__(self): + super(Model, self).__init__() + + def do_get_flags(self): + return Gtk.TreeModelFlags.LIST_ONLY + + def do_get_n_columns(self): + return len(self.columns_types) + + def do_get_column_type(self, n): + return self.columns_types[n] + + def do_get_iter(self, path): + # Return False and an empty iter when out of range + index = path.get_indices()[0] + if index < 0 or index >= self.item_count: + return False, None + + it = Gtk.TreeIter() + it.user_data = index + return True, it + + def do_get_path(self, it): + return Gtk.TreePath([it.user_data]) + + def do_get_value(self, it, column): + if column == 0: + return str(it.user_data) + elif column == 1: + return self.item_data + + def do_iter_next(self, it): + # Return False if there is not a next item + next = it.user_data + 1 + if next >= self.item_count: + return False + + # Set the iters data and return True + it.user_data = next + return True + + def do_iter_previous(self, it): + prev = it.user_data - 1 + if prev < 0: + return False + + it.user_data = prev + return True + + def do_iter_children(self, parent): + # If parent is None return the first item + if parent is None: + it = Gtk.TreeIter() + it.user_data = 0 + return True, it + return False, None + + def do_iter_has_child(self, it): + return it is None + + def do_iter_n_children(self, it): + # If iter is None, return the number of top level nodes + if it is None: + return self.item_count + return 0 + + def do_iter_nth_child(self, parent, n): + if parent is not None or n >= self.item_count: + return False, None + elif parent is None: + # If parent is None, return the nth iter + it = Gtk.TreeIter() + it.user_data = n + return True, it + + def do_iter_parent(self, child): + return False, None + + +def main(demoapp=None): + model = Model() + # Use fixed-height-mode to get better model load and display performance. + view = Gtk.TreeView(fixed_height_mode=True, headers_visible=False) + column = Gtk.TreeViewColumn() + column.props.sizing = Gtk.TreeViewColumnSizing.FIXED + + renderer1 = Gtk.CellRendererText() + renderer2 = Gtk.CellRendererText() + column.pack_start(renderer1, expand=True) + column.pack_start(renderer2, expand=True) + column.add_attribute(renderer1, 'text', 0) + column.add_attribute(renderer2, 'text', 1) + view.append_column(column) + + scrolled = Gtk.ScrolledWindow() + scrolled.add(view) + + window = Gtk.Window(title=title) + window.set_size_request(480, 640) + window.add(scrolled) + window.show_all() + GLib.timeout_add(10, lambda *args: view.set_model(model)) + return window + + +if __name__ == "__main__": + window = main() + window.connect('destroy', Gtk.main_quit) + Gtk.main() diff --git a/examples/demo/demos/__init__.py b/examples/demo/demos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/examples/demo/demos/appwindow.py b/examples/demo/demos/appwindow.py new file mode 100644 index 0000000..893ecc0 --- /dev/null +++ b/examples/demo/demos/appwindow.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Application main window" +description = """ +Demonstrates a typical application window with menubar, toolbar, statusbar. +""" + +import os + +from gi.repository import GdkPixbuf, Gtk + + +infobar = None +window = None +messagelabel = None +_demoapp = None + + +def widget_destroy(widget, button): + widget.destroy() + + +def activate_action(action, user_data=None): + global window + + name = action.get_name() + _type = type(action) + if name == 'DarkTheme': + value = action.get_active() + settings = Gtk.Settings.get_default() + settings.set_property('gtk-application-prefer-dark-theme', value) + return + + dialog = Gtk.MessageDialog(message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.CLOSE, + text='You activated action: "%s" of type %s' % (name, _type)) + + # FIXME: this should be done in the constructor + dialog.set_transient_for(window) + dialog.connect('response', widget_destroy) + dialog.show() + + +def activate_radio_action(action, current, user_data=None): + global infobar + global messagelabel + + name = current.get_name() + _type = type(current) + active = current.get_active() + value = current.get_current_value() + if active: + text = 'You activated radio action: "%s" of type %s.\n Current value: %d' % (name, _type, value) + messagelabel.set_text(text) + infobar.set_message_type(Gtk.MessageType(value)) + infobar.show() + + +def update_statusbar(buffer, statusbar): + statusbar.pop(0) + count = buffer.get_char_count() + + iter = buffer.get_iter_at_mark(buffer.get_insert()) + row = iter.get_line() + col = iter.get_line_offset() + msg = 'Cursor at row %d column %d - %d chars in document' % (row, col, count) + + statusbar.push(0, msg) + + +def mark_set_callback(buffer, new_location, mark, data): + update_statusbar(buffer, data) + + +def about_cb(widget, user_data=None): + global window + + authors = ['John (J5) Palmieri', + 'Tomeu Vizoso', + 'and many more...'] + + documentors = ['David Malcolm', + 'Zack Goldberg', + 'and many more...'] + + license = """ +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Library General Public License as +published by the Free Software Foundation; either version 2 of the +License, or (at your option) any later version. + +This 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 +Library General Public License for more details. + +You should have received a copy of the GNU Library General Public +License along with the Gnome Library; see the file COPYING.LIB. If not, +write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, +Boston, MA 02111-1307, USA. +""" + dirname = os.path.abspath(os.path.dirname(__file__)) + filename = os.path.join(dirname, 'data', 'gtk-logo-rgb.gif') + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + transparent = pixbuf.add_alpha(True, 0xff, 0xff, 0xff) + + about = Gtk.AboutDialog(parent=window, + program_name='GTK+ Code Demos', + version='0.1', + copyright='(C) 2010 The PyGI Team', + license=license, + website='http://live.gnome.org/PyGI', + comments='Program to demonstrate PyGI functions.', + authors=authors, + documenters=documentors, + logo=transparent, + title='About GTK+ Code Demos') + + about.connect('response', widget_destroy) + about.show() + + +action_entries = ( + ("FileMenu", None, "_File"), # name, stock id, label + ("OpenMenu", None, "_Open"), # name, stock id, label + ("PreferencesMenu", None, "_Preferences"), # name, stock id, label + ("ColorMenu", None, "_Color"), # name, stock id, label + ("ShapeMenu", None, "_Shape"), # name, stock id, label + ("HelpMenu", None, "_Help"), # name, stock id, label + ("New", Gtk.STOCK_NEW, # name, stock id + "_New", "N", # label, accelerator + "Create a new file", # tooltip + activate_action), + ("File1", None, # name, stock id + "File1", None, # label, accelerator + "Open first file", # tooltip + activate_action), + ("Save", Gtk.STOCK_SAVE, # name, stock id + "_Save", "S", # label, accelerator + "Save current file", # tooltip + activate_action), + ("SaveAs", Gtk.STOCK_SAVE, # name, stock id + "Save _As...", None, # label, accelerator + "Save to a file", # tooltip + activate_action), + ("Quit", Gtk.STOCK_QUIT, # name, stock id + "_Quit", "Q", # label, accelerator + "Quit", # tooltip + activate_action), + ("About", None, # name, stock id + "_About", "A", # label, accelerator + "About", # tooltip + about_cb), + ("Logo", "demo-gtk-logo", # name, stock id + None, None, # label, accelerator + "GTK+", # tooltip + activate_action), +) + +toggle_action_entries = ( + ("Bold", Gtk.STOCK_BOLD, # name, stock id + "_Bold", "B", # label, accelerator + "Bold", # tooltip + activate_action, + True), # is_active + ("DarkTheme", None, # name, stock id + "_Prefer Dark Theme", None, # label, accelerator + "Prefer Dark Theme", # tooltip + activate_action, + False), # is_active +) + +(COLOR_RED, + COLOR_GREEN, + COLOR_BLUE) = range(3) + +color_action_entries = ( + ("Red", None, # name, stock id + "_Red", "R", # label, accelerator + "Blood", COLOR_RED), # tooltip, value + ("Green", None, # name, stock id + "_Green", "G", # label, accelerator + "Grass", COLOR_GREEN), # tooltip, value + ("Blue", None, # name, stock id + "_Blue", "B", # label, accelerator + "Sky", COLOR_BLUE), # tooltip, value +) + +(SHAPE_SQUARE, + SHAPE_RECTANGLE, + SHAPE_OVAL) = range(3) + +shape_action_entries = ( + ("Square", None, # name, stock id + "_Square", "S", # label, accelerator + "Square", SHAPE_SQUARE), # tooltip, value + ("Rectangle", None, # name, stock id + "_Rectangle", "R", # label, accelerator + "Rectangle", SHAPE_RECTANGLE), # tooltip, value + ("Oval", None, # name, stock id + "_Oval", "O", # label, accelerator + "Egg", SHAPE_OVAL), # tooltip, value +) + +ui_info = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +""" + + +def _quit(*args): + Gtk.main_quit() + + +def register_stock_icons(): + """ + This function registers our custom toolbar icons, so they can be themed. + It's totally optional to do this, you could just manually insert icons + and have them not be themeable, especially if you never expect people + to theme your app. + """ + ''' + item = Gtk.StockItem() + item.stock_id = 'demo-gtk-logo' + item.label = '_GTK!' + item.modifier = 0 + item.keyval = 0 + item.translation_domain = None + + Gtk.stock_add(item, 1) + ''' + global _demoapp + + factory = Gtk.IconFactory() + factory.add_default() + + if _demoapp is None: + filename = os.path.join('data', 'gtk-logo-rgb.gif') + else: + filename = _demoapp.find_file('gtk-logo-rgb.gif') + + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + transparent = pixbuf.add_alpha(True, 0xff, 0xff, 0xff) + icon_set = Gtk.IconSet.new_from_pixbuf(transparent) + + factory.add('demo-gtk-logo', icon_set) + + +class ToolMenuAction(Gtk.Action): + __gtype_name__ = "GtkToolMenuAction" + + def do_create_tool_item(self): + return Gtk.MenuToolButton() + + +def main(demoapp=None): + global infobar + global window + global messagelabel + global _demoapp + + _demoapp = demoapp + + register_stock_icons() + + window = Gtk.Window() + window.set_title('Application Window') + window.set_icon_name('gtk-open') + window.connect_after('destroy', _quit) + table = Gtk.Table(n_rows=1, + n_columns=5, + homogeneous=False) + window.add(table) + + action_group = Gtk.ActionGroup(name='AppWindowActions') + open_action = ToolMenuAction(name='Open', + stock_id=Gtk.STOCK_OPEN, + label='_Open', + tooltip='Open a file') + + action_group.add_action(open_action) + action_group.add_actions(action_entries) + action_group.add_toggle_actions(toggle_action_entries) + action_group.add_radio_actions(color_action_entries, + COLOR_RED, + activate_radio_action) + action_group.add_radio_actions(shape_action_entries, + SHAPE_SQUARE, + activate_radio_action) + + merge = Gtk.UIManager() + merge.insert_action_group(action_group, 0) + window.add_accel_group(merge.get_accel_group()) + + merge.add_ui_from_string(ui_info) + + bar = merge.get_widget('/MenuBar') + bar.show() + table.attach(bar, 0, 1, 0, 1, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + 0, 0, 0) + + bar = merge.get_widget('/ToolBar') + bar.show() + table.attach(bar, 0, 1, 1, 2, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + 0, 0, 0) + + infobar = Gtk.InfoBar() + infobar.set_no_show_all(True) + messagelabel = Gtk.Label() + messagelabel.show() + infobar.get_content_area().pack_start(messagelabel, True, True, 0) + infobar.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) + infobar.connect('response', lambda a, b: Gtk.Widget.hide(a)) + + table.attach(infobar, 0, 1, 2, 3, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + 0, 0, 0) + + sw = Gtk.ScrolledWindow(hadjustment=None, + vadjustment=None) + sw.set_shadow_type(Gtk.ShadowType.IN) + table.attach(sw, 0, 1, 3, 4, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + 0, 0) + + contents = Gtk.TextView() + contents.grab_focus() + sw.add(contents) + + # Create statusbar + statusbar = Gtk.Statusbar() + table.attach(statusbar, 0, 1, 4, 5, + Gtk.AttachOptions.EXPAND | Gtk.AttachOptions.FILL, + 0, 0, 0) + + # show text widget info in the statusbar + buffer = contents.get_buffer() + buffer.connect('changed', update_statusbar, statusbar) + buffer.connect('mark_set', mark_set_callback, statusbar) + + update_statusbar(buffer, statusbar) + + window.set_default_size(200, 200) + window.show_all() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/assistant.py b/examples/demo/demos/assistant.py new file mode 100644 index 0000000..9e729e9 --- /dev/null +++ b/examples/demo/demos/assistant.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Assistant" +description = """ +Demonstrates a sample multistep assistant. Assistants are used to divide +an operation into several simpler sequential steps, and to guide the user +through these steps. +""" + + +from gi.repository import Gtk + + +class AssistantApp: + def __init__(self): + self.assistant = Gtk.Assistant() + self.assistant.set_default_size(-1, 300) + + self.create_page1() + self.create_page2() + self.create_page3() + + self.assistant.connect('cancel', self.on_close_cancel) + self.assistant.connect('close', self.on_close_cancel) + self.assistant.connect('apply', self.on_apply) + self.assistant.connect('prepare', self.on_prepare) + + self.assistant.show() + + def on_close_cancel(self, assistant): + assistant.destroy() + Gtk.main_quit() + + def on_apply(self, assistant): + # apply changes here; this is a fictional example so just do + # nothing here + pass + + def on_prepare(self, assistant, page): + current_page = assistant.get_current_page() + n_pages = assistant.get_n_pages() + title = 'Sample assistant (%d of %d)' % (current_page + 1, n_pages) + assistant.set_title(title) + + def on_entry_changed(self, widget): + page_number = self.assistant.get_current_page() + current_page = self.assistant.get_nth_page(page_number) + text = widget.get_text() + + if text: + self.assistant.set_page_complete(current_page, True) + else: + self.assistant.set_page_complete(current_page, False) + + def create_page1(self): + box = Gtk.HBox(homogeneous=False, + spacing=12) + box.set_border_width(12) + label = Gtk.Label(label='You must fill out this entry to continue:') + box.pack_start(label, False, False, 0) + + entry = Gtk.Entry() + box.pack_start(entry, True, True, 0) + entry.connect('changed', self.on_entry_changed) + + box.show_all() + self.assistant.append_page(box) + self.assistant.set_page_title(box, 'Page 1') + self.assistant.set_page_type(box, Gtk.AssistantPageType.INTRO) + + pixbuf = self.assistant.render_icon(Gtk.STOCK_DIALOG_INFO, + Gtk.IconSize.DIALOG, + None) + + self.assistant.set_page_header_image(box, pixbuf) + + def create_page2(self): + box = Gtk.VBox(homogeneous=False, + spacing=12) + box.set_border_width(12) + + checkbutton = Gtk.CheckButton(label='This is optional data, you may continue even if you do not check this') + box.pack_start(checkbutton, False, False, 0) + + box.show_all() + + self.assistant.append_page(box) + self.assistant.set_page_complete(box, True) + self.assistant.set_page_title(box, 'Page 2') + + pixbuf = self.assistant.render_icon(Gtk.STOCK_DIALOG_INFO, + Gtk.IconSize.DIALOG, + None) + self.assistant.set_page_header_image(box, pixbuf) + + def create_page3(self): + label = Gtk.Label(label='This is a confirmation page, press "Apply" to apply changes') + label.show() + self.assistant.append_page(label) + self.assistant.set_page_complete(label, True) + self.assistant.set_page_title(label, 'Confirmation') + self.assistant.set_page_type(label, Gtk.AssistantPageType.CONFIRM) + + pixbuf = self.assistant.render_icon(Gtk.STOCK_DIALOG_INFO, + Gtk.IconSize.DIALOG, + None) + self.assistant.set_page_header_image(label, pixbuf) + + +def main(demoapp=None): + AssistantApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/builder.py b/examples/demo/demos/builder.py new file mode 100644 index 0000000..47e09a4 --- /dev/null +++ b/examples/demo/demos/builder.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Builder" +description = """ +Demonstrates an interface loaded from a XML description. +""" + + +import os + +from gi.repository import Gtk + + +class BuilderApp: + def __init__(self, demoapp): + self.demoapp = demoapp + + self.builder = Gtk.Builder() + if demoapp is None: + filename = os.path.join('data', 'demo.ui') + else: + filename = demoapp.find_file('demo.ui') + + self.builder.add_from_file(filename) + self.builder.connect_signals(self) + + window = self.builder.get_object('window1') + window.connect('destroy', lambda x: Gtk.main_quit()) + window.show_all() + + def about_activate(self, action): + about_dlg = self.builder.get_object('aboutdialog1') + about_dlg.run() + about_dlg.hide() + + def quit_activate(self, action): + Gtk.main_quit() + + +def main(demoapp=None): + BuilderApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/button_box.py b/examples/demo/demos/button_box.py new file mode 100644 index 0000000..be94984 --- /dev/null +++ b/examples/demo/demos/button_box.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Button Boxes" +description = """ +The Button Box widgets are used to arrange buttons with padding. +""" + + +from gi.repository import Gtk + + +class ButtonBoxApp: + def __init__(self): + window = Gtk.Window() + window.set_title('Button Boxes') + window.connect('destroy', lambda x: Gtk.main_quit()) + window.set_border_width(10) + + main_vbox = Gtk.VBox(homogeneous=False, spacing=0) + window.add(main_vbox) + + frame_horz = Gtk.Frame(label='Horizontal Button Boxes') + main_vbox.pack_start(frame_horz, True, True, 10) + + vbox = Gtk.VBox(homogeneous=False, spacing=0) + vbox.set_border_width(10) + frame_horz.add(vbox) + + vbox.pack_start( + self.create_bbox(True, 'Spread', 40, Gtk.ButtonBoxStyle.SPREAD), + True, True, 0) + + vbox.pack_start( + self.create_bbox(True, 'Edge', 40, Gtk.ButtonBoxStyle.EDGE), + True, True, 5) + + vbox.pack_start( + self.create_bbox(True, 'Start', 40, Gtk.ButtonBoxStyle.START), + True, True, 5) + + vbox.pack_start( + self.create_bbox(True, 'End', 40, Gtk.ButtonBoxStyle.END), + True, True, 5) + + frame_vert = Gtk.Frame(label='Vertical Button Boxes') + main_vbox.pack_start(frame_vert, True, True, 10) + + hbox = Gtk.HBox(homogeneous=False, spacing=0) + hbox.set_border_width(10) + frame_vert.add(hbox) + + hbox.pack_start( + self.create_bbox(False, 'Spread', 30, Gtk.ButtonBoxStyle.SPREAD), + True, True, 0) + + hbox.pack_start( + self.create_bbox(False, 'Edge', 30, Gtk.ButtonBoxStyle.EDGE), + True, True, 5) + + hbox.pack_start( + self.create_bbox(False, 'Start', 30, Gtk.ButtonBoxStyle.START), + True, True, 5) + + hbox.pack_start( + self.create_bbox(False, 'End', 30, Gtk.ButtonBoxStyle.END), + True, True, 5) + + window.show_all() + + def create_bbox(self, is_horizontal, title, spacing, layout): + frame = Gtk.Frame(label=title) + + if is_horizontal: + bbox = Gtk.HButtonBox() + else: + bbox = Gtk.VButtonBox() + + bbox.set_border_width(5) + frame.add(bbox) + + bbox.set_layout(layout) + bbox.set_spacing(spacing) + + # FIXME: GtkButton consturctor should take a stock_id + button = Gtk.Button.new_from_stock(Gtk.STOCK_OK) + bbox.add(button) + + button = Gtk.Button.new_from_stock(Gtk.STOCK_CANCEL) + bbox.add(button) + + button = Gtk.Button.new_from_stock(Gtk.STOCK_HELP) + bbox.add(button) + + return frame + + +def main(demoapp=None): + ButtonBoxApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/clipboard.py b/examples/demo/demos/clipboard.py new file mode 100644 index 0000000..5a88828 --- /dev/null +++ b/examples/demo/demos/clipboard.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Clipboard" +description = """ +GtkClipboard is used for clipboard handling. This demo shows how to +copy and paste text to and from the clipboard. + +It also shows how to transfer images via the clipboard or via +drag-and-drop, and how to make clipboard contents persist after +the application exits. Clipboard persistence requires a clipboard +manager to run. +""" + + +from gi.repository import Gtk, Gdk + + +class ClipboardApp: + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Clipboard demo') + self.window.connect('destroy', lambda w: Gtk.main_quit()) + + vbox = Gtk.VBox(homogeneous=False, spacing=0) + vbox.set_border_width(8) + self.window.add(vbox) + + label = Gtk.Label(label='"Copy" will copy the text\nin the entry to the clipboard') + vbox.pack_start(label, False, False, 0) + + hbox = Gtk.HBox(homogeneous=False, spacing=4) + hbox.set_border_width(8) + vbox.pack_start(hbox, False, False, 0) + + # create first entry + entry = Gtk.Entry() + hbox.pack_start(entry, True, True, 0) + + # create button + button = Gtk.Button.new_from_stock(Gtk.STOCK_COPY) + hbox.pack_start(button, False, False, 0) + button.connect('clicked', self.copy_button_clicked, entry) + + label = Gtk.Label(label='"Paste" will paste the text from the clipboard to the entry') + vbox.pack_start(label, False, False, 0) + + hbox = Gtk.HBox(homogeneous=False, spacing=4) + hbox.set_border_width(8) + vbox.pack_start(hbox, False, False, 0) + + # create secondary entry + entry = Gtk.Entry() + hbox.pack_start(entry, True, True, 0) + # create button + button = Gtk.Button.new_from_stock(Gtk.STOCK_PASTE) + hbox.pack_start(button, False, False, 0) + button.connect('clicked', self.paste_button_clicked, entry) + + label = Gtk.Label(label='Images can be transferred via the clipboard, too') + vbox.pack_start(label, False, False, 0) + + hbox = Gtk.HBox(homogeneous=False, spacing=4) + hbox.set_border_width(8) + vbox.pack_start(hbox, False, False, 0) + + # create the first image + image = Gtk.Image(stock=Gtk.STOCK_DIALOG_WARNING, + icon_size=Gtk.IconSize.BUTTON) + + ebox = Gtk.EventBox() + ebox.add(image) + hbox.add(ebox) + + # make ebox a drag source + ebox.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, + None, Gdk.DragAction.COPY) + ebox.drag_source_add_image_targets() + ebox.connect('drag-begin', self.drag_begin, image) + ebox.connect('drag-data-get', self.drag_data_get, image) + + # accept drops on ebox + ebox.drag_dest_set(Gtk.DestDefaults.ALL, + None, Gdk.DragAction.COPY) + ebox.drag_dest_add_image_targets() + ebox.connect('drag-data-received', self.drag_data_received, image) + + # context menu on ebox + ebox.connect('button-press-event', self.button_press, image) + + # create the second image + image = Gtk.Image(stock=Gtk.STOCK_STOP, + icon_size=Gtk.IconSize.BUTTON) + + ebox = Gtk.EventBox() + ebox.add(image) + hbox.add(ebox) + + # make ebox a drag source + ebox.drag_source_set(Gdk.ModifierType.BUTTON1_MASK, + None, Gdk.DragAction.COPY) + ebox.drag_source_add_image_targets() + ebox.connect('drag-begin', self.drag_begin, image) + ebox.connect('drag-data-get', self.drag_data_get, image) + + # accept drops on ebox + ebox.drag_dest_set(Gtk.DestDefaults.ALL, + None, Gdk.DragAction.COPY) + ebox.drag_dest_add_image_targets() + ebox.connect('drag-data-received', self.drag_data_received, image) + + # context menu on ebox + ebox.connect('button-press-event', self.button_press, image) + + # tell the clipboard manager to make data persistent + # FIXME: Allow sending strings a Atoms and convert in PyGI + atom = Gdk.atom_intern('CLIPBOARD', True) + clipboard = Gtk.Clipboard.get(atom) + clipboard.set_can_store(None) + + self.window.show_all() + + def copy_button_clicked(self, button, entry): + # get the default clipboard + atom = Gdk.atom_intern('CLIPBOARD', True) + clipboard = entry.get_clipboard(atom) + + # set the clipboard's text + # FIXME: don't require passing length argument + clipboard.set_text(entry.get_text(), -1) + + def paste_received(self, clipboard, text, entry): + if text is not None: + entry.set_text(text) + + def paste_button_clicked(self, button, entry): + # get the default clipboard + atom = Gdk.atom_intern('CLIPBOARD', True) + clipboard = entry.get_clipboard(atom) + + # set the clipboard's text + clipboard.request_text(self.paste_received, entry) + + def get_image_pixbuf(self, image): + # FIXME: We should hide storage types in an override + storage_type = image.get_storage_type() + if storage_type == Gtk.ImageType.PIXBUF: + return image.get_pixbuf() + elif storage_type == Gtk.ImageType.STOCK: + (stock_id, size) = image.get_stock() + return image.render_icon(stock_id, size, None) + + return None + + def drag_begin(self, widget, context, data): + pixbuf = self.get_image_pixbuf(data) + Gtk.drag_set_icon_pixbuf(context, pixbuf, -2, -2) + + def drag_data_get(self, widget, context, selection_data, info, time, data): + pixbuf = self.get_image_pixbuf(data) + selection_data.set_pixbuf(pixbuf) + + def drag_data_received(self, widget, context, x, y, selection_data, info, time, data): + if selection_data.get_length() > 0: + pixbuf = selection_data.get_pixbuf() + data.set_from_pixbuf(pixbuf) + + def copy_image(self, item, data): + # get the default clipboard + atom = Gdk.atom_intern('CLIPBOARD', True) + clipboard = Gtk.Clipboard.get(atom) + pixbuf = self.get_image_pixbuf(data) + + clipboard.set_image(pixbuf) + + def paste_image(self, item, data): + # get the default clipboard + atom = Gdk.atom_intern('CLIPBOARD', True) + clipboard = Gtk.Clipboard.get(atom) + pixbuf = clipboard.wait_for_image() + + if pixbuf is not None: + data.set_from_pixbuf(pixbuf) + + def button_press(self, widget, event, data): + if event.button != 3: + return False + + self.menu = Gtk.Menu() + + item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_COPY, None) + item.connect('activate', self.copy_image, data) + item.show() + self.menu.append(item) + + item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_PASTE, None) + item.connect('activate', self.paste_image, data) + item.show() + self.menu.append(item) + + self.menu.popup(None, None, None, None, event.button, event.time) + + +def main(demoapp=None): + ClipboardApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/colorselector.py b/examples/demo/demos/colorselector.py new file mode 100644 index 0000000..d05ca52 --- /dev/null +++ b/examples/demo/demos/colorselector.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Color Selector" +description = """ + GtkColorSelection lets the user choose a color. GtkColorSelectionDialog is + a prebuilt dialog containing a GtkColorSelection. + """ + + +from gi.repository import Gtk, Gdk + + +class ColorSelectorApp: + def __init__(self): + # FIXME: we should allow Gdk.Color to be allocated without parameters + # Also color doesn't seem to work + self.color = Gdk.RGBA() + self.color.red = 0 + self.color.blue = 1 + self.color.green = 0 + self.color.alpha = 1 + + self.window = Gtk.Window() + self.window.set_title('Color Selection') + self.window.set_border_width(8) + self.window.connect('destroy', lambda w: Gtk.main_quit()) + + vbox = Gtk.VBox(homogeneous=False, + spacing=8) + vbox.set_border_width(8) + self.window.add(vbox) + + # create color swatch area + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + vbox.pack_start(frame, True, True, 0) + + self.da = Gtk.DrawingArea() + self.da.connect('draw', self.draw_cb) + + # set a minimum size + self.da.set_size_request(200, 200) + # set the color + self.da.override_background_color(0, self.color) + frame.add(self.da) + + alignment = Gtk.Alignment(xalign=1.0, + yalign=0.5, + xscale=0.0, + yscale=0.0) + + button = Gtk.Button(label='_Change the above color', + use_underline=True) + alignment.add(button) + vbox.pack_start(alignment, False, False, 0) + + button.connect('clicked', self.change_color_cb) + + self.window.show_all() + + def draw_cb(self, widget, cairo_ctx): + style = widget.get_style_context() + bg_color = style.get_background_color(0) + Gdk.cairo_set_source_rgba(cairo_ctx, bg_color) + cairo_ctx.paint() + + return True + + def change_color_cb(self, button): + dialog = Gtk.ColorSelectionDialog(title='Changing color') + dialog.set_transient_for(self.window) + + colorsel = dialog.get_color_selection() + colorsel.set_previous_rgba(self.color) + colorsel.set_current_rgba(self.color) + colorsel.set_has_palette(True) + + response = dialog.run() + + if response == Gtk.ResponseType.OK: + self.color = colorsel.get_current_rgba() + self.da.override_background_color(0, self.color) + + dialog.destroy() + + +def main(demoapp=None): + ColorSelectorApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/combobox.py b/examples/demo/demos/combobox.py new file mode 100644 index 0000000..36034ba --- /dev/null +++ b/examples/demo/demos/combobox.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Combo boxes" +description = """ +The ComboBox widget allows to select one option out of a list. +The ComboBoxEntry additionally allows the user to enter a value +that is not in the list of options. + +How the options are displayed is controlled by cell renderers. + """ + + +from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, GObject + + +(PIXBUF_COL, + TEXT_COL) = range(2) + + +class MaskEntry(Gtk.Entry): + __gtype_name__ = 'MaskEntry' + + def __init__(self, mask=None): + self.mask = mask + super(MaskEntry, self).__init__() + + self.connect('changed', self.changed_cb) + + self.error_color = Gdk.RGBA() + self.error_color.red = 1.0 + self.error_color.green = 0.9 + self.error_color_blue = 0.9 + self.error_color.alpha = 1.0 + + # workaround since override_color doesn't accept None yet + style_ctx = self.get_style_context() + self.normal_color = style_ctx.get_color(0) + + def set_background(self): + if self.mask: + if not GLib.regex_match_simple(self.mask, + self.get_text(), 0, 0): + self.override_color(0, self.error_color) + return + + self.override_color(0, self.normal_color) + + def changed_cb(self, entry): + self.set_background() + + +class ComboboxApp: + def __init__(self, demoapp): + self.demoapp = demoapp + + self.window = Gtk.Window() + self.window.set_title('Combo boxes') + self.window.set_border_width(10) + self.window.connect('destroy', lambda w: Gtk.main_quit()) + + vbox = Gtk.VBox(homogeneous=False, spacing=2) + self.window.add(vbox) + + frame = Gtk.Frame(label='Some stock icons') + vbox.pack_start(frame, False, False, 0) + + box = Gtk.VBox(homogeneous=False, spacing=0) + box.set_border_width(5) + frame.add(box) + + model = self.create_stock_icon_store() + combo = Gtk.ComboBox(model=model) + box.add(combo) + + renderer = Gtk.CellRendererPixbuf() + combo.pack_start(renderer, False) + + # FIXME: override set_attributes + combo.add_attribute(renderer, 'pixbuf', PIXBUF_COL) + combo.set_cell_data_func(renderer, self.set_sensitive, None) + + renderer = Gtk.CellRendererText() + combo.pack_start(renderer, True) + combo.add_attribute(renderer, 'text', TEXT_COL) + combo.set_cell_data_func(renderer, self.set_sensitive, None) + + combo.set_row_separator_func(self.is_separator, None) + combo.set_active(0) + + # a combobox demonstrating trees + frame = Gtk.Frame(label='Where are we ?') + vbox.pack_start(frame, False, False, 0) + + box = Gtk.VBox(homogeneous=False, spacing=0) + box.set_border_width(5) + frame.add(box) + + model = self.create_capital_store() + combo = Gtk.ComboBox(model=model) + box.add(combo) + + renderer = Gtk.CellRendererText() + combo.pack_start(renderer, True) + combo.add_attribute(renderer, 'text', 0) + combo.set_cell_data_func(renderer, self.is_capital_sensistive, None) + + # FIXME: make new_from_indices work + # make constructor take list or string of indices + path = Gtk.TreePath.new_from_string('0:8') + treeiter = model.get_iter(path) + combo.set_active_iter(treeiter) + + # A GtkComboBoxEntry with validation. + + frame = Gtk.Frame(label='Editable') + vbox.pack_start(frame, False, False, 0) + + box = Gtk.VBox(homogeneous=False, spacing=0) + box.set_border_width(5) + frame.add(box) + + combo = Gtk.ComboBoxText.new_with_entry() + self.fill_combo_entry(combo) + box.add(combo) + + entry = MaskEntry(mask='^([0-9]*|One|Two|2\302\275|Three)$') + + Gtk.Container.remove(combo, combo.get_child()) + combo.add(entry) + + # A combobox with string IDs + + frame = Gtk.Frame(label='String IDs') + vbox.pack_start(frame, False, False, 0) + + box = Gtk.VBox(homogeneous=False, spacing=0) + box.set_border_width(5) + frame.add(box) + + # FIXME: model is not setup when constructing Gtk.ComboBoxText() + # so we call new() - Gtk should fix this to setup the model + # in __init__, not in the constructor + combo = Gtk.ComboBoxText.new() + combo.append('never', 'Not visible') + combo.append('when-active', 'Visible when active') + combo.append('always', 'Always visible') + box.add(combo) + + entry = Gtk.Entry() + + combo.bind_property('active-id', + entry, 'text', + GObject.BindingFlags.BIDIRECTIONAL) + + box.add(entry) + self.window.show_all() + + def strip_underscore(self, s): + return s.replace('_', '') + + def create_stock_icon_store(self): + stock_id = (Gtk.STOCK_DIALOG_WARNING, + Gtk.STOCK_STOP, + Gtk.STOCK_NEW, + Gtk.STOCK_CLEAR, + None, + Gtk.STOCK_OPEN) + + cellview = Gtk.CellView() + store = Gtk.ListStore(GdkPixbuf.Pixbuf, str) + + for id in stock_id: + if id is not None: + pixbuf = cellview.render_icon(id, Gtk.IconSize.BUTTON, None) + item = Gtk.stock_lookup(id) + label = self.strip_underscore(item.label) + store.append((pixbuf, label)) + else: + store.append((None, 'separator')) + + return store + + def set_sensitive(self, cell_layout, cell, tree_model, treeiter, data): + """ + A GtkCellLayoutDataFunc that demonstrates how one can control + sensitivity of rows. This particular function does nothing + useful and just makes the second row insensitive. + """ + + path = tree_model.get_path(treeiter) + indices = path.get_indices() + + sensitive = not(indices[0] == 1) + + cell.set_property('sensitive', sensitive) + + def is_separator(self, model, treeiter, data): + """ + A GtkTreeViewRowSeparatorFunc that demonstrates how rows can be + rendered as separators. This particular function does nothing + useful and just turns the fourth row into a separator. + """ + + path = model.get_path(treeiter) + + indices = path.get_indices() + result = (indices[0] == 4) + + return result + + def create_capital_store(self): + capitals = ( + {'group': 'A - B', 'capital': None}, + {'group': None, 'capital': 'Albany'}, + {'group': None, 'capital': 'Annapolis'}, + {'group': None, 'capital': 'Atlanta'}, + {'group': None, 'capital': 'Augusta'}, + {'group': None, 'capital': 'Austin'}, + {'group': None, 'capital': 'Baton Rouge'}, + {'group': None, 'capital': 'Bismarck'}, + {'group': None, 'capital': 'Boise'}, + {'group': None, 'capital': 'Boston'}, + {'group': 'C - D', 'capital': None}, + {'group': None, 'capital': 'Carson City'}, + {'group': None, 'capital': 'Charleston'}, + {'group': None, 'capital': 'Cheyeene'}, + {'group': None, 'capital': 'Columbia'}, + {'group': None, 'capital': 'Columbus'}, + {'group': None, 'capital': 'Concord'}, + {'group': None, 'capital': 'Denver'}, + {'group': None, 'capital': 'Des Moines'}, + {'group': None, 'capital': 'Dover'}, + {'group': 'E - J', 'capital': None}, + {'group': None, 'capital': 'Frankfort'}, + {'group': None, 'capital': 'Harrisburg'}, + {'group': None, 'capital': 'Hartford'}, + {'group': None, 'capital': 'Helena'}, + {'group': None, 'capital': 'Honolulu'}, + {'group': None, 'capital': 'Indianapolis'}, + {'group': None, 'capital': 'Jackson'}, + {'group': None, 'capital': 'Jefferson City'}, + {'group': None, 'capital': 'Juneau'}, + {'group': 'K - O', 'capital': None}, + {'group': None, 'capital': 'Lansing'}, + {'group': None, 'capital': 'Lincon'}, + {'group': None, 'capital': 'Little Rock'}, + {'group': None, 'capital': 'Madison'}, + {'group': None, 'capital': 'Montgomery'}, + {'group': None, 'capital': 'Montpelier'}, + {'group': None, 'capital': 'Nashville'}, + {'group': None, 'capital': 'Oklahoma City'}, + {'group': None, 'capital': 'Olympia'}, + {'group': 'P - S', 'capital': None}, + {'group': None, 'capital': 'Phoenix'}, + {'group': None, 'capital': 'Pierre'}, + {'group': None, 'capital': 'Providence'}, + {'group': None, 'capital': 'Raleigh'}, + {'group': None, 'capital': 'Richmond'}, + {'group': None, 'capital': 'Sacramento'}, + {'group': None, 'capital': 'Salem'}, + {'group': None, 'capital': 'Salt Lake City'}, + {'group': None, 'capital': 'Santa Fe'}, + {'group': None, 'capital': 'Springfield'}, + {'group': None, 'capital': 'St. Paul'}, + {'group': 'T - Z', 'capital': None}, + {'group': None, 'capital': 'Tallahassee'}, + {'group': None, 'capital': 'Topeka'}, + {'group': None, 'capital': 'Trenton'} + ) + + parent = None + + store = Gtk.TreeStore(str) + + for item in capitals: + if item['group']: + parent = store.append(None, (item['group'],)) + elif item['capital']: + store.append(parent, (item['capital'],)) + + return store + + def is_capital_sensistive(self, cell_layout, cell, tree_model, treeiter, data): + sensitive = not tree_model.iter_has_child(treeiter) + cell.set_property('sensitive', sensitive) + + def fill_combo_entry(self, entry): + entry.append_text('One') + entry.append_text('Two') + entry.append_text('2\302\275') + entry.append_text('Three') + + +def main(demoapp=None): + ComboboxApp(demoapp) + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/data/alphatest.png b/examples/demo/demos/data/alphatest.png new file mode 100644 index 0000000000000000000000000000000000000000..eb5885f89c0b30780fa21d823224efe0286c76b2 GIT binary patch literal 26529 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 QAAtEh(9l<}QMHNqFSy{dDF6Tf literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/apple-red.png b/examples/demo/demos/data/apple-red.png new file mode 100644 index 0000000000000000000000000000000000000000..b0a24e941859564debfe60122d4fb60ab32cacab GIT binary patch literal 3545 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 T!37SN00000NkvXXu0mjf{xHB+ literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/background.jpg b/examples/demo/demos/data/background.jpg new file mode 100644 index 0000000000000000000000000000000000000000..86c006aa466a0c5875a39087c9765b6eb79433be GIT binary patch literal 22219 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 z zCR0R9CKYtz+E*Hn;Kf}L-$)9e08Lr5QtltwOZ$aM=X4*=616k4iM{Cc@?_b5?t8SAZ_Q zk~glf4L}5y)aFqnCIk}yHDmlw1^+iHp!!Kcn}s8*@EwelLyg8^cqqBw=k`*b1)fBb zT#f#wr_OizB~CRoJRR6KV0z z$tkl#<7_L%-9Nu?7IQkfYtQRosp`nItgQZ5pQSA z$d;Lk8?~o@(w~;M=~E;H25C^6mgb=>$O@^{z9u(eb1Ff?c{vBh`oye_{aX3#ezF>L z0wVUx#DnHHniX;-K+MMvb{E8$8|6`&5vtNJB-K?qu0Y=T;gSSdpvg0su(@p_nj5KQ z)A8rE+UiN&1)J!{XmFIL#~u8uHR1eD$V>l5ay}We6@VoaTpVZh#QZ=i@GgN+vQ!=h z0x`j}(qVVb0QIsRAMB&$ABwlisC@5v;Bi$pe$)<3q4|z$O&xN+X}AR{j67MM+VYqA zL^q(U8=kau~D|3y|4TSu{I`Who3jg`sT1v^S)0{{5QuCr4di&0znQG3f>l>0r2 zKZts39r8ghIG-i))){cHP}x}w&a$Fb82#$!BfBUM=)SY>`0yq;EUB~)GrQ1X|2#6? zW&JgVCGEWTB&{|tM=N?c2>0%Ns3e#46XZn$p9f?#~Msr7*qvQ38l%Vhu__Fb&;=Snhdmv%Ya=KJs3&pQmd6%k=ih7Fv%cT z2s!ep4dd3sy_L$>??&2nDAw`uPb45X2wuhW%E^nK+*qH;XlsiI`(8-e)W4QH zo^*`Po>&W3#USM<#GQT+pj=z&{V zr^8Lf4f>_CmOL|A0>awF<7NPHA64=1jXJ`T&e@tLB_saM-j;WX^un^AV--A%PkZpg zm(=yN$|m0sm((b5HSle_)3Fgd6QNZ5?NF0zs4_ z8mg1;sx9glrl?Wsp4}XW*j!9AqdZ!iZF; zav(#C*)^F1g>w~y&wG1}p`(olNZZ?xoPs)A%l>vYh0T^>0{RE5FpWDBtJ&k8T9j7X z@m=m%aOK&X%KSl#Li8WoOkdcMnNYf zpwZzIzHyWkUp7~2i42_(OmHB8NNKDji`!DS6U~R$bf$Y~6GaB{^n#!7HXD6Euy*q2 z-8YkJv;tGJ1(36e!sI_v7@bl8pSnTJU zyJtZ4N2@Y1{P|Y;?q$AacgmjLT-?`43axBRd3^`GclVU{^@XbrX+Q!S%66Z5Y`u3U8YtW!+Aw=$^6Z> z9_>w@s7m34#L?Ig2{;?)@qb0lBJr#J-csmfDXZd2L>{ej!jS{`#bRJZ1<~si~g# z=LqvLj>c{DaINjNv|kf60BW1AuZw1PV^^*+%JfP5u~LwD|6x&Xb+Xy`)flPx_i~d z^>P0Yuzv{HKLqR_0`?C9`-g!2L%{waVE+)Xe+bw=1neII_74I3hk*S>mR54*~m!fc->mR54*~m!fc-g==YzPsy8`|K^MN?%1pRtLrc`vThm!;@BJykCzX&(-ZjKj`jSJR|MB0{?4BbU! z6mUE6nUvzUfZbH}nE-jK0e1jy2*}ku?Iu zUPd)W-rasA(x+&}(EWPgb2;~n0=DrHCL*T-r08BI`S<4i0E0)6kQ-{-FV~e7ICqS5z!B5@`#9 z4P8Wb0DcUlZGWu;oad;%W!pHn_TM22RR`EFe; z2kv#0=WWl0@DXsVsxGZiW+c*91R1)B3?_d;Z4h=+ER8>SHua#YzT+t0+*6JZ7pv;5 z3gty2ZAWO2zOLteUUkDm2!&nie(_c!yDlgItQMHR`4MB0{6Lm%RK-aK~@{xqi^|M%v4cR}cKvpEu)`S}RtDg4_O8Zb%9|4}use6^G{?$=l#oqthz=5hd zw*py_NZS)+G^+YK@IuKpuXL0x1HPA2|1ZD?CE9-z*jrU+myi*ObQDE;$NN3*aqUM> z1|H2Qo0!+jCVSm4dgzeGB4967eYjx#NTeg_F?10bAR_yT$fU%*G>a=dthsOMQ3B9UeyB0B@)Qi}c^_$T1D?#1RL2IDvZ z9tXB|$o+w;o|)2Ef8b}pk#$AmRCRg*{euyab%1XGy8xfSrXP0!_o?c;&CnqdX*MD< z9M}XqP&SE`k&7S0w!mkr>LJO!(|}zBSc3ah*%Dw#mSc&CoC!?8de(DQ^(PfPOC%D> zAvwi4RaLjI`RrxjKJ0LrViB0O6yOP9s~nkQRCP{c)QvgJQ|M;dMA_IXPfsL^j)o`Fc7TjWPa?MfIg)LLE0VJmiw*fZAo_!SXA+P{TX!$aB z{CHWbbX=8C)ywKX0N51^_MjQpHL6O{3yVgxFBW|94~}+pKy0w*Ur zyyL0!65s~lZ%G`N9P+!cV41boYruvD^*ugCUiY^c{R)jb@46OPw@C&r zz%kg!qKwWj1K$ER$!KStjCywi{R-&)Wrr?& zPZ@fmbQdrYI2-t6&I1kr&c)_2tu*Yk>-!Bb@-E5`)zhxIWXuC`8@eX?tlels#};K9 zHBuYzp?81$rV4KW{XM^ni#_$5(B}+2;0LoCwjtjNBlfxeifu7eY3Mf=&|@JmC+D6^ z9Bt+qx&k*g($KLX?%jdUJTKt>MZkz0y+;Pf+r#s_@Le4Kw@xZgL5_RU4lrdg4)`C}4I;8B+3^j_ zw4KHS*r?YRR}|!M3iN1eVB12??wyNXhgQ^+_s0Tiin=&bYu$m@}}akM!Ei!7A)44-qzI3(cj{-VeC@bv(Bcb4qYx2m#-4!uI-y03+K z$mN{_ZzuX!B z)(xt<7~8M@eNJ1aI?7x+)U%0*OX7i!$)t6olCBg2|49gx{SOVRecLPFYV$u5lKt;1>wWzOhZm09PJz+tL)IdrFa>- zi1Y)dU|w9E#4>W`X+9+9fAd~&7nW_OLe3G9y{izEyx@1wxy?7$AK2gHPmb~~%q=>N zP-9CA&d|+4gat=kD;d#tB)$)N0dQ0!6=@K6sp{Jm$jN*2Kdh>^R?tpW2rwn-waT&; zu)3psmIj0kQ=8CE&be*JjL$<2ecR+HXBw`TKo%X_tLGZJh-{9z$95tiJ&lVzL;pcV z?NrHd;M(M{Ng1+?uA3se1veFJ(`9H`k%4=y;)}?d*m^-I9Xn6=>Cu0L9opzLrmO1I zie%*l=6JrMc8cIygV>Mkc!0j-Eoc=9Nb)LnvMsdDEJObg^U6#kt>@mCjBG+!be1g2 z^yp(mWQ)A3gnTDXLl==FDfZ@THE#o_S1512obpyi-?psg`qv1)NJZG0)K_NSUWxx@> zTU?&FvdAt~OI4MLh+KyarG5Cbs!sIQ%!aHJvBlCf24Y)o+iJ0ZnUx!QO}(t4@H#1< zxr}U{s{T<#UIac_S2hqE+57@PL{`HhucR>zb9Tz2W9R6ZhAtu-6Sn=(4txk4rK)#S zEO)4*%%?Zs9Sg?b!&9F3^`NTPi^x7?`!{Tktt0$HsBF@WuA3E@n{g3bFNoDdWU#8% zdWSg;J^6PA>7CO!R<0%NPWH=d8ogf>we-n3`cc^GZKv=qmLX&WlcjZj@~iYvDGXlCnL2GT+coznL8IDZz@yYuFJ@}jq|>gdSH9= zdnEszwXN1Y$9GNnI@Zu#bM!IIQlca`0VmfD^QvYs*$Z;RL}ZYvzTd$1y-?LhMC4zA z?_}K4g(cm%Iza5=obnO4Uk0vvSu9EMFwE`I)dC^jPVb~50OLgD4Pc`T8Qmkn`Iv8d z+Guu|hzw9w*UL1E=gA+%Gz^!a*RNEz8_R&Nsp{oTXzNW!nP9zoE4SEc`7X9olI6v? z3+~0mdR)&AX0Xo%$g8av(x%VQ9bh`?hJFy)FRew`VWjSt#p_sL&wBb{dOK5lXK++9 zHE0Q`hMu@Rw)5Wk9N)~2ta--ZRJs6WiK5w<&##>~34hSx|p^lm+h7Icn=GdQH#saJ93LH%h1~?YRppXXyf*(`e=)EcpCSj)EZb2gLdOwO!va5 zrJZ(d+jSAJLn9(1u=DLM9<2NS5oG^XUr*WM3o-ONRds3IXRAz){sQJk+G_5=hUwoc zv0q!7WCi%DoZC(p5tp+?(^1tAfTOXOWf}6qQ%y{IT_3)RyCb+%mlE*o~ zKDd3~_Xie=$WvHs!MCs#_MNbGj65bL(~WwH82W5%UBz`MC(B*hW#}6^%3Mq;Q0GK! zYqva(sFq#W(54xYT4w^oTNx$O_rfy3Hu8IUGNnyd6xgE!bl(jdg1g#xC2%;QfzSOj zN4u`5!>2py<;CDnO-_QntX)Y*d*$|{`nc{2e7UOMZKkigS|Go?h@DmNbKqdL)!nv7 z?PT29*dL;!P}N!3`Sqgpr;t8}F3S^SJF|d$UhFyrH{DV8jH;&YCM0WAuHR$F&C~ND z%v{>w_NM^HX~`9J5!pyY#)`-u$+bP4pUqg-E&^%d?dx|82}2 zGB`DFfF5J9`F9oH$A?(@-d+hUsc7h4BX2a>i4uL&CZFz*;d6b21&LhKqpf!xaz^LW z{ar@A%w!Wuy=jg%Pwup#yU!o=D-C#nUy+koK0}vvuouMAfVSQN_HUrE_XqwF%W+=h zyLb;dLL}V*$>!pZO$^)vpYk_(}h{)%G1F)6(;g}(; zhZ(}+$;!+2yw)YG5RsAC zVrP|9w!M~X=l~*e0o5facvMxh0`$9dcnsL4V4L$*HTaZOB+^&#P2liDipK`4C#bi8 zp0{ODBZtlki4}=dj+ddU9sT;n2U{f4kpvB) zRdpE_X|zZpfLcZa_aP3H7N2aY&-5mECP!!ibbWp0=O<3cqHOa z(+$0+VLJ)81@Nil+O+1sH^f2^y^4h(x*M3Ts;*Qykw_$i{|DHNe=WH#LOuWh002ov JPDHLkV1iW*x>f)H literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/brick2.png b/examples/demo/demos/data/brick2.png new file mode 100644 index 0000000000000000000000000000000000000000..cfcd079f87a7575e7c6186590575da913fb1d106 GIT binary patch literal 10713 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 + + + css_accordion.css + reset.css + + + css_basics.css + reset.css + + + css_multiplebgs.css + brick.png + brick2.png + cssview.css + reset.css + + diff --git a/examples/demo/demos/data/demo.ui b/examples/demo/demos/data/demo.ui new file mode 100644 index 0000000..57dd232 --- /dev/null +++ b/examples/demo/demos/data/demo.ui @@ -0,0 +1,258 @@ + + + + + + + + + + + + John + Doe + 25 + This is the John Doe row + + + Mary + Unknown + 50 + This is the Mary Unknown row + + + + + + + + + Copy + Copy selected object into the clipboard + gtk-copy + + + + + Cut + Cut selected object into the clipboard + gtk-cut + + + + + EditMenu + _Edit + + + + + FileMenu + _File + + + + + New + Create a new file + gtk-new + + + + + Open + Open a file + gtk-open + + + + + Paste + Paste object from the Clipboard + gtk-paste + + + + + Quit + Quit the program + gtk-quit + + + + + + Save + True + Save a file + gtk-save + + + + + SaveAs + Save with a different name + gtk-save-as + + + + + HelpMenu + _Help + + + + + About + gtk-about + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GtkBuilder demo + + + + + + 250 + 440 + GtkBuilder demo + + + True + + + True + + + The menubar + + + + + False + + + + + True + + + The toolbar + + + + + False + 1 + + + + + automatic + in + True + automatic + + + True + liststore1 + 3 + + + Name list + + A list of person with name, surname and age columns + + + + + + Name + + + + 0 + + + + + + + Surname + + + + 1 + + + + + + + Age + + + + 2 + + + + + + + + + + + + 2 + + + + + True + + + False + 3 + + + + + + diff --git a/examples/demo/demos/data/floppybuddy.gif b/examples/demo/demos/data/floppybuddy.gif new file mode 100644 index 0000000000000000000000000000000000000000..ac986c8ed34cc9be1bf2e3e7d0b1ede9d0828101 GIT binary patch literal 5216 zcmeH~X*3jW7rgwtm8XB6Knx{{n*3#0_*4Eb1(b3h_J#*&F*|TTQojZ5_{CPb+Js1q8udi=lU|?uy zXk=t$Y;0^|Vq$7)3WviH2*ia87tGAe%+1X$EG#T7Ev>AqE?&HN>Cz=@Yik=D8(Ujj zJ3Bjjd;80mFFQCmT)A?^(b4hh)vHcUPS>tob9Q!iadAN+k*=<;C=|-g%?*u4V=x$Z zcXuoni^JhOJUl!-J-xiV@OV6dKp+x{-rn9mK0YK8iA*L_C=_2`Un-UA=jZ3|?;j8l zaQ*uAz`($uprGL3;E<4z(9lr8^mJHQ*v*?a!^6XAG+IPNL}X-SR8&-Sbo8xTw_;*q zVq;_D;^N}t;}a4RZr{G0n3$N9ltib~@7%eQoSdAJl9HO5nwFN9o}QkOk-=av?%ut7 z@7}%4%*?E;to!%xXJ=>U%)Y6ciK|7H$CmOeS+{>nt^JtGKwhq@<*@ zw6v_Oth~IuqN1X*va+hGs=B(mrly9?X4lr%K79DFuCA`WzW&jpM-2@Pjg5^>O-;?s z&5s{Hu4^4>X=!O~ZEb67Yj1Dw=;-L|?Ck35diwNfcXxMBPtUVw&z?Vj-rL*z;>C-; zzP|qc{+BOb4h#$o4i3J0_3HKO*F!@?!^6XG-ni6&8*Vfk7*Vlji__49E@$=`;U%!6+{{4G%b8~BJOLl9w zg^`W50nExs4XVlwoaY9}{@~dS0^9(o{5u?GfO7yo{$Bu~fV62vc1t+FDCFwXik!A6 z;iHIqrj@xJu@a{|#-CQ^b=?LV#7M)d++YADYu43nQbBhFz#`KFerNW1ngFVYCmXT9 zFH0}{`>&#vLQ}w=*iGpeH7w$RT0F^6Sy&IkrJ4t2Ggw8IWsYpB;Yf-etNJq8gV$qq zVYt?-LsKqMe0HSRZ*V|^nT)qDS8nKht9{^jb`yVZZg1ea%0QTr`-bm9t)&-Tp#6gy zLG&`$#@IumagEK2LdD7RvvUyyhNo$U!WP8HBAv1u?`8nhqDzvHH=rcLOp1yXzPCH1&A^3hhgYlAta~DoxVFCTK19L&EYyO zBAdE^i(#Z9avu~H>OhK;i$v-;Fv!^t1H~0tti;lq!*jxA^&CcWO4CfnHn03Il+>=_ANp8zg0gUpv#;PbFVt%N%hD*4-1V`ko*W)IW^G^J zRQP+Nk++i=sueK*o1Wr6;maeAeebd;SaaX+m3+S44Xz_tFzLI)Lnqo&ciK=mbD27B z@r0%{75%3ryTVf6yCo+Lxi|q8)+f#63k_XwDHcjx4FQoPUWWT;P)Fkcuc8Oy6>|M$ zBe!7u`w~6a#%}RSM>3}AT$=OKhE~m5@(5dcdVT`ahdfFEnkA7C<;v77=LGc`uCm}$r78C9LY@nBU#D2oZ~+8(izyR(pEmTt1PT8JyRT_~lzD_SSlRe)gsF$*^$Ht;YZ0OMM9D6E)X$Df-{ zu%AL`e6AnNot28IV*f}%t+&dRUswuh7pY|v?v_^UIlc6yP1r=G(gp|w$ZpbYusAZ< z1a1t*;JnaaKO`B9B$Ls2S1+_1*fjubV2Gq{C-~tg*gshYL;{&Y@KBo$=*m58jK;5z$7$* zvb~(`&~(R(cf5GVi@RiYy!e0c;?Muhi~pt#3xfVsf+FJa+dv1qdJ$2c+dA&$1tt(F zV5GM<8c71<2!HgQ3`UXANXoXk|EUnU{RgX1$p1P!5%V8C9-#XVog%QT literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnome-applets.png b/examples/demo/demos/data/gnome-applets.png new file mode 100644 index 0000000000000000000000000000000000000000..8d3549e97c0898e6bb22bf67d87c7165df8cf3e5 GIT binary patch literal 3090 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 literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnome-calendar.png b/examples/demo/demos/data/gnome-calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..889f329ae5febbc3980ebeb945939f22368d7aab GIT binary patch literal 2755 zcmV;!3Ox0RP)LD)9$o8?N0lDZsHSelm4Ut z5VtU>6Fc<(5`gHyc)iJe;s%E68-cJtCc{Ms4%*Cku?dCr%|LVn;CJ8orcz3ywIWm@ z^*~Wq3WY!j3t;wtRLi=_6S`FX#36w%;vFJk-4|u#xa1t z`rKjRg^JbG7Lm$r<8y}&b(truSicPbVMw4td=+x{*Po+MD3D5}C|)U2uhfxJw!9y- zfB@73)$;n9M3zdFPM$>0ml|FN}Ba7pd#>O|$DRKnB za1ikL7rrE3Jbak`?(T<-@m;_cARXD>Q4aV@p_C$%%y4<~GL@AI?y5_zR-<06Z}7Mb zB#!OG%7}&loCgo!1Pw!-2O9S~Q6AWkd;CeH)5xW6&tr@0GJX6wpV`{2fAr2fzW{Ou zfCiRg zWPw#+&QM5q6oEj6gsL%;V>=DvIi})79yKFPoS8r_eL(L1JV%cnWj` zPXy2Wj0X=r*r1WqqL~aNp66lD&!Z}3RNm@Q_^fEv z!w{v6(Xo;lc`aVkckkQHgp?GgC9gmB7}E0a#AThB%XpjIQi zZ~@PCab1^6p@3YT7wewXz)r)mW#fG}deZ7Z8VEm71WGlNGpZQzr7D&q~o|L$Yi(CjnQ_p`(^lS<6J)gqZgK?dJl4KyT1E1T?7-2h-cz z%ie*#tgWr#q|;b6pXKFc*6bllEd}bpwr$&}t2#aB&r_|}xq9^~ z%2Mpyy_0-k;hsIqbSg!8VS%}o70v?L?kvMY!we1#66gRS1X`nw35l53ZIIZ(|?{?UI&u+T9x=1B$_T9gagP%Rf$j}h=i|2Xx)mN!|9!svvL=a+a z=_bD;&%nR{=~NmK`CW@RAFNmsTQ|#r>!P5AMr*WVJEEcEBu=-}C~86DH$X0%Yep%P z%wXF#N!w=c&|Y%691AD@O5gA>$KQB^U5`A%NA3zUSFY0E+smFkd&uQ-SXRrY3DNYY zXak{dHWTaiWY$`v^u`^ANQ_A-H%JTu1H_b9tu>iUhSk{_db3&PM@N}HcaAe-V;ryh zWV2a@b`3E&Fi5&9O>Mo_qJlug>`|NTPIM9hrL`7;Qhe{3C-ulfkKFEFlKAP5U*YLP z50UThXL@v$CD&zCYnH>1tev1*t1>k^g*CsC(Tz!12&03-*scVfXrU(iltQojigMlO z#Jk6I+gpfgMU%Q(5Nd^EC%CK2VQVVQuHj)ZdFm8PuFF()U4K;d@B^Q)y2d4MovHD& zfNpFYC|2@&xi@_yL1y>deU5yv#3gt*CMCxpg?Iu>ss1Uh>A=SS{irC4P0!UZO) z>w3aTlk|NO2_e#{q_z{5Xe-|WwAL;9N4hs23EfEqG{9T$9?t?jzz$&09JiXqwGAXq zsk38c&IhVr+`oVOo+n1W{n2~xF+MkU`Ilp3?*iL^Uh|%^5o?|?E?Tf$1D4FvF)-uB zfv8SaWZK*q0PAMJx;b_MDPw$ejJ7=zkpqrA`sfQYV`EIt%#@B5i@!1BMG`qQ5_#GP zwJ-v1dc^>f=6XE_M0FB@_?!@xhdQtpGd${1BqAWzQwI;;SD%|3zBD_#^84}e9~Rfv zYG!#DnlUQCq*0oYSo34uGw+i$i+0QS+lfHbYB4WGhQ^~{$ADO085ucTDVO=%l`F50 zmr66n_%1W%s?qo}z=|0&qJ|%%P2IdV>bFfH(FP=F1E*pyv*UfeQkfr{nfb$CCMQn< z+s(SxGM-%ouEun}YD>~~5OMnGmQL$z%Y4Hk-}FRGqu8!JZ56%AxsJWaGJ{3{YHh-+E0(mYrU1+v&sU9c?K5pWvhC;k zwzt>Y^oNSQu>GTiwMo=;tfG`md0a5pR9lY4o7!YcUH!LTXmnd%w0{~3Z}64IjZtC6 zP)Ob6)6>n4ANx15_A#wkc@0c5>RWvD(|p33Xj{+T>cd{g?LQ#Ip01hj#P$FH002ov JPDHLkV1jV$NvHq- literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnome-foot.png b/examples/demo/demos/data/gnome-foot.png new file mode 100644 index 0000000000000000000000000000000000000000..04766585175baf1d68a0f1584dc31c1abd3315b8 GIT binary patch literal 2916 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 literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnome-gimp.png b/examples/demo/demos/data/gnome-gimp.png new file mode 100644 index 0000000000000000000000000000000000000000..f6bbc6d36c241ca25f6e43e1b8a8aa01a2e326cd GIT binary patch literal 3410 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 literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnome-gsame.png b/examples/demo/demos/data/gnome-gsame.png new file mode 100644 index 0000000000000000000000000000000000000000..01c061151f69e9e80cdcc29f4190f50dce809fca GIT binary patch literal 4263 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 JPDHLkV1n+!DZT&z literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/gnu-keys.png b/examples/demo/demos/data/gnu-keys.png new file mode 100644 index 0000000000000000000000000000000000000000..58a33770e67e60817ddbd85faeee8534d6f896fe GIT binary patch literal 3852 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 O0000x4NZZI4Rubck?rb~m)Dp;<&B@mK@` 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 literal 0 HcmV?d00001 diff --git a/examples/demo/demos/data/reset.css b/examples/demo/demos/data/reset.css new file mode 100644 index 0000000..1c27a8e --- /dev/null +++ b/examples/demo/demos/data/reset.css @@ -0,0 +1,68 @@ +/* @import this colorsheet to get the default values for every property. + * This is useful when writing special CSS tests that should not be + * inluenced by themes - not even the default ones. + * Keep in mind that the output will be very ugly and not look like + * anything GTK. + * Also, when adding new style properties, please add them here. + */ + +* { + color: inherit; + font-size: inherit; + background-color: initial; + font-family: inherit; + font-style: inherit; + font-variant: inherit; + font-weight: inherit; + text-shadow: inherit; + icon-shadow: inherit; + box-shadow: initial; + margin-top: initial; + margin-left: initial; + margin-bottom: initial; + margin-right: initial; + padding-top: initial; + padding-left: initial; + padding-bottom: initial; + padding-right: initial; + border-top-style: initial; + border-top-width: initial; + border-left-style: initial; + border-left-width: initial; + border-bottom-style: initial; + border-bottom-width: initial; + border-right-style: initial; + border-right-width: initial; + border-top-left-radius: initial; + border-top-right-radius: initial; + border-bottom-right-radius: initial; + border-bottom-left-radius: initial; + outline-style: initial; + outline-width: initial; + outline-offset: initial; + background-clip: initial; + background-origin: initial; + background-size: initial; + background-position: initial; + border-top-color: initial; + border-right-color: initial; + border-bottom-color: initial; + border-left-color: initial; + outline-color: initial; + background-repeat: initial; + background-image: initial; + border-image-source: initial; + border-image-repeat: initial; + border-image-slice: initial; + border-image-width: initial; + transition-property: initial; + transition-duration: initial; + transition-timing-function: initial; + transition-delay: initial; + engine: initial; + gtk-key-bindings: initial; + + -GtkWidget-focus-line-width: 0; + -GtkWidget-focus-padding: 0; + -GtkNotebook-initial-gap: 0; +} diff --git a/examples/demo/demos/dialogs.py b/examples/demo/demos/dialogs.py new file mode 100644 index 0000000..47d6822 --- /dev/null +++ b/examples/demo/demos/dialogs.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Dialog and Message Boxes" +description = """ +Dialog widgets are used to pop up a transient window for user feedback. +""" + +from gi.repository import Gtk + + +class DialogsApp: + def __init__(self): + self.dialog_counter = 1 + + self.window = Gtk.Window(title="Dialogs") + self.window.set_border_width(8) + self.window.connect('destroy', Gtk.main_quit) + + frame = Gtk.Frame(label="Dialogs") + self.window.add(frame) + + vbox = Gtk.VBox(spacing=8) + vbox.set_border_width(8) + frame.add(vbox) + + # Standard message dialog + hbox = Gtk.HBox(spacing=8) + vbox.pack_start(hbox, False, False, 0) + button = Gtk.Button.new_with_mnemonic("_Message Dialog") + + button.connect('clicked', + self._message_dialog_clicked) + hbox.pack_start(button, False, False, 0) + + vbox.pack_start(Gtk.HSeparator(), + False, False, 0) + + # Interactive dialog + hbox = Gtk.HBox(spacing=8) + vbox.pack_start(hbox, False, False, 0) + vbox2 = Gtk.VBox(spacing=0) + button = Gtk.Button.new_with_mnemonic("_Interactive Dialog") + + button.connect('clicked', + self._interactive_dialog_clicked) + hbox.pack_start(vbox2, False, False, 0) + vbox2.pack_start(button, False, False, 0) + + table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False) + table.set_row_spacings(4) + table.set_col_spacings(4) + hbox.pack_start(table, False, False, 0) + + label = Gtk.Label.new_with_mnemonic("_Entry 1") + table.attach_defaults(label, 0, 1, 0, 1) + + self.entry1 = Gtk.Entry() + table.attach_defaults(self.entry1, 1, 2, 0, 1) + label.set_mnemonic_widget(self.entry1) + + label = Gtk.Label.new_with_mnemonic("E_ntry 2") + + table.attach_defaults(label, 0, 1, 1, 2) + + self.entry2 = Gtk.Entry() + table.attach_defaults(self.entry2, 1, 2, 1, 2) + label.set_mnemonic_widget(self.entry2) + + self.window.show_all() + + def _interactive_dialog_clicked(self, button): + dialog = Gtk.Dialog(title='Interactive Dialog', + transient_for=self.window, + modal=True, + destroy_with_parent=True) + dialog.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, + "_Non-stock Button", Gtk.ResponseType.CANCEL) + + content_area = dialog.get_content_area() + hbox = Gtk.HBox(spacing=8) + hbox.set_border_width(8) + content_area.pack_start(hbox, False, False, 0) + + stock = Gtk.Image(stock=Gtk.STOCK_DIALOG_QUESTION, + icon_size=Gtk.IconSize.DIALOG) + + hbox.pack_start(stock, False, False, 0) + + table = Gtk.Table(n_rows=2, n_columns=2, homogeneous=False) + table.set_row_spacings(4) + table.set_col_spacings(4) + hbox.pack_start(table, True, True, 0) + label = Gtk.Label.new_with_mnemonic("_Entry 1") + table.attach_defaults(label, 0, 1, 0, 1) + local_entry1 = Gtk.Entry() + local_entry1.set_text(self.entry1.get_text()) + table.attach_defaults(local_entry1, 1, 2, 0, 1) + label.set_mnemonic_widget(local_entry1) + + label = Gtk.Label.new_with_mnemonic("E_ntry 2") + table.attach_defaults(label, 0, 1, 1, 2) + + local_entry2 = Gtk.Entry() + local_entry2.set_text(self.entry2.get_text()) + table.attach_defaults(local_entry2, 1, 2, 1, 2) + label.set_mnemonic_widget(local_entry2) + + hbox.show_all() + + response = dialog.run() + if response == Gtk.ResponseType.OK: + self.entry1.set_text(local_entry1.get_text()) + self.entry2.set_text(local_entry2.get_text()) + + dialog.destroy() + + def _message_dialog_clicked(self, button): + dialog = Gtk.MessageDialog(transient_for=self.window, + modal=True, + destroy_with_parent=True, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text="This message box has been popped up the following\nnumber of times:") + dialog.format_secondary_text('%d' % self.dialog_counter) + dialog.run() + + self.dialog_counter += 1 + dialog.destroy() + + +def main(demoapp=None): + DialogsApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/drawingarea.py b/examples/demo/demos/drawingarea.py new file mode 100644 index 0000000..b04c41d --- /dev/null +++ b/examples/demo/demos/drawingarea.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Drawing Area" +description = """ +GtkDrawingArea is a blank area where you can draw custom displays +of various kinds. + +This demo has two drawing areas. The checkerboard area shows +how you can just draw something; all you have to do is write +a signal handler for expose_event, as shown here. + +The "scribble" area is a bit more advanced, and shows how to handle +events such as button presses and mouse motion. Click the mouse +and drag in the scribble area to draw squiggles. Resize the window +to clear the area. +""" + + +import cairo + +from gi.repository import Gtk, Gdk + + +class DrawingAreaApp: + def __init__(self): + self.sureface = None + + window = Gtk.Window() + window.set_title(title) + window.connect('destroy', lambda x: Gtk.main_quit()) + window.set_border_width(8) + + vbox = Gtk.VBox(homogeneous=False, spacing=8) + window.add(vbox) + + # create checkerboard area + label = Gtk.Label() + label.set_markup('Checkerboard pattern') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + vbox.pack_start(frame, True, True, 0) + + da = Gtk.DrawingArea() + da.set_size_request(100, 100) + frame.add(da) + da.connect('draw', self.checkerboard_draw_event) + + # create scribble area + label = Gtk.Label() + label.set_markup('Scribble area') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + vbox.pack_start(frame, True, True, 0) + + da = Gtk.DrawingArea() + da.set_size_request(100, 100) + frame.add(da) + da.connect('draw', self.scribble_draw_event) + da.connect('configure-event', self.scribble_configure_event) + + # event signals + da.connect('motion-notify-event', self.scribble_motion_notify_event) + da.connect('button-press-event', self.scribble_button_press_event) + + # Ask to receive events the drawing area doesn't normally + # subscribe to + da.set_events(da.get_events() | + Gdk.EventMask.LEAVE_NOTIFY_MASK | + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.POINTER_MOTION_MASK | + Gdk.EventMask.POINTER_MOTION_HINT_MASK) + + window.show_all() + + def checkerboard_draw_event(self, da, cairo_ctx): + + # At the start of a draw handler, a clip region has been set on + # the Cairo context, and the contents have been cleared to the + # widget's background color. The docs for + # gdk_window_begin_paint_region() give more details on how this + # works. + check_size = 10 + spacing = 2 + + xcount = 0 + i = spacing + width = da.get_allocated_width() + height = da.get_allocated_height() + + while i < width: + j = spacing + ycount = xcount % 2 # start with even/odd depending on row + while j < height: + if ycount % 2: + cairo_ctx.set_source_rgb(0.45777, 0, 0.45777) + else: + cairo_ctx.set_source_rgb(1, 1, 1) + # If we're outside the clip this will do nothing. + cairo_ctx.rectangle(i, j, + check_size, + check_size) + cairo_ctx.fill() + + j += check_size + spacing + ycount += 1 + + i += check_size + spacing + xcount += 1 + + return True + + def scribble_draw_event(self, da, cairo_ctx): + + cairo_ctx.set_source_surface(self.surface, 0, 0) + cairo_ctx.paint() + + return False + + def draw_brush(self, widget, x, y): + update_rect = Gdk.Rectangle() + update_rect.x = x - 3 + update_rect.y = y - 3 + update_rect.width = 6 + update_rect.height = 6 + + # paint to the surface where we store our state + cairo_ctx = cairo.Context(self.surface) + + Gdk.cairo_rectangle(cairo_ctx, update_rect) + cairo_ctx.fill() + + widget.get_window().invalidate_rect(update_rect, False) + + def scribble_configure_event(self, da, event): + + allocation = da.get_allocation() + self.surface = da.get_window().create_similar_surface(cairo.CONTENT_COLOR, + allocation.width, + allocation.height) + + cairo_ctx = cairo.Context(self.surface) + cairo_ctx.set_source_rgb(1, 1, 1) + cairo_ctx.paint() + + return True + + def scribble_motion_notify_event(self, da, event): + if self.surface is None: # paranoia check, in case we haven't gotten a configure event + return False + + # This call is very important; it requests the next motion event. + # If you don't call gdk_window_get_pointer() you'll only get + # a single motion event. The reason is that we specified + # GDK_POINTER_MOTION_HINT_MASK to gtk_widget_set_events(). + # If we hadn't specified that, we could just use event->x, event->y + # as the pointer location. But we'd also get deluged in events. + # By requesting the next event as we handle the current one, + # we avoid getting a huge number of events faster than we + # can cope. + + (window, x, y, state) = event.window.get_pointer() + + if state & Gdk.ModifierType.BUTTON1_MASK: + self.draw_brush(da, x, y) + + return True + + def scribble_button_press_event(self, da, event): + if self.surface is None: # paranoia check, in case we haven't gotten a configure event + return False + + if event.button == 1: + self.draw_brush(da, event.x, event.y) + + return True + + +def main(demoapp=None): + DrawingAreaApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/expander.py b/examples/demo/demos/expander.py new file mode 100644 index 0000000..0ec149e --- /dev/null +++ b/examples/demo/demos/expander.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Expander" +description = """ +GtkExpander allows to provide additional content that is initially hidden. +This is also known as "disclosure triangle". +""" + +from gi.repository import Gtk + + +class ExpanderApp: + def __init__(self): + self.window = Gtk.Dialog(title="GtkExpander") + self.window.add_buttons(Gtk.STOCK_CLOSE, Gtk.ResponseType.NONE) + self.window.set_resizable(False) + self.window.connect('response', lambda window, x: window.destroy()) + self.window.connect('destroy', Gtk.main_quit) + + content_area = self.window.get_content_area() + vbox = Gtk.VBox(spacing=5) + content_area.pack_start(vbox, True, True, 0) + vbox.set_border_width(5) + + label = Gtk.Label(label='Expander demo. Click on the triangle for details.') + vbox.pack_start(label, True, True, 0) + + expander = Gtk.Expander(label='Details') + vbox.pack_start(expander, False, False, 0) + + label = Gtk.Label(label='Details can be shown or hidden') + expander.add(label) + + self.window.show_all() + + +def main(demoapp=None): + ExpanderApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/flowbox.py b/examples/demo/demos/flowbox.py new file mode 100755 index 0000000..0485b7c --- /dev/null +++ b/examples/demo/demos/flowbox.py @@ -0,0 +1,752 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2014 Gian Mario Tagliaretti +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "FlowBox" +description = """ +A FlowBox allows flexible and responsive grids which reflow as needed and +support sorting and filtering. The children of a GtkFlowBox are regular widgets. +""" + +from gi.repository import Gtk, Gdk + + +class FlowBoxApp: + def __init__(self): + window = Gtk.Window() + window.connect('destroy', lambda x: Gtk.main_quit()) + window.set_border_width(10) + window.set_default_size(600, 400) + + header = Gtk.HeaderBar(title="Flow Box") + header.set_subtitle("Sample FlowBox app") + header.props.show_close_button = True + + window.set_titlebar(header) + + scrolled = Gtk.ScrolledWindow() + scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + + flowbox = Gtk.FlowBox() + flowbox.set_valign(Gtk.Align.START) + flowbox.set_max_children_per_line(30) + flowbox.set_selection_mode(Gtk.SelectionMode.NONE) + + self.create_flowbox(flowbox) + + scrolled.add(flowbox) + + window.add(scrolled) + window.show_all() + + def color_swatch_new(self, str_color): + rgba = Gdk.RGBA() + rgba.parse(str_color) + + text_rgba = Gdk.RGBA() # default is white + if max(rgba.red, rgba.green, rgba.blue) > 0.6: + text_rgba.parse('black') + + label = Gtk.Label(label=str_color) + label.override_background_color(0, rgba) + label.override_color(0, text_rgba) + return label + + def create_flowbox(self, flowbox): + colors = [ + 'AliceBlue', + 'AntiqueWhite', + 'AntiqueWhite1', + 'AntiqueWhite2', + 'AntiqueWhite3', + 'AntiqueWhite4', + 'aqua', + 'aquamarine', + 'aquamarine1', + 'aquamarine2', + 'aquamarine3', + 'aquamarine4', + 'azure', + 'azure1', + 'azure2', + 'azure3', + 'azure4', + 'beige', + 'bisque', + 'bisque1', + 'bisque2', + 'bisque3', + 'bisque4', + 'black', + 'BlanchedAlmond', + 'blue', + 'blue1', + 'blue2', + 'blue3', + 'blue4', + 'BlueViolet', + 'brown', + 'brown1', + 'brown2', + 'brown3', + 'brown4', + 'burlywood', + 'burlywood1', + 'burlywood2', + 'burlywood3', + 'burlywood4', + 'CadetBlue', + 'CadetBlue1', + 'CadetBlue2', + 'CadetBlue3', + 'CadetBlue4', + 'chartreuse', + 'chartreuse1', + 'chartreuse2', + 'chartreuse3', + 'chartreuse4', + 'chocolate', + 'chocolate1', + 'chocolate2', + 'chocolate3', + 'chocolate4', + 'coral', + 'coral1', + 'coral2', + 'coral3', + 'coral4', + 'CornflowerBlue', + 'cornsilk', + 'cornsilk1', + 'cornsilk2', + 'cornsilk3', + 'cornsilk4', + 'crimson', + 'cyan', + 'cyan1', + 'cyan2', + 'cyan3', + 'cyan4', + 'DarkBlue', + 'DarkCyan', + 'DarkGoldenrod', + 'DarkGoldenrod1', + 'DarkGoldenrod2', + 'DarkGoldenrod3', + 'DarkGoldenrod4', + 'DarkGray', + 'DarkGreen', + 'DarkGrey', + 'DarkKhaki', + 'DarkMagenta', + 'DarkOliveGreen', + 'DarkOliveGreen1', + 'DarkOliveGreen2', + 'DarkOliveGreen3', + 'DarkOliveGreen4', + 'DarkOrange', + 'DarkOrange1', + 'DarkOrange2', + 'DarkOrange3', + 'DarkOrange4', + 'DarkOrchid', + 'DarkOrchid1', + 'DarkOrchid2', + 'DarkOrchid3', + 'DarkOrchid4', + 'DarkRed', + 'DarkSalmon', + 'DarkSeaGreen', + 'DarkSeaGreen1', + 'DarkSeaGreen2', + 'DarkSeaGreen3', + 'DarkSeaGreen4', + 'DarkSlateBlue', + 'DarkSlateGray', + 'DarkSlateGray1', + 'DarkSlateGray2', + 'DarkSlateGray3', + 'DarkSlateGray4', + 'DarkSlateGrey', + 'DarkTurquoise', + 'DarkViolet', + 'DeepPink', + 'DeepPink1', + 'DeepPink2', + 'DeepPink3', + 'DeepPink4', + 'DeepSkyBlue', + 'DeepSkyBlue1', + 'DeepSkyBlue2', + 'DeepSkyBlue3', + 'DeepSkyBlue4', + 'DimGray', + 'DimGrey', + 'DodgerBlue', + 'DodgerBlue1', + 'DodgerBlue2', + 'DodgerBlue3', + 'DodgerBlue4', + 'firebrick', + 'firebrick1', + 'firebrick2', + 'firebrick3', + 'firebrick4', + 'FloralWhite', + 'ForestGreen', + 'fuchsia', + 'gainsboro', + 'GhostWhite', + 'gold', + 'gold1', + 'gold2', + 'gold3', + 'gold4', + 'goldenrod', + 'goldenrod1', + 'goldenrod2', + 'goldenrod3', + 'goldenrod4', + 'gray', + 'gray0', + 'gray1', + 'gray2', + 'gray3', + 'gray4', + 'gray5', + 'gray6', + 'gray7', + 'gray8', + 'gray9', + 'gray10', + 'gray11', + 'gray12', + 'gray13', + 'gray14', + 'gray15', + 'gray16', + 'gray17', + 'gray18', + 'gray19', + 'gray20', + 'gray21', + 'gray22', + 'gray23', + 'gray24', + 'gray25', + 'gray26', + 'gray27', + 'gray28', + 'gray29', + 'gray30', + 'gray31', + 'gray32', + 'gray33', + 'gray34', + 'gray35', + 'gray36', + 'gray37', + 'gray38', + 'gray39', + 'gray40', + 'gray41', + 'gray42', + 'gray43', + 'gray44', + 'gray45', + 'gray46', + 'gray47', + 'gray48', + 'gray49', + 'gray50', + 'gray51', + 'gray52', + 'gray53', + 'gray54', + 'gray55', + 'gray56', + 'gray57', + 'gray58', + 'gray59', + 'gray60', + 'gray61', + 'gray62', + 'gray63', + 'gray64', + 'gray65', + 'gray66', + 'gray67', + 'gray68', + 'gray69', + 'gray70', + 'gray71', + 'gray72', + 'gray73', + 'gray74', + 'gray75', + 'gray76', + 'gray77', + 'gray78', + 'gray79', + 'gray80', + 'gray81', + 'gray82', + 'gray83', + 'gray84', + 'gray85', + 'gray86', + 'gray87', + 'gray88', + 'gray89', + 'gray90', + 'gray91', + 'gray92', + 'gray93', + 'gray94', + 'gray95', + 'gray96', + 'gray97', + 'gray98', + 'gray99', + 'gray100', + 'green', + 'green1', + 'green2', + 'green3', + 'green4', + 'GreenYellow', + 'grey', + 'grey0', + 'grey1', + 'grey2', + 'grey3', + 'grey4', + 'grey5', + 'grey6', + 'grey7', + 'grey8', + 'grey9', + 'grey10', + 'grey11', + 'grey12', + 'grey13', + 'grey14', + 'grey15', + 'grey16', + 'grey17', + 'grey18', + 'grey19', + 'grey20', + 'grey21', + 'grey22', + 'grey23', + 'grey24', + 'grey25', + 'grey26', + 'grey27', + 'grey28', + 'grey29', + 'grey30', + 'grey31', + 'grey32', + 'grey33', + 'grey34', + 'grey35', + 'grey36', + 'grey37', + 'grey38', + 'grey39', + 'grey40', + 'grey41', + 'grey42', + 'grey43', + 'grey44', + 'grey45', + 'grey46', + 'grey47', + 'grey48', + 'grey49', + 'grey50', + 'grey51', + 'grey52', + 'grey53', + 'grey54', + 'grey55', + 'grey56', + 'grey57', + 'grey58', + 'grey59', + 'grey60', + 'grey61', + 'grey62', + 'grey63', + 'grey64', + 'grey65', + 'grey66', + 'grey67', + 'grey68', + 'grey69', + 'grey70', + 'grey71', + 'grey72', + 'grey73', + 'grey74', + 'grey75', + 'grey76', + 'grey77', + 'grey78', + 'grey79', + 'grey80', + 'grey81', + 'grey82', + 'grey83', + 'grey84', + 'grey85', + 'grey86', + 'grey87', + 'grey88', + 'grey89', + 'grey90', + 'grey91', + 'grey92', + 'grey93', + 'grey94', + 'grey95', + 'grey96', + 'grey97', + 'grey98', + 'grey99', + 'grey100', + 'honeydew', + 'honeydew1', + 'honeydew2', + 'honeydew3', + 'honeydew4', + 'HotPink', + 'HotPink1', + 'HotPink2', + 'HotPink3', + 'HotPink4', + 'IndianRed', + 'IndianRed1', + 'IndianRed2', + 'IndianRed3', + 'IndianRed4', + 'indigo', + 'ivory', + 'ivory1', + 'ivory2', + 'ivory3', + 'ivory4', + 'khaki', + 'khaki1', + 'khaki2', + 'khaki3', + 'khaki4', + 'lavender', + 'LavenderBlush', + 'LavenderBlush1', + 'LavenderBlush2', + 'LavenderBlush3', + 'LavenderBlush4', + 'LawnGreen', + 'LemonChiffon', + 'LemonChiffon1', + 'LemonChiffon2', + 'LemonChiffon3', + 'LemonChiffon4', + 'LightBlue', + 'LightBlue1', + 'LightBlue2', + 'LightBlue3', + 'LightBlue4', + 'LightCoral', + 'LightCyan', + 'LightCyan1', + 'LightCyan2', + 'LightCyan3', + 'LightCyan4', + 'LightGoldenrod', + 'LightGoldenrod1', + 'LightGoldenrod2', + 'LightGoldenrod3', + 'LightGoldenrod4', + 'LightGoldenrodYellow', + 'LightGray', + 'LightGreen', + 'LightGrey', + 'LightPink', + 'LightPink1', + 'LightPink2', + 'LightPink3', + 'LightPink4', + 'LightSalmon', + 'LightSalmon1', + 'LightSalmon2', + 'LightSalmon3', + 'LightSalmon4', + 'LightSeaGreen', + 'LightSkyBlue', + 'LightSkyBlue1', + 'LightSkyBlue2', + 'LightSkyBlue3', + 'LightSkyBlue4', + 'LightSlateBlue', + 'LightSlateGray', + 'LightSlateGrey', + 'LightSteelBlue', + 'LightSteelBlue1', + 'LightSteelBlue2', + 'LightSteelBlue3', + 'LightSteelBlue4', + 'LightYellow', + 'LightYellow1', + 'LightYellow2', + 'LightYellow3', + 'LightYellow4', + 'lime', + 'LimeGreen', + 'linen', + 'magenta', + 'magenta1', + 'magenta2', + 'magenta3', + 'magenta4', + 'maroon', + 'maroon1', + 'maroon2', + 'maroon3', + 'maroon4', + 'MediumAquamarine', + 'MediumBlue', + 'MediumOrchid', + 'MediumOrchid1', + 'MediumOrchid2', + 'MediumOrchid3', + 'MediumOrchid4', + 'MediumPurple', + 'MediumPurple1', + 'MediumPurple2', + 'MediumPurple3', + 'MediumPurple4', + 'MediumSeaGreen', + 'MediumSlateBlue', + 'MediumSpringGreen', + 'MediumTurquoise', + 'MediumVioletRed', + 'MidnightBlue', + 'MintCream', + 'MistyRose', + 'MistyRose1', + 'MistyRose2', + 'MistyRose3', + 'MistyRose4', + 'moccasin', + 'NavajoWhite', + 'NavajoWhite1', + 'NavajoWhite2', + 'NavajoWhite3', + 'NavajoWhite4', + 'navy', + 'NavyBlue', + 'OldLace', + 'olive', + 'OliveDrab', + 'OliveDrab1', + 'OliveDrab2', + 'OliveDrab3', + 'OliveDrab4', + 'orange', + 'orange1', + 'orange2', + 'orange3', + 'orange4', + 'OrangeRed', + 'OrangeRed1', + 'OrangeRed2', + 'OrangeRed3', + 'OrangeRed4', + 'orchid', + 'orchid1', + 'orchid2', + 'orchid3', + 'orchid4', + 'PaleGoldenrod', + 'PaleGreen', + 'PaleGreen1', + 'PaleGreen2', + 'PaleGreen3', + 'PaleGreen4', + 'PaleTurquoise', + 'PaleTurquoise1', + 'PaleTurquoise2', + 'PaleTurquoise3', + 'PaleTurquoise4', + 'PaleVioletRed', + 'PaleVioletRed1', + 'PaleVioletRed2', + 'PaleVioletRed3', + 'PaleVioletRed4', + 'PapayaWhip', + 'PeachPuff', + 'PeachPuff1', + 'PeachPuff2', + 'PeachPuff3', + 'PeachPuff4', + 'peru', + 'pink', + 'pink1', + 'pink2', + 'pink3', + 'pink4', + 'plum', + 'plum1', + 'plum2', + 'plum3', + 'plum4', + 'PowderBlue', + 'purple', + 'purple1', + 'purple2', + 'purple3', + 'purple4', + 'red', + 'red1', + 'red2', + 'red3', + 'red4', + 'RosyBrown', + 'RosyBrown1', + 'RosyBrown2', + 'RosyBrown3', + 'RosyBrown4', + 'RoyalBlue', + 'RoyalBlue1', + 'RoyalBlue2', + 'RoyalBlue3', + 'RoyalBlue4', + 'SaddleBrown', + 'salmon', + 'salmon1', + 'salmon2', + 'salmon3', + 'salmon4', + 'SandyBrown', + 'SeaGreen', + 'SeaGreen1', + 'SeaGreen2', + 'SeaGreen3', + 'SeaGreen4', + 'seashell', + 'seashell1', + 'seashell2', + 'seashell3', + 'seashell4', + 'sienna', + 'sienna1', + 'sienna2', + 'sienna3', + 'sienna4', + 'silver', + 'SkyBlue', + 'SkyBlue1', + 'SkyBlue2', + 'SkyBlue3', + 'SkyBlue4', + 'SlateBlue', + 'SlateBlue1', + 'SlateBlue2', + 'SlateBlue3', + 'SlateBlue4', + 'SlateGray', + 'SlateGray1', + 'SlateGray2', + 'SlateGray3', + 'SlateGray4', + 'SlateGrey', + 'snow', + 'snow1', + 'snow2', + 'snow3', + 'snow4', + 'SpringGreen', + 'SpringGreen1', + 'SpringGreen2', + 'SpringGreen3', + 'SpringGreen4', + 'SteelBlue', + 'SteelBlue1', + 'SteelBlue2', + 'SteelBlue3', + 'SteelBlue4', + 'tan', + 'tan1', + 'tan2', + 'tan3', + 'tan4', + 'teal', + 'thistle', + 'thistle1', + 'thistle2', + 'thistle3', + 'thistle4', + 'tomato', + 'tomato1', + 'tomato2', + 'tomato3', + 'tomato4', + 'turquoise', + 'turquoise1', + 'turquoise2', + 'turquoise3', + 'turquoise4', + 'violet', + 'VioletRed', + 'VioletRed1', + 'VioletRed2', + 'VioletRed3', + 'VioletRed4', + 'wheat', + 'wheat1', + 'wheat2', + 'wheat3', + 'wheat4', + 'white', + 'WhiteSmoke', + 'yellow', + 'yellow1', + 'yellow2', + 'yellow3', + 'yellow4', + 'YellowGreen', + ] + + for color in colors: + button = self.color_swatch_new(color) + flowbox.add(button) + + +def main(demoapp=None): + FlowBoxApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/images.py b/examples/demo/demos/images.py new file mode 100644 index 0000000..80c99af --- /dev/null +++ b/examples/demo/demos/images.py @@ -0,0 +1,305 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Images" +description = """GtkImage is used to display an image; the image can be in a +number of formats. Typically, you load an image into a GdkPixbuf, then display +the pixbuf. This demo code shows some of the more obscure cases, in the simple +case a call to gtk_image_new_from_file() is all you need. +""" + +import os +from os import path + +from gi.repository import Gtk, Gdk, GdkPixbuf, GLib, Gio, GObject + + +class ImagesApp: + def __init__(self): + self.pixbuf_loader = None + self.image_stream = None + + self.window = Gtk.Window(title="Images") + self.window.connect('destroy', self.cleanup_cb) + self.window.set_border_width(8) + + vbox = Gtk.VBox(spacing=8) + vbox.set_border_width(8) + self.window.add(vbox) + + label = Gtk.Label() + label.set_markup('Image loaded from file') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + + # The alignment keeps the frame from growing when users resize + # the window + align = Gtk.Alignment(xalign=0.5, + yalign=0.5, + xscale=0, + yscale=0) + align.add(frame) + vbox.pack_start(align, False, False, 0) + + self.base_path = os.path.abspath(os.path.dirname(__file__)) + self.base_path = os.path.join(self.base_path, 'data') + filename = os.path.join(self.base_path, 'gtk-logo-rgb.gif') + pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename) + transparent = pixbuf.add_alpha(True, 0xff, 0xff, 0xff) + image = Gtk.Image.new_from_pixbuf(transparent) + frame.add(image) + + # Animation + + label = Gtk.Label() + label.set_markup('Animation loaded from file') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + + # The alignment keeps the frame from growing when users resize + # the window + align = Gtk.Alignment(xalign=0.5, + yalign=0.5, + xscale=0, + yscale=0) + align.add(frame) + vbox.pack_start(align, False, False, 0) + + img_path = path.join(self.base_path, 'floppybuddy.gif') + image = Gtk.Image.new_from_file(img_path) + frame.add(image) + + # Symbolic icon + + label = Gtk.Label() + label.set_markup('Symbolic themed icon') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + + # The alignment keeps the frame from growing when users resize + # the window + align = Gtk.Alignment(xalign=0.5, + yalign=0.5, + xscale=0, + yscale=0) + align.add(frame) + vbox.pack_start(align, False, False, 0) + + gicon = Gio.ThemedIcon.new_with_default_fallbacks('battery-caution-charging-symbolic') + image = Gtk.Image.new_from_gicon(gicon, Gtk.IconSize.DIALOG) + frame.add(image) + + # progressive + + label = Gtk.Label() + label.set_markup('Progressive image loading') + vbox.pack_start(label, False, False, 0) + + frame = Gtk.Frame() + frame.set_shadow_type(Gtk.ShadowType.IN) + + # The alignment keeps the frame from growing when users resize + # the window + align = Gtk.Alignment(xalign=0.5, + yalign=0.5, + xscale=0, + yscale=0) + align.add(frame) + vbox.pack_start(align, False, False, 0) + + image = Gtk.Image.new_from_pixbuf(None) + frame.add(image) + + self.start_progressive_loading(image) + + # Sensistivity control + button = Gtk.ToggleButton.new_with_mnemonic('_Insensitive') + button.connect('toggled', self.toggle_sensitivity_cb, vbox) + vbox.pack_start(button, False, False, 0) + + self.window.show_all() + + def toggle_sensitivity_cb(self, button, container): + widget_list = container.get_children() + for w in widget_list: + if w != button: + w.set_sensitive(not button.get_active()) + + return True + + def progressive_timeout(self, image): + # This shows off fully-paranoid error handling, so looks scary. + # You could factor out the error handling code into a nice separate + # function to make things nicer. + + if self.image_stream: + try: + buf = self.image_stream.read(256) + except IOError as e: + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + "Failure reading image file 'alphatest.png': %s" % (str(e), )) + + self.image_stream.close() + self.image_stream = None + self.load_timeout = 0 + + dialog.show() + dialog.connect('response', lambda x, y: dialog.destroy()) + + return False # uninstall the timeout + + try: + self.pixbuf_loader.write(buf) + + except GObject.GError as e: + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + e.message) + + self.image_stream.close() + self.image_stream = None + self.load_timeout = 0 + + dialog.show() + dialog.connect('response', lambda x, y: dialog.destroy()) + + return False # uninstall the timeout + + if len(buf) < 256: # eof + self.image_stream.close() + self.image_stream = None + + # Errors can happen on close, e.g. if the image + # file was truncated we'll know on close that + # it was incomplete. + try: + self.pixbuf_loader.close() + except GObject.GError as e: + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + 'Failed to load image: %s' % e.message) + + self.load_timeout = 0 + + dialog.show() + dialog.connect('response', lambda x, y: dialog.destroy()) + + return False # uninstall the timeout + else: + img_path = path.join(self.base_path, 'alphatest.png') + try: + self.image_stream = open(img_path, 'rb') + except IOError as e: + dialog = Gtk.MessageDialog(self.window, + Gtk.DialogFlags.DESTROY_WITH_PARENT, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + str(e)) + self.load_timeout = 0 + dialog.show() + dialog.connect('response', lambda x, y: dialog.destroy()) + + return False # uninstall the timeout + + if self.pixbuf_loader: + try: + self.pixbuf_loader.close() + except GObject.GError: + pass + self.pixbuf_loader = None + + self.pixbuf_loader = GdkPixbuf.PixbufLoader() + + self.pixbuf_loader.connect('area-prepared', + self.progressive_prepared_callback, + image) + + self.pixbuf_loader.connect('area-updated', + self.progressive_updated_callback, + image) + # leave timeout installed + return True + + def progressive_prepared_callback(self, loader, image): + pixbuf = loader.get_pixbuf() + # Avoid displaying random memory contents, since the pixbuf + # isn't filled in yet. + pixbuf.fill(0xaaaaaaff) + image.set_from_pixbuf(pixbuf) + + def progressive_updated_callback(self, loader, x, y, width, height, image): + # We know the pixbuf inside the GtkImage has changed, but the image + # itself doesn't know this; so queue a redraw. If we wanted to be + # really efficient, we could use a drawing area or something + # instead of a GtkImage, so we could control the exact position of + # the pixbuf on the display, then we could queue a draw for only + # the updated area of the image. + image.queue_draw() + + def start_progressive_loading(self, image): + # This is obviously totally contrived (we slow down loading + # on purpose to show how incremental loading works). + # The real purpose of incremental loading is the case where + # you are reading data from a slow source such as the network. + # The timeout simply simulates a slow data source by inserting + # pauses in the reading process. + + self.load_timeout = Gdk.threads_add_timeout(150, + GLib.PRIORITY_DEFAULT_IDLE, + self.progressive_timeout, + image) + + def cleanup_cb(self, widget): + if self.load_timeout: + GLib.source_remove(self.load_timeout) + + if self.pixbuf_loader: + try: + self.pixbuf_loader.close() + except GObject.GError: + pass + + if self.image_stream: + self.image_stream.close() + + Gtk.main_quit() + + +def main(demoapp=None): + ImagesApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/infobars.py b/examples/demo/demos/infobars.py new file mode 100644 index 0000000..b8a1dc7 --- /dev/null +++ b/examples/demo/demos/infobars.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Info Bars" +description = """ +Info bar widgets are used to report important messages to the user. +""" + +from gi.repository import Gtk + + +class InfobarApp: + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Info Bars') + self.window.set_border_width(8) + self.window.connect('destroy', Gtk.main_quit) + + vbox = Gtk.VBox(spacing=0) + self.window.add(vbox) + + bar = Gtk.InfoBar() + vbox.pack_start(bar, False, False, 0) + bar.set_message_type(Gtk.MessageType.INFO) + label = Gtk.Label(label='This is an info bar with message type Gtk.MessageType.INFO') + bar.get_content_area().pack_start(label, False, False, 0) + + bar = Gtk.InfoBar() + vbox.pack_start(bar, False, False, 0) + bar.set_message_type(Gtk.MessageType.WARNING) + label = Gtk.Label(label='This is an info bar with message type Gtk.MessageType.WARNING') + bar.get_content_area().pack_start(label, False, False, 0) + + bar = Gtk.InfoBar() + bar.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK) + bar.connect('response', self.on_bar_response) + vbox.pack_start(bar, False, False, 0) + bar.set_message_type(Gtk.MessageType.QUESTION) + label = Gtk.Label(label='This is an info bar with message type Gtk.MessageType.QUESTION') + bar.get_content_area().pack_start(label, False, False, 0) + + bar = Gtk.InfoBar() + vbox.pack_start(bar, False, False, 0) + bar.set_message_type(Gtk.MessageType.ERROR) + label = Gtk.Label(label='This is an info bar with message type Gtk.MessageType.ERROR') + bar.get_content_area().pack_start(label, False, False, 0) + + bar = Gtk.InfoBar() + vbox.pack_start(bar, False, False, 0) + bar.set_message_type(Gtk.MessageType.OTHER) + label = Gtk.Label(label='This is an info bar with message type Gtk.MessageType.OTHER') + bar.get_content_area().pack_start(label, False, False, 0) + + frame = Gtk.Frame(label="Info bars") + vbox.pack_start(frame, False, False, 8) + + vbox2 = Gtk.VBox(spacing=8) + vbox2.set_border_width(8) + frame.add(vbox2) + + # Standard message dialog + label = Gtk.Label(label='An example of different info bars') + vbox2.pack_start(label, False, False, 0) + + self.window.show_all() + + def on_bar_response(self, info_bar, response_id): + dialog = Gtk.MessageDialog(transient_for=self.window, + modal=True, + destroy_with_parent=True, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text='You clicked on an info bar') + dialog.format_secondary_text('Your response has id %d' % response_id) + dialog.run() + dialog.destroy() + + +def main(demoapp=None): + InfobarApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/links.py b/examples/demo/demos/links.py new file mode 100644 index 0000000..c6d331d --- /dev/null +++ b/examples/demo/demos/links.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Links" +description = """ +GtkLabel can show hyperlinks. The default action is to call gtk_show_uri() on +their URI, but it is possible to override this with a custom handler. +""" + +from gi.repository import Gtk + + +class LinksApp: + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Links') + self.window.set_border_width(12) + self.window.connect('destroy', Gtk.main_quit) + + label = Gtk.Label(label="""Some text may be marked up +as hyperlinks, which can be clicked +or activated via keynav""") + + label.set_use_markup(True) + label.connect("activate-link", self.activate_link) + self.window.add(label) + label.show() + + self.window.show() + + def activate_link(self, label, uri): + if uri == 'keynav': + parent = label.get_toplevel() + markup = """The term keynav is a shorthand for +keyboard navigation and refers to the process of using +a program (exclusively) via keyboard input.""" + dialog = Gtk.MessageDialog(transient_for=parent, + destroy_with_parent=True, + message_type=Gtk.MessageType.INFO, + buttons=Gtk.ButtonsType.OK, + text=markup, + use_markup=True) + dialog.present() + dialog.connect('response', self.response_cb) + + return True + + def response_cb(self, dialog, response_id): + dialog.destroy() + + +def main(demoapp=None): + LinksApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/menus.py b/examples/demo/demos/menus.py new file mode 100644 index 0000000..7cf2e57 --- /dev/null +++ b/examples/demo/demos/menus.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Menus" +description = """There are several widgets involved in displaying menus. The +GtkMenuBar widget is a menu bar, which normally appears horizontally at the top +of an application, but can also be layed out vertically. The GtkMenu widget is +the actual menu that pops up. Both GtkMenuBar and GtkMenu are subclasses of +GtkMenuShell; a GtkMenuShell contains menu items (GtkMenuItem). Each menu item +contains text and/or images and can be selected by the user. There are several +kinds of menu item, including plain GtkMenuItem, GtkCheckMenuItem which can be +checked/unchecked, GtkRadioMenuItem which is a check menu item that's in a +mutually exclusive group, GtkSeparatorMenuItem which is a separator bar, +GtkTearoffMenuItem which allows a GtkMenu to be torn off, and GtkImageMenuItem +which can place a GtkImage or other widget next to the menu text. A GtkMenuItem +can have a submenu, which is simply a GtkMenu to pop up when the menu item is +selected. Typically, all menu items in a menu bar have submenus. GtkUIManager +provides a higher-level interface for creating menu bars and menus; while you +can construct menus manually, most people don't do that. There's a separate demo +for GtkUIManager. +""" + +from gi.repository import Gtk + + +class MenusApp: + def __init__(self): + self.window = Gtk.Window() + self.window.set_title('Menus') + self.window.connect('destroy', Gtk.main_quit) + + accel_group = Gtk.AccelGroup() + self.window.add_accel_group(accel_group) + self.window.set_border_width(0) + + box = Gtk.HBox() + self.window.add(box) + + box1 = Gtk.VBox() + box.add(box1) + + menubar = Gtk.MenuBar() + box1.pack_start(menubar, False, True, 0) + + menuitem = Gtk.MenuItem(label='test\nline2') + menuitem.set_submenu(self.create_menu(3, True)) + menubar.append(menuitem) + + menuitem = Gtk.MenuItem(label='foo') + menuitem.set_submenu(self.create_menu(4, True)) + menuitem.set_right_justified(True) + menubar.append(menuitem) + + box2 = Gtk.VBox(spacing=10) + box2.set_border_width(10) + box1.pack_start(box2, False, True, 0) + + button = Gtk.Button(label='Flip') + button.connect('clicked', self.change_orientation, menubar) + box2.pack_start(button, True, True, 0) + + button = Gtk.Button(label='Close') + button.connect('clicked', lambda x: self.window.destroy()) + box2.pack_start(button, True, True, 0) + button.set_can_default(True) + + self.window.show_all() + + def create_menu(self, depth, tearoff): + if depth < 1: + return None + + menu = Gtk.Menu() + + if tearoff: + menuitem = Gtk.TearoffMenuItem() + menu.append(menuitem) + + i = 0 + j = 1 + while i < 5: + label = 'item %2d - %d' % (depth, j) + # we should be adding this to a group but the group API + # isn't bindable - we need something more like the + # Gtk.RadioAction API + menuitem = Gtk.RadioMenuItem(label=label) + menu.append(menuitem) + + if i == 3: + menuitem.set_sensitive(False) + + menuitem.set_submenu(self.create_menu(depth - 1, True)) + + i += 1 + j += 1 + + return menu + + def change_orientation(self, button, menubar): + parent = menubar.get_parent() + orientation = parent.get_orientation() + parent.set_orientation(1 - orientation) + + if orientation == Gtk.Orientation.VERTICAL: + menubar.props.pack_direction = Gtk.PackDirection.TTB + else: + menubar.props.pack_direction = Gtk.PackDirection.LTR + + +def main(demoapp=None): + MenusApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/pickers.py b/examples/demo/demos/pickers.py new file mode 100644 index 0000000..d8a9c75 --- /dev/null +++ b/examples/demo/demos/pickers.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Pickers" +description = """These widgets are mainly intended for use in preference +dialogs. They allow to select colors, fonts, files and directories. +""" + +from gi.repository import Gtk + + +class PickersApp: + def __init__(self): + self.window = Gtk.Window(title='Pickers') + self.window.connect('destroy', Gtk.main_quit) + self.window.set_border_width(10) + + table = Gtk.Table(n_rows=4, n_columns=2, homogeneous=False) + table.set_col_spacing(0, 10) + table.set_row_spacings(3) + self.window.add(table) + table.set_border_width(10) + + label = Gtk.Label(label='Color:') + label.set_alignment(0.0, 0.5) + picker = Gtk.ColorButton() + table.attach_defaults(label, 0, 1, 0, 1) + table.attach_defaults(picker, 1, 2, 0, 1) + + label = Gtk.Label(label='Font:') + label.set_alignment(0.0, 0.5) + picker = Gtk.FontButton() + table.attach_defaults(label, 0, 1, 1, 2) + table.attach_defaults(picker, 1, 2, 1, 2) + + label = Gtk.Label(label='File:') + label.set_alignment(0.0, 0.5) + picker = Gtk.FileChooserButton.new('Pick a File', + Gtk.FileChooserAction.OPEN) + table.attach_defaults(label, 0, 1, 2, 3) + table.attach_defaults(picker, 1, 2, 2, 3) + + label = Gtk.Label(label='Folder:') + label.set_alignment(0.0, 0.5) + picker = Gtk.FileChooserButton.new('Pick a Folder', + Gtk.FileChooserAction.SELECT_FOLDER) + table.attach_defaults(label, 0, 1, 3, 4) + table.attach_defaults(picker, 1, 2, 3, 4) + + self.window.show_all() + + +def main(demoapp=None): + PickersApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/pixbuf.py b/examples/demo/demos/pixbuf.py new file mode 100644 index 0000000..c213584 --- /dev/null +++ b/examples/demo/demos/pixbuf.py @@ -0,0 +1,184 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Pixbufs" +description = """A GdkPixbuf represents an image, normally in RGB or RGBA +format. Pixbufs are normally used to load files from disk and perform image +scaling. It also shows off how to use GtkDrawingArea to do a simple animation. +Look at the Image demo for additional pixbuf usage examples. +""" + +from gi.repository import Gtk, Gdk, GdkPixbuf, GLib +import os +import math + + +class PixbufApp: + FRAME_DELAY = 50 + BACKGROUND_NAME = "background.jpg" + CYCLE_LEN = 60 + + def __init__(self): + self.background_width = 0 + self.background_height = 0 + self.background_pixbuf = None + self.frame = None + self.frame_num = 0 + self.pixbufs = [] + self.image_names = [ + "apple-red.png", + "gnome-applets.png", + "gnome-calendar.png", + "gnome-foot.png", + "gnome-gmush.png", + "gnome-gimp.png", + "gnome-gsame.png", + "gnu-keys.png" + ] + + self.window = Gtk.Window(title="Pixbufs") + self.window.set_resizable(False) + self.window.connect('destroy', self.cleanup_cb) + + try: + self.load_pixbufs() + except GLib.Error as e: + dialog = Gtk.MessageDialog(None, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + e.message) + + dialog.run() + dialog.destroy() + Gtk.main_quit() + + self.window.set_size_request(self.background_width, + self.background_height) + self.frame = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, + False, + 8, + self.background_width, + self.background_height) + self.da = Gtk.DrawingArea() + self.da.connect('draw', self.draw_cb) + self.window.add(self.da) + self.timeout_id = GLib.timeout_add(self.FRAME_DELAY, self.timeout_cb) + self.window.show_all() + + def load_pixbufs(self): + base_path = os.path.abspath(os.path.dirname(__file__)) + base_path = os.path.join(base_path, 'data') + img_path = os.path.join(base_path, self.BACKGROUND_NAME) + self.background_pixbuf = GdkPixbuf.Pixbuf.new_from_file(img_path) + self.background_height = self.background_pixbuf.get_height() + self.background_width = self.background_pixbuf.get_width() + + for img_name in self.image_names: + img_path = os.path.join(base_path, img_name) + self.pixbufs.append(GdkPixbuf.Pixbuf.new_from_file(img_path)) + + def draw_cb(self, da, cairo_ctx): + Gdk.cairo_set_source_pixbuf(cairo_ctx, self.frame, 0, 0) + cairo_ctx.paint() + + return True + + def timeout_cb(self): + self.background_pixbuf.copy_area(0, 0, + self.background_width, + self.background_height, + self.frame, + 0, 0) + + f = float(self.frame_num % self.CYCLE_LEN) / self.CYCLE_LEN + + xmid = self.background_width / 2.0 + ymid = self.background_height / 2.0 + radius = min(xmid, ymid) / 2.0 + + r1 = Gdk.Rectangle() + r2 = Gdk.Rectangle() + + i = 0 + for pb in self.pixbufs: + i += 1 + ang = 2.0 * math.pi * i / len(self.pixbufs) - f * 2.0 * math.pi + + iw = pb.get_width() + ih = pb.get_height() + + r = radius + (radius / 3.0) * math.sin(f * 2.0 * math.pi) + + xpos = math.floor(xmid + r * math.cos(ang) - iw / 2.0 + 0.5) + ypos = math.floor(ymid + r * math.sin(ang) - ih / 2.0 + 0.5) + + if i & 1: + k = math.sin(f * 2.0 * math.pi) + else: + k = math.cos(f * 2.0 * math.pi) + + k = 2.0 * k * k + k = max(0.25, k) + + r1.x = xpos + r1.y = ypos + r1.width = iw * k + r1.height = ih * k + + r2.x = 0 + r2.y = 0 + r2.width = self.background_width + r2.height = self.background_height + + success, dest = Gdk.rectangle_intersect(r1, r2) + if success: + alpha = 0 + if i & 1: + alpha = max(127, math.fabs(255 * math.sin(f * 2.0 * math.pi))) + else: + alpha = max(127, math.fabs(255 * math.cos(f * 2.0 * math.pi))) + + pb.composite(self.frame, + dest.x, dest.y, + dest.width, dest.height, + xpos, ypos, + k, k, + GdkPixbuf.InterpType.NEAREST, + alpha) + + self.da.queue_draw() + + self.frame_num += 1 + return True + + def cleanup_cb(self, widget): + GLib.source_remove(self.timeout_id) + Gtk.main_quit() + + +def main(demoapp=None): + PixbufApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/printing.py b/examples/demo/demos/printing.py new file mode 100644 index 0000000..6e68037 --- /dev/null +++ b/examples/demo/demos/printing.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Printing" +description = """ +GtkPrintOperation offers a simple API to support printing in a cross-platform way. +""" + +from gi.repository import Gtk, GLib, Pango, PangoCairo +import math +import os + + +class PrintingApp: + HEADER_HEIGHT = 10 * 72 / 25.4 + HEADER_GAP = 3 * 72 / 25.4 + + def __init__(self): + self.operation = Gtk.PrintOperation() + print_data = {'filename': os.path.abspath(__file__), + 'font_size': 12.0, + 'lines_per_page': 0, + 'lines': None, + 'num_lines': 0, + 'num_pages': 0 + } + + self.operation.connect('begin-print', self.begin_print, print_data) + self.operation.connect('draw-page', self.draw_page, print_data) + self.operation.connect('end-print', self.end_print, print_data) + + self.operation.set_use_full_page(False) + self.operation.set_unit(Gtk.Unit.POINTS) + self.operation.set_embed_page_setup(True) + + settings = Gtk.PrintSettings() + + dir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOCUMENTS) + if dir is None: + dir = GLib.get_home_dir() + if settings.get(Gtk.PRINT_SETTINGS_OUTPUT_FILE_FORMAT) == 'ps': + ext = '.ps' + elif settings.get(Gtk.PRINT_SETTINGS_OUTPUT_FILE_FORMAT) == 'svg': + ext = '.svg' + else: + ext = '.pdf' + + uri = "file://%s/gtk-demo%s" % (dir, ext) + settings.set(Gtk.PRINT_SETTINGS_OUTPUT_URI, uri) + self.operation.set_print_settings(settings) + + def run(self, parent=None): + result = self.operation.run(Gtk.PrintOperationAction.PRINT_DIALOG, parent) + + if result == Gtk.PrintOperationResult.ERROR: + message = self.operation.get_error() + + dialog = Gtk.MessageDialog(parent, + 0, + Gtk.MessageType.ERROR, + Gtk.ButtonsType.CLOSE, + message) + + dialog.run() + dialog.destroy() + + Gtk.main_quit() + + def begin_print(self, operation, print_ctx, print_data): + height = print_ctx.get_height() - self.HEADER_HEIGHT - self.HEADER_GAP + print_data['lines_per_page'] = \ + math.floor(height / print_data['font_size']) + + file_path = print_data['filename'] + if not os.path.isfile(file_path): + file_path = os.path.join('demos', file_path) + if not os.path.isfile: + raise Exception("file not found: " % (file_path, )) + + # in reality you should most likely not read the entire + # file into a buffer + source_file = open(file_path, 'r') + s = source_file.read() + print_data['lines'] = s.split('\n') + + print_data['num_lines'] = len(print_data['lines']) + print_data['num_pages'] = \ + (print_data['num_lines'] - 1) / print_data['lines_per_page'] + 1 + + operation.set_n_pages(print_data['num_pages']) + + def draw_page(self, operation, print_ctx, page_num, print_data): + cr = print_ctx.get_cairo_context() + width = print_ctx.get_width() + + cr.rectangle(0, 0, width, self.HEADER_HEIGHT) + cr.set_source_rgb(0.8, 0.8, 0.8) + cr.fill_preserve() + + cr.set_source_rgb(0, 0, 0) + cr.set_line_width(1) + cr.stroke() + + layout = print_ctx.create_pango_layout() + desc = Pango.FontDescription('sans 14') + layout.set_font_description(desc) + + layout.set_text(print_data['filename'], -1) + (text_width, text_height) = layout.get_pixel_size() + + if text_width > width: + layout.set_width(width) + layout.set_ellipsize(Pango.EllipsizeMode.START) + (text_width, text_height) = layout.get_pixel_size() + + cr.move_to((width - text_width) / 2, + (self.HEADER_HEIGHT - text_height) / 2) + PangoCairo.show_layout(cr, layout) + + page_str = "%d/%d" % (page_num + 1, print_data['num_pages']) + layout.set_text(page_str, -1) + + layout.set_width(-1) + (text_width, text_height) = layout.get_pixel_size() + cr.move_to(width - text_width - 4, + (self.HEADER_HEIGHT - text_height) / 2) + PangoCairo.show_layout(cr, layout) + + layout = print_ctx.create_pango_layout() + + desc = Pango.FontDescription('monospace') + desc.set_size(print_data['font_size'] * Pango.SCALE) + layout.set_font_description(desc) + + cr.move_to(0, self.HEADER_HEIGHT + self.HEADER_GAP) + lines_pp = int(print_data['lines_per_page']) + num_lines = print_data['num_lines'] + data_lines = print_data['lines'] + font_size = print_data['font_size'] + line = page_num * lines_pp + + for i in range(lines_pp): + if line >= num_lines: + break + + layout.set_text(data_lines[line], -1) + PangoCairo.show_layout(cr, layout) + cr.rel_move_to(0, font_size) + line += 1 + + def end_print(self, operation, print_ctx, print_data): + pass + + +def main(demoapp=None): + app = PrintingApp() + GLib.idle_add(app.run, demoapp.window) + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/rotatedtext.py b/examples/demo/demos/rotatedtext.py new file mode 100644 index 0000000..d47b1cf --- /dev/null +++ b/examples/demo/demos/rotatedtext.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python +# coding=utf-8 +# -*- Mode: Python; py-indent-offset: 4 -*- +# vim: tabstop=4 shiftwidth=4 expandtab +# +# Copyright (C) 2010 Red Hat, Inc., John (J5) Palmieri +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA + +title = "Rotated Text" +description = """This demo shows how to use PangoCairo to draw rotated and +transformed text. The right pane shows a rotated GtkLabel widget. In both +cases, a custom PangoCairo shape renderer is installed to draw a red heard using +cairo drawing operations instead of the Unicode heart character. +""" + +from gi.repository import Gtk, Pango, PangoCairo, Gdk +import cairo +import sys +import math + +# Python 2 and 3 handle UTF8 differently +if sys.version_info < (3, 0): + BYTES_TEXT = "I \xe2\x99\xa5 GTK+" + UTF8_TEXT = unicode(BYTES_TEXT, 'UTF-8') + BYTES_HEART = "\xe2\x99\xa5" + HEART = unicode(BYTES_HEART, 'UTF-8') +else: + UTF8_TEXT = "I ♥ GTK+" + BYTES_TEXT = bytes(UTF8_TEXT, 'utf-8') + HEART = "♥" + BYTES_HEART = bytes(HEART, 'utf-8') + + +class RotatedTextApp: + RADIUS = 150 + N_WORDS = 5 + FONT = "Serif 18" + + def __init__(self): + + white = Gdk.RGBA() + white.red = 1.0 + white.green = 1.0 + white.blue = 1.0 + white.alpha = 1.0 + + self.window = Gtk.Window(title="Rotated Text") + self.window.set_default_size(4 * self.RADIUS, 2 * self.RADIUS) + self.window.connect('destroy', Gtk.main_quit) + + box = Gtk.HBox() + box.set_homogeneous(True) + self.window.add(box) + + # add a drawing area + da = Gtk.DrawingArea() + box.add(da) + + # override the background color from the theme + da.override_background_color(0, white) + + da.connect('draw', self.rotated_text_draw) + + label = Gtk.Label(label=UTF8_TEXT) + box.add(label) + label.set_angle(45) + + # Setup some fancy stuff on the label + layout = label.get_layout() + + PangoCairo.context_set_shape_renderer(layout.get_context(), + self.fancy_shape_renderer, + None) + attrs = self.create_fancy_attr_list_for_layout(layout) + label.set_attributes(attrs) + + self.window.show_all() + + def fancy_shape_renderer(self, cairo_ctx, attr, do_path): + x, y = cairo_ctx.get_current_point() + cairo_ctx.translate(x, y) + + cairo_ctx.scale(float(attr.inc_rect.width) / Pango.SCALE, + float(attr.inc_rect.height) / Pango.SCALE) + + if int(attr.data) == 0x2665: # U+2665 BLACK HEART SUIT + cairo_ctx.move_to(0.5, 0.0) + cairo_ctx.line_to(0.9, -0.4) + cairo_ctx.curve_to(1.1, -0.8, 0.5, -0.9, 0.5, -0.5) + cairo_ctx.curve_to(0.5, -0.9, -0.1, -0.8, 0.1, -0.4) + cairo_ctx.close_path() + + if not do_path: + cairo_ctx.set_source_rgb(1.0, 0.0, 0.0) + cairo_ctx.fill() + + def create_fancy_attr_list_for_layout(self, layout): + pango_ctx = layout.get_context() + metrics = pango_ctx.get_metrics(layout.get_font_description(), + None) + ascent = metrics.get_ascent() + + logical_rect = Pango.Rectangle() + logical_rect.x = 0 + logical_rect.width = ascent + logical_rect.y = -ascent + logical_rect.height = ascent + + # Set fancy shape attributes for all hearts + attrs = Pango.AttrList() + + # FIXME: attr_shape_new_with_data isn't introspectable + ''' + ink_rect = logical_rect + p = BYTES_TEXT.find(BYTES_HEART) + while (p != -1): + attr = Pango.attr_shape_new_with_data(ink_rect, + logical_rect, + ord(HEART), + None) + attr.start_index = p + attr.end_index = p + len(BYTES_HEART) + p = UTF8_TEXT.find(HEART, attr.end_index) + + attrs.insert(attr) + ''' + return attrs + + def rotated_text_draw(self, da, cairo_ctx): + # Create a cairo context and set up a transformation matrix so that the user + # space coordinates for the centered square where we draw are [-RADIUS, RADIUS], + # [-RADIUS, RADIUS]. + # We first center, then change the scale. + width = da.get_allocated_width() + height = da.get_allocated_height() + device_radius = min(width, height) / 2.0 + cairo_ctx.translate( + device_radius + (width - 2 * device_radius) / 2, + device_radius + (height - 2 * device_radius) / 2) + cairo_ctx.scale(device_radius / self.RADIUS, + device_radius / self.RADIUS) + + # Create a subtle gradient source and use it. + pattern = cairo.LinearGradient(-self.RADIUS, -self.RADIUS, self.RADIUS, self.RADIUS) + pattern.add_color_stop_rgb(0.0, 0.5, 0.0, 0.0) + pattern.add_color_stop_rgb(1.0, 0.0, 0.0, 0.5) + cairo_ctx.set_source(pattern) + + # Create a PangoContext and set up our shape renderer + context = da.create_pango_context() + PangoCairo.context_set_shape_renderer(context, + self.fancy_shape_renderer, + None) + + # Create a PangoLayout, set the text, font, and attributes */ + layout = Pango.Layout(context=context) + layout.set_text(UTF8_TEXT, -1) + desc = Pango.FontDescription(self.FONT) + layout.set_font_description(desc) + + attrs = self.create_fancy_attr_list_for_layout(layout) + layout.set_attributes(attrs) + + # Draw the layout N_WORDS times in a circle */ + for i in range(self.N_WORDS): + # Inform Pango to re-layout the text with the new transformation matrix + PangoCairo.update_layout(cairo_ctx, layout) + + width, height = layout.get_pixel_size() + cairo_ctx.move_to(-width / 2, -self.RADIUS * 0.9) + PangoCairo.show_layout(cairo_ctx, layout) + + # Rotate for the next turn + cairo_ctx.rotate(math.pi * 2 / self.N_WORDS) + + return False + + +def main(demoapp=None): + RotatedTextApp() + Gtk.main() + + +if __name__ == '__main__': + main() diff --git a/examples/demo/demos/test.py b/examples/demo/demos/test.py new file mode 100644 index 0000000..4bd7684 --- /dev/null +++ b/examples/demo/demos/test.py @@ -0,0 +1,16 @@ +title = "Test Demo" +description = "Dude this is a test" + + +from gi.repository import Gtk + + +def _quit(*args): + Gtk.main_quit() + + +def main(demoapp=None): + window = Gtk.Window() + window.connect_after('destroy', _quit) + window.show_all() + Gtk.main() diff --git a/gi/_ossighelper.py b/gi/_ossighelper.py new file mode 100644 index 0000000..2c72134 --- /dev/null +++ b/gi/_ossighelper.py @@ -0,0 +1,263 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Christoph Reiter +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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, see . + +from __future__ import print_function + +import os +import sys +import socket +import signal +import ctypes +import threading +from contextlib import closing, contextmanager + + +def ensure_socket_not_inheritable(sock): + """Ensures that the socket is not inherited by child processes + + Raises: + EnvironmentError + NotImplementedError: With Python <3.4 on Windows + """ + + if hasattr(sock, "set_inheritable"): + sock.set_inheritable(False) + else: + try: + import fcntl + except ImportError: + raise NotImplementedError( + "Not implemented for older Python on Windows") + else: + fd = sock.fileno() + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + +_wakeup_fd_is_active = False +"""Since we can't check if set_wakeup_fd() is already used for nested event +loops without introducing a race condition we keep track of it globally. +""" + + +@contextmanager +def wakeup_on_signal(): + """A decorator for functions which create a glib event loop to keep + Python signal handlers working while the event loop is idling. + + In case an OS signal is received will wake the default event loop up + shortly so that any registered Python signal handlers registered through + signal.signal() can run. + + Works on Windows but needs Python 3.5+. + + In case the wrapped function is not called from the main thread it will be + called as is and it will not wake up the default loop for signals. + """ + + global _wakeup_fd_is_active + + if _wakeup_fd_is_active: + yield + return + + from gi.repository import GLib + + # On Windows only Python 3.5+ supports passing sockets to set_wakeup_fd + set_wakeup_fd_supports_socket = ( + os.name != "nt" or sys.version_info[:2] >= (3, 5)) + # On Windows only Python 3 has an implementation of socketpair() + has_socketpair = hasattr(socket, "socketpair") + + if not has_socketpair or not set_wakeup_fd_supports_socket: + yield + return + + read_socket, write_socket = socket.socketpair() + with closing(read_socket), closing(write_socket): + + for sock in [read_socket, write_socket]: + sock.setblocking(False) + ensure_socket_not_inheritable(sock) + + try: + orig_fd = signal.set_wakeup_fd(write_socket.fileno()) + except ValueError: + # Raised in case this is not the main thread -> give up. + yield + return + else: + _wakeup_fd_is_active = True + + def signal_notify(source, condition): + if condition & GLib.IO_IN: + try: + return bool(read_socket.recv(1)) + except EnvironmentError as e: + print(e) + return False + return True + else: + return False + + try: + if os.name == "nt": + channel = GLib.IOChannel.win32_new_socket( + read_socket.fileno()) + else: + channel = GLib.IOChannel.unix_new(read_socket.fileno()) + + source_id = GLib.io_add_watch( + channel, + GLib.PRIORITY_DEFAULT, + (GLib.IOCondition.IN | GLib.IOCondition.HUP | + GLib.IOCondition.NVAL | GLib.IOCondition.ERR), + signal_notify) + try: + yield + finally: + GLib.source_remove(source_id) + finally: + write_fd = signal.set_wakeup_fd(orig_fd) + if write_fd != write_socket.fileno(): + # Someone has called set_wakeup_fd while func() was active, + # so let's re-revert again. + signal.set_wakeup_fd(write_fd) + _wakeup_fd_is_active = False + + +def create_pythonapi(): + # We need our own instance of ctypes.pythonapi so we don't modify the + # global shared one. Adapted from the ctypes source. + if os.name == "nt": + return ctypes.PyDLL("python dll", None, sys.dllhandle) + elif sys.platform == "cygwin": + return ctypes.PyDLL("libpython%d.%d.dll" % sys.version_info[:2]) + else: + return ctypes.PyDLL(None) + + +pydll = create_pythonapi() +PyOS_getsig = pydll.PyOS_getsig +PyOS_getsig.restype = ctypes.c_void_p +PyOS_getsig.argtypes = [ctypes.c_int] + +# We save the signal pointer so we can detect if glib has changed the +# signal handler behind Python's back (GLib.unix_signal_add) +if signal.getsignal(signal.SIGINT) is signal.default_int_handler: + startup_sigint_ptr = PyOS_getsig(signal.SIGINT) +else: + # Something has set the handler before import, we can't get a ptr + # for the default handler so make sure the pointer will never match. + startup_sigint_ptr = -1 + + +def sigint_handler_is_default(): + """Returns if on SIGINT the default Python handler would be called""" + + return (signal.getsignal(signal.SIGINT) is signal.default_int_handler and + PyOS_getsig(signal.SIGINT) == startup_sigint_ptr) + + +@contextmanager +def sigint_handler_set_and_restore_default(handler): + """Context manager for saving/restoring the SIGINT handler default state. + + Will only restore the default handler again if the handler is not changed + while the context is active. + """ + + assert sigint_handler_is_default() + + signal.signal(signal.SIGINT, handler) + sig_ptr = PyOS_getsig(signal.SIGINT) + try: + yield + finally: + if signal.getsignal(signal.SIGINT) is handler and \ + PyOS_getsig(signal.SIGINT) == sig_ptr: + signal.signal(signal.SIGINT, signal.default_int_handler) + + +def is_main_thread(): + """Returns True in case the function is called from the main thread""" + + return threading.current_thread().name == "MainThread" + + +_callback_stack = [] +_sigint_called = False + + +@contextmanager +def register_sigint_fallback(callback): + """Installs a SIGINT signal handler in case the default Python one is + active which calls 'callback' in case the signal occurs. + + Only does something if called from the main thread. + + In case of nested context managers the signal handler will be only + installed once and the callbacks will be called in the reverse order + of their registration. + + The old signal handler will be restored in case no signal handler is + registered while the context is active. + """ + + # To handle multiple levels of event loops we need to call the last + # callback first, wait until the inner most event loop returns control + # and only then call the next callback, and so on... until we + # reach the outer most which manages the signal handler and raises + # in the end + + global _callback_stack, _sigint_called + + if not is_main_thread(): + yield + return + + if not sigint_handler_is_default(): + if _callback_stack: + # This is an inner event loop, append our callback + # to the stack so the parent context can call it. + _callback_stack.append(callback) + try: + yield + finally: + if _sigint_called: + _callback_stack.pop()() + else: + # There is a signal handler set by the user, just do nothing + yield + return + + _sigint_called = False + + def sigint_handler(sig_num, frame): + global _callback_stack, _sigint_called + + if _sigint_called: + return + _sigint_called = True + _callback_stack.pop()() + + _callback_stack.append(callback) + with sigint_handler_set_and_restore_default(sigint_handler): + try: + yield + finally: + if _sigint_called: + signal.default_int_handler() diff --git a/gi/overrides/GLib.py b/gi/overrides/GLib.py index 372d6d4..52b6af3 100644 --- a/gi/overrides/GLib.py +++ b/gi/overrides/GLib.py @@ -19,11 +19,11 @@ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 # USA -import signal import warnings import sys import socket +from .._ossighelper import wakeup_on_signal, register_sigint_fallback from ..module import get_introspection_module from .._gi import (variant_type_from_string, source_new, source_set_callback, io_channel_read) @@ -560,32 +560,13 @@ class MainLoop(GLib.MainLoop): def __new__(cls, context=None): return GLib.MainLoop.new(context, False) - # Retain classic pygobject behaviour of quitting main loops on SIGINT def __init__(self, context=None): - def _handler(loop): - loop.quit() - loop._quit_by_sigint = True - # We handle signal deletion in __del__, return True so GLib - # doesn't do the deletion for us. - return True - - if sys.platform != 'win32': - # compatibility shim, keep around until we depend on glib 2.36 - if hasattr(GLib, 'unix_signal_add'): - fn = GLib.unix_signal_add - else: - fn = GLib.unix_signal_add_full - self._signal_source = fn(GLib.PRIORITY_DEFAULT, signal.SIGINT, _handler, self) - - def __del__(self): - if hasattr(self, '_signal_source'): - GLib.source_remove(self._signal_source) + pass def run(self): - super(MainLoop, self).run() - if hasattr(self, '_quit_by_sigint'): - # caught by _main_loop_sigint_handler() - raise KeyboardInterrupt + with register_sigint_fallback(self.quit): + with wakeup_on_signal(): + super(MainLoop, self).run() MainLoop = override(MainLoop) diff --git a/gi/overrides/Gio.py b/gi/overrides/Gio.py index cdb3ccb..5ab23fc 100644 --- a/gi/overrides/Gio.py +++ b/gi/overrides/Gio.py @@ -20,6 +20,7 @@ import warnings +from .._ossighelper import wakeup_on_signal, register_sigint_fallback from ..overrides import override, deprecated_init from ..module import get_introspection_module from gi import PyGIWarning @@ -33,6 +34,18 @@ Gio = get_introspection_module('Gio') __all__ = [] +class Application(Gio.Application): + + def run(self, *args, **kwargs): + with register_sigint_fallback(self.quit): + with wakeup_on_signal(): + return Gio.Application.run(self, *args, **kwargs) + + +Application = override(Application) +__all__.append('Application') + + class VolumeMonitor(Gio.VolumeMonitor): def __init__(self, *args, **kwargs): diff --git a/gi/overrides/Gtk.py b/gi/overrides/Gtk.py index 08d2612..c495fd1 100644 --- a/gi/overrides/Gtk.py +++ b/gi/overrides/Gtk.py @@ -24,6 +24,7 @@ import sys import warnings from gi.repository import GObject +from .._ossighelper import wakeup_on_signal, register_sigint_fallback from ..overrides import override, strip_boolean_result, deprecated_init from ..module import get_introspection_module from gi import PyGIDeprecationWarning @@ -543,6 +544,11 @@ class Dialog(Gtk.Dialog, Container): if add_buttons: self.add_buttons(*add_buttons) + def run(self, *args, **kwargs): + with register_sigint_fallback(self.destroy): + with wakeup_on_signal(): + return Gtk.Dialog.run(self, *args, **kwargs) + action_area = property(lambda dialog: dialog.get_action_area()) vbox = property(lambda dialog: dialog.get_content_area()) @@ -1007,28 +1013,30 @@ class ListStore(Gtk.ListStore, TreeModel, TreeSortable): Gtk.ListStore.set_value(self, treeiter, column, value) def set(self, treeiter, *args): - - def _set_lists(columns, values): - if len(columns) != len(values): + def _set_lists(cols, vals): + if len(cols) != len(vals): raise TypeError('The number of columns do not match the number of values') - for col_num, val in zip(columns, values): + + columns = [] + values = [] + for col_num, value in zip(cols, vals): if not isinstance(col_num, int): raise TypeError('TypeError: Expected integer argument for column.') - self.set_value(treeiter, col_num, val) + + columns.append(col_num) + values.append(self._convert_value(col_num, value)) + + Gtk.ListStore.set(self, treeiter, columns, values) if args: if isinstance(args[0], int): - columns = args[::2] - values = args[1::2] - _set_lists(columns, values) + _set_lists(args[::2], args[1::2]) elif isinstance(args[0], (tuple, list)): if len(args) != 2: raise TypeError('Too many arguments') _set_lists(args[0], args[1]) elif isinstance(args[0], dict): - columns = args[0].keys() - values = args[0].values() - _set_lists(columns, values) + _set_lists(list(args[0]), args[0].values()) else: raise TypeError('Argument list must be in the form of (column, value, ...), ((columns,...), (values, ...)) or {column: value}. No -1 termination is needed.') @@ -1269,28 +1277,30 @@ class TreeStore(Gtk.TreeStore, TreeModel, TreeSortable): Gtk.TreeStore.set_value(self, treeiter, column, value) def set(self, treeiter, *args): - - def _set_lists(columns, values): - if len(columns) != len(values): + def _set_lists(cols, vals): + if len(cols) != len(vals): raise TypeError('The number of columns do not match the number of values') - for col_num, val in zip(columns, values): + + columns = [] + values = [] + for col_num, value in zip(cols, vals): if not isinstance(col_num, int): raise TypeError('TypeError: Expected integer argument for column.') - self.set_value(treeiter, col_num, val) + + columns.append(col_num) + values.append(self._convert_value(col_num, value)) + + Gtk.TreeStore.set(self, treeiter, columns, values) if args: if isinstance(args[0], int): - columns = args[::2] - values = args[1::2] - _set_lists(columns, values) + _set_lists(args[::2], args[1::2]) elif isinstance(args[0], (tuple, list)): if len(args) != 2: raise TypeError('Too many arguments') _set_lists(args[0], args[1]) elif isinstance(args[0], dict): - columns = args[0].keys() - values = args[0].values() - _set_lists(columns, values) + _set_lists(args[0].keys(), args[0].values()) else: raise TypeError('Argument list must be in the form of (column, value, ...), ((columns,...), (values, ...)) or {column: value}. No -1 termination is needed.') @@ -1590,6 +1600,16 @@ def main_quit(*args): _Gtk_main_quit() +_Gtk_main = Gtk.main + + +@override(Gtk.main) +def main(*args, **kwargs): + with register_sigint_fallback(Gtk.main_quit): + with wakeup_on_signal(): + return _Gtk_main(*args, **kwargs) + + if Gtk._version in ("2.0", "3.0"): stock_lookup = strip_boolean_result(Gtk.stock_lookup) __all__.append('stock_lookup') diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4 index 6484f03..0934a44 100644 --- a/m4/ax_code_coverage.m4 +++ b/m4/ax_code_coverage.m4 @@ -21,7 +21,7 @@ # Test also for gcov program and create GCOV variable that could be # substituted. # -# Note that all optimization flags in CFLAGS must be disabled when code +# Note that all optimisation flags in CFLAGS must be disabled when code # coverage is enabled. # # Usage example: @@ -75,7 +75,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -#serial 25 +#serial 21 AC_DEFUN([AX_CODE_COVERAGE],[ dnl Check for --enable-code-coverage @@ -218,12 +218,9 @@ CODE_COVERAGE_LCOV_RMOPTS ?= $(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT) CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\ $(if $(CODE_COVERAGE_BRANCH_COVERAGE),\ --rc genhtml_branch_coverage=$(CODE_COVERAGE_BRANCH_COVERAGE)) -CODE_COVERAGE_GENHTML_OPTIONS ?= $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT) +CODE_COVERAGE_GENHTML_OPTIONS ?= $(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULTS) CODE_COVERAGE_IGNORE_PATTERN ?= -GITIGNOREFILES ?= -GITIGNOREFILES += $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_DIRECTORY) - code_coverage_v_lcov_cap = $(code_coverage_v_lcov_cap_$(V)) code_coverage_v_lcov_cap_ = $(code_coverage_v_lcov_cap_$(AM_DEFAULT_VERBOSITY)) code_coverage_v_lcov_cap_0 = @echo " LCOV --capture"\ @@ -253,6 +250,9 @@ code-coverage-capture-hook: '"$CODE_COVERAGE_RULES_CLEAN"' +GITIGNOREFILES ?= +GITIGNOREFILES += $(CODE_COVERAGE_OUTPUT_FILE) $(CODE_COVERAGE_OUTPUT_DIRECTORY) + A''M_DISTCHECK_CONFIGURE_FLAGS ?= A''M_DISTCHECK_CONFIGURE_FLAGS += --disable-code-coverage diff --git a/m4/ax_compiler_flags_cflags.m4 b/m4/ax_compiler_flags_cflags.m4 index 9767e6a..aeb16e3 100644 --- a/m4/ax_compiler_flags_cflags.m4 +++ b/m4/ax_compiler_flags_cflags.m4 @@ -19,14 +19,13 @@ # LICENSE # # Copyright (c) 2014, 2015 Philip Withnall -# Copyright (c) 2017 Reini Urban # # Copying and distribution of this file, with or without modification, are # permitted in any medium without royalty provided the copyright notice # and this notice are preserved. This file is offered as-is, without any # warranty. -#serial 15 +#serial 14 AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[ AC_REQUIRE([AC_PROG_SED]) @@ -101,13 +100,6 @@ AC_DEFUN([AX_COMPILER_FLAGS_CFLAGS],[ -Wreturn-type dnl -Wswitch-enum dnl -Wswitch-default dnl - -Wduplicated-cond dnl - -Wduplicated-branches dnl - -Wlogical-op dnl - -Wrestrict dnl - -Wnull-dereference dnl - -Wjump-misses-init dnl - -Wdouble-promotion dnl $4 dnl $5 dnl $6 dnl diff --git a/m4/glib-2.0.m4 b/m4/glib-2.0.m4 index 4b19019..d8f03d4 100644 --- a/m4/glib-2.0.m4 +++ b/m4/glib-2.0.m4 @@ -1,9 +1,6 @@ # Configure paths for GLIB # Owen Taylor 1997-2001 -# Increment this whenever this file is changed. -#serial 1 - dnl AM_PATH_GLIB_2_0([MINIMUM-VERSION, [ACTION-IF-FOUND [, ACTION-IF-NOT-FOUND [, MODULES]]]]) dnl Test for GLIB, and define GLIB_CFLAGS and GLIB_LIBS, if gmodule, gobject, dnl gthread, or gio is specified in MODULES, pass to pkg-config @@ -93,7 +90,7 @@ dnl #include int -main (void) +main () { unsigned int major, minor, micro; diff --git a/pygobject-3.0-uninstalled.pc.in b/pygobject-3.0-uninstalled.pc.in deleted file mode 100644 index 4cec178..0000000 --- a/pygobject-3.0-uninstalled.pc.in +++ /dev/null @@ -1,12 +0,0 @@ -# you can use the --variable=pygobjectincludedir argument to -# pkg-config to get this value. You might want to use this to -# install additional headers. -pygobjectincludedir=${pcfiledir}/gi/_gobject -overridesdir=${pcfiledir}/gi/overrides - -Name: PyGObject -Description: Python bindings for GObject -Requires: gobject-2.0 -Requires.private: @LIBFFI_PC@ -Version: @VERSION@ -Cflags: -I${pcfiledir}/gi/_gobject diff --git a/pygobject.doap b/pygobject.doap index d366a2a..63f184f 100644 --- a/pygobject.doap +++ b/pygobject.doap @@ -64,4 +64,11 @@ PyGObject now dynamically accesses any GObject libraries that uses GObject Intro sfeltman + + + Christoph Reiter + + creiter + + diff --git a/setup.py b/setup.py old mode 100755 new mode 100644 index 0da9ed9..af3aa5c --- a/setup.py +++ b/setup.py @@ -1,105 +1,356 @@ #!/usr/bin/env python +# Copyright 2017 Christoph Reiter +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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 St, Fifth Floor, Boston, MA 02110-1301 +# USA +""" +ATTENTION DISTRO PACKAGERS: This is not a valid replacement for autotools. +It does not install headers, pkgconfig files and does not support running +tests. Its main use case atm is installation in virtualenvs and via pip. +""" +import io import os import re -import subprocess import sys +import errno +import subprocess +import tarfile +from email import parser + +from setuptools import setup, find_packages +from distutils.core import Extension, Distribution +from distutils.ccompiler import new_compiler +from distutils import dir_util + + +def get_command_class(name): + # Returns the right class for either distutils or setuptools + return Distribution({}).get_command_class(name) + + +def get_pycairo_pkg_config_name(): + return "py3cairo" if sys.version_info[0] == 3 else "pycairo" + -from distutils.command.build import build as orig_build -from setuptools.command.build_ext import build_ext as orig_build_ext -from setuptools.command.build_py import build_py as orig_build_py -from setuptools import setup, Extension +def get_version_requirement(conf_dir, pkg_config_name): + """Given a pkg-config module name gets the minimum version required""" + if pkg_config_name in ["cairo", "cairo-gobject"]: + return "0" -with open("configure.ac", "r") as h: - version = ".".join(re.findall("pygobject_[^\s]+_version,\s*(\d+)\)", h.read())) + mapping = { + "gobject-introspection-1.0": "introspection", + "glib-2.0": "glib", + "gio-2.0": "gio", + get_pycairo_pkg_config_name(): "pycairo", + "libffi": "libffi", + } + assert pkg_config_name in mapping + configure_ac = os.path.join(conf_dir, "configure.ac") + with io.open(configure_ac, "r", encoding="utf-8") as h: + text = h.read() + conf_name = mapping[pkg_config_name] + res = re.findall( + r"%s_required_version,\s*([\d\.]+)\)" % conf_name, text) + assert len(res) == 1 + return res[0] -def makedirs(dirpath): - """Safely make directories - By default, os.makedirs fails if the directory already exists. +def parse_versions(conf_dir): + configure_ac = os.path.join(conf_dir, "configure.ac") + with io.open(configure_ac, "r", encoding="utf-8") as h: + version = re.findall(r"pygobject_[^\s]+_version,\s*(\d+)\)", h.read()) + assert len(version) == 3 - Python 3.2 introduced the `exist_ok` argument, but we can't use it because - we want to keep supporting Python 2 for some time. + versions = { + "PYGOBJECT_MAJOR_VERSION": version[0], + "PYGOBJECT_MINOR_VERSION": version[1], + "PYGOBJECT_MICRO_VERSION": version[2], + "VERSION": ".".join(version), + } + return versions + + +def parse_pkg_info(conf_dir): + """Returns an email.message.Message instance containing the content + of the PKG-INFO file. The version info is parsed from configure.ac """ - import errno - try: - os.makedirs(dirpath) + versions = parse_versions(conf_dir) + + pkg_info = os.path.join(conf_dir, "PKG-INFO.in") + with io.open(pkg_info, "r", encoding="utf-8") as h: + text = h.read() + for key, value in versions.items(): + text = text.replace("@%s@" % key, value) + + p = parser.Parser() + message = p.parse(io.StringIO(text)) + return message + + +def _run_pkg_config(args): + command = ["pkg-config"] + args + + # Add $prefix/share/pkgconfig to PKG_CONFIG_PATH so we use the right + # pycairo in case we are in a virtualenv + env = dict(os.environ) + paths = env.get("PKG_CONFIG_PATH", "").split(os.pathsep) + paths = [p for p in paths if p] + paths.insert(0, os.path.join(sys.prefix, "share", "pkgconfig")) + env["PKG_CONFIG_PATH"] = os.pathsep.join(paths) + try: + return subprocess.check_output(command, env=env) except OSError as e: - if e.errno == errno.EEXIST: - return + if e.errno == errno.ENOENT: + raise SystemExit( + "%r not found.\nArguments: %r" % (command[0], command)) + raise SystemExit(e) + except subprocess.CalledProcessError as e: + raise SystemExit(e) + + +def pkg_config_version_check(pkg, version): + _run_pkg_config([ + "--print-errors", + "--exists", + '%s >= %s' % (pkg, version), + ]) - raise +def pkg_config_parse(opt, pkg): + ret = _run_pkg_config([opt, pkg]) + output = ret.decode() + opt = opt[-2:] + return [x.lstrip(opt) for x in output.split()] -class Build(orig_build): - """Dummy version of distutils build which runs an Autotools build system - instead. + +du_sdist = get_command_class("sdist") + + +class distcheck(du_sdist): + """Creates a tarball and does some additional sanity checks such as + checking if the tarballs includes all files and builds. """ + + def _check_manifest(self): + # make sure MANIFEST.in includes all tracked files + assert self.get_archive_files() + + if subprocess.call(["git", "status"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) != 0: + return + + included_files = self.filelist.files + assert included_files + + process = subprocess.Popen( + ["git", "ls-tree", "-r", "HEAD", "--name-only"], + stdout=subprocess.PIPE, universal_newlines=True) + out, err = process.communicate() + assert process.returncode == 0 + + tracked_files = out.splitlines() + for ignore in [".gitignore"]: + tracked_files.remove(ignore) + + diff = set(tracked_files) - set(included_files) + assert not diff, ( + "Not all tracked files included in tarball, check MANIFEST.in", + diff) + + def _check_dist(self): + # make sure the tarball builds + assert self.get_archive_files() + + distcheck_dir = os.path.join(self.dist_dir, "distcheck") + if os.path.exists(distcheck_dir): + dir_util.remove_tree(distcheck_dir) + self.mkpath(distcheck_dir) + + archive = self.get_archive_files()[0] + tfile = tarfile.open(archive, "r:gz") + tfile.extractall(distcheck_dir) + tfile.close() + + name = self.distribution.get_fullname() + extract_dir = os.path.join(distcheck_dir, name) + + old_pwd = os.getcwd() + os.chdir(extract_dir) + try: + self.spawn([sys.executable, "setup.py", "build"]) + self.spawn([sys.executable, "setup.py", "install", + "--root", "../prefix", "--record", "../log.txt"]) + finally: + os.chdir(old_pwd) + def run(self): - srcdir = os.getcwd() - builddir = os.path.join(srcdir, self.build_temp) - makedirs(builddir) - configure = os.path.join(srcdir, 'configure') - - if not os.path.exists(configure): - configure = os.path.join(srcdir, 'autogen.sh') - - subprocess.check_call([ - configure, - 'PYTHON=%s' % sys.executable, - # Put the documentation, etc. out of the way: we only want - # the Python code and extensions - '--prefix=' + os.path.join(builddir, 'prefix'), - ], - cwd=builddir) - make_args = [ - 'pythondir=%s' % os.path.join(srcdir, self.build_lib), - 'pyexecdir=%s' % os.path.join(srcdir, self.build_lib), - ] - subprocess.check_call(['make', '-C', builddir] + make_args) - subprocess.check_call(['make', '-C', builddir, 'install'] + make_args) - - -class BuildExt(orig_build_ext): - def run(self): - pass + du_sdist.run(self) + self._check_manifest() + self._check_dist() + + +du_build_ext = get_command_class("build_ext") + + +class build_ext(du_build_ext): + + def initialize_options(self): + du_build_ext.initialize_options(self) + self.compiler_type = None + + def finalize_options(self): + du_build_ext.finalize_options(self) + self.compiler_type = new_compiler(compiler=self.compiler).compiler_type + + def _write_config_h(self): + script_dir = os.path.dirname(os.path.realpath(__file__)) + target = os.path.join(script_dir, "config.h") + versions = parse_versions(script_dir) + with io.open(target, 'w', encoding="utf-8") as h: + h.write(""" +/* Configuration header created by setup.py - do not edit */ +#ifndef _CONFIG_H +#define _CONFIG_H 1 + +#define PYGOBJECT_MAJOR_VERSION %(PYGOBJECT_MAJOR_VERSION)s +#define PYGOBJECT_MINOR_VERSION %(PYGOBJECT_MINOR_VERSION)s +#define PYGOBJECT_MICRO_VERSION %(PYGOBJECT_MICRO_VERSION)s +#define VERSION "%(VERSION)s" + +#endif /* _CONFIG_H */ +""" % versions) + def _setup_extensions(self): + ext = {e.name: e for e in self.extensions} + script_dir = os.path.dirname(os.path.realpath(__file__)) + + msvc_libraries = { + "glib-2.0": ["glib-2.0"], + "gio-2.0": ["gio-2.0", "gobject-2.0", "glib-2.0"], + "gobject-introspection-1.0": + ["girepository-1.0", "gobject-2.0", "glib-2.0"], + get_pycairo_pkg_config_name(): ["cairo"], + "cairo": ["cairo"], + "cairo-gobject": + ["cairo-gobject", "cairo", "gobject-2.0", "glib-2.0"], + "libffi": ["ffi"], + } + + def add_dependency(ext, name): + fallback_libs = msvc_libraries[name] + + if self.compiler_type == "msvc": + # assume that INCLUDE and LIB contains the right paths + ext.libraries += fallback_libs + + # The PyCairo header is installed in a subdir of the + # Python installation that we are building for, so + # deduce that include path here, and use it + ext.include_dirs += [ + os.path.join(sys.prefix, "include", "pycairo")] + else: + min_version = get_version_requirement(script_dir, name) + pkg_config_version_check(name, min_version) + ext.include_dirs += pkg_config_parse("--cflags-only-I", name) + ext.library_dirs += pkg_config_parse("--libs-only-L", name) + ext.libraries += pkg_config_parse("--libs-only-l", name) + + gi_ext = ext["gi._gi"] + add_dependency(gi_ext, "glib-2.0") + add_dependency(gi_ext, "gio-2.0") + add_dependency(gi_ext, "gobject-introspection-1.0") + add_dependency(gi_ext, "libffi") + + gi_cairo_ext = ext["gi._gi_cairo"] + add_dependency(gi_cairo_ext, "glib-2.0") + add_dependency(gi_cairo_ext, "gio-2.0") + add_dependency(gi_cairo_ext, "gobject-introspection-1.0") + add_dependency(gi_cairo_ext, "libffi") + add_dependency(gi_cairo_ext, "cairo") + add_dependency(gi_cairo_ext, "cairo-gobject") + add_dependency(gi_cairo_ext, get_pycairo_pkg_config_name()) -class BuildPy(orig_build_py): def run(self): - pass - - -setup( - name='pygobject', - version=version, - description='Python bindings for GObject Introspection', - maintainer='The pygobject maintainers', - maintainer_email='http://mail.gnome.org/mailman/listinfo/python-hackers-list', - download_url='http://download.gnome.org/sources/pygobject/', - url='https://wiki.gnome.org/Projects/PyGObject', - packages=['gi', 'pygtkcompat'], - ext_modules=[ - Extension( - '_gi', sources=['gi/gimodule.c']) + self._write_config_h() + self._setup_extensions() + du_build_ext.run(self) + + +def main(): + script_dir = os.path.dirname(os.path.realpath(__file__)) + pkginfo = parse_pkg_info(script_dir) + gi_dir = os.path.join(script_dir, "gi") + + sources = [ + os.path.join("gi", n) for n in os.listdir(gi_dir) + if os.path.splitext(n)[-1] == ".c" + ] + cairo_sources = [os.path.join("gi", "pygi-foreign-cairo.c")] + for s in cairo_sources: + sources.remove(s) + + gi_ext = Extension( + name='gi._gi', + sources=sources, + include_dirs=[script_dir, gi_dir], + define_macros=[("HAVE_CONFIG_H", None)], + ) + + gi_cairo_ext = Extension( + name='gi._gi_cairo', + sources=cairo_sources, + include_dirs=[script_dir, gi_dir], + define_macros=[("HAVE_CONFIG_H", None)], + ) + + setup( + name=pkginfo["Name"], + version=pkginfo["Version"], + description=pkginfo["Summary"], + url=pkginfo["Home-page"], + author=pkginfo["Author"], + author_email=pkginfo["Author-email"], + maintainer=pkginfo["Maintainer"], + maintainer_email=pkginfo["Maintainer-email"], + license=pkginfo["License"], + download_url=pkginfo["Download-url"], + long_description=pkginfo["Description"], + platforms=pkginfo.get_all("Platform"), + classifiers=pkginfo.get_all("Classifier"), + packages=find_packages(script_dir), + ext_modules=[ + gi_ext, + gi_cairo_ext, ], - license='LGPL', - classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)', - 'Programming Language :: C', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: Implementation :: CPython', - ], - cmdclass={ - 'build': Build, - 'build_py': BuildPy, - 'build_ext': BuildExt, - }, -) + cmdclass={ + "build_ext": build_ext, + "distcheck": distcheck, + }, + install_requires=[ + "pycairo>=%s" % get_version_requirement( + script_dir, get_pycairo_pkg_config_name()), + ], + ) + + +if __name__ == "__main__": + main() diff --git a/tests/Makefile.am b/tests/Makefile.am index ff10433..aef0528 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -144,6 +144,7 @@ EXTRA_DIST = \ test_docstring.py \ test_repository.py \ test_resulttuple.py \ + test_ossig.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/Makefile.in b/tests/Makefile.in index f070538..d7e307b 100644 --- a/tests/Makefile.in +++ b/tests/Makefile.in @@ -493,6 +493,7 @@ EXTRA_DIST = \ test_docstring.py \ test_repository.py \ test_resulttuple.py \ + test_ossig.py \ compat_test_pygtk.py \ gi/__init__.py \ gi/overrides/__init__.py \ diff --git a/tests/test_everything.py b/tests/test_everything.py index 0378781..ac97e05 100644 --- a/tests/test_everything.py +++ b/tests/test_everything.py @@ -7,6 +7,7 @@ import traceback import ctypes import warnings import sys +import os from gi.repository import Regress as Everything from gi.repository import GObject @@ -19,6 +20,7 @@ try: except: Gtk = None +from compathelper import PY3 from helper import capture_exceptions @@ -246,7 +248,11 @@ class TestEverything(unittest.TestCase): self.assertEqual(Everything.test_utf8_inout(const_str), noconst_str) def test_filename_return(self): - self.assertEqual(Everything.test_filename_return(), ['åäö', '/etc/fstab']) + if PY3 and os.name != "nt": + result = [os.fsdecode(b'\xc3\xa5\xc3\xa4\xc3\xb6'), '/etc/fstab'] + else: + result = ['åäö', '/etc/fstab'] + self.assertEqual(Everything.test_filename_return(), result) def test_int_out_utf8(self): # returns g_utf8_strlen() in out argument diff --git a/tests/test_gi.py b/tests/test_gi.py index d1b0cfd..39aaf0c 100644 --- a/tests/test_gi.py +++ b/tests/test_gi.py @@ -695,6 +695,13 @@ class TestFilename(unittest.TestCase): @unittest.skipIf(os.name == "nt", "fixme") def test_filename_in(self): fname = os.path.join(self.workdir, u'testäø.txt') + + try: + os.path.exists(fname) + except ValueError: + # non-unicode fs encoding + return + self.assertRaises(GLib.GError, GLib.file_get_contents, fname) with open(fname.encode('UTF-8'), 'wb') as f: @@ -711,8 +718,15 @@ class TestFilename(unittest.TestCase): @unittest.skipIf(os.name == "nt", "fixme") def test_filename_out(self): self.assertRaises(GLib.GError, GLib.Dir.make_tmp, 'test') + name = 'testäø.XXXXXX' + + try: + os.path.exists(name) + except ValueError: + # non-unicode fs encoding + return - dirname = GLib.Dir.make_tmp('testäø.XXXXXX') + dirname = GLib.Dir.make_tmp(name) self.assertTrue(os.path.sep + 'testäø.' in dirname, dirname) self.assertTrue(os.path.isdir(dirname)) os.rmdir(dirname) @@ -868,7 +882,16 @@ class TestFilename(unittest.TestCase): if os.name != "nt": paths.append((wdb, b"\xff\xfe-5")) + def valid_path(p): + try: + os.path.exists(p) + except ValueError: + return False + return True + for (d, path) in paths: + if not valid_path(path): + continue path = os.path.join(d, path) with open(path, "wb"): self.assertTrue(GIMarshallingTests.filename_exists(path)) diff --git a/tests/test_ossig.py b/tests/test_ossig.py new file mode 100644 index 0000000..bf218b8 --- /dev/null +++ b/tests/test_ossig.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Christoph Reiter +# +# 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; either +# version 2.1 of the License, or (at your option) any later version. +# +# This 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, see . + +import os +import signal +import unittest +import threading +from contextlib import contextmanager + +from gi.repository import Gtk, Gio, GLib +from gi._ossighelper import wakeup_on_signal, register_sigint_fallback + + +class TestOverridesWakeupOnAlarm(unittest.TestCase): + + @contextmanager + def _run_with_timeout(self, timeout, abort_func): + failed = [] + + def fail(): + abort_func() + failed.append(1) + return True + + fail_id = GLib.timeout_add(timeout, fail) + try: + yield + finally: + GLib.source_remove(fail_id) + self.assertFalse(failed) + + def test_basic(self): + self.assertEqual(signal.set_wakeup_fd(-1), -1) + with wakeup_on_signal(): + pass + self.assertEqual(signal.set_wakeup_fd(-1), -1) + + def test_in_thread(self): + failed = [] + + def target(): + try: + with wakeup_on_signal(): + pass + except: + failed.append(1) + + t = threading.Thread(target=target) + t.start() + t.join(5) + self.assertFalse(failed) + + @unittest.skipIf(os.name == "nt", "not on Windows") + def test_glib_mainloop(self): + loop = GLib.MainLoop() + signal.signal(signal.SIGALRM, lambda *args: loop.quit()) + GLib.idle_add(signal.setitimer, signal.ITIMER_REAL, 0.001) + + with self._run_with_timeout(2000, loop.quit): + loop.run() + + @unittest.skipIf(os.name == "nt", "not on Windows") + def test_gio_application(self): + app = Gio.Application() + signal.signal(signal.SIGALRM, lambda *args: app.quit()) + GLib.idle_add(signal.setitimer, signal.ITIMER_REAL, 0.001) + + with self._run_with_timeout(2000, app.quit): + app.hold() + app.connect("activate", lambda *args: None) + app.run() + + @unittest.skipIf(os.name == "nt", "not on Windows") + def test_gtk_main(self): + signal.signal(signal.SIGALRM, lambda *args: Gtk.main_quit()) + GLib.idle_add(signal.setitimer, signal.ITIMER_REAL, 0.001) + + with self._run_with_timeout(2000, Gtk.main_quit): + Gtk.main() + + @unittest.skipIf(os.name == "nt", "not on Windows") + def test_gtk_dialog_run(self): + w = Gtk.Window() + d = Gtk.Dialog(transient_for=w) + signal.signal(signal.SIGALRM, lambda *args: d.destroy()) + GLib.idle_add(signal.setitimer, signal.ITIMER_REAL, 0.001) + + with self._run_with_timeout(2000, d.destroy): + d.run() + + +class TestSigintFallback(unittest.TestCase): + + def setUp(self): + self.assertEqual( + signal.getsignal(signal.SIGINT), signal.default_int_handler) + + def tearDown(self): + self.assertEqual( + signal.getsignal(signal.SIGINT), signal.default_int_handler) + + def test_replace_handler_and_restore_nested(self): + with register_sigint_fallback(lambda: None): + new_handler = signal.getsignal(signal.SIGINT) + self.assertNotEqual(new_handler, signal.default_int_handler) + with register_sigint_fallback(lambda: None): + self.assertTrue(signal.getsignal(signal.SIGINT) is new_handler) + self.assertEqual( + signal.getsignal(signal.SIGINT), signal.default_int_handler) + + def test_no_replace_if_not_default(self): + new_handler = lambda *args: None + signal.signal(signal.SIGINT, new_handler) + try: + with register_sigint_fallback(lambda: None): + self.assertTrue(signal.getsignal(signal.SIGINT) is new_handler) + with register_sigint_fallback(lambda: None): + self.assertTrue( + signal.getsignal(signal.SIGINT) is new_handler) + self.assertTrue(signal.getsignal(signal.SIGINT) is new_handler) + finally: + signal.signal(signal.SIGINT, signal.default_int_handler) + + def test_noop_in_threads(self): + failed = [] + + def target(): + try: + with register_sigint_fallback(lambda: None): + with register_sigint_fallback(lambda: None): + self.assertTrue( + signal.getsignal(signal.SIGINT) is + signal.default_int_handler) + except: + failed.append(1) + + t = threading.Thread(target=target) + t.start() + t.join(5) + self.assertFalse(failed) + + @unittest.skipIf(os.name == "nt", "not on Windows") + def test_no_replace_if_set_by_glib(self): + id_ = GLib.unix_signal_add( + GLib.PRIORITY_DEFAULT, signal.SIGINT, lambda *args: None) + try: + # signal.getsignal() doesn't pick up that unix_signal_add() + # has changed the handler, but we should anyway. + self.assertEqual( + signal.getsignal(signal.SIGINT), signal.default_int_handler) + with register_sigint_fallback(lambda: None): + self.assertEqual( + signal.getsignal(signal.SIGINT), + signal.default_int_handler) + self.assertEqual( + signal.getsignal(signal.SIGINT), signal.default_int_handler) + finally: + GLib.source_remove(id_) + signal.signal(signal.SIGINT, signal.SIG_DFL) + signal.signal(signal.SIGINT, signal.default_int_handler) diff --git a/tests/test_overrides_gtk.py b/tests/test_overrides_gtk.py index 61b7dc0..769e439 100644 --- a/tests/test_overrides_gtk.py +++ b/tests/test_overrides_gtk.py @@ -1080,6 +1080,12 @@ class TestTreeModel(unittest.TestCase): tree_store.insert(None, 1) self.assertEqual(signals, ['row-inserted']) + # One set one signal + signals.pop() + tree_iter = tree_store.append(None, (10, False)) + tree_store.set(tree_iter, (0, 1), (20, True)) + self.assertEqual(signals, ['row-inserted', 'row-changed']) + def test_list_store(self): class TestPyObject(object): pass @@ -1337,6 +1343,12 @@ class TestTreeModel(unittest.TestCase): list_store.insert(1) self.assertEqual(signals, ['row-inserted']) + # One set one signal + signals.pop() + tree_iter = list_store.append((10, False)) + list_store.set(tree_iter, (0, 1), (20, True)) + self.assertEqual(signals, ['row-inserted', 'row-changed']) + def test_tree_path(self): p1 = Gtk.TreePath() p2 = Gtk.TreePath.new_first() diff --git a/pygi-convert.sh b/tools/pygi-convert.sh similarity index 100% rename from pygi-convert.sh rename to tools/pygi-convert.sh -- 2.7.4