summaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:36:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 14:36:24 +0000
commit9b6d8e63db85c30007b463e91f91a791969fa83f (patch)
tree0899af51d73c1bf986f73ae39a03c4436083018a /subprojects
parentInitial commit. (diff)
downloadgnome-control-center-upstream.tar.xz
gnome-control-center-upstream.zip
Adding upstream version 1:3.38.4.upstream/1%3.38.4upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/gvc/README.md12
-rw-r--r--subprojects/gvc/gvc-channel-map-private.h39
-rw-r--r--subprojects/gvc/gvc-channel-map.c247
-rw-r--r--subprojects/gvc/gvc-channel-map.h73
-rw-r--r--subprojects/gvc/gvc-mixer-card-private.h35
-rw-r--r--subprojects/gvc/gvc-mixer-card.c584
-rw-r--r--subprojects/gvc/gvc-mixer-card.h102
-rw-r--r--subprojects/gvc/gvc-mixer-control-private.h35
-rw-r--r--subprojects/gvc/gvc-mixer-control.c3876
-rw-r--r--subprojects/gvc/gvc-mixer-control.h155
-rw-r--r--subprojects/gvc/gvc-mixer-event-role.c228
-rw-r--r--subprojects/gvc/gvc-mixer-event-role.h57
-rw-r--r--subprojects/gvc/gvc-mixer-sink-input.c159
-rw-r--r--subprojects/gvc/gvc-mixer-sink-input.h57
-rw-r--r--subprojects/gvc/gvc-mixer-sink.c189
-rw-r--r--subprojects/gvc/gvc-mixer-sink.h57
-rw-r--r--subprojects/gvc/gvc-mixer-source-output.c160
-rw-r--r--subprojects/gvc/gvc-mixer-source-output.h57
-rw-r--r--subprojects/gvc/gvc-mixer-source.c189
-rw-r--r--subprojects/gvc/gvc-mixer-source.h57
-rw-r--r--subprojects/gvc/gvc-mixer-stream-private.h34
-rw-r--r--subprojects/gvc/gvc-mixer-stream.c1090
-rw-r--r--subprojects/gvc/gvc-mixer-stream.h146
-rw-r--r--subprojects/gvc/gvc-mixer-ui-device.c741
-rw-r--r--subprojects/gvc/gvc-mixer-ui-device.h85
-rw-r--r--subprojects/gvc/gvc-pulseaudio-fake.h30
-rw-r--r--subprojects/gvc/libgnome-volume-control.doap32
-rw-r--r--subprojects/gvc/meson.build137
-rw-r--r--subprojects/gvc/meson_options.txt41
-rw-r--r--subprojects/gvc/test-audio-device-selection.c84
-rw-r--r--subprojects/libhandy/.dir-locals.el8
-rw-r--r--subprojects/libhandy/.editorconfig38
-rw-r--r--subprojects/libhandy/AUTHORS9
-rw-r--r--subprojects/libhandy/COPYING502
-rw-r--r--subprojects/libhandy/HACKING.md335
-rw-r--r--subprojects/libhandy/NEWS228
-rw-r--r--subprojects/libhandy/README.md71
-rw-r--r--subprojects/libhandy/data/leak-suppress.txt5
-rw-r--r--subprojects/libhandy/data/packaging/rpm/libhandy.spec60
-rw-r--r--subprojects/libhandy/debian/README.source29
-rw-r--r--subprojects/libhandy/debian/changelog1433
-rw-r--r--subprojects/libhandy/debian/control79
-rw-r--r--subprojects/libhandy/debian/copyright22
-rw-r--r--subprojects/libhandy/debian/docs1
-rw-r--r--subprojects/libhandy/debian/gir1.2-handy-1.install1
-rw-r--r--subprojects/libhandy/debian/handy-0.0-examples.examples1
-rw-r--r--subprojects/libhandy/debian/handy-0.0-examples.install1
-rw-r--r--subprojects/libhandy/debian/handy-1-examples.install1
-rw-r--r--subprojects/libhandy/debian/libhandy-1-0.install2
-rw-r--r--subprojects/libhandy/debian/libhandy-1-0.symbols328
-rw-r--r--subprojects/libhandy/debian/libhandy-1-dev.install8
-rwxr-xr-xsubprojects/libhandy/debian/rules16
-rw-r--r--subprojects/libhandy/debian/source/format1
-rwxr-xr-xsubprojects/libhandy/debian/tests/build-test30
-rw-r--r--subprojects/libhandy/debian/tests/control8
-rwxr-xr-xsubprojects/libhandy/debian/tests/python-gi-test13
-rw-r--r--subprojects/libhandy/doc/build-howto.xml159
-rw-r--r--subprojects/libhandy/doc/handy-docs.xml140
-rw-r--r--subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml356
-rw-r--r--subprojects/libhandy/doc/images/avatar.pngbin0 -> 24158 bytes
-rw-r--r--subprojects/libhandy/doc/images/header-bar.pngbin0 -> 8639 bytes
-rw-r--r--subprojects/libhandy/doc/images/keypad.pngbin0 -> 33968 bytes
-rw-r--r--subprojects/libhandy/doc/images/list.pngbin0 -> 34980 bytes
-rw-r--r--subprojects/libhandy/doc/images/preferences-window.pngbin0 -> 38792 bytes
-rw-r--r--subprojects/libhandy/doc/images/search.pngbin0 -> 17371 bytes
-rw-r--r--subprojects/libhandy/doc/images/view-switcher-bar.pngbin0 -> 9332 bytes
-rw-r--r--subprojects/libhandy/doc/images/view-switcher.pngbin0 -> 10727 bytes
-rw-r--r--subprojects/libhandy/doc/meson.build75
-rw-r--r--subprojects/libhandy/doc/visual-index.xml58
-rw-r--r--subprojects/libhandy/doc/xml/gtkdocentities.ent.in9
-rw-r--r--subprojects/libhandy/doc/xml/meson.build12
-rwxr-xr-xsubprojects/libhandy/examples/example.py32
-rw-r--r--subprojects/libhandy/examples/handy-demo.c65
-rw-r--r--subprojects/libhandy/examples/handy-demo.gresources.xml27
-rw-r--r--subprojects/libhandy/examples/hdy-demo-preferences-window.c56
-rw-r--r--subprojects/libhandy/examples/hdy-demo-preferences-window.h13
-rw-r--r--subprojects/libhandy/examples/hdy-demo-preferences-window.ui256
-rw-r--r--subprojects/libhandy/examples/hdy-demo-window.c573
-rw-r--r--subprojects/libhandy/examples/hdy-demo-window.h13
-rw-r--r--subprojects/libhandy/examples/hdy-demo-window.ui2352
-rw-r--r--subprojects/libhandy/examples/hdy-view-switcher-demo-window.c30
-rw-r--r--subprojects/libhandy/examples/hdy-view-switcher-demo-window.h13
-rw-r--r--subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui109
-rw-r--r--subprojects/libhandy/examples/icons/dark-mode-symbolic.svg68
-rw-r--r--subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg179
-rw-r--r--subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg179
-rw-r--r--subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg187
-rw-r--r--subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg187
-rw-r--r--subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg59
-rw-r--r--subprojects/libhandy/examples/icons/light-mode-symbolic.svg6
-rw-r--r--subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg66
-rw-r--r--subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-deck-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-list-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-search-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg1
-rw-r--r--subprojects/libhandy/examples/icons/widget-window-symbolic.svg3
-rw-r--r--subprojects/libhandy/examples/meson.build26
-rw-r--r--subprojects/libhandy/examples/sm.puri.Handy.Demo.json29
-rw-r--r--subprojects/libhandy/examples/style.css8
-rw-r--r--subprojects/libhandy/glade/glade-catalog.dtd196
-rw-r--r--subprojects/libhandy/glade/glade-hdy-carousel.c460
-rw-r--r--subprojects/libhandy/glade/glade-hdy-carousel.h56
-rw-r--r--subprojects/libhandy/glade/glade-hdy-expander-row.c105
-rw-r--r--subprojects/libhandy/glade/glade-hdy-expander-row.h39
-rw-r--r--subprojects/libhandy/glade/glade-hdy-header-bar.c547
-rw-r--r--subprojects/libhandy/glade/glade-hdy-header-bar.h59
-rw-r--r--subprojects/libhandy/glade/glade-hdy-header-group.c162
-rw-r--r--subprojects/libhandy/glade/glade-hdy-header-group.h24
-rw-r--r--subprojects/libhandy/glade/glade-hdy-leaflet.c533
-rw-r--r--subprojects/libhandy/glade/glade-hdy-leaflet.h56
-rw-r--r--subprojects/libhandy/glade/glade-hdy-preferences-page.c218
-rw-r--r--subprojects/libhandy/glade/glade-hdy-preferences-page.h50
-rw-r--r--subprojects/libhandy/glade/glade-hdy-preferences-window.c286
-rw-r--r--subprojects/libhandy/glade/glade-hdy-preferences-window.h50
-rw-r--r--subprojects/libhandy/glade/glade-hdy-search-bar.c107
-rw-r--r--subprojects/libhandy/glade/glade-hdy-search-bar.h34
-rw-r--r--subprojects/libhandy/glade/glade-hdy-swipe-group.c161
-rw-r--r--subprojects/libhandy/glade/glade-hdy-swipe-group.h28
-rw-r--r--subprojects/libhandy/glade/glade-hdy-utils.c121
-rw-r--r--subprojects/libhandy/glade/glade-hdy-utils.h36
-rw-r--r--subprojects/libhandy/glade/glade-hdy-window.c108
-rw-r--r--subprojects/libhandy/glade/glade-hdy-window.h34
-rw-r--r--subprojects/libhandy/glade/libhandy.xml420
-rw-r--r--subprojects/libhandy/glade/meson.build59
-rw-r--r--subprojects/libhandy/glade/rename-id.patch28
-rw-r--r--subprojects/libhandy/glade/sm.puri.Handy.Glade.json83
-rw-r--r--subprojects/libhandy/libhandy.doap37
-rw-r--r--subprojects/libhandy/libhandy.syms6
-rwxr-xr-xsubprojects/libhandy/lint/api-visibility.sh18
-rw-r--r--subprojects/libhandy/meson.build156
-rw-r--r--subprojects/libhandy/meson_options.txt25
-rw-r--r--subprojects/libhandy/po/LINGUAS6
-rw-r--r--subprojects/libhandy/po/POTFILES.in40
-rw-r--r--subprojects/libhandy/po/POTFILES.skip6
-rw-r--r--subprojects/libhandy/po/en_GB.po866
-rw-r--r--subprojects/libhandy/po/es.po850
-rw-r--r--subprojects/libhandy/po/meson.build2
-rw-r--r--subprojects/libhandy/po/pl.po76
-rw-r--r--subprojects/libhandy/po/pt_BR.po955
-rw-r--r--subprojects/libhandy/po/ro.po871
-rw-r--r--subprojects/libhandy/po/uk.po921
-rwxr-xr-xsubprojects/libhandy/run.in12
-rw-r--r--subprojects/libhandy/src/gen-public-types.sh22
-rw-r--r--subprojects/libhandy/src/gtk-window-private.h18
-rw-r--r--subprojects/libhandy/src/gtk-window.c169
-rw-r--r--subprojects/libhandy/src/gtkprogresstracker.c248
-rw-r--r--subprojects/libhandy/src/gtkprogresstrackerprivate.h74
-rw-r--r--subprojects/libhandy/src/handy.gresources.xml28
-rw-r--r--subprojects/libhandy/src/handy.h63
-rw-r--r--subprojects/libhandy/src/hdy-action-row.c774
-rw-r--r--subprojects/libhandy/src/hdy-action-row.h73
-rw-r--r--subprojects/libhandy/src/hdy-action-row.ui82
-rw-r--r--subprojects/libhandy/src/hdy-animation-private.h19
-rw-r--r--subprojects/libhandy/src/hdy-animation.c83
-rw-r--r--subprojects/libhandy/src/hdy-animation.h25
-rw-r--r--subprojects/libhandy/src/hdy-application-window.c150
-rw-r--r--subprojects/libhandy/src/hdy-application-window.h35
-rw-r--r--subprojects/libhandy/src/hdy-avatar.c811
-rw-r--r--subprojects/libhandy/src/hdy-avatar.h70
-rw-r--r--subprojects/libhandy/src/hdy-cairo-private.h21
-rw-r--r--subprojects/libhandy/src/hdy-carousel-box-private.h65
-rw-r--r--subprojects/libhandy/src/hdy-carousel-box.c1768
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-dots.c486
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-dots.h34
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-lines.c485
-rw-r--r--subprojects/libhandy/src/hdy-carousel-indicator-lines.h34
-rw-r--r--subprojects/libhandy/src/hdy-carousel.c1099
-rw-r--r--subprojects/libhandy/src/hdy-carousel.h81
-rw-r--r--subprojects/libhandy/src/hdy-carousel.ui21
-rw-r--r--subprojects/libhandy/src/hdy-clamp.c563
-rw-r--r--subprojects/libhandy/src/hdy-clamp.h37
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.c829
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.h112
-rw-r--r--subprojects/libhandy/src/hdy-combo-row.ui43
-rw-r--r--subprojects/libhandy/src/hdy-css-private.h25
-rw-r--r--subprojects/libhandy/src/hdy-css.c73
-rw-r--r--subprojects/libhandy/src/hdy-deck.c1103
-rw-r--r--subprojects/libhandy/src/hdy-deck.h102
-rw-r--r--subprojects/libhandy/src/hdy-deprecation-macros.h31
-rw-r--r--subprojects/libhandy/src/hdy-enum-value-object.c73
-rw-r--r--subprojects/libhandy/src/hdy-enum-value-object.h35
-rw-r--r--subprojects/libhandy/src/hdy-enums-private.c.in38
-rw-r--r--subprojects/libhandy/src/hdy-enums-private.h.in27
-rw-r--r--subprojects/libhandy/src/hdy-enums.c.in44
-rw-r--r--subprojects/libhandy/src/hdy-enums.h.in28
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.c762
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.h83
-rw-r--r--subprojects/libhandy/src/hdy-expander-row.ui97
-rw-r--r--subprojects/libhandy/src/hdy-header-bar.c2868
-rw-r--r--subprojects/libhandy/src/hdy-header-bar.h123
-rw-r--r--subprojects/libhandy/src/hdy-header-group.c1115
-rw-r--r--subprojects/libhandy/src/hdy-header-group.h81
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button-private.h32
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button.c331
-rw-r--r--subprojects/libhandy/src/hdy-keypad-button.ui36
-rw-r--r--subprojects/libhandy/src/hdy-keypad.c793
-rw-r--r--subprojects/libhandy/src/hdy-keypad.h76
-rw-r--r--subprojects/libhandy/src/hdy-keypad.ui216
-rw-r--r--subprojects/libhandy/src/hdy-leaflet.c1209
-rw-r--r--subprojects/libhandy/src/hdy-leaflet.h113
-rw-r--r--subprojects/libhandy/src/hdy-main-private.h24
-rw-r--r--subprojects/libhandy/src/hdy-main.c201
-rw-r--r--subprojects/libhandy/src/hdy-main.h21
-rw-r--r--subprojects/libhandy/src/hdy-navigation-direction.c26
-rw-r--r--subprojects/libhandy/src/hdy-navigation-direction.h23
-rw-r--r--subprojects/libhandy/src/hdy-nothing-private.h23
-rw-r--r--subprojects/libhandy/src/hdy-nothing.c47
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group-private.h16
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.c449
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-group.ui55
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page-private.h18
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.c365
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-page.ui34
-rw-r--r--subprojects/libhandy/src/hdy-preferences-row.c259
-rw-r--r--subprojects/libhandy/src/hdy-preferences-row.h51
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.c721
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.h58
-rw-r--r--subprojects/libhandy/src/hdy-preferences-window.ui248
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.c659
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.h51
-rw-r--r--subprojects/libhandy/src/hdy-search-bar.ui63
-rw-r--r--subprojects/libhandy/src/hdy-shadow-helper-private.h32
-rw-r--r--subprojects/libhandy/src/hdy-shadow-helper.c445
-rw-r--r--subprojects/libhandy/src/hdy-squeezer.c1576
-rw-r--r--subprojects/libhandy/src/hdy-squeezer.h83
-rw-r--r--subprojects/libhandy/src/hdy-stackable-box-private.h133
-rw-r--r--subprojects/libhandy/src/hdy-stackable-box.c3151
-rw-r--r--subprojects/libhandy/src/hdy-swipe-group.c568
-rw-r--r--subprojects/libhandy/src/hdy-swipe-group.h37
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker-private.h26
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker.c1113
-rw-r--r--subprojects/libhandy/src/hdy-swipe-tracker.h53
-rw-r--r--subprojects/libhandy/src/hdy-swipeable.c273
-rw-r--r--subprojects/libhandy/src/hdy-swipeable.h91
-rw-r--r--subprojects/libhandy/src/hdy-title-bar.c347
-rw-r--r--subprojects/libhandy/src/hdy-title-bar.h33
-rw-r--r--subprojects/libhandy/src/hdy-types.h17
-rw-r--r--subprojects/libhandy/src/hdy-value-object.c267
-rw-r--r--subprojects/libhandy/src/hdy-value-object.h45
-rw-r--r--subprojects/libhandy/src/hdy-version.h.in87
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.c392
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.h48
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-bar.ui20
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button-private.h49
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button.c536
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-button.ui105
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.c600
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.h63
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher-title.ui52
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher.c734
-rw-r--r--subprojects/libhandy/src/hdy-view-switcher.h52
-rw-r--r--subprojects/libhandy/src/hdy-window-handle-controller-private.h23
-rw-r--r--subprojects/libhandy/src/hdy-window-handle-controller.c515
-rw-r--r--subprojects/libhandy/src/hdy-window-handle.c83
-rw-r--r--subprojects/libhandy/src/hdy-window-handle.h27
-rw-r--r--subprojects/libhandy/src/hdy-window-mixin-private.h42
-rw-r--r--subprojects/libhandy/src/hdy-window-mixin.c583
-rw-r--r--subprojects/libhandy/src/hdy-window.c195
-rw-r--r--subprojects/libhandy/src/hdy-window.h35
-rw-r--r--subprojects/libhandy/src/icons/avatar-default-symbolic.svg3
-rw-r--r--subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg7
-rw-r--r--subprojects/libhandy/src/meson.build298
-rw-r--r--subprojects/libhandy/src/themes/Adwaita-dark.css197
-rw-r--r--subprojects/libhandy/src/themes/Adwaita-dark.scss5
-rw-r--r--subprojects/libhandy/src/themes/Adwaita.css197
-rw-r--r--subprojects/libhandy/src/themes/Adwaita.scss5
-rw-r--r--subprojects/libhandy/src/themes/HighContrast.css197
-rw-r--r--subprojects/libhandy/src/themes/HighContrast.scss6
-rw-r--r--subprojects/libhandy/src/themes/HighContrastInverse.css197
-rw-r--r--subprojects/libhandy/src/themes/HighContrastInverse.scss6
-rw-r--r--subprojects/libhandy/src/themes/_Adwaita-base.scss336
-rw-r--r--subprojects/libhandy/src/themes/_definitions.scss66
-rw-r--r--subprojects/libhandy/src/themes/_fallback-base.scss146
-rw-r--r--subprojects/libhandy/src/themes/_shared-base.scss21
-rw-r--r--subprojects/libhandy/src/themes/fallback.css74
-rw-r--r--subprojects/libhandy/src/themes/fallback.scss5
-rwxr-xr-xsubprojects/libhandy/src/themes/parse-sass.sh44
-rw-r--r--subprojects/libhandy/src/themes/shared.css6
-rw-r--r--subprojects/libhandy/src/themes/shared.scss5
-rw-r--r--subprojects/libhandy/tests/meson.build59
-rw-r--r--subprojects/libhandy/tests/test-action-row.c129
-rw-r--r--subprojects/libhandy/tests/test-application-window.c30
-rw-r--r--subprojects/libhandy/tests/test-avatar.c210
-rw-r--r--subprojects/libhandy/tests/test-carousel-indicator-dots.c53
-rw-r--r--subprojects/libhandy/tests/test-carousel-indicator-lines.c53
-rw-r--r--subprojects/libhandy/tests/test-carousel.c246
-rw-r--r--subprojects/libhandy/tests/test-combo-row.c67
-rw-r--r--subprojects/libhandy/tests/test-deck.c93
-rw-r--r--subprojects/libhandy/tests/test-expander-row.c153
-rw-r--r--subprojects/libhandy/tests/test-header-bar.c215
-rw-r--r--subprojects/libhandy/tests/test-header-group.c68
-rw-r--r--subprojects/libhandy/tests/test-keypad.c247
-rw-r--r--subprojects/libhandy/tests/test-leaflet.c109
-rw-r--r--subprojects/libhandy/tests/test-preferences-group.c81
-rw-r--r--subprojects/libhandy/tests/test-preferences-page.c80
-rw-r--r--subprojects/libhandy/tests/test-preferences-row.c57
-rw-r--r--subprojects/libhandy/tests/test-preferences-window.c42
-rw-r--r--subprojects/libhandy/tests/test-search-bar.c96
-rw-r--r--subprojects/libhandy/tests/test-squeezer.c161
-rw-r--r--subprojects/libhandy/tests/test-swipe-group.c45
-rw-r--r--subprojects/libhandy/tests/test-value-object.c54
-rw-r--r--subprojects/libhandy/tests/test-view-switcher-bar.c83
-rw-r--r--subprojects/libhandy/tests/test-view-switcher.c83
-rw-r--r--subprojects/libhandy/tests/test-window-handle.c30
-rw-r--r--subprojects/libhandy/tests/test-window.c30
310 files changed, 64653 insertions, 0 deletions
diff --git a/subprojects/gvc/README.md b/subprojects/gvc/README.md
new file mode 100644
index 0000000..2fabe49
--- /dev/null
+++ b/subprojects/gvc/README.md
@@ -0,0 +1,12 @@
+# libgnome-volume-control
+
+libgnome-volume-control is a copy library that's supposed to be used as
+a git sub-module. If your project uses some of libgnome-volume-control's
+strings in a user-facing manner, don't forget to add those files to your
+POTFILES.in for translation.
+
+## Projects using libgnome-volume-control
+
+- [gnome-shell](https://gitlab.gnome.org/GNOME/gnome-shell)
+- [gnome-settings-daemon](https://gitlab.gnome.org/GNOME/gnome-settings-daemon)
+- [gnome-control-center](https://gitlab.gnome.org/GNOME/gnome-control-center)
diff --git a/subprojects/gvc/gvc-channel-map-private.h b/subprojects/gvc/gvc-channel-map-private.h
new file mode 100644
index 0000000..3949de3
--- /dev/null
+++ b/subprojects/gvc/gvc-channel-map-private.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_CHANNEL_MAP_PRIVATE_H
+#define __GVC_CHANNEL_MAP_PRIVATE_H
+
+#include <glib-object.h>
+#include <pulse/pulseaudio.h>
+
+G_BEGIN_DECLS
+
+GvcChannelMap * gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *map);
+const pa_channel_map * gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map);
+
+void gvc_channel_map_volume_changed (GvcChannelMap *map,
+ const pa_cvolume *cv,
+ gboolean set);
+const pa_cvolume * gvc_channel_map_get_cvolume (const GvcChannelMap *map);
+
+G_END_DECLS
+
+#endif /* __GVC_CHANNEL_MAP_PRIVATE_H */
diff --git a/subprojects/gvc/gvc-channel-map.c b/subprojects/gvc/gvc-channel-map.c
new file mode 100644
index 0000000..bf4d737
--- /dev/null
+++ b/subprojects/gvc/gvc-channel-map.c
@@ -0,0 +1,247 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-channel-map.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcChannelMapPrivate
+{
+ pa_channel_map pa_map;
+ gboolean pa_volume_is_set;
+ pa_cvolume pa_volume;
+ gdouble extern_volume[NUM_TYPES]; /* volume, balance, fade, lfe */
+ gboolean can_balance;
+ gboolean can_fade;
+};
+
+enum {
+ VOLUME_CHANGED,
+ LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+static void gvc_channel_map_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcChannelMap, gvc_channel_map, G_TYPE_OBJECT)
+
+guint
+gvc_channel_map_get_num_channels (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), 0);
+
+ if (!pa_channel_map_valid(&map->priv->pa_map))
+ return 0;
+
+ return map->priv->pa_map.channels;
+}
+
+const gdouble *
+gvc_channel_map_get_volume (GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+ if (!pa_channel_map_valid(&map->priv->pa_map))
+ return NULL;
+
+ map->priv->extern_volume[VOLUME] = (gdouble) pa_cvolume_max (&map->priv->pa_volume);
+ if (gvc_channel_map_can_balance (map))
+ map->priv->extern_volume[BALANCE] = (gdouble) pa_cvolume_get_balance (&map->priv->pa_volume, &map->priv->pa_map);
+ else
+ map->priv->extern_volume[BALANCE] = 0;
+ if (gvc_channel_map_can_fade (map))
+ map->priv->extern_volume[FADE] = (gdouble) pa_cvolume_get_fade (&map->priv->pa_volume, &map->priv->pa_map);
+ else
+ map->priv->extern_volume[FADE] = 0;
+ if (gvc_channel_map_has_lfe (map))
+ map->priv->extern_volume[LFE] = (gdouble) pa_cvolume_get_position (&map->priv->pa_volume, &map->priv->pa_map, PA_CHANNEL_POSITION_LFE);
+ else
+ map->priv->extern_volume[LFE] = 0;
+
+ return map->priv->extern_volume;
+}
+
+gboolean
+gvc_channel_map_can_balance (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+ return map->priv->can_balance;
+}
+
+gboolean
+gvc_channel_map_can_fade (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+ return map->priv->can_fade;
+}
+
+const char *
+gvc_channel_map_get_mapping (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+ if (!pa_channel_map_valid(&map->priv->pa_map))
+ return NULL;
+
+ return pa_channel_map_to_pretty_name (&map->priv->pa_map);
+}
+
+/**
+ * gvc_channel_map_has_position: (skip)
+ * @map:
+ * @position:
+ *
+ * Returns:
+ */
+gboolean
+gvc_channel_map_has_position (const GvcChannelMap *map,
+ pa_channel_position_t position)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), FALSE);
+
+ return pa_channel_map_has_position (&(map->priv->pa_map), position);
+}
+
+const pa_channel_map *
+gvc_channel_map_get_pa_channel_map (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+ if (!pa_channel_map_valid(&map->priv->pa_map))
+ return NULL;
+
+ return &map->priv->pa_map;
+}
+
+const pa_cvolume *
+gvc_channel_map_get_cvolume (const GvcChannelMap *map)
+{
+ g_return_val_if_fail (GVC_IS_CHANNEL_MAP (map), NULL);
+
+ if (!pa_channel_map_valid(&map->priv->pa_map))
+ return NULL;
+
+ return &map->priv->pa_volume;
+}
+
+static void
+gvc_channel_map_class_init (GvcChannelMapClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->finalize = gvc_channel_map_finalize;
+
+ signals [VOLUME_CHANGED] =
+ g_signal_new ("volume-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcChannelMapClass, volume_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__BOOLEAN,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+}
+
+void
+gvc_channel_map_volume_changed (GvcChannelMap *map,
+ const pa_cvolume *cv,
+ gboolean set)
+{
+ g_return_if_fail (GVC_IS_CHANNEL_MAP (map));
+ g_return_if_fail (cv != NULL);
+ g_return_if_fail (pa_cvolume_compatible_with_channel_map(cv, &map->priv->pa_map));
+
+ if (pa_cvolume_equal(cv, &map->priv->pa_volume))
+ return;
+
+ map->priv->pa_volume = *cv;
+
+ if (map->priv->pa_volume_is_set == FALSE) {
+ map->priv->pa_volume_is_set = TRUE;
+ return;
+ }
+ g_signal_emit (map, signals[VOLUME_CHANGED], 0, set);
+}
+
+static void
+gvc_channel_map_init (GvcChannelMap *map)
+{
+ map->priv = gvc_channel_map_get_instance_private (map);
+ map->priv->pa_volume_is_set = FALSE;
+}
+
+static void
+gvc_channel_map_finalize (GObject *object)
+{
+ GvcChannelMap *channel_map;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_CHANNEL_MAP (object));
+
+ channel_map = GVC_CHANNEL_MAP (object);
+
+ g_return_if_fail (channel_map->priv != NULL);
+
+ G_OBJECT_CLASS (gvc_channel_map_parent_class)->finalize (object);
+}
+
+GvcChannelMap *
+gvc_channel_map_new (void)
+{
+ GObject *map;
+ map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL);
+ return GVC_CHANNEL_MAP (map);
+}
+
+static void
+set_from_pa_map (GvcChannelMap *map,
+ const pa_channel_map *pa_map)
+{
+ g_assert (pa_channel_map_valid(pa_map));
+
+ map->priv->can_balance = pa_channel_map_can_balance (pa_map);
+ map->priv->can_fade = pa_channel_map_can_fade (pa_map);
+
+ map->priv->pa_map = *pa_map;
+ pa_cvolume_set(&map->priv->pa_volume, pa_map->channels, PA_VOLUME_NORM);
+}
+
+GvcChannelMap *
+gvc_channel_map_new_from_pa_channel_map (const pa_channel_map *pa_map)
+{
+ GObject *map;
+ map = g_object_new (GVC_TYPE_CHANNEL_MAP, NULL);
+
+ set_from_pa_map (GVC_CHANNEL_MAP (map), pa_map);
+
+ return GVC_CHANNEL_MAP (map);
+}
diff --git a/subprojects/gvc/gvc-channel-map.h b/subprojects/gvc/gvc-channel-map.h
new file mode 100644
index 0000000..85c5772
--- /dev/null
+++ b/subprojects/gvc/gvc-channel-map.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_CHANNEL_MAP_H
+#define __GVC_CHANNEL_MAP_H
+
+#include <glib-object.h>
+#include <gvc-pulseaudio-fake.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_CHANNEL_MAP (gvc_channel_map_get_type ())
+#define GVC_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMap))
+#define GVC_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass))
+#define GVC_IS_CHANNEL_MAP(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_CHANNEL_MAP))
+#define GVC_IS_CHANNEL_MAP_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_CHANNEL_MAP))
+#define GVC_CHANNEL_MAP_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_CHANNEL_MAP, GvcChannelMapClass))
+
+typedef struct GvcChannelMapPrivate GvcChannelMapPrivate;
+
+typedef struct
+{
+ GObject parent;
+ GvcChannelMapPrivate *priv;
+} GvcChannelMap;
+
+typedef struct
+{
+ GObjectClass parent_class;
+ void (*volume_changed) (GvcChannelMap *channel_map, gboolean set);
+} GvcChannelMapClass;
+
+enum {
+ VOLUME,
+ BALANCE,
+ FADE,
+ LFE,
+ NUM_TYPES
+};
+
+GType gvc_channel_map_get_type (void);
+
+GvcChannelMap * gvc_channel_map_new (void);
+guint gvc_channel_map_get_num_channels (const GvcChannelMap *map);
+const gdouble * gvc_channel_map_get_volume (GvcChannelMap *map);
+gboolean gvc_channel_map_can_balance (const GvcChannelMap *map);
+gboolean gvc_channel_map_can_fade (const GvcChannelMap *map);
+gboolean gvc_channel_map_has_position (const GvcChannelMap *map,
+ pa_channel_position_t position);
+#define gvc_channel_map_has_lfe(x) gvc_channel_map_has_position (x, PA_CHANNEL_POSITION_LFE)
+
+const char * gvc_channel_map_get_mapping (const GvcChannelMap *map);
+
+G_END_DECLS
+
+#endif /* __GVC_CHANNEL_MAP_H */
diff --git a/subprojects/gvc/gvc-mixer-card-private.h b/subprojects/gvc/gvc-mixer-card-private.h
new file mode 100644
index 0000000..e190f7f
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-card-private.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2009 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CARD_PRIVATE_H
+#define __GVC_MIXER_CARD_PRIVATE_H
+
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-card.h"
+
+G_BEGIN_DECLS
+
+GvcMixerCard * gvc_mixer_card_new (pa_context *context,
+ guint index);
+pa_context * gvc_mixer_card_get_pa_context (GvcMixerCard *card);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CARD_PRIVATE_H */
diff --git a/subprojects/gvc/gvc-mixer-card.c b/subprojects/gvc/gvc-mixer-card.c
new file mode 100644
index 0000000..93be4da
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-card.c
@@ -0,0 +1,584 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ * Copyright (C) 2009 Bastien Nocera
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-card-private.h"
+
+static guint32 card_serial = 1;
+
+struct GvcMixerCardPrivate
+{
+ pa_context *pa_context;
+ guint id;
+ guint index;
+ char *name;
+ char *icon_name;
+ char *profile;
+ char *target_profile;
+ char *human_profile;
+ GList *profiles;
+ pa_operation *profile_op;
+ GList *ports;
+};
+
+enum
+{
+ PROP_0,
+ PROP_ID,
+ PROP_PA_CONTEXT,
+ PROP_INDEX,
+ PROP_NAME,
+ PROP_ICON_NAME,
+ PROP_PROFILE,
+ PROP_HUMAN_PROFILE,
+};
+
+static void gvc_mixer_card_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerCard, gvc_mixer_card, G_TYPE_OBJECT)
+
+static guint32
+get_next_card_serial (void)
+{
+ guint32 serial;
+
+ serial = card_serial++;
+
+ if ((gint32)card_serial < 0) {
+ card_serial = 1;
+ }
+
+ return serial;
+}
+
+pa_context *
+gvc_mixer_card_get_pa_context (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+ return card->priv->pa_context;
+}
+
+guint
+gvc_mixer_card_get_index (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+ return card->priv->index;
+}
+
+guint
+gvc_mixer_card_get_id (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), 0);
+ return card->priv->id;
+}
+
+const char *
+gvc_mixer_card_get_name (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+ return card->priv->name;
+}
+
+gboolean
+gvc_mixer_card_set_name (GvcMixerCard *card,
+ const char *name)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+
+ g_free (card->priv->name);
+ card->priv->name = g_strdup (name);
+ g_object_notify (G_OBJECT (card), "name");
+
+ return TRUE;
+}
+
+const char *
+gvc_mixer_card_get_icon_name (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+ return card->priv->icon_name;
+}
+
+gboolean
+gvc_mixer_card_set_icon_name (GvcMixerCard *card,
+ const char *icon_name)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+
+ g_free (card->priv->icon_name);
+ card->priv->icon_name = g_strdup (icon_name);
+ g_object_notify (G_OBJECT (card), "icon-name");
+
+ return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_profile: (skip)
+ * @card:
+ *
+ * Returns:
+ */
+GvcMixerCardProfile *
+gvc_mixer_card_get_profile (GvcMixerCard *card)
+{
+ GList *l;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+ g_return_val_if_fail (card->priv->profiles != NULL, NULL);
+
+ for (l = card->priv->profiles; l != NULL; l = l->next) {
+ GvcMixerCardProfile *p = l->data;
+ if (g_str_equal (card->priv->profile, p->profile)) {
+ return p;
+ }
+ }
+
+ g_assert_not_reached ();
+
+ return NULL;
+}
+
+gboolean
+gvc_mixer_card_set_profile (GvcMixerCard *card,
+ const char *profile)
+{
+ GList *l;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+ g_return_val_if_fail (card->priv->profiles != NULL, FALSE);
+
+ g_free (card->priv->profile);
+ card->priv->profile = g_strdup (profile);
+
+ g_free (card->priv->human_profile);
+ card->priv->human_profile = NULL;
+
+ for (l = card->priv->profiles; l != NULL; l = l->next) {
+ GvcMixerCardProfile *p = l->data;
+ if (g_str_equal (card->priv->profile, p->profile)) {
+ card->priv->human_profile = g_strdup (p->human_profile);
+ break;
+ }
+ }
+
+ g_object_notify (G_OBJECT (card), "profile");
+
+ return TRUE;
+}
+
+static void
+_pa_context_set_card_profile_by_index_cb (pa_context *context,
+ int success,
+ void *userdata)
+{
+ GvcMixerCard *card = GVC_MIXER_CARD (userdata);
+
+ g_assert (card->priv->target_profile);
+
+ if (success > 0) {
+ gvc_mixer_card_set_profile (card, card->priv->target_profile);
+ } else {
+ g_debug ("Failed to switch profile on '%s' from '%s' to '%s'",
+ card->priv->name,
+ card->priv->profile,
+ card->priv->target_profile);
+ }
+ g_free (card->priv->target_profile);
+ card->priv->target_profile = NULL;
+
+ pa_operation_unref (card->priv->profile_op);
+ card->priv->profile_op = NULL;
+}
+
+/**
+ * gvc_mixer_card_change_profile:
+ * @card: a #GvcMixerCard
+ * @profile: (allow-none): the profile to change to or %NULL.
+ *
+ * Change the profile in use on this card.
+ *
+ * Returns: %TRUE if profile successfully changed or already using this profile.
+ */
+gboolean
+gvc_mixer_card_change_profile (GvcMixerCard *card,
+ const char *profile)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+ g_return_val_if_fail (card->priv->profiles != NULL, FALSE);
+
+ /* Same profile, or already requested? */
+ if (g_strcmp0 (card->priv->profile, profile) == 0)
+ return TRUE;
+ if (g_strcmp0 (profile, card->priv->target_profile) == 0)
+ return TRUE;
+ if (card->priv->profile_op != NULL) {
+ pa_operation_cancel (card->priv->profile_op);
+ pa_operation_unref (card->priv->profile_op);
+ card->priv->profile_op = NULL;
+ }
+
+ if (card->priv->profile != NULL) {
+ g_free (card->priv->target_profile);
+ card->priv->target_profile = g_strdup (profile);
+
+ card->priv->profile_op = pa_context_set_card_profile_by_index (card->priv->pa_context,
+ card->priv->index,
+ card->priv->target_profile,
+ _pa_context_set_card_profile_by_index_cb,
+ card);
+
+ if (card->priv->profile_op == NULL) {
+ g_warning ("pa_context_set_card_profile_by_index() failed");
+ return FALSE;
+ }
+ } else {
+ g_assert (card->priv->human_profile == NULL);
+ card->priv->profile = g_strdup (profile);
+ }
+
+ return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_profiles:
+ *
+ * Return value: (transfer none) (element-type GvcMixerCardProfile):
+ */
+const GList *
+gvc_mixer_card_get_profiles (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+ return card->priv->profiles;
+}
+
+/**
+ * gvc_mixer_card_get_ports:
+ *
+ * Return value: (transfer none) (element-type GvcMixerCardPort):
+ */
+const GList *
+gvc_mixer_card_get_ports (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+ return card->priv->ports;
+}
+
+/**
+ * gvc_mixer_card_profile_compare:
+ *
+ * Return value: 1 if @a has a higher priority, -1 if @b has a higher
+ * priority, 0 if @a and @b have the same priority.
+ */
+int
+gvc_mixer_card_profile_compare (GvcMixerCardProfile *a,
+ GvcMixerCardProfile *b)
+{
+ if (a->priority == b->priority)
+ return 0;
+ if (a->priority > b->priority)
+ return 1;
+ return -1;
+}
+
+/**
+ * gvc_mixer_card_set_profiles:
+ * @profiles: (transfer full) (element-type GvcMixerCardProfile):
+ */
+gboolean
+gvc_mixer_card_set_profiles (GvcMixerCard *card,
+ GList *profiles)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+ g_return_val_if_fail (card->priv->profiles == NULL, FALSE);
+
+ card->priv->profiles = g_list_sort (profiles, (GCompareFunc) gvc_mixer_card_profile_compare);
+
+ return TRUE;
+}
+
+/**
+ * gvc_mixer_card_get_gicon:
+ * @card:
+ *
+ * Return value: (transfer full):
+ */
+GIcon *
+gvc_mixer_card_get_gicon (GvcMixerCard *card)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), NULL);
+
+ if (card->priv->icon_name == NULL)
+ return NULL;
+
+ return g_themed_icon_new_with_default_fallbacks (card->priv->icon_name);
+}
+
+static void
+free_port (GvcMixerCardPort *port)
+{
+ g_free (port->port);
+ g_free (port->human_port);
+ g_free (port->icon_name);
+ g_list_free (port->profiles);
+
+ g_free (port);
+}
+
+/**
+ * gvc_mixer_card_set_ports:
+ * @ports: (transfer full) (element-type GvcMixerCardPort):
+ */
+gboolean
+gvc_mixer_card_set_ports (GvcMixerCard *card,
+ GList *ports)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CARD (card), FALSE);
+ g_return_val_if_fail (card->priv->ports == NULL, FALSE);
+
+ g_list_free_full (card->priv->ports, (GDestroyNotify) free_port);
+ card->priv->ports = ports;
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_card_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerCard *self = GVC_MIXER_CARD (object);
+
+ switch (prop_id) {
+ case PROP_PA_CONTEXT:
+ self->priv->pa_context = g_value_get_pointer (value);
+ break;
+ case PROP_INDEX:
+ self->priv->index = g_value_get_ulong (value);
+ break;
+ case PROP_ID:
+ self->priv->id = g_value_get_ulong (value);
+ break;
+ case PROP_NAME:
+ gvc_mixer_card_set_name (self, g_value_get_string (value));
+ break;
+ case PROP_ICON_NAME:
+ gvc_mixer_card_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_PROFILE:
+ gvc_mixer_card_set_profile (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_card_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerCard *self = GVC_MIXER_CARD (object);
+
+ switch (prop_id) {
+ case PROP_PA_CONTEXT:
+ g_value_set_pointer (value, self->priv->pa_context);
+ break;
+ case PROP_INDEX:
+ g_value_set_ulong (value, self->priv->index);
+ break;
+ case PROP_ID:
+ g_value_set_ulong (value, self->priv->id);
+ break;
+ case PROP_NAME:
+ g_value_set_string (value, self->priv->name);
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, self->priv->icon_name);
+ break;
+ case PROP_PROFILE:
+ g_value_set_string (value, self->priv->profile);
+ break;
+ case PROP_HUMAN_PROFILE:
+ g_value_set_string (value, self->priv->human_profile);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static GObject *
+gvc_mixer_card_constructor (GType type,
+ guint n_construct_properties,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GvcMixerCard *self;
+
+ object = G_OBJECT_CLASS (gvc_mixer_card_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+ self = GVC_MIXER_CARD (object);
+
+ self->priv->id = get_next_card_serial ();
+
+ return object;
+}
+
+static void
+gvc_mixer_card_class_init (GvcMixerCardClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructor = gvc_mixer_card_constructor;
+ gobject_class->finalize = gvc_mixer_card_finalize;
+
+ gobject_class->set_property = gvc_mixer_card_set_property;
+ gobject_class->get_property = gvc_mixer_card_get_property;
+
+ g_object_class_install_property (gobject_class,
+ PROP_INDEX,
+ g_param_spec_ulong ("index",
+ "Index",
+ "The index for this card",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_ID,
+ g_param_spec_ulong ("id",
+ "id",
+ "The id for this card",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_PA_CONTEXT,
+ g_param_spec_pointer ("pa-context",
+ "PulseAudio context",
+ "The PulseAudio context for this card",
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_NAME,
+ g_param_spec_string ("name",
+ "Name",
+ "Name to display for this card",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_ICON_NAME,
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "Name of icon to display for this card",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_PROFILE,
+ g_param_spec_string ("profile",
+ "Profile",
+ "Name of current profile for this card",
+ NULL,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (gobject_class,
+ PROP_HUMAN_PROFILE,
+ g_param_spec_string ("human-profile",
+ "Profile (Human readable)",
+ "Name of current profile for this card in human readable form",
+ NULL,
+ G_PARAM_READABLE));
+}
+
+static void
+gvc_mixer_card_init (GvcMixerCard *card)
+{
+ card->priv = gvc_mixer_card_get_instance_private (card);
+}
+
+GvcMixerCard *
+gvc_mixer_card_new (pa_context *context,
+ guint index)
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_CARD,
+ "index", index,
+ "pa-context", context,
+ NULL);
+ return GVC_MIXER_CARD (object);
+}
+
+static void
+free_profile (GvcMixerCardProfile *p)
+{
+ g_free (p->profile);
+ g_free (p->human_profile);
+ g_free (p->status);
+ g_free (p);
+}
+
+static void
+gvc_mixer_card_finalize (GObject *object)
+{
+ GvcMixerCard *mixer_card;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_CARD (object));
+
+ mixer_card = GVC_MIXER_CARD (object);
+
+ g_return_if_fail (mixer_card->priv != NULL);
+
+ g_free (mixer_card->priv->name);
+ mixer_card->priv->name = NULL;
+
+ g_free (mixer_card->priv->icon_name);
+ mixer_card->priv->icon_name = NULL;
+
+ g_free (mixer_card->priv->target_profile);
+ mixer_card->priv->target_profile = NULL;
+
+ g_free (mixer_card->priv->profile);
+ mixer_card->priv->profile = NULL;
+
+ g_free (mixer_card->priv->human_profile);
+ mixer_card->priv->human_profile = NULL;
+
+ g_list_free_full (mixer_card->priv->profiles, (GDestroyNotify) free_profile);
+ mixer_card->priv->profiles = NULL;
+
+ g_list_free_full (mixer_card->priv->ports, (GDestroyNotify) free_port);
+ mixer_card->priv->ports = NULL;
+
+ G_OBJECT_CLASS (gvc_mixer_card_parent_class)->finalize (object);
+}
+
diff --git a/subprojects/gvc/gvc-mixer-card.h b/subprojects/gvc/gvc-mixer-card.h
new file mode 100644
index 0000000..814f8d4
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-card.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008-2009 Red Hat, Inc.
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CARD_H
+#define __GVC_MIXER_CARD_H
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_CARD (gvc_mixer_card_get_type ())
+#define GVC_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CARD, GvcMixerCard))
+#define GVC_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CARD, GvcMixerCardClass))
+#define GVC_IS_MIXER_CARD(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CARD))
+#define GVC_IS_MIXER_CARD_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CARD))
+#define GVC_MIXER_CARD_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CARD, GvcMixerCardClass))
+
+typedef struct GvcMixerCardPrivate GvcMixerCardPrivate;
+
+typedef struct
+{
+ GObject parent;
+ GvcMixerCardPrivate *priv;
+} GvcMixerCard;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ /* vtable */
+} GvcMixerCardClass;
+
+typedef struct
+{
+ char *profile;
+ char *human_profile;
+ char *status;
+ guint priority;
+ guint n_sinks, n_sources;
+} GvcMixerCardProfile;
+
+typedef struct
+{
+ char *port;
+ char *human_port;
+ char *icon_name;
+ guint priority;
+ gint available;
+ gint direction;
+ GList *profiles;
+} GvcMixerCardPort;
+
+GType gvc_mixer_card_get_type (void);
+
+guint gvc_mixer_card_get_id (GvcMixerCard *card);
+guint gvc_mixer_card_get_index (GvcMixerCard *card);
+const char * gvc_mixer_card_get_name (GvcMixerCard *card);
+const char * gvc_mixer_card_get_icon_name (GvcMixerCard *card);
+GvcMixerCardProfile * gvc_mixer_card_get_profile (GvcMixerCard *card);
+const GList * gvc_mixer_card_get_profiles (GvcMixerCard *card);
+const GList * gvc_mixer_card_get_ports (GvcMixerCard *card);
+gboolean gvc_mixer_card_change_profile (GvcMixerCard *card,
+ const char *profile);
+GIcon * gvc_mixer_card_get_gicon (GvcMixerCard *card);
+
+int gvc_mixer_card_profile_compare (GvcMixerCardProfile *a,
+ GvcMixerCardProfile *b);
+
+/* private */
+gboolean gvc_mixer_card_set_name (GvcMixerCard *card,
+ const char *name);
+gboolean gvc_mixer_card_set_icon_name (GvcMixerCard *card,
+ const char *name);
+gboolean gvc_mixer_card_set_profile (GvcMixerCard *card,
+ const char *profile);
+gboolean gvc_mixer_card_set_profiles (GvcMixerCard *card,
+ GList *profiles);
+gboolean gvc_mixer_card_set_ports (GvcMixerCard *stream,
+ GList *ports);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CARD_H */
diff --git a/subprojects/gvc/gvc-mixer-control-private.h b/subprojects/gvc/gvc-mixer-control-private.h
new file mode 100644
index 0000000..ac79975
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-control-private.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CONTROL_PRIVATE_H
+#define __GVC_MIXER_CONTROL_PRIVATE_H
+
+#include <glib-object.h>
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-card.h"
+
+G_BEGIN_DECLS
+
+pa_context * gvc_mixer_control_get_pa_context (GvcMixerControl *control);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CONTROL_PRIVATE_H */
diff --git a/subprojects/gvc/gvc-mixer-control.c b/subprojects/gvc/gvc-mixer-control.c
new file mode 100644
index 0000000..8b39080
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-control.c
@@ -0,0 +1,3876 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2006-2008 Lennart Poettering
+ * Copyright (C) 2008 Sjoerd Simons <sjoerd@luon.net>
+ * Copyright (C) 2008 William Jon McCann
+ * Copyright (C) 2012 Conor Curran
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/glib-mainloop.h>
+#include <pulse/ext-stream-restore.h>
+
+#ifdef HAVE_ALSA
+#include <alsa/asoundlib.h>
+#endif /* HAVE_ALSA */
+
+#include "gvc-mixer-control.h"
+#include "gvc-mixer-sink.h"
+#include "gvc-mixer-source.h"
+#include "gvc-mixer-sink-input.h"
+#include "gvc-mixer-source-output.h"
+#include "gvc-mixer-event-role.h"
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-card-private.h"
+#include "gvc-channel-map-private.h"
+#include "gvc-mixer-control-private.h"
+#include "gvc-mixer-ui-device.h"
+
+#define RECONNECT_DELAY 5
+
+enum {
+ PROP_0,
+ PROP_NAME
+};
+
+struct GvcMixerControlPrivate
+{
+ pa_glib_mainloop *pa_mainloop;
+ pa_mainloop_api *pa_api;
+ pa_context *pa_context;
+ guint server_protocol_version;
+ int n_outstanding;
+ guint reconnect_id;
+ char *name;
+
+ gboolean default_sink_is_set;
+ guint default_sink_id;
+ char *default_sink_name;
+ gboolean default_source_is_set;
+ guint default_source_id;
+ char *default_source_name;
+
+ gboolean event_sink_input_is_set;
+ guint event_sink_input_id;
+
+ GHashTable *all_streams;
+ GHashTable *sinks; /* fixed outputs */
+ GHashTable *sources; /* fixed inputs */
+ GHashTable *sink_inputs; /* routable output streams */
+ GHashTable *source_outputs; /* routable input streams */
+ GHashTable *clients;
+ GHashTable *cards;
+
+ GvcMixerStream *new_default_sink_stream; /* new default sink stream, used in gvc_mixer_control_set_default_sink () */
+ GvcMixerStream *new_default_source_stream; /* new default source stream, used in gvc_mixer_control_set_default_source () */
+
+ GHashTable *ui_outputs; /* UI visible outputs */
+ GHashTable *ui_inputs; /* UI visible inputs */
+
+ /* When we change profile on a device that is not the server default sink,
+ * it will jump back to the default sink set by the server to prevent the
+ * audio setup from being 'outputless'.
+ *
+ * All well and good but then when we get the new stream created for the
+ * new profile how do we know that this is the intended default or selected
+ * device the user wishes to use. */
+ guint profile_swapping_device_id;
+
+#ifdef HAVE_ALSA
+ int headset_card;
+ gboolean has_headsetmic;
+ gboolean has_headphonemic;
+ gboolean headset_plugged_in;
+ char *headphones_name;
+ char *headsetmic_name;
+ char *headphonemic_name;
+ char *internalspk_name;
+ char *internalmic_name;
+#endif /* HAVE_ALSA */
+
+ GvcMixerControlState state;
+};
+
+enum {
+ STATE_CHANGED,
+ STREAM_ADDED,
+ STREAM_REMOVED,
+ STREAM_CHANGED,
+ CARD_ADDED,
+ CARD_REMOVED,
+ DEFAULT_SINK_CHANGED,
+ DEFAULT_SOURCE_CHANGED,
+ ACTIVE_OUTPUT_UPDATE,
+ ACTIVE_INPUT_UPDATE,
+ OUTPUT_ADDED,
+ INPUT_ADDED,
+ OUTPUT_REMOVED,
+ INPUT_REMOVED,
+ AUDIO_DEVICE_SELECTION_NEEDED,
+ LAST_SIGNAL
+};
+
+static guint signals [LAST_SIGNAL] = { 0, };
+
+static void gvc_mixer_control_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerControl, gvc_mixer_control, G_TYPE_OBJECT)
+
+pa_context *
+gvc_mixer_control_get_pa_context (GvcMixerControl *control)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+ return control->priv->pa_context;
+}
+
+/**
+ * gvc_mixer_control_get_event_sink_input:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_event_sink_input (GvcMixerControl *control)
+{
+ GvcMixerStream *stream;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ stream = g_hash_table_lookup (control->priv->all_streams,
+ GUINT_TO_POINTER (control->priv->event_sink_input_id));
+
+ return stream;
+}
+
+static void
+gvc_mixer_control_stream_restore_cb (pa_context *c,
+ GvcMixerStream *new_stream,
+ const pa_ext_stream_restore_info *info,
+ GvcMixerControl *control)
+{
+ pa_operation *o;
+ pa_ext_stream_restore_info new_info;
+
+ if (new_stream == NULL)
+ return;
+
+ new_info.name = info->name;
+ new_info.channel_map = info->channel_map;
+ new_info.volume = info->volume;
+ new_info.mute = info->mute;
+
+ new_info.device = gvc_mixer_stream_get_name (new_stream);
+
+ o = pa_ext_stream_restore_write (control->priv->pa_context,
+ PA_UPDATE_REPLACE,
+ &new_info, 1,
+ TRUE, NULL, NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_ext_stream_restore_write() failed: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ return;
+ }
+
+ g_debug ("Changed default device for %s to %s", info->name, new_info.device);
+
+ pa_operation_unref (o);
+}
+
+static void
+gvc_mixer_control_stream_restore_sink_cb (pa_context *c,
+ const pa_ext_stream_restore_info *info,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = (GvcMixerControl *) userdata;
+ if (eol || info == NULL || !g_str_has_prefix(info->name, "sink-input-by"))
+ return;
+ gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_sink_stream, info, control);
+}
+
+static void
+gvc_mixer_control_stream_restore_source_cb (pa_context *c,
+ const pa_ext_stream_restore_info *info,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = (GvcMixerControl *) userdata;
+ if (eol || info == NULL || !g_str_has_prefix(info->name, "source-output-by"))
+ return;
+ gvc_mixer_control_stream_restore_cb (c, control->priv->new_default_source_stream, info, control);
+}
+
+/**
+ * gvc_mixer_control_lookup_device_from_stream:
+ * @control:
+ * @stream:
+ *
+ * Returns: (transfer none): a #GvcUIDevice or %NULL
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ GList *devices, *d;
+ gboolean is_network_stream;
+ const GList *ports;
+ GvcMixerUIDevice *ret;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+
+ if (GVC_IS_MIXER_SOURCE (stream))
+ devices = g_hash_table_get_values (control->priv->ui_inputs);
+ else
+ devices = g_hash_table_get_values (control->priv->ui_outputs);
+
+ ret = NULL;
+ ports = gvc_mixer_stream_get_ports (stream);
+ is_network_stream = (ports == NULL);
+
+ for (d = devices; d != NULL; d = d->next) {
+ GvcMixerUIDevice *device = d->data;
+ guint stream_id = G_MAXUINT;
+
+ g_object_get (G_OBJECT (device),
+ "stream-id", &stream_id,
+ NULL);
+
+ if (is_network_stream &&
+ stream_id == gvc_mixer_stream_get_id (stream)) {
+ g_debug ("lookup device from stream - %s - it is a network_stream ",
+ gvc_mixer_ui_device_get_description (device));
+ ret = device;
+ break;
+ } else if (!is_network_stream) {
+ const GvcMixerStreamPort *port;
+ port = gvc_mixer_stream_get_port (stream);
+
+ if (stream_id == gvc_mixer_stream_get_id (stream) &&
+ g_strcmp0 (gvc_mixer_ui_device_get_port (device),
+ port->port) == 0) {
+ g_debug ("lookup-device-from-stream found device: device description '%s', device port = '%s', device stream id %i AND stream port = '%s' stream id '%u' and stream description '%s'",
+ gvc_mixer_ui_device_get_description (device),
+ gvc_mixer_ui_device_get_port (device),
+ stream_id,
+ port->port,
+ gvc_mixer_stream_get_id (stream),
+ gvc_mixer_stream_get_description (stream));
+ ret = device;
+ break;
+ }
+ }
+ }
+
+ g_debug ("gvc_mixer_control_lookup_device_from_stream - Could not find a device for stream '%s'",gvc_mixer_stream_get_description (stream));
+
+ g_list_free (devices);
+
+ return ret;
+}
+
+gboolean
+gvc_mixer_control_set_default_sink (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ pa_operation *o;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_debug ("about to set default sink on server");
+ o = pa_context_set_default_sink (control->priv->pa_context,
+ gvc_mixer_stream_get_name (stream),
+ NULL,
+ NULL);
+ if (o == NULL) {
+ g_warning ("pa_context_set_default_sink() failed: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ return FALSE;
+ }
+
+ pa_operation_unref (o);
+
+ control->priv->new_default_sink_stream = stream;
+ g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_sink_stream);
+
+ o = pa_ext_stream_restore_read (control->priv->pa_context,
+ gvc_mixer_control_stream_restore_sink_cb,
+ control);
+
+ if (o == NULL) {
+ g_warning ("pa_ext_stream_restore_read() failed: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ return FALSE;
+ }
+
+ pa_operation_unref (o);
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_control_set_default_source (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ GvcMixerUIDevice* input;
+ pa_operation *o;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ o = pa_context_set_default_source (control->priv->pa_context,
+ gvc_mixer_stream_get_name (stream),
+ NULL,
+ NULL);
+ if (o == NULL) {
+ g_warning ("pa_context_set_default_source() failed");
+ return FALSE;
+ }
+
+ pa_operation_unref (o);
+
+ control->priv->new_default_source_stream = stream;
+ g_object_add_weak_pointer (G_OBJECT (stream), (gpointer *) &control->priv->new_default_source_stream);
+
+ o = pa_ext_stream_restore_read (control->priv->pa_context,
+ gvc_mixer_control_stream_restore_source_cb,
+ control);
+
+ if (o == NULL) {
+ g_warning ("pa_ext_stream_restore_read() failed: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ return FALSE;
+ }
+
+ pa_operation_unref (o);
+
+ /* source change successful, update the UI. */
+ input = gvc_mixer_control_lookup_device_from_stream (control, stream);
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_INPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (input));
+
+ return TRUE;
+}
+
+/**
+ * gvc_mixer_control_get_default_sink:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_default_sink (GvcMixerControl *control)
+{
+ GvcMixerStream *stream;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ if (control->priv->default_sink_is_set) {
+ stream = g_hash_table_lookup (control->priv->all_streams,
+ GUINT_TO_POINTER (control->priv->default_sink_id));
+ } else {
+ stream = NULL;
+ }
+
+ return stream;
+}
+
+/**
+ * gvc_mixer_control_get_default_source:
+ * @control:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_default_source (GvcMixerControl *control)
+{
+ GvcMixerStream *stream;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ if (control->priv->default_source_is_set) {
+ stream = g_hash_table_lookup (control->priv->all_streams,
+ GUINT_TO_POINTER (control->priv->default_source_id));
+ } else {
+ stream = NULL;
+ }
+
+ return stream;
+}
+
+static gpointer
+gvc_mixer_control_lookup_id (GHashTable *hash_table,
+ guint id)
+{
+ return g_hash_table_lookup (hash_table,
+ GUINT_TO_POINTER (id));
+}
+
+/**
+ * gvc_mixer_control_lookup_stream_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_lookup_stream_id (GvcMixerControl *control,
+ guint id)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ return gvc_mixer_control_lookup_id (control->priv->all_streams, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_card_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerCard *
+gvc_mixer_control_lookup_card_id (GvcMixerControl *control,
+ guint id)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ return gvc_mixer_control_lookup_id (control->priv->cards, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_output_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_output_id (GvcMixerControl *control,
+ guint id)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ return gvc_mixer_control_lookup_id (control->priv->ui_outputs, id);
+}
+
+/**
+ * gvc_mixer_control_lookup_input_id:
+ * @control:
+ * @id:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerUIDevice *
+gvc_mixer_control_lookup_input_id (GvcMixerControl *control,
+ guint id)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ return gvc_mixer_control_lookup_id (control->priv->ui_inputs, id);
+}
+
+/**
+ * gvc_mixer_control_get_stream_from_device:
+ * @control:
+ * @device:
+ *
+ * Returns: (transfer none):
+ */
+GvcMixerStream *
+gvc_mixer_control_get_stream_from_device (GvcMixerControl *control,
+ GvcMixerUIDevice *device)
+{
+ gint stream_id;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ stream_id = gvc_mixer_ui_device_get_stream_id (device);
+
+ if (stream_id == GVC_MIXER_UI_DEVICE_INVALID) {
+ g_debug ("gvc_mixer_control_get_stream_from_device - device has a null stream");
+ return NULL;
+ }
+ return gvc_mixer_control_lookup_stream_id (control, stream_id);
+}
+
+/**
+ * gvc_mixer_control_change_profile_on_selected_device:
+ * @control:
+ * @device:
+ * @profile: (allow-none): Can be %NULL if any profile present on this port is okay
+ *
+ * Returns: This method will attempt to swap the profile on the card of
+ * the device with given profile name. If successfull it will set the
+ * preferred profile on that device so as we know the next time the user
+ * moves to that device it should have this profile active.
+ */
+gboolean
+gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control,
+ GvcMixerUIDevice *device,
+ const gchar *profile)
+{
+ const gchar *best_profile;
+ GvcMixerCardProfile *current_profile;
+ GvcMixerCard *card;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+ g_object_get (G_OBJECT (device), "card", &card, NULL);
+ current_profile = gvc_mixer_card_get_profile (card);
+
+ if (current_profile)
+ best_profile = gvc_mixer_ui_device_get_best_profile (device, profile, current_profile->profile);
+ else
+ best_profile = profile;
+
+ g_assert (best_profile);
+
+ g_debug ("Selected '%s', moving to profile '%s' on card '%s' on stream id %i",
+ profile ? profile : "(any)", best_profile,
+ gvc_mixer_card_get_name (card),
+ gvc_mixer_ui_device_get_stream_id (device));
+
+ g_debug ("default sink name = %s and default sink id %u",
+ control->priv->default_sink_name,
+ control->priv->default_sink_id);
+
+ control->priv->profile_swapping_device_id = gvc_mixer_ui_device_get_id (device);
+
+ if (gvc_mixer_card_change_profile (card, best_profile)) {
+ gvc_mixer_ui_device_set_user_preferred_profile (device, best_profile);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gvc_mixer_control_change_output:
+ * @control:
+ * @output:
+ * This method is called from the UI when the user selects a previously unselected device.
+ * - Firstly it queries the stream from the device.
+ * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
+ * In the scenario of a NULL stream on the device
+ * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
+ * - It then caches this device in control->priv->cached_desired_output_id so that when the update_sink triggered
+ * from when we attempt to change profile we will know exactly what device to highlight on that stream.
+ * - It attempts to swap the profile on the card from that device and returns.
+ * - Next, it handles network or bluetooth streams that only require their stream to be made the default.
+ * - Next it deals with port changes so if the stream's active port is not the same as the port on the device
+ * it will attempt to change the port on that stream to be same as the device. If this fails it will return.
+ * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active output device.
+ */
+void
+gvc_mixer_control_change_output (GvcMixerControl *control,
+ GvcMixerUIDevice* output)
+{
+ GvcMixerStream *stream;
+ GvcMixerStream *default_stream;
+ const GvcMixerStreamPort *active_port;
+ const gchar *output_port;
+
+ g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (output));
+
+ g_debug ("control change output");
+
+ stream = gvc_mixer_control_get_stream_from_device (control, output);
+ if (stream == NULL) {
+ gvc_mixer_control_change_profile_on_selected_device (control,
+ output, NULL);
+ return;
+ }
+
+ /* Handle a network sink as a portless or cardless device */
+ if (!gvc_mixer_ui_device_has_ports (output)) {
+ g_debug ("Did we try to move to a software/bluetooth sink ?");
+ if (gvc_mixer_control_set_default_sink (control, stream)) {
+ /* sink change was successful, update the UI.*/
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_OUTPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (output));
+ }
+ else {
+ g_warning ("Failed to set default sink with stream from output %s",
+ gvc_mixer_ui_device_get_description (output));
+ }
+ return;
+ }
+
+ active_port = gvc_mixer_stream_get_port (stream);
+ output_port = gvc_mixer_ui_device_get_port (output);
+ /* First ensure the correct port is active on the sink */
+ if (g_strcmp0 (active_port->port, output_port) != 0) {
+ g_debug ("Port change, switch to = %s", output_port);
+ if (gvc_mixer_stream_change_port (stream, output_port) == FALSE) {
+ g_warning ("Could not change port !");
+ return;
+ }
+ }
+
+ default_stream = gvc_mixer_control_get_default_sink (control);
+
+ /* Finally if we are not on the correct stream, swap over. */
+ if (stream != default_stream) {
+ GvcMixerUIDevice* device;
+
+ g_debug ("Attempting to swap over to stream %s ",
+ gvc_mixer_stream_get_description (stream));
+ if (gvc_mixer_control_set_default_sink (control, stream)) {
+ device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_OUTPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ } else {
+ /* If the move failed for some reason reset the UI. */
+ device = gvc_mixer_control_lookup_device_from_stream (control, default_stream);
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_OUTPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ }
+ }
+}
+
+
+/**
+ * gvc_mixer_control_change_input:
+ * @control:
+ * @input:
+ * This method is called from the UI when the user selects a previously unselected device.
+ * - Firstly it queries the stream from the device.
+ * - It assumes that if the stream is null that it cannot be a bluetooth or network stream (they never show unless they have valid sinks and sources)
+ * In the scenario of a NULL stream on the device
+ * - It fetches the device's preferred profile or if NUll the profile with the highest priority on that device.
+ * - It then caches this device in control->priv->cached_desired_input_id so that when the update_source triggered
+ * from when we attempt to change profile we will know exactly what device to highlight on that stream.
+ * - It attempts to swap the profile on the card from that device and returns.
+ * - Next, it handles network or bluetooth streams that only require their stream to be made the default.
+ * - Next it deals with port changes so if the stream's active port is not the same as the port on the device
+ * it will attempt to change the port on that stream to be same as the device. If this fails it will return.
+ * - Finally it will set this new stream to be the default stream and emit a signal for the UI confirming the active input device.
+ */
+void
+gvc_mixer_control_change_input (GvcMixerControl *control,
+ GvcMixerUIDevice* input)
+{
+ GvcMixerStream *stream;
+ GvcMixerStream *default_stream;
+ const GvcMixerStreamPort *active_port;
+ const gchar *input_port;
+
+ g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (input));
+
+ stream = gvc_mixer_control_get_stream_from_device (control, input);
+ if (stream == NULL) {
+ gvc_mixer_control_change_profile_on_selected_device (control,
+ input, NULL);
+ return;
+ }
+
+ /* Handle a network sink as a portless/cardless device */
+ if (!gvc_mixer_ui_device_has_ports (input)) {
+ g_debug ("Did we try to move to a software/bluetooth source ?");
+ if (! gvc_mixer_control_set_default_source (control, stream)) {
+ g_warning ("Failed to set default source with stream from input %s",
+ gvc_mixer_ui_device_get_description (input));
+ }
+ return;
+ }
+
+ active_port = gvc_mixer_stream_get_port (stream);
+ input_port = gvc_mixer_ui_device_get_port (input);
+ /* First ensure the correct port is active on the sink */
+ if (g_strcmp0 (active_port->port, input_port) != 0) {
+ g_debug ("Port change, switch to = %s", input_port);
+ if (gvc_mixer_stream_change_port (stream, input_port) == FALSE) {
+ g_warning ("Could not change port!");
+ return;
+ }
+ }
+
+ default_stream = gvc_mixer_control_get_default_source (control);
+
+ /* Finally if we are not on the correct stream, swap over. */
+ if (stream != default_stream) {
+ g_debug ("change-input - attempting to swap over to stream %s",
+ gvc_mixer_stream_get_description (stream));
+ gvc_mixer_control_set_default_source (control, stream);
+ }
+}
+
+
+static void
+listify_hash_values_hfunc (gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ GSList **list = user_data;
+
+ *list = g_slist_prepend (*list, value);
+}
+
+static int
+gvc_name_collate (const char *namea,
+ const char *nameb)
+{
+ if (nameb == NULL && namea == NULL)
+ return 0;
+ if (nameb == NULL)
+ return 1;
+ if (namea == NULL)
+ return -1;
+
+ return g_utf8_collate (namea, nameb);
+}
+
+static int
+gvc_card_collate (GvcMixerCard *a,
+ GvcMixerCard *b)
+{
+ const char *namea;
+ const char *nameb;
+
+ g_return_val_if_fail (a == NULL || GVC_IS_MIXER_CARD (a), 0);
+ g_return_val_if_fail (b == NULL || GVC_IS_MIXER_CARD (b), 0);
+
+ namea = gvc_mixer_card_get_name (a);
+ nameb = gvc_mixer_card_get_name (b);
+
+ return gvc_name_collate (namea, nameb);
+}
+
+/**
+ * gvc_mixer_control_get_cards:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerCard):
+ */
+GSList *
+gvc_mixer_control_get_cards (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->cards,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_card_collate);
+}
+
+static int
+gvc_stream_collate (GvcMixerStream *a,
+ GvcMixerStream *b)
+{
+ const char *namea;
+ const char *nameb;
+
+ g_return_val_if_fail (a == NULL || GVC_IS_MIXER_STREAM (a), 0);
+ g_return_val_if_fail (b == NULL || GVC_IS_MIXER_STREAM (b), 0);
+
+ namea = gvc_mixer_stream_get_name (a);
+ nameb = gvc_mixer_stream_get_name (b);
+
+ return gvc_name_collate (namea, nameb);
+}
+
+/**
+ * gvc_mixer_control_get_streams:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerStream):
+ */
+GSList *
+gvc_mixer_control_get_streams (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->all_streams,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sinks:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSink):
+ */
+GSList *
+gvc_mixer_control_get_sinks (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->sinks,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sources:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSource):
+ */
+GSList *
+gvc_mixer_control_get_sources (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->sources,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_sink_inputs:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSinkInput):
+ */
+GSList *
+gvc_mixer_control_get_sink_inputs (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->sink_inputs,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+/**
+ * gvc_mixer_control_get_source_outputs:
+ * @control:
+ *
+ * Returns: (transfer container) (element-type Gvc.MixerSourceOutput):
+ */
+GSList *
+gvc_mixer_control_get_source_outputs (GvcMixerControl *control)
+{
+ GSList *retval;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), NULL);
+
+ retval = NULL;
+ g_hash_table_foreach (control->priv->source_outputs,
+ listify_hash_values_hfunc,
+ &retval);
+ return g_slist_sort (retval, (GCompareFunc) gvc_stream_collate);
+}
+
+static void
+dec_outstanding (GvcMixerControl *control)
+{
+ if (control->priv->n_outstanding <= 0) {
+ return;
+ }
+
+ if (--control->priv->n_outstanding <= 0) {
+ control->priv->state = GVC_STATE_READY;
+ g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_READY);
+ }
+}
+
+GvcMixerControlState
+gvc_mixer_control_get_state (GvcMixerControl *control)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), GVC_STATE_CLOSED);
+
+ return control->priv->state;
+}
+
+static void
+on_default_source_port_notify (GObject *object,
+ GParamSpec *pspec,
+ GvcMixerControl *control)
+{
+ char *port;
+ GvcMixerUIDevice *input;
+
+ g_object_get (object, "port", &port, NULL);
+ input = gvc_mixer_control_lookup_device_from_stream (control,
+ GVC_MIXER_STREAM (object));
+
+ g_debug ("on_default_source_port_notify - moved to port '%s' which SHOULD ?? correspond to output '%s'",
+ port,
+ gvc_mixer_ui_device_get_description (input));
+
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_INPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (input));
+
+ g_free (port);
+}
+
+
+static void
+_set_default_source (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ guint new_id;
+
+ if (stream == NULL) {
+ control->priv->default_source_id = 0;
+ control->priv->default_source_is_set = FALSE;
+ g_signal_emit (control,
+ signals[DEFAULT_SOURCE_CHANGED],
+ 0,
+ PA_INVALID_INDEX);
+ return;
+ }
+
+ new_id = gvc_mixer_stream_get_id (stream);
+
+ if (control->priv->default_source_id != new_id) {
+ GvcMixerUIDevice *input;
+ control->priv->default_source_id = new_id;
+ control->priv->default_source_is_set = TRUE;
+ g_signal_emit (control,
+ signals[DEFAULT_SOURCE_CHANGED],
+ 0,
+ new_id);
+
+ if (control->priv->default_source_is_set) {
+ g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_source (control),
+ on_default_source_port_notify,
+ control);
+ }
+
+ g_signal_connect (stream,
+ "notify::port",
+ G_CALLBACK (on_default_source_port_notify),
+ control);
+
+ input = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_INPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (input));
+ }
+}
+
+static void
+on_default_sink_port_notify (GObject *object,
+ GParamSpec *pspec,
+ GvcMixerControl *control)
+{
+ char *port;
+ GvcMixerUIDevice *output;
+
+ g_object_get (object, "port", &port, NULL);
+
+ output = gvc_mixer_control_lookup_device_from_stream (control,
+ GVC_MIXER_STREAM (object));
+ if (output != NULL) {
+ g_debug ("on_default_sink_port_notify - moved to port %s - which SHOULD correspond to output %s",
+ port,
+ gvc_mixer_ui_device_get_description (output));
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_OUTPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (output));
+ }
+ g_free (port);
+}
+
+static void
+_set_default_sink (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ guint new_id;
+
+ if (stream == NULL) {
+ /* Don't tell front-ends about an unset default
+ * sink if it's already unset */
+ if (control->priv->default_sink_is_set == FALSE)
+ return;
+ control->priv->default_sink_id = 0;
+ control->priv->default_sink_is_set = FALSE;
+ g_signal_emit (control,
+ signals[DEFAULT_SINK_CHANGED],
+ 0,
+ PA_INVALID_INDEX);
+ return;
+ }
+
+ new_id = gvc_mixer_stream_get_id (stream);
+
+ if (control->priv->default_sink_id != new_id) {
+ GvcMixerUIDevice *output;
+ if (control->priv->default_sink_is_set) {
+ g_signal_handlers_disconnect_by_func (gvc_mixer_control_get_default_sink (control),
+ on_default_sink_port_notify,
+ control);
+ }
+
+ control->priv->default_sink_id = new_id;
+
+ control->priv->default_sink_is_set = TRUE;
+ g_signal_emit (control,
+ signals[DEFAULT_SINK_CHANGED],
+ 0,
+ new_id);
+
+ g_signal_connect (stream,
+ "notify::port",
+ G_CALLBACK (on_default_sink_port_notify),
+ control);
+
+ output = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+ g_debug ("active_sink change");
+
+ g_signal_emit (G_OBJECT (control),
+ signals[ACTIVE_OUTPUT_UPDATE],
+ 0,
+ gvc_mixer_ui_device_get_id (output));
+ }
+}
+
+static gboolean
+_stream_has_name (gpointer key,
+ GvcMixerStream *stream,
+ const char *name)
+{
+ const char *t_name;
+
+ t_name = gvc_mixer_stream_get_name (stream);
+
+ if (t_name != NULL
+ && name != NULL
+ && strcmp (t_name, name) == 0) {
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static GvcMixerStream *
+find_stream_for_name (GvcMixerControl *control,
+ const char *name)
+{
+ GvcMixerStream *stream;
+
+ stream = g_hash_table_find (control->priv->all_streams,
+ (GHRFunc)_stream_has_name,
+ (char *)name);
+ return stream;
+}
+
+static void
+update_default_source_from_name (GvcMixerControl *control,
+ const char *name)
+{
+ gboolean changed = FALSE;
+
+ if ((control->priv->default_source_name == NULL
+ && name != NULL)
+ || (control->priv->default_source_name != NULL
+ && name == NULL)
+ || (name != NULL && strcmp (control->priv->default_source_name, name) != 0)) {
+ changed = TRUE;
+ }
+
+ if (changed) {
+ GvcMixerStream *stream;
+
+ g_free (control->priv->default_source_name);
+ control->priv->default_source_name = g_strdup (name);
+
+ stream = find_stream_for_name (control, name);
+ _set_default_source (control, stream);
+ }
+}
+
+static void
+update_default_sink_from_name (GvcMixerControl *control,
+ const char *name)
+{
+ gboolean changed = FALSE;
+
+ if ((control->priv->default_sink_name == NULL
+ && name != NULL)
+ || (control->priv->default_sink_name != NULL
+ && name == NULL)
+ || (name != NULL && strcmp (control->priv->default_sink_name, name) != 0)) {
+ changed = TRUE;
+ }
+
+ if (changed) {
+ GvcMixerStream *stream;
+ g_free (control->priv->default_sink_name);
+ control->priv->default_sink_name = g_strdup (name);
+
+ stream = find_stream_for_name (control, name);
+ _set_default_sink (control, stream);
+ }
+}
+
+static void
+update_server (GvcMixerControl *control,
+ const pa_server_info *info)
+{
+ if (info->default_source_name != NULL) {
+ update_default_source_from_name (control, info->default_source_name);
+ }
+ if (info->default_sink_name != NULL) {
+ g_debug ("update server");
+ update_default_sink_from_name (control, info->default_sink_name);
+ }
+}
+
+static void
+remove_stream (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ guint id;
+
+ g_object_ref (stream);
+
+ id = gvc_mixer_stream_get_id (stream);
+
+ if (id == control->priv->default_sink_id) {
+ _set_default_sink (control, NULL);
+ } else if (id == control->priv->default_source_id) {
+ _set_default_source (control, NULL);
+ }
+
+ g_hash_table_remove (control->priv->all_streams,
+ GUINT_TO_POINTER (id));
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_REMOVED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+ g_object_unref (stream);
+}
+
+static void
+add_stream (GvcMixerControl *control,
+ GvcMixerStream *stream)
+{
+ g_hash_table_insert (control->priv->all_streams,
+ GUINT_TO_POINTER (gvc_mixer_stream_get_id (stream)),
+ stream);
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_ADDED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+}
+
+/* This method will match individual stream ports against its corresponding device
+ * It does this by:
+ * - iterates through our devices and finds the one where the card-id on the device is the same as the card-id on the stream
+ * and the port-name on the device is the same as the streamport-name.
+ * This should always find a match and is used exclusively by sync_devices().
+ */
+static gboolean
+match_stream_with_devices (GvcMixerControl *control,
+ GvcMixerStreamPort *stream_port,
+ GvcMixerStream *stream)
+{
+ GList *devices, *d;
+ guint stream_card_id;
+ guint stream_id;
+ gboolean in_possession = FALSE;
+
+ stream_id = gvc_mixer_stream_get_id (stream);
+ stream_card_id = gvc_mixer_stream_get_card_index (stream);
+
+ devices = g_hash_table_get_values (GVC_IS_MIXER_SOURCE (stream) ? control->priv->ui_inputs : control->priv->ui_outputs);
+
+ for (d = devices; d != NULL; d = d->next) {
+ GvcMixerUIDevice *device;
+ gint device_stream_id;
+ gchar *device_port_name;
+ gchar *origin;
+ gchar *description;
+ GvcMixerCard *card;
+ guint card_id;
+
+ device = d->data;
+ g_object_get (G_OBJECT (device),
+ "stream-id", &device_stream_id,
+ "card", &card,
+ "origin", &origin,
+ "description", &description,
+ "port-name", &device_port_name,
+ NULL);
+
+ card_id = gvc_mixer_card_get_index (card);
+
+ g_debug ("Attempt to match_stream update_with_existing_outputs - Try description : '%s', origin : '%s', device port name : '%s', card : %p, AGAINST stream port: '%s', sink card id %i",
+ description,
+ origin,
+ device_port_name,
+ card,
+ stream_port->port,
+ stream_card_id);
+
+ if (stream_card_id == card_id &&
+ g_strcmp0 (device_port_name, stream_port->port) == 0) {
+ g_debug ("Match device with stream: We have a match with description: '%s', origin: '%s', cached already with device id %u, so set stream id to %i",
+ description,
+ origin,
+ gvc_mixer_ui_device_get_id (device),
+ stream_id);
+
+ g_object_set (G_OBJECT (device),
+ "stream-id", (gint)stream_id,
+ NULL);
+ in_possession = TRUE;
+ }
+
+ g_free (device_port_name);
+ g_free (origin);
+ g_free (description);
+
+ if (in_possession == TRUE)
+ break;
+ }
+
+ g_list_free (devices);
+ return in_possession;
+}
+
+/*
+ * This method attempts to match a sink or source with its relevant UI device.
+ * GvcMixerStream can represent both a sink or source.
+ * Using static card port introspection implies that we know beforehand what
+ * outputs and inputs are available to the user.
+ * But that does not mean that all of these inputs and outputs are available to be used.
+ * For instance we might be able to see that there is a HDMI port available but if
+ * we are on the default analog stereo output profile there is no valid sink for
+ * that HDMI device. We first need to change profile and when update_sink() is called
+ * only then can we match the new hdmi sink with its corresponding device.
+ *
+ * Firstly it checks to see if the incoming stream has no ports.
+ * - If a stream has no ports but has a valid card ID (bluetooth), it will attempt
+ * to match the device with the stream using the card id.
+ * - If a stream has no ports and no valid card id, it goes ahead and makes a new
+ * device (software/network devices are only detectable at the sink/source level)
+ * If the stream has ports it will match each port against the stream using match_stream_with_devices().
+ *
+ * This method should always find a match.
+ */
+static void
+sync_devices (GvcMixerControl *control,
+ GvcMixerStream* stream)
+{
+ /* Go through ports to see what outputs can be created. */
+ const GList *stream_ports;
+ const GList *n = NULL;
+ gboolean is_output = !GVC_IS_MIXER_SOURCE (stream);
+ gint stream_port_count = 0;
+
+ stream_ports = gvc_mixer_stream_get_ports (stream);
+
+ if (stream_ports == NULL) {
+ GvcMixerUIDevice *device;
+ /* Bluetooth, no ports but a valid card */
+ if (gvc_mixer_stream_get_card_index (stream) != PA_INVALID_INDEX) {
+ GList *devices, *d;
+ gboolean in_possession = FALSE;
+
+ devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
+
+ for (d = devices; d != NULL; d = d->next) {
+ GvcMixerCard *card;
+ guint card_id;
+
+ device = d->data;
+
+ g_object_get (G_OBJECT (device),
+ "card", &card,
+ NULL);
+ card_id = gvc_mixer_card_get_index (card);
+ g_debug ("sync devices, device description - '%s', device card id - %i, stream description - %s, stream card id - %i",
+ gvc_mixer_ui_device_get_description (device),
+ card_id,
+ gvc_mixer_stream_get_description (stream),
+ gvc_mixer_stream_get_card_index (stream));
+ if (card_id == gvc_mixer_stream_get_card_index (stream)) {
+ in_possession = TRUE;
+ break;
+ }
+ }
+ g_list_free (devices);
+
+ if (!in_possession) {
+ g_warning ("Couldn't match the portless stream (with card) - '%s' is it an input ? -> %i, streams card id -> %i",
+ gvc_mixer_stream_get_description (stream),
+ GVC_IS_MIXER_SOURCE (stream),
+ gvc_mixer_stream_get_card_index (stream));
+ return;
+ }
+
+ g_object_set (G_OBJECT (device),
+ "stream-id", (gint)gvc_mixer_stream_get_id (stream),
+ "description", gvc_mixer_stream_get_description (stream),
+ "origin", "", /*Leave it empty for these special cases*/
+ "port-name", NULL,
+ "port-available", TRUE,
+ NULL);
+ } else { /* Network sink/source has no ports and no card. */
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+ "stream-id", (gint)gvc_mixer_stream_get_id (stream),
+ "description", gvc_mixer_stream_get_description (stream),
+ "origin", "", /* Leave it empty for these special cases */
+ "port-name", NULL,
+ "port-available", TRUE,
+ NULL);
+ device = GVC_MIXER_UI_DEVICE (object);
+
+ g_hash_table_insert (is_output ? control->priv->ui_outputs : control->priv->ui_inputs,
+ GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)),
+ g_object_ref (device));
+
+ }
+ g_signal_emit (G_OBJECT (control),
+ signals[is_output ? OUTPUT_ADDED : INPUT_ADDED],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+
+ return;
+ }
+
+ /* Go ahead and make sure to match each port against a previously created device */
+ for (n = stream_ports; n != NULL; n = n->next) {
+
+ GvcMixerStreamPort *stream_port;
+ stream_port = n->data;
+ stream_port_count ++;
+
+ if (match_stream_with_devices (control, stream_port, stream))
+ continue;
+
+ g_warning ("Sync_devices: Failed to match stream id: %u, description: '%s', origin: '%s'",
+ gvc_mixer_stream_get_id (stream),
+ stream_port->human_port,
+ gvc_mixer_stream_get_description (stream));
+ }
+}
+
+static void
+set_icon_name_from_proplist (GvcMixerStream *stream,
+ pa_proplist *l,
+ const char *default_icon_name)
+{
+ const char *t;
+
+ if ((t = pa_proplist_gets (l, PA_PROP_DEVICE_ICON_NAME))) {
+ goto finish;
+ }
+
+ if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ICON_NAME))) {
+ goto finish;
+ }
+
+ if ((t = pa_proplist_gets (l, PA_PROP_WINDOW_ICON_NAME))) {
+ goto finish;
+ }
+
+ if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ICON_NAME))) {
+ goto finish;
+ }
+
+ if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
+
+ if (strcmp (t, "video") == 0 ||
+ strcmp (t, "phone") == 0) {
+ goto finish;
+ }
+
+ if (strcmp (t, "music") == 0) {
+ t = "audio";
+ goto finish;
+ }
+
+ if (strcmp (t, "game") == 0) {
+ t = "applications-games";
+ goto finish;
+ }
+
+ if (strcmp (t, "event") == 0) {
+ t = "dialog-information";
+ goto finish;
+ }
+ }
+
+ t = default_icon_name;
+
+ finish:
+ gvc_mixer_stream_set_icon_name (stream, t);
+}
+
+static GvcMixerStreamState
+translate_pa_state (pa_sink_state_t state) {
+ switch (state) {
+ case PA_SINK_RUNNING:
+ return GVC_STREAM_STATE_RUNNING;
+ case PA_SINK_IDLE:
+ return GVC_STREAM_STATE_IDLE;
+ case PA_SINK_SUSPENDED:
+ return GVC_STREAM_STATE_SUSPENDED;
+ case PA_SINK_INIT:
+ case PA_SINK_INVALID_STATE:
+ case PA_SINK_UNLINKED:
+ default:
+ return GVC_STREAM_STATE_INVALID;
+ }
+}
+
+/*
+ * Called when anything changes with a sink.
+ */
+static void
+update_sink (GvcMixerControl *control,
+ const pa_sink_info *info)
+{
+ GvcMixerStream *stream;
+ gboolean is_new;
+ pa_volume_t max_volume;
+ GvcChannelMap *map;
+ char map_buff[PA_CHANNEL_MAP_SNPRINT_MAX];
+
+ pa_channel_map_snprint (map_buff, PA_CHANNEL_MAP_SNPRINT_MAX, &info->channel_map);
+#if 1
+ g_debug ("Updating sink: index=%u name='%s' description='%s' map='%s'",
+ info->index,
+ info->name,
+ info->description,
+ map_buff);
+#endif
+
+ map = NULL;
+ is_new = FALSE;
+ stream = g_hash_table_lookup (control->priv->sinks,
+ GUINT_TO_POINTER (info->index));
+
+ if (stream == NULL) {
+ GList *list = NULL;
+ guint i;
+
+ map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+ stream = gvc_mixer_sink_new (control->priv->pa_context,
+ info->index,
+ map);
+
+ for (i = 0; i < info->n_ports; i++) {
+ GvcMixerStreamPort *port;
+
+ port = g_slice_new0 (GvcMixerStreamPort);
+ port->port = g_strdup (info->ports[i]->name);
+ port->human_port = g_strdup (info->ports[i]->description);
+ port->priority = info->ports[i]->priority;
+ port->available = info->ports[i]->available != PA_PORT_AVAILABLE_NO;
+
+ list = g_list_prepend (list, port);
+ }
+ gvc_mixer_stream_set_ports (stream, list);
+
+ g_object_unref (map);
+ is_new = TRUE;
+
+ } else if (gvc_mixer_stream_is_running (stream)) {
+ /* Ignore events if volume changes are outstanding */
+ g_debug ("Ignoring event, volume changes are outstanding");
+ return;
+ }
+
+ max_volume = pa_cvolume_max (&info->volume);
+ gvc_mixer_stream_set_name (stream, info->name);
+ gvc_mixer_stream_set_card_index (stream, info->card);
+ gvc_mixer_stream_set_description (stream, info->description);
+ set_icon_name_from_proplist (stream, info->proplist, "audio-card");
+ gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
+ gvc_mixer_stream_set_sysfs_path (stream, pa_proplist_gets (info->proplist, "sysfs.path"));
+ gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+ gvc_mixer_stream_set_is_muted (stream, info->mute);
+ gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SINK_DECIBEL_VOLUME));
+ gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
+ gvc_mixer_stream_set_state (stream, translate_pa_state (info->state));
+
+ /* Messy I know but to set the port everytime regardless of whether it has changed will cost us a
+ * port change notify signal which causes the frontend to resync.
+ * Only update the UI when something has changed. */
+ if (info->active_port != NULL) {
+ if (is_new)
+ gvc_mixer_stream_set_port (stream, info->active_port->name);
+ else {
+ const GvcMixerStreamPort *active_port;
+ active_port = gvc_mixer_stream_get_port (stream);
+ if (active_port == NULL ||
+ g_strcmp0 (active_port->port, info->active_port->name) != 0) {
+ g_debug ("update sink - apparently a port update");
+ gvc_mixer_stream_set_port (stream, info->active_port->name);
+ }
+ }
+ }
+
+ if (is_new) {
+ g_debug ("update sink - is new");
+
+ g_hash_table_insert (control->priv->sinks,
+ GUINT_TO_POINTER (info->index),
+ g_object_ref (stream));
+ add_stream (control, stream);
+ /* Always sink on a new stream to able to assign the right stream id
+ * to the appropriate outputs (multiple potential outputs per stream). */
+ sync_devices (control, stream);
+ } else {
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_CHANGED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+ }
+
+ /*
+ * When we change profile on a device that is not the server default sink,
+ * it will jump back to the default sink set by the server to prevent the audio setup from being 'outputless'.
+ * All well and good but then when we get the new stream created for the new profile how do we know
+ * that this is the intended default or selected device the user wishes to use.
+ * This is messy but it's the only reliable way that it can be done without ripping the whole thing apart.
+ */
+ if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
+ GvcMixerUIDevice *dev = NULL;
+ dev = gvc_mixer_control_lookup_output_id (control, control->priv->profile_swapping_device_id);
+ if (dev != NULL) {
+ /* now check to make sure this new stream is the same stream just matched and set on the device object */
+ if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
+ g_debug ("Looks like we profile swapped on a non server default sink");
+ gvc_mixer_control_set_default_sink (control, stream);
+ control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+ }
+ }
+ }
+
+ if (control->priv->default_sink_name != NULL
+ && info->name != NULL
+ && strcmp (control->priv->default_sink_name, info->name) == 0) {
+ _set_default_sink (control, stream);
+ }
+
+ if (map == NULL)
+ map = (GvcChannelMap *) gvc_mixer_stream_get_channel_map (stream);
+
+ gvc_channel_map_volume_changed (map, &info->volume, FALSE);
+}
+
+static void
+update_source (GvcMixerControl *control,
+ const pa_source_info *info)
+{
+ GvcMixerStream *stream;
+ gboolean is_new;
+ pa_volume_t max_volume;
+
+#if 1
+ g_debug ("Updating source: index=%u name='%s' description='%s'",
+ info->index,
+ info->name,
+ info->description);
+#endif
+
+ /* completely ignore monitors, they're not real sources */
+ if (info->monitor_of_sink != PA_INVALID_INDEX) {
+ return;
+ }
+
+ is_new = FALSE;
+
+ stream = g_hash_table_lookup (control->priv->sources,
+ GUINT_TO_POINTER (info->index));
+ if (stream == NULL) {
+ GList *list = NULL;
+ guint i;
+ GvcChannelMap *map;
+
+ map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+ stream = gvc_mixer_source_new (control->priv->pa_context,
+ info->index,
+ map);
+
+ for (i = 0; i < info->n_ports; i++) {
+ GvcMixerStreamPort *port;
+
+ port = g_slice_new0 (GvcMixerStreamPort);
+ port->port = g_strdup (info->ports[i]->name);
+ port->human_port = g_strdup (info->ports[i]->description);
+ port->priority = info->ports[i]->priority;
+ list = g_list_prepend (list, port);
+ }
+ gvc_mixer_stream_set_ports (stream, list);
+
+ g_object_unref (map);
+ is_new = TRUE;
+ } else if (gvc_mixer_stream_is_running (stream)) {
+ /* Ignore events if volume changes are outstanding */
+ g_debug ("Ignoring event, volume changes are outstanding");
+ return;
+ }
+
+ max_volume = pa_cvolume_max (&info->volume);
+
+ gvc_mixer_stream_set_name (stream, info->name);
+ gvc_mixer_stream_set_card_index (stream, info->card);
+ gvc_mixer_stream_set_description (stream, info->description);
+ set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
+ gvc_mixer_stream_set_form_factor (stream, pa_proplist_gets (info->proplist, PA_PROP_DEVICE_FORM_FACTOR));
+ gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+ gvc_mixer_stream_set_is_muted (stream, info->mute);
+ gvc_mixer_stream_set_can_decibel (stream, !!(info->flags & PA_SOURCE_DECIBEL_VOLUME));
+ gvc_mixer_stream_set_base_volume (stream, (guint32) info->base_volume);
+ g_debug ("update source");
+
+ if (info->active_port != NULL) {
+ if (is_new)
+ gvc_mixer_stream_set_port (stream, info->active_port->name);
+ else {
+ const GvcMixerStreamPort *active_port;
+ active_port = gvc_mixer_stream_get_port (stream);
+ if (active_port == NULL ||
+ g_strcmp0 (active_port->port, info->active_port->name) != 0) {
+ g_debug ("update source - apparently a port update");
+ gvc_mixer_stream_set_port (stream, info->active_port->name);
+ }
+ }
+ }
+
+ if (is_new) {
+ g_hash_table_insert (control->priv->sources,
+ GUINT_TO_POINTER (info->index),
+ g_object_ref (stream));
+ add_stream (control, stream);
+ sync_devices (control, stream);
+ } else {
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_CHANGED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+ }
+
+ if (control->priv->profile_swapping_device_id != GVC_MIXER_UI_DEVICE_INVALID) {
+ GvcMixerUIDevice *dev = NULL;
+
+ dev = gvc_mixer_control_lookup_input_id (control, control->priv->profile_swapping_device_id);
+
+ if (dev != NULL) {
+ /* now check to make sure this new stream is the same stream just matched and set on the device object */
+ if (gvc_mixer_ui_device_get_stream_id (dev) == gvc_mixer_stream_get_id (stream)) {
+ g_debug ("Looks like we profile swapped on a non server default source");
+ gvc_mixer_control_set_default_source (control, stream);
+ control->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+ }
+ }
+ }
+ if (control->priv->default_source_name != NULL
+ && info->name != NULL
+ && strcmp (control->priv->default_source_name, info->name) == 0) {
+ _set_default_source (control, stream);
+ }
+}
+
+static void
+set_is_event_stream_from_proplist (GvcMixerStream *stream,
+ pa_proplist *l)
+{
+ const char *t;
+ gboolean is_event_stream;
+
+ is_event_stream = FALSE;
+
+ if ((t = pa_proplist_gets (l, PA_PROP_MEDIA_ROLE))) {
+ if (g_str_equal (t, "event"))
+ is_event_stream = TRUE;
+ }
+
+ gvc_mixer_stream_set_is_event_stream (stream, is_event_stream);
+}
+
+static void
+set_application_id_from_proplist (GvcMixerStream *stream,
+ pa_proplist *l)
+{
+ const char *t;
+
+ if ((t = pa_proplist_gets (l, PA_PROP_APPLICATION_ID))) {
+ gvc_mixer_stream_set_application_id (stream, t);
+ }
+}
+
+static void
+update_sink_input (GvcMixerControl *control,
+ const pa_sink_input_info *info)
+{
+ GvcMixerStream *stream;
+ gboolean is_new;
+ pa_volume_t max_volume;
+ const char *name;
+
+#if 0
+ g_debug ("Updating sink input: index=%u name='%s' client=%u sink=%u",
+ info->index,
+ info->name,
+ info->client,
+ info->sink);
+#endif
+
+ is_new = FALSE;
+
+ stream = g_hash_table_lookup (control->priv->sink_inputs,
+ GUINT_TO_POINTER (info->index));
+ if (stream == NULL) {
+ GvcChannelMap *map;
+ map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+ stream = gvc_mixer_sink_input_new (control->priv->pa_context,
+ info->index,
+ map);
+ g_object_unref (map);
+ is_new = TRUE;
+ } else if (gvc_mixer_stream_is_running (stream)) {
+ /* Ignore events if volume changes are outstanding */
+ g_debug ("Ignoring event, volume changes are outstanding");
+ return;
+ }
+
+ max_volume = pa_cvolume_max (&info->volume);
+
+ name = (const char *)g_hash_table_lookup (control->priv->clients,
+ GUINT_TO_POINTER (info->client));
+ gvc_mixer_stream_set_name (stream, name);
+ gvc_mixer_stream_set_description (stream, info->name);
+
+ set_application_id_from_proplist (stream, info->proplist);
+ set_is_event_stream_from_proplist (stream, info->proplist);
+ set_icon_name_from_proplist (stream, info->proplist, "applications-multimedia");
+ gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+ gvc_mixer_stream_set_is_muted (stream, info->mute);
+ gvc_mixer_stream_set_is_virtual (stream, info->client == PA_INVALID_INDEX);
+
+ if (is_new) {
+ g_hash_table_insert (control->priv->sink_inputs,
+ GUINT_TO_POINTER (info->index),
+ g_object_ref (stream));
+ add_stream (control, stream);
+ } else {
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_CHANGED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+ }
+}
+
+static void
+update_source_output (GvcMixerControl *control,
+ const pa_source_output_info *info)
+{
+ GvcMixerStream *stream;
+ gboolean is_new;
+ pa_volume_t max_volume;
+ const char *name;
+
+#if 1
+ g_debug ("Updating source output: index=%u name='%s' client=%u source=%u",
+ info->index,
+ info->name,
+ info->client,
+ info->source);
+#endif
+
+ is_new = FALSE;
+ stream = g_hash_table_lookup (control->priv->source_outputs,
+ GUINT_TO_POINTER (info->index));
+ if (stream == NULL) {
+ GvcChannelMap *map;
+ map = gvc_channel_map_new_from_pa_channel_map (&info->channel_map);
+ stream = gvc_mixer_source_output_new (control->priv->pa_context,
+ info->index,
+ map);
+ g_object_unref (map);
+ is_new = TRUE;
+ }
+
+ name = (const char *)g_hash_table_lookup (control->priv->clients,
+ GUINT_TO_POINTER (info->client));
+
+ max_volume = pa_cvolume_max (&info->volume);
+
+ gvc_mixer_stream_set_name (stream, name);
+ gvc_mixer_stream_set_description (stream, info->name);
+ set_application_id_from_proplist (stream, info->proplist);
+ set_is_event_stream_from_proplist (stream, info->proplist);
+ gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+ gvc_mixer_stream_set_is_muted (stream, info->mute);
+ set_icon_name_from_proplist (stream, info->proplist, "audio-input-microphone");
+
+ if (is_new) {
+ g_hash_table_insert (control->priv->source_outputs,
+ GUINT_TO_POINTER (info->index),
+ g_object_ref (stream));
+ add_stream (control, stream);
+ } else {
+ g_signal_emit (G_OBJECT (control),
+ signals[STREAM_CHANGED],
+ 0,
+ gvc_mixer_stream_get_id (stream));
+ }
+}
+
+static void
+update_client (GvcMixerControl *control,
+ const pa_client_info *info)
+{
+#if 1
+ g_debug ("Updating client: index=%u name='%s'",
+ info->index,
+ info->name);
+#endif
+ g_hash_table_insert (control->priv->clients,
+ GUINT_TO_POINTER (info->index),
+ g_strdup (info->name));
+}
+
+static char *
+card_num_streams_to_status (guint sinks,
+ guint sources)
+{
+ char *sinks_str;
+ char *sources_str;
+ char *ret;
+
+ if (sinks == 0 && sources == 0) {
+ /* translators:
+ * The device has been disabled */
+ return g_strdup (_("Disabled"));
+ }
+ if (sinks == 0) {
+ sinks_str = NULL;
+ } else {
+ /* translators:
+ * The number of sound outputs on a particular device */
+ sinks_str = g_strdup_printf (ngettext ("%u Output",
+ "%u Outputs",
+ sinks),
+ sinks);
+ }
+ if (sources == 0) {
+ sources_str = NULL;
+ } else {
+ /* translators:
+ * The number of sound inputs on a particular device */
+ sources_str = g_strdup_printf (ngettext ("%u Input",
+ "%u Inputs",
+ sources),
+ sources);
+ }
+ if (sources_str == NULL)
+ return sinks_str;
+ if (sinks_str == NULL)
+ return sources_str;
+ ret = g_strdup_printf ("%s / %s", sinks_str, sources_str);
+ g_free (sinks_str);
+ g_free (sources_str);
+ return ret;
+}
+
+/*
+ * A utility method to gather which card profiles are relevant to the port .
+ */
+static GList *
+determine_profiles_for_port (pa_card_port_info *port,
+ GList* card_profiles)
+{
+ guint i;
+ GList *supported_profiles = NULL;
+ GList *p;
+ for (i = 0; i < port->n_profiles; i++) {
+ for (p = card_profiles; p != NULL; p = p->next) {
+ GvcMixerCardProfile *prof;
+ prof = p->data;
+ if (g_strcmp0 (port->profiles[i]->name, prof->profile) == 0)
+ supported_profiles = g_list_append (supported_profiles, prof);
+ }
+ }
+ g_debug ("%i profiles supported on port %s",
+ g_list_length (supported_profiles),
+ port->description);
+ return g_list_sort (supported_profiles, (GCompareFunc) gvc_mixer_card_profile_compare);
+}
+
+static gboolean
+is_card_port_an_output (GvcMixerCardPort* port)
+{
+ return port->direction == PA_DIRECTION_OUTPUT ? TRUE : FALSE;
+}
+
+/*
+ * This method will create a ui device for the given port.
+ */
+static void
+create_ui_device_from_port (GvcMixerControl* control,
+ GvcMixerCardPort* port,
+ GvcMixerCard* card)
+{
+ GvcMixerUIDeviceDirection direction;
+ GObject *object;
+ GvcMixerUIDevice *uidevice;
+ gboolean available = port->available != PA_PORT_AVAILABLE_NO;
+
+ direction = (is_card_port_an_output (port) == TRUE) ? UIDeviceOutput : UIDeviceInput;
+
+ object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+ "type", (guint)direction,
+ "card", card,
+ "port-name", port->port,
+ "description", port->human_port,
+ "origin", gvc_mixer_card_get_name (card),
+ "port-available", available,
+ "icon-name", port->icon_name,
+ NULL);
+
+ uidevice = GVC_MIXER_UI_DEVICE (object);
+ gvc_mixer_ui_device_set_profiles (uidevice, port->profiles);
+
+ g_hash_table_insert (is_card_port_an_output (port) ? control->priv->ui_outputs : control->priv->ui_inputs,
+ GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (uidevice)),
+ uidevice);
+
+
+ if (available) {
+ g_signal_emit (G_OBJECT (control),
+ signals[is_card_port_an_output (port) ? OUTPUT_ADDED : INPUT_ADDED],
+ 0,
+ gvc_mixer_ui_device_get_id (uidevice));
+ }
+
+ g_debug ("create_ui_device_from_port, direction %u, description '%s', origin '%s', port available %i",
+ direction,
+ port->human_port,
+ gvc_mixer_card_get_name (card),
+ available);
+}
+
+/*
+ * This method will match up GvcMixerCardPorts with existing devices.
+ * A match is achieved if the device's card-id and the port's card-id are the same
+ * && the device's port-name and the card-port's port member are the same.
+ * A signal is then sent adding or removing that device from the UI depending on the availability of the port.
+ */
+static void
+match_card_port_with_existing_device (GvcMixerControl *control,
+ GvcMixerCardPort *card_port,
+ GvcMixerCard *card,
+ gboolean available)
+{
+ GList *d;
+ GList *devices;
+ GvcMixerUIDevice *device;
+ gboolean is_output = is_card_port_an_output (card_port);
+
+ devices = g_hash_table_get_values (is_output ? control->priv->ui_outputs : control->priv->ui_inputs);
+
+ for (d = devices; d != NULL; d = d->next) {
+ GvcMixerCard *device_card;
+ gchar *device_port_name;
+
+ device = d->data;
+ g_object_get (G_OBJECT (device),
+ "card", &device_card,
+ "port-name", &device_port_name,
+ NULL);
+
+ if (g_strcmp0 (card_port->port, device_port_name) == 0 &&
+ device_card == card) {
+ g_debug ("Found the relevant device %s, update its port availability flag to %i, is_output %i",
+ device_port_name,
+ available,
+ is_output);
+ g_object_set (G_OBJECT (device),
+ "port-available", available, NULL);
+ g_signal_emit (G_OBJECT (control),
+ is_output ? signals[available ? OUTPUT_ADDED : OUTPUT_REMOVED] : signals[available ? INPUT_ADDED : INPUT_REMOVED],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ }
+ g_free (device_port_name);
+ }
+
+ g_list_free (devices);
+}
+
+static void
+create_ui_device_from_card (GvcMixerControl *control,
+ GvcMixerCard *card)
+{
+ GObject *object;
+ GvcMixerUIDevice *in;
+ GvcMixerUIDevice *out;
+ const GList *profiles;
+
+ /* For now just create two devices and presume this device is multi directional
+ * Ensure to remove both on card removal (available to false by default) */
+ profiles = gvc_mixer_card_get_profiles (card);
+
+ g_debug ("Portless card just registered - %i", gvc_mixer_card_get_index (card));
+
+ object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+ "type", UIDeviceInput,
+ "description", gvc_mixer_card_get_name (card),
+ "origin", "", /* Leave it empty for these special cases */
+ "port-name", NULL,
+ "port-available", FALSE,
+ "card", card,
+ NULL);
+ in = GVC_MIXER_UI_DEVICE (object);
+ gvc_mixer_ui_device_set_profiles (in, profiles);
+
+ g_hash_table_insert (control->priv->ui_inputs,
+ GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (in)),
+ g_object_ref (in));
+ object = g_object_new (GVC_TYPE_MIXER_UI_DEVICE,
+ "type", UIDeviceOutput,
+ "description", gvc_mixer_card_get_name (card),
+ "origin", "", /* Leave it empty for these special cases */
+ "port-name", NULL,
+ "port-available", FALSE,
+ "card", card,
+ NULL);
+ out = GVC_MIXER_UI_DEVICE (object);
+ gvc_mixer_ui_device_set_profiles (out, profiles);
+
+ g_hash_table_insert (control->priv->ui_outputs,
+ GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (out)),
+ g_object_ref (out));
+}
+
+#ifdef HAVE_ALSA
+typedef struct {
+ char *port_name_to_set;
+ guint32 headset_card;
+} PortStatusData;
+
+static void
+port_status_data_free (PortStatusData *data)
+{
+ if (data == NULL)
+ return;
+ g_free (data->port_name_to_set);
+ g_free (data);
+}
+
+/*
+ We need to re-enumerate sources and sinks every time the user makes a choice,
+ because they can change due to use interaction in other software (or policy
+ changes inside PulseAudio). Enumeration means PulseAudio will do a series of
+ callbacks, one for every source/sink.
+ Set the port when we find the correct source/sink.
+ */
+
+static void
+sink_info_cb (pa_context *c,
+ const pa_sink_info *i,
+ int eol,
+ void *userdata)
+{
+ PortStatusData *data = userdata;
+ pa_operation *o;
+ guint j;
+ const char *s;
+
+ if (eol != 0) {
+ port_status_data_free (data);
+ return;
+ }
+
+ if (i->card != data->headset_card)
+ return;
+
+ s = data->port_name_to_set;
+
+ if (i->active_port &&
+ strcmp (i->active_port->name, s) == 0)
+ return;
+
+ for (j = 0; j < i->n_ports; j++)
+ if (strcmp (i->ports[j]->name, s) == 0)
+ break;
+
+ if (j >= i->n_ports)
+ return;
+
+ o = pa_context_set_sink_port_by_index (c, i->index, s, NULL, NULL);
+ g_clear_pointer (&o, pa_operation_unref);
+}
+
+static void
+source_info_cb (pa_context *c,
+ const pa_source_info *i,
+ int eol,
+ void *userdata)
+{
+ PortStatusData *data = userdata;
+ pa_operation *o;
+ guint j;
+ const char *s;
+
+ if (eol != 0) {
+ port_status_data_free (data);
+ return;
+ }
+
+ if (i->card != data->headset_card)
+ return;
+
+ s = data->port_name_to_set;
+
+ for (j = 0; j < i->n_ports; j++) {
+ if (g_str_equal (i->ports[j]->name, s)) {
+ o = pa_context_set_default_source (c,
+ i->name,
+ NULL,
+ NULL);
+ if (o == NULL) {
+ g_warning ("pa_context_set_default_source() failed");
+ return;
+ }
+ }
+ }
+
+ if (i->active_port && strcmp (i->active_port->name, s) == 0)
+ return;
+
+ for (j = 0; j < i->n_ports; j++)
+ if (strcmp (i->ports[j]->name, s) == 0)
+ break;
+
+ if (j >= i->n_ports)
+ return;
+
+ o = pa_context_set_source_port_by_index(c, i->index, s, NULL, NULL);
+ g_clear_pointer (&o, pa_operation_unref);
+}
+
+static void
+gvc_mixer_control_set_port_status_for_headset (GvcMixerControl *control,
+ guint id,
+ const char *port_name,
+ gboolean is_output)
+{
+ pa_operation *o;
+ PortStatusData *data;
+
+ if (port_name == NULL)
+ return;
+
+ data = g_new0 (PortStatusData, 1);
+ data->port_name_to_set = g_strdup (port_name);
+ data->headset_card = id;
+
+ if (is_output)
+ o = pa_context_get_sink_info_list (control->priv->pa_context, sink_info_cb, data);
+ else
+ o = pa_context_get_source_info_list (control->priv->pa_context, source_info_cb, data);
+
+ g_clear_pointer (&o, pa_operation_unref);
+}
+#endif /* HAVE_ALSA */
+
+static void
+free_priv_port_names (GvcMixerControl *control)
+{
+#ifdef HAVE_ALSA
+ g_clear_pointer (&control->priv->headphones_name, g_free);
+ g_clear_pointer (&control->priv->headsetmic_name, g_free);
+ g_clear_pointer (&control->priv->headphonemic_name, g_free);
+ g_clear_pointer (&control->priv->internalspk_name, g_free);
+ g_clear_pointer (&control->priv->internalmic_name, g_free);
+#endif
+}
+
+void
+gvc_mixer_control_set_headset_port (GvcMixerControl *control,
+ guint id,
+ GvcHeadsetPortChoice choice)
+{
+ g_return_if_fail (GVC_IS_MIXER_CONTROL (control));
+
+#ifdef HAVE_ALSA
+ switch (choice) {
+ case GVC_HEADSET_PORT_CHOICE_HEADPHONES:
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalmic_name, FALSE);
+ break;
+ case GVC_HEADSET_PORT_CHOICE_HEADSET:
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphones_name, TRUE);
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headsetmic_name, FALSE);
+ break;
+ case GVC_HEADSET_PORT_CHOICE_MIC:
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->internalspk_name, TRUE);
+ gvc_mixer_control_set_port_status_for_headset (control, id, control->priv->headphonemic_name, FALSE);
+ break;
+ case GVC_HEADSET_PORT_CHOICE_NONE:
+ default:
+ g_assert_not_reached ();
+ }
+#else
+ g_warning ("BUG: libgnome-volume-control compiled without ALSA support");
+#endif /* HAVE_ALSA */
+}
+
+#ifdef HAVE_ALSA
+typedef struct {
+ const pa_card_port_info *headphones;
+ const pa_card_port_info *headsetmic;
+ const pa_card_port_info *headphonemic;
+ const pa_card_port_info *internalmic;
+ const pa_card_port_info *internalspk;
+} headset_ports;
+
+/*
+ In PulseAudio without ucm, ports will show up with the following names:
+ Headphones - analog-output-headphones
+ Headset mic - analog-input-headset-mic (was: analog-input-microphone-headset)
+ Jack in mic-in mode - analog-input-headphone-mic (was: analog-input-microphone)
+
+ However, since regular mics also show up as analog-input-microphone,
+ we need to check for certain controls on alsa mixer level too, to know
+ if we deal with a separate mic jack, or a multi-function jack with a
+ mic-in mode (also called "headphone mic").
+ We check for the following names:
+
+ Headphone Mic Jack - indicates headphone and mic-in mode share the same jack,
+ i e, not two separate jacks. Hardware cannot distinguish between a
+ headphone and a mic.
+ Headset Mic Phantom Jack - indicates headset jack where hardware can not
+ distinguish between headphones and headsets
+ Headset Mic Jack - indicates headset jack where hardware can distinguish
+ between headphones and headsets. There is no use popping up a dialog in
+ this case, unless we already need to do this for the mic-in mode.
+
+ From the PA_PROCOTOL_VERSION=34, The device_port structure adds 2 members
+ availability_group and type, with the help of these 2 members, we could
+ consolidate the port checking and port setting for non-ucm and with-ucm
+ cases.
+*/
+
+#define HEADSET_PORT_SET(dst, src) \
+ do { \
+ if (!(dst) || (dst)->priority < (src)->priority) \
+ dst = src; \
+ } while (0)
+
+#define GET_PORT_NAME(x) (x ? g_strdup (x->name) : NULL)
+
+static headset_ports *
+get_headset_ports (GvcMixerControl *control,
+ const pa_card_info *c)
+{
+ headset_ports *h;
+ guint i;
+
+ h = g_new0 (headset_ports, 1);
+
+ for (i = 0; i < c->n_ports; i++) {
+ pa_card_port_info *p = c->ports[i];
+ if (control->priv->server_protocol_version < 34) {
+ if (g_str_equal (p->name, "analog-output-headphones"))
+ h->headphones = p;
+ else if (g_str_equal (p->name, "analog-input-headset-mic"))
+ h->headsetmic = p;
+ else if (g_str_equal (p->name, "analog-input-headphone-mic"))
+ h->headphonemic = p;
+ else if (g_str_equal (p->name, "analog-input-internal-mic"))
+ h->internalmic = p;
+ else if (g_str_equal (p->name, "analog-output-speaker"))
+ h->internalspk = p;
+ } else {
+#if (PA_PROTOCOL_VERSION >= 34)
+ /* in the first loop, set only headphones */
+ /* the microphone ports are assigned in the second loop */
+ if (p->type == PA_DEVICE_PORT_TYPE_HEADPHONES) {
+ if (p->availability_group)
+ HEADSET_PORT_SET (h->headphones, p);
+ } else if (p->type == PA_DEVICE_PORT_TYPE_SPEAKER) {
+ HEADSET_PORT_SET (h->internalspk, p);
+ } else if (p->type == PA_DEVICE_PORT_TYPE_MIC) {
+ if (!p->availability_group)
+ HEADSET_PORT_SET (h->internalmic, p);
+ }
+#else
+ g_warning_once ("libgnome-volume-control running against PulseAudio %u, "
+ "but compiled against older %d, report a bug to your distribution",
+ control->priv->server_protocol_version,
+ PA_PROTOCOL_VERSION);
+#endif
+ }
+ }
+
+#if (PA_PROTOCOL_VERSION >= 34)
+ if (h->headphones && (control->priv->server_protocol_version >= 34)) {
+ for (i = 0; i < c->n_ports; i++) {
+ pa_card_port_info *p = c->ports[i];
+ if (g_strcmp0(h->headphones->availability_group, p->availability_group))
+ continue;
+ if (p->direction != PA_DIRECTION_INPUT)
+ continue;
+ if (p->type == PA_DEVICE_PORT_TYPE_HEADSET)
+ HEADSET_PORT_SET (h->headsetmic, p);
+ else if (p->type == PA_DEVICE_PORT_TYPE_MIC)
+ HEADSET_PORT_SET (h->headphonemic, p);
+ }
+ }
+#endif
+
+ return h;
+}
+
+static gboolean
+verify_alsa_card (int cardindex,
+ gboolean *headsetmic,
+ gboolean *headphonemic)
+{
+ char *ctlstr;
+ snd_hctl_t *hctl;
+ snd_ctl_elem_id_t *id;
+ int err;
+
+ *headsetmic = FALSE;
+ *headphonemic = FALSE;
+
+ ctlstr = g_strdup_printf ("hw:%i", cardindex);
+ if ((err = snd_hctl_open (&hctl, ctlstr, 0)) < 0) {
+ g_warning ("snd_hctl_open failed: %s", snd_strerror(err));
+ g_free (ctlstr);
+ return FALSE;
+ }
+ g_free (ctlstr);
+
+ if ((err = snd_hctl_load (hctl)) < 0) {
+ g_warning ("snd_hctl_load failed: %s", snd_strerror(err));
+ snd_hctl_close (hctl);
+ return FALSE;
+ }
+
+ snd_ctl_elem_id_alloca (&id);
+
+ snd_ctl_elem_id_clear (id);
+ snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+ snd_ctl_elem_id_set_name (id, "Headphone Mic Jack");
+ if (snd_hctl_find_elem (hctl, id))
+ *headphonemic = TRUE;
+
+ snd_ctl_elem_id_clear (id);
+ snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+ snd_ctl_elem_id_set_name (id, "Headset Mic Phantom Jack");
+ if (snd_hctl_find_elem (hctl, id))
+ *headsetmic = TRUE;
+
+ if (*headphonemic) {
+ snd_ctl_elem_id_clear (id);
+ snd_ctl_elem_id_set_interface (id, SND_CTL_ELEM_IFACE_CARD);
+ snd_ctl_elem_id_set_name (id, "Headset Mic Jack");
+ if (snd_hctl_find_elem (hctl, id))
+ *headsetmic = TRUE;
+ }
+
+ snd_hctl_close (hctl);
+ return *headsetmic || *headphonemic;
+}
+
+static void
+check_audio_device_selection_needed (GvcMixerControl *control,
+ const pa_card_info *info)
+{
+ headset_ports *h;
+ gboolean start_dialog, stop_dialog;
+
+ start_dialog = FALSE;
+ stop_dialog = FALSE;
+ h = get_headset_ports (control, info);
+
+ if (!h->headphones ||
+ (!h->headsetmic && !h->headphonemic)) {
+ /* Not a headset jack */
+ goto out;
+ }
+
+ if (control->priv->headset_card != (int) info->index) {
+ int cardindex;
+ gboolean hsmic = TRUE;
+ gboolean hpmic = TRUE;
+ const char *s;
+
+ s = pa_proplist_gets (info->proplist, "alsa.card");
+ if (!s)
+ goto out;
+
+ cardindex = strtol (s, NULL, 10);
+ if (cardindex == 0 && strcmp(s, "0") != 0)
+ goto out;
+
+ if (control->priv->server_protocol_version < 34) {
+ if (!verify_alsa_card(cardindex, &hsmic, &hpmic))
+ goto out;
+ }
+
+ control->priv->headset_card = info->index;
+ control->priv->has_headsetmic = hsmic && h->headsetmic;
+ control->priv->has_headphonemic = hpmic && h->headphonemic;
+ } else {
+ start_dialog = (h->headphones->available != PA_PORT_AVAILABLE_NO) && !control->priv->headset_plugged_in;
+ stop_dialog = (h->headphones->available == PA_PORT_AVAILABLE_NO) && control->priv->headset_plugged_in;
+ }
+
+ control->priv->headset_plugged_in = h->headphones->available != PA_PORT_AVAILABLE_NO;
+ free_priv_port_names (control);
+ control->priv->headphones_name = GET_PORT_NAME(h->headphones);
+ control->priv->headsetmic_name = GET_PORT_NAME(h->headsetmic);
+ control->priv->headphonemic_name = GET_PORT_NAME(h->headphonemic);
+ control->priv->internalspk_name = GET_PORT_NAME(h->internalspk);
+ control->priv->internalmic_name = GET_PORT_NAME(h->internalmic);
+
+ if (!start_dialog &&
+ !stop_dialog)
+ goto out;
+
+ if (stop_dialog) {
+ g_signal_emit (G_OBJECT (control),
+ signals[AUDIO_DEVICE_SELECTION_NEEDED],
+ 0,
+ info->index,
+ FALSE,
+ GVC_HEADSET_PORT_CHOICE_NONE);
+ } else {
+ GvcHeadsetPortChoice choices;
+
+ choices = GVC_HEADSET_PORT_CHOICE_HEADPHONES;
+ if (control->priv->has_headsetmic)
+ choices |= GVC_HEADSET_PORT_CHOICE_HEADSET;
+ if (control->priv->has_headphonemic)
+ choices |= GVC_HEADSET_PORT_CHOICE_MIC;
+
+ g_signal_emit (G_OBJECT (control),
+ signals[AUDIO_DEVICE_SELECTION_NEEDED],
+ 0,
+ info->index,
+ TRUE,
+ choices);
+ }
+
+out:
+ g_free (h);
+}
+#endif /* HAVE_ALSA */
+
+/*
+ * At this point we can determine all devices available to us (besides network 'ports')
+ * This is done by the following:
+ *
+ * - gvc_mixer_card and gvc_mixer_card_ports are created and relevant setters are called.
+ * - First it checks to see if it's a portless card. Bluetooth devices are portless AFAIHS.
+ * If so it creates two devices, an input and an output.
+ * - If it's a 'normal' card with ports it will create a new ui-device or
+ * synchronise port availability with the existing device cached for that port on this card. */
+
+static void
+update_card (GvcMixerControl *control,
+ const pa_card_info *info)
+{
+ const GList *card_ports = NULL;
+ const GList *m = NULL;
+ GvcMixerCard *card;
+ gboolean is_new = FALSE;
+#if 1
+ guint i;
+ const char *key;
+ void *state;
+
+ g_debug ("Updating card %s (index: %u driver: %s):",
+ info->name, info->index, info->driver);
+
+ for (i = 0; i < info->n_profiles; i++) {
+ struct pa_card_profile_info pi = info->profiles[i];
+ gboolean is_default;
+
+ is_default = (g_strcmp0 (pi.name, info->active_profile->name) == 0);
+ g_debug ("\tProfile '%s': %d sources %d sinks%s",
+ pi.name, pi.n_sources, pi.n_sinks,
+ is_default ? " (Current)" : "");
+ }
+ state = NULL;
+ key = pa_proplist_iterate (info->proplist, &state);
+ while (key != NULL) {
+ g_debug ("\tProperty: '%s' = '%s'",
+ key, pa_proplist_gets (info->proplist, key));
+ key = pa_proplist_iterate (info->proplist, &state);
+ }
+#endif
+ card = g_hash_table_lookup (control->priv->cards,
+ GUINT_TO_POINTER (info->index));
+ if (card == NULL) {
+ GList *profile_list = NULL;
+ GList *port_list = NULL;
+
+ for (i = 0; i < info->n_profiles; i++) {
+ GvcMixerCardProfile *profile;
+ struct pa_card_profile_info pi = info->profiles[i];
+
+ profile = g_new0 (GvcMixerCardProfile, 1);
+ profile->profile = g_strdup (pi.name);
+ profile->human_profile = g_strdup (pi.description);
+ profile->status = card_num_streams_to_status (pi.n_sinks, pi.n_sources);
+ profile->n_sinks = pi.n_sinks;
+ profile->n_sources = pi.n_sources;
+ profile->priority = pi.priority;
+ profile_list = g_list_prepend (profile_list, profile);
+ }
+ card = gvc_mixer_card_new (control->priv->pa_context,
+ info->index);
+ gvc_mixer_card_set_profiles (card, profile_list);
+
+ for (i = 0; i < info->n_ports; i++) {
+ GvcMixerCardPort *port;
+ port = g_new0 (GvcMixerCardPort, 1);
+ port->port = g_strdup (info->ports[i]->name);
+ port->human_port = g_strdup (info->ports[i]->description);
+ port->priority = info->ports[i]->priority;
+ port->available = info->ports[i]->available;
+ port->direction = info->ports[i]->direction;
+ port->icon_name = g_strdup (pa_proplist_gets (info->ports[i]->proplist, "device.icon_name"));
+ port->profiles = determine_profiles_for_port (info->ports[i], profile_list);
+ port_list = g_list_prepend (port_list, port);
+ }
+ gvc_mixer_card_set_ports (card, port_list);
+ is_new = TRUE;
+ }
+
+ gvc_mixer_card_set_name (card, pa_proplist_gets (info->proplist, "device.description"));
+ gvc_mixer_card_set_icon_name (card, pa_proplist_gets (info->proplist, "device.icon_name"));
+ gvc_mixer_card_set_profile (card, info->active_profile->name);
+
+ if (is_new) {
+ g_hash_table_insert (control->priv->cards,
+ GUINT_TO_POINTER (info->index),
+ card);
+ }
+
+ card_ports = gvc_mixer_card_get_ports (card);
+
+ if (card_ports == NULL && is_new) {
+ g_debug ("Portless card just registered - %s", gvc_mixer_card_get_name (card));
+ create_ui_device_from_card (control, card);
+ }
+
+ for (m = card_ports; m != NULL; m = m->next) {
+ GvcMixerCardPort *card_port;
+ card_port = m->data;
+ if (is_new)
+ create_ui_device_from_port (control, card_port, card);
+ else {
+ for (i = 0; i < info->n_ports; i++) {
+ if (g_strcmp0 (card_port->port, info->ports[i]->name) == 0) {
+ if ((card_port->available == PA_PORT_AVAILABLE_NO) != (info->ports[i]->available == PA_PORT_AVAILABLE_NO)) {
+ card_port->available = info->ports[i]->available;
+ g_debug ("sync port availability on card %i, card port name '%s', new available value %i",
+ gvc_mixer_card_get_index (card),
+ card_port->port,
+ card_port->available);
+ match_card_port_with_existing_device (control,
+ card_port,
+ card,
+ card_port->available != PA_PORT_AVAILABLE_NO);
+ }
+ }
+ }
+ }
+ }
+
+#ifdef HAVE_ALSA
+ check_audio_device_selection_needed (control, info);
+#endif /* HAVE_ALSA */
+
+ g_signal_emit (G_OBJECT (control),
+ signals[CARD_ADDED],
+ 0,
+ info->index);
+}
+
+static void
+_pa_context_get_sink_info_cb (pa_context *context,
+ const pa_sink_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+ return;
+ }
+
+ g_warning ("Sink callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_sink (control, i);
+}
+
+static void
+_pa_context_get_source_info_cb (pa_context *context,
+ const pa_source_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+ return;
+ }
+
+ g_warning ("Source callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_source (control, i);
+}
+
+static void
+_pa_context_get_sink_input_info_cb (pa_context *context,
+ const pa_sink_input_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+ return;
+ }
+
+ g_warning ("Sink input callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_sink_input (control, i);
+}
+
+static void
+_pa_context_get_source_output_info_cb (pa_context *context,
+ const pa_source_output_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+ return;
+ }
+
+ g_warning ("Source output callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_source_output (control, i);
+}
+
+static void
+_pa_context_get_client_info_cb (pa_context *context,
+ const pa_client_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY) {
+ return;
+ }
+
+ g_warning ("Client callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_client (control, i);
+}
+
+static void
+_pa_context_get_card_info_by_index_cb (pa_context *context,
+ const pa_card_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ if (pa_context_errno (context) == PA_ERR_NOENTITY)
+ return;
+
+ g_warning ("Card callback failure");
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ return;
+ }
+
+ update_card (control, i);
+}
+
+static void
+_pa_context_get_server_info_cb (pa_context *context,
+ const pa_server_info *i,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (i == NULL) {
+ g_warning ("Server info callback failure");
+ return;
+ }
+ g_debug ("get server info");
+ update_server (control, i);
+ dec_outstanding (control);
+}
+
+static void
+remove_event_role_stream (GvcMixerControl *control)
+{
+ g_debug ("Removing event role");
+}
+
+static void
+update_event_role_stream (GvcMixerControl *control,
+ const pa_ext_stream_restore_info *info)
+{
+ GvcMixerStream *stream;
+ gboolean is_new;
+ pa_volume_t max_volume;
+
+ if (strcmp (info->name, "sink-input-by-media-role:event") != 0) {
+ return;
+ }
+
+#if 0
+ g_debug ("Updating event role: name='%s' device='%s'",
+ info->name,
+ info->device);
+#endif
+
+ is_new = FALSE;
+
+ if (!control->priv->event_sink_input_is_set) {
+ pa_channel_map pa_map;
+ GvcChannelMap *map;
+
+ pa_map.channels = 1;
+ pa_map.map[0] = PA_CHANNEL_POSITION_MONO;
+ map = gvc_channel_map_new_from_pa_channel_map (&pa_map);
+
+ stream = gvc_mixer_event_role_new (control->priv->pa_context,
+ info->device,
+ map);
+ control->priv->event_sink_input_id = gvc_mixer_stream_get_id (stream);
+ control->priv->event_sink_input_is_set = TRUE;
+
+ is_new = TRUE;
+ } else {
+ stream = g_hash_table_lookup (control->priv->all_streams,
+ GUINT_TO_POINTER (control->priv->event_sink_input_id));
+ }
+
+ max_volume = pa_cvolume_max (&info->volume);
+
+ gvc_mixer_stream_set_name (stream, _("System Sounds"));
+ gvc_mixer_stream_set_icon_name (stream, "emblem-system-symbolic");
+ gvc_mixer_stream_set_volume (stream, (guint)max_volume);
+ gvc_mixer_stream_set_is_muted (stream, info->mute);
+
+ if (is_new) {
+ add_stream (control, stream);
+ }
+}
+
+static void
+_pa_ext_stream_restore_read_cb (pa_context *context,
+ const pa_ext_stream_restore_info *i,
+ int eol,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ if (eol < 0) {
+ g_debug ("Failed to initialized stream_restore extension: %s",
+ pa_strerror (pa_context_errno (context)));
+ remove_event_role_stream (control);
+ return;
+ }
+
+ if (eol > 0) {
+ dec_outstanding (control);
+ /* If we don't have an event stream to restore, then
+ * set one up with a default 100% volume */
+ if (!control->priv->event_sink_input_is_set) {
+ pa_ext_stream_restore_info info;
+
+ memset (&info, 0, sizeof(info));
+ info.name = "sink-input-by-media-role:event";
+ info.volume.channels = 1;
+ info.volume.values[0] = PA_VOLUME_NORM;
+ update_event_role_stream (control, &info);
+ }
+ return;
+ }
+
+ update_event_role_stream (control, i);
+}
+
+static void
+_pa_ext_stream_restore_subscribe_cb (pa_context *context,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+ pa_operation *o;
+
+ o = pa_ext_stream_restore_read (context,
+ _pa_ext_stream_restore_read_cb,
+ control);
+ if (o == NULL) {
+ g_warning ("pa_ext_stream_restore_read() failed");
+ return;
+ }
+
+ pa_operation_unref (o);
+}
+
+static void
+req_update_server_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ o = pa_context_get_server_info (control->priv->pa_context,
+ _pa_context_get_server_info_cb,
+ control);
+ if (o == NULL) {
+ g_warning ("pa_context_get_server_info() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_client_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_client_info_list (control->priv->pa_context,
+ _pa_context_get_client_info_cb,
+ control);
+ } else {
+ o = pa_context_get_client_info (control->priv->pa_context,
+ index,
+ _pa_context_get_client_info_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_client_info_list() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_card (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_card_info_list (control->priv->pa_context,
+ _pa_context_get_card_info_by_index_cb,
+ control);
+ } else {
+ o = pa_context_get_card_info_by_index (control->priv->pa_context,
+ index,
+ _pa_context_get_card_info_by_index_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_get_card_info_by_index() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_sink_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_sink_info_list (control->priv->pa_context,
+ _pa_context_get_sink_info_cb,
+ control);
+ } else {
+ o = pa_context_get_sink_info_by_index (control->priv->pa_context,
+ index,
+ _pa_context_get_sink_info_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_get_sink_info_list() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_source_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_source_info_list (control->priv->pa_context,
+ _pa_context_get_source_info_cb,
+ control);
+ } else {
+ o = pa_context_get_source_info_by_index(control->priv->pa_context,
+ index,
+ _pa_context_get_source_info_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_get_source_info_list() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_sink_input_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_sink_input_info_list (control->priv->pa_context,
+ _pa_context_get_sink_input_info_cb,
+ control);
+ } else {
+ o = pa_context_get_sink_input_info (control->priv->pa_context,
+ index,
+ _pa_context_get_sink_input_info_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_get_sink_input_info_list() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+req_update_source_output_info (GvcMixerControl *control,
+ int index)
+{
+ pa_operation *o;
+
+ if (index < 0) {
+ o = pa_context_get_source_output_info_list (control->priv->pa_context,
+ _pa_context_get_source_output_info_cb,
+ control);
+ } else {
+ o = pa_context_get_source_output_info (control->priv->pa_context,
+ index,
+ _pa_context_get_source_output_info_cb,
+ control);
+ }
+
+ if (o == NULL) {
+ g_warning ("pa_context_get_source_output_info_list() failed");
+ return;
+ }
+ pa_operation_unref (o);
+}
+
+static void
+remove_client (GvcMixerControl *control,
+ guint index)
+{
+ g_hash_table_remove (control->priv->clients,
+ GUINT_TO_POINTER (index));
+}
+
+static void
+remove_card (GvcMixerControl *control,
+ guint index)
+{
+
+ GList *devices, *d;
+
+ devices = g_list_concat (g_hash_table_get_values (control->priv->ui_inputs),
+ g_hash_table_get_values (control->priv->ui_outputs));
+
+ for (d = devices; d != NULL; d = d->next) {
+ GvcMixerCard *card;
+ GvcMixerUIDevice *device = d->data;
+
+ g_object_get (G_OBJECT (device), "card", &card, NULL);
+
+ if (gvc_mixer_card_get_index (card) == index) {
+ g_signal_emit (G_OBJECT (control),
+ signals[gvc_mixer_ui_device_is_output (device) ? OUTPUT_REMOVED : INPUT_REMOVED],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ g_debug ("Card removal remove device %s",
+ gvc_mixer_ui_device_get_description (device));
+ g_hash_table_remove (gvc_mixer_ui_device_is_output (device) ? control->priv->ui_outputs : control->priv->ui_inputs,
+ GUINT_TO_POINTER (gvc_mixer_ui_device_get_id (device)));
+ }
+ }
+
+ g_list_free (devices);
+
+ g_hash_table_remove (control->priv->cards,
+ GUINT_TO_POINTER (index));
+
+ g_signal_emit (G_OBJECT (control),
+ signals[CARD_REMOVED],
+ 0,
+ index);
+}
+
+static void
+remove_sink (GvcMixerControl *control,
+ guint index)
+{
+ GvcMixerStream *stream;
+ GvcMixerUIDevice *device;
+
+ g_debug ("Removing sink: index=%u", index);
+
+ stream = g_hash_table_lookup (control->priv->sinks,
+ GUINT_TO_POINTER (index));
+ if (stream == NULL)
+ return;
+
+ device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+ if (device != NULL) {
+ gvc_mixer_ui_device_invalidate_stream (device);
+ if (!gvc_mixer_ui_device_has_ports (device)) {
+ g_signal_emit (G_OBJECT (control),
+ signals[OUTPUT_REMOVED],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ } else {
+ GList *devices, *d;
+
+ devices = g_hash_table_get_values (control->priv->ui_outputs);
+
+ for (d = devices; d != NULL; d = d->next) {
+ guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+ device = d->data;
+ g_object_get (G_OBJECT (device),
+ "stream-id", &stream_id,
+ NULL);
+ if (stream_id == gvc_mixer_stream_get_id (stream))
+ gvc_mixer_ui_device_invalidate_stream (device);
+ }
+
+ g_list_free (devices);
+ }
+ }
+
+ g_hash_table_remove (control->priv->sinks,
+ GUINT_TO_POINTER (index));
+
+ remove_stream (control, stream);
+}
+
+static void
+remove_source (GvcMixerControl *control,
+ guint index)
+{
+ GvcMixerStream *stream;
+ GvcMixerUIDevice *device;
+
+ g_debug ("Removing source: index=%u", index);
+
+ stream = g_hash_table_lookup (control->priv->sources,
+ GUINT_TO_POINTER (index));
+ if (stream == NULL)
+ return;
+
+ device = gvc_mixer_control_lookup_device_from_stream (control, stream);
+
+ if (device != NULL) {
+ gvc_mixer_ui_device_invalidate_stream (device);
+ if (!gvc_mixer_ui_device_has_ports (device)) {
+ g_signal_emit (G_OBJECT (control),
+ signals[INPUT_REMOVED],
+ 0,
+ gvc_mixer_ui_device_get_id (device));
+ } else {
+ GList *devices, *d;
+
+ devices = g_hash_table_get_values (control->priv->ui_inputs);
+
+ for (d = devices; d != NULL; d = d->next) {
+ guint stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+ device = d->data;
+ g_object_get (G_OBJECT (device),
+ "stream-id", &stream_id,
+ NULL);
+ if (stream_id == gvc_mixer_stream_get_id (stream))
+ gvc_mixer_ui_device_invalidate_stream (device);
+ }
+
+ g_list_free (devices);
+ }
+ }
+
+ g_hash_table_remove (control->priv->sources,
+ GUINT_TO_POINTER (index));
+
+ remove_stream (control, stream);
+}
+
+static void
+remove_sink_input (GvcMixerControl *control,
+ guint index)
+{
+ GvcMixerStream *stream;
+
+ g_debug ("Removing sink input: index=%u", index);
+
+ stream = g_hash_table_lookup (control->priv->sink_inputs,
+ GUINT_TO_POINTER (index));
+ if (stream == NULL) {
+ return;
+ }
+ g_hash_table_remove (control->priv->sink_inputs,
+ GUINT_TO_POINTER (index));
+
+ remove_stream (control, stream);
+}
+
+static void
+remove_source_output (GvcMixerControl *control,
+ guint index)
+{
+ GvcMixerStream *stream;
+
+ g_debug ("Removing source output: index=%u", index);
+
+ stream = g_hash_table_lookup (control->priv->source_outputs,
+ GUINT_TO_POINTER (index));
+ if (stream == NULL) {
+ return;
+ }
+ g_hash_table_remove (control->priv->source_outputs,
+ GUINT_TO_POINTER (index));
+
+ remove_stream (control, stream);
+}
+
+static void
+_pa_context_subscribe_cb (pa_context *context,
+ pa_subscription_event_type_t t,
+ uint32_t index,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ switch (t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) {
+ case PA_SUBSCRIPTION_EVENT_SINK:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_sink (control, index);
+ } else {
+ req_update_sink_info (control, index);
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SOURCE:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_source (control, index);
+ } else {
+ req_update_source_info (control, index);
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SINK_INPUT:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_sink_input (control, index);
+ } else {
+ req_update_sink_input_info (control, index);
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SOURCE_OUTPUT:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_source_output (control, index);
+ } else {
+ req_update_source_output_info (control, index);
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_CLIENT:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_client (control, index);
+ } else {
+ req_update_client_info (control, index);
+ }
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_SERVER:
+ req_update_server_info (control, index);
+ break;
+
+ case PA_SUBSCRIPTION_EVENT_CARD:
+ if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_REMOVE) {
+ remove_card (control, index);
+ } else {
+ req_update_card (control, index);
+ }
+ break;
+ default:
+ break;
+ }
+}
+
+static void
+gvc_mixer_control_ready (GvcMixerControl *control)
+{
+ pa_operation *o;
+
+ pa_context_set_subscribe_callback (control->priv->pa_context,
+ _pa_context_subscribe_cb,
+ control);
+ o = pa_context_subscribe (control->priv->pa_context,
+ (pa_subscription_mask_t)
+ (PA_SUBSCRIPTION_MASK_SINK|
+ PA_SUBSCRIPTION_MASK_SOURCE|
+ PA_SUBSCRIPTION_MASK_SINK_INPUT|
+ PA_SUBSCRIPTION_MASK_SOURCE_OUTPUT|
+ PA_SUBSCRIPTION_MASK_CLIENT|
+ PA_SUBSCRIPTION_MASK_SERVER|
+ PA_SUBSCRIPTION_MASK_CARD),
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_subscribe() failed");
+ return;
+ }
+ pa_operation_unref (o);
+
+ req_update_server_info (control, -1);
+ req_update_card (control, -1);
+ req_update_client_info (control, -1);
+ req_update_sink_info (control, -1);
+ req_update_source_info (control, -1);
+ req_update_sink_input_info (control, -1);
+ req_update_source_output_info (control, -1);
+
+ control->priv->server_protocol_version = pa_context_get_server_protocol_version (control->priv->pa_context);
+
+ control->priv->n_outstanding = 6;
+
+ /* This call is not always supported */
+ o = pa_ext_stream_restore_read (control->priv->pa_context,
+ _pa_ext_stream_restore_read_cb,
+ control);
+ if (o != NULL) {
+ pa_operation_unref (o);
+ control->priv->n_outstanding++;
+
+ pa_ext_stream_restore_set_subscribe_cb (control->priv->pa_context,
+ _pa_ext_stream_restore_subscribe_cb,
+ control);
+
+ o = pa_ext_stream_restore_subscribe (control->priv->pa_context,
+ 1,
+ NULL,
+ NULL);
+ if (o != NULL) {
+ pa_operation_unref (o);
+ }
+
+ } else {
+ g_debug ("Failed to initialized stream_restore extension: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ }
+}
+
+static void
+gvc_mixer_new_pa_context (GvcMixerControl *self)
+{
+ pa_proplist *proplist;
+
+ g_return_if_fail (self);
+ g_return_if_fail (!self->priv->pa_context);
+
+ proplist = pa_proplist_new ();
+ pa_proplist_sets (proplist,
+ PA_PROP_APPLICATION_NAME,
+ self->priv->name);
+ pa_proplist_sets (proplist,
+ PA_PROP_APPLICATION_ID,
+ "org.gnome.VolumeControl");
+ pa_proplist_sets (proplist,
+ PA_PROP_APPLICATION_ICON_NAME,
+ "multimedia-volume-control");
+ pa_proplist_sets (proplist,
+ PA_PROP_APPLICATION_VERSION,
+ PACKAGE_VERSION);
+
+ self->priv->pa_context = pa_context_new_with_proplist (self->priv->pa_api, NULL, proplist);
+
+ pa_proplist_free (proplist);
+ g_assert (self->priv->pa_context);
+}
+
+static void
+remove_all_streams (GvcMixerControl *control, GHashTable *hash_table)
+{
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_hash_table_iter_init (&iter, hash_table);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ remove_stream (control, value);
+ g_hash_table_iter_remove (&iter);
+ }
+}
+
+static gboolean
+idle_reconnect (gpointer data)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (data);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ g_return_val_if_fail (control, FALSE);
+
+ if (control->priv->pa_context) {
+ pa_context_unref (control->priv->pa_context);
+ control->priv->pa_context = NULL;
+ control->priv->server_protocol_version = 0;
+ gvc_mixer_new_pa_context (control);
+ }
+
+ remove_all_streams (control, control->priv->sinks);
+ remove_all_streams (control, control->priv->sources);
+ remove_all_streams (control, control->priv->sink_inputs);
+ remove_all_streams (control, control->priv->source_outputs);
+
+ g_hash_table_iter_init (&iter, control->priv->clients);
+ while (g_hash_table_iter_next (&iter, &key, &value))
+ g_hash_table_iter_remove (&iter);
+
+ gvc_mixer_control_open (control); /* cannot fail */
+
+ control->priv->reconnect_id = 0;
+ return FALSE;
+}
+
+static void
+_pa_context_state_cb (pa_context *context,
+ void *userdata)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (userdata);
+
+ switch (pa_context_get_state (context)) {
+ case PA_CONTEXT_UNCONNECTED:
+ case PA_CONTEXT_CONNECTING:
+ case PA_CONTEXT_AUTHORIZING:
+ case PA_CONTEXT_SETTING_NAME:
+ break;
+
+ case PA_CONTEXT_READY:
+ gvc_mixer_control_ready (control);
+ break;
+
+ case PA_CONTEXT_FAILED:
+ control->priv->state = GVC_STATE_FAILED;
+ g_signal_emit (control, signals[STATE_CHANGED], 0, GVC_STATE_FAILED);
+ if (control->priv->reconnect_id == 0)
+ control->priv->reconnect_id = g_timeout_add_seconds (RECONNECT_DELAY, idle_reconnect, control);
+ break;
+
+ case PA_CONTEXT_TERMINATED:
+ default:
+ /* FIXME: */
+ break;
+ }
+}
+
+gboolean
+gvc_mixer_control_open (GvcMixerControl *control)
+{
+ int res;
+
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+ g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
+ g_return_val_if_fail (pa_context_get_state (control->priv->pa_context) == PA_CONTEXT_UNCONNECTED, FALSE);
+
+ pa_context_set_state_callback (control->priv->pa_context,
+ _pa_context_state_cb,
+ control);
+
+ control->priv->state = GVC_STATE_CONNECTING;
+ g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CONNECTING);
+ res = pa_context_connect (control->priv->pa_context, NULL, (pa_context_flags_t) PA_CONTEXT_NOFAIL, NULL);
+ if (res < 0) {
+ g_warning ("Failed to connect context: %s",
+ pa_strerror (pa_context_errno (control->priv->pa_context)));
+ }
+
+ return res;
+}
+
+gboolean
+gvc_mixer_control_close (GvcMixerControl *control)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), FALSE);
+ g_return_val_if_fail (control->priv->pa_context != NULL, FALSE);
+
+ pa_context_disconnect (control->priv->pa_context);
+
+ control->priv->state = GVC_STATE_CLOSED;
+ g_signal_emit (G_OBJECT (control), signals[STATE_CHANGED], 0, GVC_STATE_CLOSED);
+ return TRUE;
+}
+
+static void
+gvc_mixer_control_dispose (GObject *object)
+{
+ GvcMixerControl *control = GVC_MIXER_CONTROL (object);
+
+ if (control->priv->reconnect_id != 0) {
+ g_source_remove (control->priv->reconnect_id);
+ control->priv->reconnect_id = 0;
+ }
+
+ if (control->priv->pa_context != NULL) {
+ pa_context_unref (control->priv->pa_context);
+ control->priv->pa_context = NULL;
+ }
+
+ if (control->priv->default_source_name != NULL) {
+ g_free (control->priv->default_source_name);
+ control->priv->default_source_name = NULL;
+ }
+ if (control->priv->default_sink_name != NULL) {
+ g_free (control->priv->default_sink_name);
+ control->priv->default_sink_name = NULL;
+ }
+
+ if (control->priv->pa_mainloop != NULL) {
+ pa_glib_mainloop_free (control->priv->pa_mainloop);
+ control->priv->pa_mainloop = NULL;
+ }
+
+ if (control->priv->all_streams != NULL) {
+ g_hash_table_destroy (control->priv->all_streams);
+ control->priv->all_streams = NULL;
+ }
+
+ if (control->priv->sinks != NULL) {
+ g_hash_table_destroy (control->priv->sinks);
+ control->priv->sinks = NULL;
+ }
+ if (control->priv->sources != NULL) {
+ g_hash_table_destroy (control->priv->sources);
+ control->priv->sources = NULL;
+ }
+ if (control->priv->sink_inputs != NULL) {
+ g_hash_table_destroy (control->priv->sink_inputs);
+ control->priv->sink_inputs = NULL;
+ }
+ if (control->priv->source_outputs != NULL) {
+ g_hash_table_destroy (control->priv->source_outputs);
+ control->priv->source_outputs = NULL;
+ }
+ if (control->priv->clients != NULL) {
+ g_hash_table_destroy (control->priv->clients);
+ control->priv->clients = NULL;
+ }
+ if (control->priv->cards != NULL) {
+ g_hash_table_destroy (control->priv->cards);
+ control->priv->cards = NULL;
+ }
+ if (control->priv->ui_outputs != NULL) {
+ g_hash_table_destroy (control->priv->ui_outputs);
+ control->priv->ui_outputs = NULL;
+ }
+ if (control->priv->ui_inputs != NULL) {
+ g_hash_table_destroy (control->priv->ui_inputs);
+ control->priv->ui_inputs = NULL;
+ }
+
+ free_priv_port_names (control);
+ G_OBJECT_CLASS (gvc_mixer_control_parent_class)->dispose (object);
+}
+
+static void
+gvc_mixer_control_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerControl *self = GVC_MIXER_CONTROL (object);
+
+ switch (prop_id) {
+ case PROP_NAME:
+ g_free (self->priv->name);
+ self->priv->name = g_value_dup_string (value);
+ g_object_notify (G_OBJECT (self), "name");
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_control_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerControl *self = GVC_MIXER_CONTROL (object);
+
+ switch (prop_id) {
+ case PROP_NAME:
+ g_value_set_string (value, self->priv->name);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+
+static GObject *
+gvc_mixer_control_constructor (GType type,
+ guint n_construct_properties,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GvcMixerControl *self;
+
+ object = G_OBJECT_CLASS (gvc_mixer_control_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+ self = GVC_MIXER_CONTROL (object);
+
+ gvc_mixer_new_pa_context (self);
+ self->priv->profile_swapping_device_id = GVC_MIXER_UI_DEVICE_INVALID;
+
+ return object;
+}
+
+static void
+gvc_mixer_control_class_init (GvcMixerControlClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructor = gvc_mixer_control_constructor;
+ object_class->dispose = gvc_mixer_control_dispose;
+ object_class->finalize = gvc_mixer_control_finalize;
+ object_class->set_property = gvc_mixer_control_set_property;
+ object_class->get_property = gvc_mixer_control_get_property;
+
+ g_object_class_install_property (object_class,
+ PROP_NAME,
+ g_param_spec_string ("name",
+ "Name",
+ "Name to display for this mixer control",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+
+ signals [STATE_CHANGED] =
+ g_signal_new ("state-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, state_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [STREAM_ADDED] =
+ g_signal_new ("stream-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, stream_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [STREAM_REMOVED] =
+ g_signal_new ("stream-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, stream_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [STREAM_CHANGED] =
+ g_signal_new ("stream-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, stream_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [AUDIO_DEVICE_SELECTION_NEEDED] =
+ g_signal_new ("audio-device-selection-needed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_generic,
+ G_TYPE_NONE, 3, G_TYPE_UINT, G_TYPE_BOOLEAN, G_TYPE_UINT);
+ signals [CARD_ADDED] =
+ g_signal_new ("card-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, card_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [CARD_REMOVED] =
+ g_signal_new ("card-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, card_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [DEFAULT_SINK_CHANGED] =
+ g_signal_new ("default-sink-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, default_sink_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [DEFAULT_SOURCE_CHANGED] =
+ g_signal_new ("default-source-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, default_source_changed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [ACTIVE_OUTPUT_UPDATE] =
+ g_signal_new ("active-output-update",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, active_output_update),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [ACTIVE_INPUT_UPDATE] =
+ g_signal_new ("active-input-update",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, active_input_update),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [OUTPUT_ADDED] =
+ g_signal_new ("output-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, output_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [INPUT_ADDED] =
+ g_signal_new ("input-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, input_added),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [OUTPUT_REMOVED] =
+ g_signal_new ("output-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, output_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+ signals [INPUT_REMOVED] =
+ g_signal_new ("input-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GvcMixerControlClass, input_removed),
+ NULL, NULL,
+ g_cclosure_marshal_VOID__UINT,
+ G_TYPE_NONE, 1, G_TYPE_UINT);
+}
+
+
+static void
+gvc_mixer_control_init (GvcMixerControl *control)
+{
+ control->priv = gvc_mixer_control_get_instance_private (control);
+
+ control->priv->pa_mainloop = pa_glib_mainloop_new (g_main_context_default ());
+ g_assert (control->priv->pa_mainloop);
+
+ control->priv->pa_api = pa_glib_mainloop_get_api (control->priv->pa_mainloop);
+ g_assert (control->priv->pa_api);
+
+ control->priv->all_streams = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->sinks = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->sources = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->sink_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->source_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->cards = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->ui_outputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+ control->priv->ui_inputs = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_object_unref);
+
+ control->priv->clients = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify)g_free);
+
+#ifdef HAVE_ALSA
+ control->priv->headset_card = -1;
+#endif /* HAVE_ALSA */
+
+ control->priv->state = GVC_STATE_CLOSED;
+}
+
+static void
+gvc_mixer_control_finalize (GObject *object)
+{
+ GvcMixerControl *mixer_control;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_CONTROL (object));
+
+ mixer_control = GVC_MIXER_CONTROL (object);
+ g_free (mixer_control->priv->name);
+ mixer_control->priv->name = NULL;
+
+ g_return_if_fail (mixer_control->priv != NULL);
+ G_OBJECT_CLASS (gvc_mixer_control_parent_class)->finalize (object);
+}
+
+GvcMixerControl *
+gvc_mixer_control_new (const char *name)
+{
+ GObject *control;
+ control = g_object_new (GVC_TYPE_MIXER_CONTROL,
+ "name", name,
+ NULL);
+ return GVC_MIXER_CONTROL (control);
+}
+
+gdouble
+gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
+ return (gdouble) PA_VOLUME_NORM;
+}
+
+gdouble
+gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_CONTROL (control), 0);
+ return (gdouble) PA_VOLUME_UI_MAX;
+}
diff --git a/subprojects/gvc/gvc-mixer-control.h b/subprojects/gvc/gvc-mixer-control.h
new file mode 100644
index 0000000..8137849
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-control.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_CONTROL_H
+#define __GVC_MIXER_CONTROL_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-card.h"
+#include "gvc-mixer-ui-device.h"
+
+G_BEGIN_DECLS
+
+typedef enum
+{
+ GVC_STATE_CLOSED,
+ GVC_STATE_READY,
+ GVC_STATE_CONNECTING,
+ GVC_STATE_FAILED
+} GvcMixerControlState;
+
+typedef enum
+{
+ GVC_HEADSET_PORT_CHOICE_NONE = 0,
+ GVC_HEADSET_PORT_CHOICE_HEADPHONES = 1 << 0,
+ GVC_HEADSET_PORT_CHOICE_HEADSET = 1 << 1,
+ GVC_HEADSET_PORT_CHOICE_MIC = 1 << 2
+} GvcHeadsetPortChoice;
+
+#define GVC_TYPE_MIXER_CONTROL (gvc_mixer_control_get_type ())
+#define GVC_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControl))
+#define GVC_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass))
+#define GVC_IS_MIXER_CONTROL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_CONTROL))
+#define GVC_IS_MIXER_CONTROL_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_CONTROL))
+#define GVC_MIXER_CONTROL_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_CONTROL, GvcMixerControlClass))
+
+typedef struct GvcMixerControlPrivate GvcMixerControlPrivate;
+
+typedef struct
+{
+ GObject parent;
+ GvcMixerControlPrivate *priv;
+} GvcMixerControl;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ void (*state_changed) (GvcMixerControl *control,
+ GvcMixerControlState new_state);
+ void (*stream_added) (GvcMixerControl *control,
+ guint id);
+ void (*stream_changed) (GvcMixerControl *control,
+ guint id);
+ void (*stream_removed) (GvcMixerControl *control,
+ guint id);
+ void (*card_added) (GvcMixerControl *control,
+ guint id);
+ void (*card_removed) (GvcMixerControl *control,
+ guint id);
+ void (*default_sink_changed) (GvcMixerControl *control,
+ guint id);
+ void (*default_source_changed) (GvcMixerControl *control,
+ guint id);
+ void (*active_output_update) (GvcMixerControl *control,
+ guint id);
+ void (*active_input_update) (GvcMixerControl *control,
+ guint id);
+ void (*output_added) (GvcMixerControl *control,
+ guint id);
+ void (*input_added) (GvcMixerControl *control,
+ guint id);
+ void (*output_removed) (GvcMixerControl *control,
+ guint id);
+ void (*input_removed) (GvcMixerControl *control,
+ guint id);
+ void (*audio_device_selection_needed)
+ (GvcMixerControl *control,
+ guint id,
+ gboolean show_dialog,
+ GvcHeadsetPortChoice choices);
+} GvcMixerControlClass;
+
+GType gvc_mixer_control_get_type (void);
+
+GvcMixerControl * gvc_mixer_control_new (const char *name);
+
+gboolean gvc_mixer_control_open (GvcMixerControl *control);
+gboolean gvc_mixer_control_close (GvcMixerControl *control);
+
+GSList * gvc_mixer_control_get_cards (GvcMixerControl *control);
+GSList * gvc_mixer_control_get_streams (GvcMixerControl *control);
+GSList * gvc_mixer_control_get_sinks (GvcMixerControl *control);
+GSList * gvc_mixer_control_get_sources (GvcMixerControl *control);
+GSList * gvc_mixer_control_get_sink_inputs (GvcMixerControl *control);
+GSList * gvc_mixer_control_get_source_outputs (GvcMixerControl *control);
+
+GvcMixerStream * gvc_mixer_control_lookup_stream_id (GvcMixerControl *control,
+ guint id);
+GvcMixerCard * gvc_mixer_control_lookup_card_id (GvcMixerControl *control,
+ guint id);
+GvcMixerUIDevice * gvc_mixer_control_lookup_output_id (GvcMixerControl *control,
+ guint id);
+GvcMixerUIDevice * gvc_mixer_control_lookup_input_id (GvcMixerControl *control,
+ guint id);
+GvcMixerUIDevice * gvc_mixer_control_lookup_device_from_stream (GvcMixerControl *control,
+ GvcMixerStream *stream);
+
+GvcMixerStream * gvc_mixer_control_get_default_sink (GvcMixerControl *control);
+GvcMixerStream * gvc_mixer_control_get_default_source (GvcMixerControl *control);
+GvcMixerStream * gvc_mixer_control_get_event_sink_input (GvcMixerControl *control);
+
+gboolean gvc_mixer_control_set_default_sink (GvcMixerControl *control,
+ GvcMixerStream *stream);
+gboolean gvc_mixer_control_set_default_source (GvcMixerControl *control,
+ GvcMixerStream *stream);
+
+gdouble gvc_mixer_control_get_vol_max_norm (GvcMixerControl *control);
+gdouble gvc_mixer_control_get_vol_max_amplified (GvcMixerControl *control);
+void gvc_mixer_control_change_output (GvcMixerControl *control,
+ GvcMixerUIDevice* output);
+void gvc_mixer_control_change_input (GvcMixerControl *control,
+ GvcMixerUIDevice* input);
+GvcMixerStream* gvc_mixer_control_get_stream_from_device (GvcMixerControl *control,
+ GvcMixerUIDevice *device);
+gboolean gvc_mixer_control_change_profile_on_selected_device (GvcMixerControl *control,
+ GvcMixerUIDevice *device,
+ const gchar* profile);
+
+void gvc_mixer_control_set_headset_port (GvcMixerControl *control,
+ guint id,
+ GvcHeadsetPortChoice choices);
+
+GvcMixerControlState gvc_mixer_control_get_state (GvcMixerControl *control);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_CONTROL_H */
diff --git a/subprojects/gvc/gvc-mixer-event-role.c b/subprojects/gvc/gvc-mixer-event-role.c
new file mode 100644
index 0000000..9f5e26a
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-event-role.c
@@ -0,0 +1,228 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+#include <pulse/ext-stream-restore.h>
+
+#include "gvc-mixer-event-role.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerEventRolePrivate
+{
+ char *device;
+};
+
+enum
+{
+ PROP_0,
+ PROP_DEVICE
+};
+
+static void gvc_mixer_event_role_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerEventRole, gvc_mixer_event_role, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+update_settings (GvcMixerEventRole *role,
+ gboolean is_muted,
+ gpointer *op)
+{
+ pa_operation *o;
+ const GvcChannelMap *map;
+ pa_context *context;
+ pa_ext_stream_restore_info info;
+
+ map = gvc_mixer_stream_get_channel_map (GVC_MIXER_STREAM(role));
+
+ info.volume = *gvc_channel_map_get_cvolume(map);
+ info.name = "sink-input-by-media-role:event";
+ info.channel_map = *gvc_channel_map_get_pa_channel_map(map);
+ info.device = role->priv->device;
+ info.mute = is_muted;
+
+ context = gvc_mixer_stream_get_pa_context (GVC_MIXER_STREAM (role));
+
+ o = pa_ext_stream_restore_write (context,
+ PA_UPDATE_REPLACE,
+ &info,
+ 1,
+ TRUE,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_ext_stream_restore_write() failed");
+ return FALSE;
+ }
+
+ if (op != NULL)
+ *op = o;
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_event_role_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ return update_settings (GVC_MIXER_EVENT_ROLE (stream),
+ gvc_mixer_stream_get_is_muted (stream), op);
+}
+
+static gboolean
+gvc_mixer_event_role_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ /* Apply change straight away so that we don't get a race with
+ * gvc_mixer_event_role_push_volume().
+ * See https://bugs.freedesktop.org/show_bug.cgi?id=51413 */
+ gvc_mixer_stream_set_is_muted (stream, is_muted);
+ return update_settings (GVC_MIXER_EVENT_ROLE (stream),
+ is_muted, NULL);
+}
+
+static gboolean
+gvc_mixer_event_role_set_device (GvcMixerEventRole *role,
+ const char *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_EVENT_ROLE (role), FALSE);
+
+ g_free (role->priv->device);
+ role->priv->device = g_strdup (device);
+ g_object_notify (G_OBJECT (role), "device");
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_event_role_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object);
+
+ switch (prop_id) {
+ case PROP_DEVICE:
+ gvc_mixer_event_role_set_device (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_event_role_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerEventRole *self = GVC_MIXER_EVENT_ROLE (object);
+
+ switch (prop_id) {
+ case PROP_DEVICE:
+ g_value_set_string (value, self->priv->device);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_event_role_class_init (GvcMixerEventRoleClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+ object_class->finalize = gvc_mixer_event_role_finalize;
+ object_class->set_property = gvc_mixer_event_role_set_property;
+ object_class->get_property = gvc_mixer_event_role_get_property;
+
+ stream_class->push_volume = gvc_mixer_event_role_push_volume;
+ stream_class->change_is_muted = gvc_mixer_event_role_change_is_muted;
+
+ g_object_class_install_property (object_class,
+ PROP_DEVICE,
+ g_param_spec_string ("device",
+ "Device",
+ "Device",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+}
+
+static void
+gvc_mixer_event_role_init (GvcMixerEventRole *event_role)
+{
+ event_role->priv = gvc_mixer_event_role_get_instance_private (event_role);
+
+}
+
+static void
+gvc_mixer_event_role_finalize (GObject *object)
+{
+ GvcMixerEventRole *mixer_event_role;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_EVENT_ROLE (object));
+
+ mixer_event_role = GVC_MIXER_EVENT_ROLE (object);
+
+ g_return_if_fail (mixer_event_role->priv != NULL);
+
+ g_free (mixer_event_role->priv->device);
+
+ G_OBJECT_CLASS (gvc_mixer_event_role_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_event_role_new: (skip)
+ * @context:
+ * @device:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_event_role_new (pa_context *context,
+ const char *device,
+ GvcChannelMap *channel_map)
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_EVENT_ROLE,
+ "pa-context", context,
+ "index", 0,
+ "device", device,
+ "channel-map", channel_map,
+ NULL);
+
+ return GVC_MIXER_STREAM (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-event-role.h b/subprojects/gvc/gvc-mixer-event-role.h
new file mode 100644
index 0000000..ab4c509
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-event-role.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_EVENT_ROLE_H
+#define __GVC_MIXER_EVENT_ROLE_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_EVENT_ROLE (gvc_mixer_event_role_get_type ())
+#define GVC_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRole))
+#define GVC_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass))
+#define GVC_IS_MIXER_EVENT_ROLE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_EVENT_ROLE))
+#define GVC_IS_MIXER_EVENT_ROLE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_EVENT_ROLE))
+#define GVC_MIXER_EVENT_ROLE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_EVENT_ROLE, GvcMixerEventRoleClass))
+
+typedef struct GvcMixerEventRolePrivate GvcMixerEventRolePrivate;
+
+typedef struct
+{
+ GvcMixerStream parent;
+ GvcMixerEventRolePrivate *priv;
+} GvcMixerEventRole;
+
+typedef struct
+{
+ GvcMixerStreamClass parent_class;
+} GvcMixerEventRoleClass;
+
+GType gvc_mixer_event_role_get_type (void);
+
+GvcMixerStream * gvc_mixer_event_role_new (pa_context *context,
+ const char *device,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_EVENT_ROLE_H */
diff --git a/subprojects/gvc/gvc-mixer-sink-input.c b/subprojects/gvc/gvc-mixer-sink-input.c
new file mode 100644
index 0000000..a359daf
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-sink-input.c
@@ -0,0 +1,159 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-sink-input.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSinkInputPrivate
+{
+ gpointer dummy;
+};
+
+static void gvc_mixer_sink_input_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSinkInput, gvc_mixer_sink_input, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_sink_input_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ pa_operation *o;
+ guint index;
+ const GvcChannelMap *map;
+ pa_context *context;
+ const pa_cvolume *cv;
+
+ index = gvc_mixer_stream_get_index (stream);
+
+ map = gvc_mixer_stream_get_channel_map (stream);
+
+ cv = gvc_channel_map_get_cvolume(map);
+
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_sink_input_volume (context,
+ index,
+ cv,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_sink_input_volume() failed");
+ return FALSE;
+ }
+
+ *op = o;
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_input_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_sink_input_mute (context,
+ index,
+ is_muted,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_sink_input_mute_by_index() failed");
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_sink_input_class_init (GvcMixerSinkInputClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+ object_class->finalize = gvc_mixer_sink_input_finalize;
+
+ stream_class->push_volume = gvc_mixer_sink_input_push_volume;
+ stream_class->change_is_muted = gvc_mixer_sink_input_change_is_muted;
+}
+
+static void
+gvc_mixer_sink_input_init (GvcMixerSinkInput *sink_input)
+{
+ sink_input->priv = gvc_mixer_sink_input_get_instance_private (sink_input);
+}
+
+static void
+gvc_mixer_sink_input_finalize (GObject *object)
+{
+ GvcMixerSinkInput *mixer_sink_input;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_SINK_INPUT (object));
+
+ mixer_sink_input = GVC_MIXER_SINK_INPUT (object);
+
+ g_return_if_fail (mixer_sink_input->priv != NULL);
+ G_OBJECT_CLASS (gvc_mixer_sink_input_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_sink_input_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_sink_input_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map)
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_SINK_INPUT,
+ "pa-context", context,
+ "index", index,
+ "channel-map", channel_map,
+ NULL);
+
+ return GVC_MIXER_STREAM (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-sink-input.h b/subprojects/gvc/gvc-mixer-sink-input.h
new file mode 100644
index 0000000..17bf127
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-sink-input.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SINK_INPUT_H
+#define __GVC_MIXER_SINK_INPUT_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SINK_INPUT (gvc_mixer_sink_input_get_type ())
+#define GVC_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInput))
+#define GVC_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass))
+#define GVC_IS_MIXER_SINK_INPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK_INPUT))
+#define GVC_IS_MIXER_SINK_INPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK_INPUT))
+#define GVC_MIXER_SINK_INPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK_INPUT, GvcMixerSinkInputClass))
+
+typedef struct GvcMixerSinkInputPrivate GvcMixerSinkInputPrivate;
+
+typedef struct
+{
+ GvcMixerStream parent;
+ GvcMixerSinkInputPrivate *priv;
+} GvcMixerSinkInput;
+
+typedef struct
+{
+ GvcMixerStreamClass parent_class;
+} GvcMixerSinkInputClass;
+
+GType gvc_mixer_sink_input_get_type (void);
+
+GvcMixerStream * gvc_mixer_sink_input_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SINK_INPUT_H */
diff --git a/subprojects/gvc/gvc-mixer-sink.c b/subprojects/gvc/gvc-mixer-sink.c
new file mode 100644
index 0000000..a6115c6
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-sink.c
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-sink.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSinkPrivate
+{
+ gpointer dummy;
+};
+
+static void gvc_mixer_sink_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSink, gvc_mixer_sink, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_sink_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ pa_operation *o;
+ guint index;
+ const GvcChannelMap *map;
+ pa_context *context;
+ const pa_cvolume *cv;
+
+ index = gvc_mixer_stream_get_index (stream);
+
+ map = gvc_mixer_stream_get_channel_map (stream);
+
+ /* set the volume */
+ cv = gvc_channel_map_get_cvolume(map);
+
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_sink_volume_by_index (context,
+ index,
+ cv,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_sink_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ *op = o;
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_sink_mute_by_index (context,
+ index,
+ is_muted,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_sink_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_sink_change_port (GvcMixerStream *stream,
+ const char *port)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_sink_port_by_index (context,
+ index,
+ port,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_sink_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_sink_class_init (GvcMixerSinkClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+ object_class->finalize = gvc_mixer_sink_finalize;
+
+ stream_class->push_volume = gvc_mixer_sink_push_volume;
+ stream_class->change_port = gvc_mixer_sink_change_port;
+ stream_class->change_is_muted = gvc_mixer_sink_change_is_muted;
+}
+
+static void
+gvc_mixer_sink_init (GvcMixerSink *sink)
+{
+ sink->priv = gvc_mixer_sink_get_instance_private (sink);
+}
+
+static void
+gvc_mixer_sink_finalize (GObject *object)
+{
+ GvcMixerSink *mixer_sink;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_SINK (object));
+
+ mixer_sink = GVC_MIXER_SINK (object);
+
+ g_return_if_fail (mixer_sink->priv != NULL);
+ G_OBJECT_CLASS (gvc_mixer_sink_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_sink_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_sink_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map)
+
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_SINK,
+ "pa-context", context,
+ "index", index,
+ "channel-map", channel_map,
+ NULL);
+
+ return GVC_MIXER_STREAM (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-sink.h b/subprojects/gvc/gvc-mixer-sink.h
new file mode 100644
index 0000000..3fbe291
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-sink.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SINK_H
+#define __GVC_MIXER_SINK_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SINK (gvc_mixer_sink_get_type ())
+#define GVC_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SINK, GvcMixerSink))
+#define GVC_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass))
+#define GVC_IS_MIXER_SINK(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SINK))
+#define GVC_IS_MIXER_SINK_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SINK))
+#define GVC_MIXER_SINK_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SINK, GvcMixerSinkClass))
+
+typedef struct GvcMixerSinkPrivate GvcMixerSinkPrivate;
+
+typedef struct
+{
+ GvcMixerStream parent;
+ GvcMixerSinkPrivate *priv;
+} GvcMixerSink;
+
+typedef struct
+{
+ GvcMixerStreamClass parent_class;
+} GvcMixerSinkClass;
+
+GType gvc_mixer_sink_get_type (void);
+
+GvcMixerStream * gvc_mixer_sink_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SINK_H */
diff --git a/subprojects/gvc/gvc-mixer-source-output.c b/subprojects/gvc/gvc-mixer-source-output.c
new file mode 100644
index 0000000..c4a275a
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-source-output.c
@@ -0,0 +1,160 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-source-output.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSourceOutputPrivate
+{
+ gpointer dummy;
+};
+
+static void gvc_mixer_source_output_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSourceOutput, gvc_mixer_source_output, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_source_output_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ pa_operation *o;
+ guint index;
+ const GvcChannelMap *map;
+ pa_context *context;
+ const pa_cvolume *cv;
+
+ index = gvc_mixer_stream_get_index (stream);
+
+ map = gvc_mixer_stream_get_channel_map (stream);
+
+ cv = gvc_channel_map_get_cvolume(map);
+
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_source_output_volume (context,
+ index,
+ cv,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_source_output_volume() failed");
+ return FALSE;
+ }
+
+ *op = o;
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_output_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_source_output_mute (context,
+ index,
+ is_muted,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_source_output_mute_by_index() failed");
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_source_output_class_init (GvcMixerSourceOutputClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+ object_class->finalize = gvc_mixer_source_output_finalize;
+
+ stream_class->push_volume = gvc_mixer_source_output_push_volume;
+ stream_class->change_is_muted = gvc_mixer_source_output_change_is_muted;
+}
+
+static void
+gvc_mixer_source_output_init (GvcMixerSourceOutput *source_output)
+{
+ source_output->priv = gvc_mixer_source_output_get_instance_private (source_output);
+
+}
+
+static void
+gvc_mixer_source_output_finalize (GObject *object)
+{
+ GvcMixerSourceOutput *mixer_source_output;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_SOURCE_OUTPUT (object));
+
+ mixer_source_output = GVC_MIXER_SOURCE_OUTPUT (object);
+
+ g_return_if_fail (mixer_source_output->priv != NULL);
+ G_OBJECT_CLASS (gvc_mixer_source_output_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_source_output_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_source_output_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map)
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_SOURCE_OUTPUT,
+ "pa-context", context,
+ "index", index,
+ "channel-map", channel_map,
+ NULL);
+
+ return GVC_MIXER_STREAM (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-source-output.h b/subprojects/gvc/gvc-mixer-source-output.h
new file mode 100644
index 0000000..4d9a6d6
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-source-output.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SOURCE_OUTPUT_H
+#define __GVC_MIXER_SOURCE_OUTPUT_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SOURCE_OUTPUT (gvc_mixer_source_output_get_type ())
+#define GVC_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutput))
+#define GVC_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass))
+#define GVC_IS_MIXER_SOURCE_OUTPUT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT))
+#define GVC_IS_MIXER_SOURCE_OUTPUT_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE_OUTPUT))
+#define GVC_MIXER_SOURCE_OUTPUT_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE_OUTPUT, GvcMixerSourceOutputClass))
+
+typedef struct GvcMixerSourceOutputPrivate GvcMixerSourceOutputPrivate;
+
+typedef struct
+{
+ GvcMixerStream parent;
+ GvcMixerSourceOutputPrivate *priv;
+} GvcMixerSourceOutput;
+
+typedef struct
+{
+ GvcMixerStreamClass parent_class;
+} GvcMixerSourceOutputClass;
+
+GType gvc_mixer_source_output_get_type (void);
+
+GvcMixerStream * gvc_mixer_source_output_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SOURCE_OUTPUT_H */
diff --git a/subprojects/gvc/gvc-mixer-source.c b/subprojects/gvc/gvc-mixer-source.c
new file mode 100644
index 0000000..434eec3
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-source.c
@@ -0,0 +1,189 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-source.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+
+struct GvcMixerSourcePrivate
+{
+ gpointer dummy;
+};
+
+static void gvc_mixer_source_finalize (GObject *object);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerSource, gvc_mixer_source, GVC_TYPE_MIXER_STREAM)
+
+static gboolean
+gvc_mixer_source_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ pa_operation *o;
+ guint index;
+ const GvcChannelMap *map;
+ pa_context *context;
+ const pa_cvolume *cv;
+
+ index = gvc_mixer_stream_get_index (stream);
+
+ map = gvc_mixer_stream_get_channel_map (stream);
+
+ /* set the volume */
+ cv = gvc_channel_map_get_cvolume (map);
+
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_source_volume_by_index (context,
+ index,
+ cv,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_source_volume_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ *op = o;
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_source_mute_by_index (context,
+ index,
+ is_muted,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_source_mute_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static gboolean
+gvc_mixer_source_change_port (GvcMixerStream *stream,
+ const char *port)
+{
+ pa_operation *o;
+ guint index;
+ pa_context *context;
+
+ index = gvc_mixer_stream_get_index (stream);
+ context = gvc_mixer_stream_get_pa_context (stream);
+
+ o = pa_context_set_source_port_by_index (context,
+ index,
+ port,
+ NULL,
+ NULL);
+
+ if (o == NULL) {
+ g_warning ("pa_context_set_source_port_by_index() failed: %s", pa_strerror(pa_context_errno(context)));
+ return FALSE;
+ }
+
+ pa_operation_unref(o);
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_source_class_init (GvcMixerSourceClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GvcMixerStreamClass *stream_class = GVC_MIXER_STREAM_CLASS (klass);
+
+ object_class->finalize = gvc_mixer_source_finalize;
+
+ stream_class->push_volume = gvc_mixer_source_push_volume;
+ stream_class->change_is_muted = gvc_mixer_source_change_is_muted;
+ stream_class->change_port = gvc_mixer_source_change_port;
+}
+
+static void
+gvc_mixer_source_init (GvcMixerSource *source)
+{
+ source->priv = gvc_mixer_source_get_instance_private (source);
+}
+
+static void
+gvc_mixer_source_finalize (GObject *object)
+{
+ GvcMixerSource *mixer_source;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_SOURCE (object));
+
+ mixer_source = GVC_MIXER_SOURCE (object);
+
+ g_return_if_fail (mixer_source->priv != NULL);
+ G_OBJECT_CLASS (gvc_mixer_source_parent_class)->finalize (object);
+}
+
+/**
+ * gvc_mixer_source_new: (skip)
+ * @context:
+ * @index:
+ * @channel_map:
+ *
+ * Returns:
+ */
+GvcMixerStream *
+gvc_mixer_source_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map)
+
+{
+ GObject *object;
+
+ object = g_object_new (GVC_TYPE_MIXER_SOURCE,
+ "pa-context", context,
+ "index", index,
+ "channel-map", channel_map,
+ NULL);
+
+ return GVC_MIXER_STREAM (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-source.h b/subprojects/gvc/gvc-mixer-source.h
new file mode 100644
index 0000000..bdffe8c
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-source.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_SOURCE_H
+#define __GVC_MIXER_SOURCE_H
+
+#include <glib-object.h>
+#include "gvc-mixer-stream.h"
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_SOURCE (gvc_mixer_source_get_type ())
+#define GVC_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSource))
+#define GVC_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass))
+#define GVC_IS_MIXER_SOURCE(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_SOURCE))
+#define GVC_IS_MIXER_SOURCE_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_SOURCE))
+#define GVC_MIXER_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_SOURCE, GvcMixerSourceClass))
+
+typedef struct GvcMixerSourcePrivate GvcMixerSourcePrivate;
+
+typedef struct
+{
+ GvcMixerStream parent;
+ GvcMixerSourcePrivate *priv;
+} GvcMixerSource;
+
+typedef struct
+{
+ GvcMixerStreamClass parent_class;
+} GvcMixerSourceClass;
+
+GType gvc_mixer_source_get_type (void);
+
+GvcMixerStream * gvc_mixer_source_new (pa_context *context,
+ guint index,
+ GvcChannelMap *channel_map);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_SOURCE_H */
diff --git a/subprojects/gvc/gvc-mixer-stream-private.h b/subprojects/gvc/gvc-mixer-stream-private.h
new file mode 100644
index 0000000..b97ecf5
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-stream-private.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_STREAM_PRIVATE_H
+#define __GVC_MIXER_STREAM_PRIVATE_H
+
+#include <glib-object.h>
+
+#include "gvc-channel-map.h"
+
+G_BEGIN_DECLS
+
+pa_context * gvc_mixer_stream_get_pa_context (GvcMixerStream *stream);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_STREAM_PRIVATE_H */
diff --git a/subprojects/gvc/gvc-mixer-stream.c b/subprojects/gvc/gvc-mixer-stream.c
new file mode 100644
index 0000000..c324900
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-stream.c
@@ -0,0 +1,1090 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 William Jon McCann
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gi18n-lib.h>
+
+#include <pulse/pulseaudio.h>
+
+#include "gvc-mixer-stream.h"
+#include "gvc-mixer-stream-private.h"
+#include "gvc-channel-map-private.h"
+#include "gvc-enum-types.h"
+
+static guint32 stream_serial = 1;
+
+struct GvcMixerStreamPrivate
+{
+ pa_context *pa_context;
+ guint id;
+ guint index;
+ guint card_index;
+ GvcChannelMap *channel_map;
+ char *name;
+ char *description;
+ char *application_id;
+ char *icon_name;
+ char *form_factor;
+ char *sysfs_path;
+ gboolean is_muted;
+ gboolean can_decibel;
+ gboolean is_event_stream;
+ gboolean is_virtual;
+ pa_volume_t base_volume;
+ pa_operation *change_volume_op;
+ char *port;
+ char *human_port;
+ GList *ports;
+ GvcMixerStreamState state;
+};
+
+enum
+{
+ PROP_0,
+ PROP_ID,
+ PROP_PA_CONTEXT,
+ PROP_CHANNEL_MAP,
+ PROP_INDEX,
+ PROP_NAME,
+ PROP_DESCRIPTION,
+ PROP_APPLICATION_ID,
+ PROP_ICON_NAME,
+ PROP_FORM_FACTOR,
+ PROP_SYSFS_PATH,
+ PROP_VOLUME,
+ PROP_DECIBEL,
+ PROP_IS_MUTED,
+ PROP_CAN_DECIBEL,
+ PROP_IS_EVENT_STREAM,
+ PROP_IS_VIRTUAL,
+ PROP_CARD_INDEX,
+ PROP_PORT,
+ PROP_STATE,
+};
+
+static void gvc_mixer_stream_finalize (GObject *object);
+
+G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GvcMixerStream, gvc_mixer_stream, G_TYPE_OBJECT)
+
+static void
+free_port (GvcMixerStreamPort *p)
+{
+ g_free (p->port);
+ g_free (p->human_port);
+ g_slice_free (GvcMixerStreamPort, p);
+}
+
+static GvcMixerStreamPort *
+dup_port (GvcMixerStreamPort *p)
+{
+ GvcMixerStreamPort *m;
+
+ m = g_slice_new (GvcMixerStreamPort);
+
+ *m = *p;
+ m->port = g_strdup (p->port);
+ m->human_port = g_strdup (p->human_port);
+
+ return m;
+}
+
+G_DEFINE_BOXED_TYPE (GvcMixerStreamPort, gvc_mixer_stream_port, dup_port, free_port)
+
+static guint32
+get_next_stream_serial (void)
+{
+ guint32 serial;
+
+ serial = stream_serial++;
+
+ if ((gint32)stream_serial < 0) {
+ stream_serial = 1;
+ }
+
+ return serial;
+}
+
+pa_context *
+gvc_mixer_stream_get_pa_context (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+ return stream->priv->pa_context;
+}
+
+guint
+gvc_mixer_stream_get_index (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+ return stream->priv->index;
+}
+
+guint
+gvc_mixer_stream_get_id (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+ return stream->priv->id;
+}
+
+const GvcChannelMap *
+gvc_mixer_stream_get_channel_map (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->channel_map;
+}
+
+/**
+ * gvc_mixer_stream_get_volume:
+ * @stream:
+ *
+ * Returns: (type guint32):
+ */
+pa_volume_t
+gvc_mixer_stream_get_volume (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+ return (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME];
+}
+
+gdouble
+gvc_mixer_stream_get_decibel (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+ return pa_sw_volume_to_dB(
+ (pa_volume_t) gvc_channel_map_get_volume(stream->priv->channel_map)[VOLUME]);
+}
+
+/**
+ * gvc_mixer_stream_set_volume:
+ * @stream:
+ * @volume: (type guint32):
+ *
+ * Returns:
+ */
+gboolean
+gvc_mixer_stream_set_volume (GvcMixerStream *stream,
+ pa_volume_t volume)
+{
+ pa_cvolume cv;
+
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map);
+ pa_cvolume_scale(&cv, volume);
+
+ if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) {
+ gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE);
+ g_object_notify (G_OBJECT (stream), "volume");
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+gboolean
+gvc_mixer_stream_set_decibel (GvcMixerStream *stream,
+ gdouble db)
+{
+ pa_cvolume cv;
+
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ cv = *gvc_channel_map_get_cvolume(stream->priv->channel_map);
+ pa_cvolume_scale(&cv, pa_sw_volume_from_dB(db));
+
+ if (!pa_cvolume_equal(gvc_channel_map_get_cvolume(stream->priv->channel_map), &cv)) {
+ gvc_channel_map_volume_changed(stream->priv->channel_map, &cv, FALSE);
+ g_object_notify (G_OBJECT (stream), "volume");
+ }
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_get_is_muted (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ return stream->priv->is_muted;
+}
+
+gboolean
+gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ return stream->priv->can_decibel;
+}
+
+gboolean
+gvc_mixer_stream_set_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (is_muted != stream->priv->is_muted) {
+ stream->priv->is_muted = is_muted;
+ g_object_notify (G_OBJECT (stream), "is-muted");
+ }
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream,
+ gboolean can_decibel)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (can_decibel != stream->priv->can_decibel) {
+ stream->priv->can_decibel = can_decibel;
+ g_object_notify (G_OBJECT (stream), "can-decibel");
+ }
+
+ return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_name (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->name;
+}
+
+const char *
+gvc_mixer_stream_get_description (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->description;
+}
+
+gboolean
+gvc_mixer_stream_set_name (GvcMixerStream *stream,
+ const char *name)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->name);
+ stream->priv->name = g_strdup (name);
+ g_object_notify (G_OBJECT (stream), "name");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_description (GvcMixerStream *stream,
+ const char *description)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->description);
+ stream->priv->description = g_strdup (description);
+ g_object_notify (G_OBJECT (stream), "description");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_is_event_stream (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ return stream->priv->is_event_stream;
+}
+
+gboolean
+gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream,
+ gboolean is_event_stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ stream->priv->is_event_stream = is_event_stream;
+ g_object_notify (G_OBJECT (stream), "is-event-stream");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_is_virtual (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ return stream->priv->is_virtual;
+}
+
+gboolean
+gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream,
+ gboolean is_virtual)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ stream->priv->is_virtual = is_virtual;
+ g_object_notify (G_OBJECT (stream), "is-virtual");
+
+ return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_application_id (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->application_id;
+}
+
+gboolean
+gvc_mixer_stream_set_application_id (GvcMixerStream *stream,
+ const char *application_id)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->application_id);
+ stream->priv->application_id = g_strdup (application_id);
+ g_object_notify (G_OBJECT (stream), "application-id");
+
+ return TRUE;
+}
+
+static void
+on_channel_map_volume_changed (GvcChannelMap *channel_map,
+ gboolean set,
+ GvcMixerStream *stream)
+{
+ if (set == TRUE)
+ gvc_mixer_stream_push_volume (stream);
+
+ g_object_notify (G_OBJECT (stream), "volume");
+}
+
+static gboolean
+gvc_mixer_stream_set_channel_map (GvcMixerStream *stream,
+ GvcChannelMap *channel_map)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (channel_map != NULL) {
+ g_object_ref (channel_map);
+ }
+
+ if (stream->priv->channel_map != NULL) {
+ g_signal_handlers_disconnect_by_func (stream->priv->channel_map,
+ on_channel_map_volume_changed,
+ stream);
+ g_object_unref (stream->priv->channel_map);
+ }
+
+ stream->priv->channel_map = channel_map;
+
+ if (stream->priv->channel_map != NULL) {
+ g_signal_connect (stream->priv->channel_map,
+ "volume-changed",
+ G_CALLBACK (on_channel_map_volume_changed),
+ stream);
+
+ g_object_notify (G_OBJECT (stream), "channel-map");
+ }
+
+ return TRUE;
+}
+
+const char *
+gvc_mixer_stream_get_icon_name (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->icon_name;
+}
+
+const char *
+gvc_mixer_stream_get_form_factor (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->form_factor;
+}
+
+const char *
+gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->sysfs_path;
+}
+
+/**
+ * gvc_mixer_stream_get_gicon:
+ * @stream: a #GvcMixerStream
+ *
+ * Returns: (transfer full): a new #GIcon
+ */
+GIcon *
+gvc_mixer_stream_get_gicon (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ if (stream->priv->icon_name == NULL)
+ return NULL;
+ return g_themed_icon_new_with_default_fallbacks (stream->priv->icon_name);
+}
+
+gboolean
+gvc_mixer_stream_set_icon_name (GvcMixerStream *stream,
+ const char *icon_name)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->icon_name);
+ stream->priv->icon_name = g_strdup (icon_name);
+ g_object_notify (G_OBJECT (stream), "icon-name");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_form_factor (GvcMixerStream *stream,
+ const char *form_factor)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->form_factor);
+ stream->priv->form_factor = g_strdup (form_factor);
+ g_object_notify (G_OBJECT (stream), "form-factor");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream,
+ const char *sysfs_path)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ g_free (stream->priv->sysfs_path);
+ stream->priv->sysfs_path = g_strdup (sysfs_path);
+ g_object_notify (G_OBJECT (stream), "sysfs-path");
+
+ return TRUE;
+}
+
+/**
+ * gvc_mixer_stream_get_base_volume:
+ * @stream:
+ *
+ * Returns: (type guint32):
+ */
+pa_volume_t
+gvc_mixer_stream_get_base_volume (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), 0);
+
+ return stream->priv->base_volume;
+}
+
+/**
+ * gvc_mixer_stream_set_base_volume:
+ * @stream:
+ * @base_volume: (type guint32):
+ *
+ * Returns:
+ */
+gboolean
+gvc_mixer_stream_set_base_volume (GvcMixerStream *stream,
+ pa_volume_t base_volume)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ stream->priv->base_volume = base_volume;
+
+ return TRUE;
+}
+
+const GvcMixerStreamPort *
+gvc_mixer_stream_get_port (GvcMixerStream *stream)
+{
+ GList *l;
+
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ g_return_val_if_fail (stream->priv->ports != NULL, NULL);
+
+ for (l = stream->priv->ports; l != NULL; l = l->next) {
+ GvcMixerStreamPort *p = l->data;
+ if (g_strcmp0 (stream->priv->port, p->port) == 0) {
+ return p;
+ }
+ }
+
+ g_assert_not_reached ();
+
+ return NULL;
+}
+
+gboolean
+gvc_mixer_stream_set_port (GvcMixerStream *stream,
+ const char *port)
+{
+ GList *l;
+
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ g_return_val_if_fail (stream->priv->ports != NULL, FALSE);
+
+ g_free (stream->priv->port);
+ stream->priv->port = g_strdup (port);
+
+ g_free (stream->priv->human_port);
+ stream->priv->human_port = NULL;
+
+ for (l = stream->priv->ports; l != NULL; l = l->next) {
+ GvcMixerStreamPort *p = l->data;
+ if (g_str_equal (stream->priv->port, p->port)) {
+ stream->priv->human_port = g_strdup (p->human_port);
+ break;
+ }
+ }
+
+ g_object_notify (G_OBJECT (stream), "port");
+
+ return TRUE;
+}
+
+gboolean
+gvc_mixer_stream_change_port (GvcMixerStream *stream,
+ const char *port)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ return GVC_MIXER_STREAM_GET_CLASS (stream)->change_port (stream, port);
+}
+
+/**
+ * gvc_mixer_stream_get_ports:
+ *
+ * Return value: (transfer none) (element-type GvcMixerStreamPort):
+ */
+const GList *
+gvc_mixer_stream_get_ports (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), NULL);
+ return stream->priv->ports;
+}
+
+gboolean
+gvc_mixer_stream_set_state (GvcMixerStream *stream,
+ GvcMixerStreamState state)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (stream->priv->state != state) {
+ stream->priv->state = state;
+ g_object_notify (G_OBJECT (stream), "state");
+ }
+
+ return TRUE;
+}
+
+GvcMixerStreamState
+gvc_mixer_stream_get_state (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), GVC_STREAM_STATE_INVALID);
+ return stream->priv->state;
+}
+
+static int
+sort_ports (GvcMixerStreamPort *a,
+ GvcMixerStreamPort *b)
+{
+ if (a->priority == b->priority)
+ return 0;
+ if (a->priority > b->priority)
+ return 1;
+ return -1;
+}
+
+/**
+ * gvc_mixer_stream_set_ports:
+ * @ports: (transfer full) (element-type GvcMixerStreamPort):
+ */
+gboolean
+gvc_mixer_stream_set_ports (GvcMixerStream *stream,
+ GList *ports)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ g_return_val_if_fail (stream->priv->ports == NULL, FALSE);
+
+ stream->priv->ports = g_list_sort (ports, (GCompareFunc) sort_ports);
+
+ return TRUE;
+}
+
+guint
+gvc_mixer_stream_get_card_index (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), PA_INVALID_INDEX);
+ return stream->priv->card_index;
+}
+
+gboolean
+gvc_mixer_stream_set_card_index (GvcMixerStream *stream,
+ guint card_index)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ stream->priv->card_index = card_index;
+ g_object_notify (G_OBJECT (stream), "card-index");
+
+ return TRUE;
+}
+
+static void
+gvc_mixer_stream_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerStream *self = GVC_MIXER_STREAM (object);
+
+ switch (prop_id) {
+ case PROP_PA_CONTEXT:
+ self->priv->pa_context = g_value_get_pointer (value);
+ break;
+ case PROP_INDEX:
+ self->priv->index = g_value_get_ulong (value);
+ break;
+ case PROP_ID:
+ self->priv->id = g_value_get_ulong (value);
+ break;
+ case PROP_CHANNEL_MAP:
+ gvc_mixer_stream_set_channel_map (self, g_value_get_object (value));
+ break;
+ case PROP_NAME:
+ gvc_mixer_stream_set_name (self, g_value_get_string (value));
+ break;
+ case PROP_DESCRIPTION:
+ gvc_mixer_stream_set_description (self, g_value_get_string (value));
+ break;
+ case PROP_APPLICATION_ID:
+ gvc_mixer_stream_set_application_id (self, g_value_get_string (value));
+ break;
+ case PROP_ICON_NAME:
+ gvc_mixer_stream_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_FORM_FACTOR:
+ gvc_mixer_stream_set_form_factor (self, g_value_get_string (value));
+ break;
+ case PROP_SYSFS_PATH:
+ gvc_mixer_stream_set_sysfs_path (self, g_value_get_string (value));
+ break;
+ case PROP_VOLUME:
+ gvc_mixer_stream_set_volume (self, g_value_get_ulong (value));
+ break;
+ case PROP_DECIBEL:
+ gvc_mixer_stream_set_decibel (self, g_value_get_double (value));
+ break;
+ case PROP_IS_MUTED:
+ gvc_mixer_stream_set_is_muted (self, g_value_get_boolean (value));
+ break;
+ case PROP_IS_EVENT_STREAM:
+ gvc_mixer_stream_set_is_event_stream (self, g_value_get_boolean (value));
+ break;
+ case PROP_IS_VIRTUAL:
+ gvc_mixer_stream_set_is_virtual (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_DECIBEL:
+ gvc_mixer_stream_set_can_decibel (self, g_value_get_boolean (value));
+ break;
+ case PROP_PORT:
+ gvc_mixer_stream_set_port (self, g_value_get_string (value));
+ break;
+ case PROP_STATE:
+ gvc_mixer_stream_set_state (self, g_value_get_enum (value));
+ break;
+ case PROP_CARD_INDEX:
+ self->priv->card_index = g_value_get_long (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_stream_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerStream *self = GVC_MIXER_STREAM (object);
+
+ switch (prop_id) {
+ case PROP_PA_CONTEXT:
+ g_value_set_pointer (value, self->priv->pa_context);
+ break;
+ case PROP_INDEX:
+ g_value_set_ulong (value, self->priv->index);
+ break;
+ case PROP_ID:
+ g_value_set_ulong (value, self->priv->id);
+ break;
+ case PROP_CHANNEL_MAP:
+ g_value_set_object (value, self->priv->channel_map);
+ break;
+ case PROP_NAME:
+ g_value_set_string (value, self->priv->name);
+ break;
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, self->priv->description);
+ break;
+ case PROP_APPLICATION_ID:
+ g_value_set_string (value, self->priv->application_id);
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, self->priv->icon_name);
+ break;
+ case PROP_FORM_FACTOR:
+ g_value_set_string (value, self->priv->form_factor);
+ break;
+ case PROP_SYSFS_PATH:
+ g_value_set_string (value, self->priv->sysfs_path);
+ break;
+ case PROP_VOLUME:
+ g_value_set_ulong (value,
+ pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map)));
+ break;
+ case PROP_DECIBEL:
+ g_value_set_double (value,
+ pa_sw_volume_to_dB(pa_cvolume_max(gvc_channel_map_get_cvolume(self->priv->channel_map))));
+ break;
+ case PROP_IS_MUTED:
+ g_value_set_boolean (value, self->priv->is_muted);
+ break;
+ case PROP_IS_EVENT_STREAM:
+ g_value_set_boolean (value, self->priv->is_event_stream);
+ break;
+ case PROP_IS_VIRTUAL:
+ g_value_set_boolean (value, self->priv->is_virtual);
+ break;
+ case PROP_CAN_DECIBEL:
+ g_value_set_boolean (value, self->priv->can_decibel);
+ break;
+ case PROP_PORT:
+ g_value_set_string (value, self->priv->port);
+ break;
+ case PROP_STATE:
+ g_value_set_enum (value, self->priv->state);
+ break;
+ case PROP_CARD_INDEX:
+ g_value_set_long (value, self->priv->card_index);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static GObject *
+gvc_mixer_stream_constructor (GType type,
+ guint n_construct_properties,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GvcMixerStream *self;
+
+ object = G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+ self = GVC_MIXER_STREAM (object);
+
+ self->priv->id = get_next_stream_serial ();
+
+ return object;
+}
+
+static gboolean
+gvc_mixer_stream_real_change_port (GvcMixerStream *stream,
+ const char *port)
+{
+ return FALSE;
+}
+
+static gboolean
+gvc_mixer_stream_real_push_volume (GvcMixerStream *stream, gpointer *op)
+{
+ return FALSE;
+}
+
+static gboolean
+gvc_mixer_stream_real_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ return FALSE;
+}
+
+gboolean
+gvc_mixer_stream_push_volume (GvcMixerStream *stream)
+{
+ pa_operation *op;
+ gboolean ret;
+
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (stream->priv->is_event_stream != FALSE)
+ return TRUE;
+
+ g_debug ("Pushing new volume to stream '%s' (%s)",
+ stream->priv->description, stream->priv->name);
+
+ ret = GVC_MIXER_STREAM_GET_CLASS (stream)->push_volume (stream, (gpointer *) &op);
+ if (ret) {
+ if (stream->priv->change_volume_op != NULL)
+ pa_operation_unref (stream->priv->change_volume_op);
+ stream->priv->change_volume_op = op;
+ }
+ return ret;
+}
+
+gboolean
+gvc_mixer_stream_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted)
+{
+ gboolean ret;
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+ ret = GVC_MIXER_STREAM_GET_CLASS (stream)->change_is_muted (stream, is_muted);
+ return ret;
+}
+
+gboolean
+gvc_mixer_stream_is_running (GvcMixerStream *stream)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_STREAM (stream), FALSE);
+
+ if (stream->priv->change_volume_op == NULL)
+ return FALSE;
+
+ if ((pa_operation_get_state(stream->priv->change_volume_op) == PA_OPERATION_RUNNING))
+ return TRUE;
+
+ pa_operation_unref(stream->priv->change_volume_op);
+ stream->priv->change_volume_op = NULL;
+
+ return FALSE;
+}
+
+static void
+gvc_mixer_stream_class_init (GvcMixerStreamClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+ gobject_class->constructor = gvc_mixer_stream_constructor;
+ gobject_class->finalize = gvc_mixer_stream_finalize;
+ gobject_class->set_property = gvc_mixer_stream_set_property;
+ gobject_class->get_property = gvc_mixer_stream_get_property;
+
+ klass->push_volume = gvc_mixer_stream_real_push_volume;
+ klass->change_port = gvc_mixer_stream_real_change_port;
+ klass->change_is_muted = gvc_mixer_stream_real_change_is_muted;
+
+ g_object_class_install_property (gobject_class,
+ PROP_INDEX,
+ g_param_spec_ulong ("index",
+ "Index",
+ "The index for this stream",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_ID,
+ g_param_spec_ulong ("id",
+ "id",
+ "The id for this stream",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_CHANNEL_MAP,
+ g_param_spec_object ("channel-map",
+ "channel map",
+ "The channel map for this stream",
+ GVC_TYPE_CHANNEL_MAP,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_PA_CONTEXT,
+ g_param_spec_pointer ("pa-context",
+ "PulseAudio context",
+ "The PulseAudio context for this stream",
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT_ONLY));
+ g_object_class_install_property (gobject_class,
+ PROP_VOLUME,
+ g_param_spec_ulong ("volume",
+ "Volume",
+ "The volume for this stream",
+ 0, G_MAXULONG, 0,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (gobject_class,
+ PROP_DECIBEL,
+ g_param_spec_double ("decibel",
+ "Decibel",
+ "The decibel level for this stream",
+ -G_MAXDOUBLE, G_MAXDOUBLE, 0,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+
+ g_object_class_install_property (gobject_class,
+ PROP_NAME,
+ g_param_spec_string ("name",
+ "Name",
+ "Name to display for this stream",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_DESCRIPTION,
+ g_param_spec_string ("description",
+ "Description",
+ "Description to display for this stream",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_APPLICATION_ID,
+ g_param_spec_string ("application-id",
+ "Application identifier",
+ "Application identifier for this stream",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_ICON_NAME,
+ g_param_spec_string ("icon-name",
+ "Icon Name",
+ "Name of icon to display for this stream",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_FORM_FACTOR,
+ g_param_spec_string ("form-factor",
+ "Form Factor",
+ "Device form factor for this stream, as reported by PulseAudio",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_SYSFS_PATH,
+ g_param_spec_string ("sysfs-path",
+ "Sysfs path",
+ "Sysfs path for the device associated with this stream",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_IS_MUTED,
+ g_param_spec_boolean ("is-muted",
+ "is muted",
+ "Whether stream is muted",
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_CAN_DECIBEL,
+ g_param_spec_boolean ("can-decibel",
+ "can decibel",
+ "Whether stream volume can be converted to decibel units",
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_IS_EVENT_STREAM,
+ g_param_spec_boolean ("is-event-stream",
+ "is event stream",
+ "Whether stream's role is to play an event",
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_IS_VIRTUAL,
+ g_param_spec_boolean ("is-virtual",
+ "is virtual stream",
+ "Whether the stream is virtual",
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+ g_object_class_install_property (gobject_class,
+ PROP_PORT,
+ g_param_spec_string ("port",
+ "Port",
+ "The name of the current port for this stream",
+ NULL,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (gobject_class,
+ PROP_STATE,
+ g_param_spec_enum ("state",
+ "State",
+ "The current state of this stream",
+ GVC_TYPE_MIXER_STREAM_STATE,
+ GVC_STREAM_STATE_INVALID,
+ G_PARAM_READWRITE));
+ g_object_class_install_property (gobject_class,
+ PROP_CARD_INDEX,
+ g_param_spec_long ("card-index",
+ "Card index",
+ "The index of the card for this stream",
+ PA_INVALID_INDEX, G_MAXLONG, PA_INVALID_INDEX,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT));
+}
+
+static void
+gvc_mixer_stream_init (GvcMixerStream *stream)
+{
+ stream->priv = gvc_mixer_stream_get_instance_private (stream);
+}
+
+static void
+gvc_mixer_stream_finalize (GObject *object)
+{
+ GvcMixerStream *mixer_stream;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_IS_MIXER_STREAM (object));
+
+ mixer_stream = GVC_MIXER_STREAM (object);
+
+ g_return_if_fail (mixer_stream->priv != NULL);
+
+ g_object_unref (mixer_stream->priv->channel_map);
+ mixer_stream->priv->channel_map = NULL;
+
+ g_free (mixer_stream->priv->name);
+ mixer_stream->priv->name = NULL;
+
+ g_free (mixer_stream->priv->description);
+ mixer_stream->priv->description = NULL;
+
+ g_free (mixer_stream->priv->application_id);
+ mixer_stream->priv->application_id = NULL;
+
+ g_free (mixer_stream->priv->icon_name);
+ mixer_stream->priv->icon_name = NULL;
+
+ g_free (mixer_stream->priv->form_factor);
+ mixer_stream->priv->form_factor = NULL;
+
+ g_free (mixer_stream->priv->sysfs_path);
+ mixer_stream->priv->sysfs_path = NULL;
+
+ g_free (mixer_stream->priv->port);
+ mixer_stream->priv->port = NULL;
+
+ g_free (mixer_stream->priv->human_port);
+ mixer_stream->priv->human_port = NULL;
+
+ g_list_free_full (mixer_stream->priv->ports, (GDestroyNotify) free_port);
+ mixer_stream->priv->ports = NULL;
+
+ if (mixer_stream->priv->change_volume_op) {
+ pa_operation_unref(mixer_stream->priv->change_volume_op);
+ mixer_stream->priv->change_volume_op = NULL;
+ }
+
+ G_OBJECT_CLASS (gvc_mixer_stream_parent_class)->finalize (object);
+}
diff --git a/subprojects/gvc/gvc-mixer-stream.h b/subprojects/gvc/gvc-mixer-stream.h
new file mode 100644
index 0000000..586ec75
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-stream.h
@@ -0,0 +1,146 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_MIXER_STREAM_H
+#define __GVC_MIXER_STREAM_H
+
+#include <glib-object.h>
+#include "gvc-pulseaudio-fake.h"
+#include "gvc-channel-map.h"
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_STREAM (gvc_mixer_stream_get_type ())
+#define GVC_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStream))
+#define GVC_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass))
+#define GVC_IS_MIXER_STREAM(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GVC_TYPE_MIXER_STREAM))
+#define GVC_IS_MIXER_STREAM_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GVC_TYPE_MIXER_STREAM))
+#define GVC_MIXER_STREAM_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GVC_TYPE_MIXER_STREAM, GvcMixerStreamClass))
+
+typedef struct GvcMixerStreamPrivate GvcMixerStreamPrivate;
+
+typedef struct
+{
+ GObject parent;
+ GvcMixerStreamPrivate *priv;
+} GvcMixerStream;
+
+typedef struct
+{
+ GObjectClass parent_class;
+
+ /* vtable */
+ gboolean (*push_volume) (GvcMixerStream *stream,
+ gpointer *operation);
+ gboolean (*change_is_muted) (GvcMixerStream *stream,
+ gboolean is_muted);
+ gboolean (*change_port) (GvcMixerStream *stream,
+ const char *port);
+} GvcMixerStreamClass;
+
+typedef struct
+{
+ char *port;
+ char *human_port;
+ guint priority;
+ gboolean available;
+} GvcMixerStreamPort;
+
+typedef enum
+{
+ GVC_STREAM_STATE_INVALID,
+ GVC_STREAM_STATE_RUNNING,
+ GVC_STREAM_STATE_IDLE,
+ GVC_STREAM_STATE_SUSPENDED
+} GvcMixerStreamState;
+
+GType gvc_mixer_stream_port_get_type (void) G_GNUC_CONST;
+GType gvc_mixer_stream_get_type (void) G_GNUC_CONST;
+
+guint gvc_mixer_stream_get_index (GvcMixerStream *stream);
+guint gvc_mixer_stream_get_id (GvcMixerStream *stream);
+const GvcChannelMap *gvc_mixer_stream_get_channel_map(GvcMixerStream *stream);
+const GvcMixerStreamPort *gvc_mixer_stream_get_port (GvcMixerStream *stream);
+const GList * gvc_mixer_stream_get_ports (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_change_port (GvcMixerStream *stream,
+ const char *port);
+
+pa_volume_t gvc_mixer_stream_get_volume (GvcMixerStream *stream);
+gdouble gvc_mixer_stream_get_decibel (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_push_volume (GvcMixerStream *stream);
+pa_volume_t gvc_mixer_stream_get_base_volume (GvcMixerStream *stream);
+
+gboolean gvc_mixer_stream_get_is_muted (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_get_can_decibel (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_change_is_muted (GvcMixerStream *stream,
+ gboolean is_muted);
+gboolean gvc_mixer_stream_is_running (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_name (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_icon_name (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_form_factor (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_sysfs_path (GvcMixerStream *stream);
+GIcon * gvc_mixer_stream_get_gicon (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_description (GvcMixerStream *stream);
+const char * gvc_mixer_stream_get_application_id (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_is_event_stream (GvcMixerStream *stream);
+gboolean gvc_mixer_stream_is_virtual (GvcMixerStream *stream);
+guint gvc_mixer_stream_get_card_index (GvcMixerStream *stream);
+GvcMixerStreamState gvc_mixer_stream_get_state (GvcMixerStream *stream);
+
+/* private */
+gboolean gvc_mixer_stream_set_volume (GvcMixerStream *stream,
+ pa_volume_t volume);
+gboolean gvc_mixer_stream_set_decibel (GvcMixerStream *stream,
+ gdouble db);
+gboolean gvc_mixer_stream_set_is_muted (GvcMixerStream *stream,
+ gboolean is_muted);
+gboolean gvc_mixer_stream_set_can_decibel (GvcMixerStream *stream,
+ gboolean can_decibel);
+gboolean gvc_mixer_stream_set_name (GvcMixerStream *stream,
+ const char *name);
+gboolean gvc_mixer_stream_set_description (GvcMixerStream *stream,
+ const char *description);
+gboolean gvc_mixer_stream_set_icon_name (GvcMixerStream *stream,
+ const char *name);
+gboolean gvc_mixer_stream_set_form_factor (GvcMixerStream *stream,
+ const char *form_factor);
+gboolean gvc_mixer_stream_set_sysfs_path (GvcMixerStream *stream,
+ const char *sysfs_path);
+gboolean gvc_mixer_stream_set_is_event_stream (GvcMixerStream *stream,
+ gboolean is_event_stream);
+gboolean gvc_mixer_stream_set_is_virtual (GvcMixerStream *stream,
+ gboolean is_event_stream);
+gboolean gvc_mixer_stream_set_application_id (GvcMixerStream *stream,
+ const char *application_id);
+gboolean gvc_mixer_stream_set_base_volume (GvcMixerStream *stream,
+ pa_volume_t base_volume);
+gboolean gvc_mixer_stream_set_port (GvcMixerStream *stream,
+ const char *port);
+gboolean gvc_mixer_stream_set_ports (GvcMixerStream *stream,
+ GList *ports);
+gboolean gvc_mixer_stream_set_card_index (GvcMixerStream *stream,
+ guint card_index);
+gboolean gvc_mixer_stream_set_state (GvcMixerStream *stream,
+ GvcMixerStreamState state);
+
+G_END_DECLS
+
+#endif /* __GVC_MIXER_STREAM_H */
diff --git a/subprojects/gvc/gvc-mixer-ui-device.c b/subprojects/gvc/gvc-mixer-ui-device.c
new file mode 100644
index 0000000..f7dd33e
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-ui-device.c
@@ -0,0 +1,741 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * gvc-mixer-ui-device.c
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ * Copyright (C) 2012 David Henningsson, Canonical Ltd. <david.henningsson@canonical.com>
+ *
+ * gvc-mixer-ui-device.c is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gvc-mixer-ui-device.c is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <string.h>
+
+#include "gvc-mixer-ui-device.h"
+#include "gvc-mixer-card.h"
+
+struct GvcMixerUIDevicePrivate
+{
+ gchar *first_line_desc;
+ gchar *second_line_desc;
+
+ GvcMixerCard *card;
+ gchar *port_name;
+ char *icon_name;
+ guint stream_id;
+ guint id;
+ gboolean port_available;
+
+ /* These two lists contain pointers to GvcMixerCardProfile objects. Those objects are owned by GvcMixerCard. *
+ * TODO: Do we want to add a weak reference to the GvcMixerCard for this reason? */
+ GList *supported_profiles; /* all profiles supported by this port.*/
+ GList *profiles; /* profiles to be added to combobox, subset of supported_profiles. */
+ GvcMixerUIDeviceDirection type;
+ gboolean disable_profile_swapping;
+ gchar *user_preferred_profile;
+};
+
+enum
+{
+ PROP_0,
+ PROP_DESC_LINE_1,
+ PROP_DESC_LINE_2,
+ PROP_CARD,
+ PROP_PORT_NAME,
+ PROP_STREAM_ID,
+ PROP_UI_DEVICE_TYPE,
+ PROP_PORT_AVAILABLE,
+ PROP_ICON_NAME,
+};
+
+static void gvc_mixer_ui_device_finalize (GObject *object);
+
+static void gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
+ const char *icon_name);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GvcMixerUIDevice, gvc_mixer_ui_device, G_TYPE_OBJECT)
+
+static guint32
+get_next_output_serial (void)
+{
+ static guint32 output_serial = 1;
+ guint32 serial;
+
+ serial = output_serial++;
+
+ if ((gint32)output_serial < 0)
+ output_serial = 1;
+
+ return serial;
+}
+
+static void
+gvc_mixer_ui_device_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
+
+ switch (property_id) {
+ case PROP_DESC_LINE_1:
+ g_value_set_string (value, self->priv->first_line_desc);
+ break;
+ case PROP_DESC_LINE_2:
+ g_value_set_string (value, self->priv->second_line_desc);
+ break;
+ case PROP_CARD:
+ g_value_set_pointer (value, self->priv->card);
+ break;
+ case PROP_PORT_NAME:
+ g_value_set_string (value, self->priv->port_name);
+ break;
+ case PROP_STREAM_ID:
+ g_value_set_uint (value, self->priv->stream_id);
+ break;
+ case PROP_UI_DEVICE_TYPE:
+ g_value_set_uint (value, (guint)self->priv->type);
+ break;
+ case PROP_PORT_AVAILABLE:
+ g_value_set_boolean (value, self->priv->port_available);
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, gvc_mixer_ui_device_get_icon_name (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+gvc_mixer_ui_device_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ GvcMixerUIDevice *self = GVC_MIXER_UI_DEVICE (object);
+
+ switch (property_id) {
+ case PROP_DESC_LINE_1:
+ g_free (self->priv->first_line_desc);
+ self->priv->first_line_desc = g_value_dup_string (value);
+ g_debug ("gvc-mixer-output-set-property - 1st line: %s",
+ self->priv->first_line_desc);
+ break;
+ case PROP_DESC_LINE_2:
+ g_free (self->priv->second_line_desc);
+ self->priv->second_line_desc = g_value_dup_string (value);
+ g_debug ("gvc-mixer-output-set-property - 2nd line: %s",
+ self->priv->second_line_desc);
+ break;
+ case PROP_CARD:
+ self->priv->card = g_value_get_pointer (value);
+ g_debug ("gvc-mixer-output-set-property - card: %p",
+ self->priv->card);
+ break;
+ case PROP_PORT_NAME:
+ g_free (self->priv->port_name);
+ self->priv->port_name = g_value_dup_string (value);
+ g_debug ("gvc-mixer-output-set-property - card port name: %s",
+ self->priv->port_name);
+ break;
+ case PROP_STREAM_ID:
+ self->priv->stream_id = g_value_get_uint (value);
+ g_debug ("gvc-mixer-output-set-property - sink/source id: %i",
+ self->priv->stream_id);
+ break;
+ case PROP_UI_DEVICE_TYPE:
+ self->priv->type = (GvcMixerUIDeviceDirection) g_value_get_uint (value);
+ break;
+ case PROP_PORT_AVAILABLE:
+ self->priv->port_available = g_value_get_boolean (value);
+ g_debug ("gvc-mixer-output-set-property - port available %i, value passed in %i",
+ self->priv->port_available, g_value_get_boolean (value));
+ break;
+ case PROP_ICON_NAME:
+ gvc_mixer_ui_device_set_icon_name (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static GObject *
+gvc_mixer_ui_device_constructor (GType type,
+ guint n_construct_properties,
+ GObjectConstructParam *construct_params)
+{
+ GObject *object;
+ GvcMixerUIDevice *self;
+
+ object = G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->constructor (type, n_construct_properties, construct_params);
+
+ self = GVC_MIXER_UI_DEVICE (object);
+ self->priv->id = get_next_output_serial ();
+ self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+ return object;
+}
+
+static void
+gvc_mixer_ui_device_init (GvcMixerUIDevice *device)
+{
+ device->priv = gvc_mixer_ui_device_get_instance_private (device);
+}
+
+static void
+gvc_mixer_ui_device_dispose (GObject *object)
+{
+ GvcMixerUIDevice *device;
+
+ g_return_if_fail (object != NULL);
+ g_return_if_fail (GVC_MIXER_UI_DEVICE (object));
+
+ device = GVC_MIXER_UI_DEVICE (object);
+
+ g_clear_pointer (&device->priv->port_name, g_free);
+ g_clear_pointer (&device->priv->icon_name, g_free);
+ g_clear_pointer (&device->priv->first_line_desc, g_free);
+ g_clear_pointer (&device->priv->second_line_desc, g_free);
+ g_clear_pointer (&device->priv->profiles, g_list_free);
+ g_clear_pointer (&device->priv->supported_profiles, g_list_free);
+ g_clear_pointer (&device->priv->user_preferred_profile, g_free);
+
+ G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->dispose (object);
+}
+
+static void
+gvc_mixer_ui_device_finalize (GObject *object)
+{
+ G_OBJECT_CLASS (gvc_mixer_ui_device_parent_class)->finalize (object);
+}
+
+static void
+gvc_mixer_ui_device_class_init (GvcMixerUIDeviceClass *klass)
+{
+ GObjectClass* object_class = G_OBJECT_CLASS (klass);
+ GParamSpec *pspec;
+
+ object_class->constructor = gvc_mixer_ui_device_constructor;
+ object_class->dispose = gvc_mixer_ui_device_dispose;
+ object_class->finalize = gvc_mixer_ui_device_finalize;
+ object_class->set_property = gvc_mixer_ui_device_set_property;
+ object_class->get_property = gvc_mixer_ui_device_get_property;
+
+ pspec = g_param_spec_string ("description",
+ "Description construct prop",
+ "Set first line description",
+ "no-name-set",
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_DESC_LINE_1, pspec);
+
+ pspec = g_param_spec_string ("origin",
+ "origin construct prop",
+ "Set second line description name",
+ "no-name-set",
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_DESC_LINE_2, pspec);
+
+ pspec = g_param_spec_pointer ("card",
+ "Card from pulse",
+ "Set/Get card",
+ G_PARAM_READWRITE);
+
+ g_object_class_install_property (object_class, PROP_CARD, pspec);
+
+ pspec = g_param_spec_string ("port-name",
+ "port-name construct prop",
+ "Set port-name",
+ NULL,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_PORT_NAME, pspec);
+
+ pspec = g_param_spec_uint ("stream-id",
+ "stream id assigned by gvc-stream",
+ "Set/Get stream id",
+ 0,
+ G_MAXUINT,
+ GVC_MIXER_UI_DEVICE_INVALID,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_STREAM_ID, pspec);
+
+ pspec = g_param_spec_uint ("type",
+ "ui-device type",
+ "determine whether its an input and output",
+ 0, 1, 0, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_UI_DEVICE_TYPE, pspec);
+
+ pspec = g_param_spec_boolean ("port-available",
+ "available",
+ "determine whether this port is available",
+ FALSE,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_PORT_AVAILABLE, pspec);
+
+ pspec = g_param_spec_string ("icon-name",
+ "Icon Name",
+ "Name of icon to display for this card",
+ NULL,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_ICON_NAME, pspec);
+}
+
+/* Removes the part of the string that starts with skip_prefix
+ * ie. corresponding to the other direction.
+ * Normally either "input:" or "output:"
+ *
+ * Example: if given the input string "output:hdmi-stereo+input:analog-stereo" and
+ * skip_prefix "input:", the resulting string is "output:hdmi-stereo".
+ *
+ * The returned string must be freed with g_free().
+ */
+static gchar *
+get_profile_canonical_name (const gchar *profile_name, const gchar *skip_prefix)
+{
+ gchar *result = NULL;
+ gchar **s;
+ guint i;
+
+ /* optimisation for the simple case. */
+ if (strstr (profile_name, skip_prefix) == NULL)
+ return g_strdup (profile_name);
+
+ s = g_strsplit (profile_name, "+", 0);
+ for (i = 0; i < g_strv_length (s); i++) {
+ if (g_str_has_prefix (s[i], skip_prefix))
+ continue;
+ if (result == NULL)
+ result = g_strdup (s[i]);
+ else {
+ gchar *c = g_strdup_printf("%s+%s", result, s[i]);
+ g_free(result);
+ result = c;
+ }
+ }
+
+ g_strfreev(s);
+
+ if (!result)
+ return g_strdup("off");
+
+ return result;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device, const gchar *profile)
+{
+ const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
+ gchar *target_cname = get_profile_canonical_name (profile, skip_prefix);
+ GList *l;
+ gchar *result = NULL;
+
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+ g_return_val_if_fail (profile != NULL, NULL);
+
+ for (l = device->priv->profiles; l != NULL; l = l->next) {
+ gchar *canonical_name;
+ GvcMixerCardProfile* p = l->data;
+ canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+ if (strcmp (canonical_name, target_cname) == 0)
+ result = p->profile;
+ g_free (canonical_name);
+ }
+
+ g_free (target_cname);
+ g_debug ("Matching profile for '%s' is '%s'", profile, result ? result : "(null)");
+ return result;
+}
+
+
+static void
+add_canonical_names_of_profiles (GvcMixerUIDevice *device,
+ const GList *in_profiles,
+ GHashTable *added_profiles,
+ const gchar *skip_prefix,
+ gboolean only_canonical)
+{
+ const GList *l;
+
+ for (l = in_profiles; l != NULL; l = l->next) {
+ gchar *canonical_name;
+ GvcMixerCardProfile* p = l->data;
+
+ canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+ g_debug ("The canonical name for '%s' is '%s'", p->profile, canonical_name);
+
+ /* Have we already added the canonical version of this profile? */
+ if (g_hash_table_contains (added_profiles, canonical_name)) {
+ g_free (canonical_name);
+ continue;
+ }
+
+ if (only_canonical && strcmp (p->profile, canonical_name) != 0) {
+ g_free (canonical_name);
+ continue;
+ }
+
+ g_free (canonical_name);
+
+ /* https://bugzilla.gnome.org/show_bug.cgi?id=693654
+ * Don't add a profile that will make the UI device completely disappear */
+ if (p->n_sinks == 0 && p->n_sources == 0)
+ continue;
+
+ g_debug ("Adding profile to combobox: '%s' - '%s'", p->profile, p->human_profile);
+ g_hash_table_insert (added_profiles, g_strdup (p->profile), p);
+ device->priv->profiles = g_list_append (device->priv->profiles, p);
+ }
+}
+
+/**
+ * gvc_mixer_ui_device_set_profiles:
+ * @in_profiles: (element-type Gvc.MixerCardProfile): a list of GvcMixerCardProfile
+ *
+ * Assigns value to
+ * - device->priv->profiles (profiles to be added to combobox)
+ * - device->priv->supported_profiles (all profiles of this port)
+ * - device->priv->disable_profile_swapping (whether to show the combobox)
+ *
+ * This method attempts to reduce the list of profiles visible to the user by figuring out
+ * from the context of that device (whether it's an input or an output) what profiles
+ * actually provide an alternative.
+ *
+ * It does this by the following.
+ * - It ignores off profiles.
+ * - It takes the canonical name of the profile. That name is what you get when you
+ * ignore the other direction.
+ * - In the first iteration, it only adds the names of canonical profiles - i e
+ * when the other side is turned off.
+ * - Normally the first iteration covers all cases, but sometimes (e g bluetooth)
+ * it doesn't, so add other profiles whose canonical name isn't already added
+ * in a second iteration.
+ */
+void
+gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device,
+ const GList *in_profiles)
+{
+ GHashTable *added_profiles;
+ const gchar *skip_prefix = device->priv->type == UIDeviceInput ? "output:" : "input:";
+
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+
+ g_debug ("Set profiles for '%s'", gvc_mixer_ui_device_get_description(device));
+
+ if (in_profiles == NULL)
+ return;
+
+ device->priv->supported_profiles = g_list_copy ((GList*) in_profiles);
+
+ added_profiles = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+
+ /* Run two iterations: First, add profiles which are canonical themselves,
+ * Second, add profiles for which the canonical name is not added already. */
+
+ add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, TRUE);
+ add_canonical_names_of_profiles(device, in_profiles, added_profiles, skip_prefix, FALSE);
+
+ /* TODO: Consider adding the "Off" profile here */
+
+ device->priv->disable_profile_swapping = g_hash_table_size (added_profiles) <= 1;
+ g_hash_table_destroy (added_profiles);
+}
+
+/**
+ * gvc_mixer_ui_device_get_best_profile:
+ * @selected: (allow-none): The selected profile or its canonical name or %NULL for any profile
+ * @current: The currently selected profile
+ *
+ * Returns: (transfer none): a profile name, valid as long as the UI device profiles are.
+ */
+const gchar *
+gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device,
+ const gchar *selected,
+ const gchar *current)
+{
+ GList *candidates, *l;
+ const gchar *result;
+ const gchar *skip_prefix;
+ gchar *canonical_name_selected;
+
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+ g_return_val_if_fail (current != NULL, NULL);
+
+ if (device->priv->type == UIDeviceInput)
+ skip_prefix = "output:";
+ else
+ skip_prefix = "input:";
+
+ /* First make a list of profiles acceptable to switch to */
+ canonical_name_selected = NULL;
+ if (selected)
+ canonical_name_selected = get_profile_canonical_name (selected, skip_prefix);
+
+ candidates = NULL;
+ for (l = device->priv->supported_profiles; l != NULL; l = l->next) {
+ gchar *canonical_name;
+ GvcMixerCardProfile* p = l->data;
+ canonical_name = get_profile_canonical_name (p->profile, skip_prefix);
+ if (!canonical_name_selected || strcmp (canonical_name, canonical_name_selected) == 0) {
+ candidates = g_list_append (candidates, p);
+ g_debug ("Candidate for profile switching: '%s'", p->profile);
+ }
+ g_free (canonical_name);
+ }
+
+ if (!candidates) {
+ g_warning ("No suitable profile candidates for '%s'", selected ? selected : "(null)");
+ g_free (canonical_name_selected);
+ return current;
+ }
+
+ /* 1) Maybe we can skip profile switching altogether? */
+ result = NULL;
+ for (l = candidates; (result == NULL) && (l != NULL); l = l->next) {
+ GvcMixerCardProfile* p = l->data;
+ if (strcmp (current, p->profile) == 0)
+ result = p->profile;
+ }
+
+ /* 2) Try to keep the other side unchanged if possible */
+ if (result == NULL) {
+ guint prio = 0;
+ const gchar *skip_prefix_reverse = device->priv->type == UIDeviceInput ? "input:" : "output:";
+ gchar *current_reverse = get_profile_canonical_name (current, skip_prefix_reverse);
+ for (l = candidates; l != NULL; l = l->next) {
+ gchar *p_reverse;
+ GvcMixerCardProfile* p = l->data;
+ p_reverse = get_profile_canonical_name (p->profile, skip_prefix_reverse);
+ g_debug ("Comparing '%s' (from '%s') with '%s', prio %d", p_reverse, p->profile, current_reverse, p->priority);
+ if (strcmp (p_reverse, current_reverse) == 0 && (!result || p->priority > prio)) {
+ result = p->profile;
+ prio = p->priority;
+ }
+ g_free (p_reverse);
+ }
+ g_free (current_reverse);
+ }
+
+ /* 3) All right, let's just pick the profile with highest priority.
+ * TODO: We could consider asking a GUI question if this stops streams
+ * in the other direction */
+ if (result == NULL) {
+ guint prio = 0;
+ for (l = candidates; l != NULL; l = l->next) {
+ GvcMixerCardProfile* p = l->data;
+ if ((p->priority > prio) || !result) {
+ result = p->profile;
+ prio = p->priority;
+ }
+ }
+ }
+
+ g_list_free (candidates);
+ g_free (canonical_name_selected);
+ return result;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device)
+{
+ GvcMixerCardProfile *profile;
+
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ if (device->priv->card == NULL) {
+ g_warning ("Device did not have an appropriate card");
+ return NULL;
+ }
+
+ profile = gvc_mixer_card_get_profile (device->priv->card);
+ return gvc_mixer_ui_device_get_matching_profile (device, profile->profile);
+}
+
+gboolean
+gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+ return device->priv->disable_profile_swapping;
+}
+
+/**
+ * gvc_mixer_ui_device_get_profiles:
+ * @device:
+ *
+ * Returns: (transfer none) (element-type Gvc.MixerCardProfile):
+ */
+GList*
+gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->profiles;
+}
+
+/**
+ * gvc_mixer_ui_device_get_supported_profiles:
+ * @device:
+ *
+ * Returns: (transfer none) (element-type Gvc.MixerCardProfile):
+ */
+GList*
+gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->supported_profiles;
+}
+
+guint
+gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
+
+ return device->priv->id;
+}
+
+guint
+gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), 0);
+
+ return device->priv->stream_id;
+}
+
+void
+gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *self)
+{
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (self));
+
+ self->priv->stream_id = GVC_MIXER_UI_DEVICE_INVALID;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->first_line_desc;
+}
+
+const char *
+gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ if (device->priv->icon_name)
+ return device->priv->icon_name;
+
+ if (device->priv->card)
+ return gvc_mixer_card_get_icon_name (device->priv->card);
+
+ return NULL;
+}
+
+static void
+gvc_mixer_ui_device_set_icon_name (GvcMixerUIDevice *device,
+ const char *icon_name)
+{
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+
+ g_free (device->priv->icon_name);
+ device->priv->icon_name = g_strdup (icon_name);
+ g_object_notify (G_OBJECT (device), "icon-name");
+}
+
+
+/**
+ * gvc_mixer_ui_device_get_gicon:
+ * @device:
+ *
+ * Returns: (transfer full):
+ */
+GIcon *
+gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device)
+{
+ const char *icon_name;
+
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ icon_name = gvc_mixer_ui_device_get_icon_name (device);
+
+ if (icon_name != NULL)
+ return g_themed_icon_new_with_default_fallbacks (icon_name);
+ else
+ return NULL;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->second_line_desc;
+}
+
+const gchar*
+gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->user_preferred_profile;
+}
+
+const gchar *
+gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device)
+{
+ GList *last;
+ GvcMixerCardProfile *profile;
+
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ last = g_list_last (device->priv->supported_profiles);
+ profile = last->data;
+
+ return profile->profile;
+}
+
+void
+gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device,
+ const gchar *profile)
+{
+ g_return_if_fail (GVC_IS_MIXER_UI_DEVICE (device));
+ g_return_if_fail (profile != NULL);
+
+ g_free (device->priv->user_preferred_profile);
+ device->priv->user_preferred_profile = g_strdup (profile);
+}
+
+const gchar *
+gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), NULL);
+
+ return device->priv->port_name;
+}
+
+gboolean
+gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+ return (device->priv->port_name != NULL);
+}
+
+gboolean
+gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device)
+{
+ g_return_val_if_fail (GVC_IS_MIXER_UI_DEVICE (device), FALSE);
+
+ return (device->priv->type == UIDeviceOutput);
+}
diff --git a/subprojects/gvc/gvc-mixer-ui-device.h b/subprojects/gvc/gvc-mixer-ui-device.h
new file mode 100644
index 0000000..69095cb
--- /dev/null
+++ b/subprojects/gvc/gvc-mixer-ui-device.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */
+/*
+ * Copyright (C) Conor Curran 2011 <conor.curran@canonical.com>
+ *
+ * This is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * gvc-mixer-ui-device.h is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef _GVC_MIXER_UI_DEVICE_H_
+#define _GVC_MIXER_UI_DEVICE_H_
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+G_BEGIN_DECLS
+
+#define GVC_TYPE_MIXER_UI_DEVICE (gvc_mixer_ui_device_get_type ())
+#define GVC_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDevice))
+#define GVC_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass))
+#define GVC_IS_MIXER_UI_DEVICE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GVC_TYPE_MIXER_UI_DEVICE))
+#define GVC_IS_MIXER_UI_DEVICE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GVC_TYPE_MIXER_UI_DEVICE))
+#define GVC_MIXER_UI_DEVICE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GVC_TYPE_MIXER_UI_DEVICE, GvcMixerUIDeviceClass))
+
+#define GVC_MIXER_UI_DEVICE_INVALID 0
+
+typedef struct GvcMixerUIDevicePrivate GvcMixerUIDevicePrivate;
+
+typedef struct
+{
+ GObjectClass parent_class;
+} GvcMixerUIDeviceClass;
+
+typedef struct
+{
+ GObject parent_instance;
+ GvcMixerUIDevicePrivate *priv;
+} GvcMixerUIDevice;
+
+typedef enum
+{
+ UIDeviceInput,
+ UIDeviceOutput,
+} GvcMixerUIDeviceDirection;
+
+GType gvc_mixer_ui_device_get_type (void) G_GNUC_CONST;
+
+guint gvc_mixer_ui_device_get_id (GvcMixerUIDevice *device);
+guint gvc_mixer_ui_device_get_stream_id (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_description (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_icon_name (GvcMixerUIDevice *device);
+GIcon * gvc_mixer_ui_device_get_gicon (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_origin (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_port (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_best_profile (GvcMixerUIDevice *device,
+ const gchar *selected,
+ const gchar *current);
+const gchar * gvc_mixer_ui_device_get_active_profile (GvcMixerUIDevice* device);
+const gchar * gvc_mixer_ui_device_get_matching_profile (GvcMixerUIDevice *device,
+ const gchar *profile);
+const gchar * gvc_mixer_ui_device_get_user_preferred_profile (GvcMixerUIDevice *device);
+const gchar * gvc_mixer_ui_device_get_top_priority_profile (GvcMixerUIDevice *device);
+GList * gvc_mixer_ui_device_get_profiles (GvcMixerUIDevice *device);
+GList * gvc_mixer_ui_device_get_supported_profiles (GvcMixerUIDevice *device);
+gboolean gvc_mixer_ui_device_should_profiles_be_hidden (GvcMixerUIDevice *device);
+void gvc_mixer_ui_device_set_profiles (GvcMixerUIDevice *device,
+ const GList *in_profiles);
+void gvc_mixer_ui_device_set_user_preferred_profile (GvcMixerUIDevice *device,
+ const gchar *profile);
+void gvc_mixer_ui_device_invalidate_stream (GvcMixerUIDevice *device);
+gboolean gvc_mixer_ui_device_has_ports (GvcMixerUIDevice *device);
+gboolean gvc_mixer_ui_device_is_output (GvcMixerUIDevice *device);
+
+G_END_DECLS
+
+#endif /* _GVC_MIXER_UI_DEVICE_H_ */
diff --git a/subprojects/gvc/gvc-pulseaudio-fake.h b/subprojects/gvc/gvc-pulseaudio-fake.h
new file mode 100644
index 0000000..92a41b6
--- /dev/null
+++ b/subprojects/gvc/gvc-pulseaudio-fake.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2008 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ *
+ */
+
+#ifndef __GVC_PULSEAUDIO_FAKE_H
+#define __GVC_PULSEAUDIO_FAKE_H
+
+#ifndef PA_API_VERSION
+#define pa_channel_position_t int
+#define pa_volume_t guint32
+#define pa_context gpointer
+#endif /* PA_API_VERSION */
+
+#endif /* __GVC_PULSEAUDIO_FAKE_H */
diff --git a/subprojects/gvc/libgnome-volume-control.doap b/subprojects/gvc/libgnome-volume-control.doap
new file mode 100644
index 0000000..2fcc8e1
--- /dev/null
+++ b/subprojects/gvc/libgnome-volume-control.doap
@@ -0,0 +1,32 @@
+<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
+ xmlns:gnome="http://api.gnome.org/doap-extensions#"
+ xmlns="http://usefulinc.com/ns/doap#">
+
+ <name xml:lang="en">libgnome-volume-control</name>
+ <shortdesc xml:lang="en">GObject layer for PulseAudio</shortdesc>
+ <description>
+ This library contains code to access PulseAudio using a GObject
+ based library, shared between gnome-control-center, gnome-settings-daemon
+ and gnome-shell. It is not API stable, and it is meant to be used
+ as a submodule.
+ </description>
+
+ <!-- <category rdf:resource="http://api.gnome.org/doap-extensions#desktop" /> -->
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Giovanni Campagna</foaf:name>
+ <foaf:mbox rdf:resource="mailto:scampa.giovanni@gmail.com" />
+ <gnome:userid>gcampagna</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Bastien Nocera</foaf:name>
+ <foaf:mbox rdf:resource="mailto:hadess@hadess.net" />
+ <gnome:userid>hadess</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+</Project>
diff --git a/subprojects/gvc/meson.build b/subprojects/gvc/meson.build
new file mode 100644
index 0000000..a1a2af5
--- /dev/null
+++ b/subprojects/gvc/meson.build
@@ -0,0 +1,137 @@
+project('gvc', 'c',
+ meson_version: '>= 0.42.0',
+ default_options: ['static=true']
+)
+
+assert(meson.is_subproject(), 'This project is only intended to be used as a subproject!')
+
+gnome = import('gnome')
+
+pkglibdir = get_option('pkglibdir')
+pkgdatadir = get_option('pkgdatadir')
+
+cdata = configuration_data()
+cdata.set_quoted('GETTEXT_PACKAGE', get_option('package_name'))
+cdata.set_quoted('PACKAGE_VERSION', get_option('package_version'))
+
+libgvc_gir_headers = [
+ 'gvc-channel-map.h',
+ 'gvc-mixer-card.h',
+ 'gvc-mixer-control.h',
+ 'gvc-mixer-event-role.h',
+ 'gvc-mixer-sink.h',
+ 'gvc-mixer-sink-input.h',
+ 'gvc-mixer-source.h',
+ 'gvc-mixer-source-output.h',
+ 'gvc-mixer-stream.h',
+ 'gvc-mixer-ui-device.h'
+]
+
+libgvc_enums = gnome.mkenums_simple('gvc-enum-types',
+ sources: libgvc_gir_headers
+)
+
+libgvc_gir_sources = [
+ 'gvc-channel-map.c',
+ 'gvc-mixer-card.c',
+ 'gvc-mixer-control.c',
+ 'gvc-mixer-event-role.c',
+ 'gvc-mixer-sink.c',
+ 'gvc-mixer-sink-input.c',
+ 'gvc-mixer-source.c',
+ 'gvc-mixer-source-output.c',
+ 'gvc-mixer-stream.c',
+ 'gvc-mixer-ui-device.c'
+]
+
+libgvc_no_gir_sources = [
+ 'gvc-mixer-card-private.h',
+ 'gvc-mixer-stream-private.h',
+ 'gvc-channel-map-private.h',
+ 'gvc-mixer-control-private.h',
+ 'gvc-pulseaudio-fake.h'
+]
+
+libgvc_deps = [
+ dependency('gio-2.0'),
+ dependency('gobject-2.0'),
+ dependency('libpulse', version: '>= 12.99.3'),
+ dependency('libpulse-mainloop-glib')
+]
+
+enable_alsa = get_option('alsa')
+if enable_alsa
+ libgvc_deps += dependency('alsa')
+endif
+cdata.set('HAVE_ALSA', enable_alsa)
+
+enable_static = get_option('static')
+enable_introspection = get_option('introspection')
+
+assert(not enable_static or not enable_introspection, 'Currently meson requires a shared library for building girs.')
+assert(enable_static or pkglibdir != '', 'Installing shared library, but pkglibdir is unset!')
+
+c_args = ['-DG_LOG_DOMAIN="Gvc"']
+
+if enable_introspection
+ c_args += '-DWITH_INTROSPECTION'
+endif
+
+if enable_static
+ libgvc_static = static_library('gvc',
+ sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums,
+ dependencies: libgvc_deps,
+ c_args: c_args
+ )
+
+ libgvc = libgvc_static
+else
+ if pkglibdir == ''
+ error('Installing shared library, but pkglibdir is unset!')
+ endif
+
+ libgvc_shared = shared_library('gvc',
+ sources: libgvc_gir_sources + libgvc_no_gir_sources + libgvc_enums,
+ dependencies: libgvc_deps,
+ c_args: c_args,
+ install_dir: pkglibdir,
+ install: true
+ )
+
+ libgvc = libgvc_shared
+endif
+
+if enable_introspection
+ assert(pkgdatadir != '', 'Installing introspection, but pkgdatadir is unset!')
+
+ libgvc_gir = gnome.generate_gir(libgvc,
+ sources: libgvc_gir_sources + libgvc_gir_headers + libgvc_enums,
+ nsversion: '1.0',
+ namespace: 'Gvc',
+ includes: ['Gio-2.0', 'GObject-2.0'],
+ extra_args: ['-DWITH_INTROSPECTION', '--quiet'],
+ install_dir_gir: pkgdatadir,
+ install_dir_typelib: pkglibdir,
+ install: true
+ )
+endif
+
+if enable_alsa
+ executable('test-audio-device-selection',
+ sources: 'test-audio-device-selection.c',
+ link_with: libgvc,
+ dependencies: libgvc_deps,
+ c_args: c_args
+ )
+endif
+
+libgvc_dep = declare_dependency(
+ link_with: libgvc,
+ include_directories: include_directories('.'),
+ dependencies: libgvc_deps
+)
+
+configure_file(
+ output: 'config.h',
+ configuration: cdata
+)
diff --git a/subprojects/gvc/meson_options.txt b/subprojects/gvc/meson_options.txt
new file mode 100644
index 0000000..38513e3
--- /dev/null
+++ b/subprojects/gvc/meson_options.txt
@@ -0,0 +1,41 @@
+option('package_name',
+ type: 'string',
+ value: '',
+ description: 'The value for the GETTEXT_PACKAGE define.'
+)
+
+option('package_version',
+ type: 'string',
+ value: '',
+ description: 'The value for the PACKAGE_VERSION define.'
+)
+
+option('pkglibdir',
+ type: 'string',
+ value: '',
+ description: 'The private directory the shared library/typelib will be installed into.'
+)
+
+option('pkgdatadir',
+ type: 'string',
+ value: '',
+ description: 'The private directory the gir file will be installed into.'
+)
+
+option('alsa',
+ type: 'boolean',
+ value: true,
+ description: 'Build ALSA support.'
+)
+
+option('static',
+ type: 'boolean',
+ value: false,
+ description: 'Build as a static library.'
+)
+
+option('introspection',
+ type: 'boolean',
+ value: false,
+ description: 'Build gobject-introspection support'
+)
diff --git a/subprojects/gvc/test-audio-device-selection.c b/subprojects/gvc/test-audio-device-selection.c
new file mode 100644
index 0000000..8195f9d
--- /dev/null
+++ b/subprojects/gvc/test-audio-device-selection.c
@@ -0,0 +1,84 @@
+
+#include <stdio.h>
+#include <locale.h>
+#include <pulse/pulseaudio.h>
+#include "gvc-mixer-control.h"
+
+#define MAX_ATTEMPTS 3
+
+typedef struct {
+ GvcHeadsetPortChoice choice;
+ const char *name;
+} AudioSelectionChoice;
+
+static AudioSelectionChoice audio_selection_choices[] = {
+ { GVC_HEADSET_PORT_CHOICE_HEADPHONES, "headphones" },
+ { GVC_HEADSET_PORT_CHOICE_HEADSET, "headset" },
+ { GVC_HEADSET_PORT_CHOICE_MIC, "microphone" },
+};
+
+static void
+audio_selection_needed (GvcMixerControl *volume,
+ guint id,
+ gboolean show_dialog,
+ GvcHeadsetPortChoice choices,
+ gpointer user_data)
+{
+ const char *args[G_N_ELEMENTS (audio_selection_choices) + 1];
+ guint i, n;
+ int response = -1;
+
+ if (!show_dialog) {
+ g_print ("--- Audio selection not needed anymore for id %d\n", id);
+ return;
+ }
+
+ n = 0;
+ for (i = 0; i < G_N_ELEMENTS (audio_selection_choices); ++i) {
+ if (choices & audio_selection_choices[i].choice)
+ args[n++] = audio_selection_choices[i].name;
+ }
+ args[n] = NULL;
+
+ g_print ("+++ Audio selection needed for id %d\n", id);
+ g_print (" Choices are:\n");
+ for (i = 0; args[i] != NULL; i++)
+ g_print (" %d. %s\n", i + 1, args[i]);
+
+ for (i = 0; response < 0 && i < MAX_ATTEMPTS; i++) {
+ int res;
+
+ g_print ("What is your choice?\n");
+ if (scanf ("%d", &res) == 1 &&
+ res > 0 &&
+ res < (int) g_strv_length ((char **) args)) {
+ response = res;
+ break;
+ }
+ }
+
+ gvc_mixer_control_set_headset_port (volume,
+ id,
+ audio_selection_choices[response - 1].choice);
+}
+
+int main (int argc, char **argv)
+{
+ GMainLoop *loop;
+ GvcMixerControl *volume;
+
+ setlocale (LC_ALL, "");
+
+ loop = g_main_loop_new (NULL, FALSE);
+
+ volume = gvc_mixer_control_new ("GNOME Volume Control test");
+ g_signal_connect (volume,
+ "audio-device-selection-needed",
+ G_CALLBACK (audio_selection_needed),
+ NULL);
+ gvc_mixer_control_open (volume);
+
+ g_main_loop_run (loop);
+
+ return 0;
+}
diff --git a/subprojects/libhandy/.dir-locals.el b/subprojects/libhandy/.dir-locals.el
new file mode 100644
index 0000000..0d066d3
--- /dev/null
+++ b/subprojects/libhandy/.dir-locals.el
@@ -0,0 +1,8 @@
+(
+ (c-mode . (
+ (c-file-style . "linux")
+ (indent-tabs-mode . nil)
+ (c-basic-offset . 2)
+ ))
+)
+
diff --git a/subprojects/libhandy/.editorconfig b/subprojects/libhandy/.editorconfig
new file mode 100644
index 0000000..70827f6
--- /dev/null
+++ b/subprojects/libhandy/.editorconfig
@@ -0,0 +1,38 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+
+[meson.build]
+indent_size = 2
+tab_size = 2
+indent_style = space
+
+[*.{c,h,h.in}]
+indent_size = 2
+tab_size = 2
+indent_style = space
+max_line_length = 80
+
+[*.css]
+indent_size = 2
+tab_size = 2
+indent_style = space
+
+[*.xml]
+indent_size = 2
+tab_size = 2
+indent_style = space
+
+[*.json]
+indent_size = 2
+tab_size = 2
+indent_style = space
+
+[NEWS]
+indent_size = 2
+tab_size = 2
+indent_style = space
+max_line_length = 72
diff --git a/subprojects/libhandy/AUTHORS b/subprojects/libhandy/AUTHORS
new file mode 100644
index 0000000..e061b0d
--- /dev/null
+++ b/subprojects/libhandy/AUTHORS
@@ -0,0 +1,9 @@
+Adrien Plazas <adrien.plazas@puri.sm>
+Bob Ham <bob.ham@puri.sm>
+Dorota Czaplejewicz <dorota.czaplejewicz@puri.sm>
+Guido Günther <agx@sigxcpu.org>
+Heather Ellsworth <heather.ellsworth@puri.sm>
+Julian Richen <julian@richen.io>
+Julian Sparber <julian@sparber.net>
+Sebastien Lafargue <slafargue@gnome.org>
+Zander Brown <zbrown@gnome.org>
diff --git a/subprojects/libhandy/COPYING b/subprojects/libhandy/COPYING
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/subprojects/libhandy/COPYING
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/subprojects/libhandy/HACKING.md b/subprojects/libhandy/HACKING.md
new file mode 100644
index 0000000..c0cd1be
--- /dev/null
+++ b/subprojects/libhandy/HACKING.md
@@ -0,0 +1,335 @@
+Building
+========
+For build instructions see the README.md
+
+Pull requests
+=============
+Before filing a pull request run the tests:
+
+```sh
+ninja -C _build test
+```
+
+Use descriptive commit messages, see
+
+ https://wiki.gnome.org/Git/CommitMessages
+
+and check
+
+ https://wiki.openstack.org/wiki/GitCommitMessages
+
+for good examples.
+
+Coding Style
+============
+We mostly use kernel style but
+
+* Use spaces, never tabs
+* Use 2 spaces for indentation
+
+GTK style function argument indentation
+----------------------------------------
+Use GTK style function argument indentation. It's harder for renames but it's
+what GNOME upstream projects do.
+
+*Good*:
+
+```c
+static gboolean
+key_press_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ gpointer data)
+```
+
+*Bad*:
+
+```c
+static gboolean
+key_press_event_cb (GtkWidget *widget, GdkEvent *event, gpointer data)
+```
+
+
+Braces
+------
+Everything besides functions and structs have the opening curly brace on the same line.
+
+*Good*:
+
+```c
+if (i < 0) {
+ ...
+}
+```
+
+*Bad*:
+
+```c
+if (i < 0)
+{
+ ...
+}
+```
+
+Single line `if` or `else` statements don't need braces but if either `if` or
+`else` have braces both get them:
+
+*Good*:
+
+```c
+if (i < 0)
+ i++;
+else
+ i--;
+```
+
+```c
+if (i < 0) {
+ i++;
+ j++;
+} else {
+ i--;
+}
+```
+
+```c
+if (i < 0) {
+ i++;
+} else {
+ i--;
+ j--;
+}
+```
+
+*Bad*:
+
+```c
+if (i < 0) {
+ i++;
+} else {
+ i--;
+}
+```
+
+```c
+if (i < 0) {
+ i++;
+ j++;
+} else
+ i--;
+```
+
+```c
+if (i < 0)
+ i++;
+else {
+ i--;
+ j--;
+}
+```
+
+Function calls have a space between function name and invocation:
+
+*Good*:
+
+```c
+visible_child_name = gtk_stack_get_visible_child_name (GTK_STACK (self->stack));
+```
+
+*Bad*:
+
+```c
+visible_child_name = gtk_stack_get_visible_child_name(GTK_STACK(self->stack));
+```
+
+
+Header Inclusion Guards
+-----------------------
+Guard header inclusion with `#pragma once` rather than the traditional
+`#ifndef`-`#define`-`#endif` trio.
+
+Internal headers (for consistency, whether they need to be installed or not)
+should contain the following guard to prevent users from directly including
+them:
+```c
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+```
+
+Only after these should you include headers.
+
+
+Signals
+-------
+Prefix signal enum names with *SIGNAL_*.
+
+*Good*:
+
+```c
+enum {
+ SIGNAL_SUBMITTED = 0,
+ SIGNAL_DELETED,
+ SIGNAL_SYMBOL_CLICKED,
+ SIGNAL_LAST_SIGNAL,
+};
+```
+
+Also note that the last element ends with a comma to reduce diff noise when
+adding further signals.
+
+
+Properties
+----------
+Prefix property enum names with *PROP_*.
+
+*Good*:
+
+```c
+enum {
+ PROP_0 = 0,
+ PROP_NUMBER,
+ PROP_SHOW_ACTION_BUTTONS,
+ PROP_COLUMN_SPACING,
+ PROP_ROW_SPACING,
+ PROP_RELIEF,
+ PROP_LAST_PROP,
+};
+```
+
+Also note that the last element ends with a comma to reduce diff noise when
+adding further properties.
+
+Comment style
+-------------
+In comments use full sentences with proper capitalization and punctuation.
+
+*Good*:
+
+```c
+/* Make sure we don't overflow. */
+```
+
+*Bad:*
+
+```c
+/* overflow check */
+```
+
+
+Callbacks
+---------
+Signal callbacks have a *_cb* suffix.
+
+*Good*:
+
+```c
+g_signal_connect(self, "clicked", G_CALLBACK (button_clicked_cb), NULL);
+```
+
+*Bad*:
+
+```c
+g_signal_connect(self, "clicked", G_CALLBACK (handle_button_clicked), NULL);
+```
+
+
+Static functions
+----------------
+Static functions don't need the class prefix. E.g. with a type foo_bar:
+
+*Good*:
+
+```c
+static void
+grab_focus_cb (HdyDialer *dialer,
+ gpointer unused)
+```
+
+*Bad*:
+
+```c
+static void
+hdy_dialer_grab_focus_cb (HdyDialer *dialer,
+ gpointer unused)
+```
+
+Note however that virtual methods like
+*<class_name>_{init,constructed,finalize,dispose}* do use the class prefix.
+These functions are usually never called directly but only assigned once in
+*<class_name>_constructed* so the longer name is kind of acceptable. This also
+helps to distinguish virtual methods from regular private methods.
+
+Self argument
+-------------
+The first argument is usually the object itself so call it *self*. E.g. for a
+non public function:
+
+*Good*:
+
+```c
+static gboolean
+expire_cb (FooButton *self)
+{
+ g_return_val_if_fail (BAR_IS_FOO_BUTTON (self), FALSE);
+ ...
+ return FALSE;
+}
+```
+
+And for a public function:
+
+*Good*:
+
+```c
+gint
+foo_button_get_state (FooButton *self)
+{
+ FooButtonPrivate *priv = bar_foo_get_instance_private(self);
+
+ g_return_val_if_fail (BAR_IS_FOO_BUTTON (self), -1);
+ return priv->state;
+}
+```
+
+User interface files
+--------------------
+User interface files should end in *.ui*. If there are multiple ui
+files put them in a ui/ subdirectory below the sources
+(e.g. *src/ui/main-window.ui*).
+
+### Properties
+Use minus signs instead of underscores in property names:
+
+*Good*:
+
+```xml
+<property name="margin-start">12</property>
+```
+
+*Bad*:
+
+```xml
+<property name="margin_start">12</property>
+```
+
+Automatic cleanup
+-----------------
+It's recommended to use `g_auto()`, `g_autoptr()`, `g_autofree()` for
+automatic resource cleanup when possible.
+
+*Good*:
+
+```c
+g_autoptr(GdkPixbuf) sigterm = pixbuf = gtk_icon_info_load_icon (info, NULL);
+```
+
+*Bad*:
+
+```c
+GdkPixbuf *pixbuf = gtk_icon_info_load_icon (info, NULL);
+...
+g_object_unref (pixbuf);
+```
+
+Using the above is fine since libhandy doesn't target any older glib versions
+or non GCC/Clang compilers at the moment.
diff --git a/subprojects/libhandy/NEWS b/subprojects/libhandy/NEWS
new file mode 100644
index 0000000..c335a1e
--- /dev/null
+++ b/subprojects/libhandy/NEWS
@@ -0,0 +1,228 @@
+==============
+Version 0.90.0
+==============
+
+- Stop requiring the HANDY_USE_UNSTABLE_API guard.
+- Stop transforming close buttons into back buttons for dialogs on the
+ desktop.
+- Give some nice default and minimum sizes to HdyPreferencesWindow.
+- HdyCarousel:
+ - Add HdyCarouselIndicatorDots and HdyCarouselIndicatorLines.
+ - Drop the indicator-style, indicator-spacing, and center-content
+ properties.
+- Revamp the colors of HdyAvatar and augment its colors number to 14.
+- Set the default column and row spacing of HdyKeypad to 6 pixels.
+- Don't present an arrow and a popover in HdyComboRow when its model has
+ less than 2 items.
+- Support CSS sizing properties for HdySqueezer and HdyViewSwitcher.
+- Drop the icon-size properties of HdyViewSwitcher, HdyViewSwitcherTitle
+ and HdyViewSwitcherBar.
+- Give some horizontal margins to the view switcher of
+ HdyViewSwitcherTitle via CSS.
+- Add all files back to tarballs except the debian directory.
+
+==============
+Version 0.85.0
+==============
+
+- HdyAvatar:
+ - Add the icon-name property to allow setting a different default icon
+ than avatar-default-symbolic.
+ - Ship avatar-default-symbolic as a resource to ensure it's there.
+ This shouldn't affect icon themes already offering it.
+ - Check the icon exists before using it to avoid a crash.
+- HdyDeck and HdyLeaflet:
+ - Allow dragging the higher sibling only from the border where it
+ sits, rather than from the anywhere on the currently visible child,
+ reinforcing spatialization.
+ - Add the get_child_by_name() methods.
+- HdyLeaflet:
+ - Rename the 'allow-visible' child property into 'navigatable'.
+- HdySwipeable:
+ - Add a navigation direction param and a gesture type param to
+ get_swipe_area().
+- HdyPreferencesWindow:
+ - Allow presenting a subpage over the window via the new
+ present_subpage() and close_subpage() methods.
+ - Add the 'can-swipe-back' property to allow closing a subpage via a
+ back swipe gesture.
+ - Exclude untitled rows as well as invisible pages, groups, and rows
+ from the search results.
+- HdyKeypad:
+ - Replace the 'show-symbols' property by 'letters-visible'.
+ - Replace the 'only-digits' property by 'symbols-visible', whose
+ boolean meaning is inverted.
+ - Replace the 'left-action' property by 'start-action'.
+ - Replace the 'right-action' property by 'end-action'.
+ - Make the 'entry' property declare it uses the GtkEntry type rather
+ than GtkWidget.
+- HdySqeezer:
+ - Add the 'xalign' and 'yalign' properties to help aligning the
+ children during transitions.
+- HdyViewSwitcherTitle:
+ - Set the 'policy' property default to 'auto' as in HdyViewSwitcher.
+- HdyTitleBar:
+ - Fix an accidental mix of natural and minimum sizes in measure().
+- Harden the ABI by making symbols implicitly private and explicitly
+ public.
+- Translation updates:
+ - Romanian
+ - Ukrainian
+
+==============
+Version 0.84.0
+==============
+
+- HdyHeaderGroup:
+ - Replace GtkHeaderBar as the child type by HdyHeaderGroupChild, and
+ adjust the matching accessors. HdyHeaderGroupChild can hold a
+ GtkHeaderBar, a HdyHeaderBar, and a HdyHeaderGroup, allowing to nest
+ header groups.
+ - Replace the 'focus' property by the 'decorate-all' property.
+ - Add the update-decoration-layouts signal, used when nesting header
+ groups.
+- HdyHeaderBar:
+ - Slight size request fix.
+- Use the window node's radius instead of the decoration node's one to
+ mask HdyWindow and HdyApplicationWindow.
+- Make HdyAvatar, HdyHeaderGroup, HdySqueezer, HdyViewSwitcher,
+ HdyViewSwitcherBar, HdyViewSwitcherTitle, and HdyWindowHandle final.
+- Replace usage of (allow-none) by (nullable) or (optional).
+- Translation updates:
+ - Ukrainian
+
+==============
+Version 0.83.0
+==============
+
+- Initialization:
+ - Add hdy_init() back, with a different prototype. See its
+ documentation to know how to use it.
+ - Drop initializing the library via a constructor as it was causing
+ many issues.
+ - Drop the now useless Python override.
+ - Directly update themes on changes.
+- Add HdySwipeTracker.
+- HdySwipeable:
+ - Drop the begin_swipe(), update_swipe(), end_swipe() and get_range()
+ virtual methods
+ - Add the get_swipe_tracker() and get_swipe_area() virtual methods.
+ - Add the …_switch_child(), …_emit_child_switched(),
+ …_get_swipe_tracker(), …_get_distance(), …_get_snap_points(),
+ …_get_progress(), …_get_cancel_progress(), and …_get_swipe_area()
+ functions.
+ - Make implementing get_snap_points() mandatory by dropping its
+ default implementation, compensating the disparition of get_range().
+ - Rename the switch-child signal to child-switched to avoid a naming
+ collision with the switch_child() method.
+- HdyDeck and HdyLeaflet:
+ - Add an outline to shadows to make them slightly more contrasted yet
+ subtle.
+ - Make shadows work over OpenGL content.
+ - Cache shadows for child transitions.
+ - Stop drawing invisible shadows when no transition is running.
+ - Rewrite the transition code to give a window to all children, fixing
+ numerous issues.
+- HdyExpanderRow:
+ - Add hdy_expander_row_add_prefix().
+- Add libhandy.syms back to tarballs as it was mistakenly removed.
+- Translation updates:
+ - Polish
+ - Spanish
+
+==============
+Version 0.82.0
+==============
+
+- Unblacklist run.in for inclusion into the tarball, fixing the build.
+- HdyClamp:
+ - Rename HdyColumn as HdyClamp.
+ - Make it implement GtkOrientable.
+ - Rename its properties from maximum-width to maximum-size, and
+ linear-growth-width to tightening-threshold.
+ - Rename the style classes it sets on itself from .narrow, .medium and
+ .wide style to .small, .medium and .large.
+ - Set the default value of maximum-size to 600, and of
+ tightening-threshold to 400.
+ - Notify when changing size properties, and guard non-changes.
+- HdyCarousel, HdyDeck and HdyLeaflet:
+ - Move the swipe tracker event handling to the bubble phase, giving
+ the priority to the inner widget.
+- HdyDeck:
+ - Avoid some useless allocation computations.
+- HdyLeaflet:
+ - Don't count children of size 0 to compute the fold state.
+ - Don't fold when there is only 1 visible size.
+- HdySwipeable:
+ - Add the missing direct header inclusion guard.
+- HdyWindow and HdyApplicationWindow:
+ - Implement destroy() to correctly destroy the internal widgets.
+- Drop hdy_list_box_separator_header().
+- Don't install Glade files outside prefix.
+- Update the project description.
+- Translation updates:
+ - Spanish
+ - Ukrainian
+
+==============
+Version 0.81.0
+==============
+
+- Migrated the project to https://gitlab.gnome.org/GNOME/libhandy/.
+ - Archived the project at https://source.puri.sm/Librem5/libhandy/.
+ - Updated URLs and email addresses across the project.
+ - Switch the CI to use GNOME's.
+ - Build and publish the nightly reference manual via GitLab Pages at
+ https://gnome.pages.gitlab.gnome.org/libhandy/.
+- Make the reference manual and the Glade catalog parallel-installable
+ with libhandy 0.0.
+- Add a Python override to ensure the library is initialized on import.
+- Themes:
+ - Add the HighContrast theme.
+ - Split the shared theme into the fallback theme whose style can be
+ overridden by other themes, and the shared theme whose style
+ overrides the themes.
+ - Move window corners from the shared theme to Adwaita, so elementary
+ can do what they want.
+ - Make the leaflet and deck drop shadows darker for dark variants and
+ HighContrast, to ensure it's visible.
+ - Drop the .h4 fallback to avoid conflicts with .heading. Themes are
+ now expected to implement .heading, or optionally .h4.
+ - Fix list.preferences nested list bottom corner rounding issues.
+- CSS support:
+ - Account for the CSS box-shadow property when clipping in HdyAvatar,
+ HdyHeaderBar, and HdyTitleBar.
+ - Support the CSS min-width and min-height properties in HdyHeaderBar,
+ and HdyTitleBar.
+- HdyDeck and HdyLeaflet:
+ - Add *_get_adjacent_child() to get the child a swipe or a call to
+ *_navigate() would present.
+ - Don't skip the swipes with a 0 (child for leaflet) transition
+ duration.
+ - Correctly cancel transitions when the duration is 0 or the
+ transition is NONE.
+- HdyCarousel:
+ - Allow mouse drag by default.
+ - Add the 'reveal-duration' property.
+ - Animate child addition and deletion.
+- HdyExpanderRow:
+ - Move switch to the left of the arrow.
+ - Add hdy_expander_row_add_action_widget() and the 'action' child type
+ to allow adding widgets before the arrow and the switch.
+- HdyHeaderBar:
+ - Add the .titlebar style class by default.
+- HdyKeypad:
+ - Make it inherit from GtkBin instead of GtkGrid, contain one instead.
+ - Add spacing properties to set the grid's spacing.
+ - Don't make it visible by default.
+- HdyPreferencesGroup:
+ - Use the .heading style class for the title in addition to .h4.
+- HdyPreferencesWindow:
+ - Make clicking search rows work again.
+- HdySwipeable:
+ - Add the get_distance(), get_range(), get_snap_points(),
+ get_progress(), and get_cancel_progress() virtual methods.
+- HdyViewSwitcherTitle:
+ - Remove the useless has-subtitle property.
+ - Prevent gtk_widget_show_all() from modifying its internal state.
+ - Make dispose() reentrant.
diff --git a/subprojects/libhandy/README.md b/subprojects/libhandy/README.md
new file mode 100644
index 0000000..0eb6cbe
--- /dev/null
+++ b/subprojects/libhandy/README.md
@@ -0,0 +1,71 @@
+# Handy
+[![Pipeline status](https://gitlab.gnome.org/GNOME/libhandy/badges/master/build.svg)](https://gitlab.gnome.org/GNOME/libhandy/commits/master)
+[![Code coverage](https://gitlab.gnome.org/GNOME/libhandy/badges/master/coverage.svg)](https://gitlab.gnome.org/GNOME/libhandy/commits/master)
+
+The aim of the Handy library is to help with developing UI for mobile devices
+using GTK/GNOME.
+
+## License
+
+libhandy is licensed under the LGPL-2.1+.
+
+## Build dependencies
+
+To build libhandy you need to first install the build-deps defined by [the debian/control file](https://gitlab.gnome.org/GNOME/libhandy/blob/master/debian/control#L6).
+
+If you are running a Debian based distribution, you can easily install all those the dependencies making use of the following command
+
+```sh
+sudo apt-get build-dep .
+```
+
+## Building
+
+We use the Meson (and thereby Ninja) build system for libhandy. The quickest
+way to get going is to do the following:
+
+```sh
+meson . _build
+ninja -C _build
+ninja -C _build install
+```
+
+For build options see [meson_options.txt](./meson_options.txt). E.g. to enable documentation:
+
+```sh
+meson . _build -Dgtk_doc=true
+ninja -C _build libhandy-doc
+```
+
+## Usage
+
+There's a C example:
+
+```sh
+_build/examples/example
+```
+
+and one in Python. When running from the built source tree it
+needs several environment variables so use \_build/run to set them:
+
+```sh
+_build/run examples/example.py
+```
+
+### Glade
+
+To be able to use Handy's widgets in the glade interface designer without
+installing the library use:
+
+```sh
+_build/run glade
+```
+
+## Documentation
+
+The documentation can be found online
+[here](https://gnome.pages.gitlab.gnome.org/libhandy).
+
+## Getting in touch
+
+Matrix room: [#libhandy:talk.puri.sm](https://gnome.element.io/#/room/#libhandy:talk.puri.sm)
diff --git a/subprojects/libhandy/data/leak-suppress.txt b/subprojects/libhandy/data/leak-suppress.txt
new file mode 100644
index 0000000..f44eb82
--- /dev/null
+++ b/subprojects/libhandy/data/leak-suppress.txt
@@ -0,0 +1,5 @@
+# Use via environment variable LSAN_OPTIONS=suppressions=data/leak-suppress.txt
+# Ignore fontconfig reported leaks. It's caches cause false positives.
+leak:libfontconfig.so.1
+# https://gitlab.gnome.org/GNOME/gtk/merge_requests/823
+leak:gtk_header_bar_set_decoration_layout
diff --git a/subprojects/libhandy/data/packaging/rpm/libhandy.spec b/subprojects/libhandy/data/packaging/rpm/libhandy.spec
new file mode 100644
index 0000000..739b751
--- /dev/null
+++ b/subprojects/libhandy/data/packaging/rpm/libhandy.spec
@@ -0,0 +1,60 @@
+%global _vpath_srcdir %{name}
+
+Name: libhandy
+Version: 0.90.0
+Release: 1%{?dist}
+Summary: A library full of GTK widgets for mobile phones
+
+License: LGPLv2+
+Url: https://gitlab.gnome.org/GNOME/libhandy
+Source0: https://gitlab.gnome.org/GNOME/libhandy/archive/master.tar.gz
+
+BuildRequires: gcc
+BuildRequires: gobject-introspection
+BuildRequires: gtk-doc
+BuildRequires: meson >= 0.40.1
+BuildRequires: pkgconfig(gio-2.0)
+BuildRequires: pkgconfig(gladeui-2.0)
+BuildRequires: pkgconfig(glib-2.0)
+BuildRequires: pkgconfig(gmodule-2.0)
+BuildRequires: pkgconfig(gtk+-3.0)
+BuildRequires: pkgconf-pkg-config
+BuildRequires: vala
+
+%description
+%{summary}.
+
+%package devel
+Summary: Development libraries, headers, and documentation for %{name}
+Requires: libhandy = %{version}-%{release}
+
+%description devel
+%{summary}.
+
+%prep
+%setup -c -q
+
+%build
+%meson -Dexamples=false -Dgtk_doc=true
+%meson_build
+
+%install
+%meson_install
+
+%files
+%{_libdir}/libhandy*.so.*
+%{_libdir}/girepository-1.0/Handy*.typelib
+
+%files devel
+%{_includedir}/libhandy*
+%{_libdir}/libhandy*.so
+%{_libdir}/pkgconfig/libhandy*.pc
+%{_datadir}/gir-1.0/Handy*.gir
+%{_datadir}/glade/catalogs/libhandy.xml
+%{_datadir}/vala/vapi/libhandy*.deps
+%{_datadir}/vala/vapi/libhandy*.vapi
+%{_datadir}/gtk-doc
+
+%changelog
+* Fri May 18 2018 Julian Richen <julian@richen.io> - 0.0.0-1
+- Update to 0.0.0-1
diff --git a/subprojects/libhandy/debian/README.source b/subprojects/libhandy/debian/README.source
new file mode 100644
index 0000000..90e1f00
--- /dev/null
+++ b/subprojects/libhandy/debian/README.source
@@ -0,0 +1,29 @@
+This package is maintained with git-buildpackage(1). It follows DEP-14
+for branch naming (e.g. using debian/sid for the current version
+in Debian unstable).
+
+It uses pristine-tar(1) to store enough information in git to generate
+bit identical tarballs when building the package without having
+downloaded an upstream tarball first.
+
+When working with patches it is recommended to use "gbp pq import" to
+import the patches, modify the source and then use "gbp pq export
+--commit" to commit the modifications.
+
+The changelog is generated using "gbp dch" so if you submit any
+changes don't bother to add changelog entries but rather provide
+a nice git commit message that can then end up in the changelog.
+
+It is recommended to build the package with pbuilder using:
+
+ gbp buildpackage --git-pbuilder
+
+For information on how to set up a pbuilder environment see the
+git-pbuilder(1) manpage. In short:
+
+ DIST=sid git-pbuilder create
+ gbp clone <project-url>
+ cd <project>
+ gbp buildpackage --git-pbuilder
+
+ -- Guido Günther <agx@sigxcpu.org>, Wed, 2 Dec 2015 18:51:15 +0100
diff --git a/subprojects/libhandy/debian/changelog b/subprojects/libhandy/debian/changelog
new file mode 100644
index 0000000..60c5911
--- /dev/null
+++ b/subprojects/libhandy/debian/changelog
@@ -0,0 +1,1433 @@
+libhandy-1 (0.90.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 07 Aug 2020 13:23:50 +0200
+
+libhandy-1 (0.85.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Thu, 30 Jul 2020 08:51:53 +0200
+
+libhandy-1 (0.84.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 17 Jul 2020 13:13:25 +0200
+
+libhandy-1 (0.83.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Thu, 02 Jul 2020 09:37:18 +0200
+
+libhandy-1 (0.82.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 19 Jun 2020 09:30:31 +0200
+
+libhandy-1 (0.81.0) amber-phone; urgency=medium
+
+ * New upstream release
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Fri, 05 Jun 2020 13:08:25 +0200
+
+libhandy-1 (0.80.0) amber-phone; urgency=medium
+
+ [ Guido Günther ]
+ * Bump API version to 1.0.0.
+
+ [ Adrien Plazas ]
+ * Release libhandy 0.80.0.
+ * meson: Don't allow to build as a static library.
+ * meson: Fix disabling the Glade catalog.
+ * meson: Separate public and private enums.
+ * doc: Add the Handy 0.0 to Handy 1 migration guide.
+ * Document CSS nodes and style classes.
+ * Rename HdyPaginator into HdyCarousel.
+ Aslo rename HdyPaginatorBox into HdyCarouselBox.
+ * Remove the deprecated widgets.
+ Remove HdyArrows, HdyDialer, HdyDialerButton, and HdyDialerCycleButton.
+ * Drop HdyDialog.
+ It was deemed not the right way to implement the features we want from it.
+ * Drop HdyFold.
+ It has been replaced by a boolean.
+ * Drop the hdy prefix from CSS node names.
+ This matches what Adwaita does and will help better blend with it.
+ * Drop UTF-8 string functions.
+ They were unused and are not core to what libhandy wants to offer.
+ * Add HdyViewSwitcherTitle.
+ A view switcher designed to be used as a window title.
+ * action-row: Widget overhaul.
+ Drop the ability to add widgets below, and remove the 'action' buildable
+ child type and hdy_action_row_add_action(), instead widgets are appended at
+ the end of the row.
+ Add the 'activated' signal, and automatically make the row activatable when
+ is is given an activatable widget.
+ Define the sizes with CSS, style the title and subtitle with CSS, and rename
+ the .row-header style class to .header.
+ * column: Add the .narrow, .medium and .wide style classes.
+ Allow to easily update style based on the column's clamping state.
+ * column: Ensure the column is at least as wide as its child.
+ * combo-row: Make it activatable when it has a bound model.
+ * combo-row: Popover style overhaul and add a checkmark to the selected row.
+ * expander-row: Widget overhaul.
+ Completely redesign the widget. Also prevent gtk_widget_show_all().
+ * flatpak: Update the example command name and drop useless build options.
+ * glade: Support Glade 3.36.
+ Make the catalog support both 3.24 and 3.36, and add a Glade+libhandy
+ flatpak manifest which uses glade 3.36.
+ * icons: Add hdy-expander-arrow-symbolic.
+ * leaflet: Add the .folded and .unfolded style classes.
+ Allow to easily update style based on the leaflet's fold state.
+ * leaflet: Default to the 'over' transition type.
+ This better match the expected behavior or a leaflet.
+ * leaflet: Avoid divisions by 0, don't implement the unused GtkBuildableIface
+ and drop the deprecated transition props and types.
+ * main: Automatically init libhandy.
+ Add a library constructor to init on startup and remove hdy_init().
+ Initialize the global styles and icons when the main loop starts.
+ * preferences-window: Add the 'search-enabled' property.
+ * preferences-window: Hide filtered-out rows.
+ * preferences-window: Use HdyViewSwitcherTitle.
+ * style: Style overhaul.
+ Use SASS to implement the styles, offer both a shared base theme used as
+ a fallback and an Adwaita-specific theme, and offer a dark variant.
+ Ship pre-compiled CSS and dynamically load it depending on the theme.
+ * style: Add the button.list-button CSS style class.
+ * style: Add the list.preferences CSS style class.
+ * swipe-tracker: Fix a leak in …_confirm_swipe().
+ * swipe-tracker: Use g_clear_pointer() where possible.
+ * view-switcher-bar: Document relation with HdyViewSwitcherTitle.
+ * view-switcher-bar: Don't reveal for less than two pages.
+ * debian: Use HdyKeypad in the Python GI test.
+ * examples: Add a dark theme toggle button.
+ * examples: Add a HdyDeck example.
+ * examples: Bind the switch-rows to their switches.
+ * examples: Don't set action rows as unactivatable.
+ * examples: Drop setting the header group focus.
+ * examples: Give its own header to the search bar demo.
+ * examples: Make the radio buttons non-focusable.
+ * examples: Make the resource path match the app ID.
+ * examples: Put the right header bar in a GtkStack.
+ * examples: Use HdyViewSwitcherTitle.
+ * examples: Use the button.list-button CSS style class.
+ * examples: Vertically align the radio buttons.
+
+ [ Alexander Mikhaylenko ]
+ * Add HdyDeck.
+ A swipeable container widget allowing to stack widgets.
+ * Add HdyNavigationDirection.
+ * Add HdyStackableBox.
+ A private object easing the implementation of HdyDeck and HdyLeaflet.
+ * Add HdyWindow.
+ A free-form window widget with rounded corners.
+ * Add HdyApplicationWindow.
+ A free-form application window widget with rounded corners.
+ * Add HdyWindowHandle.
+ A bin widget allowing to control a window like with a titlebar.
+ * Add HdyNothing.
+ A private empty widget, easing the implementation of free-form window types.
+ * Add HdyWindowMixin.
+ A private object easing the implementation of free-form window types.
+ * Add HdyWindowHandleController.
+ A private object allowing a widget to control a window like with a titlebar.
+ * action-row: Don't allow adding null prefixes, and implement remove().
+ * column: Queue resize after changing maximum width.
+ * expander-row: Fix forall(), and implement remove().
+ * glade: Properly support all public widgets and objects.
+ The glade catalog has been overhauled, fixing support of widgets and objects
+ already included in the catalog, and adding the newly added ones.
+ * gtk-window: Add hdy_gtk_window_get_state().
+ * header-bar: Add a window handle controller.
+ Also make it register its own window to get the needed events.
+ * header-bar: Remove some unused variables.
+ * Introduce hdy-cairo-private.h.
+ This helps automatically cleanup up Cairo objects.
+ * leaflet: Add a function for moving back/forward programmatically.
+ * leaflet: Allow hdy_leaflet_navigate() regardless of swipe properties.
+ * leaflet: Document visible child functions.
+ * leaflet: Fix hdy_leaflet_get_can_swipe_forward() docs and some typos.
+ * leaflet: Make HdyLeaflet a wrapper around HdyStackableBox.
+ * leaflet: Remove 'todo' vfunc.
+ * main: Don't use G_SOURCE_FUNC() macro.
+ * preferences-group: Implement remove().
+ * preferences-group: Subclass GtkBin instead of GtkBox.
+ * preferences-page: Implement remove() and forall().
+ * preferences-page: Subclass GtkBin instead of GtkScrolledWindow.
+ * preferences-window: Add .titlebar to the headerbar.
+ * preferences-window: Erase search terms after hiding search bar.
+ * preferences-window: Implement remove() and forall().
+ * preferences-window: Name signal callbacks _cb.
+ * preferences-window: Port to HdyWindow.
+ * preferences-window: Use crossfade transition.
+ * preferences-window: Use GDK_EVENT_* constants.
+ * preferences-window: Use gtk_search_entry_handle_event().
+ * shadow-helper: Don't set style context parent.
+ * stackable-box: Avoid use-after-free in remove().
+ * stackable-box: Check is visible child exists in folded mode
+ * stackable-box: Disconnect the signal handler after removing a child
+ * stackable-box: Fix a typo in a comment
+ * stackable-box: Only count allow-visible=true children for index
+ * stackable-box: Only hide last visible child when folded
+ * stackable-box: Remove an extra line in a doc comment
+ * stackable-box: Skip mode transitions for deck
+ * stackable-box: Unset last_visible_child after removing or hiding
+ * swipeable: Use HdyNavigationDirection for begin_swipe() direction
+ * swipe-tracker: Fix crash in confirm_swipe().
+ * swipe-tracker: Reject drags in window's draggable areas.
+ * view-switcher: Extend bin instead of box.
+ * view-switcher-title: Unset stack before destroying.
+ * glade: List all the missing public widgets.
+ List HdyApplicationWindow, HdyAvatar, HdyDeck, HdyKeypad,
+ HdyViewSwitcherTitle, HdyWindow, and HdyWindowHandle.
+ * example: Add a HdyWindow demo.
+ * example: Add .titlebar to all headerbars.
+ * example: Don't leave an empty autoptr declaration.
+ * example: Fix a typo on the HdyWindow page.
+ * example: Fix leaflet/deck typos.
+ * example: Make the "Go to the next page" row activatable.
+ * example: Port main window to HdyApplicationWindow.
+ * example: Port view switcher window to HdyWindow.
+ * example: Stop using "fold" HdyLeaflet property.
+ * example: Use a menu model for primary menu.
+ * example: Use HdyDeck in complex dialog demo.
+ * example: Use hdy_leaflet_navigate() for back button and clicking rows.
+
+ [ Ujjwal Kumar ]
+ * preferences-window: Cancel search from keyboard.
+ * Return GtkWidget* with _new().
+ * Coding style fixes.
+ * doc: Tell about widget constructor changes.
+ * example: Resize demo window.
+ * example: Replace deprecated Dialer with Keypad in example.py.
+ * example: Add some spacing between widgets.
+
+ [ Julian Sparber ]
+ * Add HdyAvatar.
+ A widget to visually represent a contact.
+
+ [ Felix Pojtinger ]
+ * doc: Add macOS build instructions.
+
+ [ louib ]
+ * Fix acknowledge typo in build doc.
+ * Adding new example apps using libhandy.
+
+ [ Alberto Fanjul ]
+ * glade: Adapt to Glade 3.36 API changes.
+
+ -- Adrien Plazas <adrien.plazas@puri.sm> Tue, 19 May 2020 09:45:02 +0200
+
+libhandy (0.0.13) amber-phone; urgency=medium
+
+ [ Alexander Mikhaylenko ]
+ * paginator-box: Stop using gtk_widget_set_child_visible()
+ This function is meant for widgets that don't need to be mapped along with
+ parent widget, not for scrolled out widgets. Additionally, using it causes
+ strange side effects with GtkOverlay window z-ordering. Stop using it and
+ instead track visiblity manually. Also, clarify the code a bit.
+ * leaflet: Correctly handle 0 duration for swipe snap-back
+ * swipe-tracker: Don't animate when the distance is 0.
+ Usually it makes sense to restrict the minimum animation duration. However,
+ if the progress already matches the end progress, it just causes a delay,
+ so skip it completely.
+
+ [ Julian Sparber ]
+ * Keypad: Do not show allow typing + when only_digits is true.
+ The keypad shouldn't allow typing or show + when only_digits
+ is set to true. Therefore this adds the correct behavior.
+
+ [ Guido Günther ]
+ * Release libhandy 0.0.13
+
+ -- Guido Günther <agx@sigxcpu.org> Fri, 27 Dec 2019 12:22:18 +0100
+
+libhandy (0.0.12) experimental; urgency=medium
+
+ [ Zander Brown ]
+ * build: Don't install glade catalogue when used as submodule
+
+ [ Alexander Mikhaylenko ]
+ * swipe-tracker: Grab widget during the gesture
+ * swipe-tracker: Animate when canceled.
+ There are some cases where not animating the canceled gesture looks
+ awkward. For example, when tapping a paginator while it animates.
+ * swipe-tracker: Don't add GDK_ALL_EVENTS_MASK.
+ That was a debugging leftover.
+ * header-group: Fix a leftover GtkSizeGroup mention
+ * paginator: Delegate hdy_paginator_scroll_to() to scroll_to_full()
+ This will help to avoid duplicating code in later commits.
+ * paginator-box: Add hdy_paginator_box_get_nth_child()
+ * doc: Add 0.0.12 index
+ * Add HdySwipeable.
+ A common interface that swipeable widgets should implement and that
+ HdySwipeGroup and HdySwipeTracker will use.
+ * paginator: Implement HdySwipeable
+ * swipe-tracker: Port to HdySwipeable.
+ Use a HdySwipeable instead of GtkWidget. Remove 'begin', 'update' and 'end'
+ signals and instead call HdySwipeable methods.
+ * Add HdySwipeGroup.
+ An object that allows to synchronize swipe animations of multiple widgets.
+ This can be used to sync widgets between headerbar and window content area.
+ * tests: Add HdySwipeGroup test
+ * glade: Support HdySwipeGroup.
+ Do the same thing as for HdyHeaderGroup.
+ * leaflet: Fix the folding sliding children padding.
+ Sets the children padding of the folding sliding animation depending on
+ the surface they'll be drawn on.
+ This doesn't change a thing for the sliding animation, but this will
+ avoid the children to be moved when snapshotting them, which is needed
+ for the over and under animations — which will be added in the next
+ commit — to work correctly.
+ * leaflet: Only clip visible area during transitions.
+ Adjust width and height of the clip rectangle to avoid drawing areas
+ outside of the widget.
+ * Introduce HdyShadowHelper.
+ This will be used in the following commits to add shadows to HeyLeaflet
+ transitions.
+ * leaflet: Dim bottom children during transitions.
+ Draw a dimming layer and a drop shadow over bottom child during 'over' and
+ 'under' mode and child transitions.
+ The dimming, shadow and border styles are defined in CSS. The current style
+ is based on the similar animation in WebKit.
+ * swipe-tracker: Reduce base distance for vertical swipes.
+ Use 300px instead of 400px, otherwise it can be hard to use on small
+ touchpads.
+ * paginator-box: Adjust index when removing pages.
+ Prevent jumping when removing pages to the left of the current one.
+ * paginator: Support discrete scrolling.
+ Support scrolling on devices like mice. Switch a page when a scroll event
+ arrives and add a delay to prevent too fast scrolling.
+ Use animation duration as a delay, but don't let it go below 250ms, mainly
+ to ensure it still works with animations disabled.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/155
+ * swipe-tracker: Stop handling trackpoint.
+ Handle it like discrete scrolling instead.
+ * leaflet: Mention replacements in deprecations.
+ Have more useful warnings.
+ * leaflet: Mark child-transition and mode-transition as deprecated.
+ Properties are deprecated too, not just accessors.
+ * leaflet: Ignore deprecations for transition type acccessor declarations.
+ Since enums are deprecated now, these declarations trigger warnings in
+ modules that use libhandy. Since these functions are already deprecated
+ anyway, silence these warnings.
+ * deprecation-macros: Stop referencing nonexistent macros.
+ G_DEPRECATED_* and G_DEPRECATED_*_FOR aren't a thing.
+ * swipe-tracker: Make dragging touch-only.
+ Since HdyPaginator has mouse scrolling now, there's no need to have
+ dragging available on non-touch devices, so drop it.
+ * paginator-box: Wrap children into child info structs.
+ This will allow to carry additional data for them later.
+ * paginator-box: Put children into their own GdkWindows.
+ This allows to stop doing size allocation on each frame, and will allow
+ to implement drawing cache in the next commit.
+ * paginator-box: Implement drawing cache.
+ Keep a Cairo surface for each child. Paint children onto their surfaces,
+ then compose the final image. Instead of painting the whole children,
+ track invalidations and paint only changed parts. This means most paginator
+ redraws don't involve any child redraws. This should significantly speed
+ up scrolling when children are expensive to draw.
+ * paginator-box: Add animation-stopped signal.
+ This will be used in the next commit to add page-changed signal to
+ HdyPaginator.
+ * paginator: Add page-changed signal.
+ Allows to know when the current page has changed, this can be used to
+ implement "infinite scrolling" by connecting to this signal and amending
+ the pages.
+ * leaflet: Allocate last visible child during child transitions.
+ Fixes one cause of https://source.puri.sm/Librem5/libhandy/issues/85
+ * keypad: Immediately assign g_autoptrs to NULL.
+ Avoid compile-time warnings.
+ * paginator-box: Create window with correct dimensions.
+ It doesn't matter because it gets overridden later, but still fix it.
+ * example: Remove leftover adjustments.
+ See aa7a4eca68d8c75ff6347202c90515c5aea30c64
+ * paginator-box: Fix hdy_paginator_box_get_nth_child()
+ Return the actual widget, not child info struct.
+ A leftover from 710bcaacb97bdfac6061726a77665235279d4fe6
+ * leaflet: Use provided duration for child transitions.
+ Actually use the value from the function argument.
+ * swipeable: Provide swipe direction when preparing.
+ This will allow to restrict the swipe to only one direction for leaflet.
+ * swipeable: Distinguish direct and indirect swipes.
+ Add "direct" parameter to hdy_paginator_begin_swipe() and the corresponding
+ vfunc, providing a way to tell apart swipes started via HdySwipeGroup sync.
+ This will be used to have leaflet in headerbar that's not swipeable, but
+ can still animate along with leaflet in content area.
+ * swipe-tracker: Skip swipes in wrong direction.
+ Prevent swiping if the direction doesn't match tracker orientation. This
+ allows to have GtkScrolledArea inside or around swipeable widgets without
+ swipes taking over scrolling.
+ * leaflet: Add allow-visible child property.
+ This will be used to prevent swiping to widgets such as separators.
+ * leaflet: Add properties for controlling swipes.
+ This will allow to selectively enable back and/or forward swipes for
+ HdyLeaflet. By default swipes are disabled.
+ * leaflet: Implement back/forward swipe gesture.
+ Implement HdySwipeable and use HdySwipeTracker to detect back/forward
+ swipes.
+ Use can-swipe-back and can-swipe-forward properties for controlling swipes,
+ and use allow-visible child property to exclude certain widgets, such as
+ separators, from the gesture.
+ Multiple leaflets can be synced via HdySwipeGroup.
+ * example: Enable back swipe in the leaflet.
+ Set can-swipe-back=true on the content leaflet, allow-visible=false for
+ separators and use HdySwipeGroup for syncing leaflets rather than binding
+ visible child name.
+ * leaflet: Queue relayout after child transition ends.
+ Prevents close button from occasionally disappearing after swipes.
+ * swipe-tracker: Add 'allow-mouse-drag' property
+ * paginator: Add 'allow-mouse-drag' property.
+ Usually we don't want this, because there's scrolling. However, phosh
+ still needs this for lockscreen, hence optionally allow it.
+ * paginator-box: Register window before setting parent.
+ Prevents newly created widgets from reusing parent's window.
+ Fixes a regression from e6a477492de6cc4d5107147b9724980ffd7343ea
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/165
+ * swipeable: Fix signal names for docs
+ * swipe-group: Don't escape tag names for docs
+ * leaflet: Deprecate old transition type properties.
+ They did already have the deprecated flag, but weren't shown as deprecated
+ in docs.
+ * Update @See_also for swipeable widgets.
+ Mention HdyLeaflet in HdySwipeable, HdySwipeGroup and HdySwipeTracker.
+
+ [ louib ]
+ * Fix typo in README.
+ * Remove casts requiring increased alignment.
+ Some casts were increasing the required alignment in
+ callbacks, raising warnings when compiled on arm with gcc.
+
+ [ Guido Günther ]
+ * Add deprecation macros.
+ The macros are libhandy internal (should not be used in application
+ code) and are as such marked with a '_'. This also makes gtk-doc
+ happy since it treats it as a public symbol otherwise.
+ * Deprecate all hdy-dialer{-cycle}-button api.
+ It's considered HdyDialer internal API
+ * HdyDialer: Remove excessive '*'
+ * build: Install new header file.
+ Fixes: ac94e649aac540c1ecaa9df98364049e182605cc
+ * Release libhandy 0.0.12
+
+ [ Adrien Plazas ]
+ * leaflet: Clip children when drawing unfolded.
+ This will clip children to ensure they don't get drawn on or under the
+ visible child, which will allow to create mode transition animations
+ where other children appear to be drawn under the visible child.
+ * leaflet: Clip the end surface when drawing folded.
+ This will clip the end surface to ensure it doesn't get drawn on or
+ under the visible child, which will allow to create mode transition
+ animations where other children appear to be drawn under the visible
+ child.
+ * leaflet: Add the over and under mode transition animations.
+ This allows the mode transition animation to match the semantic of the
+ over and under child transitions.
+ * leaflet: Unify the transition types.
+ Add the HdyLeafletTransitionType enumeration and the transition-type
+ property to define both the mode and child transitions, as having them
+ different makes no sense and could lead to spatialization issues.
+ This new type doesn't offer a crossfade transition on purpose as it was
+ deemed inappropriate for the leaflet, for which the position of the
+ children is inherently important.
+ This also deprecates the two previous properties and their respective
+ types.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/92.
+ * leaflet: Remove the over and under mode transitions.
+ There is no point in adding enum values and deprecating them in the same
+ version, so let's just remove them. The animations are still available
+ via the newly added HdyLeafletTransitionType type and the
+ transition-type property, so this also encourages migrating to the new
+ API.
+ * examples: Add a Leaflet page.
+ This adds a page to demo the leaflet transitions, drops usage of the
+ deprecated leaflet transition types and properties, and defaults to the
+ 'over' transition to demo it and its shadow effect.
+ * Deprecate HdyArrows.
+ As far as we know, nothing uses it anymore and it's not part of our
+ latest designs.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/126.
+ * examples: Drop the Arrows page.
+ HdyArrows is now deprecated, so we don't want to promote it.
+ * leaflet: Drop some old TODOs.
+ We just don't need them anymore.
+ * leaflet: Add Alexander Mikhaylenko's copyright.
+ His work on this class is far from negligeable, let's reflect that in
+ the copyright.
+ * view-switcher-button: Fix the action bar hover style.
+ This makes the buttons out of a header bar slightly lighter when hovered
+ and the window is focused. Previously they were the same color as the
+ unfocused buttons and the action bar, making them look less good and
+ harder to use.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/147.
+
+ [ Julian Sparber ]
+ * Keypad: Add a general keypad.
+ This is based on HdyDialer, but with more flexible API.
+ The new Keypad allows to set a custom Widget to the left/right
+ lower corner, replacing the original widget.
+ The Keypad extents directly GtkGrid which exposes all grid properties.
+ It also allows to replace/change every button in the Keypad, just like
+ in GtkGrid.
+ It also adds a GtkEntry which can be used as the focus widget,
+ it has the key-press-event already connected and it grabs focus once
+ it's mapped. The Entry isn't part of the keypad, it's just a
+ convenienced way to create a Entry, you would expect to use with a
+ keypad.
+ * Tests: add keypad tests
+ * Docs: add docs and demo for keypad
+ * Dialer: deprecate hdydialer
+ * HdyDialer: Remove it from the demo.
+ Remove the dialer from the demo since it's deprecated.
+ * HdyDialer: Deprecate objects related to dialer.
+ HdyDialerButton, HdyDialerCycleButton and HdyDialer objects where not
+ deprecated, only there methods were.
+
+ [ Oliver Galvin ]
+ * README: minor punctuation fixes, and update Fractal URL to GNOME namespace
+ * docs: Consistently use full sentences in short descriptions.
+ * docs: Add sections about building and bundling to the 'Compiling with
+ libhandy' page, and generally tidy the page. * docs: Update copyright
+ year range.
+ * meson: fix configure-time warning - Use the 'pie' kwarg instead of passing
+ '-fpie' manually. Also bump Meson to 0.49.0, when the pie kwarg was added.
+ * meson: Tidy build files. Use / operator (added in Meson 0.49.0) instead of
+ join_paths. Use package_api_name variable to avoid repetition.
+ * style: Remove odd tabs as per 'Coding Style' in HACKING.md, and fix typo.
+
+ [ Ting-Wei Lan ]
+ * keypad: Fix compilation error for clang.
+ Function hdy_keypad_button_get_digit is declared to return 'char' in
+ src/hdy-keypad-button-private.h but defined to return 'const char' in
+ src/hdy-keypad-button.c. This is not allowed by clang. Since it is
+ unusual to mark a return value itself as const, just drop const here.
+
+ -- Guido Günther <agx@sigxcpu.org> Thu, 12 Dec 2019 09:49:04 +0100
+
+libhandy (0.0.11) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * dialer: Work around GtkGrid row homogeneity.
+ Puts the buttons into a vertical size group rather than making the rows
+ homogeneous. This prevents a bug from GtkGrid to make the buttons too
+ tall when the action buttons are hidden.
+ * dialog: Don't warn if the titlebar isn't a GtkHeaderBar.
+ Using another widget is perfectly valid, so we should just return
+ instead.
+ * dialog: Refactor the transient-for workaround.
+ This will make introducing new properties simpler.
+ * dialog: Add the narrow property.
+ * header-bar: Show a back button in a narrow HdyDialog.
+ If a header bar is in a narrow HdyDialog, it will display a back button
+ at its start in place of its usual window decorations.
+ * examples: Add a complex HdyDialog example.
+ This shows how to use HdyHeaderBar and HdyDialog to create a more
+ complex adaptive dialog.
+ * header-bar: Show a back button on small non-sovereign windows.
+ This will show the back button not only in small HdyDialog but in all
+ small windows that are not sovereign.
+ * meson: Set the log domain.
+ This makes the log messages from libhandy look like `Handy-Debug: …`
+ rather than `** Debug: …`, making them easier to distinguish.
+ * README.md: Update the documentation URL.
+ It's on the developer.puri.sm now.
+ * Add animation helpers.
+ Add various animation helpers to avoid coyping them around.
+ * squeezer: Support animation disablement.
+ This will animate the child transitions only if animations are enabled.
+ * preferences-group: Use the h4 style class.
+ Use the h4 style class instead of hardcoding the bold style for the
+ preferences group title, and implement a fallback making the font bold.
+ This is needed by elementary to use their own style.
+ * animation: Make some functions public.
+ This makes hdy_get_enable_animations() and hdy_ease_out_cubic() public.
+ * view-switcher-button: Don't make transparent on hover.
+ This doesn't make the background transparent when hovering and apply the
+ same style as non-hovered buttons on hovered buttons in a headerbar.
+
+ [ Gabriele Musco ]
+ * Added Unifydmin to Python 3 examples
+ * Add HydraPaper to Python 3 examples
+
+ [ Ting-Wei Lan ]
+ * Don't require GNU sed
+
+ [ Jeremy Bicha ]
+ * Debian packaging improvements
+
+ [ Guido Günther ]
+ * debian: Ship example program and files
+ * Release libhandy 0.0.11
+
+ [ Alexander Mikhaylenko ]
+ * search-bar: Hide start and end boxes instead of close button.
+ * glade: Update catalog dtd.
+ * Add new HdySwipeTracker widget.
+ This will be used to implement swipes in new widgets.
+ * Add new HdyPaginator widget.
+ Display set of pages with swipe based navigation.
+
+ [ David Boddie ]
+ * Deploy documentation for the master branch
+
+ [ Michael Catanzaro ]
+ * glade: Don't install glade files outside build prefix.
+
+ -- Guido Günther <agx@sigxcpu.org> Tue, 27 Aug 2019 12:50:01 +0200
+
+libhandy (0.0.10) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * .editorconfig: Add CSS
+ * arrows: Refresh HdyArrowsDirection docs.
+ This moves the HdyArrowsDirection documentation to the C file and
+ removes the final period from the values definitions, like for all other
+ enums documentations.
+ * docs: Add section for new symbols in 0.0.10
+ * view-switcher: Fix stack children callbacks.
+ This fixes the callbacks when a child is added or removed from the view
+ switcher's stack.
+ * view-switcher-button: Make an active button's label bold.
+ This makes the view switcher easier to read.
+ It uses multiple labels with or without the specific style rather than a
+ single label with the style toggled on and off to ensure the size
+ requests don't change depending on whether the button is active or not.
+ * leaflet: Synchronize paired notifications.
+ This ensures users can't react to a visible child change notification or
+ a fold change notification before we finish emitting all related
+ notifications.
+ * Add HdySqueezer.
+ This can be used to automatically hide a widget like a HdyViewSwitcher
+ in a header bar when there is not enough space for it and show a title
+ label instead.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/100
+ * examples: Use a HdySqueezer.
+ Use a HdySqueezer in the view switcher window to show either the view
+ switcher in the header bar, or a window title and a view switcher bar
+ depending on the window's width.
+ * view-switcher-button: Allow to elipsize in narrow mode.
+ This will be used to let HdyViewSwitcherBar reach even narrower widths.
+ * view-switcher: Allow to elipsize in narrow mode.
+ This will be used to let HdyViewSwitcherBar reach even narrower widths.
+ * view-switcher-bar: Ellipsize in narrow mode.
+ This lets HdyViewSwitcherBar reach even narrower widths.
+ * view-switcher-button: Use buttons borders in size.
+ When computing the size of the button, take the button's border into
+ account.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/108
+ * view-switcher-bar: Sort properties by alphabetical order.
+ This fixes a code style error and will avoid to propagate it as the file
+ gets edited.
+ * view-switcher-bar: Add margins.
+ Add margings around the view switcher to better match the mockups.
+ * view-switcher: Define a minimum natural width.
+ This prevents the buttons from looking terribly narrow in a wide bar by
+ making them request a minimum good looking natural size.
+ * Add HdyPreferencesRow.
+ This will be used as the base row for the preferences window, offering
+ data needed to search a preference entry.
+ * action-row: Extend HdyPreferencesRow.
+ This allows to use HdyActionRow and its derivatives as preferences rows.
+ * Add HdyPreferencesGroup.
+ This will be used to group preferences rows as a coherent group in a
+ preferences page.
+ * Add HdyPreferencesPage.
+ This will be used to group preferences as pages in a preferences window.
+ * Add HdyPreferencesWindow.
+ This allows to easily create searchable preferences windows.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/101
+ * examples: Add a HdyPreferencesWindow example
+ * Add private GtkWindow functions.
+ Add the private GtkWindow functions _gtk_window_toggle_maximized()
+ and gtk_window_get_icon_for_size() which will be used in the next commit
+ by HdyHeaderBar.
+ * Add HdyHeaderBar.
+ Fork GtkHeaderBar to help fixing shortcomings caused by adaptive designs
+ or coming from GtkHeaderBar itself as features are not accepted into GTK
+ 3 anymore.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/102
+ * examples: Use HdyHeaderBar in the View Switcher page.
+ This correctly centers the view switcher and demoes HdyHeaderBar.
+ * view-switcher: Recommend to use a HdyHeaderBar.
+ This will help users of HdyViewSwitcher to know how to make it look
+ good in a header bar.
+ * examples: Drop un unused signal connection.
+ This avoids a run time warning.
+ * docs: Add images for HdyViewSwitcher and HdyViewSwitcherBar
+ * preferences-window: Strictly center the header bar.
+ This makes the header bar's widgets look better by ensuring they are
+ always centered, even if it means they will be narrower.
+ * conbo-row: Make the popover relative to the arrow.
+ Consistently point to the arrow rather than sometimes to the arrow and
+ sometimes to the invisible box containing the current value.
+ * combo-row: Add HdyComboRowGetName.
+ Replace HdyComboRowCreateLabelData by HdyComboRowGetName and keep a
+ reference to in the combo row to allow accessing it externally. It will
+ be needed to automatically handle converting the value into a name to
+ display as the subtitle of the row.
+ * combo-row: Add the use-subtitle property.
+ Allow to display the current value as the subtitle rather than at the
+ end of the row.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/95
+ * header-bar: Render margins and borders.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/121
+
+ [ Zander Brown ]
+ * Add HdyViewSwitcherButton.
+ This will be used in the next commit by HdyViewSwitcher.
+ * Add HdyViewSwitcher.
+ This more modern and adaptive take on GtkStackSwitcher helps building
+ adaptive UIs for phones.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/64
+ * Add HdyViewSwitcherBar.
+ This action bar offers a HdyViewSwitcher and is designed to be put at
+ the bottom of windows. It is designed to be revealed when the header bar
+ doesn't have enough room to fit a HdyViewSwitcher, helping the windows
+ to reach narrower widths.
+ * examples: Add the View Switcher page.
+ This example presents a HdyViewSwitcher and a HdyViewSwitcherBar in
+ their own window. Currently both are visible at the same time, a later
+ commit should make only one visible at a time, depending on the
+ available width.
+
+ [ Aearil ]
+ * Update components list for the external projects in the README
+
+ [ Mohammed Sadiq ]
+ * dialog: Fix typos in documentation
+ * demo-window: Fix typo in property name
+
+ [ Oliver Galvin ]
+ * Change GTK+ to GTK
+ * Fix a few typos and grammatical mistakes
+ * Expand the visual overview.
+ Add more widgets and a comparison of HdyDialog
+
+ [ Guido Günther ]
+ * Release libhandy 0.0.10
+ * HACKING:
+ - Properly end emphasis
+ - Document extra space after function calls
+ * ci improvements
+ - Split doc build to different stage
+ - Split out unit tests to different stage
+ - Drop coverage on Fedora. It's not evaulated anyway
+ - Split out build commands
+ - Drop tests from static build
+ - Move Debian package to packaging stage
+ * gitlab-ci: Archive the build debs
+ * HdyArrows:
+ - Fix obvious documentation errors
+ - Only redraw widget if visible
+ - Don't emit notify signals on unchanged properties
+ - Redraw arrows on property changes
+ * HdyDemoWindow: Don't schedule arrow redraws
+ * Add suppression for ASAN
+ * tests-dialer: cleanups
+ * HdyDialer: Make show_action_buttons match the initial property default
+
+ -- Guido Günther <agx@sigxcpu.org> Wed, 12 Jun 2019 17:23:21 +0200
+
+libhandy (0.0.9) experimental; urgency=medium
+
+ [ Benjamin Berg ]
+ * glade: Mark ActionRow properties as translatable/icon.
+ Without this, it is impossible to set the translatable flag in glade,
+ making it hard to create proper UI definitions.
+
+ [ Bastien Nocera ]
+ * Use correct i18n include.
+ From the Internationalization section of the GLib API docs:
+ In order to use these macros in an application, you must include
+ <glib/gi18n.h>. For use in a library, you must include <glib/gi18n-lib.h>
+ after defining the GETTEXT_PACKAGE macro suitably for your library
+ * Fix broken translations in all libhandy applications.
+ Translations in all the applications using libhandy would be broken
+ after a call to hdy_init() as it changed the default gettext translation
+ domain.
+ See https://gitlab.gnome.org/GNOME/gnome-control-center/issues/393
+
+ [ Adrien Plazas ]
+ * examples: Update the Flatpak command.
+ The command should changed with the demo application name.
+ * leaflet: Improve the slide child transition description.
+ This makes the slide child transition description match the one of the
+ slide mode transition one.
+ * action-row: Upcast self to check the activated row.
+ Upcast the HdyActionRow rather than downcasting the activated row to
+ compare their pointers. This prevents error messages when a sibbling row
+ that isn't a HdyActionRow is activated. Also use a simple cast rather
+ than a safe cast as it is there only to please the compiler and is
+ useless for a pointer comparison and it's faster.
+ * Drop 'dialer' from the UI resources path.
+ This makes the UI file paths more correct and simpler.
+ * leaflet: Add hdy_leaflet_stop_child_transition()
+ This makes the code clearer by encapsulating child mode transition
+ cancellation into its own function.
+ * leaflet: Factorize bin window move and resize.
+ This ensures we move or resize it consistently.
+ * leaflet: Move the bin window on child transition cancellation.
+ This avoids the children to be drawn out of place when a mode transition
+ is triggered while a child transition was ongoing.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/93
+ * Add HDY_STYLE_PROVIDER_PRIORITY.
+ Add and use HDY_STYLE_PROVIDER_PRIORITY to help ensuring custom styling
+ is applied consistently and correctly accross all the library.
+ * expander-row: Move the custom style to a resource.
+ This makes the code cleaner, easier to read, and simnpler to modify.
+ * combo-row: Move the custom style to a resource.
+ This makes the code cleaner, easier to read, and simnpler to modify.
+ * expander-row: Add the expanded property.
+ This can be used to reveal external widgets depending on the state of
+ the row.
+
+ [ Guido Günther ]
+ * debian: Test GObject introspection.
+ This makes sure we have the typelib file installed correctly.
+ * debian/tests: Drop API version from include.
+ This makes sure we respect pkg-config's findings.
+ * examples: Add API version to demo name.
+ This makes different versions co-installable.
+ * build: Don't hardcode API version
+ * Release libhandy 0.0.9
+
+ -- Guido Günther <agx@sigxcpu.org> Thu, 07 Mar 2019 12:37:34 +0100
+
+libhandy (0.0.8) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * examples: Use the "frame" stylesheet on listboxes.
+ This avoids using GtkFrame where it's not relevant and shows the
+ example.
+ * examples: Refactor the Dialer panel.
+ This makes it more in line with the other panels.
+ * examples: Refactor the Arrows panel.
+ This makes it more in line with the other panels.
+ * examples: Fix the Lists panel column width.
+ We were accidentally using the widths from the Column panel.
+ * examples: Fix a typo
+ * action-row: Add the row-header style class to the header box.
+ This will allow to style the row's header separately.
+ * expander-row: Add the expander style class.
+ This will allow to style the row's padding appropriately to be used as
+ an expander.
+ * README.md: Add GNOME Settings and GNOME Web to users
+ * meson: Don't install if it's a static subproject
+ * title-bar: Drop useless definitions and inclusions.
+ These were copy and paste errors.
+ * README.md: Add gnome-bluetooth as a user
+ * examples: Rename the example program to handy-demo.
+ This also renames the type and files to match the new name.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/81
+ * meson: Fix the examples option description.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/82
+ * expander-row: Animate the arrow rotation.
+ Because we can!
+ * leaflet: Support RTL languages when unfolded.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/86
+
+ [ Benjamin Berg ]
+ * Add -s -noreset to xvfb-run calls.
+ Xvfb will close when the last client exists, which may be the cause of
+ sporadic test failures. Add -s -noreset to the command line to prevent
+ this from happening.
+ * combo-row: Fix memory leak
+ g_list_model_get_item returns a referenced GObject which needs to be
+ unref'ed.
+ * combo-row: Fix memory leak in set_for_enum
+ * value-object: Add an object to stuff a GValue into a GListModel.
+ This is useful to store arbitrary (but simple) values inside a
+ HdyComboRow.
+ * example: Use value object rather.
+ The code was storing strings in labels, just to extract them again.
+ Also, the code was leaking the labels as g_list_store_insert does not
+ sink the reference of the passed object.
+ * tests: Add tests for HdyValueObject
+ * action-row: Destroy the contained widget.
+ The GtkBox that contains everything is an internal child which must be
+ destroyed explicitly.
+
+ [ Guido Günther ]
+ * run.in: Set GLADE_MODULE_SEARCH_PATH as well.
+ This makes sure we're using the freshly built module when running
+ from the source tree.
+ * Release libhandy 0.0.8
+
+ [ Pellegrino Prevete ]
+ * README: added Daty to example apps
+ * build: Force default libdir location for libhandy target on Windows to
+ keep MinGW compatibility
+
+ [ Alexander Mikhaylenko ]
+ * leaflet: Add missing check for moving child window.
+ Prevent child window from moving in transitions that don't require it,
+ instead just resize it.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/80
+ * leaflet: Drop commented out 'under' child transition.
+ It's going to be replaced with the actual implementation in the next
+ commit.
+ * leaflet: Make 'over' child transition symmetric.
+ Implement 'under' child transition animation, use it for 'over' for right
+ and down directions, matching 'over' description.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/79
+ * leaflet: Add 'under' child transition.
+ Use same animations as 'over', but with reversed directions.
+ Documentation descriptions by Adrien Plazas.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/84
+ * leaflet: Clip bottom child during child transitions.
+ Prevents bottom child from being visible through the top one during 'over'
+ and 'under' child transitions.
+
+ [ maxice8 ]
+ * meson: pass -DHANDY_COMPILATION to GIR compiler.
+ Fixes cross compilation of GIR in Void Linux.
+
+ -- Guido Günther <agx@sigxcpu.org> Fri, 15 Feb 2019 11:27:35 +0100
+
+libhandy (0.0.7) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * glade: Add row widgets to the widget classes. They are missing and don't
+ appear in Glade.
+ * glade: Add that HdySearchBar. It's in libhandy since 0.0.6
+ * action-row: Handle show_all()
+ This avoids an empty image, an empty subtitle and an empty prefixes box
+ to be visible when calling show_all(), as they are handled by the row
+ itself.
+ * action-row: Add the Since annotation to properties
+ * example: Make the row with no action non-activatable
+ * tests: Init libhandy.
+ This ensures we run the test the same way applications are expected to
+ run libhandy.
+ * docs: Add section for new symbols in 0.0.7
+ * action-row: Add the activatable-widget property.
+ This allows to bind the activation of the row by a click or a mnemonic
+ to the activation of a widget.
+ * action-row: Chain up the parent dispose method
+ * combo-row: Release the model on dispose.
+ This avoids errors when trying to disconnect signals on finalization.
+ * combo-box: Rename selected_position to selecxted_index.
+ This will better match the name for its accessors which will be added in
+ the next commit.
+ * combo-row: Add the selected-index property.
+ This allows to access the selected item.
+ * main: Explicitely load the resources in hdy_init()
+ This is mandatory to use resources of a static version of libhandy, and
+ is hence mandatory to allow to build libhandy as a static library.
+ * meson: Bump Meson to 0.47.0.
+ This is required to use the feature option type in the next commit.
+ * meson: Make introspection and the Glade catalog features.
+ This avoids having to disable them when their dependencies aren't
+ available and it will allow to disable them properly when libhandy will
+ be allowed to be built as a static library in the next commit.
+ * meson: Allow to build as a static library.
+ This also disables the Glade catalog as it doesn't work with a static
+ libhandy.
+ * action-row: Drop pointers to internals on destruction.
+ This avoids crashes when trying to access pointers to already dropped
+ widgets.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/69
+ * expander-row: Drop pointers to internals on destruction.
+ This avoids crashes when trying to access pointers to already dropped
+ widgets.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/69
+ * examples: Make the Dialog section look nicer.
+ This improves the spacing, adds and icon and adds a description to the
+ Dialog section.
+ * dialog: Close when pressing the back button.
+ Close the dialog instead of destroying it when clicking the back button.
+ This is the same behavior as when pressing escape or clicking the close
+ button and allows the dialog to be reused as some applications like to
+ do.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/70
+
+ [ louib ]
+ * Add GNOME Contacts as example
+
+ [ Guido Günther ]
+ * HdyComboRow: Don't use g_autoptr for GEnumClass
+ g_autoptr for GEnumClass was added post 2.56, so using it makes it
+ harder for people to package for distros. Not using g_autoptr there
+ doesn't make the code much less readable.
+ * HdyDialer: Don't use class method slot for 'delete'
+ We used the one of 'submit' so far due to a c'n'p error. (Closes: #67)
+ * HdyComboRow: hdy_combo_row_get_model: Add missing scope annotation
+ * gitlab-ci: Build static library.
+ The library build is sufficiently different that we want to run the
+ build and tests.
+ * Release libhandy 0.0.7
+
+ [ David Cordero ]
+ * Update documentation regarding build dependencies
+
+ [ Zander Brown ]
+ * Implement HdyDialog, an adaptive GtkDialog
+ https://source.puri.sm/Librem5/libhandy/issues/52
+ * example: Add to example application.
+ Silly simple demo of HdyDialog.
+
+ [ Benjamin Berg ]
+ * combo-row: Rework selected-index property setting and notification.
+ The notify::selected-index signal was not selected in most cases. Rework
+ the selection handling to ensure that it is always emited when it changes
+ or if the module is replaced.
+ Also fixed are a few checks on whether the selection index is valid.
+
+ -- Guido Günther <agx@sigxcpu.org> Fri, 18 Jan 2019 14:38:30 +0100
+
+libhandy (0.0.6) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * Set relevant ATK roles.
+ This will help the widgets to be more accessible.
+ * doc: Rephrase the unstability ack section.
+ Rephrase the documentation explaining how to include libhandy in a way
+ that could include other languages such as Vala.
+ * doc: Document the unstability ack for Vala
+ * Guard header inclusions with #pragma once.
+ This standardizes the header inclusion guards to #pragma once, which
+ shouldn't be a problem as we already were using it in some files.
+ * hacking: Document header inclusion guard preferences
+ * example: Disable more libhandy options in Flatpak.
+ Disable generation of the GObject Introspection files, the VAPI and the
+ tests in the example Flatpak as they are not used by it.
+ * arrow: Use a measure() method.
+ This will simplify porting to GTK+ 4.
+ * column: Use a measure() method.
+ This will simplify porting to GTK+ 4.
+ * dialer-button: Use a measure() method.
+ This will simplify porting to GTK+ 4.
+ * leaflet: Use a measure() method.
+ This will simplify porting to GTK+ 4.
+ * init: Make the arguments optional.
+ Annotate the arguments of hdy_init() with (optional) to specify that NULL
+ arguments are valid. This also replaces the deprecated (allow-none) by
+ (nullable) to specify that the array pointed at by argv can be NULL.
+ * init: Document that libhandy can't be reinitialized
+ * Normalize and document private header guards
+ * Add HdySearchBar.
+ This is similar to GtkSearchBar except it allows the central widget
+ (typically a GtkEntry) to fill all the available space. This is needed to
+ manage an entry's width via a HdyColumn, letting the entry (and by
+ extention the search bar) have a limited maximum width while allowing it to
+ shrink to accomodate smaller windows.
+ * example: Add the 'Search bar' page.
+ This adds a demo of HdySearchBar.
+ * example: Put the content in a scrolled window.
+ This ensures the example can fit windows of any height. This also makes
+ the stack containing the content non vertically homogeneous so the
+ scrollbar appears only on examples needing it, while keeping it
+ horizontally homogeneous for to keep when the leaflets will be folded
+ consistent.
+ * build: Set the shared object install directory.
+ This is required for Meson subprojects to work as intended.
+ * build: Do not install hdy-public-types.c.
+ There is no point in installing this generated C file.
+ * leaflet: Allow editing the children list when looping through it.
+ This avoids potential crashes when destroying a leaflet and this avoids
+ leaks as not all children where looped through as the children list was
+ edited while being looped through when destroying the leaflet. This fixes
+ https://source.puri.sm/Librem5/libhandy/issues/42.
+ * Add hdy_list_box_separator_header()
+ This list box header update function is commonly used by many applications
+ and is going to be used by HdyComboRow which is going to be added to
+ libhandy later.This makes it available for everyone.
+ * examples: Use hdy_list_box_separator_header()
+ This makes the code simpler.
+ * Add HdyActionRow.
+ This implements a very commonly used list box row pattern and make it
+ convenient to use. It is going to be used as the base class for many
+ other commonly used row types.
+ * examples: Use HdyRow.
+ This makes the code simpler and demoes the widget.
+ * Add HdyExpanderRow
+ * Add HdyEnumValueObject.
+ This will be used in the next commit to use enumeration values in a
+ GListModel.
+ * Add HdyComboRow
+ * examples: Add the Lists page.
+ This page presents GtkListBox related widgets like HdyRow and its
+ descendants.
+ * examples: Put the scrolled window in the end pane size group.
+ This fixes the fold synchronization of the leaflets in the example
+ application's window.
+
+ [ Guido Günther ]
+ * hdy-enums: Make build reproducible.
+ Use @basename@ instead of @filename@ since the later uses the full
+ path which varies across builds.
+ * HACKING: Clarify braces in if-else.
+ Document common practice in the other files.
+ * spec: Sort dependencies
+ * spec: Build-depend on libgladeui-2.0
+ * gitlab-ci: Deduplicate tags
+ * gitlab-ci: Build on Fedora as well.
+ This gives us more confidence that we build succesfully and without
+ warnings on an OS much used by GNOME developers. It also makes sure we
+ validate the spec file.
+ * gitlab-ci: Switch to clang-tools
+ clang-3.9 does not contain scan-build anymore.
+ * HdyHeaderGroup: Cleanup references to header bars in dispose.
+ The dispose heandler is meant to break refs to other objects, not
+ finalize.
+ * HdyHeaderGroup: Disconnect from header bar's signals during dispose. The
+ header bars might still emit signals which leads to CRITICALS or actual
+ crashes. Fixes parts of #56
+ * docs: Add section for new symbols in 0.0.6
+ * Annotate APIs new in 0.0.6
+ * Release libhandy 0.0.6
+
+ [ Alexander Mikhaylenko ]
+ * init: Add (transfer none) to argv parameter.
+ This allows to call the function from Vala more easily.
+ * header-group: Ref itself instead of header bars.
+ When adding a header bar, ref the header group and connect to 'destroy'
+ signal of the header bar. When a header bar is destroyed or
+ hdy_header_group_remove_header_bar() is called, unref the header bar and
+ remove it from the list.
+ This way, a non-empty header group is only destroyed after every header
+ bar it contains has been removed from the group or destroyed.
+ Fixes #56
+ * Revert "HdyHeaderGroup: Disconnect from header bar's signals during
+ dispose"
+ Since commit c5bf27d44022bdfa94b3f560aac8c22115e06363 header bars are
+ destroyed before header group, so when destroying the header group, the
+ list of header bars is always empty, so there's nothing to unref anymore.
+ Reverts commit 14e5fc7b923440a99c3a62635cf895e73c5a49cd.
+
+ [ tallero ]
+ * build: Don't use -fstack-protector-strong on mingw64.
+ This unbreaks compilation on that platform. (Closes: #64)
+
+ -- Guido Günther <agx@sigxcpu.org> Mon, 17 Dec 2018 16:26:19 +0100
+
+libhandy (0.0.5) experimental; urgency=medium
+
+ [ Guido Günther ]
+ * Release libhandy 0.0.5
+ * meson: Properly depend on the generated headers.
+ This fixes dependency problems with the generated headers such as
+ https://arm01.puri.sm/job/debs/job/deb-libhandy-buster-armhf/263/console
+ See
+ http://mesonbuild.com/Wrap-best-practices-and-tips.html#eclare-generated-headers-explicitly
+ * debian: Make sure we create a strict shlibs file libhandy's ABI changes a
+ lot so make sure we generate dependencies that always require the upstream
+ version built against.
+ * debian: Mark buil-deps for tests as <!nocheck>
+ * gitlab-ci: Deduplicate before_script
+ * gitlab-ci: Build with clang (scan-build) as well.
+ We currently don't fail on warnings:
+ https://github.com/mesonbuild/meson/issues/4334
+ * HdyLeaflet: Remove unused initializations spotted by clang
+ * doc: Add that virtual methods carry the class prefix (Closes: #53)
+ * docs: Add libhandy users. This allows to find in uses examples easily.
+ * docs: Mention meson as well. Fewer and fewer GNOME projects use
+ autotools.
+ * docs: Drop package_ver_str from include path. We add this in the
+ pkg-config file so no need to specify it again.
+ * Add i18n infrastructure
+ * Add hdy_init() This initializes i18n. (Closes: #36)
+ * meson: Depend on glib that supports g_auto*. Related to #33
+ * HACKING: document using g_auto* is o.k. (Closes: #33)
+ * HACKING: Use syntax highlighting.
+ * Drop Jenkinsfile. We run in gitlab-ci now
+ * build: Detect if ld supports a version script. This is e.g. not the case
+ for Clang on OSX. (Closes: #58)
+
+ [ Jeremy Bicha ]
+ * debian: Have libhandy-0.0-dev depend on libgtk-3-dev (Closes: #910384)
+ * debian: Use dh --with gir so that gir1.2-handy gets its dependencies set
+ correctly
+ * debian: Simplify debian/rules.
+
+ [ Adrien Plazas ]
+ * example: Drop Glade support in flatpak build.
+ * main: Init public GObject types in hdy_init() This will avoid our users to
+ manually ensure libhandy widget types are loaded before using them in
+ GtkBuilder templates.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/20
+ * dialer: Descend from GtkBin directly.
+ Makes HdyDialer descend from GtkBin directly rather than from
+ GtkEventBox. GtkEventBox will be dropped in GTK+ 4 and brings no
+ functionality to HdyDialer.
+ * example: Rename margin-left/right to margin-start/end.
+ Left and right margin names are not RTL friendly and will be dropped in
+ GTK+ 4.
+ * HACKING.md: Rename margin-left to margin-start.
+ Left and right margin names are not RTL friendly and will be dropped in
+ GTK+ 4.
+ * titlebar: Fix a mention of HdyLeaflet in the docs
+ * example: Do not access event fields.
+ This is needed to port to GTK+ 4 as these fields will be private.
+ * dialer: Do not access event fields.
+ This is needed to port to GTK+ 4 as these fields will be private.
+
+ [ Alexander Mikhaylenko ]
+ * example: Remove styles present in GTK+ 3.24.1.
+ Libhandy requires `gtk+-3.0 >= 3.24.1` anyway, so these styles aren't
+ necessary, and also break upstream `.devel` style.
+
+ [ Jan Tojnar ]
+ * Use pkg-config for obtaining glade catalogdir
+
+ -- Guido Günther <agx@sigxcpu.org> Wed, 07 Nov 2018 11:17:14 +0100
+
+libhandy (0.0.4) experimental; urgency=medium
+
+ [ Mohammed Sadiq ]
+ * dialer-button: Fix emitting signal.
+ As the properties where set to not explicitly fire ::notify, no
+ signals where emitted. Let it be not explicit so that
+ the signal will be emitted on change
+ * ci: Enable code coverage.
+ GitLab pages isn't supported now. So simply store the artifacts.
+ * README: Add build and coverage status images
+ * dialer: Handle delete button long press.
+ Make the delete button clear the whole user input on long press
+
+ [ Alexander Mikhaylenko ]
+ * example: Remove sidebar border less aggressively.
+ Applying the style to every element inside 'stacksidebar' also removes
+ border from unrelated elements such as scrollbars. Hence only remove it
+ from lists.
+
+ [ Adrien Plazas ]
+ * leaflet: Add the folded property.
+ This is a boolean equivalent of the fold property, it is a needed
+ convenience as is can be used in GtkBuilder declarations while the fold
+ property is more convenient to use from C as it enables stronger typing.
+ * example: Bind back and close buttons visibility to fold.
+ Directly bind whether the back button and the close button are visible
+ to whether the headerbar is folded.
+ * Add HdyHeaderGroup
+ * example: Use a HdyHeaderGroup.
+ This automatically updates the headerbars' window decoration layout.
+ * dialer-button: Replace digit and letters by symbols.
+ Unify the digit and the letters of a dialer button as its symbols. This
+ allows to make the code simpler by limiting the number of special cases.
+ digit. This also handles Unicode characters.
+ * dialer-cycle-button: Don't make the secondary label dim.
+ This helps making it clear that these symbols are available, contrary to
+ the dim ones from a regular dialer button.
+ * dialer-button: Make the secondary label smaller.
+ Makes the secondary text smaller to better match the mockups for Calls.
+ * Add CSS names to the widgets
+ * leaflet: Document the fold and folded properties
+ * dialer: Set buttons checked instead setting relief.
+ When digit keys are pressed, check the buttons state to checked rather
+ than changing the relief.
+ * dialer: Add the relief property.
+ This allows to set the relief of the dialer buttons.
+ * header-group: Drop forgotten log.
+ This was accidentally left in.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/47
+ * example: Let the Column panel reach narrower widths.
+ Readjust the column widget's margins and ellipsize its labels to let it
+ reach narrower widths.
+ * example: Separate the listbox items
+ * example: Let the Dialer panel reach narrower widths.
+ Put the dialer into a column rather than forcing its width to let it
+ reach narrower widths.
+ * example: Enlarge the dialer label.
+ This makes the dialed number more readable.
+ * example: Let the Welcome panel reach narrower widths.
+ Let the welcome panel's labels wrap to let it reach narrower widths.
+ * header-group: Sanitize the decoration layout.
+ Checks whether the decoration layout is NULL, empty or has at least one
+ colon.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/44
+ * header-group: Better handle references of header bars.
+ Take a reference when adding a header bar, release them on destruction
+ and don't take extra references on the focused child. This avoids using
+ pointers to dead header bars or to leak them.
+ * header-group: Fix the type of the focus property.
+ This also fixes the types of the accessor functions.
+ Fixes https://source.puri.sm/Librem5/libhandy/issues/46
+ * header-group: Fix the docs of the focus property.
+ This also improves the documentation of its accessor functions.
+ * header-group: Guard the focused header bar setter.
+ Better guard the focused header bar setter by checking that the set
+ header bar actually is one and is part of the group.
+ * meson: Require GTK+ 3.24.1 or newer.
+ GTK+ 3.24.1 received style fixes required for HdyTitleBar to work as
+ expected.
+
+ [ Felix Pojtinger ]
+ * docs: Format README to enable syntax highlighting.
+ This also adds code fences and blanks around headers.
+
+ [ Guido Günther ]
+ * Depend on generated headers.
+ If tests or examples are built early we want that hdy-enums.h is alread
+ there.
+ * docs: Add HdyFold.
+ This makes sure it can be linked to by HdyLeaflet.
+ * HdyLealflet: Use glib-mkenums.
+ This makes the enums clickable in the HdyLeaflet documentation
+ and makes the code smaller.
+ * HdyFold: Use glib-mkenums.
+ This makes the enums clickable in the HdyLeaflet documentation,
+ HdyFold usable in GtkBuild and makes the code smaller.
+ * HdyHeaderGroup: Document hdy_group_set_focus()
+ This makes newer newer Gir scanner happy (and is a good thing anyway).
+ * debian: Update shared library symbols
+ * d/rules: Set a proper locale for the tests.
+ * Check the debian package build during CI as well.
+ This make sure we notice build breackage before it hits Jenkins to build
+ the official debs.
+ * ci: Fail on gtkdoc warnings.
+ Gitlab seems to get confused by the '!' expression so use if instead.
+ * tests: Test hdy_header_group_{get,set}_focus
+ * HdyDialer: Apply 'keypad' style class.
+ This applies the 'keypad' style class to both the keypad itself and its
+ buttons. This allows to style the buttons and the keypad in the
+ application.
+ * glade: Verify catalog data via xmllint
+ * debian: Add dependenies for running xmllint.
+ This also makes sure we have it available during CI
+ * HdyHeaderGroup: Allow to get and remove the headerbars
+ * debian: Add new symbols
+ * glade: Add a module so we can handle HdyHeaderGroup
+ * run: Add glade lib to LD_LIBRARY_PATH.
+ This makes it simple to test the built version.
+ * Move glade catalog from data/ to glade/
+ Given that there will be more complex widgets lets keep the catalog
+ and module together.
+ * glade: Use a custom DTD.
+ Glades DTD is not up to date. Use a custom copy until this is fixed
+ upstream:
+ https://gitlab.gnome.org/GNOME/glade/merge_requests/11
+ We do this as a separate commit so we can revert it once upstream glade is
+ fixed.
+ * glade: Support HdyHeaderGroup (Closes: #38)
+ * debian: Ship glade module
+
+ -- Guido Günther <agx@sigxcpu.org> Fri, 05 Oct 2018 18:32:42 +0200
+
+libhandy (0.0.3) experimental; urgency=medium
+
+ [ Adrien Plazas ]
+ * New HdyTitleBar widget. This coupled with a transparent headerbar
+ background can work around graphical glitches when animation header bars
+ in a leaflet.
+ * column: Add the linear-growth-width property
+ * glade: Fix the generic name of HdyArrows
+ * flatpak: Switch the runtime of the example to master.
+ * column: Add a missing break statement.
+ * leaflet: Hide children on transition end only when folded.
+ * leaflet: Init mode transition positions to the final values.
+ * example: Always show a close button.
+ * example: Load custom style from CSS resource
+ * example: Draw the right color for sidebar separators.
+ * example: Use separators to separate the panels.
+ * leaflet: Start the child transition only when folded.
+
+ [ Christopher Davis ]
+ * Add HdyColumn to libhandy.xml for glade.
+
+ [ Heather Ellsworth ]
+ * Add issue template
+
+ [ Jordan Petridis ]
+ * leaflet: initialize a variable.
+
+ [ Guido Günther ]
+ * HdyButton: Chain up to parent on finalize
+ * gitlab-ci: Fail on compile warnings
+ * meson: Warn about possible uninitialized variables
+ * HdyLeaflet: Fix two uninitialized variables
+ * Update list of compiler warnings from phosh
+ and fix the fallout.
+
+ -- Guido Günther <agx@sigxcpu.org> Wed, 12 Sep 2018 12:03:54 +0200
+
+libhandy (0.0.2) experimental; urgency=medium
+
+ [ Guido Günther ]
+ * Use source.puri.sm instead of code.puri.sm.
+ * Add AUTHORS file
+ * gitlab-ci: Build on Debian buster using provided build-deps.
+ * arrows: test object construction
+ * Multiple gtk-doc fixes
+ * docs: Abort on warnings.
+ * DialerButton: free letters
+
+ [ Adrien Plazas ]
+ * dialer: Make the grid visible and forbid show all.
+ * example: Drop usage of show_all()
+ * dialer: Add column-spacing and row-spacing props.
+ * example: Change the grid's spacing and minimum size request.
+ * flatpak: Allow access to the dconf config dir.
+ * Replace phone-dial-symbolic by call-start-symbolic.
+ * column: Fix height for width request.
+
+ -- Guido Günther <agx@sigxcpu.org> Wed, 18 Jul 2018 13:12:10 +0200
+
+libhandy (0.0.1) experimental; urgency=medium
+
+ [ Guido Günther ]
+ * Release 0.0.1
+
+ [ Adrien Plazas ]
+ * Add HdyColumn widget
+
+ -- Guido Günther <agx@sigxcpu.org> Sat, 09 Jun 2018 09:12:06 +0200
+
+libhandy (0.0~git20180517) unstable; urgency=medium
+
+ * Add an arrows widget.
+ The widget prints a number of arrows one by one to indicate a sliding
+ direction. Number of arrows and animation duration are configurable.
+ * Add symbols file
+
+ -- Guido Günther <agx@sigxcpu.org> Thu, 17 May 2018 15:51:01 +0200
+
+libhandy (0.0~git20180429) unstable; urgency=medium
+
+ [ Guido Günther ]
+ * New git snapshot
+ * HdyDialer: Emit symbol-clicked signal. This signal is emitted when a
+ symbol button (numbers or '#' or '*') is clicked.
+ * HdyDialer: Emit signal when delete button was clicked.
+ * dialer: Make it simple to clear the stored number.
+ This also makes sure we don't send multiple number changed events
+ when nothing changed.
+ * dialer: Delay number notify. On button press send out the number changed
+ signal at the very end so listeners can process the button event prior to
+ the number update event.
+
+ [ Adrien Plazas ]
+ * leaflet: Refactor homogeneity.
+ This makes factorizes the homogeneity functions of HdyLeaflet to make
+ the code a bit shorter.
+ * build: Add '--c-include=handy.h' GIR options back.
+ This is necessary for introspection to know the header file to use.
+ * dialer: Check params of the 'number' prop accessors.
+ Sanitize the parameters of the 'number' property accessor. This will
+ warn or misusages of the API at runtime and avoid potential crashes.
+ * dialer: Style cleanup of the 'number' prop accessors.
+ Use gchar instead of char, use GNOME style pointer spacing and name the
+ number parameter 'number'. This is all cosmetic but will make the code
+ look a bit more GNOME-like.
+ * example: Drop hardcoded default window size.
+ This avoid overridding with the one we set in the the .ui file of the
+ window.
+ * example: Move window title to .ui file.
+ This avoid hardcoding values when we can put them in the UI description.
+ * example-window: Make the default size more phone-like
+
+ [ Bob Ham ]
+ * dialer: Add "show-action-buttons" property.
+ Add a new boolean "show-action-buttons" property that specifies
+ whether the submit and delete buttons are displayed.
+
+ -- Guido Günther <agx@sigxcpu.org> Sun, 29 Apr 2018 12:01:58 +0200
+
+libhandy (0.0~git20180402) unstable; urgency=medium
+
+ * Initial release
+
+ -- Guido Günther <agx@sigxcpu.org> Mon, 02 Apr 2018 12:17:44 +0200
diff --git a/subprojects/libhandy/debian/control b/subprojects/libhandy/debian/control
new file mode 100644
index 0000000..b414420
--- /dev/null
+++ b/subprojects/libhandy/debian/control
@@ -0,0 +1,79 @@
+Source: libhandy-1
+Section: libs
+Priority: optional
+Maintainer: Guido Günther <agx@sigxcpu.org>
+Build-Depends:
+ debhelper-compat (= 12),
+ dh-sequence-gir,
+ gtk-doc-tools,
+ libgirepository1.0-dev,
+ libgladeui-dev,
+ libglib2.0-doc,
+ libgnome-desktop-3-dev,
+ libgtk-3-doc,
+ libgtk-3-dev,
+ libxml2-utils,
+ meson,
+ pkg-config,
+ valac (>= 0.20),
+# to run the tests
+ xvfb <!nocheck>,
+ xauth <!nocheck>,
+Standards-Version: 4.1.3
+Homepage: https://gitlab.gnome.org/GNOME/libhandy
+Vcs-Browser: https://salsa.debian.org/DebianOnMobile-team/libhandy
+Vcs-Git: https://salsa.debian.org/DebianOnMobile-team/libhandy.git
+
+Package: libhandy-1-0
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+Description: Library with GTK widgets for mobile phones
+ libhandy provides GTK widgets and GObjects to ease developing
+ applications for mobile phones.
+ .
+ This package contains the shared library.
+
+Package: libhandy-1-dev
+Architecture: any
+Multi-Arch: same
+Section: libdevel
+Depends:
+ ${misc:Depends},
+ ${shlibs:Depends},
+ gir1.2-handy-1 (= ${binary:Version}),
+ libhandy-1-0 (= ${binary:Version}),
+ libgtk-3-dev,
+Recommends: pkg-config
+Description: Development files for libhandy
+ libhandy provides GTK widgets and GObjects to ease developing
+ applications for mobile phones.
+ .
+ This package contains the development files and documentation.
+
+Package: gir1.2-handy-1
+Architecture: any
+Multi-Arch: same
+Section: introspection
+Depends:
+ ${gir:Depends},
+ ${misc:Depends},
+Description: GObject introspection files for libhandy
+ libhandy provides GTK widgets and GObjects to ease developing
+ applications for mobile phones.
+ .
+ This package contains the GObject-introspection data in binary typelib format.
+
+Package: handy-1-examples
+Section: x11
+Architecture: any
+Depends: ${misc:Depends},
+ ${shlibs:Depends},
+Description: Example programs for libhandy
+ libhandy provides GTK widgets and GObjects to ease developing
+ applications for mobile phones.
+ .
+ This package contains example files and the demonstration program for
+ libhandy.
diff --git a/subprojects/libhandy/debian/copyright b/subprojects/libhandy/debian/copyright
new file mode 100644
index 0000000..357e8cd
--- /dev/null
+++ b/subprojects/libhandy/debian/copyright
@@ -0,0 +1,22 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: libhandy
+Source: https://gitlab.gnome.org/GNOME/libhandy
+
+Files: *
+Copyright: 2018 Purism SPC
+License: LGPL-2.1+
+ This package is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 3 of the License, or
+ (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>
+ .
+ On Debian systems, the complete text of the GNU General
+ Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
diff --git a/subprojects/libhandy/debian/docs b/subprojects/libhandy/debian/docs
new file mode 100644
index 0000000..edc0071
--- /dev/null
+++ b/subprojects/libhandy/debian/docs
@@ -0,0 +1 @@
+NEWS
diff --git a/subprojects/libhandy/debian/gir1.2-handy-1.install b/subprojects/libhandy/debian/gir1.2-handy-1.install
new file mode 100644
index 0000000..ccdda39
--- /dev/null
+++ b/subprojects/libhandy/debian/gir1.2-handy-1.install
@@ -0,0 +1 @@
+usr/lib/*/girepository-1.0/*
diff --git a/subprojects/libhandy/debian/handy-0.0-examples.examples b/subprojects/libhandy/debian/handy-0.0-examples.examples
new file mode 100644
index 0000000..2c8fad4
--- /dev/null
+++ b/subprojects/libhandy/debian/handy-0.0-examples.examples
@@ -0,0 +1 @@
+examples/*.py
diff --git a/subprojects/libhandy/debian/handy-0.0-examples.install b/subprojects/libhandy/debian/handy-0.0-examples.install
new file mode 100644
index 0000000..bbaef53
--- /dev/null
+++ b/subprojects/libhandy/debian/handy-0.0-examples.install
@@ -0,0 +1 @@
+usr/bin/handy-0.0-demo
diff --git a/subprojects/libhandy/debian/handy-1-examples.install b/subprojects/libhandy/debian/handy-1-examples.install
new file mode 100644
index 0000000..e772481
--- /dev/null
+++ b/subprojects/libhandy/debian/handy-1-examples.install
@@ -0,0 +1 @@
+usr/bin
diff --git a/subprojects/libhandy/debian/libhandy-1-0.install b/subprojects/libhandy/debian/libhandy-1-0.install
new file mode 100644
index 0000000..5afd80a
--- /dev/null
+++ b/subprojects/libhandy/debian/libhandy-1-0.install
@@ -0,0 +1,2 @@
+usr/lib/*/libhandy-?.so.*
+usr/share/locale
diff --git a/subprojects/libhandy/debian/libhandy-1-0.symbols b/subprojects/libhandy/debian/libhandy-1-0.symbols
new file mode 100644
index 0000000..77c9884
--- /dev/null
+++ b/subprojects/libhandy/debian/libhandy-1-0.symbols
@@ -0,0 +1,328 @@
+libhandy-1.so.0 libhandy-1-0 #MINVER#
+ LIBHANDY_1_0@LIBHANDY_1_0 0.0~git20180429
+ hdy_action_row_activate@LIBHANDY_1_0 0.0.6
+ hdy_action_row_add_prefix@LIBHANDY_1_0 0.0.6
+ hdy_action_row_get_activatable_widget@LIBHANDY_1_0 0.0.7
+ hdy_action_row_get_icon_name@LIBHANDY_1_0 0.0.6
+ hdy_action_row_get_subtitle@LIBHANDY_1_0 0.0.6
+ hdy_action_row_get_type@LIBHANDY_1_0 0.0.6
+ hdy_action_row_get_use_underline@LIBHANDY_1_0 0.0.6
+ hdy_action_row_new@LIBHANDY_1_0 0.0.6
+ hdy_action_row_set_activatable_widget@LIBHANDY_1_0 0.0.7
+ hdy_action_row_set_icon_name@LIBHANDY_1_0 0.0.6
+ hdy_action_row_set_subtitle@LIBHANDY_1_0 0.0.6
+ hdy_action_row_set_use_underline@LIBHANDY_1_0 0.0.6
+ hdy_application_window_get_type@LIBHANDY_1_0 0.80.0
+ hdy_application_window_new@LIBHANDY_1_0 0.80.0
+ hdy_avatar_get_icon_name@LIBHANDY_1_0 0.85.0
+ hdy_avatar_get_show_initials@LIBHANDY_1_0 0.80.0
+ hdy_avatar_get_size@LIBHANDY_1_0 0.80.0
+ hdy_avatar_get_text@LIBHANDY_1_0 0.80.0
+ hdy_avatar_get_type@LIBHANDY_1_0 0.80.0
+ hdy_avatar_new@LIBHANDY_1_0 0.80.0
+ hdy_avatar_set_icon_name@LIBHANDY_1_0 0.85.0
+ hdy_avatar_set_image_load_func@LIBHANDY_1_0 0.80.0
+ hdy_avatar_set_show_initials@LIBHANDY_1_0 0.80.0
+ hdy_avatar_set_size@LIBHANDY_1_0 0.80.0
+ hdy_avatar_set_text@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_allow_mouse_drag@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_animation_duration@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_interactive@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_n_pages@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_position@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_reveal_duration@LIBHANDY_1_0 0.81.0
+ hdy_carousel_get_spacing@LIBHANDY_1_0 0.80.0
+ hdy_carousel_get_type@LIBHANDY_1_0 0.80.0
+ hdy_carousel_indicator_dots_get_type@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_dots_get_carousel@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_dots_new@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_dots_set_carousel@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_lines_get_type@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_lines_get_carousel@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_lines_new@LIBHANDY_1_0 0.90.0
+ hdy_carousel_indicator_lines_set_carousel@LIBHANDY_1_0 0.90.0
+ hdy_carousel_insert@LIBHANDY_1_0 0.80.0
+ hdy_carousel_new@LIBHANDY_1_0 0.80.0
+ hdy_carousel_prepend@LIBHANDY_1_0 0.80.0
+ hdy_carousel_reorder@LIBHANDY_1_0 0.80.0
+ hdy_carousel_scroll_to@LIBHANDY_1_0 0.80.0
+ hdy_carousel_scroll_to_full@LIBHANDY_1_0 0.80.0
+ hdy_carousel_set_allow_mouse_drag@LIBHANDY_1_0 0.80.0
+ hdy_carousel_set_animation_duration@LIBHANDY_1_0 0.80.0
+ hdy_carousel_set_interactive@LIBHANDY_1_0 0.80.0
+ hdy_carousel_set_reveal_duration@LIBHANDY_1_0 0.81.0
+ hdy_carousel_set_spacing@LIBHANDY_1_0 0.80.0
+ hdy_centering_policy_get_type@LIBHANDY_1_0 0.0.10
+ hdy_clamp_get_maximum_size@LIBHANDY_1_0 0.82.0
+ hdy_clamp_get_tightening_threshold@LIBHANDY_1_0 0.82.0
+ hdy_clamp_get_type@LIBHANDY_1_0 0.82.0
+ hdy_clamp_new@LIBHANDY_1_0 0.82.0
+ hdy_clamp_set_maximum_size@LIBHANDY_1_0 0.82.0
+ hdy_clamp_set_tightening_threshold@LIBHANDY_1_0 0.82.0
+ hdy_combo_row_bind_model@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_bind_name_model@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_get_model@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_get_selected_index@LIBHANDY_1_0 0.0.7
+ hdy_combo_row_get_type@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_get_use_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_combo_row_new@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_set_for_enum@LIBHANDY_1_0 0.0.6
+ hdy_combo_row_set_get_name_func@LIBHANDY_1_0 0.0.10
+ hdy_combo_row_set_selected_index@LIBHANDY_1_0 0.0.7
+ hdy_combo_row_set_use_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_deck_get_adjacent_child@LIBHANDY_1_0 0.81.0
+ hdy_deck_get_can_swipe_back@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_can_swipe_forward@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_child_by_name@LIBHANDY_1_0 0.85.0
+ hdy_deck_get_homogeneous@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_interpolate_size@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_transition_duration@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_transition_running@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_transition_type@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_type@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_visible_child@LIBHANDY_1_0 0.80.0
+ hdy_deck_get_visible_child_name@LIBHANDY_1_0 0.80.0
+ hdy_deck_navigate@LIBHANDY_1_0 0.80.0
+ hdy_deck_new@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_can_swipe_back@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_can_swipe_forward@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_homogeneous@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_interpolate_size@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_transition_duration@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_transition_type@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_visible_child@LIBHANDY_1_0 0.80.0
+ hdy_deck_set_visible_child_name@LIBHANDY_1_0 0.80.0
+ hdy_deck_transition_type_get_type@LIBHANDY_1_0 0.80.0
+ hdy_ease_out_cubic@LIBHANDY_1_0 0.0.11
+ hdy_enum_value_object_get_name@LIBHANDY_1_0 0.0.6
+ hdy_enum_value_object_get_nick@LIBHANDY_1_0 0.0.6
+ hdy_enum_value_object_get_type@LIBHANDY_1_0 0.0.6
+ hdy_enum_value_object_get_value@LIBHANDY_1_0 0.0.6
+ hdy_enum_value_object_new@LIBHANDY_1_0 0.0.6
+ hdy_enum_value_row_name@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_add_action@LIBHANDY_1_0 0.81.0
+ hdy_expander_row_add_prefix@LIBHANDY_1_0 0.82.0
+ hdy_expander_row_get_enable_expansion@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_get_expanded@LIBHANDY_1_0 0.0.9
+ hdy_expander_row_get_icon_name@LIBHANDY_1_0 0.80.0
+ hdy_expander_row_get_show_enable_switch@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_get_subtitle@LIBHANDY_1_0 0.80.0
+ hdy_expander_row_get_type@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_get_use_underline@LIBHANDY_1_0 0.80.0
+ hdy_expander_row_new@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_set_enable_expansion@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_set_expanded@LIBHANDY_1_0 0.0.9
+ hdy_expander_row_set_icon_name@LIBHANDY_1_0 0.80.0
+ hdy_expander_row_set_show_enable_switch@LIBHANDY_1_0 0.0.6
+ hdy_expander_row_set_subtitle@LIBHANDY_1_0 0.80.0
+ hdy_expander_row_set_use_underline@LIBHANDY_1_0 0.80.0
+ hdy_get_enable_animations@LIBHANDY_1_0 0.0.11
+ hdy_header_bar_get_centering_policy@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_custom_title@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_decoration_layout@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_has_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_interpolate_size@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_show_close_button@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_title@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_transition_duration@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_transition_running@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_get_type@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_new@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_pack_end@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_pack_start@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_centering_policy@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_custom_title@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_decoration_layout@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_has_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_interpolate_size@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_show_close_button@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_subtitle@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_title@LIBHANDY_1_0 0.0.10
+ hdy_header_bar_set_transition_duration@LIBHANDY_1_0 0.0.10
+ hdy_header_group_add_gtk_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_add_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_add_header_group@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_get_child_type@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_get_gtk_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_get_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_get_header_group@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_get_type@LIBHANDY_1_0 0.83.0
+ hdy_header_group_child_type_get_type@LIBHANDY_1_0 0.83.0
+ hdy_header_group_get_children@LIBHANDY_1_0 0.83.0
+ hdy_header_group_get_decorate_all@LIBHANDY_1_0 0.83.0
+ hdy_header_group_get_type@LIBHANDY_1_0 0.0.3
+ hdy_header_group_new@LIBHANDY_1_0 0.0.3
+ hdy_header_group_remove_child@LIBHANDY_1_0 0.83.0
+ hdy_header_group_remove_gtk_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_remove_header_bar@LIBHANDY_1_0 0.83.0
+ hdy_header_group_remove_header_group@LIBHANDY_1_0 0.83.0
+ hdy_header_group_set_decorate_all@LIBHANDY_1_0 0.83.0
+ hdy_init@LIBHANDY_1_0 0.82.0
+ hdy_keypad_get_column_spacing@LIBHANDY_1_0 0.81.0
+ hdy_keypad_get_end_action@LIBHANDY_1_0 0.85.0
+ hdy_keypad_get_entry@LIBHANDY_1_0 0.0.12
+ hdy_keypad_get_letters_visible@LIBHANDY_1_0 0.85.0
+ hdy_keypad_get_row_spacing@LIBHANDY_1_0 0.81.0
+ hdy_keypad_get_start_action@LIBHANDY_1_0 0.85.0
+ hdy_keypad_get_symbols_visible@LIBHANDY_1_0 0.85.0
+ hdy_keypad_get_type@LIBHANDY_1_0 0.0.12
+ hdy_keypad_new@LIBHANDY_1_0 0.0.12
+ hdy_keypad_set_column_spacing@LIBHANDY_1_0 0.81.0
+ hdy_keypad_set_end_action@LIBHANDY_1_0 0.85.0
+ hdy_keypad_set_entry@LIBHANDY_1_0 0.0.12
+ hdy_keypad_set_letters_visible@LIBHANDY_1_0 0.85.0
+ hdy_keypad_set_row_spacing@LIBHANDY_1_0 0.81.0
+ hdy_keypad_set_start_action@LIBHANDY_1_0 0.85.0
+ hdy_keypad_set_symbols_visible@LIBHANDY_1_0 0.85.0
+ hdy_leaflet_get_adjacent_child@LIBHANDY_1_0 0.81.0
+ hdy_leaflet_get_can_swipe_back@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_get_can_swipe_forward@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_get_child_by_name@LIBHANDY_1_0 0.85.0
+ hdy_leaflet_get_child_transition_duration@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_child_transition_running@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_folded@LIBHANDY_1_0 0.80.0
+ hdy_leaflet_get_homogeneous@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_interpolate_size@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_mode_transition_duration@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_transition_type@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_get_type@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_visible_child@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_get_visible_child_name@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_navigate@LIBHANDY_1_0 0.80.0
+ hdy_leaflet_new@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_can_swipe_back@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_set_can_swipe_forward@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_set_child_transition_duration@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_homogeneous@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_interpolate_size@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_mode_transition_duration@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_transition_type@LIBHANDY_1_0 0.0.12
+ hdy_leaflet_set_visible_child@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_set_visible_child_name@LIBHANDY_1_0 0.0~git20180429
+ hdy_leaflet_transition_type_get_type@LIBHANDY_1_0 0.0.12
+ hdy_navigation_direction_get_type@LIBHANDY_1_0 0.9.9
+ hdy_preferences_group_get_description@LIBHANDY_1_0 0.0.10
+ hdy_preferences_group_get_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_group_get_type@LIBHANDY_1_0 0.0.10
+ hdy_preferences_group_new@LIBHANDY_1_0 0.0.10
+ hdy_preferences_group_set_description@LIBHANDY_1_0 0.0.10
+ hdy_preferences_group_set_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_get_icon_name@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_get_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_get_type@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_new@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_set_icon_name@LIBHANDY_1_0 0.0.10
+ hdy_preferences_page_set_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_get_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_get_type@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_get_use_underline@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_new@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_set_title@LIBHANDY_1_0 0.0.10
+ hdy_preferences_row_set_use_underline@LIBHANDY_1_0 0.0.10
+ hdy_preferences_window_close_subpage@LIBHANDY_1_0 0.85.0
+ hdy_preferences_window_get_can_swipe_back@LIBHANDY_1_0 0.85.0
+ hdy_preferences_window_get_search_enabled@LIBHANDY_1_0 0.80.0
+ hdy_preferences_window_get_type@LIBHANDY_1_0 0.0.10
+ hdy_preferences_window_new@LIBHANDY_1_0 0.0.10
+ hdy_preferences_window_present_subpage@LIBHANDY_1_0 0.85.0
+ hdy_preferences_window_set_can_swipe_back@LIBHANDY_1_0 0.85.0
+ hdy_preferences_window_set_search_enabled@LIBHANDY_1_0 0.80.0
+ hdy_search_bar_connect_entry@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_get_search_mode@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_get_show_close_button@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_get_type@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_handle_event@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_new@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_set_search_mode@LIBHANDY_1_0 0.0.6
+ hdy_search_bar_set_show_close_button@LIBHANDY_1_0 0.0.6
+ hdy_squeezer_get_child_enabled@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_homogeneous@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_interpolate_size@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_transition_duration@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_transition_running@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_transition_type@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_type@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_visible_child@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_get_xalign@LIBHANDY_1_0 0.85.0
+ hdy_squeezer_get_yalign@LIBHANDY_1_0 0.85.0
+ hdy_squeezer_new@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_child_enabled@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_homogeneous@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_interpolate_size@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_transition_duration@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_transition_type@LIBHANDY_1_0 0.0.10
+ hdy_squeezer_set_xalign@LIBHANDY_1_0 0.85.0
+ hdy_squeezer_set_yalign@LIBHANDY_1_0 0.85.0
+ hdy_squeezer_transition_type_get_type@LIBHANDY_1_0 0.0.10
+ hdy_swipe_group_add_swipeable@LIBHANDY_1_0 0.0.12
+ hdy_swipe_group_get_swipeables@LIBHANDY_1_0 0.0.12
+ hdy_swipe_group_get_type@LIBHANDY_1_0 0.0.12
+ hdy_swipe_group_new@LIBHANDY_1_0 0.0.12
+ hdy_swipe_group_remove_swipeable@LIBHANDY_1_0 0.0.12
+ hdy_swipe_tracker_get_allow_mouse_drag@LIBHANDY_1_0 0.0.12
+ hdy_swipe_tracker_get_enabled@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_get_reversed@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_get_swipeable@LIBHANDY_1_0 0.82.0
+ hdy_swipe_tracker_get_type@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_new@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_set_allow_mouse_drag@LIBHANDY_1_0 0.0.12
+ hdy_swipe_tracker_set_enabled@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_set_reversed@LIBHANDY_1_0 0.0.11
+ hdy_swipe_tracker_shift_position@LIBHANDY_1_0 0.81.0
+ hdy_swipeable_emit_child_switched@LIBHANDY_1_0 0.82.0
+ hdy_swipeable_get_cancel_progress@LIBHANDY_1_0 0.81.0
+ hdy_swipeable_get_distance@LIBHANDY_1_0 0.81.0
+ hdy_swipeable_get_progress@LIBHANDY_1_0 0.81.0
+ hdy_swipeable_get_snap_points@LIBHANDY_1_0 0.81.0
+ hdy_swipeable_get_swipe_area@LIBHANDY_1_0 0.82.0
+ hdy_swipeable_get_swipe_tracker@LIBHANDY_1_0 0.82.0
+ hdy_swipeable_get_type@LIBHANDY_1_0 0.0.12
+ hdy_swipeable_switch_child@LIBHANDY_1_0 0.0.12
+ hdy_title_bar_get_selection_mode@LIBHANDY_1_0 0.0.3
+ hdy_title_bar_get_type@LIBHANDY_1_0 0.0.3
+ hdy_title_bar_new@LIBHANDY_1_0 0.0.3
+ hdy_title_bar_set_selection_mode@LIBHANDY_1_0 0.0.3
+ hdy_value_object_copy_value@LIBHANDY_1_0 0.0.8
+ hdy_value_object_dup_string@LIBHANDY_1_0 0.0.8
+ hdy_value_object_get_string@LIBHANDY_1_0 0.0.8
+ hdy_value_object_get_type@LIBHANDY_1_0 0.0.8
+ hdy_value_object_get_value@LIBHANDY_1_0 0.0.8
+ hdy_value_object_new@LIBHANDY_1_0 0.0.8
+ hdy_value_object_new_collect@LIBHANDY_1_0 0.0.8
+ hdy_value_object_new_string@LIBHANDY_1_0 0.0.8
+ hdy_value_object_new_take_string@LIBHANDY_1_0 0.0.8
+ hdy_view_switcher_bar_get_policy@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_get_reveal@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_get_stack@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_get_type@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_new@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_set_policy@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_set_reveal@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_bar_set_stack@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_get_narrow_ellipsize@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_get_policy@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_get_stack@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_get_type@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_new@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_policy_get_type@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_set_narrow_ellipsize@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_set_policy@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_set_stack@LIBHANDY_1_0 0.0.10
+ hdy_view_switcher_title_get_policy@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_stack@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_subtitle@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_title@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_title_visible@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_type@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_get_view_switcher_enabled@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_new@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_set_policy@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_set_stack@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_set_subtitle@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_set_title@LIBHANDY_1_0 0.80.0
+ hdy_view_switcher_title_set_view_switcher_enabled@LIBHANDY_1_0 0.80.0
+ hdy_window_get_type@LIBHANDY_1_0 0.80.0
+ hdy_window_handle_get_type@LIBHANDY_1_0 0.80.0
+ hdy_window_handle_new@LIBHANDY_1_0 0.80.0
+ hdy_window_new@LIBHANDY_1_0 0.80.0
diff --git a/subprojects/libhandy/debian/libhandy-1-dev.install b/subprojects/libhandy/debian/libhandy-1-dev.install
new file mode 100644
index 0000000..9cccd33
--- /dev/null
+++ b/subprojects/libhandy/debian/libhandy-1-dev.install
@@ -0,0 +1,8 @@
+usr/include/*
+usr/lib/*/libhandy-?.so
+usr/lib/*/glade/modules/libglade-handy-?.so
+usr/lib/*/pkgconfig/*
+usr/share/gir-1.0/*
+usr/share/glade/catalogs/
+usr/share/gtk-doc/
+usr/share/vala/vapi/
diff --git a/subprojects/libhandy/debian/rules b/subprojects/libhandy/debian/rules
new file mode 100755
index 0000000..e1f6d22
--- /dev/null
+++ b/subprojects/libhandy/debian/rules
@@ -0,0 +1,16 @@
+#!/usr/bin/make -f
+
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+
+%:
+ dh $@
+
+override_dh_auto_configure:
+ dh_auto_configure -- -Dgtk_doc=true
+
+override_dh_auto_test:
+ xvfb-run -s -noreset dh_auto_test
+
+override_dh_makeshlibs:
+ dh_makeshlibs --package=libhandy-1-0 -- -c2
+
diff --git a/subprojects/libhandy/debian/source/format b/subprojects/libhandy/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/subprojects/libhandy/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/subprojects/libhandy/debian/tests/build-test b/subprojects/libhandy/debian/tests/build-test
new file mode 100755
index 0000000..434a846
--- /dev/null
+++ b/subprojects/libhandy/debian/tests/build-test
@@ -0,0 +1,30 @@
+#!/bin/sh
+set -eu
+
+if [ -n "${DEB_HOST_GNU_TYPE:-}" ]; then
+ CROSS_COMPILE="$DEB_HOST_GNU_TYPE-"
+else
+ CROSS_COMPILE=
+fi
+
+cd "$AUTOPKGTEST_TMP"
+
+cat <<EOF > handytest.c
+#include <gtk/gtk.h>
+#include <handy.h>
+
+int
+main (int argc,
+ char **argv)
+{
+ gtk_init(&argc, &argv);
+ hdy_init();
+ hdy_keypad_new(FALSE, TRUE);
+}
+EOF
+
+${CROSS_COMPILE}gcc -o handytest handytest.c $(${CROSS_COMPILE}pkg-config --cflags --libs libhandy-1)
+echo "build ok"
+[ -x handytest ]
+xvfb-run -a -s "-screen 0 1024x768x24" ./handytest
+echo "starts ok"
diff --git a/subprojects/libhandy/debian/tests/control b/subprojects/libhandy/debian/tests/control
new file mode 100644
index 0000000..595abee
--- /dev/null
+++ b/subprojects/libhandy/debian/tests/control
@@ -0,0 +1,8 @@
+Tests: build-test
+Depends: libhandy-1-dev, build-essential, pkg-config, xauth, xvfb
+Restrictions: allow-stderr
+
+Tests: python-gi-test
+Depends: gir1.2-handy-1, python3-gi, python3
+Restrictions: allow-stderr
+
diff --git a/subprojects/libhandy/debian/tests/python-gi-test b/subprojects/libhandy/debian/tests/python-gi-test
new file mode 100755
index 0000000..dfb1bf3
--- /dev/null
+++ b/subprojects/libhandy/debian/tests/python-gi-test
@@ -0,0 +1,13 @@
+#!/usr/bin/python3
+#
+# Make sure gobject introspection works
+
+import gi
+
+gi.require_version('Handy', '1')
+from gi.repository import Handy
+
+group = Handy.SwipeGroup()
+
+assert type(group).__name__ == 'SwipeGroup'
+assert group.get_swipeables() == []
diff --git a/subprojects/libhandy/doc/build-howto.xml b/subprojects/libhandy/doc/build-howto.xml
new file mode 100644
index 0000000..a6f731f
--- /dev/null
+++ b/subprojects/libhandy/doc/build-howto.xml
@@ -0,0 +1,159 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+ <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent">
+ %gtkdocentities;
+]>
+
+<refentry id="build-howto">
+ <refmeta>
+ <refentrytitle>Compiling with &package_string;</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>Compiling with &package_string;</refname><refpurpose>Notes on compiling.</refpurpose>
+ </refnamediv>
+
+ <refsect2>
+ <title>Building</title>
+
+ <para>
+ If you need to build <application>&package_string;</application>, get the
+ source from <ulink type="http" url="&package_url;">here</ulink> and see
+ the <literal>README.md</literal> file.
+ </para>
+ </refsect2>
+
+ <refsect2>
+ <title>Using pkg-config</title>
+
+ <para> Like other GNOME libraries,
+ <application>&package_string;</application> uses
+ <application>pkg-config</application> to provide compiler options. The
+ package name is "<literal>&package_ver_str;</literal>".
+ </para>
+
+ <para>
+ If you use Automake/Autoconf, in your <literal>configure.ac</literal>
+ script, you might specify something like:
+ </para>
+
+ <informalexample><programlisting>
+ PKG_CHECK_MODULES(LIBHANDY, [&package_ver_str;])
+ AC_SUBST(LIBHANDY_CFLAGS)
+ AC_SUBST(LIBHANDY_LIBS)
+ </programlisting></informalexample>
+
+ <para>
+ Or when using the Meson build system you can declare a dependency like:
+ </para>
+
+ <informalexample><programlisting>
+ dependency('&package_ver_str;')
+ </programlisting></informalexample>
+
+ <para>
+ The "<literal>&package_api_version;</literal>" in the package name is the
+ "API version" (indicating "the version of the <application>
+ &package_string;</application> API that first appeared in version
+ &package_api_version;") and is essentially just part of the package name.
+ </para>
+ </refsect2>
+
+ <refsect2>
+ <title>Bundling the library</title>
+
+ <para>
+ As <application>&package_string;</application> uses the Meson build
+ system, bundling it as a subproject when it is not installed is easy.
+ Add this to your <literal>meson.build</literal>:
+ </para>
+
+ <informalexample><programlisting>
+ &package_string;_dep = dependency('&package_ver_str;', version: '>= &package_version;', required: false)
+ if not &package_string;_dep.found()
+ &package_string; = subproject(
+ '&package_string;',
+ install: false,
+ default_options: [
+ 'examples=false',
+ 'package_subdir=my-project-name',
+ 'tests=false',
+ ]
+ )
+ &package_string;_dep = &package_string;.get_variable('&package_string;_dep')
+ endif
+ </programlisting></informalexample>
+
+ <para>
+ Then add &package_string; as a git submodule:
+ </para>
+
+ <informalexample><programlisting>
+ git submodule add &package_url;.git subprojects/&package_string;
+ </programlisting></informalexample>
+
+ <para>
+ To bundle the library with your Flatpak application, add the following
+ module to your manifest:
+ </para>
+
+ <informalexample><programlisting>
+ {
+ "name" : "&package_string;",
+ "buildsystem" : "meson",
+ "builddir" : true,
+ "config-opts": [
+ "-Dexamples=false",
+ "-Dtests=false"
+ ],
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "&package_url;.git"
+ }
+ ]
+ }
+ </programlisting></informalexample>
+ </refsect2>
+
+ <refsect2>
+ <title>Building on macOS</title>
+
+ <para>
+ To build on macOS you need to install the build-dependencies first. This can e.g. be done via <ulink url="https://brew.sh"><literal>brew</literal></ulink>:
+ </para>
+
+ <informalexample>
+ <programlisting>
+ brew install pkg-config gtk+3 adwaita-icon-theme meson glade gobject-introspection vala
+ </programlisting>
+ </informalexample>
+
+ <para>
+ After running the command above, one may now build the library:
+ </para>
+
+ <informalexample>
+ <programlisting>
+ git clone https://gitlab.gnome.org/GNOME/libhandy.git
+ cd libhandy
+ meson . _build
+ ninja -C _build test
+ ninja -C _build install
+ </programlisting>
+ </informalexample>
+
+ <para>
+ Working with the library on macOS is pretty much the same as on Linux. To link it, use <literal>pkg-config</literal>:
+ </para>
+
+ <informalexample>
+ <programlisting>
+ gcc $(pkg-config --cflags --libs gtk+-3.0) $(pkg-config --cflags --libs libhandy-1) main.c -o main
+ </programlisting>
+ </informalexample>
+ </refsect2>
+</refentry>
diff --git a/subprojects/libhandy/doc/handy-docs.xml b/subprojects/libhandy/doc/handy-docs.xml
new file mode 100644
index 0000000..0bf7eab
--- /dev/null
+++ b/subprojects/libhandy/doc/handy-docs.xml
@@ -0,0 +1,140 @@
+<?xml version="1.0"?>
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd"
+[
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+ <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent">
+ %gtkdocentities;
+]>
+<book id="index">
+ <bookinfo>
+ <title>&package_name; Reference Manual</title>
+ <releaseinfo>
+ <para>This document is the API reference for &package_name; &package_version;.</para>
+ <para>
+ <ulink type="http" url="&package_url;">Handy</ulink> is a library to help you write apps for GTK/GNOME based mobile phones.
+ </para>
+ <para>
+ If you find any issues in this API reference, please report it using
+ <ulink type="http" url="&package_bugreport;">the bugtracker</ulink>.
+ </para>
+ </releaseinfo>
+
+ <copyright>
+ <year>2017-2020</year>
+ <holder>Purism SPC</holder>
+ </copyright>
+ </bookinfo>
+
+ <chapter id="intro">
+ <title>Introduction</title>
+
+ <xi:include href="build-howto.xml"/>
+ <xi:include href="visual-index.xml"/>
+ </chapter>
+
+ <chapter id="core-api">
+ <title>Widgets and Objects</title>
+ <xi:include href="xml/hdy-action-row.xml"/>
+ <xi:include href="xml/hdy-animation.xml"/>
+ <xi:include href="xml/hdy-application-window.xml"/>
+ <xi:include href="xml/hdy-avatar.xml"/>
+ <xi:include href="xml/hdy-carousel.xml"/>
+ <xi:include href="xml/hdy-carousel-indicator-dots.xml"/>
+ <xi:include href="xml/hdy-carousel-indicator-lines.xml"/>
+ <xi:include href="xml/hdy-clamp.xml"/>
+ <xi:include href="xml/hdy-combo-row.xml"/>
+ <xi:include href="xml/hdy-deck.xml"/>
+ <xi:include href="xml/hdy-enum-value-object.xml"/>
+ <xi:include href="xml/hdy-expander-row.xml"/>
+ <xi:include href="xml/hdy-header-bar.xml"/>
+ <xi:include href="xml/hdy-header-group.xml"/>
+ <xi:include href="xml/hdy-keypad.xml"/>
+ <xi:include href="xml/hdy-leaflet.xml"/>
+ <xi:include href="xml/hdy-navigation-direction.xml"/>
+ <xi:include href="xml/hdy-preferences-group.xml"/>
+ <xi:include href="xml/hdy-preferences-page.xml"/>
+ <xi:include href="xml/hdy-preferences-row.xml"/>
+ <xi:include href="xml/hdy-preferences-window.xml"/>
+ <xi:include href="xml/hdy-search-bar.xml"/>
+ <xi:include href="xml/hdy-squeezer.xml"/>
+ <xi:include href="xml/hdy-swipeable.xml"/>
+ <xi:include href="xml/hdy-swipe-group.xml"/>
+ <xi:include href="xml/hdy-swipe-tracker.xml"/>
+ <xi:include href="xml/hdy-title-bar.xml"/>
+ <xi:include href="xml/hdy-value-object.xml"/>
+ <xi:include href="xml/hdy-view-switcher.xml"/>
+ <xi:include href="xml/hdy-view-switcher-bar.xml"/>
+ <xi:include href="xml/hdy-view-switcher-title.xml"/>
+ <xi:include href="xml/hdy-window.xml"/>
+ <xi:include href="xml/hdy-window-handle.xml"/>
+ </chapter>
+
+ <chapter id="helpers">
+ <title>Helpers</title>
+ <xi:include href="xml/hdy-version.xml"/>
+ <xi:include href="xml/hdy-main.xml"/>
+ </chapter>
+
+ <chapter id="migrating">
+ <title>Migrating from Previous Versions of Handy</title>
+
+ <xi:include href="hdy-migrating-0-0-to-1.xml"/>
+ </chapter>
+
+ <chapter id="object-tree">
+ <title>Object Hierarchy</title>
+ <xi:include href="xml/tree_index.sgml"/>
+ </chapter>
+
+ <index id="api-index-full">
+ <title>API Index</title>
+ <xi:include href="xml/api-index-full.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="deprecated-api-index" role="deprecated">
+ <title>Index of deprecated API</title>
+ <xi:include href="xml/api-index-deprecated.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-6" role="0.0.6">
+ <title>Index of new symbols in 0.0.6</title>
+ <xi:include href="xml/api-index-0.0.6.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-7" role="0.0.7">
+ <title>Index of new symbols in 0.0.7</title>
+ <xi:include href="xml/api-index-0.0.7.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-8" role="0.0.8">
+ <title>Index of new symbols in 0.0.8</title>
+ <xi:include href="xml/api-index-0.0.8.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-10" role="0.0.10">
+ <title>Index of new symbols in 0.0.10</title>
+ <xi:include href="xml/api-index-0.0.10.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-11" role="0.0.11">
+ <title>Index of new symbols in 0.0.11</title>
+ <xi:include href="xml/api-index-0.0.11.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-0-0-12" role="0.0.12">
+ <title>Index of new symbols in 0.0.12</title>
+ <xi:include href="xml/api-index-0.0.12.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="api-index-1-0" role="1.0">
+ <title>Index of new symbols in 1.0</title>
+ <xi:include href="xml/api-index-1.0.xml"><xi:fallback /></xi:include>
+ </index>
+
+ <index id="annotations-glossary">
+ <title>Annotations glossary</title>
+ <xi:include href="xml/annotation-glossary.xml"><xi:fallback /></xi:include>
+ </index>
+
+</book>
diff --git a/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml b/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml
new file mode 100644
index 0000000..70b5e15
--- /dev/null
+++ b/subprojects/libhandy/doc/hdy-migrating-0-0-to-1.xml
@@ -0,0 +1,356 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+ <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent">
+ %gtkdocentities;
+]>
+
+<refentry id="hdy-migrating-0-0-to-1">
+ <refmeta>
+ <refentrytitle>Migrating from Handy 0.0.x to Handy 1</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+
+ <refnamediv>
+ <refname>Migrating from Handy 0.0.x to Handy 1</refname><refpurpose>Notes on migration to Handy 1.</refpurpose>
+ </refnamediv>
+
+ <para>
+ Handy 1 is a major new version of Handy that breaks both API and ABI
+ compared to Handy 0.0.x. Thankfully, most of the changes are not hard to
+ adapt to and there are a number of steps that you can take to prepare your
+ Handy 0.0.x application for the switch to Handy 1. After that, there's a
+ number of adjustments that you may have to do when you actually switch your
+ application to build against Handy 1.
+ </para>
+
+ <refsect2>
+ <title>Preparation in Handy 0.0.x</title>
+
+ <para>
+ The steps outlined in the following sections assume that your application
+ is working with Handy 0.0.13, which is the final stable release of Handy
+ 0.0.x. It includes all the necessary APIs and tools to help you port your
+ application to Handy 1. If you are using an older version of Handy 0.0.x,
+ you should first get your application to build and work with Handy 0.0.13.
+ </para>
+
+ <refsect3>
+ <title>Do not use the static build option</title>
+ <para>
+ Static linking support has been removed, and so did the static build
+ option.
+ You must adapt you program to link to the library dynamically, using
+ the package_subdir build option if needed.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Do not use deprecated symbols</title>
+ <para>
+ Over the years, a number of functions, and in some cases, entire widgets
+ have been deprecated. These deprecations are clearly spelled out in the
+ API reference, with hints about the recommended replacements.
+ The API reference for GTK 3 also includes an
+ <ulink url="https://developer.puri.sm/projects/libhandy/unstable/deprecated-api-index.html">index</ulink>
+ of all deprecated symbols.
+ </para>
+ </refsect3>
+
+ </refsect2>
+
+ <refsect2>
+ <title>Changes that need to be done at the time of the switch</title>
+
+ <para>
+ This section outlines porting tasks that you need to tackle when you get
+ to the point that you actually build your application against Handy 1.
+ Making it possible to prepare for these in Handy 0.0 would have been
+ either impossible or impractical.
+ </para>
+
+ <refsect3>
+ <title>hdy_init takes no parameters</title>
+ <para>
+ hdy_init() has been modified to take no parameters.
+ It must be called just after initializing GTK, if you are using
+ #GtkApplication it means it must be called when the
+ #GApplication::startup signal is emitted.
+ </para>
+ <para>
+ It initializes the localization, the types, the themes, and the icons.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to widget constructor changes</title>
+ <para>
+ All widget constructors now return the #GtkWidget type rather than the
+ constructed widget's type, following the same convention as GTK 3.
+ </para>
+ <para>
+ Affected widgets:
+ #HdyActionRow, #HdyComboRow, #HdyExpanderRow,
+ #HdyPreferencesGroup, #HdyPreferencesPage, #HdyPreferencesRow,
+ #HdyPreferencesWindow, #HdySqueezer, #HdyTitleBar, #HdyViewSwitcherBar,
+ #HdyViewSwitcher
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to derivability changes</title>
+ <para>
+ Some widgets are now final, if your code is deriving from them, use
+ composition instead.
+ </para>
+ <para>
+ Affected widgets:
+ #HdySqueezer, #HdyViewSwitcher, #HdyViewSwitcherBar
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>HdyFold has been removed</title>
+ <para>
+ #HdyFold has been removed. This affects the API of #HdyLeaflet, see the
+ “Adapt to HdyLeaflet API changes” section to know how.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Replace HdyColumn by HdyClamp</title>
+ <para>
+ HdyColumn has been renamed #HdyClamp as it now implements
+ #GtkOrientable, so you should replace the former by the later.
+ Its “maximum-width” and “linear-growth-width” properties have been
+ renamed #HdyClamp:maximum-size and #HdyClamp:tightening-threshold
+ respectively to better reflect their role.
+ It won't set the .narrow, .medium and .wide style classes depending on
+ its size, but the .small, .medium and .large ones instead.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyPaginator API changes</title>
+ <para>
+ HdyPaginator has been renamed HdyCarousel, so you should replace the
+ former by the later.
+ </para>
+ <para>
+ The “indicator-style”, “indicator-spacing” and “center-content”
+ properties have been removed, instead use #HdyCarouselIndicatorDots
+ or #HdyCarouselIndicatorLines widgets.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyHeaderGroup API changes</title>
+ <para>
+ The #HdyHeaderGroup object has been largely redesigned, most of its
+ methods changed, see its documentation to know more.
+ </para>
+ <para>
+ The child type is now #HdyHeaderGroupChild, which can represent either
+ a #GtkHeaderBar, a #HdyHeaderBar, or a #HdyHeaderGroup.
+ </para>
+ <para>
+ The “focus” property has been replaced by #HdyHeaderGroup:decorate-all,
+ which works quite differently.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyLeaflet API changes</title>
+ <para>
+ The #HdyFold type has been removed in favor of using a boolean, and
+ #HdyLeaflet adjusted to that as the #HdyLeaflet:fold property has been
+ removed in favor of #HdyLeaflet:folded.
+ Also, the hdy_leaflet_get_homogeneous() and
+ hdy_leaflet_set_homogeneous() accessors take a boolean parameter instead
+ of a #HdyFold.
+ </para>
+ <para>
+ On touchscreens, swiping forward with the “over” transition and swiping
+ back with the “under” transition can now only be done from the edge
+ where the upper child is.
+ </para>
+ <para>
+ The “over” and “under” transitions can draw their shadow on top of the
+ window's transparent areas, like the rounded corners.
+ This is a side-effect of allowing shadows to be drawn on top of OpenGL
+ areas.
+ It can be mitigated by using #HdyWindow or #HdyApplicationWindow as they
+ will crop anything drawn beyond the rounded corners.
+ </para>
+ <para>
+ The “allow-visible” child property has been renamed “navigatable”.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyLeaflet API changes</title>
+ <para>
+ The “none” transition type has been removed. The default value for the
+ #HdyLeaflet:transition-type property has been changed to “over”.
+ “over” is the recommended transition for typical #HdyLeaflet use-cases,
+ if this isn't what you want to use, be sure to adapt your code. If
+ transitions are undesired, set #HdyLeaflet:mode-transition-duration and
+ #HdyLeaflet:child-transition-duration properties to 0.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyViewSwitcher API changes</title>
+ <para>
+ #HdyViewSwitcher doesn't subclass #GtkBox anymore. Instead, it
+ subclasses #GtkBin and contains a box.
+ </para>
+ <para>
+ The “icon-size” property has been dropped without replacement, you must
+ stop using it.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyViewSwitcherBar API changes</title>
+ <para>
+ #HdyViewSwitcherBar won't be revealed if the #HdyViewSwitcherBar:stack
+ property is %NULL or if it has less than two pages, even if you set
+ #HdyViewSwitcherBar:reveal to %TRUE.
+ </para>
+ <para>
+ The “icon-size” property has been dropped without replacement, you must
+ stop using it.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to CSS node name changes</title>
+ <para>
+ Widgets with a customn CSS node name got their name changed to be the
+ class' name in lowercase, with no separation between words, and with no
+ namespace prefix. E.g. the CSS node name of HdyViewSwitcher is
+ viewswitcher.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyActionRow API changes</title>
+ <para>
+ Action items were packed from the end toward the start of the row. It is
+ now reversed, and widgets have to be packed from the start to the end.
+ </para>
+ <para>
+ It isn't possible to add children at the bottom of a #HdyActionRow
+ anymore, instead use other widgets like #HdyExpanderRow.
+ Widgets added to a #HdyActionRow will now be added at the end of the
+ row, and the hdy_action_row_add_action() method and the action child
+ type have been removed.
+ </para>
+ <para>
+ The main horizontal box of #HdyActionRow had the row-header CSS style
+ class, it now has the header CSS style class and can hence be accessed
+ as box.header subnode.
+ </para>
+ <para>
+ #HdyActionRow is now unactivatable by default, giving it an activatable
+ widget will automatically make it activatable.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyComboRow API changes</title>
+ <para>
+ #HdyComboRow is now unactivatable by default, binding and unbinding a
+ model will toggle its activatability.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyExpanderRow API changes</title>
+ <para>
+ #HdyExpanderRow doesn't descend from #HdyActionRow anymore but from
+ #HdyPreferencesRow.
+ It reimplement some features from #HdyActionRow, like the
+ #HdyExpanderRow:title, #HdyExpanderRow:subtitle,
+ #HdyExpanderRow:use-underline and #HdyExpanderRow:icon-name, but it
+ doesn't offer the “activate” signal nor the ability to add widgets in
+ its header row.
+ </para>
+ <para>
+ Widgets you add to it will be added to its inner #GtkListBox.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyPreferencesPage API changes</title>
+ <para>
+ #HdyPreferencesPage doesn't subclass #GtkScrolledWindow anymore.
+ Instead, it subclasses #GtkBin and contains a scrolled window.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyPreferencesGroup API changes</title>
+ <para>
+ #HdyPreferencesGroup doesn't subclass #GtkBox anymore.
+ Instead, it subclasses #GtkBin and contains a box.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Adapt to HdyKeypad API changes</title>
+ <para>
+ #HdyKeypad doesn't subclass #Gtkgrid anymore. Instead, it subclasses
+ #GtkBin and contains a grid.
+ </para>
+ <para>
+ The “show-symbols” property has been replaced by
+ #HdyHeaderGroup:letters-visible.
+ </para>
+ <para>
+ The “only-digits” property has been replaced by
+ #HdyHeaderGroup:symbols-visible, which has a inverse boolean meaning.
+ This also affects the corresponding parameter of the constructor.
+ </para>
+ <para>
+ The “left-action” property has been replaced by
+ #HdyHeaderGroup:start-action, and the “right-action” property has been
+ replaced by #HdyHeaderGroup:end-action.
+ </para>
+ <para>
+ The “entry” property isn't a #GtkWidget anymore but a #GtkEntry.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Stop using hdy_list_box_separator_header()</title>
+ <para>
+ Instead, either use CSS styling (the list.content style class may
+ fit your need), or implement it yourself as it is trivial.
+ </para>
+ </refsect3>
+
+ <refsect3>
+ <title>Stop acknowledging the Instability</title>
+ <para>
+ When the library was young and changing a lot, we required you to
+ acknowledge that your are using an unstable API. To do so, you had to
+ define <literal>HANDY_USE_UNSTABLE_API</literal> for compilation to
+ succeed.
+ </para>
+ <para>
+ The API remained stable since many versions, despite this acknowlegment
+ still being required. To reflect that proven stability, the
+ acknowlegment isn't necessary and you can stop defining
+ <literal>HANDY_USE_UNSTABLE_API</literal>, either before including the
+ &package_string; header in C-compatible languages, or with the
+ definition option of your compiler.
+ </para>
+ </refsect3>
+
+ </refsect2>
+
+</refentry>
+
diff --git a/subprojects/libhandy/doc/images/avatar.png b/subprojects/libhandy/doc/images/avatar.png
new file mode 100644
index 0000000..07a156f
--- /dev/null
+++ b/subprojects/libhandy/doc/images/avatar.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/header-bar.png b/subprojects/libhandy/doc/images/header-bar.png
new file mode 100644
index 0000000..c15b1f6
--- /dev/null
+++ b/subprojects/libhandy/doc/images/header-bar.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/keypad.png b/subprojects/libhandy/doc/images/keypad.png
new file mode 100644
index 0000000..6bc18d5
--- /dev/null
+++ b/subprojects/libhandy/doc/images/keypad.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/list.png b/subprojects/libhandy/doc/images/list.png
new file mode 100644
index 0000000..e02833c
--- /dev/null
+++ b/subprojects/libhandy/doc/images/list.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/preferences-window.png b/subprojects/libhandy/doc/images/preferences-window.png
new file mode 100644
index 0000000..bd696a7
--- /dev/null
+++ b/subprojects/libhandy/doc/images/preferences-window.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/search.png b/subprojects/libhandy/doc/images/search.png
new file mode 100644
index 0000000..38d14ef
--- /dev/null
+++ b/subprojects/libhandy/doc/images/search.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/view-switcher-bar.png b/subprojects/libhandy/doc/images/view-switcher-bar.png
new file mode 100644
index 0000000..3a0836d
--- /dev/null
+++ b/subprojects/libhandy/doc/images/view-switcher-bar.png
Binary files differ
diff --git a/subprojects/libhandy/doc/images/view-switcher.png b/subprojects/libhandy/doc/images/view-switcher.png
new file mode 100644
index 0000000..b8727df
--- /dev/null
+++ b/subprojects/libhandy/doc/images/view-switcher.png
Binary files differ
diff --git a/subprojects/libhandy/doc/meson.build b/subprojects/libhandy/doc/meson.build
new file mode 100644
index 0000000..eeab57a
--- /dev/null
+++ b/subprojects/libhandy/doc/meson.build
@@ -0,0 +1,75 @@
+if get_option('gtk_doc')
+
+subdir('xml')
+
+private_headers = [
+ 'config.h',
+ 'gtkprogresstrackerprivate.h',
+ 'gtk-window-private.h',
+ 'hdy-animation-private.h',
+ 'hdy-carousel-box-private.h',
+ 'hdy-css-private.h',
+ 'hdy-enums.h',
+ 'hdy-enums-private.h',
+ 'hdy-main-private.h',
+ 'hdy-nothing-private.h',
+ 'hdy-keypad-button-private.h',
+ 'hdy-preferences-group-private.h',
+ 'hdy-preferences-page-private.h',
+ 'hdy-shadow-helper-private.h',
+ 'hdy-stackable-box-private.h',
+ 'hdy-swipe-tracker-private.h',
+ 'hdy-types.h',
+ 'hdy-view-switcher-button-private.h',
+ 'hdy-window-handle-controller-private.h',
+ 'hdy-window-mixin-private.h',
+]
+
+images = [
+ 'images/avatar.png',
+ 'images/header-bar.png',
+ 'images/keypad.png',
+ 'images/list.png',
+ 'images/preferences-window.png',
+ 'images/search.png',
+ 'images/view-switcher.png',
+ 'images/view-switcher-bar.png',
+]
+
+content_files = [
+ 'build-howto.xml',
+ 'hdy-migrating-0-0-to-1.xml',
+ 'visual-index.xml',
+]
+
+glib_prefix = dependency('glib-2.0').get_pkgconfig_variable('prefix')
+glib_docpath = glib_prefix / 'share' / 'gtk-doc' / 'html'
+docpath = get_option('datadir') / 'gtk-doc' / 'html'
+
+gnome.gtkdoc('libhandy',
+ main_xml: 'handy-docs.xml',
+ src_dir: [
+ meson.source_root() / 'src',
+ meson.build_root() / 'src',
+ ],
+ dependencies: libhandy_dep,
+ gobject_typesfile: 'libhandy.types',
+ scan_args: [
+ '--rebuild-types',
+ '--ignore-headers=' + ' '.join(private_headers),
+ ],
+ fixxref_args: [
+ '--html-dir=@0@'.format(docpath),
+ '--extra-dir=@0@'.format(glib_docpath / 'glib'),
+ '--extra-dir=@0@'.format(glib_docpath / 'gobject'),
+ '--extra-dir=@0@'.format(glib_docpath / 'gio'),
+ '--extra-dir=@0@'.format(glib_docpath / 'gi'),
+ '--extra-dir=@0@'.format(glib_docpath / 'gtk3'),
+ '--extra-dir=@0@'.format(glib_docpath / 'gdk-pixbuf'),
+ ],
+ install_dir: 'libhandy-' + apiversion,
+ content_files: content_files,
+ html_assets: images,
+ install: true)
+
+endif
diff --git a/subprojects/libhandy/doc/visual-index.xml b/subprojects/libhandy/doc/visual-index.xml
new file mode 100644
index 0000000..1648608
--- /dev/null
+++ b/subprojects/libhandy/doc/visual-index.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.3//EN"
+ "http://www.oasis-open.org/docbook/xml/4.3/docbookx.dtd" [
+ <!ENTITY % local.common.attrib "xmlns:xi CDATA #FIXED 'http://www.w3.org/2003/XInclude'">
+ <!ENTITY % gtkdocentities SYSTEM "xml/gtkdocentities.ent">
+ %gtkdocentities;
+]>
+
+<refentry id="visual-index">
+ <refmeta>
+ <refentrytitle>Visual index</refentrytitle>
+ <manvolnum>3</manvolnum>
+ </refmeta>
+ <refnamediv>
+ <refname>Widgets in &package_string;</refname><refpurpose>Widget overview.</refpurpose>
+ </refnamediv>
+
+ <refsect2>
+ <title>Widgets</title>
+ <para role="gallery">
+ <link linkend="HdyAvatar">
+ <inlinegraphic fileref="avatar.png" format="PNG"></inlinegraphic>
+ </link>
+ </para>
+ <para role="gallery">
+ <link linkend="HdyKeypad">
+ <inlinegraphic fileref="keypad.png" format="PNG"></inlinegraphic>
+ </link>
+ </para>
+ <para role="gallery">
+ <link>
+ <inlinegraphic fileref="list.png" format="PNG" scale="60"></inlinegraphic>
+ </link>
+ <link linkend="HdySearchBar">
+ <inlinegraphic fileref="search.png" format="PNG"></inlinegraphic>
+ </link>
+ </para>
+ <para role="gallery">
+ <link linkend="HdyHeaderBar">
+ <inlinegraphic fileref="header-bar.png" format="PNG"></inlinegraphic>
+ </link>
+ <link linkend="HdyPreferencesWindow">
+ <inlinegraphic fileref="preferences-window.png" format="PNG"></inlinegraphic>
+ </link>
+ </para>
+ </refsect2>
+ <refsect2>
+ <title>HdyViewSwitcher</title>
+ <para role="gallery">
+ <link linkend="HdyViewSwitcher">
+ <inlinegraphic fileref="view-switcher.png" format="PNG"></inlinegraphic>
+ </link>
+ <link linkend="HdyViewSwitcherBar">
+ <inlinegraphic fileref="view-switcher-bar.png" format="PNG"></inlinegraphic>
+ </link>
+ </para>
+ </refsect2>
+</refentry>
diff --git a/subprojects/libhandy/doc/xml/gtkdocentities.ent.in b/subprojects/libhandy/doc/xml/gtkdocentities.ent.in
new file mode 100644
index 0000000..45d322c
--- /dev/null
+++ b/subprojects/libhandy/doc/xml/gtkdocentities.ent.in
@@ -0,0 +1,9 @@
+<!ENTITY package "@PACKAGE@">
+<!ENTITY package_bugreport "@PACKAGE_BUGREPORT@">
+<!ENTITY package_name "@PACKAGE_NAME@">
+<!ENTITY package_string "@PACKAGE_STRING@">
+<!ENTITY package_tarname "@PACKAGE_TARNAME@">
+<!ENTITY package_url "@PACKAGE_URL@">
+<!ENTITY package_version "@PACKAGE_VERSION@">
+<!ENTITY package_api_version "@PACKAGE_API_VERSION@">
+<!ENTITY package_ver_str "@PACKAGE_API_NAME@">
diff --git a/subprojects/libhandy/doc/xml/meson.build b/subprojects/libhandy/doc/xml/meson.build
new file mode 100644
index 0000000..5f73097
--- /dev/null
+++ b/subprojects/libhandy/doc/xml/meson.build
@@ -0,0 +1,12 @@
+ent_conf = configuration_data()
+ent_conf.set('PACKAGE', 'Handy')
+ent_conf.set('PACKAGE_BUGREPORT', 'https://gitlab.gnome.org/GNOME/libhandy/issues')
+ent_conf.set('PACKAGE_NAME', 'Handy')
+ent_conf.set('PACKAGE_STRING', 'libhandy')
+ent_conf.set('PACKAGE_TARNAME', 'libhandy-' + meson.project_version())
+ent_conf.set('PACKAGE_URL', 'https://gitlab.gnome.org/GNOME/libhandy')
+ent_conf.set('PACKAGE_VERSION', meson.project_version())
+ent_conf.set('PACKAGE_API_VERSION', apiversion)
+ent_conf.set('PACKAGE_API_NAME', package_api_name)
+configure_file(input: 'gtkdocentities.ent.in', output: 'gtkdocentities.ent', configuration: ent_conf)
+
diff --git a/subprojects/libhandy/examples/example.py b/subprojects/libhandy/examples/example.py
new file mode 100755
index 0000000..2a6f27a
--- /dev/null
+++ b/subprojects/libhandy/examples/example.py
@@ -0,0 +1,32 @@
+#!/usr/bin/python3
+
+import gi
+
+gi.require_version('Gtk', '3.0')
+from gi.repository import Gtk
+gi.require_version('Handy', '1')
+from gi.repository import Handy
+import sys
+
+
+window = Gtk.Window(title = "Keypad Example with Python")
+vbox = Gtk.Box(orientation = Gtk.Orientation.VERTICAL)
+entry = Gtk.Entry()
+keypad = Handy.Keypad()
+
+vbox.add(entry) # widget to show dialed number
+vbox.add(keypad)
+vbox.set_halign(Gtk.Align.CENTER)
+vbox.set_valign(Gtk.Align.CENTER)
+
+vbox.props.margin = 18
+vbox.props.spacing = 18
+keypad.set_row_spacing(6)
+keypad.set_column_spacing(6)
+
+keypad.set_entry(entry) # attach the entry widget
+
+window.connect("destroy", Gtk.main_quit)
+window.add(vbox)
+window.show_all()
+Gtk.main()
diff --git a/subprojects/libhandy/examples/handy-demo.c b/subprojects/libhandy/examples/handy-demo.c
new file mode 100644
index 0000000..052ce4c
--- /dev/null
+++ b/subprojects/libhandy/examples/handy-demo.c
@@ -0,0 +1,65 @@
+#include <gtk/gtk.h>
+#include <handy.h>
+
+#include "hdy-demo-preferences-window.h"
+#include "hdy-demo-window.h"
+
+static void
+show_preferences (GSimpleAction *action,
+ GVariant *state,
+ gpointer user_data)
+{
+ GtkApplication *app = GTK_APPLICATION (user_data);
+ GtkWindow *window = gtk_application_get_active_window (app);
+ HdyDemoPreferencesWindow *preferences = hdy_demo_preferences_window_new ();
+
+ gtk_window_set_transient_for (GTK_WINDOW (preferences), window);
+ gtk_widget_show (GTK_WIDGET (preferences));
+}
+
+static void
+startup (GtkApplication *app)
+{
+ GtkCssProvider *css_provider = gtk_css_provider_new ();
+
+ hdy_init ();
+
+ gtk_css_provider_load_from_resource (css_provider, "/sm/puri/Handy/Demo/ui/style.css");
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
+
+ g_object_unref (css_provider);
+}
+
+static void
+show_window (GtkApplication *app)
+{
+ HdyDemoWindow *window;
+
+ window = hdy_demo_window_new (app);
+
+ gtk_widget_show (GTK_WIDGET (window));
+}
+
+int
+main (int argc,
+ char **argv)
+{
+ GtkApplication *app;
+ int status;
+ static GActionEntry app_entries[] = {
+ { "preferences", show_preferences, NULL, NULL, NULL },
+ };
+
+ app = gtk_application_new ("sm.puri.Handy.Demo", G_APPLICATION_FLAGS_NONE);
+ g_action_map_add_action_entries (G_ACTION_MAP (app),
+ app_entries, G_N_ELEMENTS (app_entries),
+ app);
+ g_signal_connect (app, "startup", G_CALLBACK (startup), NULL);
+ g_signal_connect (app, "activate", G_CALLBACK (show_window), NULL);
+ status = g_application_run (G_APPLICATION (app), argc, argv);
+ g_object_unref (app);
+
+ return status;
+}
diff --git a/subprojects/libhandy/examples/handy-demo.gresources.xml b/subprojects/libhandy/examples/handy-demo.gresources.xml
new file mode 100644
index 0000000..e0825c1
--- /dev/null
+++ b/subprojects/libhandy/examples/handy-demo.gresources.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/sm/puri/Handy/Demo">
+ <file preprocess="xml-stripblanks">icons/dark-mode-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg</file>
+ <file preprocess="xml-stripblanks">icons/gesture-touchscreen-swipe-back-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/gesture-touchpad-swipe-back-symbolic-rtl.svg</file>
+ <file preprocess="xml-stripblanks">icons/gesture-touchpad-swipe-back-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/gnome-smartphone-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/light-mode-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-carousel-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-clamp-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-deck-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-keypad-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-leaflet-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-list-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-search-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-view-switcher-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/widget-window-symbolic.svg</file>
+ </gresource>
+ <gresource prefix="/sm/puri/Handy/Demo/ui">
+ <file preprocess="xml-stripblanks">hdy-demo-preferences-window.ui</file>
+ <file preprocess="xml-stripblanks">hdy-demo-window.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-demo-window.ui</file>
+ <file compressed="true">style.css</file>
+ </gresource>
+</gresources>
diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.c b/subprojects/libhandy/examples/hdy-demo-preferences-window.c
new file mode 100644
index 0000000..4481664
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.c
@@ -0,0 +1,56 @@
+#include "hdy-demo-preferences-window.h"
+
+struct _HdyDemoPreferencesWindow
+{
+ HdyPreferencesWindow parent_instance;
+
+ GtkWidget *subpage1;
+ GtkWidget *subpage2;
+};
+
+G_DEFINE_TYPE (HdyDemoPreferencesWindow, hdy_demo_preferences_window, HDY_TYPE_PREFERENCES_WINDOW)
+
+HdyDemoPreferencesWindow *
+hdy_demo_preferences_window_new (void)
+{
+ return g_object_new (HDY_TYPE_DEMO_PREFERENCES_WINDOW, NULL);
+}
+
+static void
+return_to_preferences_cb (HdyDemoPreferencesWindow *self)
+{
+ hdy_preferences_window_close_subpage (HDY_PREFERENCES_WINDOW (self));
+}
+
+static void
+subpage1_activated_cb (HdyDemoPreferencesWindow *self)
+{
+ hdy_preferences_window_present_subpage (HDY_PREFERENCES_WINDOW (self), self->subpage1);
+}
+
+static void
+subpage2_activated_cb (HdyDemoPreferencesWindow *self)
+{
+ hdy_preferences_window_present_subpage (HDY_PREFERENCES_WINDOW (self), self->subpage2);
+}
+
+static void
+hdy_demo_preferences_window_class_init (HdyDemoPreferencesWindowClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-demo-preferences-window.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoPreferencesWindow, subpage1);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoPreferencesWindow, subpage2);
+
+ gtk_widget_class_bind_template_callback (widget_class, return_to_preferences_cb);
+ gtk_widget_class_bind_template_callback (widget_class, subpage1_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, subpage2_activated_cb);
+}
+
+static void
+hdy_demo_preferences_window_init (HdyDemoPreferencesWindow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.h b/subprojects/libhandy/examples/hdy-demo-preferences-window.h
new file mode 100644
index 0000000..accb7b5
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_DEMO_PREFERENCES_WINDOW (hdy_demo_preferences_window_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyDemoPreferencesWindow, hdy_demo_preferences_window, HDY, DEMO_PREFERENCES_WINDOW, HdyPreferencesWindow)
+
+HdyDemoPreferencesWindow *hdy_demo_preferences_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/examples/hdy-demo-preferences-window.ui b/subprojects/libhandy/examples/hdy-demo-preferences-window.ui
new file mode 100644
index 0000000..83c83a9
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-preferences-window.ui
@@ -0,0 +1,256 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyDemoPreferencesWindow" parent="HdyPreferencesWindow">
+ <property name="can-swipe-back">True</property>
+ <child>
+ <object class="HdyPreferencesPage">
+ <property name="icon_name">edit-select-all-symbolic</property>
+ <property name="title">Layout</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyPreferencesRow">
+ <property name="title" bind-source="welcome_label" bind-property="label" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="welcome_label">
+ <property name="ellipsize">end</property>
+ <property name="label" translatable="yes">This is a preferences window</property>
+ <property name="margin">12</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="description" translatable="yes">Preferences are organized in pages, this example has the following pages:</property>
+ <property name="title" translatable="yes">Pages</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Layout</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Search</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="description" translatable="yes">Preferences are grouped together, a group can have a tile and a description. Descriptions will be wrapped if they are too long. This page has the following groups:</property>
+ <property name="title" translatable="yes">Groups</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">An untitled group</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Pages</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Groups</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Preferences</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="title" translatable="yes">Preferences</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Preferences rows are appended to the list box</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <style>
+ <class name="inline-toolbar"/>
+ </style>
+ <child>
+ <object class="GtkLabel">
+ <property name="ellipsize">end</property>
+ <property name="label" translatable="yes">Other widgets are appended after the list box</property>
+ <property name="margin">12</property>
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="description" translatable="yes">Preferences windows can have subpages.</property>
+ <property name="title" translatable="yes">Subpages</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Go to a subpage</property>
+ <property name="visible">True</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="subpage1_activated_cb" swapped="yes"/>
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Go to another subpage</property>
+ <property name="visible">True</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="subpage2_activated_cb" swapped="yes"/>
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesPage">
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="title">Search</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyPreferencesGroup">
+ <property name="description">Preferences can be searched, do so using one of the following ways:</property>
+ <property name="title">Searching</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Activate the search button</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyPreferencesRow">
+ <property name="title" translatable="yes">Ctrl + F</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkShortcutLabel">
+ <property name="accelerator">&lt;ctrl&gt;f</property>
+ <property name="margin">12</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Directly type your search</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkBox" id="subpage1">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This is a subpage</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="opacity">0.5</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Return to the preferences</property>
+ <signal name="clicked" handler="return_to_preferences_cb" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <object class="GtkBox" id="subpage2">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This is another subpage</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="opacity">0.5</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Return to the preferences</property>
+ <signal name="clicked" handler="return_to_preferences_cb" swapped="yes"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/examples/hdy-demo-window.c b/subprojects/libhandy/examples/hdy-demo-window.c
new file mode 100644
index 0000000..55c872f
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-window.c
@@ -0,0 +1,573 @@
+#include "hdy-demo-window.h"
+
+#include <glib/gi18n.h>
+#include "hdy-view-switcher-demo-window.h"
+
+struct _HdyDemoWindow
+{
+ HdyApplicationWindow parent_instance;
+
+ HdyLeaflet *content_box;
+ GtkStack *header_revealer;
+ GtkStack *header_stack;
+ GtkImage *theme_variant_image;
+ GtkStackSidebar *sidebar;
+ GtkStack *stack;
+ HdyComboRow *leaflet_transition_row;
+ HdyDeck *content_deck;
+ HdyComboRow *deck_transition_row;
+ GtkWidget *box_keypad;
+ GtkListBox *keypad_listbox;
+ HdyKeypad *keypad;
+ HdySearchBar *search_bar;
+ GtkEntry *search_entry;
+ GtkListBox *lists_listbox;
+ HdyComboRow *combo_row;
+ HdyComboRow *enum_combo_row;
+ HdyCarousel *carousel;
+ GtkBox *carousel_box;
+ GtkListBox *carousel_listbox;
+ GtkStack *carousel_indicators_stack;
+ HdyComboRow *carousel_orientation_row;
+ HdyComboRow *carousel_indicators_row;
+ GListStore *carousel_indicators_model;
+ HdyAvatar *avatar;
+ GtkEntry *avatar_text;
+ GtkFileChooserButton *avatar_filechooser;
+ GtkListBox *avatar_contacts;
+};
+
+G_DEFINE_TYPE (HdyDemoWindow, hdy_demo_window, HDY_TYPE_APPLICATION_WINDOW)
+
+static void
+theme_variant_button_clicked_cb (HdyDemoWindow *self)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+ gboolean prefer_dark_theme;
+
+ g_object_get (settings, "gtk-application-prefer-dark-theme", &prefer_dark_theme, NULL);
+ g_object_set (settings, "gtk-application-prefer-dark-theme", !prefer_dark_theme, NULL);
+}
+
+static gboolean
+prefer_dark_theme_to_icon_name_cb (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ g_value_set_string (to_value,
+ g_value_get_boolean (from_value) ? "light-mode-symbolic" :
+ "dark-mode-symbolic");
+
+ return TRUE;
+}
+
+static gboolean
+hdy_demo_window_key_pressed_cb (GtkWidget *sender,
+ GdkEvent *event,
+ HdyDemoWindow *self)
+{
+ GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask ();
+ guint keyval;
+ GdkModifierType state;
+
+ gdk_event_get_keyval (event, &keyval);
+ gdk_event_get_state (event, &state);
+
+ if ((keyval == GDK_KEY_q || keyval == GDK_KEY_Q) &&
+ (state & default_modifiers) == GDK_CONTROL_MASK) {
+ gtk_widget_destroy (GTK_WIDGET (self));
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+update (HdyDemoWindow *self)
+{
+ const gchar *header_bar_name = "default";
+
+ if (g_strcmp0 (gtk_stack_get_visible_child_name (self->stack), "deck") == 0)
+ header_bar_name = "deck";
+ else if (g_strcmp0 (gtk_stack_get_visible_child_name (self->stack), "search-bar") == 0)
+ header_bar_name = "search-bar";
+
+ gtk_stack_set_visible_child_name (self->header_stack, header_bar_name);
+}
+
+static void
+hdy_demo_window_notify_deck_visible_child_cb (HdyDemoWindow *self)
+{
+ update (self);
+}
+
+static void
+hdy_demo_window_notify_visible_child_cb (GObject *sender,
+ GParamSpec *pspec,
+ HdyDemoWindow *self)
+{
+ update (self);
+
+ hdy_leaflet_navigate (self->content_box, HDY_NAVIGATION_DIRECTION_FORWARD);
+}
+
+static void
+hdy_demo_window_back_clicked_cb (GtkWidget *sender,
+ HdyDemoWindow *self)
+{
+ hdy_leaflet_navigate (self->content_box, HDY_NAVIGATION_DIRECTION_BACK);
+}
+
+static void
+hdy_demo_window_deck_back_clicked_cb (GtkWidget *sender,
+ HdyDemoWindow *self)
+{
+ hdy_deck_navigate (self->content_deck, HDY_NAVIGATION_DIRECTION_BACK);
+}
+
+static gchar *
+leaflet_transition_name (HdyEnumValueObject *value,
+ gpointer user_data)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL);
+
+ switch (hdy_enum_value_object_get_value (value)) {
+ case HDY_LEAFLET_TRANSITION_TYPE_OVER:
+ return g_strdup (_("Over"));
+ case HDY_LEAFLET_TRANSITION_TYPE_UNDER:
+ return g_strdup (_("Under"));
+ case HDY_LEAFLET_TRANSITION_TYPE_SLIDE:
+ return g_strdup (_("Slide"));
+ default:
+ return NULL;
+ }
+}
+
+static void
+notify_leaflet_transition_cb (GObject *sender,
+ GParamSpec *pspec,
+ HdyDemoWindow *self)
+{
+ HdyComboRow *row = HDY_COMBO_ROW (sender);
+
+ g_assert (HDY_IS_COMBO_ROW (row));
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ hdy_leaflet_set_transition_type (HDY_LEAFLET (self->content_box), hdy_combo_row_get_selected_index (row));
+}
+
+static gchar *
+deck_transition_name (HdyEnumValueObject *value,
+ gpointer user_data)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL);
+
+ switch (hdy_enum_value_object_get_value (value)) {
+ case HDY_DECK_TRANSITION_TYPE_OVER:
+ return g_strdup (_("Over"));
+ case HDY_DECK_TRANSITION_TYPE_UNDER:
+ return g_strdup (_("Under"));
+ case HDY_DECK_TRANSITION_TYPE_SLIDE:
+ return g_strdup (_("Slide"));
+ default:
+ return NULL;
+ }
+}
+
+static void
+notify_deck_transition_cb (GObject *sender,
+ GParamSpec *pspec,
+ HdyDemoWindow *self)
+{
+ HdyComboRow *row = HDY_COMBO_ROW (sender);
+
+ g_assert (HDY_IS_COMBO_ROW (row));
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ hdy_deck_set_transition_type (HDY_DECK (self->content_deck), hdy_combo_row_get_selected_index (row));
+}
+
+static void
+deck_go_next_row_activated_cb (HdyDemoWindow *self)
+{
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ hdy_deck_navigate (self->content_deck, HDY_NAVIGATION_DIRECTION_FORWARD);
+}
+
+static void
+view_switcher_demo_clicked_cb (GtkButton *btn,
+ HdyDemoWindow *self)
+{
+ HdyViewSwitcherDemoWindow *window = hdy_view_switcher_demo_window_new ();
+
+ gtk_window_set_transient_for (GTK_WINDOW (window), GTK_WINDOW (self));
+ gtk_widget_show (GTK_WIDGET (window));
+}
+
+static gchar *
+carousel_orientation_name (HdyEnumValueObject *value,
+ gpointer user_data)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL);
+
+ switch (hdy_enum_value_object_get_value (value)) {
+ case GTK_ORIENTATION_HORIZONTAL:
+ return g_strdup (_("Horizontal"));
+ case GTK_ORIENTATION_VERTICAL:
+ return g_strdup (_("Vertical"));
+ default:
+ return NULL;
+ }
+}
+
+static void
+notify_carousel_orientation_cb (GObject *sender,
+ GParamSpec *pspec,
+ HdyDemoWindow *self)
+{
+ HdyComboRow *row = HDY_COMBO_ROW (sender);
+
+ g_assert (HDY_IS_COMBO_ROW (row));
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (self->carousel_box),
+ 1 - hdy_combo_row_get_selected_index (row));
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (self->carousel),
+ hdy_combo_row_get_selected_index (row));
+}
+
+static gchar *
+carousel_indicators_name (HdyValueObject *value)
+{
+ const gchar *style;
+
+ g_assert (HDY_IS_VALUE_OBJECT (value));
+
+ style = hdy_value_object_get_string (value);
+
+ if (!g_strcmp0 (style, "dots"))
+ return g_strdup (_("Dots"));
+
+ if (!g_strcmp0 (style, "lines"))
+ return g_strdup (_("Lines"));
+
+ return NULL;
+}
+
+static void
+notify_carousel_indicators_cb (GObject *sender,
+ GParamSpec *pspec,
+ HdyDemoWindow *self)
+{
+ HdyComboRow *row = HDY_COMBO_ROW (sender);
+ HdyValueObject *obj;
+
+ g_assert (HDY_IS_COMBO_ROW (row));
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ obj = g_list_model_get_item (G_LIST_MODEL (self->carousel_indicators_model),
+ hdy_combo_row_get_selected_index (row));
+
+ gtk_stack_set_visible_child_name (self->carousel_indicators_stack,
+ hdy_value_object_get_string (obj));
+}
+
+static void
+carousel_return_clicked_cb (GtkButton *btn,
+ HdyDemoWindow *self)
+{
+ g_autoptr (GList) children =
+ gtk_container_get_children (GTK_CONTAINER (self->carousel));
+
+ hdy_carousel_scroll_to (self->carousel, GTK_WIDGET (children->data));
+}
+
+HdyDemoWindow *
+hdy_demo_window_new (GtkApplication *application)
+{
+ return g_object_new (HDY_TYPE_DEMO_WINDOW, "application", application, NULL);
+}
+
+static void
+avatar_file_remove_cb (HdyDemoWindow *self)
+{
+ g_autofree gchar *file = NULL;
+
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ g_signal_handlers_disconnect_by_data (self->avatar, self);
+ file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->avatar_filechooser));
+ if (file)
+ gtk_file_chooser_unselect_filename (GTK_FILE_CHOOSER (self->avatar_filechooser), file);
+ hdy_avatar_set_image_load_func (self->avatar, NULL, NULL, NULL);
+}
+
+static GdkPixbuf *
+avatar_load_file (gint size, HdyDemoWindow *self)
+{
+ g_autoptr (GError) error = NULL;
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ g_autofree gchar *file = NULL;
+ gint width, height;
+
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (self->avatar_filechooser));
+
+ gdk_pixbuf_get_file_info (file, &width, &height);
+
+ pixbuf = gdk_pixbuf_new_from_file_at_scale (file,
+ (width <= height) ? size : -1,
+ (width >= height) ? size : -1,
+ TRUE,
+ &error);
+ if (error != NULL) {
+ g_critical ("Failed to create pixbuf from file: %s", error->message);
+
+ return NULL;
+ }
+
+ return g_steal_pointer (&pixbuf);
+}
+
+static void
+avatar_file_set_cb (HdyDemoWindow *self)
+{
+ g_assert (HDY_IS_DEMO_WINDOW (self));
+
+ hdy_avatar_set_image_load_func (self->avatar, (HdyAvatarImageLoadFunc) avatar_load_file, self, NULL);
+}
+
+static gchar *
+avatar_new_random_name (void)
+{
+ static const char *first_names[] = {
+ "Adam",
+ "Adrian",
+ "Anna",
+ "Charlotte",
+ "Frédérique",
+ "Ilaria",
+ "Jakub",
+ "Jennyfer",
+ "Julia",
+ "Justin",
+ "Mario",
+ "Miriam",
+ "Mohamed",
+ "Nourimane",
+ "Owen",
+ "Peter",
+ "Petra",
+ "Rachid",
+ "Rebecca",
+ "Sarah",
+ "Thibault",
+ "Wolfgang",
+ };
+ static const char *last_names[] = {
+ "Bailey",
+ "Berat",
+ "Chen",
+ "Farquharson",
+ "Ferber",
+ "Franco",
+ "Galinier",
+ "Han",
+ "Lawrence",
+ "Lepied",
+ "Lopez",
+ "Mariotti",
+ "Rossi",
+ "Urasawa",
+ "Zwickelman",
+ };
+
+ return g_strdup_printf ("%s %s",
+ first_names[g_random_int_range (0, G_N_ELEMENTS (first_names))],
+ last_names[g_random_int_range (0, G_N_ELEMENTS (last_names))]);
+}
+
+static void
+avatar_update_contacts (HdyDemoWindow *self)
+{
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->avatar_contacts));
+
+ for (GList *child = children; child; child = child->next)
+ gtk_container_remove (GTK_CONTAINER (self->avatar_contacts), child->data);
+
+ for (int i = 0; i < 30; i++) {
+ g_autofree gchar *name = avatar_new_random_name ();
+ GtkWidget *contact = hdy_action_row_new ();
+ GtkWidget *avatar = hdy_avatar_new (40, name, TRUE);
+
+ gtk_widget_show (contact);
+ gtk_widget_show (avatar);
+
+ gtk_widget_set_margin_top (avatar, 12);
+ gtk_widget_set_margin_bottom (avatar, 12);
+
+ hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (contact), name);
+ hdy_action_row_add_prefix (HDY_ACTION_ROW (contact), avatar);
+ gtk_container_add (GTK_CONTAINER (self->avatar_contacts), contact);
+ }
+}
+
+static void
+hdy_demo_window_constructed (GObject *object)
+{
+ HdyDemoWindow *self = HDY_DEMO_WINDOW (object);
+
+ G_OBJECT_CLASS (hdy_demo_window_parent_class)->constructed (object);
+
+ hdy_search_bar_connect_entry (self->search_bar, self->search_entry);
+}
+
+
+static void
+hdy_demo_window_class_init (HdyDemoWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->constructed = hdy_demo_window_constructed;
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-demo-window.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, content_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, header_revealer);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, header_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, theme_variant_image);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, sidebar);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, leaflet_transition_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, content_deck);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, deck_transition_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, box_keypad);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, keypad_listbox);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, keypad);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, search_bar);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, search_entry);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, lists_listbox);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, combo_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, enum_combo_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_listbox);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_indicators_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_orientation_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, carousel_indicators_row);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_text);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_filechooser);
+ gtk_widget_class_bind_template_child (widget_class, HdyDemoWindow, avatar_contacts);
+ gtk_widget_class_bind_template_callback_full (widget_class, "key_pressed_cb", G_CALLBACK(hdy_demo_window_key_pressed_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_visible_child_cb", G_CALLBACK(hdy_demo_window_notify_visible_child_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_deck_visible_child_cb", G_CALLBACK(hdy_demo_window_notify_deck_visible_child_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "back_clicked_cb", G_CALLBACK(hdy_demo_window_back_clicked_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "deck_back_clicked_cb", G_CALLBACK(hdy_demo_window_deck_back_clicked_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_leaflet_transition_cb", G_CALLBACK(notify_leaflet_transition_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_deck_transition_cb", G_CALLBACK(notify_deck_transition_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "deck_go_next_row_activated_cb", G_CALLBACK(deck_go_next_row_activated_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "theme_variant_button_clicked_cb", G_CALLBACK(theme_variant_button_clicked_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "view_switcher_demo_clicked_cb", G_CALLBACK(view_switcher_demo_clicked_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_carousel_orientation_cb", G_CALLBACK(notify_carousel_orientation_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "notify_carousel_indicators_cb", G_CALLBACK(notify_carousel_indicators_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "carousel_return_clicked_cb", G_CALLBACK(carousel_return_clicked_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "avatar_file_remove_cb", G_CALLBACK(avatar_file_remove_cb));
+ gtk_widget_class_bind_template_callback_full (widget_class, "avatar_file_set_cb", G_CALLBACK(avatar_file_set_cb));
+}
+
+static void
+lists_page_init (HdyDemoWindow *self)
+{
+ GListStore *list_store;
+ HdyValueObject *obj;
+
+ list_store = g_list_store_new (HDY_TYPE_VALUE_OBJECT);
+
+ obj = hdy_value_object_new_string ("Foo");
+ g_list_store_insert (list_store, 0, obj);
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_string ("Bar");
+ g_list_store_insert (list_store, 1, obj);
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_string ("Baz");
+ g_list_store_insert (list_store, 2, obj);
+ g_clear_object (&obj);
+
+ hdy_combo_row_bind_name_model (self->combo_row, G_LIST_MODEL (list_store), (HdyComboRowGetNameFunc) hdy_value_object_dup_string, NULL, NULL);
+
+ hdy_combo_row_set_for_enum (self->enum_combo_row, GTK_TYPE_LICENSE, hdy_enum_value_row_name, NULL, NULL);
+ update (self);
+}
+
+static void
+carousel_page_init (HdyDemoWindow *self)
+{
+ HdyValueObject *obj;
+
+ hdy_combo_row_set_for_enum (self->carousel_orientation_row,
+ GTK_TYPE_ORIENTATION,
+ carousel_orientation_name,
+ NULL,
+ NULL);
+
+ self->carousel_indicators_model = g_list_store_new (HDY_TYPE_VALUE_OBJECT);
+
+ obj = hdy_value_object_new_string ("dots");
+ g_list_store_insert (self->carousel_indicators_model, 0, obj);
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_string ("lines");
+ g_list_store_insert (self->carousel_indicators_model, 1, obj);
+ g_clear_object (&obj);
+
+ hdy_combo_row_bind_name_model (self->carousel_indicators_row,
+ G_LIST_MODEL (self->carousel_indicators_model),
+ (HdyComboRowGetNameFunc) carousel_indicators_name,
+ NULL,
+ NULL);
+}
+
+static void
+avatar_page_init (HdyDemoWindow *self)
+{
+ g_autofree gchar *name = avatar_new_random_name ();
+
+ gtk_entry_set_text (self->avatar_text, name);
+
+ avatar_update_contacts (self);
+}
+
+static void
+hdy_demo_window_init (HdyDemoWindow *self)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ g_object_bind_property_full (settings, "gtk-application-prefer-dark-theme",
+ self->theme_variant_image, "icon-name",
+ G_BINDING_SYNC_CREATE,
+ prefer_dark_theme_to_icon_name_cb,
+ NULL,
+ NULL,
+ NULL);
+
+ hdy_combo_row_set_for_enum (self->leaflet_transition_row, HDY_TYPE_LEAFLET_TRANSITION_TYPE, leaflet_transition_name, NULL, NULL);
+ hdy_combo_row_set_selected_index (self->leaflet_transition_row, HDY_LEAFLET_TRANSITION_TYPE_OVER);
+
+ hdy_combo_row_set_for_enum (self->deck_transition_row, HDY_TYPE_DECK_TRANSITION_TYPE, deck_transition_name, NULL, NULL);
+ hdy_combo_row_set_selected_index (self->deck_transition_row, HDY_DECK_TRANSITION_TYPE_OVER);
+
+ lists_page_init (self);
+ carousel_page_init (self);
+ avatar_page_init (self);
+
+ hdy_leaflet_set_visible_child_name (self->content_box, "content");
+}
diff --git a/subprojects/libhandy/examples/hdy-demo-window.h b/subprojects/libhandy/examples/hdy-demo-window.h
new file mode 100644
index 0000000..d263e13
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-window.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_DEMO_WINDOW (hdy_demo_window_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyDemoWindow, hdy_demo_window, HDY, DEMO_WINDOW, HdyApplicationWindow)
+
+HdyDemoWindow *hdy_demo_window_new (GtkApplication *application);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/examples/hdy-demo-window.ui b/subprojects/libhandy/examples/hdy-demo-window.ui
new file mode 100644
index 0000000..9d00588
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-demo-window.ui
@@ -0,0 +1,2352 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.1 -->
+<interface>
+ <requires lib="gtk+" version="3.16"/>
+ <requires lib="libhandy" version="1.0"/>
+ <menu id="primary_menu">
+ <section>
+ <item>
+ <attribute name="label" translatable="yes">Preferences</attribute>
+ <attribute name="action">app.preferences</attribute>
+ </item>
+ </section>
+ </menu>
+ <template class="HdyDemoWindow" parent="HdyApplicationWindow">
+ <property name="can_focus">False</property>
+ <property name="title">Handy Demo</property>
+ <property name="default_width">800</property>
+ <property name="default_height">576</property>
+ <signal name="key-press-event" handler="key_pressed_cb" after="yes" swapped="no"/>
+ <child>
+ <object class="HdyLeaflet" id="content_box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="can-swipe-back">True</property>
+ <property name="width-request">360</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRevealer" id="header_revealer">
+ <property name="visible">True</property>
+ <property name="transition-type">slide-down</property>
+ <property name="reveal-child" bind-source="window_header_revealer_switch" bind-property="state" bind-flags="sync-create | bidirectional"/>
+ <child>
+ <object class="HdyHeaderBar" id="header_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title">Handy Demo</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <signal name="clicked" handler="theme_variant_button_clicked_cb" swapped="yes"/>
+ <child>
+ <object class="GtkImage" id="theme_variant_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkMenuButton" id="menu_button">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="menu_model">primary_menu</property>
+ <property name="use_popover">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">open-menu-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackSidebar" id="sidebar">
+ <property name="width_request">270</property>
+ <property name="visible">True</property>
+ <property name="vexpand">True</property>
+ <property name="can_focus">False</property>
+ <property name="stack">stack</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">sidebar</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRevealer">
+ <property name="visible">True</property>
+ <property name="transition-type" bind-source="header_revealer" bind-property="transition-type" bind-flags="bidirectional|sync-create"/>
+ <property name="reveal-child" bind-source="header_revealer" bind-property="reveal-child" bind-flags="bidirectional|sync-create"/>
+ <child>
+ <object class="HdyWindowHandle" id="header_separator">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <style>
+ <class name="sidebar"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSeparator">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="vexpand">True</property>
+ <style>
+ <class name="sidebar"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="navigatable">False</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="right_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkRevealer">
+ <property name="visible">True</property>
+ <property name="transition-type" bind-source="header_revealer" bind-property="transition-type" bind-flags="bidirectional|sync-create"/>
+ <property name="reveal-child" bind-source="header_revealer" bind-property="reveal-child" bind-flags="bidirectional|sync-create"/>
+ <child>
+ <object class="GtkStack" id="header_stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">False</property>
+ <property name="transition-type" bind-source="stack" bind-property="transition-type" bind-flags="sync-create"/>
+ <child>
+ <object class="HdyHeaderBar" id="default_header_bar">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkButton" id="back">
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
+ <signal name="clicked" handler="back_clicked_cb"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-back">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="back_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">default</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyDeck" id="header_deck">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="can-swipe-back">True</property>
+ <property name="transition-type" bind-source="content_deck" bind-property="transition-type" bind-flags="sync-create"/>
+ <child>
+ <object class="HdyHeaderBar" id="deck_header_bar">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkButton" id="deck-back">
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
+ <signal name="clicked" handler="back_clicked_cb"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-deck-back">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyHeaderBar" id="deck_sub_header_bar">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkButton" id="deck-sub-back">
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="visible">True</property>
+ <signal name="clicked" handler="deck_back_clicked_cb"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-deck-sub-back">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">deck</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyHeaderBar" id="search_bar_header_bar">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <property name="show_close_button">True</property>
+ <child>
+ <object class="GtkButton">
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="visible" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
+ <signal name="clicked" handler="back_clicked_cb"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject">
+ <property name="accessible-name" translatable="yes">Back</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">go-previous-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="use-underline">True</property>
+ <property name="active" bind-source="search_bar" bind-property="search-mode-enabled" bind-flags="sync-create|bidirectional"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-search">
+ <property name="accessible-name" translatable="yes">Search</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="search_image">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search-bar</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="visible">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="vexpand">True</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vhomogeneous">False</property>
+ <signal name="notify::visible-child" handler="notify_visible_child_cb" after="yes" swapped="no"/>
+ <child>
+ <object class="GtkBox" id="welcome">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="margin_bottom">18</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">gnome-smartphone-symbolic</property>
+ <property name="icon_size">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="halign">center</property>
+ <property name="margin_bottom">12</property>
+ <property name="label" translatable="yes">Welcome to Handy Demo</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="empty-state-label">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="label" translatable="yes">This is a tour of the features the library has to offer.</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">welcome</property>
+ <property name="title">Welcome</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-leaflet-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Leaflet</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A widget showing either all its children or only one, depending on the available space. This window is using a leaflet, you can control it with the settings below.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="label" translatable="yes">Leaflet</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyComboRow" id="leaflet_transition_row">
+ <property name="subtitle" translatable="yes">The type of transition to use when the leaflet adapts its size or when changing the visible child</property>
+ <property name="title" translatable="yes">Transition type</property>
+ <property name="visible">True</property>
+ <signal name="notify::selected-index" handler="notify_leaflet_transition_cb" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">leaflet</property>
+ <property name="title">Leaflet</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyDeck" id="content_deck">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="vexpand">True</property>
+ <property name="transition-type">over</property>
+ <property name="can-swipe-back">True</property>
+ <signal name="notify::visible-child" handler="notify_deck_visible_child_cb" after="yes" swapped="yes"/>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-deck-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Deck</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A widget showing only one of its children at a time. This page is using decks, you can control them with the settings below.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="label" translatable="yes">Deck</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyComboRow" id="deck_transition_row">
+ <property name="subtitle" translatable="yes">The type of transition to use when the decks adapt their size or when changing the visible child</property>
+ <property name="title" translatable="yes">Transition type</property>
+ <property name="visible">True</property>
+ <signal name="notify::selected-index" handler="notify_deck_transition_cb" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Go to the next page of the deck</property>
+ <property name="use_underline">True</property>
+ <property name="visible">True</property>
+ <property name="activatable">True</property>
+ <signal name="activated" handler="deck_go_next_row_activated_cb" swapped="yes"/>
+ <child>
+ <object class="GtkImage">
+ <property name="icon_name">go-next-symbolic</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <property name="margin">12</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="halign">center</property>
+ <property name="label" translatable="yes">Go back</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">gesture-touchscreen-swipe-back-symbolic</property>
+ <property name="pixel-size">128</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="icon-name">gesture-touchpad-swipe-back-symbolic</property>
+ <property name="pixel-size">128</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">sub</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">deck</property>
+ <property name="title">Deck</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-keypad-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Keypad</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A number keypad.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="maximum-size">300</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox" id="box_keypad">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkListBox" id="keypad_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Show letters</property>
+ <property name="activatable_widget">keypad_letters_visible</property>
+ <child>
+ <object class="GtkSwitch" id="keypad_letters_visible">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="state">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Show symbols</property>
+ <property name="activatable_widget">keypad_symbols_visible</property>
+ <child>
+ <object class="GtkSwitch" id="keypad_symbols_visible">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="state">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEntry" id="entry_keypad">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyKeypad" id="keypad">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="entry">entry_keypad</property>
+ <property name="symbols-visible" bind-source="keypad_symbols_visible" bind-property="state" bind-flags="sync-create | bidirectional"/>
+ <property name="letters-visible" bind-source="keypad_letters_visible" bind-property="state" bind-flags="sync-create | bidirectional"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="pack_type">end</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">keypad</property>
+ <property name="title">Keypad</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size" bind-source="clamp_maximum_size_adjustment" bind-property="value" bind-flags="sync-create"/>
+ <property name="tightening-threshold" bind-source="clamp_tightening_threshold_adjustment" bind-property="value" bind-flags="sync-create"/>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-clamp-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Clamp</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">This page is clamped to smoothly grow up to a maximum width.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="label" translatable="yes">Clamp</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="clamp_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Maximum width</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="adjustment">clamp_maximum_size_adjustment</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Tightening threshold</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSpinButton">
+ <property name="adjustment">clamp_tightening_threshold_adjustment</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">clamp</property>
+ <property name="title">Clamp</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-list-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Lists</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Rows and helpers for &lt;i&gt;GtkListBox&lt;/i&gt;.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="label" translatable="yes">Lists</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="lists_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="icon_name" translatable="yes">preferences-other-symbolic</property>
+ <property name="subtitle" translatable="yes">They also have a subtitle and an icon</property>
+ <property name="title" translatable="yes">Rows have a title</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="activatable_widget">frobnicate</property>
+ <property name="title" translatable="yes">Rows can have action widgets</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkButton" id="frobnicate">
+ <property name="can_focus">True</property>
+ <property name="halign">end</property>
+ <property name="label" translatable="yes">Frobnicate</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="list-button"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="activatable_widget">radio_button_1</property>
+ <property name="title" translatable="yes">Rows can have prefix widgets</property>
+ <property name="visible">True</property>
+ <child type="prefix">
+ <object class="GtkRadioButton" id="radio_button_1">
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="activatable_widget">radio_button_2</property>
+ <property name="title" translatable="yes">Rows can have prefix widgets</property>
+ <property name="visible">True</property>
+ <child type="prefix">
+ <object class="GtkRadioButton" id="radio_button_2">
+ <property name="can_focus">False</property>
+ <property name="group">radio_button_1</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyComboRow" id="combo_row">
+ <property name="title" translatable="yes">Combo row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyComboRow" id="enum_combo_row">
+ <property name="subtitle" translatable="yes">This combo row was created from an enumeration</property>
+ <property name="title" translatable="yes">Enumeration combo row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow" id="expander_row">
+ <property name="title" translatable="yes">Expander row</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBoxRow">
+ <property name="activatable">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="label" translatable="yes">Hello, world!</property>
+ <property name="margin">12</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow" id="action_expander_row">
+ <property name="title" translatable="yes">Expander row with an action</property>
+ <property name="visible">True</property>
+ <child type="action">
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="list-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">edit-copy-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">A nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Another nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow">
+ <property name="title" translatable="yes">Expander row with a prefix</property>
+ <property name="visible">True</property>
+ <child type="prefix">
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="list-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-shutdown-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">A nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Another nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow">
+ <property name="title" translatable="yes">Expander row with a prefix and icon</property>
+ <property name="visible">True</property>
+ <property name="icon-name">action-unavailable-symbolic</property>
+ <child type="prefix">
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="list-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">system-shutdown-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">A nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Another nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow" id="enable_expander_row">
+ <property name="show_enable_switch">True</property>
+ <property name="title" translatable="yes">Toggleable expander row</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">A nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Another nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyExpanderRow" id="action_switch_expander_row">
+ <property name="show_enable_switch">True</property>
+ <property name="title" translatable="yes">Toggleable expander row with an action</property>
+ <property name="visible">True</property>
+ <child type="action">
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="valign">center</property>
+ <style>
+ <class name="list-button"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">edit-copy-symbolic</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">A nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="title" translatable="yes">Another nested row</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">lists</property>
+ <property name="title">Lists</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkOverlay">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child type="overlay">
+ <object class="HdySearchBar" id="search_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">start</property>
+ <property name="hexpand">True</property>
+ <property name="show-close-button">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="margin_bottom">18</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-search-symbolic</property>
+ <property name="icon_size">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <property name="margin_start">12</property>
+ <property name="margin_end">12</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="halign">center</property>
+ <property name="margin_bottom">12</property>
+ <property name="label" translatable="yes">Search bar</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="margin_bottom">6</property>
+ <property name="label" translatable="yes">A search bar that gives your search entry all the space it needs.</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="opacity">0.5</property>
+ <property name="label" translatable="yes">Try using it with an horizontaly expanded clamp to make your search entry adaptive.</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="use_markup">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search-bar</property>
+ <property name="title">Search bar</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-view-switcher-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">View Switcher</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Widgets to switch the window's view.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="label" translatable="yes">Run the demo</property>
+ <signal name="clicked" handler="view_switcher_demo_clicked_cb" swapped="no"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ <property name="position">1</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">view-switcher</property>
+ <property name="title">View Switcher</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="carousel_box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkBox" id="carousel_empty_box">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyCarousel" id="carousel">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-carousel-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ <class name="carousel-icon"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Carousel</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A widget for paginated scrolling.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkListBox" id="carousel_listbox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyComboRow" id="carousel_orientation_row">
+ <property name="title" translatable="yes">Orientation</property>
+ <property name="visible">True</property>
+ <signal name="notify::selected-index" handler="notify_carousel_orientation_cb" swapped="no"/>
+ </object>
+ </child>
+ <child>
+ <object class="HdyComboRow" id="carousel_indicators_row">
+ <property name="title" translatable="yes">Page Indicators</property>
+ <property name="visible">True</property>
+ <signal name="notify::selected-index" handler="notify_carousel_indicators_cb" swapped="no"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="hexpand">True</property>
+ <property name="spacing">24</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Another page</property>
+ <property name="justify">center</property>
+ <property name="wrap">True</property>
+ <property name="opacity">0.5</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">_Return to the first page</property>
+ <property name="use-underline">True</property>
+ <signal name="clicked" handler="carousel_return_clicked_cb" swapped="no"/>
+ <style>
+ <class name="suggested-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="carousel_indicators_stack">
+ <property name="visible">True</property>
+ <property name="homogeneous">False</property>
+ <property name="margin">6</property>
+ <child>
+ <object class="HdyCarouselIndicatorDots">
+ <property name="visible">True</property>
+ <property name="carousel">carousel</property>
+ <property name="orientation" bind-source="carousel" bind-property="orientation" bind-flags="sync-create"/>
+ </object>
+ <packing>
+ <property name="name">dots</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyCarouselIndicatorLines">
+ <property name="visible">True</property>
+ <property name="carousel">carousel</property>
+ <property name="orientation" bind-source="carousel" bind-property="orientation" bind-flags="sync-create"/>
+ </object>
+ <packing>
+ <property name="name">lines</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">carousel</property>
+ <property name="title">Carousel</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="HdyAvatar" id="avatar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="halign">center</property>
+ <property name="size" bind-source="avatar_size" bind-property="value" bind-flags="sync-create"></property>
+ <property name="margin-bottom">18</property>
+ <property name="show-initials" bind-source="avatar_show_initials" bind-property="state" bind-flags="sync-create"/>
+ <property name="text" bind-source="avatar_text" bind-property="text" bind-flags="sync-create"/>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Avatar</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A user avatar with generated fallback.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="hexpand">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Text</property>
+ <child>
+ <object class="GtkEntry" id="avatar_text">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Show initials</property>
+ <property name="activatable_widget">avatar_show_initials</property>
+ <child>
+ <object class="GtkSwitch" id="avatar_show_initials">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <property name="state">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">File</property>
+ <child>
+ <object class="GtkFileChooserButton" id="avatar_filechooser">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="title" translatable="yes"/>
+ <signal name="file-set" swapped="yes" handler="avatar_file_set_cb"/>
+ <property name="filter">avatar_file_filter</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <signal name="clicked" swapped="yes" handler="avatar_file_remove_cb"/>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="icon_name">user-trash-symbolic</property>
+ <property name="icon_size">1</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Size</property>
+ <child>
+ <object class="GtkSpinButton" id="avatar_size">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="valign">center</property>
+ <property name="numeric">True</property>
+ <property name="adjustment">avatar_adjustment</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox" id="avatar_contacts">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">avatar</property>
+ <property name="title">Avatar</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="halign">fill</property>
+ <property name="valign">fill</property>
+ <property name="margin-bottom">32</property>
+ <property name="margin-start">12</property>
+ <property name="margin-end">12</property>
+ <property name="margin-top">32</property>
+ <property name="expand">True</property>
+ <property name="maximum-size">400</property>
+ <property name="tightening-threshold">300</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="valign">start</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="margin-bottom">32</property>
+ <property name="expand">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="valign">center</property>
+ <property name="pixel_size">128</property>
+ <property name="icon_name">widget-window-symbolic</property>
+ <property name="icon-size">0</property>
+ <property name="margin-bottom">18</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">Window</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="margin-bottom">6</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ <attribute name="scale" value="2"/>
+ </attributes>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">A freeform window.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ <property name="margin-bottom">6</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="opacity">0.5</property>
+ <property name="can_focus">False</property>
+ <property name="label" translatable="yes">It allows to have headerbar in content area, incl. above content, and round corners on the bottom. This window is an example, try hiding the titlebar.</property>
+ <property name="justify">center</property>
+ <property name="use_markup">true</property>
+ <property name="wrap">True</property>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">False</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="label" translatable="yes">Window</property>
+ <property name="justify">left</property>
+ <property name="halign">start</property>
+ <property name="margin-bottom">12</property>
+ <attributes>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">Show titlebar</property>
+ <property name="activatable_widget">window_header_revealer_switch</property>
+ <child>
+ <object class="GtkSwitch" id="window_header_revealer_switch">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="state">True</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="HdyWindowHandle">
+ <property name="visible">True</property>
+ <property name="margin-top">12</property>
+ <child>
+ <object class="GtkListBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="selection-mode">none</property>
+ <style>
+ <class name="content"/>
+ </style>
+ <child>
+ <object class="HdyActionRow">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="title" translatable="yes">This row acts as a titlebar</property>
+ <property name="subtitle" translatable="yes">Try dragging or right clicking it.</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">window</property>
+ <property name="title">Window</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">content</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">vertical</property>
+ <widgets>
+ <widget name="header_bar"/>
+ <widget name="header_separator"/>
+ <widget name="header_stack"/>
+ </widgets>
+ </object>
+ <object class="HdyHeaderGroup" id="header_group">
+ <property name="decorate-all" bind-source="content_box" bind-property="folded" bind-flags="sync-create"/>
+ <headerbars>
+ <headerbar name="header_bar"/>
+ <headerbar name="default_header_bar"/>
+ <headerbar name="search_bar_header_bar"/>
+ <headerbar name="deck_header_bar"/>
+ <headerbar name="deck_sub_header_bar"/>
+ </headerbars>
+ </object>
+ <object class="HdySwipeGroup" id="deck_swipe_group">
+ <swipeables>
+ <swipeable name="header_deck"/>
+ <swipeable name="content_deck"/>
+ </swipeables>
+ </object>
+ <object class="GtkAdjustment" id="clamp_maximum_size_adjustment">
+ <property name="lower">0</property>
+ <property name="upper">10000</property>
+ <property name="value">600</property>
+ <property name="page-increment">100</property>
+ <property name="step-increment">10</property>
+ </object>
+ <object class="GtkAdjustment" id="clamp_tightening_threshold_adjustment">
+ <property name="lower">0</property>
+ <property name="upper">10000</property>
+ <property name="value">500</property>
+ <property name="page-increment">100</property>
+ <property name="step-increment">10</property>
+ </object>
+ <object class="GtkSizeGroup">
+ <property name="mode">both</property>
+ <widgets>
+ <widget name="carousel_empty_box"/>
+ <widget name="carousel_indicators_stack"/>
+ </widgets>
+ </object>
+ <object class="GtkAdjustment" id="avatar_adjustment">
+ <property name="lower">24</property>
+ <property name="upper">320</property>
+ <property name="value">128</property>
+ <property name="page-increment">8</property>
+ <property name="step-increment">8</property>
+ </object>
+ <object class="GtkFileFilter" id="avatar_file_filter">
+ <mime-types>
+ <mime-type>image/png</mime-type>
+ <mime-type>image/jpeg</mime-type>
+ <mime-type>image/jpg</mime-type>
+ <mime-type>image/gif</mime-type>
+ </mime-types>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c
new file mode 100644
index 0000000..2251658
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.c
@@ -0,0 +1,30 @@
+#include "hdy-view-switcher-demo-window.h"
+
+#include <glib/gi18n.h>
+
+struct _HdyViewSwitcherDemoWindow
+{
+ HdyWindow parent_instance;
+};
+
+G_DEFINE_TYPE (HdyViewSwitcherDemoWindow, hdy_view_switcher_demo_window, HDY_TYPE_WINDOW)
+
+static void
+hdy_view_switcher_demo_window_class_init (HdyViewSwitcherDemoWindowClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ gtk_widget_class_set_template_from_resource (widget_class, "/sm/puri/Handy/Demo/ui/hdy-view-switcher-demo-window.ui");
+}
+
+static void
+hdy_view_switcher_demo_window_init (HdyViewSwitcherDemoWindow *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+HdyViewSwitcherDemoWindow *
+hdy_view_switcher_demo_window_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_DEMO_WINDOW, NULL);
+}
diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h
new file mode 100644
index 0000000..76dcdd7
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.h
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <handy.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_DEMO_WINDOW (hdy_view_switcher_demo_window_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherDemoWindow, hdy_view_switcher_demo_window, HDY, VIEW_SWITCHER_DEMO_WINDOW, HdyWindow)
+
+HdyViewSwitcherDemoWindow *hdy_view_switcher_demo_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui
new file mode 100644
index 0000000..d6cc82e
--- /dev/null
+++ b/subprojects/libhandy/examples/hdy-view-switcher-demo-window.ui
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.22.0 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <requires lib="libhandy" version="0.0"/>
+ <template class="HdyViewSwitcherDemoWindow" parent="HdyWindow">
+ <property name="can_focus">False</property>
+ <property name="modal">True</property>
+ <property name="window_position">center-on-parent</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="HdyHeaderBar">
+ <property name="visible">True</property>
+ <property name="centering_policy">strict</property>
+ <property name="can_focus">False</property>
+ <property name="show_close_button">True</property>
+ <property name="title">HdyViewSwitcher Demo</property>
+ <child type="title">
+ <object class="HdyViewSwitcherTitle" id="switcher_title">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stack">stack</property>
+ <property name="title" translatable="yes">View Switcher Example</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">24</property>
+ <property name="label" translatable="yes">World</property>
+ </object>
+ <packing>
+ <property name="name">page1</property>
+ <property name="title" translatable="yes">World</property>
+ <property name="icon_name">help-about-symbolic</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">24</property>
+ <property name="label" translatable="yes">Alarm</property>
+ </object>
+ <packing>
+ <property name="name">page2</property>
+ <property name="title" translatable="yes">Alarm</property>
+ <property name="icon_name">alarm-symbolic</property>
+ <property name="position">1</property>
+ <property name="needs_attention">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">24</property>
+ <property name="label" translatable="yes">Stopwatch</property>
+ </object>
+ <packing>
+ <property name="name">page3</property>
+ <property name="title" translatable="yes">Stopwatch</property>
+ <property name="icon_name">document-print-symbolic</property>
+ <property name="position">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="margin">24</property>
+ <property name="label" translatable="yes">Timer</property>
+ </object>
+ <packing>
+ <property name="name">page0</property>
+ <property name="title" translatable="yes">Timer</property>
+ <property name="icon_name">weather-clear-symbolic</property>
+ <property name="position">3</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ <property name="fill">True</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyViewSwitcherBar" id="switcher_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stack">stack</property>
+ <property name="reveal" bind-source="switcher_title" bind-property="title-visible" bind-flags="sync-create"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg b/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg
new file mode 100644
index 0000000..256ca41
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/dark-mode-symbolic.svg
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0beta2 (2b71d25d45, 2019-12-03)"
+ sodipodi:docname="dark-mode-symbolic.svg"
+ id="svg8"
+ version="1.1"
+ height="16"
+ width="16">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ inkscape:current-layer="g6"
+ inkscape:window-maximized="1"
+ inkscape:window-y="0"
+ inkscape:window-x="0"
+ inkscape:cy="9.636866"
+ inkscape:cx="14.061833"
+ inkscape:zoom="35.858323"
+ showgrid="true"
+ id="namedview10"
+ inkscape:window-height="1016"
+ inkscape:window-width="1920"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ inkscape:document-rotation="0"
+ bordercolor="#666666"
+ pagecolor="#ffffff">
+ <inkscape:grid
+ id="grid841"
+ type="xygrid" />
+ </sodipodi:namedview>
+ <g
+ id="g6"
+ fill="#2e3436">
+ <path
+ d="M 8 4.0058594 C 5.805 4.0058594 4 5.8108594 4 8.0058594 C 4 10.200859 5.805 12.005859 8 12.005859 C 10.195 12.005859 12 10.200859 12 8.0058594 C 12 5.8108594 10.195 4.0058594 8 4.0058594 z M 8 6 C 9.1007925 6 10.005859 6.9050669 10.005859 8.0058594 C 10.005859 9.1066519 9.1007925 10.011719 8 10.011719 C 6.8992075 10.011719 5.9941406 9.1066519 5.9941406 8.0058594 C 5.9941406 6.9050669 6.8992075 6 8 6 z "
+ style="line-height:normal;-inkscape-font-specification:Sans;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none"
+ id="path2" />
+ <path
+ style="fill:#2e3436;fill-opacity:1"
+ d="m 12.596194,2.6966997 0.707107,0.7071067 c 0.195869,0.1958686 0.195869,0.5112382 0,0.7071068 L 12.596194,4.81802 c -0.195868,0.1958686 -0.511238,0.1958686 -0.707106,0 L 11.181981,4.1109132 c -0.195817,-0.1950323 -0.195817,-0.5120745 0,-0.7071068 l 0.707107,-0.7071067 c 0.195868,-0.1958686 0.511238,-0.1958686 0.707106,0 z m -8.4852811,8.4852813 0.7071068,0.707107 c 0.1958686,0.195868 0.1958686,0.511238 0,0.707107 l -0.7071068,0.707106 c -0.1958686,0.195869 -0.5112382,0.195869 -0.7071068,0 L 2.6966994,12.596195 c -0.1958161,-0.195033 -0.1958166,-0.512075 0,-0.707107 l 0.7071067,-0.707107 c 0.1958686,-0.195869 0.5112382,-0.195869 0.7071068,0 z M 7.5000757,1.0005015 h 0.999849 C 8.7762374,0.99998906 9.0003616,1.2241133 8.9998492,1.500426 V 2.500124 C 9.0003616,2.7764366 8.7762374,3.0005609 8.4999247,3.0000485 H 7.5000757 C 7.2237631,3.0005609 6.9996388,2.7764366 7.0001512,2.500124 V 1.500426 C 6.9996388,1.2241133 7.2237631,0.99998906 7.5000757,1.0005015 Z M 7.4993686,13.00066 8.4999247,12.999953 c 0.2763126,-5.13e-4 0.5004372,0.223611 0.4999245,0.499924 l 7.071e-4,1.000405 c 5.127e-4,0.276313 -0.2236119,0.500437 -0.4999245,0.499925 L 7.5000757,14.9995 C 7.2237631,15.000012 6.9996386,14.775888 7.0001512,14.499575 V 13.499877 C 6.9996386,13.223564 7.2237631,12.99944 7.5000757,12.999953 Z M 1.0005012,8.499925 V 7.500076 C 0.99998861,7.2237635 1.2241132,6.9996389 1.5004257,7.0001515 h 0.999698 C 2.7764364,6.999639 3.0005607,7.2237633 3.0000482,7.500076 V 8.499925 C 3.0005607,8.7762375 2.7764362,9.0003621 2.5001237,8.9998495 H 1.5004257 C 1.224113,9.000362 0.99998869,8.7762377 1.0005012,8.499925 Z M 13.000659,8.5006321 12.999952,7.500076 C 12.99944,7.2237633 13.223564,6.999639 13.499877,7.0001515 h 0.999698 c 0.276312,-5.126e-4 0.500437,0.223612 0.499924,0.4999245 l 7.07e-4,1.0005561 c 5.13e-4,0.2763127 -0.223611,0.500437 -0.499924,0.4999245 L 13.499877,8.9998495 C 13.223564,9.0003621 12.99944,8.7762375 12.999952,8.499925 Z m 0.302642,4.0955629 -0.707107,0.707106 c -0.195868,0.195869 -0.511238,0.195869 -0.707106,0 l -0.707107,-0.707106 c -0.195817,-0.195033 -0.195816,-0.512075 0,-0.707107 l 0.707107,-0.707107 c 0.195868,-0.195869 0.511238,-0.195869 0.707106,0 l 0.707107,0.707107 c 0.195869,0.195868 0.195869,0.511238 0,0.707107 z M 4.8180197,4.1109132 4.1109129,4.81802 c -0.1958686,0.1958686 -0.5112382,0.1958686 -0.7071068,0 L 2.6966994,4.1109132 c -0.1958164,-0.1950323 -0.1958164,-0.5120745 0,-0.7071068 L 3.4038061,2.6966997 c 0.1958686,-0.1958686 0.5112382,-0.1958686 0.7071068,0 l 0.7071068,0.7071067 c 0.1958686,0.1958686 0.1958686,0.5112382 0,0.7071068 z"
+ id="path844"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="sssssccsssssssccssccccccccccccccccccccccccccccccccccccccsssccsssssssccssss" />
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg
new file mode 100644
index 0000000..e25947c
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic-rtl.svg
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 64 64.000001"
+ id="svg6535"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="two-finger-swipe-left.svg">
+ <defs
+ id="defs6537" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="0.8190337"
+ inkscape:cy="13.44962"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:showpageshadow="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2560"
+ inkscape:window-height="1376"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid7931" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata6540">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(180,-470.14793)">
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 38.271484 6.0039062 C 37.981892 5.9956378 37.692703 6.0019094 37.40625 6.0214844 C 34.246377 6.2374271 31.332975 8.1249345 29.902344 11.082031 C 29.173793 10.990041 28.447857 10.978702 27.738281 11.0625 C 24.36672 11.460668 21.373919 13.762084 20.242188 17.148438 L 12 17.148438 L 12 10.148438 L 2 19.648438 L 12 29.148438 L 12 22.148438 L 20.048828 22.148438 C 20.694441 24.742195 22.473591 26.994506 25 28.162109 L 25 47 L 24 47 L 24 39.185547 C 24 36.866788 22.215997 35 20 35 C 17.784003 35 16 36.866788 16 39.185547 L 16 47 L 16 55.814453 L 16 60 L 20 60 L 45 60 L 56 60 C 58.215997 60 60 58.133212 60 55.814453 L 60 43.185547 L 60 34.185547 C 60 31.866788 58.215997 30 56 30 C 53.784003 30 52 31.866788 52 34.185547 L 52 39 L 51 39 L 51 28.185547 C 51 25.866788 49.215997 24 47 24 C 44.784003 24 43 25.866788 43 28.185547 L 43 39 L 42 39 L 42 23.042969 C 44.136586 21.978813 45.853243 20.084457 46.603516 17.640625 C 48.028796 12.998095 45.481769 8.0270562 40.880859 6.4726562 C 40.018187 6.1812063 39.140263 6.0287116 38.271484 6.0039062 z M 38.171875 8.9980469 C 38.752124 9.0164906 39.339992 9.1204781 39.919922 9.3164062 C 43.012882 10.361346 44.694478 13.640769 43.736328 16.761719 C 43.402083 17.85044 42.787396 18.769252 42 19.474609 L 42 15.185547 C 42 12.866788 40.215997 11 38 11 C 35.784003 11 34 12.866788 34 15.185547 L 34 22.945312 C 33.72503 23.436938 33.386634 23.876948 33 24.261719 L 33 20.185547 C 33 17.866788 31.215997 16 29 16 C 26.784003 16 25 17.866788 25 20.185547 L 25 24.666016 C 23.150282 23.179661 22.297135 20.683826 23.027344 18.265625 C 23.981604 15.105485 27.261604 13.320305 30.433594 14.234375 A 1.50015 1.50015 0 0 0 32.361328 12.945312 A 1.50015 1.50015 0 0 0 32.371094 12.921875 C 33.289974 10.433529 35.657461 8.9181241 38.171875 8.9980469 z M 34 27.302734 L 34 39 L 33 39 L 33 27.925781 C 33.346618 27.7409 33.679606 27.531269 34 27.302734 z "
+ transform="translate(-180,470.14793)"
+ id="rect7308" />
+ <g
+ style="display:inline"
+ transform="translate(80,114.14791)"
+ id="g7391">
+ <g
+ id="g7393">
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="391"
+ x="-324"
+ height="24.999994"
+ width="7.99999"
+ id="rect7395"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7397"
+ width="7.99999"
+ height="43.999989"
+ x="-315"
+ y="372.00003"
+ ry="4.1854858"
+ rx="3.999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="367"
+ x="-306"
+ height="49.000015"
+ width="7.99999"
+ id="rect7399"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7401"
+ width="7.99999"
+ height="36.000011"
+ x="-297"
+ y="380"
+ ry="4.1854858"
+ rx="3.999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="386"
+ x="-288"
+ height="30.000006"
+ width="7.99999"
+ id="rect7403"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7405"
+ width="28.999994"
+ height="12.99999"
+ x="-324"
+ y="403" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7407"
+ width="34.999985"
+ height="21"
+ x="-315"
+ y="395"
+ ry="4.1854858"
+ rx="3.999995" />
+ <path
+ sodipodi:open="true"
+ d="m -309.03562,368.40192 a 7.4999938,7.5000024 0 0 1 9.43617,-4.50737 7.4999938,7.5000024 0 0 1 4.76917,9.30661 7.4999938,7.5000024 0 0 1 -9.16976,5.02725"
+ sodipodi:end="1.8407347"
+ sodipodi:start="3.4953343"
+ sodipodi:ry="7.5000024"
+ sodipodi:rx="7.4999938"
+ sodipodi:cy="371"
+ sodipodi:cx="-302"
+ sodipodi:type="arc"
+ id="path7409"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <path
+ sodipodi:open="true"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="path7411"
+ sodipodi:type="arc"
+ sodipodi:cx="-311.22797"
+ sodipodi:cy="375.99963"
+ sodipodi:rx="7.4999938"
+ sodipodi:ry="7.5000024"
+ sodipodi:start="0.31864739"
+ sodipodi:end="4.9929531"
+ d="m -304.10552,378.34925 a 7.4999938,7.5000024 0 0 1 -9.38146,4.80209 7.4999938,7.5000024 0 0 1 -4.92078,-9.31976 7.4999938,7.5000024 0 0 1 9.25653,-5.03869" />
+ </g>
+ </g>
+ <path
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ d="m -248,499.29695 -10,-9.5 10,-9.5 z"
+ id="path7413"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccc" />
+ <rect
+ transform="scale(-1,-1)"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7415"
+ width="13.061281"
+ height="4.9999938"
+ x="238.62494"
+ y="-492.29697" />
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg
new file mode 100644
index 0000000..e27b4b1
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/gesture-touchpad-swipe-back-symbolic.svg
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 64 64.000001"
+ id="svg6535"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="two-finger-swipe-right.svg">
+ <defs
+ id="defs6537" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="1"
+ inkscape:cx="15.562966"
+ inkscape:cy="0.15090121"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:showpageshadow="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="2560"
+ inkscape:window-height="1376"
+ inkscape:window-x="0"
+ inkscape:window-y="27"
+ inkscape:window-maximized="1">
+ <inkscape:grid
+ type="xygrid"
+ id="grid7931" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata6540">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(180,-470.14793)">
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m -129,475.29637 0,7 -14.48047,0 c -0.8685,-2.58098 -2.88029,-4.74387 -5.63867,-5.67578 -1.15023,-0.3886 -2.3288,-0.52948 -3.47461,-0.45118 -3.16004,0.21596 -6.07335,2.10345 -7.50391,5.06055 -0.72859,-0.0926 -1.45444,-0.10529 -2.16406,-0.0215 -3.45482,0.40801 -6.51775,2.81143 -7.58203,6.33594 -1.30748,4.32992 0.83762,8.91584 4.84375,10.76757 l 0,18.83594 -1,0 0,-7.81445 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,7.81445 0,8.81445 0,4.18555 4,0 25,0 11,0 c 2.216,0 4,-1.86679 4,-4.18555 l 0,-12.6289 0,-9 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,4.81445 -1,0 0,-10.81445 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,10.81445 -1,0 0,-15.95703 c 2.13659,-1.06416 3.85324,-2.95852 4.60352,-5.40235 0.0502,-0.16362 0.0846,-0.32791 0.125,-0.49218 l 14.27148,0 0,7 10,-9.5 -10,-9.5 z m -22.82812,3.84961 c 0.58024,0.0183 1.16811,0.12047 1.74804,0.3164 2.47175,0.83508 4.03799,3.09857 4.08008,5.56641 l 0,0.23242 c -0.012,0.54544 -0.0952,1.09784 -0.26367,1.64649 -0.33419,1.08853 -0.94913,2.00757 -1.73633,2.71289 l 0,-4.28711 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,7.75976 c -0.27497,0.49163 -0.61337,0.93164 -1,1.31641 l 0,-4.07617 c 0,-2.31876 -1.784,-4.18555 -4,-4.18555 -2.216,0 -4,1.86679 -4,4.18555 l 0,4.48242 c -1.84972,-1.48636 -2.70286,-3.98414 -1.97266,-6.40235 0.95426,-3.16014 4.23426,-4.94531 7.40625,-4.03125 a 1.50015,1.50015 0 0 0 1.92774,-1.28906 1.5004025,1.5004025 0 0 0 0.01,-0.0234 c 0.91888,-2.48834 3.28636,-4.00326 5.80078,-3.92383 z M -156,497.45066 l 0,11.69727 -1,0 0,-11.07227 c 0.34667,-0.1849 0.67956,-0.39643 1,-0.625 z"
+ id="rect6513"
+ inkscape:connector-curvature="0" />
+ <g
+ id="g7304"
+ transform="translate(18,114.14791)"
+ style="display:inline">
+ <g
+ id="g7306">
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7308"
+ width="7.99999"
+ height="24.999994"
+ x="-324"
+ y="391"
+ ry="4.1854858"
+ rx="3.999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="372.00003"
+ x="-315"
+ height="43.999989"
+ width="7.99999"
+ id="rect7310"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7312"
+ width="7.99999"
+ height="49.000015"
+ x="-306"
+ y="367"
+ ry="4.1854858"
+ rx="3.999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="380"
+ x="-297"
+ height="36.000011"
+ width="7.99999"
+ id="rect7314"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect7316"
+ width="7.99999"
+ height="30.000006"
+ x="-288"
+ y="386"
+ ry="4.1854858"
+ rx="3.999995" />
+ <rect
+ y="403"
+ x="-324"
+ height="12.99999"
+ width="28.999994"
+ id="rect7318"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="395"
+ x="-315"
+ height="21"
+ width="34.999985"
+ id="rect7320"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <path
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ id="path7322"
+ sodipodi:type="arc"
+ sodipodi:cx="-302"
+ sodipodi:cy="371"
+ sodipodi:rx="7.4999938"
+ sodipodi:ry="7.5000024"
+ sodipodi:start="3.4953343"
+ sodipodi:end="1.8407347"
+ d="m -309.03562,368.40192 a 7.4999938,7.5000024 0 0 1 9.43617,-4.50737 7.4999938,7.5000024 0 0 1 4.76917,9.30661 7.4999938,7.5000024 0 0 1 -9.16976,5.02725"
+ sodipodi:open="true" />
+ <path
+ d="m -304.10552,378.34925 a 7.4999938,7.5000024 0 0 1 -9.38146,4.80209 7.4999938,7.5000024 0 0 1 -4.92078,-9.31976 7.4999938,7.5000024 0 0 1 9.25653,-5.03869"
+ sodipodi:end="4.9929531"
+ sodipodi:start="0.31864739"
+ sodipodi:ry="7.5000024"
+ sodipodi:rx="7.4999938"
+ sodipodi:cy="375.99963"
+ sodipodi:cx="-311.22797"
+ sodipodi:type="arc"
+ id="path7324"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate"
+ sodipodi:open="true" />
+ </g>
+ </g>
+ <path
+ sodipodi:nodetypes="cccc"
+ inkscape:connector-curvature="0"
+ id="path7326"
+ d="m -260.99992,494.29695 10,-9.5 -10,-9.5 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate" />
+ <rect
+ y="-487.29697"
+ x="-278"
+ height="4.9999919"
+ width="20.686279"
+ id="rect7328"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ transform="scale(1,-1)" />
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg
new file mode 100644
index 0000000..a395bf5
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic-rtl.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 64 64.000001"
+ id="svg6535"
+ version="1.1"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"
+ sodipodi:docname="gesture-palm-swipe-right-rtl-symbolic.svg">
+ <defs
+ id="defs6537" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="6.7670154"
+ inkscape:cx="32.723254"
+ inkscape:cy="14.119354"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:showpageshadow="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1280"
+ inkscape:window-height="1376"
+ inkscape:window-x="1280"
+ inkscape:window-y="27"
+ inkscape:window-maximized="0"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-bbox="false"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4193" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata6540">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(180,-470.14793)">
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 9.6542969 6.9921875 C 8.6081142 6.96848 7.5705827 7.347386 6.7871094 8.1308594 C 5.2201627 9.697806 5.2783584 12.278358 6.9179688 13.917969 L 10 17 L 10 7.0332031 C 9.8847052 7.0208034 9.7697279 6.9948033 9.6542969 6.9921875 z M 6.1171875 16.185547 C 5.0710043 16.16184 4.0354273 16.538791 3.2519531 17.322266 C 1.6850047 18.889214 1.7432022 21.471718 3.3828125 23.111328 L 10 29.728516 L 10 18.414062 L 9.0390625 17.453125 C 8.2192573 16.63332 7.1633707 16.209254 6.1171875 16.185547 z M 6.8242188 29.621094 C 5.778036 29.597386 4.7424577 29.974339 3.9589844 30.757812 C 2.3920377 32.324759 2.4502333 34.905312 4.0898438 36.544922 L 10 42.455078 L 10 31.142578 L 9.7460938 30.888672 C 8.9262885 30.068867 7.8704014 29.644801 6.8242188 29.621094 z "
+ transform="translate(-180,470.14793)"
+ id="rect7306" />
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 54 10 L 54 49 L 62 49 L 62 34 L 54 10 z "
+ transform="translate(-180,470.14793)"
+ id="path838" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="M 10 7 L 10 57 L 12 57 L 12 7 L 10 7 z M 52 7 L 52 57 L 54 57 L 54 7 L 52 7 z "
+ transform="translate(-180,470.14793)"
+ id="rect869" />
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m -148,493.14844 c -4.95279,0 -9,4.0472 -9,9 0,4.95279 4.04721,9 9,9 4.95279,0 9,-4.04721 9,-9 0,-4.9528 -4.04721,-9 -9,-9 z m 0,3 c 3.33147,0 6,2.66852 6,6 0,3.33148 -2.66853,6 -6,6 -3.33147,0 -6,-2.66852 -6,-6 0,-3.33148 2.66853,-6 6,-6 z"
+ id="ellipse7314"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z"
+ id="path831"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccsscccccc" />
+ <path
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z"
+ id="path7413"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ <g
+ id="g1107"
+ transform="translate(-64)">
+ <g
+ id="g1080"
+ transform="rotate(-45,-191.43501,474.81355)"
+ style="fill:#000000;fill-opacity:1">
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="490.14792"
+ x="-185"
+ height="34"
+ width="7.99999"
+ id="rect1074"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect1076"
+ width="7.999999"
+ height="30"
+ x="-194"
+ y="494.14792"
+ ry="4.1854858"
+ rx="3.9999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="504.14792"
+ x="-203"
+ height="20"
+ width="7.99999"
+ id="rect1078"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ </g>
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -134,480.14793 v 39 h 16.00021 v -15 l -8.00013,-24 z"
+ id="path1082"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ id="rect1084"
+ width="44"
+ height="50"
+ x="-170"
+ y="477.14792" />
+ <rect
+ y="477.14792"
+ x="-168"
+ height="50"
+ width="40"
+ id="rect1086"
+ style="opacity:1;fill:#26a269;fill-opacity:1;stroke:none;stroke-width:1.86338997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
+ <ellipse
+ cx="-148"
+ cy="502.14792"
+ rx="7.4999938"
+ ry="7.5000024"
+ id="ellipse1088"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z"
+ id="path1090"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccsscccccc" />
+ <path
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z"
+ id="path1092"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path1094"
+ d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg
new file mode 100644
index 0000000..74b3e39
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/gesture-touchscreen-swipe-back-symbolic.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="64"
+ height="64"
+ viewBox="0 0 64 64.000001"
+ id="svg6535"
+ version="1.1"
+ inkscape:version="0.92.4 5da689c313, 2019-01-14"
+ sodipodi:docname="gesture-palm-swipe-right-symbolic.svg">
+ <defs
+ id="defs6537" />
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0.0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="6.7670154"
+ inkscape:cx="32.723254"
+ inkscape:cy="14.119354"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ units="px"
+ inkscape:showpageshadow="false"
+ fit-margin-top="0"
+ fit-margin-left="0"
+ fit-margin-right="0"
+ fit-margin-bottom="0"
+ inkscape:window-width="1280"
+ inkscape:window-height="1376"
+ inkscape:window-x="1280"
+ inkscape:window-y="27"
+ inkscape:window-maximized="0"
+ inkscape:object-paths="true"
+ inkscape:snap-intersection-paths="true"
+ inkscape:object-nodes="true"
+ inkscape:snap-nodes="true"
+ inkscape:snap-bbox="false"
+ inkscape:bbox-paths="true"
+ inkscape:bbox-nodes="true">
+ <inkscape:grid
+ type="xygrid"
+ id="grid4193" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata6540">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(180,-470.14793)">
+ <path
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 9.6542969 6.9921875 C 8.6081142 6.96848 7.5705827 7.347386 6.7871094 8.1308594 C 5.2201627 9.697806 5.2783584 12.278358 6.9179688 13.917969 L 10 17 L 10 7.0332031 C 9.8847052 7.0208034 9.7697279 6.9948033 9.6542969 6.9921875 z M 6.1171875 16.185547 C 5.0710043 16.16184 4.0354273 16.538791 3.2519531 17.322266 C 1.6850047 18.889214 1.7432022 21.471718 3.3828125 23.111328 L 10 29.728516 L 10 18.414062 L 9.0390625 17.453125 C 8.2192573 16.63332 7.1633707 16.209254 6.1171875 16.185547 z M 6.8242188 29.621094 C 5.778036 29.597386 4.7424577 29.974339 3.9589844 30.757812 C 2.3920377 32.324759 2.4502333 34.905312 4.0898438 36.544922 L 10 42.455078 L 10 31.142578 L 9.7460938 30.888672 C 8.9262885 30.068867 7.8704014 29.644801 6.8242188 29.621094 z "
+ transform="translate(-180,470.14793)"
+ id="rect7306" />
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="M 54 10 L 54 49 L 62 49 L 62 34 L 54 10 z "
+ transform="translate(-180,470.14793)"
+ id="path838" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="M 10 7 L 10 57 L 12 57 L 12 7 L 10 7 z M 52 7 L 52 57 L 54 57 L 54 7 L 52 7 z "
+ transform="translate(-180,470.14793)"
+ id="rect869" />
+ <path
+ style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="m -148,493.14844 c -4.95279,0 -9,4.0472 -9,9 0,4.95279 4.04721,9 9,9 4.95279,0 9,-4.04721 9,-9 0,-4.9528 -4.04721,-9 -9,-9 z m 0,3 c 3.33147,0 6,2.66852 6,6 0,3.33148 -2.66853,6 -6,6 -3.33147,0 -6,-2.66852 -6,-6 0,-3.33148 2.66853,-6 6,-6 z"
+ id="ellipse7314"
+ inkscape:connector-curvature="0" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z"
+ id="path831"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccsscccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path906"
+ d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccc" />
+ <g
+ id="g1107"
+ transform="translate(-64)">
+ <g
+ id="g1080"
+ transform="rotate(-45,-191.43501,474.81355)"
+ style="fill:#000000;fill-opacity:1">
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="490.14792"
+ x="-185"
+ height="34"
+ width="7.99999"
+ id="rect1074"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ <rect
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ id="rect1076"
+ width="7.999999"
+ height="30"
+ x="-194"
+ y="494.14792"
+ ry="4.1854858"
+ rx="3.9999995" />
+ <rect
+ rx="3.999995"
+ ry="4.1854858"
+ y="504.14792"
+ x="-203"
+ height="20"
+ width="7.99999"
+ id="rect1078"
+ style="color:#000000;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#4a90d9;stroke-width:0;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" />
+ </g>
+ <path
+ style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.00000656px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m -134,480.14793 v 39 h 16.00021 v -15 l -8.00013,-24 z"
+ id="path1082"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccc" />
+ <rect
+ style="opacity:1;fill:#1a5fb4;fill-opacity:1;stroke:none;stroke-width:1.95433986;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ id="rect1084"
+ width="44"
+ height="50"
+ x="-170"
+ y="477.14792" />
+ <rect
+ y="477.14792"
+ x="-168"
+ height="50"
+ width="40"
+ id="rect1086"
+ style="opacity:1;fill:#26a269;fill-opacity:1;stroke:none;stroke-width:1.86338997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" />
+ <ellipse
+ cx="-148"
+ cy="502.14792"
+ rx="7.4999938"
+ ry="7.5000024"
+ id="ellipse1088"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:none;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />
+ <path
+ style="opacity:1;fill:#000000;fill-opacity:1;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
+ d="m -148,498.14793 c -2.20914,0 -4,1.79086 -4,4 0.001,1.06041 0.42341,2.07695 1.17383,2.82617 l -0.002,0.002 22.17192,22.17183 H -122 c 2.216,0 4,-1.784 4,-4 v -8 c 0,-2.216 -1.784,-4 -4,-4 h -11.34375 l -11.78711,-11.78711 -0.041,-0.041 -0.002,0.002 c -0.74917,-0.75046 -1.76571,-1.17267 -2.82612,-1.17387 z"
+ id="path1090"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="ccccccsscccccc" />
+ <path
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ d="m -159,494.64792 -8,7.5 8,7.5 v -5 h 3.5 v -5 h -3.5 z"
+ id="path1092"
+ inkscape:connector-curvature="0"
+ sodipodi:nodetypes="cccccccc" />
+ <path
+ inkscape:connector-curvature="0"
+ id="path1094"
+ d="m -137,494.64792 8,7.5 -8,7.5 v -5 h -3.5 v -5 h 3.5 z"
+ style="color:#000000;display:inline;overflow:visible;visibility:visible;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;marker:none;enable-background:accumulate"
+ sodipodi:nodetypes="cccccccc" />
+ </g>
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg b/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg
new file mode 100644
index 0000000..39cc8cc
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/gnome-smartphone-symbolic.svg
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="16"
+ height="16"
+ version="1.1"
+ id="svg4"
+ sodipodi:docname="gnome-smartphone-symbolic.svg"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
+ <metadata
+ id="metadata10">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs8" />
+ <sodipodi:namedview
+ inkscape:document-rotation="0"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="2560"
+ inkscape:window-height="1376"
+ id="namedview6"
+ showgrid="true"
+ inkscape:zoom="90.509668"
+ inkscape:cx="7.1287622"
+ inkscape:cy="10.843601"
+ inkscape:window-x="0"
+ inkscape:window-y="0"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="svg4">
+ <inkscape:grid
+ type="xygrid"
+ id="grid855" />
+ </sodipodi:namedview>
+ <path
+ d="M 4.1972656 0 C 2.9868777 0.01088842 2.0086867 0.9907661 2 2.2011719 L 2 13.800781 C 2 14.999781 2.9952656 16 4.1972656 16 L 11.804688 16 C 13.015135 15.98964 13.992393 15.009296 14 13.798828 L 14 2.1992188 C 14 1.0002185 13.006687 -2.9605947e-16 11.804688 0 L 4.1972656 0 z M 4.1992188 2 L 11.800781 2 C 11.911581 2 12 2.0884187 12 2.1992188 L 12 11.800781 C 12 11.911581 11.911581 12 11.800781 12 L 4.1992188 12 C 4.0884187 12 4 11.911581 4 11.800781 L 4 2.1992188 C 4 2.0884187 4.0884187 2 4.1992188 2 z M 8 12.529297 L 9.9023438 14.431641 C 9.9625935 14.491891 10 14.573683 10 14.666016 L 10 15 L 9.6660156 15 C 9.5736826 15 9.4918906 14.962594 9.4316406 14.902344 L 8 13.470703 L 6.5683594 14.902344 C 6.5081094 14.962594 6.4263177 15 6.3339844 15 L 6 15 L 6 14.666016 C 6 14.573686 6.0374063 14.491891 6.0976562 14.431641 L 8 12.529297 z "
+ style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000000;solid-opacity:1;marker:none"
+ id="path2" />
+</svg>
diff --git a/subprojects/libhandy/examples/icons/light-mode-symbolic.svg b/subprojects/libhandy/examples/icons/light-mode-symbolic.svg
new file mode 100644
index 0000000..c2e769b
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/light-mode-symbolic.svg
@@ -0,0 +1,6 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <g fill="#2e3436">
+ <path d="M8 4.006c-2.195 0-4 1.805-4 4 0 2.195 1.805 4 4 4 2.195 0 4-1.805 4-4 0-2.195-1.805-4-4-4z" style="line-height:normal;-inkscape-font-specification:Sans;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" color="#bebebe" font-weight="400" font-family="Sans" overflow="visible"/>
+ <path d="M7.5 0h1c.277 0 .5.223.5.5v2c0 .277-.223.5-.5.5h-1a.499.499 0 0 1-.5-.5v-2c0-.277.223-.5.5-.5zM7.5 13h1c.277 0 .5.223.5.5v2c0 .277-.223.5-.5.5h-1a.499.499 0 0 1-.5-.5v-2c0-.277.223-.5.5-.5zM1.99 2.697l.707-.707a.499.499 0 0 1 .707 0l1.414 1.414a.499.499 0 0 1 0 .707l-.707.707a.499.499 0 0 1-.707 0L1.99 3.404a.499.499 0 0 1 0-.707zM11.182 11.89l.707-.708a.499.499 0 0 1 .707 0l1.415 1.414a.499.499 0 0 1 0 .707l-.708.707a.499.499 0 0 1-.707 0l-1.414-1.414a.499.499 0 0 1 0-.707zM2.697 14.01l-.707-.707a.499.499 0 0 1 0-.707l1.414-1.414a.499.499 0 0 1 .707 0l.707.707a.499.499 0 0 1 0 .707L3.404 14.01a.499.499 0 0 1-.707 0zM11.89 4.818l-.708-.707a.499.499 0 0 1 0-.707l1.414-1.414a.499.499 0 0 1 .707 0l.708.707a.499.499 0 0 1 0 .707l-1.415 1.414a.499.499 0 0 1-.707 0zM16 7.5v1c0 .277-.223.5-.5.5h-2a.499.499 0 0 1-.5-.5v-1c0-.277.223-.5.5-.5h2c.277 0 .5.223.5.5zM3 7.5v1c0 .277-.223.5-.5.5h-2a.499.499 0 0 1-.5-.5v-1c0-.277.223-.5.5-.5h2c.277 0 .5.223.5.5z"/>
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg b/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg
new file mode 100644
index 0000000..2764cc2
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-carousel-symbolic.svg
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
+ sodipodi:docname="view-continuous-symbolic.svg"
+ id="svg8"
+ version="1.1"
+ height="16"
+ width="16">
+ <metadata
+ id="metadata14">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs12" />
+ <sodipodi:namedview
+ inkscape:current-layer="svg8"
+ inkscape:window-maximized="0"
+ inkscape:window-y="23"
+ inkscape:window-x="26"
+ inkscape:cy="8"
+ inkscape:cx="8"
+ inkscape:zoom="32.480545"
+ showgrid="false"
+ id="namedview10"
+ inkscape:window-height="742"
+ inkscape:window-width="1230"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ guidetolerance="10"
+ gridtolerance="10"
+ objecttolerance="10"
+ borderopacity="1"
+ bordercolor="#666666"
+ pagecolor="#ffffff" />
+ <g
+ transform="rotate(-90,8,8)"
+ id="g6"
+ fill="#474747"
+ color="#bebebe">
+ <path
+ id="path2"
+ opacity="0.35"
+ overflow="visible"
+ style="marker:none"
+ d="M 9.625,11 H 6.375 A 0.374,0.374 0 0 0 6,11.375 v 1.25 C 6,12.833 6.167,13 6.375,13 h 3.25 A 0.374,0.374 0 0 0 10,12.625 v -1.25 A 0.374,0.374 0 0 0 9.625,11 Z m 0,-11 H 6.375 A 0.374,0.374 0 0 0 6,0.375 v 1.25 C 6,1.833 6.167,2 6.375,2 h 3.25 A 0.374,0.374 0 0 0 10,1.625 V 0.375 A 0.374,0.374 0 0 0 9.625,0 Z m 0,14 H 6.375 A 0.374,0.374 0 0 0 6,14.375 v 1.25 C 6,15.833 6.167,16 6.375,16 h 3.25 A 0.374,0.374 0 0 0 10,15.625 v -1.25 A 0.374,0.374 0 0 0 9.625,14 Z m 0,-11 H 6.375 A 0.374,0.374 0 0 0 6,3.375 v 1.25 C 6,4.833 6.167,5 6.375,5 h 3.25 A 0.374,0.374 0 0 0 10,4.625 V 3.375 A 0.374,0.374 0 0 0 9.625,3 Z" />
+ <path
+ id="path4"
+ overflow="visible"
+ style="marker:none"
+ d="M 14,7 H 2 v 2 h 12 z" />
+ </g>
+</svg>
diff --git a/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg b/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg
new file mode 100644
index 0000000..9269c40
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-clamp-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" fill="#474747"><path d="M12.293 5.293L9.586 8l2.707 2.707 1.414-1.414L12.414 8l1.293-1.293z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/><path d="M13 10h1v1h-1zm0-5h1v1h-1z" style="marker:none" overflow="visible"/><path d="M13 5c.554 0 1 .446 1 1s-.446 1-1 1-1-.446-1-1 .446-1 1-1zm0 4c.554 0 1 .446 1 1s-.446 1-1 1-1-.446-1-1 .446-1 1-1z" style="marker:none" overflow="visible"/><path d="M3.707 5.293L2.293 6.707 3.586 8 2.293 9.293l1.414 1.414L6.414 8z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/><path d="M3 10H2v1h1zm0-5H2v1h1z" style="marker:none" overflow="visible"/><path d="M3 5c-.554 0-1 .446-1 1s.446 1 1 1 1-.446 1-1-.446-1-1-1zm0 4c-.554 0-1 .446-1 1s.446 1 1 1 1-.446 1-1-.446-1-1-1z" style="marker:none" overflow="visible"/></g></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg b/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg
new file mode 100644
index 0000000..3fe8afa
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-deck-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#bebebe" fill="#474747"><path d="M1 0v13h12V0zm2 2h8v9H3z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/><path d="M14 3v11H4v2h12V3z" style="line-height:normal;-inkscape-font-specification:Sans;text-indent:0;text-align:start;text-decoration-line:none;text-transform:none;marker:none" font-weight="400" font-family="Sans" overflow="visible"/><path d="M8.625 4h-3.25A.374.374 0 005 4.375v1.25c0 .208.167.375.375.375h3.25A.374.374 0 009 5.625v-1.25A.374.374 0 008.625 4zm0 3h-3.25A.374.374 0 005 7.375v1.25c0 .208.167.375.375.375h3.25A.374.374 0 009 8.625v-1.25A.374.374 0 008.625 7z" style="marker:none" overflow="visible" opacity=".35"/></g></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg b/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg
new file mode 100644
index 0000000..f390fd6
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-keypad-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M2.5 1c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-8 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-8 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm4 0c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5zm-4 4c-.277 0-.5.223-.5.5v2c0 .277.223.5.5.5h2c.277 0 .5-.223.5-.5v-2c0-.277-.223-.5-.5-.5z" fill="#2e3436"/></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg b/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg
new file mode 100644
index 0000000..5763db7
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-leaflet-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#bebebe" fill="#474747"><path d="M0 1v13h6c.176 0 .535.14.822.332.288.192.467.371.467.371l.719.727.711-.735S9.615 14 10 14h6V1h-6c-.901 0-1.572.353-2.043.701-.025-.017-.018-.018-.045-.035C7.452 1.362 6.828 1 6 1zm2 2h4c.138 0 .515.138.813.334.297.196.492.385.492.385l.717.693.695-.715S9.619 3 10 3h4v9h-4c-.89 0-1.562.348-2.033.693-.018-.012-.013-.013-.031-.025C7.476 12.36 6.836 12 6 12H2z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" overflow="visible"/><path d="M5.625 5h-2.25A.374.374 0 003 5.375v1.25c0 .207.167.375.375.375h2.25A.374.374 0 006 6.625v-1.25A.374.374 0 005.625 5zm0 3h-2.25A.374.374 0 003 8.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 006 9.625v-1.25A.374.374 0 005.625 8zm7-3h-2.25a.374.374 0 00-.375.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 0013 6.625v-1.25A.374.374 0 0012.625 5zm0 3h-2.25a.374.374 0 00-.375.375v1.25c0 .208.167.375.375.375h2.25A.374.374 0 0013 9.625v-1.25A.374.374 0 0012.625 8z" style="marker:none" overflow="visible" opacity=".35"/></g></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-list-symbolic.svg b/subprojects/libhandy/examples/icons/widget-list-symbolic.svg
new file mode 100644
index 0000000..c9fec6a
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-list-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M3 3h10v2H3zm0 4h10v2H3zm0 4h10v2H3z" style="marker:none" overflow="visible" color="#bebebe" fill="#474747"/></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-search-symbolic.svg b/subprojects/libhandy/examples/icons/widget-search-symbolic.svg
new file mode 100644
index 0000000..1a6200e
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-search-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><g color="#000" font-weight="400" font-family="sans-serif" fill="#474747"><path d="M6.508 1C3.48 1 1.002 3.474 1.002 6.5S3.48 12 6.508 12s5.505-2.474 5.505-5.5S9.536 1 6.508 1zm0 2a3.488 3.488 0 013.505 3.5c0 1.944-1.557 3.5-3.505 3.5a3.488 3.488 0 01-3.506-3.5c0-1.944 1.557-3.5 3.506-3.5z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" overflow="visible"/><path d="M10 8.99a1 1 0 00-.696 1.717l4.004 4a1 1 0 101.414-1.414l-4.003-4a1 1 0 00-.72-.303z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" overflow="visible"/></g></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg b/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg
new file mode 100644
index 0000000..255754c
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-view-switcher-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M2 6.006a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2zm6 0a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2zm6 0a2 2 0 012 2 2 2 0 01-2 2 2 2 0 01-2-2 2 2 0 012-2z" fill="#2e3436"/></svg> \ No newline at end of file
diff --git a/subprojects/libhandy/examples/icons/widget-window-symbolic.svg b/subprojects/libhandy/examples/icons/widget-window-symbolic.svg
new file mode 100644
index 0000000..993fd89
--- /dev/null
+++ b/subprojects/libhandy/examples/icons/widget-window-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <path d="M3.012 1.027c-1.215 0-1.994.779-1.995 1.95V14a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V2.955c0-1.238-.8-1.928-1.972-1.928zm7.005 1.963h1v1h1v-1h1v1h-1v1h1v1h-1v-1h-1v1h-1v-1h1v-1h-1zm-7 4.076h10v5.935h-10z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;white-space:normal;shape-padding:0;isolation:auto;mix-blend-mode:normal;solid-color:#000;solid-opacity:1;marker:none" color="#000" font-weight="400" font-family="sans-serif" overflow="visible" fill="#2e3436"/>
+</svg>
diff --git a/subprojects/libhandy/examples/meson.build b/subprojects/libhandy/examples/meson.build
new file mode 100644
index 0000000..883607e
--- /dev/null
+++ b/subprojects/libhandy/examples/meson.build
@@ -0,0 +1,26 @@
+if get_option('examples')
+
+handy_demo_resources = gnome.compile_resources(
+ 'handy-demo-resources',
+ 'handy-demo.gresources.xml',
+
+ c_name: 'hdy',
+)
+
+handy_demo_sources = [
+ handy_demo_resources,
+ 'handy-demo.c',
+ 'hdy-demo-preferences-window.c',
+ 'hdy-demo-window.c',
+ 'hdy-view-switcher-demo-window.c',
+ libhandy_generated_headers,
+]
+
+handy_demo = executable('handy-@0@-demo'.format(apiversion),
+ handy_demo_sources,
+ dependencies: libhandy_dep,
+ gui_app: true,
+ install: true,
+)
+
+endif
diff --git a/subprojects/libhandy/examples/sm.puri.Handy.Demo.json b/subprojects/libhandy/examples/sm.puri.Handy.Demo.json
new file mode 100644
index 0000000..f59ea8f
--- /dev/null
+++ b/subprojects/libhandy/examples/sm.puri.Handy.Demo.json
@@ -0,0 +1,29 @@
+{
+ "app-id": "sm.puri.Handy.Demo",
+ "runtime": "org.gnome.Platform",
+ "runtime-version": "master",
+ "sdk": "org.gnome.Sdk",
+ "command": "handy-1-demo",
+ "finish-args": [
+ "--device=all",
+ "--share=ipc",
+ "--socket=wayland",
+ "--socket=x11"
+ ],
+ "modules": [
+ {
+ "name": "libhandy",
+ "buildsystem": "meson",
+ "builddir": true,
+ "config-opts": [
+ "-Dglade_catalog=disabled"
+ ],
+ "sources": [
+ {
+ "type": "git",
+ "url": "https://gitlab.gnome.org/GNOME/libhandy.git"
+ }
+ ]
+ }
+ ]
+}
diff --git a/subprojects/libhandy/examples/style.css b/subprojects/libhandy/examples/style.css
new file mode 100644
index 0000000..7182936
--- /dev/null
+++ b/subprojects/libhandy/examples/style.css
@@ -0,0 +1,8 @@
+stacksidebar list {
+ border-left-width: 0px;
+ border-right-width: 0px;
+}
+
+carousel.vertical .carousel-icon {
+ -gtk-icon-transform: rotate(90deg);
+}
diff --git a/subprojects/libhandy/glade/glade-catalog.dtd b/subprojects/libhandy/glade/glade-catalog.dtd
new file mode 100644
index 0000000..c0d073e
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-catalog.dtd
@@ -0,0 +1,196 @@
+<!ELEMENT glade-catalog (init-function?,
+ glade-widget-classes?,
+ glade-widget-group*)>
+
+<!ATTLIST glade-catalog name CDATA #REQUIRED
+ version CDATA #IMPLIED
+ targetable CDATA #IMPLIED
+ library CDATA #IMPLIED
+ depends CDATA #IMPLIED
+ domain CDATA #IMPLIED
+ book CDATA #IMPLIED
+ icon-prefix CDATA #IMPLIED
+ requires CDATA #IMPLIED>
+
+<!ELEMENT glade-widget-classes (glade-widget-class+)>
+
+<!ELEMENT glade-widget-class (post-create-function?,
+ add-child-verify-function?,
+ add-child-function?,
+ remove-child-function?,
+ replace-child-function?,
+ get-children-function?,
+ get-internal-child-function?,
+ child-property-applies-function?,
+ child-action-activate-function?,
+ read-widget-function?,
+ write-widget-function?,
+ get-property-function?,
+ set-property-function?,
+ child-set-property-function?,
+ child-get-property-function?,
+ action-activate-function?,
+ verify-function?,
+ special-child-type?,
+ packing-properties?,
+ packing-actions?,
+ properties?,
+ children?,
+ packing-defaults?,
+ actions?)>
+
+<!ATTLIST glade-widget-class toplevel CDATA #IMPLIED
+ since CDATA #IMPLIED
+ deprecated CDATA #IMPLIED
+ use-placeholders CDATA #IMPLIED
+ default-width CDATA #IMPLIED
+ default-height CDATA #IMPLIED
+ name CDATA #REQUIRED
+ generic-name CDATA #IMPLIED
+ icon-name CDATA #IMPLIED
+ title CDATA #REQUIRED
+ parent CDATA #IMPLIED
+ get-type-function CDATA #IMPLIED
+ adaptor CDATA #IMPLIED>
+
+<!ELEMENT properties (property+)>
+
+<!ELEMENT property (spec?,
+ type?,
+ parameter-spec?,
+ tooltip?,
+ parameters?,
+ set-function?,
+ get-function?,
+ verify-function?,
+ displayable-values?)>
+
+<!ATTLIST property id CDATA #REQUIRED
+ since CDATA #IMPLIED
+ deprecated CDATA #IMPLIED
+ create-type CDATA #IMPLIED
+ name CDATA #IMPLIED
+ tooltip CDATA #IMPLIED
+ themed-icon CDATA #IMPLIED
+ stock CDATA #IMPLIED
+ stock-icon CDATA #IMPLIED
+ weight CDATA #IMPLIED
+ transfer-on-paste CDATA #IMPLIED
+ save-always CDATA #IMPLIED
+ parentless-widget CDATA #IMPLIED
+ atk-property CDATA #IMPLIED
+ default CDATA #IMPLIED
+ query CDATA #IMPLIED
+ save CDATA #IMPLIED
+ common CDATA #IMPLIED
+ disabled CDATA #IMPLIED
+ visible CDATA #IMPLIED
+ custom-layout CDATA #IMPLIED
+ multiline CDATA #IMPLIED
+ optional CDATA #IMPLIED
+ optional-default CDATA #IMPLIED
+ ignore CDATA #IMPLIED
+ needs-sync CDATA #IMPLIED
+ construct-only CDATA #IMPLIED
+ translatable CDATA #IMPLIED>
+
+<!ELEMENT parameter-spec (type?,
+ value-type?,
+ min?)>
+<!ELEMENT value-type (#PCDATA)>
+<!ELEMENT min (#PCDATA)>
+<!ELEMENT set-function (#PCDATA)>
+<!ELEMENT get-function (#PCDATA)>
+<!ELEMENT spec (#PCDATA)>
+<!ELEMENT tooltip (#PCDATA)>
+<!ELEMENT verify-function (#PCDATA)>
+
+<!ELEMENT displayable-values (value+)>
+
+<!ELEMENT value EMPTY>
+
+<!ATTLIST value id CDATA #REQUIRED
+ name CDATA #REQUIRED>
+
+<!ELEMENT parameters (parameter+)>
+
+<!ELEMENT parameter EMPTY>
+
+<!ATTLIST parameter key CDATA #REQUIRED
+ value CDATA #REQUIRED>
+
+<!ELEMENT children (child+)>
+
+<!ELEMENT child (type,
+ add-child-function?,
+ remove-child-function?,
+ get-children-function?,
+ get-all-children-function?,
+ set-property-function?,
+ get-property-function?,
+ replace-child-function?,
+ fill-empty-function?,
+ properties?)>
+
+<!ELEMENT type (#PCDATA)>
+<!ELEMENT add-child-verify-function (#PCDATA)>
+<!ELEMENT add-child-function (#PCDATA)>
+<!ELEMENT remove-child-function (#PCDATA)>
+<!ELEMENT get-children-function (#PCDATA)>
+<!ELEMENT get-all-children-function (#PCDATA)>
+<!ELEMENT set-prop-function (#PCDATA)>
+<!ELEMENT get-prop-function (#PCDATA)>
+<!ELEMENT fill-empty-function (#PCDATA)>
+<!ELEMENT replace-child-function (#PCDATA)>
+<!ELEMENT child-set-property-function (#PCDATA)>
+<!ELEMENT child-get-property-function (#PCDATA)>
+<!ELEMENT action-activate-function (#PCDATA)>
+
+<!ELEMENT post-create-function (#PCDATA)>
+<!ELEMENT get-internal-child-function (#PCDATA)>
+<!ELEMENT child-property-applies-function (#PCDATA)>
+
+<!ELEMENT child-action-activate-function (#PCDATA)>
+
+<!ELEMENT read-widget-function (#PCDATA)>
+<!ELEMENT write-widget-function (#PCDATA)>
+<!ELEMENT get-property-function (#PCDATA)>
+<!ELEMENT set-property-function (#PCDATA)>
+
+<!ELEMENT glade-widget-group (default-palette-state?,
+ glade-widget-class-ref+)>
+
+<!ATTLIST glade-widget-group name CDATA #REQUIRED
+ title CDATA #REQUIRED>
+
+<!ELEMENT default-palette-state EMPTY>
+<!ATTLIST default-palette-state expanded CDATA #IMPLIED>
+
+<!ELEMENT glade-widget-class-ref EMPTY>
+<!ATTLIST glade-widget-class-ref name CDATA #REQUIRED>
+
+<!ELEMENT packing-defaults (parent-class+)>
+
+<!ELEMENT parent-class (child-property+)>
+<!ATTLIST parent-class name CDATA #REQUIRED>
+
+<!ELEMENT child-property EMPTY>
+<!ATTLIST child-property id CDATA #REQUIRED
+ default CDATA #REQUIRED>
+
+<!ELEMENT special-child-type (#PCDATA)>
+
+<!ELEMENT packing-properties (property+)>
+
+<!ELEMENT packing-actions (action+)>
+
+<!ELEMENT actions (action+)>
+
+<!ELEMENT action EMPTY>
+
+<!ATTLIST action id CDATA #REQUIRED
+ name CDATA #REQUIRED
+ stock CDATA #IMPLIED
+ important CDATA #IMPLIED>
+
+<!ELEMENT init-function (#PCDATA)>
diff --git a/subprojects/libhandy/glade/glade-hdy-carousel.c b/subprojects/libhandy/glade/glade-hdy-carousel.c
new file mode 100644
index 0000000..c06a4e4
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-carousel.c
@@ -0,0 +1,460 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-carousel.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+#include <math.h>
+
+static gint
+hdy_carousel_get_page (HdyCarousel *carousel)
+{
+ return round (hdy_carousel_get_position (carousel));
+}
+
+static gboolean
+hdy_carousel_is_transient (HdyCarousel *carousel)
+{
+ return fmod (hdy_carousel_get_position (carousel), 1.0) > 0.00001;
+}
+
+static gint
+get_n_pages_excluding_placeholders (GtkContainer *container)
+{
+ GList *children, *l;
+ gint n_pages;
+
+ children = gtk_container_get_children (container);
+
+ n_pages = 0;
+ for (l = children; l; l = l->next)
+ if (!GLADE_IS_PLACEHOLDER (l->data))
+ n_pages++;
+
+ g_list_free (children);
+
+
+ return n_pages;
+}
+
+static void
+selection_changed_cb (GladeProject *project,
+ GladeWidget *gwidget)
+{
+ GList *list;
+ GtkWidget *page, *sel_widget;
+ GtkContainer *container;
+ GList *children, *l;
+ gint index;
+
+ list = glade_project_selection_get (project);
+ if (!list || g_list_length (list) != 1)
+ return;
+
+ sel_widget = list->data;
+
+ container = GTK_CONTAINER (glade_widget_get_object (gwidget));
+
+ if (!GTK_IS_WIDGET (sel_widget) ||
+ !gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container)))
+ return;
+
+ children = gtk_container_get_children (container);
+ for (l = children; l; l = l->next) {
+ page = l->data;
+ if (sel_widget == page || gtk_widget_is_ancestor (sel_widget, page)) {
+ hdy_carousel_scroll_to (HDY_CAROUSEL (container), page);
+ index = glade_hdy_get_child_index (container, page);
+ glade_widget_property_set (gwidget, "page", index);
+ break;
+ }
+ }
+ g_list_free (children);
+}
+
+static void
+project_changed_cb (GladeWidget *gwidget,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GladeProject *project, *old_project;
+
+ project = glade_widget_get_project (gwidget);
+ old_project = g_object_get_data (G_OBJECT (gwidget), "carousel-project-ptr");
+
+ if (old_project)
+ g_signal_handlers_disconnect_by_func (G_OBJECT (old_project),
+ G_CALLBACK (selection_changed_cb),
+ gwidget);
+
+ if (project)
+ g_signal_connect (G_OBJECT (project), "selection-changed",
+ G_CALLBACK (selection_changed_cb), gwidget);
+
+ g_object_set_data (G_OBJECT (gwidget), "carousel-project-ptr", project);
+}
+
+static void
+position_changed_cb (HdyCarousel *carousel,
+ GParamSpec *pspec,
+ GladeWidget *gwidget)
+{
+ gint old_page, new_page;
+
+ glade_widget_property_get (gwidget, "page", &old_page);
+ new_page = hdy_carousel_get_page (carousel);
+
+ if (old_page == new_page || hdy_carousel_is_transient (carousel))
+ return;
+
+ glade_widget_property_set (gwidget, "page", new_page);
+}
+
+void
+glade_hdy_carousel_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (container);
+
+ if (reason == GLADE_CREATE_USER)
+ gtk_container_add (GTK_CONTAINER (container), glade_placeholder_new ());
+
+ g_signal_connect (G_OBJECT (gwidget), "notify::project",
+ G_CALLBACK (project_changed_cb), NULL);
+
+ project_changed_cb (gwidget, NULL, NULL);
+
+ g_signal_connect (G_OBJECT (container), "notify::position",
+ G_CALLBACK (position_changed_cb), gwidget);
+}
+
+void
+glade_hdy_carousel_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path)
+{
+ if (!strcmp (action_path, "insert_page_after") ||
+ !strcmp (action_path, "insert_page_before")) {
+ GladeWidget *parent;
+ GladeProperty *property;
+ GtkWidget *placeholder;
+ gint pages, index;
+
+ parent = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (parent, "pages", &pages);
+
+ glade_command_push_group (_("Insert placeholder to %s"),
+ glade_widget_get_name (parent));
+
+ index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (object));
+ if (!strcmp (action_path, "insert_page_after"))
+ index++;
+
+ placeholder = glade_placeholder_new ();
+
+ hdy_carousel_insert (HDY_CAROUSEL (container), placeholder, index);
+ hdy_carousel_scroll_to (HDY_CAROUSEL (container), placeholder);
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ property = glade_widget_get_property (parent, "pages");
+ glade_command_set_property (property, pages + 1);
+
+ property = glade_widget_get_property (parent, "page");
+ glade_command_set_property (property, index);
+
+ glade_command_pop_group ();
+ } else if (strcmp (action_path, "remove_page") == 0) {
+ GladeWidget *parent;
+ GladeProperty *property;
+ gint pages, position;
+
+ parent = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (parent, "pages", &pages);
+
+ glade_command_push_group (_("Remove placeholder from %s"),
+ glade_widget_get_name (parent));
+
+ g_assert (GLADE_IS_PLACEHOLDER (object));
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ property = glade_widget_get_property (parent, "pages");
+ glade_command_set_property (property, pages - 1);
+
+ glade_widget_property_get (parent, "page", &position);
+ property = glade_widget_get_property (parent, "page");
+ glade_command_set_property (property, position);
+
+ glade_command_pop_group ();
+ } else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor,
+ container,
+ object,
+ action_path);
+}
+
+static void
+set_n_pages (GObject *container,
+ const GValue *value)
+{
+ GladeWidget *gbox;
+ GtkWidget *child;
+ gint old_size, new_size, i, page;
+
+ new_size = g_value_get_int (value);
+ old_size = hdy_carousel_get_n_pages (HDY_CAROUSEL (container));
+
+ if (old_size == new_size)
+ return;
+
+ for (i = old_size; i < new_size; i++)
+ gtk_container_add (GTK_CONTAINER (container), glade_placeholder_new ());
+
+ for (i = old_size; i > 0; i--) {
+ if (old_size <= new_size)
+ break;
+ child = glade_hdy_get_nth_child (GTK_CONTAINER (container), i - 1);
+ if (GLADE_IS_PLACEHOLDER (child)) {
+ gtk_container_remove (GTK_CONTAINER (container), child);
+ old_size--;
+ }
+ }
+
+ gbox = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+static void
+set_page (GObject *object,
+ const GValue *value)
+{
+ gint new_page;
+ GtkWidget *child;
+
+ new_page = g_value_get_int (value);
+ child = glade_hdy_get_nth_child (GTK_CONTAINER (object), new_page);
+
+ if (child)
+ hdy_carousel_scroll_to (HDY_CAROUSEL (object), child);
+}
+
+void
+glade_hdy_carousel_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "pages"))
+ set_n_pages (object, value);
+ else if (!strcmp (id, "page"))
+ set_page (object, value);
+ else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value);
+ }
+}
+
+void
+glade_hdy_carousel_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value)
+{
+ if (!strcmp (id, "pages")) {
+ g_value_reset (value);
+ g_value_set_int (value, hdy_carousel_get_n_pages (HDY_CAROUSEL (object)));
+ } else if (!strcmp (id, "page")) {
+ g_value_reset (value);
+ g_value_set_int (value, hdy_carousel_get_page (HDY_CAROUSEL (object)));
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value);
+ }
+}
+
+static gboolean
+glade_hdy_carousel_verify_n_pages (GObject *object,
+ const GValue *value)
+{
+ gint new_size, old_size;
+
+ new_size = g_value_get_int (value);
+ old_size = get_n_pages_excluding_placeholders (GTK_CONTAINER (object));
+
+ return old_size <= new_size;
+}
+
+static gboolean
+glade_hdy_carousel_verify_page (GObject *object,
+ const GValue *value)
+{
+ gint page, pages;
+
+ page = g_value_get_int (value);
+ pages = hdy_carousel_get_n_pages (HDY_CAROUSEL (object));
+
+ return 0 <= page && page < pages;
+}
+
+gboolean
+glade_hdy_carousel_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "pages"))
+ return glade_hdy_carousel_verify_n_pages (object, value);
+ else if (!strcmp (id, "page"))
+ return glade_hdy_carousel_verify_page (object, value);
+ else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property)
+ return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object,
+ id, value);
+
+ return TRUE;
+}
+
+void
+glade_hdy_carousel_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child)
+{
+ GladeWidget *gbox, *gchild;
+ gint pages, page;
+
+ if (!glade_widget_superuser () && !GLADE_IS_PLACEHOLDER (child)) {
+ GList *l, *children;
+
+ children = gtk_container_get_children (GTK_CONTAINER (container));
+
+ for (l = g_list_last (children); l; l = l->prev) {
+ GtkWidget *widget = l->data;
+
+ if (GLADE_IS_PLACEHOLDER (widget)) {
+ gtk_container_remove (GTK_CONTAINER (container), widget);
+ break;
+ }
+ }
+
+ g_list_free (children);
+ }
+
+ gtk_container_add (GTK_CONTAINER (container), GTK_WIDGET (child));
+
+ gchild = glade_widget_get_from_gobject (child);
+ if (gchild)
+ glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE);
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ gbox = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+void
+glade_hdy_carousel_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child)
+{
+ GladeWidget *gbox;
+ gint pages, page;
+
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (child));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ gbox = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+void
+glade_hdy_carousel_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget)
+{
+ GladeWidget *gbox, *gchild;
+ gint pages, page, index;
+
+ index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (current));
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (current));
+ hdy_carousel_insert (HDY_CAROUSEL (container), GTK_WIDGET (new_widget),
+ index);
+ hdy_carousel_scroll_to_full (HDY_CAROUSEL (container),
+ GTK_WIDGET (new_widget), 0);
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ gchild = glade_widget_get_from_gobject (new_widget);
+ if (gchild)
+ glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE);
+
+ /* NOTE: make sure to sync this at the end because new_widget could be
+ * a placeholder and syncing these properties could destroy it.
+ */
+ gbox = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+void
+glade_hdy_carousel_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0)
+ g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container),
+ GTK_WIDGET (child)));
+ else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+}
+
+void
+glade_hdy_carousel_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0) {
+ glade_hdy_reorder_child (GTK_CONTAINER (container),
+ GTK_WIDGET (child),
+ g_value_get_int (value));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-carousel.h b/subprojects/libhandy/glade/glade-hdy-carousel.h
new file mode 100644
index 0000000..211c0a6
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-carousel.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+
+void glade_hdy_carousel_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+void glade_hdy_carousel_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_carousel_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+void glade_hdy_carousel_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value);
+gboolean glade_hdy_carousel_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+
+void glade_hdy_carousel_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_carousel_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_carousel_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget);
+
+void glade_hdy_carousel_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
+void glade_hdy_carousel_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
diff --git a/subprojects/libhandy/glade/glade-hdy-expander-row.c b/subprojects/libhandy/glade/glade-hdy-expander-row.c
new file mode 100644
index 0000000..d1a2ccc
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-expander-row.c
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-list-box.c - GladeWidgetAdaptor for GtkListBox
+ * Copyright (C) 2013 Kalev Lember
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-expander-row.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+void
+glade_hdy_expander_row_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ g_object_set (container, "expanded", TRUE, NULL);
+}
+
+void
+glade_hdy_expander_row_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0)
+ g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container),
+ GTK_WIDGET (child)));
+ else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+}
+
+void
+glade_hdy_expander_row_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0)
+ glade_hdy_reorder_child (GTK_CONTAINER (container),
+ GTK_WIDGET (child),
+ g_value_get_int (value));
+ else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+}
+
+void
+glade_hdy_expander_row_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (object));
+}
+
+void
+glade_hdy_expander_row_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (object));
+}
+
+gboolean
+glade_hdy_expander_row_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback)
+{
+ if (GTK_IS_LIST_BOX_ROW (child))
+ return TRUE;
+
+ if (user_feedback) {
+ GladeWidgetAdaptor *row_adaptor =
+ glade_widget_adaptor_get_by_type (GTK_TYPE_LIST_BOX_ROW);
+
+ glade_util_ui_message (glade_app_get_window (),
+ GLADE_UI_INFO, NULL,
+ ONLY_THIS_GOES_IN_THAT_MSG,
+ glade_widget_adaptor_get_title (row_adaptor),
+ glade_widget_adaptor_get_title (adaptor));
+ }
+
+ return FALSE;
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-expander-row.h b/subprojects/libhandy/glade/glade-hdy-expander-row.h
new file mode 100644
index 0000000..221f65e
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-expander-row.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+void glade_hdy_expander_row_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+void glade_hdy_expander_row_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
+void glade_hdy_expander_row_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
+
+void glade_hdy_expander_row_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+
+void glade_hdy_expander_row_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+
+gboolean glade_hdy_expander_row_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback);
diff --git a/subprojects/libhandy/glade/glade-hdy-header-bar.c b/subprojects/libhandy/glade/glade-hdy-header-bar.c
new file mode 100644
index 0000000..12b7afe
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-header-bar.c
@@ -0,0 +1,547 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-header-bar.c - GladeWidgetAdaptor for GtkHeaderBar
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-header-bar.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+#define TITLE_DISABLED_MESSAGE _("This property does not apply when a custom title is set")
+
+typedef struct {
+ GtkContainer *parent;
+ GtkWidget *custom_title;
+ gboolean include_placeholders;
+ gint count;
+} ChildrenData;
+
+static void
+count_children (GtkWidget *widget, gpointer data)
+{
+ ChildrenData *cdata = data;
+
+ if (widget == cdata->custom_title)
+ return;
+
+ if ((GLADE_IS_PLACEHOLDER (widget) && cdata->include_placeholders) ||
+ glade_widget_get_from_gobject (widget) != NULL)
+ cdata->count++;
+}
+
+static gboolean
+verify_size (GObject *object,
+ const GValue *value)
+{
+ gint new_size;
+ ChildrenData data;
+
+ new_size = g_value_get_int (value);
+
+ data.parent = GTK_CONTAINER (object);
+ data.custom_title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object));
+ data.include_placeholders = FALSE;
+ data.count = 0;
+
+ gtk_container_foreach (data.parent, count_children, &data);
+
+ return data.count <= new_size;
+}
+
+static gint
+get_n_children (GObject *object)
+{
+ ChildrenData data;
+
+ data.parent = GTK_CONTAINER (object);
+ data.custom_title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object));
+ data.include_placeholders = TRUE;
+ data.count = 0;
+
+ gtk_container_foreach (data.parent, count_children, &data);
+
+ return data.count;
+}
+
+static void
+parse_finished_cb (GladeProject *project,
+ GObject *object)
+{
+ GladeWidget *gbox;
+
+ gbox = glade_widget_get_from_gobject (object);
+ glade_widget_property_set (gbox, "size", get_n_children (object));
+ glade_widget_property_set (gbox, "use-custom-title", hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) != NULL);
+}
+
+void
+glade_hdy_header_bar_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ GladeWidget *parent = glade_widget_get_from_gobject (container);
+ GladeProject *project = glade_widget_get_project (parent);
+
+ if (reason == GLADE_CREATE_LOAD) {
+ g_signal_connect (project, "parse-finished",
+ G_CALLBACK (parse_finished_cb),
+ container);
+
+ return;
+ }
+
+ if (reason == GLADE_CREATE_USER)
+ hdy_header_bar_pack_start (HDY_HEADER_BAR (container),
+ glade_placeholder_new ());
+}
+
+void
+glade_hdy_header_bar_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path)
+{
+ if (!strcmp (action_path, "add_slot")) {
+ GladeWidget *parent;
+ GladeProperty *property;
+ gint size;
+
+ parent = glade_widget_get_from_gobject (object);
+
+ glade_command_push_group (_("Insert placeholder to %s"),
+ glade_widget_get_name (parent));
+
+ property = glade_widget_get_property (parent, "size");
+ glade_property_get (property, &size);
+ glade_command_set_property (property, size + 1);
+
+ glade_command_pop_group ();
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor,
+ object,
+ action_path);
+ }
+}
+
+void
+glade_hdy_header_bar_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path)
+{
+ if (strcmp (action_path, "remove_slot") == 0) {
+ GladeWidget *parent;
+ GladeProperty *property;
+
+ parent = glade_widget_get_from_gobject (container);
+ glade_command_push_group (_("Remove placeholder from %s"),
+ glade_widget_get_name (parent));
+
+ if (g_object_get_data (object, "special-child-type")) {
+ property = glade_widget_get_property (parent, "use-custom-title");
+ glade_command_set_property (property, FALSE);
+ } else {
+ gint size;
+
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object));
+
+ property = glade_widget_get_property (parent, "size");
+ glade_property_get (property, &size);
+ glade_command_set_property (property, size - 1);
+ }
+
+ glade_command_pop_group ();
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor,
+ container,
+ object,
+ action_path);
+ }
+}
+
+void
+glade_hdy_header_bar_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value)
+{
+ if (!strcmp (id, "use-custom-title")) {
+ g_value_reset (value);
+ g_value_set_boolean (value, hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) != NULL);
+ } else if (!strcmp (id, "size")) {
+ g_value_reset (value);
+ g_value_set_int (value, get_n_children (object));
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value);
+ }
+}
+
+static void
+set_size (GObject *object,
+ const GValue *value)
+{
+ GList *l, *next;
+ g_autoptr (GList) children = NULL;
+ GtkWidget *child;
+ guint new_size, old_size, i;
+
+ if (glade_util_object_is_loading (object))
+ return;
+
+ children = gtk_container_get_children (GTK_CONTAINER (object));
+ l = children;
+
+ while (l) {
+ next = l->next;
+ if (l->data == hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object)) ||
+ (!glade_widget_get_from_gobject (l->data) && !GLADE_IS_PLACEHOLDER (l->data)))
+ children = g_list_delete_link (children, l);
+ l = next;
+ }
+
+ old_size = g_list_length (children);
+ new_size = g_value_get_int (value);
+
+ if (old_size == new_size)
+ return;
+
+ for (i = old_size; i < new_size; i++) {
+ GtkWidget *placeholder = glade_placeholder_new ();
+ hdy_header_bar_pack_start (HDY_HEADER_BAR (object), placeholder);
+ }
+
+ for (l = g_list_last (children); l && old_size > new_size; l = l->prev) {
+ child = l->data;
+ if (glade_widget_get_from_gobject (child) || !GLADE_IS_PLACEHOLDER (child))
+ continue;
+
+ gtk_container_remove (GTK_CONTAINER (object), child);
+ old_size--;
+ }
+}
+
+static void
+set_use_custom_title (GObject *object,
+ gboolean use_custom_title)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (object);
+ GtkWidget *child;
+
+ if (use_custom_title) {
+ child = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (object));
+ if (!child) {
+ child = glade_placeholder_new ();
+ g_object_set_data (G_OBJECT (child), "special-child-type", "title");
+ }
+ } else {
+ child = NULL;
+ }
+
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (object), child);
+
+ if (GLADE_IS_PLACEHOLDER (child)) {
+ GList *list, *l;
+
+ list = glade_placeholder_packing_actions (GLADE_PLACEHOLDER (child));
+ for (l = list; l; l = l->next) {
+ GladeWidgetAction *gwa = l->data;
+ if (!strcmp (glade_widget_action_get_def (gwa)->id, "remove_slot"))
+ glade_widget_action_set_visible (gwa, FALSE);
+ }
+ }
+
+ if (use_custom_title) {
+ glade_widget_property_set_sensitive (gwidget, "title", FALSE, TITLE_DISABLED_MESSAGE);
+ glade_widget_property_set_sensitive (gwidget, "subtitle", FALSE, TITLE_DISABLED_MESSAGE);
+ glade_widget_property_set_sensitive (gwidget, "has-subtitle", FALSE, TITLE_DISABLED_MESSAGE);
+ } else {
+ glade_widget_property_set_sensitive (gwidget, "title", TRUE, NULL);
+ glade_widget_property_set_sensitive (gwidget, "subtitle", TRUE, NULL);
+ glade_widget_property_set_sensitive (gwidget, "has-subtitle", TRUE, NULL);
+ }
+}
+
+void
+glade_hdy_header_bar_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "use-custom-title")) {
+ set_use_custom_title (object, g_value_get_boolean (value));
+ } else if (!strcmp (id, "show-close-button")) {
+ GladeWidget *gwidget = glade_widget_get_from_gobject (object);
+
+ /* We don't set the property to 'ignore' so that we catch this in the adaptor,
+ * but we also do not apply the property to the runtime object here, thus
+ * avoiding showing the close button which would in turn close glade itself
+ * when clicked.
+ */
+ glade_widget_property_set_sensitive (gwidget, "decoration-layout",
+ g_value_get_boolean (value),
+ _("The decoration layout does not apply to header bars "
+ "which do no show window controls"));
+ } else if (!strcmp (id, "size")) {
+ set_size (object, value);
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value);
+ }
+}
+
+void
+glade_hdy_header_bar_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *parent,
+ GObject *child)
+{
+ GladeWidget *gbox, *gchild;
+ gint size;
+ gchar *special_child_type;
+
+ gchild = glade_widget_get_from_gobject (child);
+ if (gchild)
+ glade_widget_set_pack_action_visible (gchild, "remove_slot", FALSE);
+
+ special_child_type = g_object_get_data (child, "special-child-type");
+
+ if (special_child_type && !strcmp (special_child_type, "title")) {
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (parent), GTK_WIDGET (child));
+
+ return;
+ }
+
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->add (adaptor, parent, child);
+
+ gbox = glade_widget_get_from_gobject (parent);
+ if (!glade_widget_superuser ()) {
+ glade_widget_property_get (gbox, "size", &size);
+ glade_widget_property_set (gbox, "size", size);
+ }
+}
+
+void
+glade_hdy_header_bar_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GladeWidget *gbox;
+ gint size;
+ gchar *special_child_type;
+
+ special_child_type = g_object_get_data (child, "special-child-type");
+
+ if (special_child_type && !strcmp (special_child_type, "title")) {
+ GtkWidget *replacement = glade_placeholder_new ();
+
+ g_object_set_data (G_OBJECT (replacement), "special-child-type", "title");
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (object), replacement);
+
+ return;
+ }
+
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+
+ /* Synchronize number of placeholders, this should trigger the set_property method with the
+ * correct value (not the arbitrary number of children currently in the headerbar)
+ */
+ gbox = glade_widget_get_from_gobject (object);
+ if (!glade_widget_superuser ()) {
+ glade_widget_property_get (gbox, "size", &size);
+ glade_widget_property_set (gbox, "size", size);
+ }
+}
+
+void
+glade_hdy_header_bar_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget)
+{
+ GladeWidget *gbox;
+ gchar *special_child_type;
+ gint size;
+
+ special_child_type =
+ g_object_get_data (G_OBJECT (current), "special-child-type");
+
+ if (special_child_type && !strcmp (special_child_type, "title")) {
+ g_object_set_data (G_OBJECT (new_widget), "special-child-type", "title");
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (container),
+ GTK_WIDGET (new_widget));
+
+ return;
+ }
+
+ g_object_set_data (G_OBJECT (new_widget), "special-child-type", NULL);
+
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->replace_child (adaptor,
+ container,
+ current,
+ new_widget);
+
+ gbox = glade_widget_get_from_gobject (container);
+ if (!glade_widget_superuser ()) {
+ glade_widget_property_get (gbox, "size", &size);
+ glade_widget_property_set (gbox, "size", size);
+ }
+}
+
+gboolean
+glade_hdy_header_bar_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "size"))
+ return verify_size (object, value);
+ else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property)
+ return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor,
+ object,
+ id,
+ value);
+
+ return TRUE;
+}
+
+static gint
+sort_children (GtkWidget *widget_a, GtkWidget *widget_b, GtkWidget *bar)
+{
+ GladeWidget *gwidget_a, *gwidget_b;
+ gint position_a, position_b;
+ GtkWidget *title;
+
+ /* title goes first */
+ title = hdy_header_bar_get_custom_title (HDY_HEADER_BAR (bar));
+ if (title == widget_a)
+ return -1;
+ if (title == widget_b)
+ return 1;
+
+ if ((gwidget_a = glade_widget_get_from_gobject (widget_a)) &&
+ (gwidget_b = glade_widget_get_from_gobject (widget_b))) {
+ glade_widget_pack_property_get (gwidget_a, "position", &position_a);
+ glade_widget_pack_property_get (gwidget_b, "position", &position_b);
+
+ /* If position is the same, try to give an stable order */
+ if (position_a == position_b)
+ return g_strcmp0 (glade_widget_get_name (gwidget_a),
+ glade_widget_get_name (gwidget_b));
+ } else {
+ gtk_container_child_get (GTK_CONTAINER (bar), widget_a,
+ "position", &position_a, NULL);
+ gtk_container_child_get (GTK_CONTAINER (bar), widget_b,
+ "position", &position_b, NULL);
+ }
+
+ return position_a - position_b;
+}
+
+GList *
+glade_hdy_header_bar_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *container)
+{
+ GList *children = GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_children (adaptor, container);
+
+ return g_list_sort_with_data (children, (GCompareDataFunc) sort_children, container);
+}
+
+
+void
+glade_hdy_header_bar_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value)
+{
+ GladeWidget *gbox, *gchild, *gchild_iter;
+ GList *children, *list;
+ gboolean is_position;
+ gint old_position, iter_position, new_position;
+ static gboolean recursion = FALSE;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (container));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+ g_return_if_fail (property_name != NULL || value != NULL);
+
+ gbox = glade_widget_get_from_gobject (container);
+ gchild = glade_widget_get_from_gobject (child);
+
+ g_return_if_fail (GLADE_IS_WIDGET (gbox));
+
+ /* Get old position */
+ if ((is_position = (strcmp (property_name, "position") == 0)) != FALSE) {
+ gtk_container_child_get (GTK_CONTAINER (container),
+ GTK_WIDGET (child),
+ "position", &old_position,
+ NULL);
+
+
+ /* Get the real value */
+ new_position = g_value_get_int (value);
+ }
+
+ if (is_position && recursion == FALSE) {
+ children = glade_widget_get_children (gbox);
+
+ for (list = children; list; list = list->next) {
+ gchild_iter = glade_widget_get_from_gobject (list->data);
+
+ if (gchild_iter == gchild) {
+ gtk_container_child_set (GTK_CONTAINER (container),
+ GTK_WIDGET (child),
+ "position", new_position,
+ NULL);
+
+ continue;
+ }
+
+ /* Get the old value from glade */
+ glade_widget_pack_property_get (gchild_iter, "position", &iter_position);
+
+ /* Search for the child at the old position and update it */
+ if (iter_position == new_position &&
+ glade_property_superuser () == FALSE) {
+ /* Update glade with the real value */
+ recursion = TRUE;
+ glade_widget_pack_property_set (gchild_iter, "position", old_position);
+ recursion = FALSE;
+
+ continue;
+ } else {
+ gtk_container_child_set (GTK_CONTAINER (container),
+ GTK_WIDGET (list->data),
+ "position", iter_position,
+ NULL);
+ }
+ }
+
+ for (list = children; list; list = list->next) {
+ gchild_iter = glade_widget_get_from_gobject (list->data);
+
+ /* Refresh values yet again */
+ glade_widget_pack_property_get (gchild_iter, "position", &iter_position);
+
+ gtk_container_child_set (GTK_CONTAINER (container),
+ GTK_WIDGET (list->data),
+ "position", iter_position,
+ NULL);
+ }
+
+ if (children)
+ g_list_free (children);
+ }
+
+ /* Chain Up */
+ if (!is_position)
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-header-bar.h b/subprojects/libhandy/glade/glade-hdy-header-bar.h
new file mode 100644
index 0000000..8f6c84a
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-header-bar.h
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+
+void glade_hdy_header_bar_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+void glade_hdy_header_bar_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_header_bar_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_header_bar_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value);
+void glade_hdy_header_bar_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+
+void glade_hdy_header_bar_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *parent,
+ GObject *child);
+void glade_hdy_header_bar_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+void glade_hdy_header_bar_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget);
+
+gboolean glade_hdy_header_bar_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+
+GList *glade_hdy_header_bar_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *container);
+
+void glade_hdy_header_bar_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value);
diff --git a/subprojects/libhandy/glade/glade-hdy-header-group.c b/subprojects/libhandy/glade/glade-hdy-header-group.c
new file mode 100644
index 0000000..3ef633e
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-header-group.c
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-size-group.c - GladeWidgetAdaptor for GtkSizeGroup
+ * Copyright (C) 2013 Tristan Van Berkom
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-header-group.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+#define GLADE_TAG_HEADERGROUP_WIDGETS "headerbars"
+#define GLADE_TAG_HEADERGROUP_WIDGET "headerbar"
+
+
+static void
+glade_hdy_header_group_read_widgets (GladeWidget *widget, GladeXmlNode *node)
+{
+ GladeXmlNode *widgets_node;
+ GladeProperty *property;
+ gchar *string = NULL;
+
+ if ((widgets_node =
+ glade_xml_search_child (node, GLADE_TAG_HEADERGROUP_WIDGETS)) != NULL) {
+ GladeXmlNode *n;
+
+ for (n = glade_xml_node_get_children (widgets_node);
+ n; n = glade_xml_node_next (n)) {
+ gchar *widget_name, *tmp;
+
+ if (!glade_xml_node_verify (n, GLADE_TAG_HEADERGROUP_WIDGET))
+ continue;
+
+ widget_name = glade_xml_get_property_string_required
+ (n, GLADE_TAG_NAME, NULL);
+
+ if (string == NULL) {
+ string = widget_name;
+ } else if (widget_name != NULL) {
+ tmp =
+ g_strdup_printf ("%s%s%s", string, GLADE_PROPERTY_DEF_OBJECT_DELIMITER,
+ widget_name);
+ string = (g_free (string), tmp);
+ g_free (widget_name);
+ }
+ }
+ }
+
+ if (string) {
+ property = glade_widget_get_property (widget, "headerbars");
+ g_assert (property);
+
+ /* we must synchronize this directly after loading this project
+ * (i.e. lookup the actual objects after they've been parsed and
+ * are present).
+ */
+ g_object_set_data_full (G_OBJECT (property),
+ "glade-loaded-object", string, g_free);
+ }
+}
+
+void
+glade_hdy_header_group_read_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlNode *node)
+{
+ if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) ||
+ glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE)))
+ return;
+
+ /* First chain up and read in all the normal properties.. */
+ GWA_GET_CLASS (G_TYPE_OBJECT)->read_widget (adaptor, widget, node);
+
+ glade_hdy_header_group_read_widgets (widget, node);
+}
+
+
+static void
+glade_hdy_header_group_write_widgets (GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node)
+{
+ GladeXmlNode *widgets_node, *widget_node;
+ GList *widgets = NULL, *list;
+ GladeWidget *awidget;
+
+ widgets_node = glade_xml_node_new (context, GLADE_TAG_HEADERGROUP_WIDGETS);
+
+ if (glade_widget_property_get (widget, "headerbars", &widgets)) {
+ for (list = widgets; list; list = list->next) {
+ awidget = glade_widget_get_from_gobject (list->data);
+ widget_node =
+ glade_xml_node_new (context, GLADE_TAG_HEADERGROUP_WIDGET);
+ glade_xml_node_append_child (widgets_node, widget_node);
+ glade_xml_node_set_property_string (widget_node, GLADE_TAG_NAME,
+ glade_widget_get_name (awidget));
+ }
+ }
+
+ if (!glade_xml_node_get_children (widgets_node))
+ glade_xml_node_delete (widgets_node);
+ else
+ glade_xml_node_append_child (node, widgets_node);
+}
+
+
+void
+glade_hdy_header_group_write_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node)
+{
+ if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) ||
+ glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE)))
+ return;
+
+ /* First chain up and read in all the normal properties.. */
+ GWA_GET_CLASS (G_TYPE_OBJECT)->write_widget (adaptor, widget, context, node);
+
+ glade_hdy_header_group_write_widgets (widget, context, node);
+}
+
+
+void
+glade_hdy_header_group_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *property_name,
+ const GValue *value)
+{
+ if (!strcmp (property_name, "headerbars")) {
+ GSList *sg_widgets, *slist;
+ GList *widgets, *list;
+
+ /* remove old widgets */
+ if ((sg_widgets =
+ hdy_header_group_get_children (HDY_HEADER_GROUP (object))) != NULL) {
+ /* copy since we are modifying an internal list */
+ sg_widgets = g_slist_copy (sg_widgets);
+ for (slist = sg_widgets; slist; slist = slist->next)
+ hdy_header_group_remove_child (HDY_HEADER_GROUP (object),
+ HDY_HEADER_GROUP_CHILD (slist->data));
+ g_slist_free (sg_widgets);
+ }
+
+ /* add new widgets */
+ if ((widgets = g_value_get_boxed (value)) != NULL) {
+ for (list = widgets; list; list = list->next)
+ hdy_header_group_add_header_bar (HDY_HEADER_GROUP (object),
+ HDY_HEADER_BAR (list->data));
+ }
+ } else {
+ GWA_GET_CLASS (G_TYPE_OBJECT)->set_property (adaptor, object,
+ property_name, value);
+ }
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-header-group.h b/subprojects/libhandy/glade/glade-hdy-header-group.h
new file mode 100644
index 0000000..5333c0f
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-header-group.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+
+void glade_hdy_header_group_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *property_name,
+ const GValue *value);
+void glade_hdy_header_group_write_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node);
+void glade_hdy_header_group_read_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlNode *node);
diff --git a/subprojects/libhandy/glade/glade-hdy-leaflet.c b/subprojects/libhandy/glade/glade-hdy-leaflet.c
new file mode 100644
index 0000000..4032f9f
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-leaflet.c
@@ -0,0 +1,533 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-leaflet.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+#define PAGE_DISABLED_MESSAGE _("This property only applies when the leaflet is folded")
+
+static void
+selection_changed_cb (GladeProject *project,
+ GladeWidget *gwidget)
+{
+ GList *list;
+ GtkWidget *page, *sel_widget;
+ GtkContainer *container = GTK_CONTAINER (glade_widget_get_object (gwidget));
+ gint index;
+
+ if ((list = glade_project_selection_get (project)) != NULL &&
+ g_list_length (list) == 1) {
+ sel_widget = list->data;
+
+ if (GTK_IS_WIDGET (sel_widget) &&
+ gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container))) {
+ g_autoptr (GList) children = gtk_container_get_children (container);
+ GList *l;
+
+ index = 0;
+ for (l = children; l; l = l->next) {
+ page = l->data;
+ if (sel_widget == page ||
+ gtk_widget_is_ancestor (sel_widget, page)) {
+ glade_widget_property_set (gwidget, "page", index);
+
+ break;
+ }
+
+ index++;
+ }
+ }
+ }
+}
+
+static void
+project_changed_cb (GladeWidget *gwidget,
+ GParamSpec *pspec,
+ gpointer userdata)
+{
+ GladeProject *project = glade_widget_get_project (gwidget);
+ GladeProject *old_project = g_object_get_data (G_OBJECT (gwidget),
+ "project-ptr");
+
+ if (old_project)
+ g_signal_handlers_disconnect_by_func (G_OBJECT (old_project),
+ G_CALLBACK (selection_changed_cb),
+ gwidget);
+
+ if (project)
+ g_signal_connect (G_OBJECT (project),
+ "selection-changed",
+ G_CALLBACK (selection_changed_cb),
+ gwidget);
+
+ g_object_set_data (G_OBJECT (gwidget), "project-ptr", project);
+}
+
+static void
+add_named (GtkContainer *container,
+ GtkWidget *child,
+ const gchar *name)
+{
+ gtk_container_add_with_properties (container,
+ child,
+ "name", name,
+ NULL);
+}
+
+static void
+folded_changed_cb (HdyLeaflet *leaflet,
+ GParamSpec *pspec,
+ gpointer userdata)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (leaflet);
+ gboolean folded = hdy_leaflet_get_folded (leaflet);
+
+ glade_widget_property_set_sensitive (gwidget,
+ "page",
+ folded,
+ folded ? NULL : PAGE_DISABLED_MESSAGE);
+}
+
+void
+glade_hdy_leaflet_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (container);
+
+ if (reason == GLADE_CREATE_USER)
+ add_named (GTK_CONTAINER (container),
+ glade_placeholder_new (),
+ "page0");
+
+ g_signal_connect (G_OBJECT (gwidget),
+ "notify::project",
+ G_CALLBACK (project_changed_cb),
+ NULL);
+
+ project_changed_cb (gwidget, NULL, NULL);
+
+ if (HDY_IS_LEAFLET (container)) {
+ g_signal_connect (container,
+ "notify::folded",
+ G_CALLBACK (folded_changed_cb),
+ NULL);
+
+ folded_changed_cb (HDY_LEAFLET (container), NULL, NULL);
+ }
+}
+
+static GtkWidget *
+get_child_by_name (GtkContainer *container,
+ const gchar *name)
+{
+ g_autoptr (GList) children = gtk_container_get_children (container);
+ GList *l;
+
+ for (l = children; l; l = l->next) {
+ const gchar *child_name;
+
+ gtk_container_child_get (container, l->data, "name", &child_name, NULL);
+
+ if (child_name && !strcmp (child_name, name))
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static gchar *
+get_unused_name (GtkContainer *container)
+{
+ gint i = 0;
+
+ while (TRUE) {
+ g_autofree gchar *name = g_strdup_printf ("page%d", i);
+
+ if (get_child_by_name (container, name) == NULL)
+ return g_steal_pointer (&name);
+
+ i++;
+ }
+
+ return NULL;
+}
+
+void
+glade_hdy_leaflet_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path)
+{
+ if (!strcmp (action_path, "insert_page_after") ||
+ !strcmp (action_path, "insert_page_before")) {
+ GladeWidget *parent = glade_widget_get_from_gobject (container);
+ GladeProperty *property;
+ g_autofree gchar *name = NULL;
+ GtkWidget *new_widget;
+ gint pages, index;
+
+ glade_widget_property_get (parent, "pages", &pages);
+
+ glade_command_push_group (_("Insert placeholder to %s"),
+ glade_widget_get_name (parent));
+
+ index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (object));
+
+ if (!strcmp (action_path, "insert_page_after"))
+ index++;
+
+ name = get_unused_name (GTK_CONTAINER (container));
+ new_widget = glade_placeholder_new ();
+ add_named (GTK_CONTAINER (container), new_widget, name);
+ glade_hdy_reorder_child (GTK_CONTAINER (container), new_widget, index);
+ g_object_set (container, "visible-child", new_widget, NULL);
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ property = glade_widget_get_property (parent, "pages");
+ glade_command_set_property (property, pages + 1);
+
+ property = glade_widget_get_property (parent, "page");
+ glade_command_set_property (property, index);
+
+ glade_command_pop_group ();
+ } else if (strcmp (action_path, "remove_page") == 0) {
+ GladeWidget *parent = glade_widget_get_from_gobject (container);
+ GladeProperty *property;
+ gint pages, index;
+
+ glade_widget_property_get (parent, "pages", &pages);
+
+ glade_command_push_group (_("Remove placeholder from %s"),
+ glade_widget_get_name (parent));
+ g_assert (GLADE_IS_PLACEHOLDER (object));
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (object));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ property = glade_widget_get_property (parent, "pages");
+ glade_command_set_property (property, pages - 1);
+
+ glade_widget_property_get (parent, "page", &index);
+ property = glade_widget_get_property (parent, "page");
+ glade_command_set_property (property, index);
+
+ glade_command_pop_group ();
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_action_activate (adaptor,
+ container,
+ object,
+ action_path);
+ }
+}
+
+typedef struct {
+ gint size;
+ gboolean include_placeholders;
+} ChildData;
+
+static void
+count_child (GtkWidget *child,
+ gpointer data)
+{
+ ChildData *cdata = data;
+
+ if (cdata->include_placeholders || !GLADE_IS_PLACEHOLDER (child))
+ cdata->size++;
+}
+
+static gint
+get_n_pages (GtkContainer *container,
+ gboolean include_placeholders)
+{
+ ChildData data;
+
+ data.size = 0;
+ data.include_placeholders = include_placeholders;
+ gtk_container_foreach (container, count_child, &data);
+
+ return data.size;
+}
+
+static void
+set_n_pages (GObject *object,
+ const GValue *value)
+{
+ GladeWidget *gbox;
+ GtkContainer *container = GTK_CONTAINER (object);
+ GtkWidget *child;
+ gint new_size = g_value_get_int (value);
+ gint old_size = get_n_pages (container, TRUE);
+ gint i, page;
+
+ if (old_size == new_size)
+ return;
+
+ for (i = old_size; i < new_size; i++) {
+ g_autofree gchar *name = get_unused_name (container);
+ child = glade_placeholder_new ();
+ add_named (container, child, name);
+ }
+
+ for (i = old_size; i > 0; i--) {
+ if (old_size <= new_size)
+ break;
+
+ child = glade_hdy_get_nth_child (container, i - 1);
+ if (GLADE_IS_PLACEHOLDER (child)) {
+ gtk_container_remove (container, child);
+ old_size--;
+ }
+ }
+
+ gbox = glade_widget_get_from_gobject (container);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+static void
+set_page (GObject *object,
+ const GValue *value)
+{
+ gint new_page = g_value_get_int (value);
+ GtkWidget *child = glade_hdy_get_nth_child (GTK_CONTAINER (object), new_page);
+
+ if (!child)
+ return;
+
+ g_object_set (object, "visible-child", child, NULL);
+}
+
+static gint
+get_page (GtkContainer *container)
+{
+ GtkWidget *child;
+
+ g_object_get (container, "visible-child", &child, NULL);
+
+ return glade_hdy_get_child_index (container, child);
+}
+
+void
+glade_hdy_leaflet_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "pages"))
+ set_n_pages (object, value);
+ else if (!strcmp (id, "page"))
+ set_page (object, value);
+ else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->set_property (adaptor, object, id, value);
+}
+
+void
+glade_hdy_leaflet_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value)
+{
+ if (!strcmp (id, "pages")) {
+ g_value_reset (value);
+ g_value_set_int (value, get_n_pages (GTK_CONTAINER (object), TRUE));
+ } else if (!strcmp (id, "page")) {
+ g_value_reset (value);
+ g_value_set_int (value, get_page (GTK_CONTAINER (object)));
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->get_property (adaptor, object, id, value);
+ }
+}
+
+static gboolean
+verify_n_pages (GObject *object,
+ const GValue *value)
+{
+ gint new_size = g_value_get_int (value);
+ gint old_size = get_n_pages (GTK_CONTAINER (object), FALSE);
+
+ return old_size <= new_size;
+}
+
+static gboolean
+verify_page (GObject *object,
+ const GValue *value)
+{
+ gint page = g_value_get_int (value);
+ gint pages = get_n_pages (GTK_CONTAINER (object), TRUE);
+
+ if (page < 0 && page >= pages)
+ return FALSE;
+
+ if (HDY_IS_LEAFLET (object)) {
+ GtkWidget *child = glade_hdy_get_nth_child (GTK_CONTAINER (object), page);
+ gboolean navigatable;
+
+ gtk_container_child_get (GTK_CONTAINER (object), child,
+ "navigatable", &navigatable,
+ NULL);
+
+ if (!navigatable)
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+gboolean
+glade_hdy_leaflet_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value)
+{
+ if (!strcmp (id, "pages"))
+ return verify_n_pages (object, value);
+ else if (!strcmp (id, "page"))
+ return verify_page (object, value);
+ else if (GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property)
+ return GWA_GET_CLASS (GTK_TYPE_CONTAINER)->verify_property (adaptor, object, id, value);
+
+ return TRUE;
+}
+
+
+void
+glade_hdy_leaflet_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0)
+ g_value_set_int (value, glade_hdy_get_child_index (GTK_CONTAINER (container),
+ GTK_WIDGET (child)));
+ else
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+}
+
+void
+glade_hdy_leaflet_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (strcmp (property_name, "position") == 0) {
+ glade_hdy_reorder_child (GTK_CONTAINER (container),
+ GTK_WIDGET (child),
+ g_value_get_int (value));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
+
+void
+glade_hdy_leaflet_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GladeWidget *gbox, *gchild;
+ gint pages, page;
+
+ if (!glade_widget_superuser () && !GLADE_IS_PLACEHOLDER (child)) {
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (object));
+ GList *l;
+
+ for (l = g_list_last (children); l; l = l->prev) {
+ GtkWidget *widget = l->data;
+ if (GLADE_IS_PLACEHOLDER (widget)) {
+ gtk_container_remove (GTK_CONTAINER (object), widget);
+
+ break;
+ }
+ }
+ }
+
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (object));
+
+ gchild = glade_widget_get_from_gobject (child);
+ if (gchild != NULL)
+ glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE);
+
+ gbox = glade_widget_get_from_gobject (object);
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+void
+glade_hdy_leaflet_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GladeWidget *gbox;
+ gint pages, page;
+
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (object));
+
+ gbox = glade_widget_get_from_gobject (object);
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
+
+void
+glade_hdy_leaflet_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget)
+{
+ GladeWidget *gchild;
+ GladeWidget *gbox;
+ gint pages, page, index;
+
+ index = glade_hdy_get_child_index (GTK_CONTAINER (container), GTK_WIDGET (current));
+ gtk_container_remove (GTK_CONTAINER (container), GTK_WIDGET (current));
+ gtk_container_add (GTK_CONTAINER (container), GTK_WIDGET (new_widget));
+ glade_hdy_reorder_child (GTK_CONTAINER (container), GTK_WIDGET (new_widget), index);
+
+ glade_hdy_sync_child_positions (GTK_CONTAINER (container));
+
+ gbox = glade_widget_get_from_gobject (container);
+
+ gchild = glade_widget_get_from_gobject (new_widget);
+ if (gchild != NULL)
+ glade_widget_set_pack_action_visible (gchild, "remove_page", FALSE);
+
+ /* NOTE: make sure to sync this at the end because new_widget could be
+ * a placeholder and syncing these properties could destroy it.
+ */
+ glade_widget_property_get (gbox, "pages", &pages);
+ glade_widget_property_set (gbox, "pages", pages);
+ glade_widget_property_get (gbox, "page", &page);
+ glade_widget_property_set (gbox, "page", page);
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-leaflet.h b/subprojects/libhandy/glade/glade-hdy-leaflet.h
new file mode 100644
index 0000000..9f274b0
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-leaflet.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+
+void glade_hdy_leaflet_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+void glade_hdy_leaflet_child_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_leaflet_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+void glade_hdy_leaflet_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ GValue *value);
+gboolean glade_hdy_leaflet_verify_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *id,
+ const GValue *value);
+
+void glade_hdy_leaflet_get_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
+void glade_hdy_leaflet_set_child_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
+
+void glade_hdy_leaflet_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_leaflet_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_leaflet_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget);
diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-page.c b/subprojects/libhandy/glade/glade-hdy-preferences-page.c
new file mode 100644
index 0000000..f1a62c7
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-preferences-page.c
@@ -0,0 +1,218 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-preferences-page.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+static GtkWidget *
+get_child_by_title (GtkContainer *container,
+ const gchar *title)
+{
+ g_autoptr (GList) children = gtk_container_get_children (container);
+ GList *l;
+
+ for (l = children; l; l = l->next) {
+ const gchar *child_title;
+
+ g_assert (HDY_IS_PREFERENCES_GROUP (l->data));
+
+ child_title = hdy_preferences_group_get_title (HDY_PREFERENCES_GROUP (l->data));
+
+ if (child_title && !strcmp (child_title, title))
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static gchar *
+get_unused_title (GtkContainer *container)
+{
+ gint i = 1;
+
+ while (TRUE) {
+ g_autofree gchar *title = g_strdup_printf ("Group %d", i);
+
+ if (get_child_by_title (container, title) == NULL)
+ return g_steal_pointer (&title);
+
+ i++;
+ }
+
+ return NULL;
+}
+
+static void
+add_group (GladeWidgetAdaptor *adaptor,
+ GObject *container)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (container);
+ GladeWidget *gpage;
+ GladeWidgetAdaptor *page_adaptor;
+ g_autofree gchar *title = get_unused_title (GTK_CONTAINER (container));
+
+ page_adaptor = glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP);
+
+ gpage = glade_widget_adaptor_create_widget (page_adaptor, FALSE,
+ "parent", gwidget,
+ "project", glade_widget_get_project (gwidget),
+ NULL);
+
+ glade_widget_property_set (gpage, "title", title);
+
+ glade_widget_add_child (gwidget, gpage, FALSE);
+}
+
+void
+glade_hdy_preferences_page_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ if (reason == GLADE_CREATE_USER) {
+ add_group (adaptor, container);
+ add_group (adaptor, container);
+ add_group (adaptor, container);
+ }
+}
+
+gboolean
+glade_hdy_preferences_page_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback)
+{
+ if (!HDY_IS_PREFERENCES_GROUP (child)) {
+ if (user_feedback) {
+ GladeWidgetAdaptor *page_adaptor =
+ glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP);
+
+ glade_util_ui_message (glade_app_get_window (),
+ GLADE_UI_INFO, NULL,
+ ONLY_THIS_GOES_IN_THAT_MSG,
+ glade_widget_adaptor_get_title (page_adaptor),
+ glade_widget_adaptor_get_title (adaptor));
+ }
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+glade_hdy_preferences_page_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+}
+
+void
+glade_hdy_preferences_page_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+}
+
+void
+glade_hdy_preferences_page_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *current,
+ GObject *new_widget)
+{
+ gint index = glade_hdy_get_child_index (GTK_CONTAINER (object), GTK_WIDGET (current));
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (current));
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (new_widget));
+ glade_hdy_reorder_child (GTK_CONTAINER (object), GTK_WIDGET (new_widget), index);
+}
+
+GList *
+glade_hdy_preferences_page_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object)
+{
+ return gtk_container_get_children (GTK_CONTAINER (object));
+}
+
+void
+glade_hdy_preferences_page_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path)
+{
+ GladeWidget *parent = glade_widget_get_from_gobject (object);
+
+ if (!g_strcmp0 (action_path, "add_group")) {
+ g_autofree gchar *title = get_unused_title (GTK_CONTAINER (object));
+ GladeWidget *gchild;
+
+ glade_command_push_group (_("Add group to %s"),
+ glade_widget_get_name (parent));
+
+ gchild = glade_command_create (glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_GROUP),
+ parent,
+ NULL,
+ glade_widget_get_project (parent));
+
+ glade_widget_property_set (gchild, "title", title);
+
+ glade_command_pop_group ();
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor,
+ object,
+ action_path);
+ }
+}
+
+void
+glade_hdy_preferences_page_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value)
+{
+ if (!g_strcmp0 (property_name, "position")) {
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child));
+
+ gtk_container_child_set_property (GTK_CONTAINER (parent),
+ GTK_WIDGET (child), property_name, value);
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
+
+void
+glade_hdy_preferences_page_child_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (!g_strcmp0 (property_name, "position")) {
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child));
+
+ gtk_container_child_get_property (GTK_CONTAINER (parent),
+ GTK_WIDGET (child), property_name, value);
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-page.h b/subprojects/libhandy/glade/glade-hdy-preferences-page.h
new file mode 100644
index 0000000..2fd3f88
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-preferences-page.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+void glade_hdy_preferences_page_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+gboolean glade_hdy_preferences_page_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback);
+
+void glade_hdy_preferences_page_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_preferences_page_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_preferences_page_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget);
+
+GList *glade_hdy_preferences_page_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object);
+
+void glade_hdy_preferences_page_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_preferences_page_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value);
+
+void glade_hdy_preferences_page_child_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-window.c b/subprojects/libhandy/glade/glade-hdy-preferences-window.c
new file mode 100644
index 0000000..1add369
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-preferences-window.c
@@ -0,0 +1,286 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-stack.c - GladeWidgetAdaptor for GtkStack
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-preferences-window.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+static void
+selection_changed_cb (GladeProject *project,
+ GladeWidget *gwidget)
+{
+ GList *list;
+ GtkWidget *page, *sel_widget;
+ GtkContainer *container = GTK_CONTAINER (glade_widget_get_object (gwidget));
+ gint index;
+
+ if ((list = glade_project_selection_get (project)) != NULL &&
+ g_list_length (list) == 1) {
+ sel_widget = list->data;
+
+ if (GTK_IS_WIDGET (sel_widget) &&
+ gtk_widget_is_ancestor (sel_widget, GTK_WIDGET (container))) {
+ g_autoptr (GList) children = gtk_container_get_children (container);
+ GList *l;
+
+ index = 0;
+ for (l = children; l; l = l->next) {
+ page = l->data;
+ if (sel_widget == page ||
+ gtk_widget_is_ancestor (sel_widget, page)) {
+ GtkWidget *parent = gtk_widget_get_parent (page);
+
+ g_object_set (parent, "visible-child", page, NULL);
+
+ break;
+ }
+
+ index++;
+ }
+ }
+ }
+}
+
+static void
+project_changed_cb (GladeWidget *gwidget,
+ GParamSpec *pspec,
+ gpointer userdata)
+{
+ GladeProject *project = glade_widget_get_project (gwidget);
+ GladeProject *old_project = g_object_get_data (G_OBJECT (gwidget),
+ "project-ptr");
+
+ if (old_project)
+ g_signal_handlers_disconnect_by_func (G_OBJECT (old_project),
+ G_CALLBACK (selection_changed_cb),
+ gwidget);
+
+ if (project)
+ g_signal_connect (G_OBJECT (project),
+ "selection-changed",
+ G_CALLBACK (selection_changed_cb),
+ gwidget);
+
+ g_object_set_data (G_OBJECT (gwidget), "project-ptr", project);
+}
+
+static GtkWidget *
+get_child_by_title (GtkContainer *container,
+ const gchar *title)
+{
+ g_autoptr (GList) children = gtk_container_get_children (container);
+ GList *l;
+
+ for (l = children; l; l = l->next) {
+ const gchar *child_title;
+
+ g_assert (HDY_IS_PREFERENCES_PAGE (l->data));
+
+ child_title = hdy_preferences_page_get_title (HDY_PREFERENCES_PAGE (l->data));
+
+ if (child_title && !strcmp (child_title, title))
+ return l->data;
+ }
+
+ return NULL;
+}
+
+static gchar *
+get_unused_title (GtkContainer *container)
+{
+ gint i = 1;
+
+ while (TRUE) {
+ g_autofree gchar *title = g_strdup_printf ("Page %d", i);
+
+ if (get_child_by_title (container, title) == NULL)
+ return g_steal_pointer (&title);
+
+ i++;
+ }
+
+ return NULL;
+}
+
+static void
+add_page (GladeWidgetAdaptor *adaptor,
+ GObject *container)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (container);
+ GladeWidget *gpage;
+ GladeWidgetAdaptor *page_adaptor;
+ g_autofree gchar *title = get_unused_title (GTK_CONTAINER (container));
+
+ page_adaptor = glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE);
+
+ gpage = glade_widget_adaptor_create_widget (page_adaptor, FALSE,
+ "parent", gwidget,
+ "project", glade_widget_get_project (gwidget),
+ NULL);
+
+ glade_widget_property_set (gpage, "title", title);
+
+ glade_widget_add_child (gwidget, gpage, FALSE);
+}
+
+void
+glade_hdy_preferences_window_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason)
+{
+ GladeWidget *gwidget = glade_widget_get_from_gobject (container);
+
+ if (reason == GLADE_CREATE_USER) {
+ add_page (adaptor, container);
+ add_page (adaptor, container);
+ add_page (adaptor, container);
+ }
+
+ g_signal_connect (G_OBJECT (gwidget),
+ "notify::project",
+ G_CALLBACK (project_changed_cb),
+ NULL);
+
+ project_changed_cb (gwidget, NULL, NULL);
+}
+
+gboolean
+glade_hdy_preferences_window_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback)
+{
+ if (!HDY_IS_PREFERENCES_PAGE (child)) {
+ if (user_feedback) {
+ GladeWidgetAdaptor *page_adaptor =
+ glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE);
+
+ glade_util_ui_message (glade_app_get_window (),
+ GLADE_UI_INFO, NULL,
+ ONLY_THIS_GOES_IN_THAT_MSG,
+ glade_widget_adaptor_get_title (page_adaptor),
+ glade_widget_adaptor_get_title (adaptor));
+ }
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+void
+glade_hdy_preferences_window_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+}
+
+void
+glade_hdy_preferences_window_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+}
+
+void
+glade_hdy_preferences_window_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *current,
+ GObject *new_widget)
+{
+ gint index = glade_hdy_get_child_index (GTK_CONTAINER (object), GTK_WIDGET (current));
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (current));
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (new_widget));
+ glade_hdy_reorder_child (GTK_CONTAINER (object), GTK_WIDGET (new_widget), index);
+}
+
+GList *
+glade_hdy_preferences_window_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object)
+{
+ return gtk_container_get_children (GTK_CONTAINER (object));
+}
+
+void
+glade_hdy_preferences_window_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path)
+{
+ GladeWidget *parent = glade_widget_get_from_gobject (object);
+
+ if (!g_strcmp0 (action_path, "add_page")) {
+ g_autofree gchar *title = get_unused_title (GTK_CONTAINER (object));
+ GladeWidget *gchild;
+
+ glade_command_push_group (_("Add page to %s"),
+ glade_widget_get_name (parent));
+
+ gchild = glade_command_create (glade_widget_adaptor_get_by_type (HDY_TYPE_PREFERENCES_PAGE),
+ parent,
+ NULL,
+ glade_widget_get_project (parent));
+
+ glade_widget_property_set (gchild, "title", title);
+
+ glade_command_pop_group ();
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->action_activate (adaptor,
+ object,
+ action_path);
+ }
+}
+
+void
+glade_hdy_preferences_window_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value)
+{
+ if (!g_strcmp0 (property_name, "position")) {
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child));
+
+ gtk_container_child_set_property (GTK_CONTAINER (parent),
+ GTK_WIDGET (child), property_name, value);
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_set_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
+
+void
+glade_hdy_preferences_window_child_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value)
+{
+ if (!g_strcmp0 (property_name, "position")) {
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (child));
+
+ gtk_container_child_get_property (GTK_CONTAINER (parent),
+ GTK_WIDGET (child), property_name, value);
+ } else {
+ GWA_GET_CLASS (GTK_TYPE_CONTAINER)->child_get_property (adaptor,
+ container,
+ child,
+ property_name,
+ value);
+ }
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-preferences-window.h b/subprojects/libhandy/glade/glade-hdy-preferences-window.h
new file mode 100644
index 0000000..e4f1f37
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-preferences-window.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+void glade_hdy_preferences_window_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GladeCreateReason reason);
+
+gboolean glade_hdy_preferences_window_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback);
+
+void glade_hdy_preferences_window_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_preferences_window_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child);
+void glade_hdy_preferences_window_replace_child (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *current,
+ GObject *new_widget);
+
+GList *glade_hdy_preferences_window_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object);
+
+void glade_hdy_preferences_window_action_activate (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *action_path);
+
+void glade_hdy_preferences_window_child_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ const GValue *value);
+
+void glade_hdy_preferences_window_child_get_property (GladeWidgetAdaptor *adaptor,
+ GObject *container,
+ GObject *child,
+ const gchar *property_name,
+ GValue *value);
diff --git a/subprojects/libhandy/glade/glade-hdy-search-bar.c b/subprojects/libhandy/glade/glade-hdy-search-bar.c
new file mode 100644
index 0000000..c13f1eb
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-search-bar.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-searchbar.c - GladeWidgetAdaptor for GtkSearchBar
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-search-bar.h"
+
+#include <gladeui/glade.h>
+
+void
+glade_hdy_search_bar_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *widget,
+ GladeCreateReason reason)
+{
+ if (reason == GLADE_CREATE_USER) {
+ GtkWidget *child = glade_placeholder_new ();
+ gtk_container_add (GTK_CONTAINER (widget), child);
+ g_object_set_data (G_OBJECT (widget), "child", child);
+ }
+
+ hdy_search_bar_set_search_mode (HDY_SEARCH_BAR (widget), TRUE);
+ hdy_search_bar_set_show_close_button (HDY_SEARCH_BAR (widget), FALSE);
+}
+
+void
+glade_hdy_search_bar_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GObject *current = g_object_get_data (G_OBJECT (object), "child");
+
+ if (current)
+ gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (current))),
+ GTK_WIDGET (current));
+
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+ g_object_set_data (G_OBJECT (object), "child", child);
+}
+
+void
+glade_hdy_search_bar_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GObject *current = g_object_get_data (G_OBJECT (object), "child");
+ GtkWidget *new_child;
+
+ if (current != child)
+ return;
+
+ gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (child))), GTK_WIDGET (child));
+ new_child = glade_placeholder_new ();
+ gtk_container_add (GTK_CONTAINER (object), new_child);
+ g_object_set_data (G_OBJECT (object), "child", new_child);
+}
+
+void
+glade_hdy_search_bar_replace_child (GladeWidgetAdaptor *adaptor,
+ GtkWidget *container,
+ GtkWidget *current,
+ GtkWidget *new_widget)
+{
+ if (current != (GtkWidget *) g_object_get_data (G_OBJECT (container), "child"))
+ return;
+
+ gtk_container_remove (GTK_CONTAINER (gtk_widget_get_parent (GTK_WIDGET (current))),
+ GTK_WIDGET (current));
+ gtk_container_add (GTK_CONTAINER (container), new_widget);
+ g_object_set_data (G_OBJECT (container), "child", new_widget);
+}
+
+GList *
+glade_hdy_search_bar_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *widget)
+{
+ GObject *current = g_object_get_data (G_OBJECT (widget), "child");
+
+ return g_list_append (NULL, current);
+}
+
+gboolean
+glade_hdy_search_bar_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *container,
+ GtkWidget *child,
+ gboolean user_feedback)
+{
+ GObject *current = g_object_get_data (G_OBJECT (container), "child");
+
+ if (!GLADE_IS_PLACEHOLDER (current)) {
+ if (user_feedback)
+ glade_util_ui_message (glade_app_get_window (),
+ GLADE_UI_INFO, NULL,
+ _("Search bar is already full"));
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-search-bar.h b/subprojects/libhandy/glade/glade-hdy-search-bar.h
new file mode 100644
index 0000000..05306b8
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-search-bar.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+void glade_hdy_search_bar_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *widget,
+ GladeCreateReason reason);
+
+void glade_hdy_search_bar_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+void glade_hdy_search_bar_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+void glade_hdy_search_bar_replace_child (GladeWidgetAdaptor *adaptor,
+ GtkWidget *container,
+ GtkWidget *current,
+ GtkWidget *new_widget);
+
+GList *glade_hdy_search_bar_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *widget);
+
+gboolean glade_hdy_search_bar_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *container,
+ GtkWidget *child,
+ gboolean user_feedback);
diff --git a/subprojects/libhandy/glade/glade-hdy-swipe-group.c b/subprojects/libhandy/glade/glade-hdy-swipe-group.c
new file mode 100644
index 0000000..059138d
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-swipe-group.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * hdy-header-group.c - GladeWidgetAdaptor for HdyHeaderGroup
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2013 Tristan Van Berkom
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-swipe-group.h"
+
+#include <gladeui/glade.h>
+#include "glade-hdy-utils.h"
+
+#define PROP_SWIPEABLES "swipeables"
+#define GLADE_TAG_SWIPEGROUP_SWIPEABLES "swipeables"
+#define GLADE_TAG_SWIPEGROUP_SWIPEABLE "swipeable"
+
+static void
+glade_hdy_swipe_group_read_widgets (GladeWidget *widget,
+ GladeXmlNode *node)
+{
+ GladeXmlNode *widgets_node;
+ GladeProperty *property;
+ gchar *string = NULL;
+
+ if ((widgets_node =
+ glade_xml_search_child (node, GLADE_TAG_SWIPEGROUP_SWIPEABLES)) != NULL) {
+ GladeXmlNode *n;
+
+ for (n = glade_xml_node_get_children (widgets_node);
+ n; n = glade_xml_node_next (n)) {
+ gchar *widget_name, *tmp;
+
+ if (!glade_xml_node_verify (n, GLADE_TAG_SWIPEGROUP_SWIPEABLE))
+ continue;
+
+ widget_name = glade_xml_get_property_string_required
+ (n, GLADE_TAG_NAME, NULL);
+
+ if (string == NULL) {
+ string = widget_name;
+ } else if (widget_name != NULL) {
+ tmp =
+ g_strdup_printf ("%s%s%s", string, GLADE_PROPERTY_DEF_OBJECT_DELIMITER,
+ widget_name);
+ string = (g_free (string), tmp);
+ g_free (widget_name);
+ }
+ }
+ }
+
+ if (string) {
+ property = glade_widget_get_property (widget, PROP_SWIPEABLES);
+ g_assert (property);
+
+ /* we must synchronize this directly after loading this project
+ * (i.e. lookup the actual objects after they've been parsed and
+ * are present).
+ */
+ g_object_set_data_full (G_OBJECT (property),
+ "glade-loaded-object", string, g_free);
+ }
+}
+
+void
+glade_hdy_swipe_group_read_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlNode *node)
+{
+ if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) ||
+ glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE)))
+ return;
+
+ /* First chain up and read in all the normal properties.. */
+ GWA_GET_CLASS (G_TYPE_OBJECT)->read_widget (adaptor, widget, node);
+
+ glade_hdy_swipe_group_read_widgets (widget, node);
+}
+
+static void
+glade_hdy_swipe_group_write_widgets (GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node)
+{
+ GladeXmlNode *widgets_node, *widget_node;
+ GList *widgets = NULL, *list;
+ GladeWidget *awidget;
+
+ widgets_node = glade_xml_node_new (context, GLADE_TAG_SWIPEGROUP_SWIPEABLES);
+
+ if (glade_widget_property_get (widget, PROP_SWIPEABLES, &widgets)) {
+ for (list = widgets; list; list = list->next) {
+ awidget = glade_widget_get_from_gobject (list->data);
+ widget_node =
+ glade_xml_node_new (context, GLADE_TAG_SWIPEGROUP_SWIPEABLE);
+ glade_xml_node_append_child (widgets_node, widget_node);
+ glade_xml_node_set_property_string (widget_node, GLADE_TAG_NAME,
+ glade_widget_get_name (awidget));
+ }
+ }
+
+ if (!glade_xml_node_get_children (widgets_node))
+ glade_xml_node_delete (widgets_node);
+ else
+ glade_xml_node_append_child (node, widgets_node);
+}
+
+void
+glade_hdy_swipe_group_write_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node)
+{
+ if (!(glade_xml_node_verify_silent (node, GLADE_XML_TAG_WIDGET) ||
+ glade_xml_node_verify_silent (node, GLADE_XML_TAG_TEMPLATE)))
+ return;
+
+ /* First chain up and read in all the normal properties.. */
+ GWA_GET_CLASS (G_TYPE_OBJECT)->write_widget (adaptor, widget, context, node);
+
+ glade_hdy_swipe_group_write_widgets (widget, context, node);
+}
+
+void
+glade_hdy_swipe_group_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *property_name,
+ const GValue *value)
+{
+ if (!strcmp (property_name, PROP_SWIPEABLES)) {
+ GSList *sg_widgets, *slist;
+ GList *widgets, *list;
+
+ /* remove old widgets */
+ if ((sg_widgets =
+ hdy_swipe_group_get_swipeables (HDY_SWIPE_GROUP (object))) != NULL) {
+ /* copy since we are modifying an internal list */
+ sg_widgets = g_slist_copy (sg_widgets);
+ for (slist = sg_widgets; slist; slist = slist->next)
+ hdy_swipe_group_remove_swipeable (HDY_SWIPE_GROUP (object),
+ HDY_SWIPEABLE (slist->data));
+ g_slist_free (sg_widgets);
+ }
+
+ /* add new widgets */
+ if ((widgets = g_value_get_boxed (value)) != NULL) {
+ for (list = widgets; list; list = list->next)
+ hdy_swipe_group_add_swipeable (HDY_SWIPE_GROUP (object),
+ HDY_SWIPEABLE (list->data));
+ }
+ } else {
+ GWA_GET_CLASS (G_TYPE_OBJECT)->set_property (adaptor, object,
+ property_name, value);
+ }
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-swipe-group.h b/subprojects/libhandy/glade/glade-hdy-swipe-group.h
new file mode 100644
index 0000000..5f593e5
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-swipe-group.h
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * hdy-header-group.h - GladeWidgetAdaptor for HdyHeaderGroup
+ * Copyright (C) 2018 Purism SPC
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+
+void glade_hdy_swipe_group_set_property (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ const gchar *property_name,
+ const GValue *value);
+void glade_hdy_swipe_group_write_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlContext *context,
+ GladeXmlNode *node);
+void glade_hdy_swipe_group_read_widget (GladeWidgetAdaptor *adaptor,
+ GladeWidget *widget,
+ GladeXmlNode *node);
diff --git a/subprojects/libhandy/glade/glade-hdy-utils.c b/subprojects/libhandy/glade/glade-hdy-utils.c
new file mode 100644
index 0000000..81b8ae5
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-utils.c
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-utils.h"
+
+#include <gladeui/glade.h>
+
+void
+glade_hdy_init (const gchar *name)
+{
+ g_assert (strcmp (name, "libhandy") == 0);
+
+ gtk_init (NULL, NULL);
+ hdy_init ();
+}
+
+/* This function has been copied and modified from:
+ * glade-gtk-list-box.c - GladeWidgetAdaptor for GtkListBox widget
+ *
+ * Copyright (C) 2013 Kalev Lember
+ *
+ * Authors:
+ * Kalev Lember <kalevlember@gmail.com>
+ */
+void
+glade_hdy_sync_child_positions (GtkContainer *container)
+{
+ g_autoptr (GList) children = NULL;
+ GList *l;
+ gint position;
+ static gboolean recursion = FALSE;
+
+ /* Avoid feedback loop */
+ if (recursion)
+ return;
+
+ children = gtk_container_get_children (container);
+
+ position = 0;
+ for (l = children; l; l = l->next) {
+ gint old_position;
+
+ glade_widget_pack_property_get (glade_widget_get_from_gobject (l->data),
+ "position", &old_position);
+ if (position != old_position) {
+ /* Update glade with the new value */
+ recursion = TRUE;
+ glade_widget_pack_property_set (glade_widget_get_from_gobject (l->data),
+ "position", position);
+ recursion = FALSE;
+ }
+
+ position++;
+ }
+}
+
+gint
+glade_hdy_get_child_index (GtkContainer *container,
+ GtkWidget *child)
+{
+ g_autoptr (GList) children = gtk_container_get_children (container);
+
+ return g_list_index (children, child);
+}
+
+void
+glade_hdy_reorder_child (GtkContainer *container,
+ GtkWidget *child,
+ gint index)
+{
+ gint old_index = glade_hdy_get_child_index (container, child);
+ gint i = 0, n;
+ g_autoptr (GList) children = NULL;
+ g_autoptr (GList) removed_children = NULL;
+ GList *l;
+
+ if (old_index == index)
+ return;
+
+ gtk_container_remove (container, g_object_ref (child));
+
+ children = gtk_container_get_children (container);
+ n = g_list_length (children);
+
+ children = g_list_reverse (children);
+ l = children;
+
+ if (index > old_index)
+ n--;
+
+ for (i = n - 1; i >= index; i--) {
+ GtkWidget *last_child = l->data;
+
+ gtk_container_remove (container, g_object_ref (last_child));
+ l = l->next;
+
+ removed_children = g_list_prepend (removed_children, last_child);
+ }
+
+ removed_children = g_list_prepend (removed_children, child);
+
+ for (l = removed_children; l; l = l->next) {
+ gtk_container_add (container, l->data);
+ g_object_unref (l->data);
+ }
+}
+
+GtkWidget *
+glade_hdy_get_nth_child (GtkContainer *container,
+ gint n)
+{
+ g_autoptr (GList) children = gtk_container_get_children (container);
+
+ return g_list_nth_data (children, n);
+}
diff --git a/subprojects/libhandy/glade/glade-hdy-utils.h b/subprojects/libhandy/glade/glade-hdy-utils.h
new file mode 100644
index 0000000..11870d1
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-utils.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+#define ONLY_THIS_GOES_IN_THAT_MSG _("Only objects of type %s can be added to objects of type %s.")
+
+/* Guess whether we are using a Glade version older than 3.36.
+ *
+ * If yes, redefine some symbols which got renamed.
+ */
+#ifndef GLADE_PROPERTY_DEF_OBJECT_DELIMITER
+#define GLADE_PROPERTY_DEF_OBJECT_DELIMITER GPC_OBJECT_DELIMITER
+#define glade_widget_action_get_def glade_widget_action_get_class
+#endif
+
+void glade_hdy_init (const gchar *name);
+
+void glade_hdy_sync_child_positions (GtkContainer *container);
+
+gint glade_hdy_get_child_index (GtkContainer *container,
+ GtkWidget *child);
+
+void glade_hdy_reorder_child (GtkContainer *container,
+ GtkWidget *child,
+ gint index);
+
+GtkWidget *glade_hdy_get_nth_child (GtkContainer *container,
+ gint n);
diff --git a/subprojects/libhandy/glade/glade-hdy-window.c b/subprojects/libhandy/glade/glade-hdy-window.c
new file mode 100644
index 0000000..7832f91
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-window.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Based on
+ * glade-gtk-searchbar.c - GladeWidgetAdaptor for GtkSearchBar
+ * Copyright (C) 2014 Red Hat, Inc.
+ */
+
+#include <config.h>
+#include <glib/gi18n-lib.h>
+
+#include "glade-hdy-window.h"
+
+#include <gladeui/glade.h>
+
+#define ALREADY_HAS_A_CHILD_MSG _("%s cannot have more than one child.")
+
+static GtkWidget *
+get_child (GtkContainer *window)
+{
+ g_autoptr (GList) children = gtk_container_get_children (window);
+
+ if (!children)
+ return NULL;
+
+ return children->data;
+}
+
+void
+glade_hdy_window_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GladeCreateReason reason)
+{
+ if (reason != GLADE_CREATE_USER)
+ return;
+
+ gtk_container_add (GTK_CONTAINER (object), glade_placeholder_new ());
+}
+
+void
+glade_hdy_window_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ GtkWidget *window_child = get_child (GTK_CONTAINER (object));
+
+ if (window_child) {
+ if (GLADE_IS_PLACEHOLDER (window_child)) {
+ gtk_container_remove (GTK_CONTAINER (object), window_child);
+ } else {
+ g_critical ("Can't add more than one widget to a HdyWindow");
+
+ return;
+ }
+ }
+
+ gtk_container_add (GTK_CONTAINER (object), GTK_WIDGET (child));
+}
+
+void
+glade_hdy_window_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child)
+{
+ gtk_container_remove (GTK_CONTAINER (object), GTK_WIDGET (child));
+ gtk_container_add (GTK_CONTAINER (object), glade_placeholder_new ());
+}
+
+void
+glade_hdy_window_replace_child (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *current,
+ GtkWidget *new_widget)
+{
+ gtk_container_remove (GTK_CONTAINER (object), current);
+ gtk_container_add (GTK_CONTAINER (object), new_widget);
+}
+
+GList *
+glade_hdy_window_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object)
+{
+ return gtk_container_get_children (GTK_CONTAINER (object));
+}
+
+gboolean
+glade_hdy_window_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback)
+{
+ GtkWidget *window_child = get_child (GTK_CONTAINER (object));
+
+ if (window_child && !GLADE_IS_PLACEHOLDER (window_child)) {
+ if (user_feedback)
+ glade_util_ui_message (glade_app_get_window (),
+ GLADE_UI_INFO, NULL,
+ ALREADY_HAS_A_CHILD_MSG,
+ glade_widget_adaptor_get_title (adaptor));
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
diff --git a/subprojects/libhandy/glade/glade-hdy-window.h b/subprojects/libhandy/glade/glade-hdy-window.h
new file mode 100644
index 0000000..75f15e4
--- /dev/null
+++ b/subprojects/libhandy/glade/glade-hdy-window.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gladeui/glade.h>
+
+#include <handy.h>
+
+void glade_hdy_window_post_create (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GladeCreateReason reason);
+
+void glade_hdy_window_add_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+void glade_hdy_window_remove_child (GladeWidgetAdaptor *adaptor,
+ GObject *object,
+ GObject *child);
+void glade_hdy_window_replace_child (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *current,
+ GtkWidget *new_widget);
+
+GList *glade_hdy_window_get_children (GladeWidgetAdaptor *adaptor,
+ GObject *object);
+
+gboolean glade_hdy_window_add_verify (GladeWidgetAdaptor *adaptor,
+ GtkWidget *object,
+ GtkWidget *child,
+ gboolean user_feedback);
diff --git a/subprojects/libhandy/glade/libhandy.xml b/subprojects/libhandy/glade/libhandy.xml
new file mode 100644
index 0000000..7f60118
--- /dev/null
+++ b/subprojects/libhandy/glade/libhandy.xml
@@ -0,0 +1,420 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<glade-catalog name="libhandy" library="glade-handy-1" depends="gtk+" book="libhandy">
+ <init-function>glade_hdy_init</init-function>
+ <glade-widget-classes>
+ <glade-widget-class name="HdyActionRow" generic-name="actionrow" title="Action Row" since="0.0.6">
+ <properties>
+ <property id="icon-name" themed-icon="True" />
+ <property id="title" translatable="True" />
+ <property id="subtitle" translatable="True" />
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyAvatar" generic-name="avatar" title="Avatar" since="1.0"/>
+ <glade-widget-class name="HdyApplicationWindow" generic-name="applicationwindow" title="Application Window" since="1.0" use-placeholders="False">
+ <post-create-function>glade_hdy_window_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_window_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_window_add_child</add-child-function>
+ <remove-child-function>glade_hdy_window_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_window_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_window_get_children</get-children-function>
+ <properties>
+ <property id="show-menubar" disabled="True" />
+ <property id="use-csd" disabled="True" />
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyCarousel" generic-name="carousel" title="Carousel" since="1.0">
+ <post-create-function>glade_hdy_carousel_post_create</post-create-function>
+ <add-child-function>glade_hdy_carousel_add_child</add-child-function>
+ <remove-child-function>glade_hdy_carousel_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_carousel_replace_child</replace-child-function>
+ <child-action-activate-function>glade_hdy_carousel_child_action_activate</child-action-activate-function>
+ <get-property-function>glade_hdy_carousel_get_property</get-property-function>
+ <set-property-function>glade_hdy_carousel_set_property</set-property-function>
+ <child-set-property-function>glade_hdy_carousel_set_child_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_carousel_get_child_property</child-get-property-function>
+ <verify-function>glade_hdy_carousel_verify_property</verify-function>
+ <packing-properties>
+ <property id="position" name="Position" default="0" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>The position of the page in the carousel</tooltip>
+ </property>
+ </packing-properties>
+ <packing-actions>
+ <action id="insert_page_before" name="Insert Page Before" stock="list-add"/>
+ <action id="insert_page_after" name="Insert Page After" stock="list-add"/>
+ <action id="remove_page" name="Remove Page" stock="list-remove"/>
+ </packing-actions>
+ <properties>
+ <property id="pages" name="Number of pages" save="False" default="1">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>1</min>
+ </parameter-spec>
+ <tooltip>The number of pages in the stack</tooltip>
+ </property>
+ <property id="page" name="Edit page" save="False" default="0">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>Set the currently active page to edit, this property will not be saved</tooltip>
+ </property>
+ <property id="above-child" disabled="True" />
+ <property id="visible-window" disabled="True" />
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyCarouselIndicatorDots" generic-name="carouselindicatordots" title="Carousel Indicator Dots" since="1.0"/>
+ <glade-widget-class name="HdyCarouselIndicatorLines" generic-name="carouselindicatorlines" title="Carousel Indicator Lines" since="1.0"/>
+ <glade-widget-class name="HdyClamp" generic-name="clamp" title="Clamp" since="1.0"/>
+ <glade-widget-class name="HdyComboRow" generic-name="comborow" title="Combo Row" since="0.0.6"/>
+ <glade-widget-class name="HdyDeck" generic-name="deck" title="Deck" since="1.0">
+ <post-create-function>glade_hdy_leaflet_post_create</post-create-function>
+ <add-child-function>glade_hdy_leaflet_add_child</add-child-function>
+ <remove-child-function>glade_hdy_leaflet_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_leaflet_replace_child</replace-child-function>
+ <child-action-activate-function>glade_hdy_leaflet_child_action_activate</child-action-activate-function>
+ <get-property-function>glade_hdy_leaflet_get_property</get-property-function>
+ <set-property-function>glade_hdy_leaflet_set_property</set-property-function>
+ <child-set-property-function>glade_hdy_leaflet_set_child_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_leaflet_get_child_property</child-get-property-function>
+ <verify-function>glade_hdy_leaflet_verify_property</verify-function>
+ <packing-properties>
+ <property id="position" name="Position" default="0" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>The position of the page in the deck</tooltip>
+ </property>
+ </packing-properties>
+ <packing-actions>
+ <action id="insert_page_before" name="Insert Page Before" stock="list-add"/>
+ <action id="insert_page_after" name="Insert Page After" stock="list-add"/>
+ <action id="remove_page" name="Remove Page" stock="list-remove"/>
+ </packing-actions>
+ <properties>
+ <property id="pages" name="Number of pages" save="False" default="1">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>1</min>
+ </parameter-spec>
+ <tooltip>The number of pages in the deck</tooltip>
+ </property>
+ <property id="page" name="Edit page" save="False" default="0">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>Set the currently active page to edit, this property will not be saved</tooltip>
+ </property>
+ <property id="transition-type">
+ <displayable-values>
+ <!-- HdyDeckTransitionType enumeration value -->
+ <value id="HDY_DECK_TRANSITION_TYPE_OVER" name="Over"/>
+ <!-- HdyDeckTransitionType enumeration value -->
+ <value id="HDY_DECK_TRANSITION_TYPE_UNDER" name="Under"/>
+ <!-- HdyDeckTransitionType enumeration value -->
+ <value id="HDY_DECK_TRANSITION_TYPE_SLIDE" name="Slide"/>
+ </displayable-values>
+ </property>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyExpanderRow" generic-name="expanderrow" title="Expander Row" since="0.0.6" use-placeholders="False">
+ <post-create-function>glade_hdy_expander_row_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_expander_row_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_expander_row_add_child</add-child-function>
+ <remove-child-function>glade_hdy_expander_row_remove_child</remove-child-function>
+ <child-set-property-function>glade_hdy_expander_row_set_child_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_expander_row_get_child_property</child-get-property-function>
+ <packing-properties>
+ <property id="position" name="Position" default="0" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>The position of the list row in the expander row</tooltip>
+ </property>
+ </packing-properties>
+ <properties>
+ <property id="expanded" save="True" ignore="True"/>
+ <property id="enable-expansion" save="True" ignore="True"/>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyHeaderBar" generic-name="headerbar" title="Header Bar" since="0.0.10">
+ <post-create-function>glade_hdy_header_bar_post_create</post-create-function>
+ <add-child-function>glade_hdy_header_bar_add_child</add-child-function>
+ <remove-child-function>glade_hdy_header_bar_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_header_bar_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_header_bar_get_children</get-children-function>
+ <child-action-activate-function>glade_hdy_header_bar_child_action_activate</child-action-activate-function>
+ <get-property-function>glade_hdy_header_bar_get_property</get-property-function>
+ <set-property-function>glade_hdy_header_bar_set_property</set-property-function>
+ <child-set-property-function>glade_hdy_header_bar_child_set_property</child-set-property-function>
+ <action-activate-function>glade_hdy_header_bar_action_activate</action-activate-function>
+ <verify-function>glade_hdy_header_bar_verify_property</verify-function>
+ <special-child-type>type</special-child-type>
+ <packing-properties>
+ <property id="pack-type" transfer-on-paste="True" />
+ </packing-properties>
+ <packing-actions>
+ <action id="remove_slot" name="Remove Slot" stock="gtk-remove"/>
+ </packing-actions>
+ <properties>
+ <property id="title" translatable="True"/>
+ <property id="subtitle" translatable="True"/>
+ <property id="has-subtitle" name="Reserve space for subtitle">
+ <tooltip>Keep the headerbar height the same as the subtitle changes dynamically.</tooltip>
+ </property>
+ <property id="show-close-button" name="Show window controls" needs-sync="True"/>
+ <property id="spacing"/>
+ <property id="decoration-layout"/>
+ <property id="decoration-layout-set" disabled="True"/>
+ <property id="custom-title" disabled="True"/>
+ <property id="use-custom-title" name="Custom Title" default="FALSE" visible="True" save="False">
+ <parameter-spec>
+ <type>GParamBoolean</type>
+ </parameter-spec>
+ </property>
+ <property visible="True" save="False" id="size" default="1" name="Number of items">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>The number of items in the header bar</tooltip>
+ </property>
+ <property id="centering-policy">
+ <displayable-values>
+ <!-- HdyCenteringPolicy enumeration value -->
+ <value id="HDY_CENTERING_POLICY_LOOSE" name="Loose"/>
+ <!-- HdyCenteringPolicy enumeration value -->
+ <value id="HDY_CENTERING_POLICY_STRICT" name="Strict"/>
+ </displayable-values>
+ </property>
+ </properties>
+ <actions>
+ <action id="add_slot" name="Add Slot" stock="list-add"/>
+ </actions>
+ </glade-widget-class>
+ <glade-widget-class name="HdyHeaderGroup" generic-name="headergroup" title="Header Group" toplevel="True">
+ <read-widget-function>glade_hdy_header_group_read_widget</read-widget-function>
+ <write-widget-function>glade_hdy_header_group_write_widget</write-widget-function>
+ <set-property-function>glade_hdy_header_group_set_property</set-property-function>
+ <properties>
+ <property id="headerbars" name="Headerbars" save="False">
+ <parameter-spec>
+ <type>GladeParamObjects</type>
+ <value-type>HdyHeaderBar</value-type>
+ </parameter-spec>
+ <tooltip>List of headerbars in this group</tooltip>
+ </property>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyKeypad" generic-name="keypad" title="Keypad"/>
+ <glade-widget-class name="HdyLeaflet" generic-name="leaflet" title="Leaflet">
+ <post-create-function>glade_hdy_leaflet_post_create</post-create-function>
+ <add-child-function>glade_hdy_leaflet_add_child</add-child-function>
+ <remove-child-function>glade_hdy_leaflet_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_leaflet_replace_child</replace-child-function>
+ <child-action-activate-function>glade_hdy_leaflet_child_action_activate</child-action-activate-function>
+ <get-property-function>glade_hdy_leaflet_get_property</get-property-function>
+ <set-property-function>glade_hdy_leaflet_set_property</set-property-function>
+ <child-set-property-function>glade_hdy_leaflet_set_child_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_leaflet_get_child_property</child-get-property-function>
+ <verify-function>glade_hdy_leaflet_verify_property</verify-function>
+ <packing-properties>
+ <property id="position" name="Position" default="0" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>The position of the page in the leaflet</tooltip>
+ </property>
+ </packing-properties>
+ <packing-actions>
+ <action id="insert_page_before" name="Insert Page Before" stock="list-add"/>
+ <action id="insert_page_after" name="Insert Page After" stock="list-add"/>
+ <action id="remove_page" name="Remove Page" stock="list-remove"/>
+ </packing-actions>
+ <properties>
+ <property id="pages" name="Number of pages" save="False" default="1">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>1</min>
+ </parameter-spec>
+ <tooltip>The number of pages in the leaflet</tooltip>
+ </property>
+ <property id="page" name="Edit page" save="False" default="0">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>0</min>
+ </parameter-spec>
+ <tooltip>Set the currently active page to edit, this property will not be saved</tooltip>
+ </property>
+ <property id="transition-type">
+ <displayable-values>
+ <!-- HdyLeafletTransitionType enumeration value -->
+ <value id="HDY_LEAFLET_TRANSITION_TYPE_OVER" name="Over"/>
+ <!-- HdyLeafletTransitionType enumeration value -->
+ <value id="HDY_LEAFLET_TRANSITION_TYPE_UNDER" name="Under"/>
+ <!-- HdyLeafletTransitionType enumeration value -->
+ <value id="HDY_LEAFLET_TRANSITION_TYPE_SLIDE" name="Slide"/>
+ </displayable-values>
+ </property>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyPreferencesGroup" generic-name="preferencesgroup" title="Preferences Group" since="0.0.10" use-placeholders="False">
+ <properties>
+ <property id="title" translatable="True" />
+ <property id="description" translatable="True" />
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyPreferencesPage" generic-name="preferencespage" title="Preferences Page" since="0.0.10" use-placeholders="False">
+ <post-create-function>glade_hdy_preferences_page_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_preferences_page_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_preferences_page_add_child</add-child-function>
+ <remove-child-function>glade_hdy_preferences_page_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_preferences_page_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_preferences_page_get_children</get-children-function>
+ <child-set-property-function>glade_hdy_preferences_page_child_set_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_preferences_page_child_get_property</child-get-property-function>
+ <action-activate-function>glade_hdy_preferences_page_action_activate</action-activate-function>
+ <packing-properties>
+ <property id="position" name="Position" default="-1" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>-1</min>
+ </parameter-spec>
+ <tooltip>The position of the group in the preferences page</tooltip>
+ </property>
+ </packing-properties>
+ <properties>
+ <property id="icon-name" themed-icon="True" />
+ <property id="title" translatable="True" />
+ </properties>
+ <actions>
+ <action id="add_group" name="Add Group" stock="list-add" important="True"/>
+ </actions>
+ </glade-widget-class>
+ <glade-widget-class name="HdyPreferencesRow" generic-name="preferencesrow" title="Preferences Row" since="0.0.10"/>
+ <glade-widget-class name="HdyPreferencesWindow" generic-name="preferenceswindow" title="Preferences Window" since="0.0.10" use-placeholders="False">
+ <post-create-function>glade_hdy_preferences_window_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_preferences_window_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_preferences_window_add_child</add-child-function>
+ <remove-child-function>glade_hdy_preferences_window_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_preferences_window_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_preferences_window_get_children</get-children-function>
+ <child-set-property-function>glade_hdy_preferences_window_child_set_property</child-set-property-function>
+ <child-get-property-function>glade_hdy_preferences_window_child_get_property</child-get-property-function>
+ <action-activate-function>glade_hdy_preferences_window_action_activate</action-activate-function>
+ <packing-properties>
+ <property id="position" name="Position" default="-1" save="False">
+ <parameter-spec>
+ <type>GParamInt</type>
+ <min>-1</min>
+ </parameter-spec>
+ <tooltip>The position of the page in the preferences window</tooltip>
+ </property>
+ </packing-properties>
+ <properties>
+ <property id="use-csd" disabled="True" />
+ </properties>
+ <actions>
+ <action id="add_page" name="Add Page" stock="list-add" important="True"/>
+ </actions>
+ </glade-widget-class>
+ <glade-widget-class name="HdySearchBar" generic-name="searchbar" title="Search Bar" since="0.0.6">
+ <post-create-function>glade_hdy_search_bar_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_search_bar_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_search_bar_add_child</add-child-function>
+ <remove-child-function>glade_hdy_search_bar_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_search_bar_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_search_bar_get_children</get-children-function>
+ <properties>
+ <property id="search-mode-enabled" save="True" ignore="True"/>
+ <property id="show-close-button" save="True" ignore="True"/>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdySqueezer" generic-name="squeezer" title="Squeezer" since="0.0.10"/>
+ <glade-widget-class name="HdySwipeGroup" generic-name="swipegroup" title="Swipe Group" toplevel="True">
+ <read-widget-function>glade_hdy_swipe_group_read_widget</read-widget-function>
+ <write-widget-function>glade_hdy_swipe_group_write_widget</write-widget-function>
+ <set-property-function>glade_hdy_swipe_group_set_property</set-property-function>
+ <properties>
+ <property id="swipeables" name="Widgets" save="False">
+ <parameter-spec>
+ <type>GladeParamObjects</type>
+ <value-type>HdySwipeable</value-type>
+ </parameter-spec>
+ <tooltip>List of widgets in this group</tooltip>
+ </property>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyTitleBar" generic-name="titlebar" title="Title Bar"/>
+ <glade-widget-class name="HdyViewSwitcher" generic-name="viewswitcher" title="View Switcher" since="0.0.10"/>
+ <glade-widget-class name="HdyViewSwitcherBar" generic-name="viewswitcherbar" title="View Switcher Bar" since="0.0.10"/>
+ <glade-widget-class name="HdyViewSwitcherTitle" generic-name="viewswitchertitle" title="View Switcher Title" since="1.0">
+ <properties>
+ <property id="policy">
+ <displayable-values>
+ <!-- HdyViewSwitcherPolicy enumeration value -->
+ <value id="HDY_VIEW_SWITCHER_POLICY_AUTO" name="Auto"/>
+ <!-- HdyViewSwitcherPolicy enumeration value -->
+ <value id="HDY_VIEW_SWITCHER_POLICY_NARROW" name="Narrow"/>
+ <!-- HdyViewSwitcherPolicy enumeration value -->
+ <value id="HDY_VIEW_SWITCHER_POLICY_WIDE" name="Wide"/>
+ </displayable-values>
+ </property>
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyWindow" generic-name="window" title="Window" since="1.0" use-placeholders="False">
+ <post-create-function>glade_hdy_window_post_create</post-create-function>
+ <add-child-verify-function>glade_hdy_window_add_verify</add-child-verify-function>
+ <add-child-function>glade_hdy_window_add_child</add-child-function>
+ <remove-child-function>glade_hdy_window_remove_child</remove-child-function>
+ <replace-child-function>glade_hdy_window_replace_child</replace-child-function>
+ <get-children-function>glade_hdy_window_get_children</get-children-function>
+ <properties>
+ <property id="use-csd" disabled="True" />
+ </properties>
+ </glade-widget-class>
+ <glade-widget-class name="HdyWindowHandle" generic-name="windowhandle" title="Window Handle" since="1.0">
+ <properties>
+ <property id="above-child" disabled="True" />
+ <property id="visible-window" disabled="True" />
+ </properties>
+ </glade-widget-class>
+ </glade-widget-classes>
+
+ <glade-widget-group name="handy" title="Libhandy Widgets">
+ <glade-widget-class-ref name="HdyActionRow"/>
+ <glade-widget-class-ref name="HdyApplicationWindow"/>
+ <glade-widget-class-ref name="HdyAvatar"/>
+ <glade-widget-class-ref name="HdyCarousel"/>
+ <glade-widget-class-ref name="HdyCarouselIndicatorDots"/>
+ <glade-widget-class-ref name="HdyCarouselIndicatorLines"/>
+ <glade-widget-class-ref name="HdyClamp"/>
+ <glade-widget-class-ref name="HdyComboRow"/>
+ <glade-widget-class-ref name="HdyDeck"/>
+ <glade-widget-class-ref name="HdyExpanderRow"/>
+ <glade-widget-class-ref name="HdyHeaderBar"/>
+ <glade-widget-class-ref name="HdyHeaderGroup"/>
+ <glade-widget-class-ref name="HdyKeypad"/>
+ <glade-widget-class-ref name="HdyLeaflet"/>
+ <glade-widget-class-ref name="HdyPreferencesGroup"/>
+ <glade-widget-class-ref name="HdyPreferencesPage"/>
+ <glade-widget-class-ref name="HdyPreferencesRow"/>
+ <glade-widget-class-ref name="HdyPreferencesWindow"/>
+ <glade-widget-class-ref name="HdySearchBar"/>
+ <glade-widget-class-ref name="HdySqueezer"/>
+ <glade-widget-class-ref name="HdySwipeGroup"/>
+ <glade-widget-class-ref name="HdyTitleBar"/>
+ <glade-widget-class-ref name="HdyViewSwitcher"/>
+ <glade-widget-class-ref name="HdyViewSwitcherBar"/>
+ <glade-widget-class-ref name="HdyViewSwitcherTitle"/>
+ <glade-widget-class-ref name="HdyWindow"/>
+ <glade-widget-class-ref name="HdyWindowHandle"/>
+ </glade-widget-group>
+</glade-catalog>
diff --git a/subprojects/libhandy/glade/meson.build b/subprojects/libhandy/glade/meson.build
new file mode 100644
index 0000000..cba9a36
--- /dev/null
+++ b/subprojects/libhandy/glade/meson.build
@@ -0,0 +1,59 @@
+if glade_catalog
+
+glade_xml = 'libhandy.xml'
+module_dir = gladeui_dep.get_pkgconfig_variable('moduledir',
+ define_variable: ['libdir', get_option('libdir')])
+dtd = meson.current_source_dir() / 'glade-catalog.dtd'
+glade_catalogdir = gladeui_dep.get_pkgconfig_variable('catalogdir',
+ define_variable: ['datadir', get_option('datadir')])
+
+libglade_hdy_sources = [
+ 'glade-hdy-carousel.c',
+ 'glade-hdy-expander-row.c',
+ 'glade-hdy-header-bar.c',
+ 'glade-hdy-header-group.c',
+ 'glade-hdy-leaflet.c',
+ 'glade-hdy-preferences-page.c',
+ 'glade-hdy-preferences-window.c',
+ 'glade-hdy-search-bar.c',
+ 'glade-hdy-swipe-group.c',
+ 'glade-hdy-window.c',
+ 'glade-hdy-utils.c',
+]
+
+libglade_hdy_deps = [
+ libhandy_dep,
+ gladeui_dep,
+]
+
+libglade_hdy_args = []
+# Our custom glade module
+libglade_hdy = shared_library(
+ 'glade-handy-' + apiversion,
+ libglade_hdy_sources,
+ c_args: libglade_hdy_args,
+ dependencies: libglade_hdy_deps,
+ include_directories: [ root_inc, src_inc ],
+ install: true,
+ install_dir: module_dir,
+)
+
+# Validate glade catalog
+xmllint = find_program('xmllint', required: true)
+if xmllint.found()
+ custom_target(
+ 'xmllint',
+ build_by_default: true,
+ input: glade_xml,
+ output: 'doesnotexist',
+ command: [xmllint, '--dtdvalid', dtd, '--noout', '@INPUT@'],
+ )
+endif
+
+# Install glade catalog
+install_data(
+ glade_xml,
+ rename: 'libhandy-@0@.xml'.format(apiversion),
+ install_dir: glade_catalogdir)
+
+endif
diff --git a/subprojects/libhandy/glade/rename-id.patch b/subprojects/libhandy/glade/rename-id.patch
new file mode 100644
index 0000000..0dc3ab1
--- /dev/null
+++ b/subprojects/libhandy/glade/rename-id.patch
@@ -0,0 +1,28 @@
+From f4711b392e26d36266a88df4371230418e24cfc9 Mon Sep 17 00:00:00 2001
+From: Adrien Plazas <kekun.plazas@laposte.net>
+Date: Mon, 11 May 2020 10:45:17 +0200
+Subject: [PATCH] Rename the ID to sm.puri.Handy.Glade
+
+Make the GtkApplication ID match the Flatpak ID, which is required for
+the app to start.
+
+---
+ src/main.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/src/main.c b/src/main.c
+index 8a81771f..7a505255 100644
+--- a/src/main.c
++++ b/src/main.c
+@@ -161,7 +161,7 @@ main (int argc, char *argv[])
+ return -1;
+ }
+
+- app = gtk_application_new ("org.gnome.Glade", G_APPLICATION_HANDLES_OPEN);
++ app = gtk_application_new ("sm.puri.Handy.Glade", G_APPLICATION_HANDLES_OPEN);
+
+ g_application_set_option_context_summary (G_APPLICATION (app),
+ N_("Create or edit user interface designs for GTK+ or GNOME applications."));
+--
+2.26.0
+
diff --git a/subprojects/libhandy/glade/sm.puri.Handy.Glade.json b/subprojects/libhandy/glade/sm.puri.Handy.Glade.json
new file mode 100644
index 0000000..ef73458
--- /dev/null
+++ b/subprojects/libhandy/glade/sm.puri.Handy.Glade.json
@@ -0,0 +1,83 @@
+{
+ "app-id" : "sm.puri.Handy.Glade",
+ "runtime" : "org.gnome.Platform",
+ "runtime-version" : "master",
+ "sdk" : "org.gnome.Sdk",
+ "command" : "glade",
+ "rename-desktop-file" : "org.gnome.Glade.desktop",
+ "rename-appdata-file" : "org.gnome.Glade.appdata.xml",
+ "rename-icon" : "org.gnome.Glade",
+ "copy-icon" : true,
+ "desktop-file-name-suffix" : " (Handy Nightly)",
+ "finish-args" : [
+ /* X11 + XShm access */
+ "--share=ipc", "--socket=fallback-x11",
+ /* Wayland access */
+ "--socket=wayland",
+ /* We want full fs access so we can read the files */
+ "--filesystem=host",
+ /* Support GL widgets */
+ "--device=dri"
+ ],
+ "cleanup" : ["/include", "/lib/pkgconfig",
+ "/share/pkgconfig", "/share/aclocal",
+ "/man", "/share/man", "/share/gtk-doc",
+ "/share/vala",
+ "*.la", "*.a"],
+ "modules" : [
+ {
+ "name" : "gnome-common",
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://gitlab.gnome.org/GNOME/gnome-common.git"
+ }
+ ]
+ },
+ {
+ "name" : "intltool",
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://gitlab.gnome.org/World/intltool.git"
+ }
+ ]
+ },
+ {
+ "name" : "glade",
+ "config-opts": [
+ "--disable-man-pages",
+ "--disable-introspection"
+ ],
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://gitlab.gnome.org/GNOME/glade.git",
+ "tag" : "GLADE_3_36_0"
+ },
+ {
+ "type" : "patch",
+ "path" : "rename-id.patch"
+ }
+ ]
+ },
+ {
+ "name" : "libhandy",
+ "buildsystem" : "meson",
+ "builddir" : true,
+ "config-opts" : [
+ "-Dexamples=false",
+ "-Dglade_catalog=enabled",
+ "-Dintrospection=disabled",
+ "-Dtests=false",
+ "-Dvapi=false"
+ ],
+ "sources" : [
+ {
+ "type" : "git",
+ "url" : "https://gitlab.gnome.org/GNOME/libhandy.git"
+ }
+ ]
+ }
+ ]
+}
diff --git a/subprojects/libhandy/libhandy.doap b/subprojects/libhandy/libhandy.doap
new file mode 100644
index 0000000..d5bb1e8
--- /dev/null
+++ b/subprojects/libhandy/libhandy.doap
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Project xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
+ xmlns:foaf="http://xmlns.com/foaf/0.1/"
+ xmlns:gnome="http://api.gnome.org/doap-extensions#"
+ xmlns="http://usefulinc.com/ns/doap#">
+
+ <name>libhandy</name>
+ <shortname>libhandy</shortname>
+ <shortdesc>Building blocks for modern adaptive GNOME apps</shortdesc>
+ <description>
+ libhandy is a collection of GTK widgets for adaptive applications targeting
+ form-factors from mobile to desktop.
+ It also offers innovative widgets following the GNOME design guidelines.
+ </description>
+ <homepage rdf:resource="https://gitlab.gnome.org/GNOME/libhandy" />
+ <license rdf:resource="http://usefulinc.com/doap/licenses/lgpl" />
+
+ <programming-language>C</programming-language>
+
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Guido Günther</foaf:name>
+ <foaf:mbox rdf:resource="mailto:agx@sigxcpu.org" />
+ <gnome:userid>guidog</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+ <maintainer>
+ <foaf:Person>
+ <foaf:name>Adrien Plazas</foaf:name>
+ <foaf:mbox rdf:resource="mailto:kekun.plazas@laposte.net" />
+ <gnome:userid>aplazas</gnome:userid>
+ </foaf:Person>
+ </maintainer>
+
+</Project>
+
diff --git a/subprojects/libhandy/libhandy.syms b/subprojects/libhandy/libhandy.syms
new file mode 100644
index 0000000..64dafcb
--- /dev/null
+++ b/subprojects/libhandy/libhandy.syms
@@ -0,0 +1,6 @@
+LIBHANDY_1_0 {
+ global:
+ hdy_*;
+ local:
+ *;
+};
diff --git a/subprojects/libhandy/lint/api-visibility.sh b/subprojects/libhandy/lint/api-visibility.sh
new file mode 100755
index 0000000..ce8e7f5
--- /dev/null
+++ b/subprojects/libhandy/lint/api-visibility.sh
@@ -0,0 +1,18 @@
+#!/bin/bash
+
+# Check that private headers aren't included in public ones.
+if grep "include.*private.h" $(ls src/*.h | grep -v "private.h");
+then
+ echo "Private headers shouldn't be included in public ones."
+ exit 1
+fi
+
+# Check that handy.h contains all the public headers.
+for header in $(ls src | grep "\.h$" | grep -v "private.h" | grep -v handy.h);
+do
+ if ! grep -q "$(basename $header)" src/handy.h;
+ then
+ echo "The public header" $(basename $header) "should be included in handy.h."
+ exit 1
+ fi
+done
diff --git a/subprojects/libhandy/meson.build b/subprojects/libhandy/meson.build
new file mode 100644
index 0000000..aae0533
--- /dev/null
+++ b/subprojects/libhandy/meson.build
@@ -0,0 +1,156 @@
+project('libhandy', 'c',
+ version: '0.90.0',
+ license: 'LGPL-2.1+',
+ meson_version: '>= 0.49.0',
+ default_options: [ 'warning_level=1', 'buildtype=debugoptimized', 'c_std=gnu11' ],
+)
+
+version_arr = meson.project_version().split('.')
+handy_version_major = version_arr[0].to_int()
+handy_version_minor = version_arr[1].to_int()
+handy_version_micro = version_arr[2].to_int()
+
+# The major api version as encoded in the libraries name
+apiversion = '1'
+# The so major version of the library
+soversion = 0
+package_api_name = '@0@-@1@'.format(meson.project_name(), apiversion)
+
+if handy_version_minor.is_odd()
+ handy_interface_age = 0
+else
+ handy_interface_age = handy_version_micro
+endif
+
+# maintaining compatibility with libtool versioning
+# current = minor * 100 + micro - interface
+# revision = interface
+current = handy_version_minor * 100 + handy_version_micro - handy_interface_age
+revision = handy_interface_age
+libversion = '@0@.@1@.@2@'.format(soversion, current, revision)
+
+add_project_arguments([
+ '-DHAVE_CONFIG_H',
+ '-DHANDY_COMPILATION',
+ '-I' + meson.build_root(),
+], language: 'c')
+
+root_inc = include_directories('.')
+src_inc = include_directories('src')
+
+cc = meson.get_compiler('c')
+
+global_c_args = []
+test_c_args = [
+ '-Wcast-align',
+ '-Wdate-time',
+ '-Wdeclaration-after-statement',
+ ['-Werror=format-security', '-Werror=format=2'],
+ '-Wendif-labels',
+ '-Werror=incompatible-pointer-types',
+ '-Werror=missing-declarations',
+ '-Werror=overflow',
+ '-Werror=return-type',
+ '-Werror=shift-count-overflow',
+ '-Werror=shift-overflow=2',
+ '-Werror=implicit-fallthrough=3',
+ '-Wformat-nonliteral',
+ '-Wformat-security',
+ '-Winit-self',
+ '-Wmaybe-uninitialized',
+ '-Wmissing-field-initializers',
+ '-Wmissing-include-dirs',
+ '-Wmissing-noreturn',
+ '-Wnested-externs',
+ '-Wno-missing-field-initializers',
+ '-Wno-sign-compare',
+ '-Wno-strict-aliasing',
+ '-Wno-unused-parameter',
+ '-Wold-style-definition',
+ '-Wpointer-arith',
+ '-Wredundant-decls',
+ '-Wshadow',
+ '-Wstrict-prototypes',
+ '-Wswitch-default',
+ '-Wswitch-enum',
+ '-Wtype-limits',
+ '-Wundef',
+ '-Wunused-function',
+]
+
+target_system = target_machine.system()
+
+if get_option('buildtype') != 'plain'
+ if target_system == 'windows'
+ test_c_args += '-fstack-protector'
+ else
+ test_c_args += '-fstack-protector-strong'
+ endif
+endif
+if get_option('profiling')
+ test_c_args += '-pg'
+endif
+
+foreach arg: test_c_args
+ if cc.has_multi_arguments(arg)
+ global_c_args += arg
+ endif
+endforeach
+add_project_arguments(
+ global_c_args,
+ language: 'c'
+)
+
+# Setup various paths that subdirectory meson.build files need
+package_subdir = get_option('package_subdir') # When used as subproject
+datadir = get_option('datadir') / package_subdir
+libdir = get_option('libdir') / package_subdir
+girdir = get_option('datadir') / package_subdir / 'gir-1.0'
+typelibdir = get_option('libdir') / package_subdir / 'girepository-1.0'
+if package_subdir != ''
+ vapidir = get_option('datadir') / package_subdir / 'vapi'
+else
+ vapidir = get_option('datadir') / 'vala' / 'vapi'
+endif
+
+glade_catalog_feature = get_option('glade_catalog')
+gladeui_dep = dependency('gladeui-2.0', required : glade_catalog_feature)
+glade_catalog = not glade_catalog_feature.disabled() and gladeui_dep.found()
+
+introspection_feature = get_option('introspection')
+introspection = introspection_feature.enabled() or introspection_feature.auto()
+
+gnome = import('gnome')
+
+subdir('src')
+subdir('po')
+subdir('examples')
+subdir('tests')
+subdir('doc')
+subdir('glade')
+
+run_data = configuration_data()
+run_data.set('ABS_BUILDDIR', meson.current_build_dir())
+run_data.set('ABS_SRCDIR', meson.current_source_dir())
+configure_file(
+ input: 'run.in',
+ output: 'run',
+ configuration: run_data)
+
+summary = [
+ '',
+ '------',
+ 'Handy @0@ (@1@)'.format(current, apiversion),
+ '',
+ ' Tests: @0@'.format(get_option('tests')),
+ ' Examples: @0@'.format(get_option('examples')),
+ ' Documentation: @0@'.format(get_option('gtk_doc')),
+ ' Introspection: @0@'.format(introspection),
+ ' Vapi: @0@'.format(get_option('vapi')),
+ ' Glade Catalog: @0@'.format(glade_catalog),
+ '------',
+ ''
+]
+
+message('\n'.join(summary))
+
diff --git a/subprojects/libhandy/meson_options.txt b/subprojects/libhandy/meson_options.txt
new file mode 100644
index 0000000..fd0eea5
--- /dev/null
+++ b/subprojects/libhandy/meson_options.txt
@@ -0,0 +1,25 @@
+# Performance and debugging related options
+option('profiling', type: 'boolean', value: false)
+
+option('introspection', type: 'feature', value: 'auto')
+option('vapi', type: 'boolean', value: true)
+
+# Subproject
+option('package_subdir', type: 'string',
+ description: 'Subdirectory to append to all installed files, for use as subproject'
+)
+
+option('gtk_doc',
+ type: 'boolean', value: false,
+ description: 'Whether to generate the API reference for Handy')
+
+option('tests',
+ type: 'boolean', value: true,
+ description: 'Whether to compile unit tests')
+
+option('examples',
+ type: 'boolean', value: true,
+ description: 'Build and install the examples and demo applications')
+
+option('glade_catalog', type: 'feature', value: 'auto',
+ description: 'Install a glade catalog file')
diff --git a/subprojects/libhandy/po/LINGUAS b/subprojects/libhandy/po/LINGUAS
new file mode 100644
index 0000000..7534794
--- /dev/null
+++ b/subprojects/libhandy/po/LINGUAS
@@ -0,0 +1,6 @@
+en_GB
+es
+pl
+pt_BR
+ro
+uk
diff --git a/subprojects/libhandy/po/POTFILES.in b/subprojects/libhandy/po/POTFILES.in
new file mode 100644
index 0000000..b8dd5d5
--- /dev/null
+++ b/subprojects/libhandy/po/POTFILES.in
@@ -0,0 +1,40 @@
+# List of source files containing translatable strings.
+# Please keep this file sorted alphabetically.
+glade/glade-hdy-carousel.c
+glade/glade-hdy-header-bar.c
+glade/glade-hdy-leaflet.c
+glade/glade-hdy-preferences-page.c
+glade/glade-hdy-preferences-window.c
+glade/glade-hdy-search-bar.c
+glade/glade-hdy-utils.h
+src/hdy-action-row.c
+src/hdy-carousel-box.c
+src/hdy-carousel.c
+src/hdy-carousel-indicator-dots.c
+src/hdy-carousel-indicator-lines.c
+src/hdy-clamp.c
+src/hdy-combo-row.c
+src/hdy-deck.c
+src/hdy-expander-row.c
+src/hdy-header-bar.c
+src/hdy-header-group.c
+src/hdy-keypad-button.c
+src/hdy-keypad.c
+src/hdy-leaflet.c
+src/hdy-preferences-group.c
+src/hdy-preferences-page.c
+src/hdy-preferences-row.c
+src/hdy-preferences-window.c
+src/hdy-preferences-window.ui
+src/hdy-search-bar.c
+src/hdy-shadow-helper.c
+src/hdy-squeezer.c
+src/hdy-stackable-box.c
+src/hdy-swipe-tracker.c
+src/hdy-title-bar.c
+src/hdy-value-object.c
+src/hdy-view-switcher-bar.c
+src/hdy-view-switcher-button.c
+src/hdy-view-switcher.c
+src/hdy-view-switcher-title.c
+src/hdy-window-handle-controller.c
diff --git a/subprojects/libhandy/po/POTFILES.skip b/subprojects/libhandy/po/POTFILES.skip
new file mode 100644
index 0000000..0c7d9c5
--- /dev/null
+++ b/subprojects/libhandy/po/POTFILES.skip
@@ -0,0 +1,6 @@
+# List of source files that should *not* be translated.
+# Please keep this file sorted alphabetically.
+examples/hdy-demo-preferences-window.ui
+examples/hdy-demo-window.c
+examples/hdy-demo-window.ui
+examples/hdy-view-switcher-demo-window.ui
diff --git a/subprojects/libhandy/po/en_GB.po b/subprojects/libhandy/po/en_GB.po
new file mode 100644
index 0000000..d4bd6ab
--- /dev/null
+++ b/subprojects/libhandy/po/en_GB.po
@@ -0,0 +1,866 @@
+# British English translation for libhandy.
+# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER
+# This file is distributed under the same license as the libhandy package.
+# Zander Brown <zbrown@gnome.org>, 2020.
+# Bruce Cowan <bruce@bcowan.me.uk>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy master\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-08-06 15:34+0000\n"
+"PO-Revision-Date: 2020-08-06 19:58+0100\n"
+"Last-Translator: Bruce Cowan <bruce@bcowan.me.uk>\n"
+"Language-Team: English - United Kingdom <en@li.org>\n"
+"Language: en_GB\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Gtranslator 3.36.0\n"
+
+#: glade/glade-hdy-carousel.c:160 glade/glade-hdy-header-bar.c:118
+#: glade/glade-hdy-leaflet.c:184
+#, c-format
+msgid "Insert placeholder to %s"
+msgstr "Insert placeholder to %s"
+
+#: glade/glade-hdy-carousel.c:189 glade/glade-hdy-header-bar.c:144
+#: glade/glade-hdy-leaflet.c:214
+#, c-format
+msgid "Remove placeholder from %s"
+msgstr "Remove placeholder from %s"
+
+#: glade/glade-hdy-header-bar.c:18
+msgid "This property does not apply when a custom title is set"
+msgstr "This property does not apply when a custom title is set"
+
+#: glade/glade-hdy-header-bar.c:289
+msgid ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+msgstr ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+
+#: glade/glade-hdy-leaflet.c:19
+msgid "This property only applies when the leaflet is folded"
+msgstr "This property only applies when the leaflet is folded"
+
+#: glade/glade-hdy-preferences-page.c:160
+#, c-format
+msgid "Add group to %s"
+msgstr "Add group to %s"
+
+#: glade/glade-hdy-preferences-window.c:228
+#, c-format
+msgid "Add page to %s"
+msgstr "Add page to %s"
+
+#: glade/glade-hdy-search-bar.c:101
+msgid "Search bar is already full"
+msgstr "Search bar is already full"
+
+#: glade/glade-hdy-utils.h:14
+#, c-format
+msgid "Only objects of type %s can be added to objects of type %s."
+msgstr "Only objects of type %s can be added to objects of type %s."
+
+#: src/hdy-action-row.c:354 src/hdy-action-row.c:355 src/hdy-expander-row.c:314
+#: src/hdy-expander-row.c:315 src/hdy-preferences-page.c:179
+#: src/hdy-preferences-page.c:180
+msgid "Icon name"
+msgstr "Icon name"
+
+#: src/hdy-action-row.c:368
+msgid "Activatable widget"
+msgstr "Activatable widget"
+
+#: src/hdy-action-row.c:369
+msgid "The widget to be activated when the row is activated"
+msgstr "The widget to be activated when the row is activated"
+
+#: src/hdy-action-row.c:382 src/hdy-action-row.c:383 src/hdy-expander-row.c:285
+#: src/hdy-header-bar.c:2105 src/hdy-view-switcher-title.c:272
+msgid "Subtitle"
+msgstr "Subtitle"
+
+#: src/hdy-action-row.c:397 src/hdy-expander-row.c:300
+#: src/hdy-preferences-row.c:130
+msgid "Use underline"
+msgstr "Use underline"
+
+#: src/hdy-action-row.c:398 src/hdy-expander-row.c:301
+#: src/hdy-preferences-row.c:131
+msgid ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+msgstr ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+
+#: src/hdy-carousel-box.c:1088 src/hdy-carousel-box.c:1089
+#: src/hdy-carousel.c:575 src/hdy-carousel.c:576
+msgid "Number of pages"
+msgstr "Number of pages"
+
+#: src/hdy-carousel-box.c:1104 src/hdy-carousel.c:592 src/hdy-header-bar.c:2091
+msgid "Position"
+msgstr "Position"
+
+#: src/hdy-carousel-box.c:1105 src/hdy-carousel.c:593
+msgid "Current scrolling position"
+msgstr "Current scrolling position"
+
+#: src/hdy-carousel-box.c:1120 src/hdy-carousel.c:623 src/hdy-header-bar.c:2119
+msgid "Spacing"
+msgstr "Spacing"
+
+#: src/hdy-carousel-box.c:1121 src/hdy-carousel.c:624
+msgid "Spacing between pages"
+msgstr "Spacing between pages"
+
+#: src/hdy-carousel-box.c:1137 src/hdy-carousel.c:668
+msgid "Reveal duration"
+msgstr "Reveal duration"
+
+#: src/hdy-carousel-box.c:1138 src/hdy-carousel.c:669
+msgid "Page reveal duration"
+msgstr "Page reveal duration"
+
+#: src/hdy-carousel.c:609
+msgid "Interactive"
+msgstr "Interactive"
+
+#: src/hdy-carousel.c:610
+msgid "Whether the widget can be swiped"
+msgstr "Whether the widget can be swiped"
+
+#: src/hdy-carousel.c:639
+msgid "Animation duration"
+msgstr "Animation duration"
+
+#: src/hdy-carousel.c:640
+msgid "Default animation duration"
+msgstr "Default animation duration"
+
+#: src/hdy-carousel.c:654 src/hdy-swipe-tracker.c:803
+msgid "Allow mouse drag"
+msgstr "Allow mouse drag"
+
+#: src/hdy-carousel.c:655 src/hdy-swipe-tracker.c:804
+msgid "Whether to allow dragging with mouse pointer"
+msgstr "Whether to allow dragging with mouse pointer"
+
+#: src/hdy-carousel-indicator-dots.c:392 src/hdy-carousel-indicator-dots.c:393
+#: src/hdy-carousel-indicator-lines.c:391
+#: src/hdy-carousel-indicator-lines.c:392
+msgid "Carousel"
+msgstr "Carousel"
+
+#: src/hdy-clamp.c:417
+msgid "Maximum size"
+msgstr "Maximum size"
+
+#: src/hdy-clamp.c:418
+msgid "The maximum size allocated to the child"
+msgstr "The maximum size allocated to the child"
+
+#: src/hdy-clamp.c:442
+msgid "Tightening threshold"
+msgstr "Tightening threshold"
+
+#: src/hdy-clamp.c:443
+msgid "The size from which the clamp will tighten its grip on the child"
+msgstr "The size from which the clamp will tighten its grip on the child"
+
+#: src/hdy-combo-row.c:411
+msgid "Selected index"
+msgstr "Selected index"
+
+#: src/hdy-combo-row.c:412
+msgid "The index of the selected item"
+msgstr "The index of the selected item"
+
+#: src/hdy-combo-row.c:430
+msgid "Use subtitle"
+msgstr "Use subtitle"
+
+#: src/hdy-combo-row.c:431
+msgid "Set the current value as the subtitle"
+msgstr "Set the current value as the subtitle"
+
+#: src/hdy-deck.c:888
+msgid "Horizontally homogeneous"
+msgstr "Horizontally homogeneous"
+
+#: src/hdy-deck.c:889
+msgid "Horizontally homogeneous sizing"
+msgstr "Horizontally homogeneous sizing"
+
+#: src/hdy-deck.c:902
+msgid "Vertically homogeneous"
+msgstr "Vertically homogeneous"
+
+#: src/hdy-deck.c:903
+msgid "Vertically homogeneous sizing"
+msgstr "Vertically homogeneous sizing"
+
+#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1098
+#: src/hdy-stackable-box.c:2993
+msgid "Visible child"
+msgstr "Visible child"
+
+#: src/hdy-deck.c:917
+msgid "The widget currently visible"
+msgstr "The widget currently visible"
+
+#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000
+msgid "Name of visible child"
+msgstr "Name of visible child"
+
+#: src/hdy-deck.c:931
+msgid "The name of the widget currently visible"
+msgstr "The name of the widget currently visible"
+
+#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1112
+#: src/hdy-stackable-box.c:3019
+msgid "Transition type"
+msgstr "Transition type"
+
+#: src/hdy-deck.c:950
+msgid "The type of animation used to transition between children"
+msgstr "The type of animation used to transition between children"
+
+#: src/hdy-deck.c:963 src/hdy-header-bar.c:2201 src/hdy-squeezer.c:1105
+msgid "Transition duration"
+msgstr "Transition duration"
+
+#: src/hdy-deck.c:964
+msgid "The transition animation duration, in milliseconds"
+msgstr "The transition animation duration, in milliseconds"
+
+#: src/hdy-deck.c:977 src/hdy-header-bar.c:2208 src/hdy-squeezer.c:1120
+msgid "Transition running"
+msgstr "Transition running"
+
+#: src/hdy-deck.c:978 src/hdy-header-bar.c:2209 src/hdy-squeezer.c:1121
+msgid "Whether or not the transition is currently running"
+msgstr "Whether or not the transition is currently running"
+
+#: src/hdy-deck.c:992 src/hdy-header-bar.c:2215 src/hdy-leaflet.c:1072
+#: src/hdy-squeezer.c:1127 src/hdy-stackable-box.c:3047
+msgid "Interpolate size"
+msgstr "Interpolate size"
+
+#: src/hdy-deck.c:993 src/hdy-header-bar.c:2216 src/hdy-leaflet.c:1073
+#: src/hdy-squeezer.c:1128 src/hdy-stackable-box.c:3048
+msgid ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+msgstr ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+
+#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497
+#: src/hdy-stackable-box.c:3062
+msgid "Can swipe back"
+msgstr "Can swipe back"
+
+#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063
+msgid ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+msgstr ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+
+#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077
+msgid "Can swipe forward"
+msgstr "Can swipe forward"
+
+#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078
+msgid "Whether or not swipe gesture can be used to switch to the next child"
+msgstr "Whether or not swipe gesture can be used to switch to the next child"
+
+#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111
+msgid "Name"
+msgstr "Name"
+
+#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112
+msgid "The name of the child page"
+msgstr "The name of the child page"
+
+#: src/hdy-expander-row.c:286
+msgid "The subtitle for this row"
+msgstr "The subtitle for this row"
+
+#: src/hdy-expander-row.c:326
+msgid "Expanded"
+msgstr "Expanded"
+
+#: src/hdy-expander-row.c:327
+msgid "Whether the row is expanded"
+msgstr "Whether the row is expanded"
+
+#: src/hdy-expander-row.c:338
+msgid "Enable expansion"
+msgstr "Enable expansion"
+
+#: src/hdy-expander-row.c:339
+msgid "Whether the expansion is enabled"
+msgstr "Whether the expansion is enabled"
+
+#: src/hdy-expander-row.c:350
+msgid "Show enable switch"
+msgstr "Show enable switch"
+
+#: src/hdy-expander-row.c:351
+msgid "Whether the switch enabling the expansion is visible"
+msgstr "Whether the switch enabling the expansion is visible"
+
+#: src/hdy-header-bar.c:485
+msgid "Application menu"
+msgstr "Application menu"
+
+#: src/hdy-header-bar.c:507 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Minimise"
+
+#: src/hdy-header-bar.c:529 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Restore"
+
+#: src/hdy-header-bar.c:529 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Maximise"
+
+#: src/hdy-header-bar.c:547 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Close"
+
+#: src/hdy-header-bar.c:563
+msgid "Back"
+msgstr "Back"
+
+#: src/hdy-header-bar.c:2084
+msgid "Pack type"
+msgstr "Pack type"
+
+#: src/hdy-header-bar.c:2085
+msgid ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+msgstr ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+
+#: src/hdy-header-bar.c:2092
+msgid "The index of the child in the parent"
+msgstr "The index of the child in the parent"
+
+#: src/hdy-header-bar.c:2098 src/hdy-preferences-group.c:265
+#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193
+#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115
+#: src/hdy-view-switcher-title.c:258
+msgid "Title"
+msgstr "Title"
+
+#: src/hdy-header-bar.c:2099 src/hdy-view-switcher-title.c:259
+msgid "The title to display"
+msgstr "The title to display"
+
+#: src/hdy-header-bar.c:2106 src/hdy-view-switcher-title.c:273
+msgid "The subtitle to display"
+msgstr "The subtitle to display"
+
+#: src/hdy-header-bar.c:2112
+msgid "Custom Title"
+msgstr "Custom Title"
+
+#: src/hdy-header-bar.c:2113
+msgid "Custom title widget to display"
+msgstr "Custom title widget to display"
+
+#: src/hdy-header-bar.c:2120
+msgid "The amount of space between children"
+msgstr "The amount of space between children"
+
+#: src/hdy-header-bar.c:2139
+msgid "Show decorations"
+msgstr "Show decorations"
+
+#: src/hdy-header-bar.c:2140
+msgid "Whether to show window decorations"
+msgstr "Whether to show window decorations"
+
+#: src/hdy-header-bar.c:2158
+msgid "Decoration Layout"
+msgstr "Decoration Layout"
+
+#: src/hdy-header-bar.c:2159
+msgid "The layout for window decorations"
+msgstr "The layout for window decorations"
+
+#: src/hdy-header-bar.c:2172
+msgid "Decoration Layout Set"
+msgstr "Decoration Layout Set"
+
+#: src/hdy-header-bar.c:2173
+msgid "Whether the decoration-layout property has been set"
+msgstr "Whether the decoration-layout property has been set"
+
+#: src/hdy-header-bar.c:2187
+msgid "Has Subtitle"
+msgstr "Has Subtitle"
+
+#: src/hdy-header-bar.c:2188
+msgid "Whether to reserve space for a subtitle"
+msgstr "Whether to reserve space for a subtitle"
+
+#: src/hdy-header-bar.c:2194
+msgid "Centering policy"
+msgstr "Centring policy"
+
+#: src/hdy-header-bar.c:2195
+msgid "The policy to horizontally align the center widget"
+msgstr "The policy to horizontally align the centre widget"
+
+#: src/hdy-header-bar.c:2202 src/hdy-squeezer.c:1106
+msgid "The animation duration, in milliseconds"
+msgstr "The animation duration, in milliseconds"
+
+#: src/hdy-header-group.c:827
+msgid "Decorate all"
+msgstr "Decorate all"
+
+#: src/hdy-header-group.c:828
+msgid ""
+"Whether the elements of the group should all receive the full decoration"
+msgstr ""
+"Whether the elements of the group should all receive the full decoration"
+
+#: src/hdy-keypad-button.c:225
+msgid "Digit"
+msgstr "Digit"
+
+#: src/hdy-keypad-button.c:226
+msgid "The keypad digit of the button"
+msgstr "The keypad digit of the button"
+
+#: src/hdy-keypad-button.c:232
+msgid "Symbols"
+msgstr "Symbols"
+
+#: src/hdy-keypad-button.c:233
+msgid "The keypad symbols of the button. The first symbol is used as the digit"
+msgstr ""
+"The keypad symbols of the button. The first symbol is used as the digit"
+
+#: src/hdy-keypad-button.c:239
+msgid "Show symbols"
+msgstr "Show symbols"
+
+#: src/hdy-keypad-button.c:240
+msgid "Whether the second line of symbols should be shown or not"
+msgstr "Whether the second line of symbols should be shown or not"
+
+#: src/hdy-keypad.c:247
+msgid "Row spacing"
+msgstr "Row spacing"
+
+#: src/hdy-keypad.c:248
+msgid "The amount of space between two consecutive rows"
+msgstr "The amount of space between two consecutive rows"
+
+#: src/hdy-keypad.c:261
+msgid "Column spacing"
+msgstr "Column spacing"
+
+#: src/hdy-keypad.c:262
+msgid "The amount of space between two consecutive columns"
+msgstr "The amount of space between two consecutive columns"
+
+#: src/hdy-keypad.c:276
+msgid "Letters visible"
+msgstr "Letters visible"
+
+#: src/hdy-keypad.c:277
+msgid "Whether the letters below the digits should be visible"
+msgstr "Whether the letters below the digits should be visible"
+
+#: src/hdy-keypad.c:291
+msgid "Symbols visible"
+msgstr "Symbols visible"
+
+#: src/hdy-keypad.c:292
+msgid "Whether the hash, plus, and asterisk symbols should be visible"
+msgstr "Whether the hash, plus, and asterisk symbols should be visible"
+
+#: src/hdy-keypad.c:306
+msgid "Entry"
+msgstr "Entry"
+
+#: src/hdy-keypad.c:307
+msgid "The entry widget connected to the keypad"
+msgstr "The entry widget connected to the keypad"
+
+#: src/hdy-keypad.c:320
+msgid "End action"
+msgstr "End action"
+
+#: src/hdy-keypad.c:321
+msgid "The end action widget"
+msgstr "The end action widget"
+
+#: src/hdy-keypad.c:334
+msgid "Start action"
+msgstr "Start action"
+
+#: src/hdy-keypad.c:335
+msgid "The start action widget"
+msgstr "The start action widget"
+
+#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938
+msgid "Folded"
+msgstr "Folded"
+
+#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939
+msgid "Whether the widget is folded"
+msgstr "Whether the widget is folded"
+
+#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950
+msgid "Horizontally homogeneous folded"
+msgstr "Horizontally homogeneous folded"
+
+#: src/hdy-leaflet.c:976
+msgid "Horizontally homogeneous sizing when the leaflet is folded"
+msgstr "Horizontally homogeneous sizing when the leaflet is folded"
+
+#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962
+msgid "Vertically homogeneous folded"
+msgstr "Vertically homogeneous folded"
+
+#: src/hdy-leaflet.c:988
+msgid "Vertically homogeneous sizing when the leaflet is folded"
+msgstr "Vertically homogeneous sizing when the leaflet is folded"
+
+#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974
+msgid "Box horizontally homogeneous"
+msgstr "Box horizontally homogeneous"
+
+#: src/hdy-leaflet.c:1000
+msgid "Horizontally homogeneous sizing when the leaflet is unfolded"
+msgstr "Horizontally homogeneous sizing when the leaflet is unfolded"
+
+#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986
+msgid "Box vertically homogeneous"
+msgstr "Box vertically homogeneous"
+
+#: src/hdy-leaflet.c:1012
+msgid "Vertically homogeneous sizing when the leaflet is unfolded"
+msgstr "Vertically homogeneous sizing when the leaflet is unfolded"
+
+#: src/hdy-leaflet.c:1019
+msgid "The widget currently visible when the leaflet is folded"
+msgstr "The widget currently visible when the leaflet is folded"
+
+#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001
+msgid "The name of the widget currently visible when the children are stacked"
+msgstr "The name of the widget currently visible when the children are stacked"
+
+#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020
+msgid "The type of animation used to transition between modes and children"
+msgstr "The type of animation used to transition between modes and children"
+
+#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026
+msgid "Mode transition duration"
+msgstr "Mode transition duration"
+
+#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027
+msgid "The mode transition animation duration, in milliseconds"
+msgstr "The mode transition animation duration, in milliseconds"
+
+#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033
+msgid "Child transition duration"
+msgstr "Child transition duration"
+
+#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034
+msgid "The child transition animation duration, in milliseconds"
+msgstr "The child transition animation duration, in milliseconds"
+
+#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040
+msgid "Child transition running"
+msgstr "Child transition running"
+
+#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041
+msgid "Whether or not the child transition is currently running"
+msgstr "Whether or not the child transition is currently running"
+
+#: src/hdy-leaflet.c:1129
+msgid "Navigatable"
+msgstr "Navigable"
+
+#: src/hdy-leaflet.c:1130
+msgid "Whether the child can be navigated to"
+msgstr "Whether the child can be navigated to"
+
+#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252
+msgid "Description"
+msgstr "Description"
+
+#: src/hdy-preferences-row.c:116
+msgid "The title of the preference"
+msgstr "The title of the preference"
+
+#: src/hdy-preferences-window.c:141
+msgid "Untitled page"
+msgstr "Untitled page"
+
+#: src/hdy-preferences-window.c:483
+msgid "Search enabled"
+msgstr "Search enabled"
+
+#: src/hdy-preferences-window.c:484
+msgid "Whether search is enabled"
+msgstr "Whether search is enabled"
+
+#: src/hdy-preferences-window.c:498
+msgid ""
+"Whether or not swipe gesture can be used to switch from a subpage to the "
+"preferences"
+msgstr ""
+"Whether or not swipe gesture can be used to switch from a sub-page to the "
+"preferences"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Preferences"
+
+#: src/hdy-preferences-window.ui:78
+msgid "Search"
+msgstr "Search"
+
+#: src/hdy-preferences-window.ui:201
+msgid "No Results Found"
+msgstr "No Results Found"
+
+#: src/hdy-preferences-window.ui:216
+msgid "Try a different search"
+msgstr "Try a different search"
+
+#: src/hdy-search-bar.c:451
+msgid "Search Mode Enabled"
+msgstr "Search Mode Enabled"
+
+#: src/hdy-search-bar.c:452
+msgid "Whether the search mode is on and the search bar shown"
+msgstr "Whether the search mode is on and the search bar shown"
+
+#: src/hdy-search-bar.c:463
+msgid "Show Close Button"
+msgstr "Show Close Button"
+
+#: src/hdy-search-bar.c:464
+msgid "Whether to show the close button in the toolbar"
+msgstr "Whether to show the close button in the toolbar"
+
+#: src/hdy-shadow-helper.c:254
+msgid "Widget"
+msgstr "Widget"
+
+#: src/hdy-shadow-helper.c:255
+msgid "The widget the shadow will be drawn for"
+msgstr "The widget the shadow will be drawn for"
+
+#: src/hdy-squeezer.c:1091
+msgid "Homogeneous"
+msgstr "Homogeneous"
+
+#: src/hdy-squeezer.c:1092
+msgid "Homogeneous sizing"
+msgstr "Homogeneous sizing"
+
+#: src/hdy-squeezer.c:1099
+msgid "The widget currently visible in the squeezer"
+msgstr "The widget currently visible in the squeezer"
+
+#: src/hdy-squeezer.c:1113
+msgid "The type of animation used to transition"
+msgstr "The type of animation used to transition"
+
+#: src/hdy-squeezer.c:1148
+msgid "X align"
+msgstr "X align"
+
+#: src/hdy-squeezer.c:1149
+msgid "The horizontal alignment, from 0 (start) to 1 (end)"
+msgstr "The horizontal alignment, from 0 (start) to 1 (end)"
+
+#: src/hdy-squeezer.c:1170
+msgid "Y align"
+msgstr "Y align"
+
+#: src/hdy-squeezer.c:1171
+msgid "The vertical alignment, from 0 (top) to 1 (bottom)"
+msgstr "The vertical alignment, from 0 (top) to 1 (bottom)"
+
+#: src/hdy-squeezer.c:1180 src/hdy-swipe-tracker.c:773
+msgid "Enabled"
+msgstr "Enabled"
+
+#: src/hdy-squeezer.c:1181
+msgid ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+msgstr ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+
+#: src/hdy-stackable-box.c:2951
+msgid "Horizontally homogeneous sizing when the widget is folded"
+msgstr "Horizontally homogeneous sizing when the widget is folded"
+
+#: src/hdy-stackable-box.c:2963
+msgid "Vertically homogeneous sizing when the widget is folded"
+msgstr "Vertically homogeneous sizing when the widget is folded"
+
+#: src/hdy-stackable-box.c:2975
+msgid "Horizontally homogeneous sizing when the widget is unfolded"
+msgstr "Horizontally homogeneous sizing when the widget is unfolded"
+
+#: src/hdy-stackable-box.c:2987
+msgid "Vertically homogeneous sizing when the widget is unfolded"
+msgstr "Vertically homogeneous sizing when the widget is unfolded"
+
+#: src/hdy-stackable-box.c:2994
+msgid "The widget currently visible when the widget is folded"
+msgstr "The widget currently visible when the widget is folded"
+
+#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085
+msgid "Orientation"
+msgstr "Orientation"
+
+#: src/hdy-swipe-tracker.c:758
+msgid "Swipeable"
+msgstr "Swipe-able"
+
+#: src/hdy-swipe-tracker.c:759
+msgid "The swipeable the swipe tracker is attached to"
+msgstr "The swipe-able the swipe tracker is attached to"
+
+#: src/hdy-swipe-tracker.c:774
+msgid "Whether the swipe tracker processes events"
+msgstr "Whether the swipe tracker processes events"
+
+#: src/hdy-swipe-tracker.c:788
+msgid "Reversed"
+msgstr "Reversed"
+
+#: src/hdy-swipe-tracker.c:789
+msgid "Whether swipe direction is reversed"
+msgstr "Whether swipe direction is reversed"
+
+#: src/hdy-title-bar.c:308
+msgid "Selection mode"
+msgstr "Selection mode"
+
+#: src/hdy-title-bar.c:309
+msgid "Whether or not the title bar is in selection mode"
+msgstr "Whether or not the title bar is in selection mode"
+
+#: src/hdy-value-object.c:191
+msgctxt "HdyValueObjectClass"
+msgid "Value"
+msgstr "Value"
+
+#: src/hdy-value-object.c:192
+msgctxt "HdyValueObjectClass"
+msgid "The contained value"
+msgstr "The contained value"
+
+#: src/hdy-view-switcher-bar.c:178 src/hdy-view-switcher.c:509
+#: src/hdy-view-switcher-title.c:230
+msgid "Policy"
+msgstr "Policy"
+
+#: src/hdy-view-switcher-bar.c:179 src/hdy-view-switcher.c:510
+#: src/hdy-view-switcher-title.c:231
+msgid "The policy to determine the mode to use"
+msgstr "The policy to determine the mode to use"
+
+#: src/hdy-view-switcher-bar.c:192 src/hdy-view-switcher-bar.c:193
+#: src/hdy-view-switcher.c:544 src/hdy-view-switcher.c:545
+#: src/hdy-view-switcher-title.c:244 src/hdy-view-switcher-title.c:245
+msgid "Stack"
+msgstr "Stack"
+
+#: src/hdy-view-switcher-bar.c:206
+msgid "Reveal"
+msgstr "Reveal"
+
+#: src/hdy-view-switcher-bar.c:207
+msgid "Whether the view switcher is revealed"
+msgstr "Whether the view switcher is revealed"
+
+#: src/hdy-view-switcher-button.c:203
+msgid "Icon Name"
+msgstr "Icon Name"
+
+#: src/hdy-view-switcher-button.c:204
+msgid "Icon name for image"
+msgstr "Icon name for image"
+
+#: src/hdy-view-switcher-button.c:217
+msgid "Icon Size"
+msgstr "Icon Size"
+
+#: src/hdy-view-switcher-button.c:218
+msgid "Symbolic size to use for named icon"
+msgstr "Symbolic size to use for named icon"
+
+#: src/hdy-view-switcher-button.c:234
+msgid "Needs attention"
+msgstr "Needs attention"
+
+#: src/hdy-view-switcher-button.c:235
+msgid "Hint the view needs attention"
+msgstr "Hint the view needs attention"
+
+#: src/hdy-view-switcher.c:529
+msgid "Narrow ellipsize"
+msgstr "Narrow ellipsise"
+
+#: src/hdy-view-switcher.c:530
+msgid ""
+"The preferred place to ellipsize the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+msgstr ""
+"The preferred place to ellipsise the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+
+#: src/hdy-view-switcher-title.c:286
+msgid "View switcher enabled"
+msgstr "View switcher enabled"
+
+#: src/hdy-view-switcher-title.c:287
+msgid "Whether the view switcher is enabled"
+msgstr "Whether the view switcher is enabled"
+
+#: src/hdy-view-switcher-title.c:300
+msgid "Title visible"
+msgstr "Title visible"
+
+#: src/hdy-view-switcher-title.c:301
+msgid "Whether the title label is visible"
+msgstr "Whether the title label is visible"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Move"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Resize"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Always on Top"
diff --git a/subprojects/libhandy/po/es.po b/subprojects/libhandy/po/es.po
new file mode 100644
index 0000000..a55ee22
--- /dev/null
+++ b/subprojects/libhandy/po/es.po
@@ -0,0 +1,850 @@
+# Spanish translation for libhandy.
+# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER
+# This file is distributed under the same license as the libhandy package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+# FULL NAME <EMAIL@ADDRESS>, 2020.
+# Daniel Mustieles <daniel.mustieles@gmail.com>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy master\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-06-12 07:20+0000\n"
+"PO-Revision-Date: 2020-07-01 09:37+0200\n"
+"Last-Translator: Daniel Mustieles <daniel.mustieles@gmail.com>\n"
+"Language-Team: Spanish - Spain <gnome-es-list@gnome.org>\n"
+"Language: es_ES\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+"X-Generator: Gtranslator 3.36.0\n"
+
+#: glade/glade-hdy-carousel.c:20
+msgid "This property does not apply unless Show Indicators is set."
+msgstr ""
+
+#: glade/glade-hdy-carousel.c:166 glade/glade-hdy-header-bar.c:117
+#: glade/glade-hdy-leaflet.c:183
+#, c-format
+msgid "Insert placeholder to %s"
+msgstr "Insertar marcador a %s"
+
+#: glade/glade-hdy-carousel.c:195 glade/glade-hdy-header-bar.c:143
+#: glade/glade-hdy-leaflet.c:213
+#, c-format
+msgid "Remove placeholder from %s"
+msgstr "Quitar marcador de %s"
+
+#: glade/glade-hdy-header-bar.c:17
+msgid "This property does not apply when a custom title is set"
+msgstr ""
+
+#: glade/glade-hdy-header-bar.c:288
+msgid ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+msgstr ""
+
+#: glade/glade-hdy-leaflet.c:18
+msgid "This property only applies when the leaflet is folded"
+msgstr ""
+
+#: glade/glade-hdy-preferences-page.c:159
+#, c-format
+msgid "Add group to %s"
+msgstr "Añadir grupo a %s"
+
+#: glade/glade-hdy-preferences-window.c:227
+#, c-format
+msgid "Add page to %s"
+msgstr "Añadir página a %s"
+
+#: glade/glade-hdy-search-bar.c:100
+msgid "Search bar is already full"
+msgstr "La barra de búsqueda ya está llena"
+
+#: glade/glade-hdy-utils.h:14
+#, c-format
+msgid "Only objects of type %s can be added to objects of type %s."
+msgstr ""
+
+#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:328
+#: src/hdy-expander-row.c:329 src/hdy-preferences-page.c:179
+#: src/hdy-preferences-page.c:180
+msgid "Icon name"
+msgstr "Nombre del icono"
+
+#: src/hdy-action-row.c:375
+msgid "Activatable widget"
+msgstr "Widget activable"
+
+#: src/hdy-action-row.c:376
+msgid "The widget to be activated when the row is activated"
+msgstr ""
+
+#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:299
+#: src/hdy-header-bar.c:2149 src/hdy-view-switcher-title.c:295
+msgid "Subtitle"
+msgstr "Subtítulo"
+
+#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:285
+#: src/hdy-header-bar.c:2142 src/hdy-preferences-group.c:265
+#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193
+#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115
+#: src/hdy-view-switcher-title.c:281
+msgid "Title"
+msgstr "Título"
+
+#: src/hdy-action-row.c:418 src/hdy-expander-row.c:314
+#: src/hdy-preferences-row.c:130
+msgid "Use underline"
+msgstr "Usar subrayado"
+
+#: src/hdy-action-row.c:419 src/hdy-expander-row.c:315
+#: src/hdy-preferences-row.c:131
+msgid ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+msgstr ""
+
+#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096
+#: src/hdy-carousel.c:970 src/hdy-carousel.c:971
+msgid "Number of pages"
+msgstr "Número de páginas"
+
+#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:987 src/hdy-header-bar.c:2135
+msgid "Position"
+msgstr "Posición"
+
+#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:988
+msgid "Current scrolling position"
+msgstr "Posición actual del desplazamiento"
+
+#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1069
+#: src/hdy-header-bar.c:2163
+msgid "Spacing"
+msgstr "Espaciado"
+
+#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1070
+msgid "Spacing between pages"
+msgstr "Espacio entre páginas"
+
+#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1114
+msgid "Reveal duration"
+msgstr "Mostrar duración"
+
+#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1115
+msgid "Page reveal duration"
+msgstr "Tiempo que se muestra la página"
+
+#: src/hdy-carousel.c:1004
+msgid "Interactive"
+msgstr "Interactivo"
+
+#: src/hdy-carousel.c:1005
+msgid "Whether the widget can be swiped"
+msgstr ""
+
+#: src/hdy-carousel.c:1020
+msgid "Indicator style"
+msgstr "Indicador de estilo"
+
+#: src/hdy-carousel.c:1021
+msgid "Page indicator style"
+msgstr "Indicador de estilo de la página"
+
+#: src/hdy-carousel.c:1036
+msgid "Indicator spacing"
+msgstr "Indicador de espaciado"
+
+#: src/hdy-carousel.c:1037
+msgid "Spacing between content and indicators"
+msgstr "Espaciado entre contenido e indicadores"
+
+#: src/hdy-carousel.c:1055
+msgid "Center content"
+msgstr "Centrar contenido"
+
+#: src/hdy-carousel.c:1056
+msgid "Whether to center pages to compensate for indicators"
+msgstr "Indica si se deben centrar las páginas para compensar los indicadores"
+
+#: src/hdy-carousel.c:1085
+msgid "Animation duration"
+msgstr "Duración de la animación"
+
+#: src/hdy-carousel.c:1086
+msgid "Default animation duration"
+msgstr "Duración predeterminada de la animación"
+
+#: src/hdy-carousel.c:1100 src/hdy-swipe-tracker.c:645
+msgid "Allow mouse drag"
+msgstr "Permitir arrastre con el ratón"
+
+#: src/hdy-carousel.c:1101 src/hdy-swipe-tracker.c:646
+msgid "Whether to allow dragging with mouse pointer"
+msgstr ""
+
+#: src/hdy-column.c:306
+msgid "Maximum width"
+msgstr "Anchura máxima"
+
+#: src/hdy-column.c:307
+msgid "The maximum width allocated to the child"
+msgstr ""
+
+#: src/hdy-column.c:318
+msgid "Linear growth width"
+msgstr ""
+
+#: src/hdy-column.c:319
+msgid "The width up to which the child will be allocated all the width"
+msgstr ""
+
+#: src/hdy-combo-row.c:411
+msgid "Selected index"
+msgstr "Índice seleccionada"
+
+#: src/hdy-combo-row.c:412
+msgid "The index of the selected item"
+msgstr "El índice del elemento seleccionado"
+
+#: src/hdy-combo-row.c:430
+msgid "Use subtitle"
+msgstr "Usar subtítulo"
+
+#: src/hdy-combo-row.c:431
+msgid "Set the current value as the subtitle"
+msgstr ""
+
+#: src/hdy-deck.c:881
+msgid "Horizontally homogeneous"
+msgstr "Homogéneo horizontalmente"
+
+#: src/hdy-deck.c:882
+msgid "Horizontally homogeneous sizing"
+msgstr ""
+
+#: src/hdy-deck.c:895
+msgid "Vertically homogeneous"
+msgstr "Homogéneo verticalmente"
+
+#: src/hdy-deck.c:896
+msgid "Vertically homogeneous sizing"
+msgstr ""
+
+#: src/hdy-deck.c:909 src/hdy-leaflet.c:1010 src/hdy-squeezer.c:1100
+#: src/hdy-stackable-box.c:3436
+msgid "Visible child"
+msgstr "Hijo visible"
+
+#: src/hdy-deck.c:910
+msgid "The widget currently visible"
+msgstr "El widget actualmente visible"
+
+#: src/hdy-deck.c:923 src/hdy-leaflet.c:1017 src/hdy-stackable-box.c:3443
+msgid "Name of visible child"
+msgstr "Nombre del hijo visible"
+
+#: src/hdy-deck.c:924
+msgid "The name of the widget currently visible"
+msgstr ""
+
+#: src/hdy-deck.c:942 src/hdy-leaflet.c:1036 src/hdy-squeezer.c:1114
+#: src/hdy-stackable-box.c:3462
+msgid "Transition type"
+msgstr "Tipo de transición"
+
+#: src/hdy-deck.c:943
+msgid "The type of animation used to transition between children"
+msgstr "El tipo de animación usada para cambiar entre hijos"
+
+#: src/hdy-deck.c:956 src/hdy-header-bar.c:2245 src/hdy-squeezer.c:1107
+msgid "Transition duration"
+msgstr "Duración de la transición"
+
+#: src/hdy-deck.c:957
+msgid "The transition animation duration, in milliseconds"
+msgstr ""
+
+#: src/hdy-deck.c:970 src/hdy-header-bar.c:2252 src/hdy-squeezer.c:1122
+msgid "Transition running"
+msgstr "Transición en ejecución"
+
+#: src/hdy-deck.c:971 src/hdy-header-bar.c:2253 src/hdy-squeezer.c:1123
+msgid "Whether or not the transition is currently running"
+msgstr ""
+
+#: src/hdy-deck.c:985 src/hdy-header-bar.c:2259 src/hdy-leaflet.c:1064
+#: src/hdy-squeezer.c:1129 src/hdy-stackable-box.c:3490
+msgid "Interpolate size"
+msgstr "Interpolar tamaño"
+
+#: src/hdy-deck.c:986 src/hdy-header-bar.c:2260 src/hdy-leaflet.c:1065
+#: src/hdy-squeezer.c:1130 src/hdy-stackable-box.c:3491
+msgid ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+msgstr ""
+
+#: src/hdy-deck.c:1000 src/hdy-leaflet.c:1079 src/hdy-stackable-box.c:3505
+msgid "Can swipe back"
+msgstr ""
+
+#: src/hdy-deck.c:1001 src/hdy-leaflet.c:1080 src/hdy-stackable-box.c:3506
+msgid ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+msgstr ""
+
+#: src/hdy-deck.c:1014 src/hdy-leaflet.c:1094 src/hdy-stackable-box.c:3520
+msgid "Can swipe forward"
+msgstr ""
+
+#: src/hdy-deck.c:1015 src/hdy-leaflet.c:1095 src/hdy-stackable-box.c:3521
+msgid "Whether or not swipe gesture can be used to switch to the next child"
+msgstr ""
+
+#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3537
+msgid "Name"
+msgstr "Nombre"
+
+#: src/hdy-deck.c:1024 src/hdy-leaflet.c:1104 src/hdy-stackable-box.c:3538
+msgid "The name of the child page"
+msgstr "El nombre de la página hija"
+
+#: src/hdy-expander-row.c:286
+msgid "The title for this row"
+msgstr "El título para esta fila"
+
+#: src/hdy-expander-row.c:300
+msgid "The subtitle for this row"
+msgstr "El subtítulo para esta fila"
+
+#: src/hdy-expander-row.c:340
+msgid "Expanded"
+msgstr "Expandido"
+
+#: src/hdy-expander-row.c:341
+msgid "Whether the row is expanded"
+msgstr "Indica si la fila está expandida"
+
+#: src/hdy-expander-row.c:352
+msgid "Enable expansion"
+msgstr "Activar expansión"
+
+#: src/hdy-expander-row.c:353
+msgid "Whether the expansion is enabled"
+msgstr "Indica si la expansión está activada"
+
+#: src/hdy-expander-row.c:364
+msgid "Show enable switch"
+msgstr ""
+
+#: src/hdy-expander-row.c:365
+msgid "Whether the switch enabling the expansion is visible"
+msgstr ""
+
+#: src/hdy-header-bar.c:484
+msgid "Application menu"
+msgstr "Menú de la aplicación"
+
+#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Minimizar"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Restaurar"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Maximizar"
+
+#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Cerrar"
+
+#: src/hdy-header-bar.c:562
+msgid "Back"
+msgstr "Atrás"
+
+#: src/hdy-header-bar.c:2128
+msgid "Pack type"
+msgstr "Tipo de paquete"
+
+#: src/hdy-header-bar.c:2129
+msgid ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+msgstr ""
+
+#: src/hdy-header-bar.c:2136
+msgid "The index of the child in the parent"
+msgstr "El índice del hijo en el padre"
+
+#: src/hdy-header-bar.c:2143 src/hdy-view-switcher-title.c:282
+msgid "The title to display"
+msgstr "El título que mostrar"
+
+#: src/hdy-header-bar.c:2150 src/hdy-view-switcher-title.c:296
+msgid "The subtitle to display"
+msgstr "El subtítulo que mostrar"
+
+#: src/hdy-header-bar.c:2156
+msgid "Custom Title"
+msgstr "Título personalizado"
+
+#: src/hdy-header-bar.c:2157
+msgid "Custom title widget to display"
+msgstr "Wigdet de título personalizado que mostrar"
+
+#: src/hdy-header-bar.c:2164
+msgid "The amount of space between children"
+msgstr ""
+
+#: src/hdy-header-bar.c:2183
+msgid "Show decorations"
+msgstr "Mostrar decoración"
+
+#: src/hdy-header-bar.c:2184
+msgid "Whether to show window decorations"
+msgstr "Indica si se debe mostrar la decoración de la ventana"
+
+#: src/hdy-header-bar.c:2202
+msgid "Decoration Layout"
+msgstr "Distribución de la decoración"
+
+#: src/hdy-header-bar.c:2203
+msgid "The layout for window decorations"
+msgstr "La distribución de la decoración de la ventana"
+
+#: src/hdy-header-bar.c:2216
+msgid "Decoration Layout Set"
+msgstr ""
+
+#: src/hdy-header-bar.c:2217
+msgid "Whether the decoration-layout property has been set"
+msgstr ""
+
+#: src/hdy-header-bar.c:2231
+msgid "Has Subtitle"
+msgstr "Tiene subtítulo"
+
+#: src/hdy-header-bar.c:2232
+msgid "Whether to reserve space for a subtitle"
+msgstr "Indica si se debe reservar espacio para un subtítulo"
+
+#: src/hdy-header-bar.c:2238
+msgid "Centering policy"
+msgstr "Política de centrado"
+
+#: src/hdy-header-bar.c:2239
+msgid "The policy to horizontally align the center widget"
+msgstr "La política para alinear horizontalmente el widget centrad"
+
+#: src/hdy-header-bar.c:2246 src/hdy-squeezer.c:1108
+msgid "The animation duration, in milliseconds"
+msgstr "La duración de la animación en milisegundos"
+
+#: src/hdy-header-group.c:590
+msgid "Focus"
+msgstr "Foco"
+
+#: src/hdy-header-group.c:591
+msgid "The header bar that should have the focus"
+msgstr "La barra de cabecera que debe tener el foco"
+
+#: src/hdy-keypad-button.c:227
+msgid "Digit"
+msgstr "Dígito"
+
+#: src/hdy-keypad-button.c:228
+msgid "The keypad digit of the button"
+msgstr ""
+
+#: src/hdy-keypad-button.c:234
+msgid "Symbols"
+msgstr "Símbolos"
+
+#: src/hdy-keypad-button.c:235
+msgid "The keypad symbols of the button. The first symbol is used as the digit"
+msgstr ""
+
+#: src/hdy-keypad-button.c:241 src/hdy-keypad.c:278
+msgid "Show Symbols"
+msgstr ""
+
+#: src/hdy-keypad-button.c:242 src/hdy-keypad.c:279
+msgid "Whether the second line of symbols should be shown or not"
+msgstr ""
+
+#: src/hdy-keypad.c:264
+msgid "Row spacing"
+msgstr "Espaciado de filas"
+
+#: src/hdy-keypad.c:265
+msgid "The amount of space between two consecutive rows"
+msgstr ""
+
+#: src/hdy-keypad.c:271
+msgid "Column spacing"
+msgstr "Espaciado de columnas"
+
+#: src/hdy-keypad.c:272
+msgid "The amount of space between two consecutive columns"
+msgstr ""
+
+#: src/hdy-keypad.c:285
+msgid "Only Digits"
+msgstr "Sólo dígitos"
+
+#: src/hdy-keypad.c:286
+msgid ""
+"Whether the keypad should show only digits or also extra buttons for #, *"
+msgstr ""
+
+#: src/hdy-keypad.c:292
+msgid "Entry widget"
+msgstr "Widget de entrada"
+
+#: src/hdy-keypad.c:293
+msgid "The entry widget connected to the keypad"
+msgstr ""
+
+#: src/hdy-keypad.c:299
+msgid "Right action widget"
+msgstr ""
+
+#: src/hdy-keypad.c:300
+msgid "The right action widget"
+msgstr ""
+
+#: src/hdy-keypad.c:306
+msgid "Left action widget"
+msgstr ""
+
+#: src/hdy-keypad.c:307
+msgid "The left action widget"
+msgstr ""
+
+#: src/hdy-leaflet.c:955 src/hdy-stackable-box.c:3381
+msgid "Folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:956 src/hdy-stackable-box.c:3382
+msgid "Whether the widget is folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:967 src/hdy-stackable-box.c:3393
+msgid "Horizontally homogeneous folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:968
+msgid "Horizontally homogeneous sizing when the leaflet is folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:979 src/hdy-stackable-box.c:3405
+msgid "Vertically homogeneous folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:980
+msgid "Vertically homogeneous sizing when the leaflet is folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:991 src/hdy-stackable-box.c:3417
+msgid "Box horizontally homogeneous"
+msgstr ""
+
+#: src/hdy-leaflet.c:992
+msgid "Horizontally homogeneous sizing when the leaflet is unfolded"
+msgstr ""
+
+#: src/hdy-leaflet.c:1003 src/hdy-stackable-box.c:3429
+msgid "Box vertically homogeneous"
+msgstr ""
+
+#: src/hdy-leaflet.c:1004
+msgid "Vertically homogeneous sizing when the leaflet is unfolded"
+msgstr ""
+
+#: src/hdy-leaflet.c:1011
+msgid "The widget currently visible when the leaflet is folded"
+msgstr ""
+
+#: src/hdy-leaflet.c:1018 src/hdy-stackable-box.c:3444
+msgid "The name of the widget currently visible when the children are stacked"
+msgstr ""
+
+#: src/hdy-leaflet.c:1037 src/hdy-stackable-box.c:3463
+msgid "The type of animation used to transition between modes and children"
+msgstr ""
+
+#: src/hdy-leaflet.c:1043 src/hdy-stackable-box.c:3469
+msgid "Mode transition duration"
+msgstr ""
+
+#: src/hdy-leaflet.c:1044 src/hdy-stackable-box.c:3470
+msgid "The mode transition animation duration, in milliseconds"
+msgstr ""
+
+#: src/hdy-leaflet.c:1050 src/hdy-stackable-box.c:3476
+msgid "Child transition duration"
+msgstr ""
+
+#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3477
+msgid "The child transition animation duration, in milliseconds"
+msgstr ""
+
+#: src/hdy-leaflet.c:1057 src/hdy-stackable-box.c:3483
+msgid "Child transition running"
+msgstr ""
+
+#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3484
+msgid "Whether or not the child transition is currently running"
+msgstr ""
+
+#: src/hdy-leaflet.c:1120
+msgid "Allow visible"
+msgstr ""
+
+#: src/hdy-leaflet.c:1121
+msgid "Whether the child can be visible in folded mode"
+msgstr ""
+
+#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252
+msgid "Description"
+msgstr "Descripción"
+
+#: src/hdy-preferences-row.c:116
+msgid "The title of the preference"
+msgstr "El título de la preferencia"
+
+#: src/hdy-preferences-window.c:135
+msgid "Untitled page"
+msgstr "Página sin título"
+
+#: src/hdy-preferences-window.c:438
+msgid "Search enabled"
+msgstr "Búsqueda activada"
+
+#: src/hdy-preferences-window.c:439
+msgid "Whether search is enabled"
+msgstr "Indica si la búsqueda está activada"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Preferencias"
+
+#: src/hdy-preferences-window.ui:72
+msgid "Search"
+msgstr "Buscar"
+
+#: src/hdy-preferences-window.ui:197
+msgid "No Results Found"
+msgstr "No se han encontrado resultados"
+
+#: src/hdy-preferences-window.ui:212
+msgid "Try a different search"
+msgstr "Pruebe a hacer una búsqueda diferente"
+
+#: src/hdy-search-bar.c:451
+msgid "Search Mode Enabled"
+msgstr "Modo de búsqueda activado"
+
+#: src/hdy-search-bar.c:452
+msgid "Whether the search mode is on and the search bar shown"
+msgstr ""
+
+#: src/hdy-search-bar.c:463
+msgid "Show Close Button"
+msgstr "Mostrar botón de cerrar"
+
+#: src/hdy-search-bar.c:464
+msgid "Whether to show the close button in the toolbar"
+msgstr ""
+"Indica si se debe mostrar el botón de cerrar en la barra de herramientas"
+
+#: src/hdy-shadow-helper.c:246
+msgid "Widget"
+msgstr "Widget"
+
+#: src/hdy-shadow-helper.c:247
+msgid "The widget the shadow will be drawn for"
+msgstr ""
+
+#: src/hdy-squeezer.c:1093
+msgid "Homogeneous"
+msgstr "Homogéneo"
+
+#: src/hdy-squeezer.c:1094
+msgid "Homogeneous sizing"
+msgstr "Tamaño homogéneo"
+
+#: src/hdy-squeezer.c:1101
+msgid "The widget currently visible in the squeezer"
+msgstr ""
+
+#: src/hdy-squeezer.c:1115
+msgid "The type of animation used to transition"
+msgstr "El tipo de animación usado para la transición"
+
+#: src/hdy-squeezer.c:1138 src/hdy-swipe-tracker.c:615
+msgid "Enabled"
+msgstr "Activado"
+
+#: src/hdy-squeezer.c:1139
+msgid ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3394
+msgid "Horizontally homogeneous sizing when the widget is folded"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3406
+msgid "Vertically homogeneous sizing when the widget is folded"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3418
+msgid "Horizontally homogeneous sizing when the widget is unfolded"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3430
+msgid "Vertically homogeneous sizing when the widget is unfolded"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3437
+msgid "The widget currently visible when the widget is folded"
+msgstr ""
+
+#: src/hdy-stackable-box.c:3527 src/hdy-stackable-box.c:3528
+msgid "Orientation"
+msgstr "Orientación"
+
+#: src/hdy-swipe-tracker.c:600
+msgid "Swipeable"
+msgstr ""
+
+#: src/hdy-swipe-tracker.c:601
+msgid "The swipeable the swipe tracker is attached to"
+msgstr ""
+
+#: src/hdy-swipe-tracker.c:616
+msgid "Whether the swipe tracker processes events"
+msgstr ""
+
+#: src/hdy-swipe-tracker.c:630
+msgid "Reversed"
+msgstr ""
+
+#: src/hdy-swipe-tracker.c:631
+msgid "Whether swipe direction is reversed"
+msgstr ""
+
+#: src/hdy-title-bar.c:308
+msgid "Selection mode"
+msgstr "Modo de selección"
+
+#: src/hdy-title-bar.c:309
+msgid "Whether or not the title bar is in selection mode"
+msgstr ""
+
+#: src/hdy-value-object.c:191
+msgctxt "HdyValueObjectClass"
+msgid "Value"
+msgstr "Valor"
+
+#: src/hdy-value-object.c:192
+msgctxt "HdyValueObjectClass"
+msgid "The contained value"
+msgstr "El valor contenido"
+
+#: src/hdy-view-switcher-bar.c:185 src/hdy-view-switcher.c:530
+#: src/hdy-view-switcher-title.c:238
+msgid "Policy"
+msgstr "Política"
+
+#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:531
+#: src/hdy-view-switcher-title.c:239
+msgid "The policy to determine the mode to use"
+msgstr "La política para determinar el modo que usar"
+
+#: src/hdy-view-switcher-bar.c:200 src/hdy-view-switcher-button.c:228
+#: src/hdy-view-switcher.c:545 src/hdy-view-switcher-title.c:253
+msgid "Icon Size"
+msgstr "Tamaño del icono"
+
+#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:229
+#: src/hdy-view-switcher.c:546 src/hdy-view-switcher-title.c:254
+msgid "Symbolic size to use for named icon"
+msgstr "Tamaño simbólico que usar para el icono con nombre"
+
+#: src/hdy-view-switcher-bar.c:214 src/hdy-view-switcher-bar.c:215
+#: src/hdy-view-switcher.c:580 src/hdy-view-switcher.c:581
+#: src/hdy-view-switcher-title.c:267 src/hdy-view-switcher-title.c:268
+msgid "Stack"
+msgstr "Pila"
+
+#: src/hdy-view-switcher-bar.c:228
+msgid "Reveal"
+msgstr "Mostrar"
+
+#: src/hdy-view-switcher-bar.c:229
+msgid "Whether the view switcher is revealed"
+msgstr ""
+
+#: src/hdy-view-switcher-button.c:214
+msgid "Icon Name"
+msgstr "Nombre del icono"
+
+#: src/hdy-view-switcher-button.c:215
+msgid "Icon name for image"
+msgstr "Nombre de icono para la imagen"
+
+#: src/hdy-view-switcher-button.c:245
+msgid "Needs attention"
+msgstr "Requiere atención"
+
+#: src/hdy-view-switcher-button.c:246
+msgid "Hint the view needs attention"
+msgstr ""
+
+#: src/hdy-view-switcher.c:565
+msgid "Narrow ellipsize"
+msgstr "Estrechar elipse"
+
+#: src/hdy-view-switcher.c:566
+msgid ""
+"The preferred place to ellipsize the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+msgstr ""
+
+#: src/hdy-view-switcher-title.c:309
+msgid "View switcher enabled"
+msgstr ""
+
+#: src/hdy-view-switcher-title.c:310
+msgid "Whether the view switcher is enabled"
+msgstr ""
+
+#: src/hdy-view-switcher-title.c:323
+msgid "Title visible"
+msgstr "Título visible"
+
+#: src/hdy-view-switcher-title.c:324
+msgid "Whether the title label is visible"
+msgstr "Indica si la etiqueta de título es visible"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Mover"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Redimensionar"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Siempre_encima"
diff --git a/subprojects/libhandy/po/meson.build b/subprojects/libhandy/po/meson.build
new file mode 100644
index 0000000..d1f4e16
--- /dev/null
+++ b/subprojects/libhandy/po/meson.build
@@ -0,0 +1,2 @@
+i18n = import('i18n')
+i18n.gettext('libhandy', preset : 'glib')
diff --git a/subprojects/libhandy/po/pl.po b/subprojects/libhandy/po/pl.po
new file mode 100644
index 0000000..99f76a2
--- /dev/null
+++ b/subprojects/libhandy/po/pl.po
@@ -0,0 +1,76 @@
+# Polish translation for libhandy.
+# Copyright © 2020 the libhandy authors.
+# This file is distributed under the same license as the libhandy package.
+# Piotr Drąg <piotrdrag@gmail.com>, 2020.
+# Aviary.pl <community-poland@mozilla.org>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-06-20 07:06+0000\n"
+"PO-Revision-Date: 2020-06-21 11:20+0200\n"
+"Last-Translator: Piotr Drąg <piotrdrag@gmail.com>\n"
+"Language-Team: Polish <community-poland@mozilla.org>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2);\n"
+
+#: src/hdy-header-bar.c:484
+msgid "Application menu"
+msgstr "Menu programu"
+
+#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Zminimalizuj"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Przywróć"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Zmaksymalizuj"
+
+#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Zamknij"
+
+#: src/hdy-header-bar.c:562
+msgid "Back"
+msgstr "Wstecz"
+
+#: src/hdy-preferences-window.c:135
+msgid "Untitled page"
+msgstr "Strona bez tytułu"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Preferencje"
+
+#: src/hdy-preferences-window.ui:72
+msgid "Search"
+msgstr "Wyszukiwanie"
+
+#: src/hdy-preferences-window.ui:195
+msgid "No Results Found"
+msgstr "Brak wyników"
+
+#: src/hdy-preferences-window.ui:210
+msgid "Try a different search"
+msgstr "Proszę spróbować innych słów"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Przenieś"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Zmień rozmiar"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Zawsze na wierzchu"
diff --git a/subprojects/libhandy/po/pt_BR.po b/subprojects/libhandy/po/pt_BR.po
new file mode 100644
index 0000000..f9facba
--- /dev/null
+++ b/subprojects/libhandy/po/pt_BR.po
@@ -0,0 +1,955 @@
+# Brazilian Portuguese translation for libhandy.
+# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER
+# This file is distributed under the same license as the libhandy package.
+# Rafael Fontenelle <rafaelff@gnome.org>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy master\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-07-29 16:22+0000\n"
+"PO-Revision-Date: 2020-07-30 17:40-0300\n"
+"Last-Translator: Rafael Fontenelle <rafaelff@gnome.org>\n"
+"Language-Team: Brazilian Portuguese <gnome-pt_br-list@gnome.org>\n"
+"Language: pt_BR\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1)\n"
+"X-Generator: Gtranslator 3.36.0\n"
+
+#: glade/glade-hdy-carousel.c:21
+msgid "This property does not apply unless Show Indicators is set."
+msgstr ""
+"Esta propriedade não se aplica a menos que “Mostrar indicadores” estiver "
+"definido."
+
+#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118
+#: glade/glade-hdy-leaflet.c:184
+#, c-format
+msgid "Insert placeholder to %s"
+msgstr "Inserir espaço reservado para %s"
+
+#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144
+#: glade/glade-hdy-leaflet.c:214
+#, c-format
+msgid "Remove placeholder from %s"
+msgstr "Remover espaço reservado de %s"
+
+#: glade/glade-hdy-header-bar.c:18
+msgid "This property does not apply when a custom title is set"
+msgstr ""
+"Esta propriedade não se aplica quando um título personalizado está definido"
+
+#: glade/glade-hdy-header-bar.c:289
+msgid ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+msgstr ""
+"O layout de decoração não se aplica à barras de cabeçalho que não mostram os "
+"controles de janela"
+
+#: glade/glade-hdy-leaflet.c:19
+msgid "This property only applies when the leaflet is folded"
+msgstr "Esta propriedade somente se aplica quando o folheto está dobrado"
+
+#: glade/glade-hdy-preferences-page.c:160
+#, c-format
+msgid "Add group to %s"
+msgstr "Adicionar grupo a %s"
+
+#: glade/glade-hdy-preferences-window.c:228
+#, c-format
+msgid "Add page to %s"
+msgstr "Adicionar página a %s"
+
+#: glade/glade-hdy-search-bar.c:101
+msgid "Search bar is already full"
+msgstr "A barra de pesquisa já está cheia"
+
+#: glade/glade-hdy-utils.h:14
+#, c-format
+msgid "Only objects of type %s can be added to objects of type %s."
+msgstr "Somente objetos do tipo %s podem ser adicionados a objetos do tipo %s."
+
+#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335
+#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179
+#: src/hdy-preferences-page.c:180
+msgid "Icon name"
+msgstr "Nome do ícone"
+
+#: src/hdy-action-row.c:375
+msgid "Activatable widget"
+msgstr "Widget ativável"
+
+#: src/hdy-action-row.c:376
+msgid "The widget to be activated when the row is activated"
+msgstr "O widget para ser ativado quando a linha está ativada"
+
+#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306
+#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294
+msgid "Subtitle"
+msgstr "Subtítulo"
+
+#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292
+#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265
+#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193
+#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115
+#: src/hdy-view-switcher-title.c:280
+msgid "Title"
+msgstr "Título"
+
+#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321
+#: src/hdy-preferences-row.c:130
+msgid "Use underline"
+msgstr "Usar sublinhado"
+
+#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322
+#: src/hdy-preferences-row.c:131
+msgid ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+msgstr ""
+"Se definir, um sublinhado no texto indica a próprio caractere deve ser usado "
+"para uma tecla aceleradora mnemônica"
+
+#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096
+#: src/hdy-carousel.c:947 src/hdy-carousel.c:948
+msgid "Number of pages"
+msgstr "Número de páginas"
+
+#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:964 src/hdy-header-bar.c:2133
+msgid "Position"
+msgstr "Posição"
+
+#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:965
+msgid "Current scrolling position"
+msgstr "Posição de rolagem atual"
+
+#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1046
+#: src/hdy-header-bar.c:2161
+msgid "Spacing"
+msgstr "Espaçamento"
+
+#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1047
+msgid "Spacing between pages"
+msgstr "Espaçamento entre páginas"
+
+#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1091
+msgid "Reveal duration"
+msgstr "Revelar duração"
+
+#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1092
+msgid "Page reveal duration"
+msgstr "Duração de revelação de página"
+
+#: src/hdy-carousel.c:981
+msgid "Interactive"
+msgstr "Interativa"
+
+#: src/hdy-carousel.c:982
+msgid "Whether the widget can be swiped"
+msgstr "Se é possível deslizar pelo widget"
+
+#: src/hdy-carousel.c:997
+msgid "Indicator style"
+msgstr "Estilo do indicador"
+
+#: src/hdy-carousel.c:998
+msgid "Page indicator style"
+msgstr "Estilo do indicador de página"
+
+#: src/hdy-carousel.c:1013
+msgid "Indicator spacing"
+msgstr "Espaçamento do indicador"
+
+#: src/hdy-carousel.c:1014
+msgid "Spacing between content and indicators"
+msgstr "Espaçamento entro conteúdo e indicadores"
+
+#: src/hdy-carousel.c:1032
+msgid "Center content"
+msgstr "Centralizar conteúdo"
+
+#: src/hdy-carousel.c:1033
+msgid "Whether to center pages to compensate for indicators"
+msgstr "Se deve-se centralizar páginas para compensar os indicadores"
+
+#: src/hdy-carousel.c:1062
+msgid "Animation duration"
+msgstr "Duração da animação"
+
+#: src/hdy-carousel.c:1063
+msgid "Default animation duration"
+msgstr "Duração padrão da animação"
+
+#: src/hdy-carousel.c:1077 src/hdy-swipe-tracker.c:802
+msgid "Allow mouse drag"
+msgstr "Permitir arrastar com mouse"
+
+#: src/hdy-carousel.c:1078 src/hdy-swipe-tracker.c:803
+msgid "Whether to allow dragging with mouse pointer"
+msgstr "Se deve-se permitir arrastar com ponteiro do mouse"
+
+#: src/hdy-clamp.c:417
+#| msgid "Maximize"
+msgid "Maximum size"
+msgstr "Tamanho máximo"
+
+#: src/hdy-clamp.c:418
+#| msgid "The maximum width allocated to the child"
+msgid "The maximum size allocated to the child"
+msgstr "O tamanho máximo alocado para o filho"
+
+#: src/hdy-clamp.c:442
+msgid "Tightening threshold"
+msgstr "Limiar de aperto"
+
+#: src/hdy-clamp.c:443
+msgid "The size from which the clamp will tighten its grip on the child"
+msgstr ""
+"O tamanho a partir do qual a operação de clamping vai apertar o tamanho do "
+"filho"
+
+#: src/hdy-combo-row.c:411
+msgid "Selected index"
+msgstr "Índice selecionado"
+
+#: src/hdy-combo-row.c:412
+msgid "The index of the selected item"
+msgstr "O índice do item selecionado"
+
+#: src/hdy-combo-row.c:430
+msgid "Use subtitle"
+msgstr "Usar subtítulo"
+
+#: src/hdy-combo-row.c:431
+msgid "Set the current value as the subtitle"
+msgstr "Define o valor atual como o subtítulo"
+
+#: src/hdy-deck.c:888
+msgid "Horizontally homogeneous"
+msgstr "Horizontalmente homogêneo"
+
+#: src/hdy-deck.c:889
+msgid "Horizontally homogeneous sizing"
+msgstr "Dimensionamento horizontalmente homogêneo"
+
+#: src/hdy-deck.c:902
+msgid "Vertically homogeneous"
+msgstr "Verticalmente homogêneo"
+
+#: src/hdy-deck.c:903
+msgid "Vertically homogeneous sizing"
+msgstr "Dimensionamento verticalmente homogêneo"
+
+#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1093
+#: src/hdy-stackable-box.c:2993
+msgid "Visible child"
+msgstr "Filho visível"
+
+#: src/hdy-deck.c:917
+msgid "The widget currently visible"
+msgstr "O widget atualmente visível"
+
+#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000
+msgid "Name of visible child"
+msgstr "Nome do filho visível"
+
+#: src/hdy-deck.c:931
+msgid "The name of the widget currently visible"
+msgstr "O nome do widget atualmente visível"
+
+#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1107
+#: src/hdy-stackable-box.c:3019
+msgid "Transition type"
+msgstr "Tipo de transição"
+
+#: src/hdy-deck.c:950
+msgid "The type of animation used to transition between children"
+msgstr "O tipo de animação usada para a transição entre filhos"
+
+#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1100
+msgid "Transition duration"
+msgstr "Duração de transição"
+
+#: src/hdy-deck.c:964
+msgid "The transition animation duration, in milliseconds"
+msgstr "A duração da animação de transição, em milissegundos"
+
+#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1115
+msgid "Transition running"
+msgstr "Execução de transição"
+
+#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1116
+msgid "Whether or not the transition is currently running"
+msgstr "Se a transição está atualmente em execução"
+
+#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072
+#: src/hdy-squeezer.c:1122 src/hdy-stackable-box.c:3047
+msgid "Interpolate size"
+msgstr "Tamanho da interpolação"
+
+#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073
+#: src/hdy-squeezer.c:1123 src/hdy-stackable-box.c:3048
+msgid ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+msgstr ""
+"Se o tamanho deve ou não ser suavemente alterado ao alterar entre filhos de "
+"tamanhos diferentes"
+
+#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497
+#: src/hdy-stackable-box.c:3062
+msgid "Can swipe back"
+msgstr "Pode deslizar para trás"
+
+#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063
+msgid ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+msgstr ""
+"Se o gesto de deslize pode ou não ser usado para alternar para o filho "
+"anterior"
+
+#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077
+msgid "Can swipe forward"
+msgstr "Pode deslizar para frente"
+
+#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078
+msgid "Whether or not swipe gesture can be used to switch to the next child"
+msgstr ""
+"Se o gesto de deslize pode ou não ser usado para alternar para o próximo "
+"filho"
+
+#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111
+msgid "Name"
+msgstr "Nome"
+
+#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112
+msgid "The name of the child page"
+msgstr "O nome da página filha"
+
+#: src/hdy-expander-row.c:293
+msgid "The title for this row"
+msgstr "O título para esta linha"
+
+#: src/hdy-expander-row.c:307
+msgid "The subtitle for this row"
+msgstr "O subtítulo para esta linha"
+
+#: src/hdy-expander-row.c:347
+msgid "Expanded"
+msgstr "Expandido"
+
+#: src/hdy-expander-row.c:348
+msgid "Whether the row is expanded"
+msgstr "Se a linha é expandida"
+
+#: src/hdy-expander-row.c:359
+msgid "Enable expansion"
+msgstr "Habilitar expansão"
+
+#: src/hdy-expander-row.c:360
+msgid "Whether the expansion is enabled"
+msgstr "Se a expansão está habilitada"
+
+#: src/hdy-expander-row.c:371
+msgid "Show enable switch"
+msgstr "Mostrar alternador de habilitação"
+
+#: src/hdy-expander-row.c:372
+msgid "Whether the switch enabling the expansion is visible"
+msgstr "Se o alternador que habilita a expansão é visível"
+
+#: src/hdy-header-bar.c:484
+msgid "Application menu"
+msgstr "Menu de aplicativo"
+
+#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Minimizar"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Restaurar"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Maximizar"
+
+#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Fechar"
+
+#: src/hdy-header-bar.c:562
+msgid "Back"
+msgstr "Retornar"
+
+#: src/hdy-header-bar.c:2126
+msgid "Pack type"
+msgstr "Tipo de embalagem"
+
+#: src/hdy-header-bar.c:2127
+msgid ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+msgstr ""
+"Um GtkPackType indicando se o filho está empacotado com referência ao início "
+"ou final do pai"
+
+#: src/hdy-header-bar.c:2134
+msgid "The index of the child in the parent"
+msgstr "O índice do filho dentro do pai"
+
+#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281
+msgid "The title to display"
+msgstr "O título para a ser exibido"
+
+#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295
+msgid "The subtitle to display"
+msgstr "Subtítulo a ser exibido"
+
+#: src/hdy-header-bar.c:2154
+msgid "Custom Title"
+msgstr "Título personalizado"
+
+#: src/hdy-header-bar.c:2155
+msgid "Custom title widget to display"
+msgstr "Componente de título personalizado a ser exibido"
+
+#: src/hdy-header-bar.c:2162
+msgid "The amount of space between children"
+msgstr "A quantidade de espaço entre filhos"
+
+#: src/hdy-header-bar.c:2181
+msgid "Show decorations"
+msgstr "Mostrar decorações"
+
+#: src/hdy-header-bar.c:2182
+msgid "Whether to show window decorations"
+msgstr "Se deve mostrar decorações da janela"
+
+#: src/hdy-header-bar.c:2200
+msgid "Decoration Layout"
+msgstr "Disposição de decoração"
+
+#: src/hdy-header-bar.c:2201
+msgid "The layout for window decorations"
+msgstr "A disposição de decorações da janela"
+
+#: src/hdy-header-bar.c:2214
+msgid "Decoration Layout Set"
+msgstr "Disposição de decoração definido"
+
+#: src/hdy-header-bar.c:2215
+msgid "Whether the decoration-layout property has been set"
+msgstr "Se a propriedade “decoration-layout” foi definida"
+
+#: src/hdy-header-bar.c:2229
+msgid "Has Subtitle"
+msgstr "Possui subtítulo"
+
+#: src/hdy-header-bar.c:2230
+msgid "Whether to reserve space for a subtitle"
+msgstr "Se deve reservar espaço para um subtítulo"
+
+#: src/hdy-header-bar.c:2236
+msgid "Centering policy"
+msgstr "Política de centralização"
+
+#: src/hdy-header-bar.c:2237
+msgid "The policy to horizontally align the center widget"
+msgstr "A política para alinhar horizontalmente o widget central"
+
+#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1101
+msgid "The animation duration, in milliseconds"
+msgstr "A duração da animação, em milissegundos"
+
+#: src/hdy-header-group.c:827
+#| msgid "Decoration Layout"
+msgid "Decorate all"
+msgstr "Decorar todos"
+
+#: src/hdy-header-group.c:828
+msgid ""
+"Whether the elements of the group should all receive the full decoration"
+msgstr "Se os elementos do grupo devem todos receber a decoração completa"
+
+#: src/hdy-keypad-button.c:225
+msgid "Digit"
+msgstr "Dígito"
+
+#: src/hdy-keypad-button.c:226
+msgid "The keypad digit of the button"
+msgstr "O dígito de teclado numérico do botão"
+
+#: src/hdy-keypad-button.c:232
+msgid "Symbols"
+msgstr "Símbolos"
+
+#: src/hdy-keypad-button.c:233
+msgid "The keypad symbols of the button. The first symbol is used as the digit"
+msgstr ""
+"Os símbolos de teclado numérico do botão. O primeiro símbolo está suado como "
+"o dígito"
+
+#: src/hdy-keypad-button.c:239
+#| msgid "Show Symbols"
+msgid "Show symbols"
+msgstr "Mostrar símbolos"
+
+#: src/hdy-keypad-button.c:240
+msgid "Whether the second line of symbols should be shown or not"
+msgstr "Se a segunda linha de símbolos deve ser mostrada ou não"
+
+#: src/hdy-keypad.c:247
+msgid "Row spacing"
+msgstr "Espaçamento de linha"
+
+#: src/hdy-keypad.c:248
+msgid "The amount of space between two consecutive rows"
+msgstr "A quantidade de espaço entre duas linhas consecutivas"
+
+#: src/hdy-keypad.c:261
+msgid "Column spacing"
+msgstr "Espaçamento de coluna"
+
+#: src/hdy-keypad.c:262
+msgid "The amount of space between two consecutive columns"
+msgstr "A quantidade de espaço entre duas colunas consecutivas"
+
+#: src/hdy-keypad.c:276
+#| msgid "Title visible"
+msgid "Letters visible"
+msgstr "Letras visíveis"
+
+#: src/hdy-keypad.c:277
+#| msgid "Whether the title label is visible"
+msgid "Whether the letters below the digits should be visible"
+msgstr "Se as letras embaixo dos dígitos devem estar visíveis"
+
+#: src/hdy-keypad.c:291
+#| msgid "Allow visible"
+msgid "Symbols visible"
+msgstr "Símbolos visíveis"
+
+#: src/hdy-keypad.c:292
+#| msgid "Whether the second line of symbols should be shown or not"
+msgid "Whether the hash, plus, and asterisk symbols should be visible"
+msgstr "Se os símbolos de cerquilha, mais e asterisco devem estar visíveis"
+
+#: src/hdy-keypad.c:306
+msgid "Entry"
+msgstr "Entrada"
+
+#: src/hdy-keypad.c:307
+msgid "The entry widget connected to the keypad"
+msgstr "O widget de entrada conectado ao teclado"
+
+#: src/hdy-keypad.c:320
+msgid "End action"
+msgstr "Ação de fim"
+
+#: src/hdy-keypad.c:321
+#| msgid "The left action widget"
+msgid "The end action widget"
+msgstr "O widget de ação de fim"
+
+#: src/hdy-keypad.c:334
+#| msgid "Orientation"
+msgid "Start action"
+msgstr "Ação de início"
+
+#: src/hdy-keypad.c:335
+#| msgid "The right action widget"
+msgid "The start action widget"
+msgstr "O widget de ação de início"
+
+#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938
+msgid "Folded"
+msgstr "Dobrado"
+
+#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939
+msgid "Whether the widget is folded"
+msgstr "Se o widget está dobrado"
+
+#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950
+msgid "Horizontally homogeneous folded"
+msgstr "Dobrado horizontalmente homogêneo"
+
+#: src/hdy-leaflet.c:976
+msgid "Horizontally homogeneous sizing when the leaflet is folded"
+msgstr ""
+"Dimensionamento horizontalmente homogêneo quando o folheto está dobrado"
+
+#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962
+msgid "Vertically homogeneous folded"
+msgstr "Dobrado verticalmente homogêneo"
+
+#: src/hdy-leaflet.c:988
+msgid "Vertically homogeneous sizing when the leaflet is folded"
+msgstr "Dimensionamento verticalmente homogêneo quando o folheto está dobrado"
+
+#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974
+msgid "Box horizontally homogeneous"
+msgstr "Caixa horizontalmente homogênea"
+
+#: src/hdy-leaflet.c:1000
+msgid "Horizontally homogeneous sizing when the leaflet is unfolded"
+msgstr ""
+"Dimensionamento horizontalmente homogêneo quando o folheto está desdobrado"
+
+#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986
+msgid "Box vertically homogeneous"
+msgstr "Caixa verticalmente homogênea"
+
+#: src/hdy-leaflet.c:1012
+msgid "Vertically homogeneous sizing when the leaflet is unfolded"
+msgstr ""
+"Dimensionamento verticalmente homogêneo quando o folheto está desdobrado"
+
+#: src/hdy-leaflet.c:1019
+msgid "The widget currently visible when the leaflet is folded"
+msgstr "O widget atualmente visível quando o folheto está dobrado"
+
+#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001
+msgid "The name of the widget currently visible when the children are stacked"
+msgstr "O nome do widget atualmente visível quando os filhos estão empilhados"
+
+#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020
+msgid "The type of animation used to transition between modes and children"
+msgstr "O tipo da animação usada para transicionar entre modos e filhos"
+
+#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026
+msgid "Mode transition duration"
+msgstr "Duração da transição de modo"
+
+#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027
+msgid "The mode transition animation duration, in milliseconds"
+msgstr "A duração da animação da transição de modo, em milissegundos"
+
+#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033
+msgid "Child transition duration"
+msgstr "Duração da transição de filho"
+
+#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034
+msgid "The child transition animation duration, in milliseconds"
+msgstr "A duração da animação de transição de filho, em milissegundos"
+
+#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040
+msgid "Child transition running"
+msgstr "Execução da transição de filho"
+
+#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041
+msgid "Whether or not the child transition is currently running"
+msgstr "Se a transição de filho está ou não atualmente em execução"
+
+#: src/hdy-leaflet.c:1129
+msgid "Navigatable"
+msgstr "Navegável"
+
+#: src/hdy-leaflet.c:1130
+#| msgid "Whether the child can be visible in folded mode"
+msgid "Whether the child can be navigated to"
+msgstr "Se o filho pode ser navegado"
+
+#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252
+msgid "Description"
+msgstr "Descrição"
+
+#: src/hdy-preferences-row.c:116
+msgid "The title of the preference"
+msgstr "O título da preferência"
+
+#: src/hdy-preferences-window.c:141
+msgid "Untitled page"
+msgstr "Página sem título"
+
+#: src/hdy-preferences-window.c:483
+msgid "Search enabled"
+msgstr "Pesquisa habilitada"
+
+#: src/hdy-preferences-window.c:484
+msgid "Whether search is enabled"
+msgstr "Se a pesquisa está habilitada"
+
+#: src/hdy-preferences-window.c:498
+#| msgid ""
+#| "Whether or not swipe gesture can be used to switch to the previous child"
+msgid ""
+"Whether or not swipe gesture can be used to switch from a subpage to the "
+"preferences"
+msgstr ""
+"Se o gesto de deslize pode ou não ser usado para alternar de uma subpágina "
+"para as preferências"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Preferências"
+
+#: src/hdy-preferences-window.ui:78
+msgid "Search"
+msgstr "Pesquisa"
+
+#: src/hdy-preferences-window.ui:201
+msgid "No Results Found"
+msgstr "Nenhum resultado encontrado"
+
+#: src/hdy-preferences-window.ui:216
+msgid "Try a different search"
+msgstr "Tente uma pesquisa diferente"
+
+#: src/hdy-search-bar.c:451
+msgid "Search Mode Enabled"
+msgstr "Modo de pesquisa habilitado"
+
+#: src/hdy-search-bar.c:452
+msgid "Whether the search mode is on and the search bar shown"
+msgstr "Se o modo de pesquisa está habilitado e a barra de pesquisa mostrada"
+
+#: src/hdy-search-bar.c:463
+msgid "Show Close Button"
+msgstr "Mostrar o botão de fechar"
+
+#: src/hdy-search-bar.c:464
+msgid "Whether to show the close button in the toolbar"
+msgstr "Se deve ser mostrado o botão de fechar na barra de ferramentas"
+
+#: src/hdy-shadow-helper.c:254
+msgid "Widget"
+msgstr "Widget"
+
+#: src/hdy-shadow-helper.c:255
+msgid "The widget the shadow will be drawn for"
+msgstr "O widget para a qual a sombra será desenhada"
+
+#: src/hdy-squeezer.c:1086
+msgid "Homogeneous"
+msgstr "Homogêneo"
+
+#: src/hdy-squeezer.c:1087
+msgid "Homogeneous sizing"
+msgstr "Dimensionamento homogêneo"
+
+#: src/hdy-squeezer.c:1094
+msgid "The widget currently visible in the squeezer"
+msgstr "O widget atualmente visível no squeezer"
+
+#: src/hdy-squeezer.c:1108
+msgid "The type of animation used to transition"
+msgstr "O tipo de animação usada para a transição"
+
+#: src/hdy-squeezer.c:1143
+msgid "X align"
+msgstr "Alinh. X"
+
+#: src/hdy-squeezer.c:1144
+msgid "The horizontal alignment, from 0 (start) to 1 (end)"
+msgstr "O alinhamento horizontal, de 0 (início) até 1 (fim)"
+
+#: src/hdy-squeezer.c:1165
+msgid "Y align"
+msgstr "Alinh. Y"
+
+#: src/hdy-squeezer.c:1166
+msgid "The vertical alignment, from 0 (top) to 1 (bottom)"
+msgstr "O alinhamento vertical, de 0 (topo) até 1 (base)"
+
+#: src/hdy-squeezer.c:1175 src/hdy-swipe-tracker.c:772
+msgid "Enabled"
+msgstr "Habilitado"
+
+#: src/hdy-squeezer.c:1176
+msgid ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+msgstr ""
+"Se o filho pode ser escolhido ou deve ser ignorado ao procurar pelo filho "
+"que caiba no melhor tamanho disponível"
+
+#: src/hdy-stackable-box.c:2951
+msgid "Horizontally homogeneous sizing when the widget is folded"
+msgstr "Dimensionamento horizontalmente homogêneo quando o widget está dobrado"
+
+#: src/hdy-stackable-box.c:2963
+msgid "Vertically homogeneous sizing when the widget is folded"
+msgstr "Dimensionamento verticalmente homogêneo quando o widget está dobrado"
+
+#: src/hdy-stackable-box.c:2975
+msgid "Horizontally homogeneous sizing when the widget is unfolded"
+msgstr ""
+"Dimensionamento horizontalmente homogêneo quando o widget está desdobrado"
+
+#: src/hdy-stackable-box.c:2987
+msgid "Vertically homogeneous sizing when the widget is unfolded"
+msgstr ""
+"Dimensionamento verticalmente homogêneo quando o widget está desdobrado"
+
+#: src/hdy-stackable-box.c:2994
+msgid "The widget currently visible when the widget is folded"
+msgstr "O widget atualmente visível quando o widget está desdobrado"
+
+#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085
+msgid "Orientation"
+msgstr "Orientação"
+
+#: src/hdy-swipe-tracker.c:757
+msgid "Swipeable"
+msgstr "Deslizável"
+
+#: src/hdy-swipe-tracker.c:758
+msgid "The swipeable the swipe tracker is attached to"
+msgstr "O deslizável ao qual o rastreador de deslize está anexado"
+
+#: src/hdy-swipe-tracker.c:773
+msgid "Whether the swipe tracker processes events"
+msgstr "Se o rastreador de deslize processa eventos"
+
+#: src/hdy-swipe-tracker.c:787
+msgid "Reversed"
+msgstr "Invertida"
+
+#: src/hdy-swipe-tracker.c:788
+msgid "Whether swipe direction is reversed"
+msgstr "Se a direção de deslize é revertida"
+
+#: src/hdy-title-bar.c:308
+msgid "Selection mode"
+msgstr "Modo de seleção"
+
+#: src/hdy-title-bar.c:309
+msgid "Whether or not the title bar is in selection mode"
+msgstr "Se a barra de título estão ou não no modo de seleção"
+
+#: src/hdy-value-object.c:191
+msgctxt "HdyValueObjectClass"
+msgid "Value"
+msgstr "Valor"
+
+#: src/hdy-value-object.c:192
+msgctxt "HdyValueObjectClass"
+msgid "The contained value"
+msgstr "O valor contido"
+
+#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511
+#: src/hdy-view-switcher-title.c:237
+msgid "Policy"
+msgstr "Política"
+
+#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512
+#: src/hdy-view-switcher-title.c:238
+msgid "The policy to determine the mode to use"
+msgstr "A política para determinar o modo para usar"
+
+#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217
+#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252
+msgid "Icon Size"
+msgstr "Tamanho do ícone"
+
+#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218
+#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253
+msgid "Symbolic size to use for named icon"
+msgstr "Tamanho simbólico a ser utilizado para ícone nomeado"
+
+#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216
+#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562
+#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267
+msgid "Stack"
+msgstr "Pilha"
+
+#: src/hdy-view-switcher-bar.c:229
+msgid "Reveal"
+msgstr "Revelar"
+
+#: src/hdy-view-switcher-bar.c:230
+msgid "Whether the view switcher is revealed"
+msgstr "Se o alternador de visão é revelado"
+
+#: src/hdy-view-switcher-button.c:203
+msgid "Icon Name"
+msgstr "Nome do ícone"
+
+#: src/hdy-view-switcher-button.c:204
+msgid "Icon name for image"
+msgstr "Nome do ícone para a imagem"
+
+#: src/hdy-view-switcher-button.c:234
+msgid "Needs attention"
+msgstr "Precisa de atenção"
+
+#: src/hdy-view-switcher-button.c:235
+msgid "Hint the view needs attention"
+msgstr "Sugere que a visão precisa de atenção"
+
+#: src/hdy-view-switcher.c:546
+msgid "Narrow ellipsize"
+msgstr "Reticência estreita"
+
+#: src/hdy-view-switcher.c:547
+msgid ""
+"The preferred place to ellipsize the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+msgstr ""
+"O lugar preferido para colocar reticências na string, se o rótulo do modo "
+"estreito não tiver espaço suficiente para exibir a string inteira"
+
+#: src/hdy-view-switcher-title.c:308
+msgid "View switcher enabled"
+msgstr "Alternador de visão habilitado"
+
+#: src/hdy-view-switcher-title.c:309
+msgid "Whether the view switcher is enabled"
+msgstr "Se o alternador de visão está habilitado"
+
+#: src/hdy-view-switcher-title.c:322
+msgid "Title visible"
+msgstr "Título visível"
+
+#: src/hdy-view-switcher-title.c:323
+msgid "Whether the title label is visible"
+msgstr "Se o rótulo do título está visível"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Mover"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Redimensionar"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Sempre no topo"
+
+#~ msgid "Maximum width"
+#~ msgstr "Largura máxima"
+
+#~ msgid "Linear growth width"
+#~ msgstr "Largura de crescimento linear"
+
+#~ msgid "The width up to which the child will be allocated all the width"
+#~ msgstr "A largura até a qual o filho terá a alocação de toda a largura"
+
+#~ msgid "Focus"
+#~ msgstr "Foco"
+
+#~ msgid "The header bar that should have the focus"
+#~ msgstr "A barra de cabeçalho que deve ter o foco"
+
+#~ msgid "Only Digits"
+#~ msgstr "Apenas dígitos"
+
+#~ msgid ""
+#~ "Whether the keypad should show only digits or also extra buttons for #, *"
+#~ msgstr ""
+#~ "Se o teclado numérico deve mostrar apenas dígitos ou também botões extras "
+#~ "para #, *"
+
+#~ msgid "Entry widget"
+#~ msgstr "Widget de entrada"
+
+#~ msgid "Right action widget"
+#~ msgstr "Widget de ação direito"
+
+#~ msgid "Left action widget"
+#~ msgstr "Widget de ação esquerdo"
diff --git a/subprojects/libhandy/po/ro.po b/subprojects/libhandy/po/ro.po
new file mode 100644
index 0000000..a7d2664
--- /dev/null
+++ b/subprojects/libhandy/po/ro.po
@@ -0,0 +1,871 @@
+# Romanian translation for libhandy.
+# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER
+# This file is distributed under the same license as the libhandy package.
+# Florentina Mușat <florentina.musat.28@gmail.com>, 2020.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy master\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-07-26 22:20+0000\n"
+"PO-Revision-Date: 2020-07-27 15:48+0300\n"
+"Language-Team: Romanian <gnomero-list@lists.sourceforge.net>\n"
+"Language: ro\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < "
+"20)) ? 1 : 2);;\n"
+"Last-Translator: Florentina Mușat <florentina.musat.28@gmail.com>\n"
+"X-Generator: Poedit 2.4\n"
+
+#: glade/glade-hdy-carousel.c:21
+msgid "This property does not apply unless Show Indicators is set."
+msgstr ""
+"Această proprietate nu se aplică decât dacă Arată Indicatori este stabilită."
+
+#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118
+#: glade/glade-hdy-leaflet.c:184
+#, c-format
+msgid "Insert placeholder to %s"
+msgstr "Inserează substituent pentru %s"
+
+#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144
+#: glade/glade-hdy-leaflet.c:214
+#, c-format
+msgid "Remove placeholder from %s"
+msgstr "Elimină substituentul din %s"
+
+#: glade/glade-hdy-header-bar.c:18
+msgid "This property does not apply when a custom title is set"
+msgstr ""
+"Această proprietate nu se aplică când un titlu personalizat este stabilit"
+
+#: glade/glade-hdy-header-bar.c:289
+msgid ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+msgstr ""
+"Aspectul decorației nu se aplică barelor de antet care nu afișează controale "
+"pentru fereastră"
+
+#: glade/glade-hdy-leaflet.c:19
+msgid "This property only applies when the leaflet is folded"
+msgstr "Această proprietate se aplică doar când manifestul este pliat"
+
+#: glade/glade-hdy-preferences-page.c:160
+#, c-format
+msgid "Add group to %s"
+msgstr "Adaugă grupul la %s"
+
+#: glade/glade-hdy-preferences-window.c:228
+#, c-format
+msgid "Add page to %s"
+msgstr "Adaugă pagina la %s"
+
+#: glade/glade-hdy-search-bar.c:101
+msgid "Search bar is already full"
+msgstr "Bara de căutare este plină deja"
+
+#: glade/glade-hdy-utils.h:14
+#, c-format
+msgid "Only objects of type %s can be added to objects of type %s."
+msgstr "Doar obiectele de tipul %s pot fi adăugate obiectelor de tip %s."
+
+#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335
+#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179
+#: src/hdy-preferences-page.c:180
+msgid "Icon name"
+msgstr "Nume iconiță"
+
+#: src/hdy-action-row.c:375
+msgid "Activatable widget"
+msgstr "Widget care se poate activa"
+
+#: src/hdy-action-row.c:376
+msgid "The widget to be activated when the row is activated"
+msgstr "Widgetul de activat când rândul este activat"
+
+#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306
+#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294
+msgid "Subtitle"
+msgstr "Subitlu"
+
+#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292
+#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265
+#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193
+#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115
+#: src/hdy-view-switcher-title.c:280
+msgid "Title"
+msgstr "Titlu"
+
+#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321
+#: src/hdy-preferences-row.c:130
+msgid "Use underline"
+msgstr "Utilizează sublinieri"
+
+#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322
+#: src/hdy-preferences-row.c:131
+msgid ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+msgstr ""
+"Dacă este stabilit, o linie de subliniere în text indică faptul că următorul "
+"caracter ar trebui să fie utilizat ca tasta de accelerare mnemonic"
+
+#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096
+#: src/hdy-carousel.c:949 src/hdy-carousel.c:950
+msgid "Number of pages"
+msgstr "Număr de pagini"
+
+#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:966 src/hdy-header-bar.c:2133
+msgid "Position"
+msgstr "Poziționează"
+
+#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:967
+msgid "Current scrolling position"
+msgstr "Poziție de derulare curentă"
+
+#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1048
+#: src/hdy-header-bar.c:2161
+msgid "Spacing"
+msgstr "Spațiere"
+
+#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1049
+msgid "Spacing between pages"
+msgstr "Spațiere între pagini"
+
+#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1093
+msgid "Reveal duration"
+msgstr "Durată de dezvăluire"
+
+#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1094
+msgid "Page reveal duration"
+msgstr "Durată de dezvăluire a paginii"
+
+#: src/hdy-carousel.c:983
+msgid "Interactive"
+msgstr "Interactiv"
+
+#: src/hdy-carousel.c:984
+msgid "Whether the widget can be swiped"
+msgstr "Dacă widgetul poate fi glisat"
+
+#: src/hdy-carousel.c:999
+msgid "Indicator style"
+msgstr "Stil de indicator"
+
+#: src/hdy-carousel.c:1000
+msgid "Page indicator style"
+msgstr "Stil de indicator de pagină"
+
+#: src/hdy-carousel.c:1015
+msgid "Indicator spacing"
+msgstr "Spațiere de indicator"
+
+#: src/hdy-carousel.c:1016
+msgid "Spacing between content and indicators"
+msgstr "Spațiere între conținut și indicatori"
+
+#: src/hdy-carousel.c:1034
+msgid "Center content"
+msgstr "Centrează conținutul"
+
+#: src/hdy-carousel.c:1035
+msgid "Whether to center pages to compensate for indicators"
+msgstr "Dacă să se centreze paginile pentru a compensa pentru indicatori"
+
+#: src/hdy-carousel.c:1064
+msgid "Animation duration"
+msgstr "Durata animației"
+
+#: src/hdy-carousel.c:1065
+msgid "Default animation duration"
+msgstr "Durata de animație implicită"
+
+#: src/hdy-carousel.c:1079 src/hdy-swipe-tracker.c:802
+msgid "Allow mouse drag"
+msgstr "Permite tragerea mausului"
+
+#: src/hdy-carousel.c:1080 src/hdy-swipe-tracker.c:803
+msgid "Whether to allow dragging with mouse pointer"
+msgstr "Dacă să se permită tragerea cu indicatorul mausului"
+
+#: src/hdy-clamp.c:417
+msgid "Maximum size"
+msgstr "Dimensiunea maximă"
+
+#: src/hdy-clamp.c:418
+msgid "The maximum size allocated to the child"
+msgstr "Dimensiunea maximă alocată la copil"
+
+#: src/hdy-clamp.c:442
+msgid "Tightening threshold"
+msgstr "Pragul de strângere"
+
+#: src/hdy-clamp.c:443
+msgid "The size from which the clamp will tighten its grip on the child"
+msgstr "Dimensiunea de la care clema va strânge strânsoarea asupra copilului"
+
+#: src/hdy-combo-row.c:411
+msgid "Selected index"
+msgstr "Indexul selectat"
+
+#: src/hdy-combo-row.c:412
+msgid "The index of the selected item"
+msgstr "Indexul elementului selectat"
+
+#: src/hdy-combo-row.c:430
+msgid "Use subtitle"
+msgstr "Utilizează subtitrare"
+
+#: src/hdy-combo-row.c:431
+msgid "Set the current value as the subtitle"
+msgstr "Stabilește valoarea curentă ca subtitrare"
+
+#: src/hdy-deck.c:888
+msgid "Horizontally homogeneous"
+msgstr "Omogenă orizontal"
+
+#: src/hdy-deck.c:889
+msgid "Horizontally homogeneous sizing"
+msgstr "Dimensionare omogenă orizontală"
+
+#: src/hdy-deck.c:902
+msgid "Vertically homogeneous"
+msgstr "Omogenă vertical"
+
+#: src/hdy-deck.c:903
+msgid "Vertically homogeneous sizing"
+msgstr "Dimensionare verticală omogenă"
+
+#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1073
+#: src/hdy-stackable-box.c:3001
+msgid "Visible child"
+msgstr "Copil vizibil"
+
+#: src/hdy-deck.c:917
+msgid "The widget currently visible"
+msgstr "Widget-ul vizibil curent"
+
+#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3008
+msgid "Name of visible child"
+msgstr "Numele copilului vizibil"
+
+#: src/hdy-deck.c:931
+msgid "The name of the widget currently visible"
+msgstr "Numele widget-ului vizibil curent"
+
+#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1087
+#: src/hdy-stackable-box.c:3027
+msgid "Transition type"
+msgstr "Tip de tranziție"
+
+#: src/hdy-deck.c:950
+msgid "The type of animation used to transition between children"
+msgstr "Tipul de animație utilizat pentru tranziția dintre copii"
+
+#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1080
+msgid "Transition duration"
+msgstr "Durata tranziției"
+
+#: src/hdy-deck.c:964
+msgid "The transition animation duration, in milliseconds"
+msgstr "Durata animației de tranziție, în milisecunde"
+
+#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1095
+msgid "Transition running"
+msgstr "Tranziția rulează"
+
+#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1096
+msgid "Whether or not the transition is currently running"
+msgstr "Dacă rulează sau nu tranziția în mod curent"
+
+#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072
+#: src/hdy-squeezer.c:1102 src/hdy-stackable-box.c:3055
+msgid "Interpolate size"
+msgstr "Dimensiune interpolare"
+
+#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073
+#: src/hdy-squeezer.c:1103 src/hdy-stackable-box.c:3056
+msgid ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+msgstr ""
+"Dacă ar trebui sau nu să se schimbe neted dimensiunea când se comută între "
+"copiii dimensionați diferit"
+
+#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-stackable-box.c:3070
+msgid "Can swipe back"
+msgstr "Poate să gliseze înapoi"
+
+#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3071
+msgid ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+msgstr ""
+"Dacă se poate sau nu să se utilizeze un gest de glisare pentru a comuta la "
+"copilul anterior"
+
+#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3085
+msgid "Can swipe forward"
+msgstr "Poate să gliseze înainte"
+
+#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3086
+msgid "Whether or not swipe gesture can be used to switch to the next child"
+msgstr ""
+"Dacă se poate sau nu să se utilizeze un gest de glisare pentru a comuta la "
+"copilul următor"
+
+#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111 src/hdy-stackable-box.c:3102
+msgid "Name"
+msgstr "Nume"
+
+#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112 src/hdy-stackable-box.c:3103
+msgid "The name of the child page"
+msgstr "Numele paginii copilului"
+
+#: src/hdy-expander-row.c:293
+msgid "The title for this row"
+msgstr "Titlul pentru acest rând"
+
+#: src/hdy-expander-row.c:307
+msgid "The subtitle for this row"
+msgstr "Subtitrarea pentru acest rând"
+
+#: src/hdy-expander-row.c:347
+msgid "Expanded"
+msgstr "Extins"
+
+#: src/hdy-expander-row.c:348
+msgid "Whether the row is expanded"
+msgstr "Dacă rândul este extins"
+
+#: src/hdy-expander-row.c:359
+msgid "Enable expansion"
+msgstr "Activează extinderea"
+
+#: src/hdy-expander-row.c:360
+msgid "Whether the expansion is enabled"
+msgstr "Dacă extinderea este activată"
+
+#: src/hdy-expander-row.c:371
+msgid "Show enable switch"
+msgstr "Arată activează comutarea"
+
+#: src/hdy-expander-row.c:372
+msgid "Whether the switch enabling the expansion is visible"
+msgstr "Dacă comutatorul care activează extinderea este vizibil"
+
+#: src/hdy-header-bar.c:484
+msgid "Application menu"
+msgstr "Meniul aplicației"
+
+#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Minimizează"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Restaurează"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Maximizează"
+
+#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Închide"
+
+#: src/hdy-header-bar.c:562
+msgid "Back"
+msgstr "Înapoi"
+
+#: src/hdy-header-bar.c:2126
+msgid "Pack type"
+msgstr "Tip de pachet"
+
+#: src/hdy-header-bar.c:2127
+msgid ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+msgstr ""
+"Un GtkPackType care indică dacă copilul este împachetat cu referința la "
+"începutul sau sfârșitul părintelui"
+
+#: src/hdy-header-bar.c:2134
+msgid "The index of the child in the parent"
+msgstr "Indexul copilului în părinte"
+
+#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281
+msgid "The title to display"
+msgstr "Titlul de afișat"
+
+#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295
+msgid "The subtitle to display"
+msgstr "Subtitrarea de afișat"
+
+#: src/hdy-header-bar.c:2154
+msgid "Custom Title"
+msgstr "Titlu personalizat"
+
+#: src/hdy-header-bar.c:2155
+msgid "Custom title widget to display"
+msgstr "Widget de titlu personalizat de afișat"
+
+#: src/hdy-header-bar.c:2162
+msgid "The amount of space between children"
+msgstr "Cantitatea de spațiu între copii"
+
+#: src/hdy-header-bar.c:2181
+msgid "Show decorations"
+msgstr "Arată decorațiile"
+
+#: src/hdy-header-bar.c:2182
+msgid "Whether to show window decorations"
+msgstr "Dacă să se arate decorațiile ferestrei"
+
+#: src/hdy-header-bar.c:2200
+msgid "Decoration Layout"
+msgstr "Aspect decorație"
+
+#: src/hdy-header-bar.c:2201
+msgid "The layout for window decorations"
+msgstr "Aspectul decorațiilor ferestrei"
+
+#: src/hdy-header-bar.c:2214
+msgid "Decoration Layout Set"
+msgstr "Aranjament de decorație stabilit"
+
+#: src/hdy-header-bar.c:2215
+msgid "Whether the decoration-layout property has been set"
+msgstr "Dacă proprietatea de aranjament-decorație a fost stabilită"
+
+#: src/hdy-header-bar.c:2229
+msgid "Has Subtitle"
+msgstr "Are subtitrare"
+
+#: src/hdy-header-bar.c:2230
+msgid "Whether to reserve space for a subtitle"
+msgstr "Dacă să se rezerve spațiu pentru o subtitrare"
+
+#: src/hdy-header-bar.c:2236
+msgid "Centering policy"
+msgstr "Politică de centrare"
+
+#: src/hdy-header-bar.c:2237
+msgid "The policy to horizontally align the center widget"
+msgstr "Politic de aliniere orizontală a widgetului de centru"
+
+#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1081
+msgid "The animation duration, in milliseconds"
+msgstr "Durata animației, în milisecunde"
+
+#: src/hdy-header-group.c:827
+msgid "Decorate all"
+msgstr "Decorează toate"
+
+#: src/hdy-header-group.c:828
+msgid ""
+"Whether the elements of the group should all receive the full decoration"
+msgstr ""
+"Dacă elementele grupului ar trebui ca toate să primească decorația completă"
+
+#: src/hdy-keypad-button.c:225
+msgid "Digit"
+msgstr "Cifră"
+
+#: src/hdy-keypad-button.c:226
+msgid "The keypad digit of the button"
+msgstr "Cifra tastaturii butonului"
+
+#: src/hdy-keypad-button.c:232
+msgid "Symbols"
+msgstr "Simboluri"
+
+#: src/hdy-keypad-button.c:233
+msgid "The keypad symbols of the button. The first symbol is used as the digit"
+msgstr "Simbolurile tastaturii butonului. Primul simbol este utilizat ca cifra"
+
+#: src/hdy-keypad-button.c:239 src/hdy-keypad.c:278
+msgid "Show Symbols"
+msgstr "Arată simbolurile"
+
+#: src/hdy-keypad-button.c:240 src/hdy-keypad.c:279
+msgid "Whether the second line of symbols should be shown or not"
+msgstr "Dacă a doua linie de simboluri ar trebui să fie arătată sau nu"
+
+#: src/hdy-keypad.c:264
+msgid "Row spacing"
+msgstr "Spațiere rânduri"
+
+#: src/hdy-keypad.c:265
+msgid "The amount of space between two consecutive rows"
+msgstr "Cantitatea de spațiu între două rânduri consecutive"
+
+#: src/hdy-keypad.c:271
+msgid "Column spacing"
+msgstr "Spațiere coloane"
+
+#: src/hdy-keypad.c:272
+msgid "The amount of space between two consecutive columns"
+msgstr "Cantitatea de spațiu între două coloane consecutive"
+
+#: src/hdy-keypad.c:285
+msgid "Only Digits"
+msgstr "Doar cifre"
+
+#: src/hdy-keypad.c:286
+msgid ""
+"Whether the keypad should show only digits or also extra buttons for #, *"
+msgstr ""
+"Dacă tastatura ar trebui să arate doar cifre sau de asemenea butoane extra "
+"pentru #, *"
+
+#: src/hdy-keypad.c:292
+msgid "Entry widget"
+msgstr "Widget de intrare"
+
+#: src/hdy-keypad.c:293
+msgid "The entry widget connected to the keypad"
+msgstr "Widget-ul de intrare conectat la tastatură"
+
+#: src/hdy-keypad.c:299
+msgid "Right action widget"
+msgstr "Widget de acțiune dreapta"
+
+#: src/hdy-keypad.c:300
+msgid "The right action widget"
+msgstr "Widget-ul de acțiune dreapta"
+
+#: src/hdy-keypad.c:306
+msgid "Left action widget"
+msgstr "Widget de acțiune stânga"
+
+#: src/hdy-keypad.c:307
+msgid "The left action widget"
+msgstr "Widget-ul de acțiune stânga"
+
+#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2946
+msgid "Folded"
+msgstr "Pliat"
+
+#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2947
+msgid "Whether the widget is folded"
+msgstr "Dacă widget-ul este pliat"
+
+#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2958
+msgid "Horizontally homogeneous folded"
+msgstr "Pliat omogen orizontal"
+
+#: src/hdy-leaflet.c:976
+msgid "Horizontally homogeneous sizing when the leaflet is folded"
+msgstr "Dimensionare omogenă orizontală când manifestul este pliat"
+
+#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2970
+msgid "Vertically homogeneous folded"
+msgstr "Pliat omogen vertical"
+
+#: src/hdy-leaflet.c:988
+msgid "Vertically homogeneous sizing when the leaflet is folded"
+msgstr "Dimensionare omogenă verticală când manifestul este pliat"
+
+#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2982
+msgid "Box horizontally homogeneous"
+msgstr "Omogen orizontal cutie"
+
+#: src/hdy-leaflet.c:1000
+msgid "Horizontally homogeneous sizing when the leaflet is unfolded"
+msgstr "Dimensionare omogenă orizontală când manifestul nu este pliat"
+
+#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2994
+msgid "Box vertically homogeneous"
+msgstr "Omogen vertical cutie"
+
+#: src/hdy-leaflet.c:1012
+msgid "Vertically homogeneous sizing when the leaflet is unfolded"
+msgstr "Dimensionare omogenă verticală când manifestul nu este pliat"
+
+#: src/hdy-leaflet.c:1019
+msgid "The widget currently visible when the leaflet is folded"
+msgstr "Widget-ul vizibil curent când manifestul este pliat"
+
+#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3009
+msgid "The name of the widget currently visible when the children are stacked"
+msgstr "Numele widget-ului vizibil curent când copiii sunt stivuiți"
+
+#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3028
+msgid "The type of animation used to transition between modes and children"
+msgstr ""
+"Tipul de animație utilizat pentru a face tranziție între moduri și copii"
+
+#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3034
+msgid "Mode transition duration"
+msgstr "Durata tranziției modului"
+
+#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3035
+msgid "The mode transition animation duration, in milliseconds"
+msgstr "Durata animației tranziției modului, în milisecunde"
+
+#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3041
+msgid "Child transition duration"
+msgstr "Durata tranziției copilului"
+
+#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3042
+msgid "The child transition animation duration, in milliseconds"
+msgstr "Durata animației tranziției copilului, în milisecunde"
+
+#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3048
+msgid "Child transition running"
+msgstr "Rularea tranziției copilului"
+
+#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3049
+msgid "Whether or not the child transition is currently running"
+msgstr "Dacă rulează sau nu în mod curent tranziția copilului"
+
+#: src/hdy-leaflet.c:1128
+msgid "Allow visible"
+msgstr "Permite vizibil"
+
+#: src/hdy-leaflet.c:1129
+msgid "Whether the child can be visible in folded mode"
+msgstr "Dacă copilul poate fi vizibil în modul pliat"
+
+#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252
+msgid "Description"
+msgstr "Descriere"
+
+#: src/hdy-preferences-row.c:116
+msgid "The title of the preference"
+msgstr "Titlul preferinței"
+
+#: src/hdy-preferences-window.c:135
+msgid "Untitled page"
+msgstr "Pagină fără titlu"
+
+#: src/hdy-preferences-window.c:438
+msgid "Search enabled"
+msgstr "Căutarea activată"
+
+#: src/hdy-preferences-window.c:439
+msgid "Whether search is enabled"
+msgstr "Dacă căutarea este activată"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Preferințe"
+
+#: src/hdy-preferences-window.ui:72
+msgid "Search"
+msgstr "Caută"
+
+#: src/hdy-preferences-window.ui:195
+msgid "No Results Found"
+msgstr "Nu s-au găsit rezultate"
+
+#: src/hdy-preferences-window.ui:210
+msgid "Try a different search"
+msgstr "Încercați o căutare diferită"
+
+#: src/hdy-search-bar.c:451
+msgid "Search Mode Enabled"
+msgstr "Modul de căutare activat"
+
+#: src/hdy-search-bar.c:452
+msgid "Whether the search mode is on and the search bar shown"
+msgstr "Dacă modul de căutare este activ și bara de căutare este arătată"
+
+#: src/hdy-search-bar.c:463
+msgid "Show Close Button"
+msgstr "Arată butonul de închidere"
+
+#: src/hdy-search-bar.c:464
+msgid "Whether to show the close button in the toolbar"
+msgstr "Dacă să se afișeze butonul de închidere în bara cu unelte"
+
+#: src/hdy-shadow-helper.c:254
+msgid "Widget"
+msgstr "Widget"
+
+#: src/hdy-shadow-helper.c:255
+msgid "The widget the shadow will be drawn for"
+msgstr "Widget-ul pentru care va fi desenată umbra"
+
+#: src/hdy-squeezer.c:1066
+msgid "Homogeneous"
+msgstr "Omogen"
+
+#: src/hdy-squeezer.c:1067
+msgid "Homogeneous sizing"
+msgstr "Dimensionare omogenă"
+
+#: src/hdy-squeezer.c:1074
+msgid "The widget currently visible in the squeezer"
+msgstr "Widget-ul vizibil curent în storcător"
+
+#: src/hdy-squeezer.c:1088
+msgid "The type of animation used to transition"
+msgstr "Tipul de animație folosit la tranziție"
+
+#: src/hdy-squeezer.c:1111 src/hdy-swipe-tracker.c:772
+msgid "Enabled"
+msgstr "Activat"
+
+#: src/hdy-squeezer.c:1112
+msgid ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+msgstr ""
+"Dacă copilul poate fi ales sau ar trebui să fie ignorat când se caută pentru "
+"copilul care se potrivește cel mai bine cu dimensiunea disponibilă"
+
+#: src/hdy-stackable-box.c:2959
+msgid "Horizontally homogeneous sizing when the widget is folded"
+msgstr "Dimensiune omogenă orizontală când widget-ul este pliat"
+
+#: src/hdy-stackable-box.c:2971
+msgid "Vertically homogeneous sizing when the widget is folded"
+msgstr "Dimensiune omogenă verticală când widget-ul este pliat"
+
+#: src/hdy-stackable-box.c:2983
+msgid "Horizontally homogeneous sizing when the widget is unfolded"
+msgstr "Dimensionare omogenă orizontală când widget-ul nu este pliat"
+
+#: src/hdy-stackable-box.c:2995
+msgid "Vertically homogeneous sizing when the widget is unfolded"
+msgstr "Dimensiune omogenă verticală când widget-ul nu este pliat"
+
+#: src/hdy-stackable-box.c:3002
+msgid "The widget currently visible when the widget is folded"
+msgstr "Widget-ul vizibil curent când widget-ul este pliat"
+
+#: src/hdy-stackable-box.c:3092 src/hdy-stackable-box.c:3093
+msgid "Orientation"
+msgstr "Orientare"
+
+#: src/hdy-swipe-tracker.c:757
+msgid "Swipeable"
+msgstr "Glisabil"
+
+#: src/hdy-swipe-tracker.c:758
+msgid "The swipeable the swipe tracker is attached to"
+msgstr "Glisabilul la care este atașat urmăritorul de glisare"
+
+#: src/hdy-swipe-tracker.c:773
+msgid "Whether the swipe tracker processes events"
+msgstr "Dacă urmăritorul de glisare procesează evenimente"
+
+#: src/hdy-swipe-tracker.c:787
+msgid "Reversed"
+msgstr "Inversat"
+
+#: src/hdy-swipe-tracker.c:788
+msgid "Whether swipe direction is reversed"
+msgstr "Dacă direcția de glisare este inversată"
+
+#: src/hdy-title-bar.c:308
+msgid "Selection mode"
+msgstr "Mod de selecție"
+
+#: src/hdy-title-bar.c:309
+msgid "Whether or not the title bar is in selection mode"
+msgstr "Dacă bara de titlu se află sau nu în modul de selecție"
+
+#: src/hdy-value-object.c:191
+msgctxt "HdyValueObjectClass"
+msgid "Value"
+msgstr "Valoare"
+
+#: src/hdy-value-object.c:192
+msgctxt "HdyValueObjectClass"
+msgid "The contained value"
+msgstr "Valoarea conținută"
+
+#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511
+#: src/hdy-view-switcher-title.c:237
+msgid "Policy"
+msgstr "Politică"
+
+#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512
+#: src/hdy-view-switcher-title.c:238
+msgid "The policy to determine the mode to use"
+msgstr "Politica de determinare a modului de utilizat"
+
+#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217
+#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252
+msgid "Icon Size"
+msgstr "Dimensiunea iconiței"
+
+#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218
+#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253
+msgid "Symbolic size to use for named icon"
+msgstr "Dimensiunea simbolică de utilizat pentru iconița numită"
+
+#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216
+#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562
+#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267
+msgid "Stack"
+msgstr "Stivă"
+
+#: src/hdy-view-switcher-bar.c:229
+msgid "Reveal"
+msgstr "Dezvăluie"
+
+#: src/hdy-view-switcher-bar.c:230
+msgid "Whether the view switcher is revealed"
+msgstr "Dacă comutatorul de vizualizare este dezvăluit"
+
+#: src/hdy-view-switcher-button.c:203
+msgid "Icon Name"
+msgstr "Nume iconiță"
+
+#: src/hdy-view-switcher-button.c:204
+msgid "Icon name for image"
+msgstr "Numele iconiței pentru imagine"
+
+#: src/hdy-view-switcher-button.c:234
+msgid "Needs attention"
+msgstr "Are nevoie de atenție"
+
+#: src/hdy-view-switcher-button.c:235
+msgid "Hint the view needs attention"
+msgstr "Indiciu cum că vizualizarea are nevoie de atenție"
+
+#: src/hdy-view-switcher.c:546
+msgid "Narrow ellipsize"
+msgstr "Transformare în elipsă îngustă"
+
+#: src/hdy-view-switcher.c:547
+msgid ""
+"The preferred place to ellipsize the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+msgstr ""
+"Locul preferat pentru a transforma în elipsă șirul, dacă eticheta modului "
+"îngust nu are destul spațiu pentru a afișa întregul șir"
+
+#: src/hdy-view-switcher-title.c:308
+msgid "View switcher enabled"
+msgstr "Comutator de vizualizare activat"
+
+#: src/hdy-view-switcher-title.c:309
+msgid "Whether the view switcher is enabled"
+msgstr "Dacă comutatorul de vizualizare este activat"
+
+#: src/hdy-view-switcher-title.c:322
+msgid "Title visible"
+msgstr "Titlu vizibil"
+
+#: src/hdy-view-switcher-title.c:323
+msgid "Whether the title label is visible"
+msgstr "Dacă eticheta titlului este vizibilă"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Mută"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Redimensionează"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Întotdeauna deasupra"
diff --git a/subprojects/libhandy/po/uk.po b/subprojects/libhandy/po/uk.po
new file mode 100644
index 0000000..b92f214
--- /dev/null
+++ b/subprojects/libhandy/po/uk.po
@@ -0,0 +1,921 @@
+# Ukrainian translation for libhandy.
+# Copyright (C) 2020 libhandy's COPYRIGHT HOLDER
+# This file is distributed under the same license as the libhandy package.
+#
+# Yuri Chornoivan <yurchor@ukr.net>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: libhandy master\n"
+"Report-Msgid-Bugs-To: https://gitlab.gnome.org/GNOME/libhandy/issues\n"
+"POT-Creation-Date: 2020-07-29 16:22+0000\n"
+"PO-Revision-Date: 2020-07-30 13:02+0300\n"
+"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
+"Language-Team: Ukrainian <trans-uk@lists.fedoraproject.org>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=4; plural=n==1 ? 3 : n%10==1 && n%100!=11 ? 0 : n"
+"%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Lokalize 20.03.70\n"
+
+#: glade/glade-hdy-carousel.c:21
+msgid "This property does not apply unless Show Indicators is set."
+msgstr ""
+"Цю властивість не буде застосовано, доки не встановлено «Показувати "
+"індикатори»."
+
+#: glade/glade-hdy-carousel.c:167 glade/glade-hdy-header-bar.c:118
+#: glade/glade-hdy-leaflet.c:184
+#, c-format
+msgid "Insert placeholder to %s"
+msgstr "Вставка заповнювача місця до %s"
+
+#: glade/glade-hdy-carousel.c:196 glade/glade-hdy-header-bar.c:144
+#: glade/glade-hdy-leaflet.c:214
+#, c-format
+msgid "Remove placeholder from %s"
+msgstr "Вилучення заповнювача з %s"
+
+#: glade/glade-hdy-header-bar.c:18
+msgid "This property does not apply when a custom title is set"
+msgstr "Ця властивість не застосовується, якщо встановлено нетиповий заголовок"
+
+#: glade/glade-hdy-header-bar.c:289
+msgid ""
+"The decoration layout does not apply to header bars which do no show window "
+"controls"
+msgstr ""
+"Компонування декорацій не стосується смужок заголовків, на яких не буде "
+"показано засоби керування вікном"
+
+#: glade/glade-hdy-leaflet.c:19
+msgid "This property only applies when the leaflet is folded"
+msgstr "Ця властивість застосовується, лише якщо листівку згорнуто"
+
+#: glade/glade-hdy-preferences-page.c:160
+#, c-format
+msgid "Add group to %s"
+msgstr "Додати групу до %s"
+
+#: glade/glade-hdy-preferences-window.c:228
+#, c-format
+msgid "Add page to %s"
+msgstr "Додати сторінку до %s"
+
+#: glade/glade-hdy-search-bar.c:101
+msgid "Search bar is already full"
+msgstr "Панель пошуку вже заповнено"
+
+#: glade/glade-hdy-utils.h:14
+#, c-format
+msgid "Only objects of type %s can be added to objects of type %s."
+msgstr "Лише об'єкти типу %s можна додавати до об'єктів типу %s."
+
+#: src/hdy-action-row.c:361 src/hdy-action-row.c:362 src/hdy-expander-row.c:335
+#: src/hdy-expander-row.c:336 src/hdy-preferences-page.c:179
+#: src/hdy-preferences-page.c:180
+msgid "Icon name"
+msgstr "Назва піктограми"
+
+#: src/hdy-action-row.c:375
+msgid "Activatable widget"
+msgstr "Активований віджет"
+
+#: src/hdy-action-row.c:376
+msgid "The widget to be activated when the row is activated"
+msgstr "Віджет, який буде активовано, якщо активовано рядок"
+
+#: src/hdy-action-row.c:389 src/hdy-action-row.c:390 src/hdy-expander-row.c:306
+#: src/hdy-header-bar.c:2147 src/hdy-view-switcher-title.c:294
+msgid "Subtitle"
+msgstr "Підзаголовок"
+
+#: src/hdy-action-row.c:403 src/hdy-action-row.c:404 src/hdy-expander-row.c:292
+#: src/hdy-header-bar.c:2140 src/hdy-preferences-group.c:265
+#: src/hdy-preferences-group.c:266 src/hdy-preferences-page.c:193
+#: src/hdy-preferences-page.c:194 src/hdy-preferences-row.c:115
+#: src/hdy-view-switcher-title.c:280
+msgid "Title"
+msgstr "Заголовок"
+
+#: src/hdy-action-row.c:418 src/hdy-expander-row.c:321
+#: src/hdy-preferences-row.c:130
+msgid "Use underline"
+msgstr "Використовувати підкреслення"
+
+#: src/hdy-action-row.c:419 src/hdy-expander-row.c:322
+#: src/hdy-preferences-row.c:131
+msgid ""
+"If set, an underline in the text indicates the next character should be used "
+"for the mnemonic accelerator key"
+msgstr ""
+"Якщо встановлено, то підкреслення в тексті означає, що наступний символ має "
+"використовуватися в комбінації клавіш."
+
+#: src/hdy-carousel-box.c:1095 src/hdy-carousel-box.c:1096
+#: src/hdy-carousel.c:947 src/hdy-carousel.c:948
+msgid "Number of pages"
+msgstr "Кількість сторінок"
+
+#: src/hdy-carousel-box.c:1111 src/hdy-carousel.c:964 src/hdy-header-bar.c:2133
+msgid "Position"
+msgstr "Позиція"
+
+#: src/hdy-carousel-box.c:1112 src/hdy-carousel.c:965
+msgid "Current scrolling position"
+msgstr "Поточна позиція гортання"
+
+#: src/hdy-carousel-box.c:1127 src/hdy-carousel.c:1046
+#: src/hdy-header-bar.c:2161
+msgid "Spacing"
+msgstr "Інтервал"
+
+#: src/hdy-carousel-box.c:1128 src/hdy-carousel.c:1047
+msgid "Spacing between pages"
+msgstr "Інтервал між сторінками"
+
+#: src/hdy-carousel-box.c:1144 src/hdy-carousel.c:1091
+msgid "Reveal duration"
+msgstr "Тривалість появи"
+
+#: src/hdy-carousel-box.c:1145 src/hdy-carousel.c:1092
+msgid "Page reveal duration"
+msgstr "Тривалість появи сторінки"
+
+#: src/hdy-carousel.c:981
+msgid "Interactive"
+msgstr "Інтерактивний"
+
+#: src/hdy-carousel.c:982
+msgid "Whether the widget can be swiped"
+msgstr "Визначає, чи можна змахнути віджет"
+
+#: src/hdy-carousel.c:997
+msgid "Indicator style"
+msgstr "Стиль індикаторів"
+
+#: src/hdy-carousel.c:998
+msgid "Page indicator style"
+msgstr "Стиль індикаторів сторінки"
+
+#: src/hdy-carousel.c:1013
+msgid "Indicator spacing"
+msgstr "Інтервал індикатора"
+
+#: src/hdy-carousel.c:1014
+msgid "Spacing between content and indicators"
+msgstr "Інтервал між вмістом та індикаторами"
+
+#: src/hdy-carousel.c:1032
+msgid "Center content"
+msgstr "Центрувати вміст"
+
+#: src/hdy-carousel.c:1033
+msgid "Whether to center pages to compensate for indicators"
+msgstr "Визначає, чи слід центрувати сторінки для компенсації індикаторів"
+
+#: src/hdy-carousel.c:1062
+msgid "Animation duration"
+msgstr "Тривалість анімації"
+
+#: src/hdy-carousel.c:1063
+msgid "Default animation duration"
+msgstr "Типова тривалість анімації"
+
+#: src/hdy-carousel.c:1077 src/hdy-swipe-tracker.c:802
+msgid "Allow mouse drag"
+msgstr "Дозволити перетягування мишею"
+
+#: src/hdy-carousel.c:1078 src/hdy-swipe-tracker.c:803
+msgid "Whether to allow dragging with mouse pointer"
+msgstr "Визначає, чи можна перетягувати за допомогою вказівника миші"
+
+#: src/hdy-clamp.c:417
+msgid "Maximum size"
+msgstr "Максимальний розмір"
+
+#: src/hdy-clamp.c:418
+msgid "The maximum size allocated to the child"
+msgstr "Максимальний розмір, який буде отримано для дочірнього об'єкта"
+
+#: src/hdy-clamp.c:442
+msgid "Tightening threshold"
+msgstr "Поріг ущільнення"
+
+#: src/hdy-clamp.c:443
+msgid "The size from which the clamp will tighten its grip on the child"
+msgstr "Розмір, починаючи з якого защіпка починає ущільнювати дочірній об'єкт"
+
+#: src/hdy-combo-row.c:411
+msgid "Selected index"
+msgstr "Індекс позначеного"
+
+#: src/hdy-combo-row.c:412
+msgid "The index of the selected item"
+msgstr "Індекс позначеного об'єкта"
+
+#: src/hdy-combo-row.c:430
+msgid "Use subtitle"
+msgstr "Використовувати підзаголовок"
+
+#: src/hdy-combo-row.c:431
+msgid "Set the current value as the subtitle"
+msgstr "Встановити поточне значення як підзаголовок"
+
+#: src/hdy-deck.c:888
+msgid "Horizontally homogeneous"
+msgstr "Горизонтально однорідний"
+
+#: src/hdy-deck.c:889
+msgid "Horizontally homogeneous sizing"
+msgstr "Однорідний за розміром горизонтально"
+
+#: src/hdy-deck.c:902
+msgid "Vertically homogeneous"
+msgstr "Вертикально однорідний"
+
+#: src/hdy-deck.c:903
+msgid "Vertically homogeneous sizing"
+msgstr "Однорідний за розміром вертикально"
+
+#: src/hdy-deck.c:916 src/hdy-leaflet.c:1018 src/hdy-squeezer.c:1093
+#: src/hdy-stackable-box.c:2993
+msgid "Visible child"
+msgstr "Видимий дочірній елемент"
+
+#: src/hdy-deck.c:917
+msgid "The widget currently visible"
+msgstr "Віджет є видимим"
+
+#: src/hdy-deck.c:930 src/hdy-leaflet.c:1025 src/hdy-stackable-box.c:3000
+msgid "Name of visible child"
+msgstr "Назва видимого дочірнього елемента"
+
+#: src/hdy-deck.c:931
+msgid "The name of the widget currently visible"
+msgstr "Назва віджета є видимою"
+
+#: src/hdy-deck.c:949 src/hdy-leaflet.c:1044 src/hdy-squeezer.c:1107
+#: src/hdy-stackable-box.c:3019
+msgid "Transition type"
+msgstr "Тип переходу"
+
+#: src/hdy-deck.c:950
+msgid "The type of animation used to transition between children"
+msgstr ""
+"Тип анімації, який буде використано для переходу між дочірніми об'єктами"
+
+#: src/hdy-deck.c:963 src/hdy-header-bar.c:2243 src/hdy-squeezer.c:1100
+msgid "Transition duration"
+msgstr "Тривалість переходу"
+
+#: src/hdy-deck.c:964
+msgid "The transition animation duration, in milliseconds"
+msgstr "Тривалість анімації переходу у мілісекундах"
+
+#: src/hdy-deck.c:977 src/hdy-header-bar.c:2250 src/hdy-squeezer.c:1115
+msgid "Transition running"
+msgstr "Виконання переходу"
+
+#: src/hdy-deck.c:978 src/hdy-header-bar.c:2251 src/hdy-squeezer.c:1116
+msgid "Whether or not the transition is currently running"
+msgstr "Чи виконується перехід цієї миті"
+
+#: src/hdy-deck.c:992 src/hdy-header-bar.c:2257 src/hdy-leaflet.c:1072
+#: src/hdy-squeezer.c:1122 src/hdy-stackable-box.c:3047
+msgid "Interpolate size"
+msgstr "Інтерполювати розмір"
+
+#: src/hdy-deck.c:993 src/hdy-header-bar.c:2258 src/hdy-leaflet.c:1073
+#: src/hdy-squeezer.c:1123 src/hdy-stackable-box.c:3048
+msgid ""
+"Whether or not the size should smoothly change when changing between "
+"differently sized children"
+msgstr ""
+"Визначає, чи повинен розмір змінюватися плавно при перемиканні між дочірніми "
+"елементами різних розмірів"
+
+#: src/hdy-deck.c:1007 src/hdy-leaflet.c:1087 src/hdy-preferences-window.c:497
+#: src/hdy-stackable-box.c:3062
+msgid "Can swipe back"
+msgstr "Можна змахнути назад"
+
+#: src/hdy-deck.c:1008 src/hdy-leaflet.c:1088 src/hdy-stackable-box.c:3063
+msgid ""
+"Whether or not swipe gesture can be used to switch to the previous child"
+msgstr ""
+"Визначає, чи можна скористатися жестом змахування для перемикання до "
+"попереднього дочірнього об'єкта"
+
+#: src/hdy-deck.c:1022 src/hdy-leaflet.c:1102 src/hdy-stackable-box.c:3077
+msgid "Can swipe forward"
+msgstr "Можна змахувати вперед"
+
+#: src/hdy-deck.c:1023 src/hdy-leaflet.c:1103 src/hdy-stackable-box.c:3078
+msgid "Whether or not swipe gesture can be used to switch to the next child"
+msgstr ""
+"Визначає, чи можна скористатися жестом змахування для перемикання до "
+"наступного дочірнього об'єкта"
+
+#: src/hdy-deck.c:1031 src/hdy-leaflet.c:1111
+msgid "Name"
+msgstr "Назва"
+
+#: src/hdy-deck.c:1032 src/hdy-leaflet.c:1112
+msgid "The name of the child page"
+msgstr "Назва дочірньої сторінки"
+
+#: src/hdy-expander-row.c:293
+msgid "The title for this row"
+msgstr "Заголовок цього рядка"
+
+#: src/hdy-expander-row.c:307
+msgid "The subtitle for this row"
+msgstr "Підзаголовок цього рядка"
+
+#: src/hdy-expander-row.c:347
+msgid "Expanded"
+msgstr "Розгорнутий"
+
+#: src/hdy-expander-row.c:348
+msgid "Whether the row is expanded"
+msgstr "Чи є рядок розгорнутим"
+
+#: src/hdy-expander-row.c:359
+msgid "Enable expansion"
+msgstr "Увімкнути розгортання"
+
+#: src/hdy-expander-row.c:360
+msgid "Whether the expansion is enabled"
+msgstr "Визначає, чи увімкнено розгортання"
+
+#: src/hdy-expander-row.c:371
+msgid "Show enable switch"
+msgstr "Показувати перемикач вмикання"
+
+#: src/hdy-expander-row.c:372
+msgid "Whether the switch enabling the expansion is visible"
+msgstr "Визначає, чи є видимим перемикач, який вмикає розгортання"
+
+#: src/hdy-header-bar.c:484
+msgid "Application menu"
+msgstr "Меню програм"
+
+#: src/hdy-header-bar.c:506 src/hdy-window-handle-controller.c:275
+msgid "Minimize"
+msgstr "Мінімізувати"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:241
+msgid "Restore"
+msgstr "Відновити"
+
+#: src/hdy-header-bar.c:528 src/hdy-window-handle-controller.c:284
+msgid "Maximize"
+msgstr "Максимізувати"
+
+#: src/hdy-header-bar.c:546 src/hdy-window-handle-controller.c:311
+msgid "Close"
+msgstr "Закрити"
+
+#: src/hdy-header-bar.c:562
+msgid "Back"
+msgstr "Назад"
+
+#: src/hdy-header-bar.c:2126
+msgid "Pack type"
+msgstr "Тип упаковки"
+
+#: src/hdy-header-bar.c:2127
+msgid ""
+"A GtkPackType indicating whether the child is packed with reference to the "
+"start or end of the parent"
+msgstr ""
+"Об'єкт GtkPackType, що визначає відносно чого упаковується вкладений об'єкт "
+"-- відносно початку, кінця, чи батьківського об'єкта"
+
+#: src/hdy-header-bar.c:2134
+msgid "The index of the child in the parent"
+msgstr "Індекс вкладеного елемента у батьківському"
+
+#: src/hdy-header-bar.c:2141 src/hdy-view-switcher-title.c:281
+msgid "The title to display"
+msgstr "Заголовок для показу"
+
+#: src/hdy-header-bar.c:2148 src/hdy-view-switcher-title.c:295
+msgid "The subtitle to display"
+msgstr "Підзаголовок для показу"
+
+#: src/hdy-header-bar.c:2154
+msgid "Custom Title"
+msgstr "Нетиповий заголовок"
+
+#: src/hdy-header-bar.c:2155
+msgid "Custom title widget to display"
+msgstr "Віджет для показу нетипового заголовка"
+
+#: src/hdy-header-bar.c:2162
+msgid "The amount of space between children"
+msgstr "Відстані між вкладеними елементами"
+
+#: src/hdy-header-bar.c:2181
+msgid "Show decorations"
+msgstr "Показувати оформлення"
+
+#: src/hdy-header-bar.c:2182
+msgid "Whether to show window decorations"
+msgstr "Чи показувати оформлення вікна"
+
+#: src/hdy-header-bar.c:2200
+msgid "Decoration Layout"
+msgstr "Компонування оформлення"
+
+#: src/hdy-header-bar.c:2201
+msgid "The layout for window decorations"
+msgstr "Компонування оформлення вікон"
+
+#: src/hdy-header-bar.c:2214
+msgid "Decoration Layout Set"
+msgstr "Компонування оформлення встановлено"
+
+#: src/hdy-header-bar.c:2215
+msgid "Whether the decoration-layout property has been set"
+msgstr "Чи було встановлено властивість decoration-layout"
+
+#: src/hdy-header-bar.c:2229
+msgid "Has Subtitle"
+msgstr "Має підзаголовок"
+
+#: src/hdy-header-bar.c:2230
+msgid "Whether to reserve space for a subtitle"
+msgstr "Чи резервувати місце для підзаголовка"
+
+#: src/hdy-header-bar.c:2236
+msgid "Centering policy"
+msgstr "Правила центрування"
+
+#: src/hdy-header-bar.c:2237
+msgid "The policy to horizontally align the center widget"
+msgstr "Правила для горизонтального вирівнювання центрального віджета"
+
+#: src/hdy-header-bar.c:2244 src/hdy-squeezer.c:1101
+msgid "The animation duration, in milliseconds"
+msgstr "Тривалість анімації у мілісекундах"
+
+#: src/hdy-header-group.c:827
+msgid "Decorate all"
+msgstr "Декорувати усе"
+
+#: src/hdy-header-group.c:828
+msgid ""
+"Whether the elements of the group should all receive the full decoration"
+msgstr "Чи мають елементи групи усі отримувати повну декорацію"
+
+#: src/hdy-keypad-button.c:225
+msgid "Digit"
+msgstr "Цифра"
+
+#: src/hdy-keypad-button.c:226
+msgid "The keypad digit of the button"
+msgstr "Цифра цифрової панелі для кнопки"
+
+#: src/hdy-keypad-button.c:232
+msgid "Symbols"
+msgstr "Символи"
+
+#: src/hdy-keypad-button.c:233
+msgid "The keypad symbols of the button. The first symbol is used as the digit"
+msgstr ""
+"Символи цифрової панелі на кнопці. Перший символ буде використано як цифру"
+
+#: src/hdy-keypad-button.c:239
+msgid "Show symbols"
+msgstr "Показувати символи"
+
+#: src/hdy-keypad-button.c:240
+msgid "Whether the second line of symbols should be shown or not"
+msgstr "Визначає, чи має бути показано другий рядок символів"
+
+#: src/hdy-keypad.c:247
+msgid "Row spacing"
+msgstr "Міжрядковий інтервал"
+
+#: src/hdy-keypad.c:248
+msgid "The amount of space between two consecutive rows"
+msgstr "Інтервал між двома послідовними рядками"
+
+#: src/hdy-keypad.c:261
+msgid "Column spacing"
+msgstr "Інтервал між стовпчиками"
+
+#: src/hdy-keypad.c:262
+msgid "The amount of space between two consecutive columns"
+msgstr "Інтервал між двома послідовними стовпчиками"
+
+#: src/hdy-keypad.c:276
+msgid "Letters visible"
+msgstr "Видимі літери"
+
+#: src/hdy-keypad.c:277
+msgid "Whether the letters below the digits should be visible"
+msgstr "Визначає, чи будуть видимими літери під цифрами"
+
+#: src/hdy-keypad.c:291
+msgid "Symbols visible"
+msgstr "Видимі символи"
+
+#: src/hdy-keypad.c:292
+msgid "Whether the hash, plus, and asterisk symbols should be visible"
+msgstr "Визначає, чи має бути показано символи решітки, плюса та зірочки"
+
+#: src/hdy-keypad.c:306
+msgid "Entry"
+msgstr "Введення"
+
+#: src/hdy-keypad.c:307
+msgid "The entry widget connected to the keypad"
+msgstr "Віджет введення, з'єднаний із цифровою панеллю"
+
+#: src/hdy-keypad.c:320
+msgid "End action"
+msgstr "Завершення дії"
+
+#: src/hdy-keypad.c:321
+msgid "The end action widget"
+msgstr "Віджет завершення дії"
+
+#: src/hdy-keypad.c:334
+msgid "Start action"
+msgstr "Початок дії"
+
+#: src/hdy-keypad.c:335
+msgid "The start action widget"
+msgstr "Віджет початку дії"
+
+#: src/hdy-leaflet.c:963 src/hdy-stackable-box.c:2938
+msgid "Folded"
+msgstr "Згорнуто"
+
+#: src/hdy-leaflet.c:964 src/hdy-stackable-box.c:2939
+msgid "Whether the widget is folded"
+msgstr "Визначає, чи згорнуто віджет"
+
+#: src/hdy-leaflet.c:975 src/hdy-stackable-box.c:2950
+msgid "Horizontally homogeneous folded"
+msgstr "Горизонтально однорідний згорнутий"
+
+#: src/hdy-leaflet.c:976
+msgid "Horizontally homogeneous sizing when the leaflet is folded"
+msgstr "Однорідний за розміром горизонтально, якщо листівку згорнуто"
+
+#: src/hdy-leaflet.c:987 src/hdy-stackable-box.c:2962
+msgid "Vertically homogeneous folded"
+msgstr "Вертикально однорідний згорнутий"
+
+#: src/hdy-leaflet.c:988
+msgid "Vertically homogeneous sizing when the leaflet is folded"
+msgstr "Однорідний за розміром вертикально, якщо листівку згорнуто"
+
+#: src/hdy-leaflet.c:999 src/hdy-stackable-box.c:2974
+msgid "Box horizontally homogeneous"
+msgstr "Панель горизонтально однорідна"
+
+#: src/hdy-leaflet.c:1000
+msgid "Horizontally homogeneous sizing when the leaflet is unfolded"
+msgstr "Однорідний за розміром горизонтально, якщо листівку розгорнуто"
+
+#: src/hdy-leaflet.c:1011 src/hdy-stackable-box.c:2986
+msgid "Box vertically homogeneous"
+msgstr "Панель вертикально однорідна"
+
+#: src/hdy-leaflet.c:1012
+msgid "Vertically homogeneous sizing when the leaflet is unfolded"
+msgstr "Однорідний за розміром вертикально, якщо листівку розгорнуто"
+
+#: src/hdy-leaflet.c:1019
+msgid "The widget currently visible when the leaflet is folded"
+msgstr "Віджет є видимим, коли листівку згорнуто"
+
+#: src/hdy-leaflet.c:1026 src/hdy-stackable-box.c:3001
+msgid "The name of the widget currently visible when the children are stacked"
+msgstr "Назва віджета є видимою, коли дочірні об'єкти розташовано у стосі"
+
+#: src/hdy-leaflet.c:1045 src/hdy-stackable-box.c:3020
+msgid "The type of animation used to transition between modes and children"
+msgstr ""
+"Тип анімації, який буде використано для переходу між режимами і дочірніми "
+"об'єктами"
+
+#: src/hdy-leaflet.c:1051 src/hdy-stackable-box.c:3026
+msgid "Mode transition duration"
+msgstr "Тривалість переходу між режимами"
+
+#: src/hdy-leaflet.c:1052 src/hdy-stackable-box.c:3027
+msgid "The mode transition animation duration, in milliseconds"
+msgstr "Тривалість анімації переходу між режимами у мілісекундах"
+
+#: src/hdy-leaflet.c:1058 src/hdy-stackable-box.c:3033
+msgid "Child transition duration"
+msgstr "Тривалість переходу між дочірніми об'єктами"
+
+#: src/hdy-leaflet.c:1059 src/hdy-stackable-box.c:3034
+msgid "The child transition animation duration, in milliseconds"
+msgstr "Тривалість анімації переходу між дочірніми об'єктами у мілісекундах"
+
+#: src/hdy-leaflet.c:1065 src/hdy-stackable-box.c:3040
+msgid "Child transition running"
+msgstr "Виконання переходу між дочірніми об'єктами"
+
+#: src/hdy-leaflet.c:1066 src/hdy-stackable-box.c:3041
+msgid "Whether or not the child transition is currently running"
+msgstr "Чи виконується перехід між дочірніми об'єктами цієї миті"
+
+#: src/hdy-leaflet.c:1129
+msgid "Navigatable"
+msgstr "Придатний до навігації"
+
+#: src/hdy-leaflet.c:1130
+msgid "Whether the child can be navigated to"
+msgstr "Визначає, чи можна переходити до дочірнього об'єкта"
+
+#: src/hdy-preferences-group.c:251 src/hdy-preferences-group.c:252
+msgid "Description"
+msgstr "Опис"
+
+#: src/hdy-preferences-row.c:116
+msgid "The title of the preference"
+msgstr "Заголовок налаштувань"
+
+#: src/hdy-preferences-window.c:141
+msgid "Untitled page"
+msgstr "Сторінка без назви"
+
+#: src/hdy-preferences-window.c:483
+msgid "Search enabled"
+msgstr "Пошук увімкнено"
+
+#: src/hdy-preferences-window.c:484
+msgid "Whether search is enabled"
+msgstr "Визначає, чи увімкнено пошук"
+
+#: src/hdy-preferences-window.c:498
+msgid ""
+"Whether or not swipe gesture can be used to switch from a subpage to the "
+"preferences"
+msgstr ""
+"Визначає, чи можна скористатися жестом змахування для перемикання з "
+"допоміжної сторінки до сторінки параметрів"
+
+#: src/hdy-preferences-window.ui:9
+msgid "Preferences"
+msgstr "Налаштування"
+
+#: src/hdy-preferences-window.ui:78
+msgid "Search"
+msgstr "Пошук"
+
+#: src/hdy-preferences-window.ui:201
+msgid "No Results Found"
+msgstr "Нічого не знайдено"
+
+#: src/hdy-preferences-window.ui:216
+msgid "Try a different search"
+msgstr "Спробувати інші критерії пошуку"
+
+#: src/hdy-search-bar.c:451
+msgid "Search Mode Enabled"
+msgstr "Режим пошуку увімкнено"
+
+#: src/hdy-search-bar.c:452
+msgid "Whether the search mode is on and the search bar shown"
+msgstr "Чи увімкнено режим пошуку й чи відкрита панель пошуку"
+
+#: src/hdy-search-bar.c:463
+msgid "Show Close Button"
+msgstr "Показувати кнопку закривання"
+
+#: src/hdy-search-bar.c:464
+msgid "Whether to show the close button in the toolbar"
+msgstr "Чи показувати кнопку закривання на панелі інструментів"
+
+#: src/hdy-shadow-helper.c:254
+msgid "Widget"
+msgstr "Віджет"
+
+#: src/hdy-shadow-helper.c:255
+msgid "The widget the shadow will be drawn for"
+msgstr "Віджет, для якого буде намальовано тінь"
+
+#: src/hdy-squeezer.c:1086
+msgid "Homogeneous"
+msgstr "Однорідний"
+
+#: src/hdy-squeezer.c:1087
+msgid "Homogeneous sizing"
+msgstr "Однорідний розмір"
+
+#: src/hdy-squeezer.c:1094
+msgid "The widget currently visible in the squeezer"
+msgstr "Віджет, який зараз показано у відтискачі"
+
+#: src/hdy-squeezer.c:1108
+msgid "The type of animation used to transition"
+msgstr "Тип анімації, яку буде використано для переходу"
+
+#: src/hdy-squeezer.c:1143
+msgid "X align"
+msgstr "Вирівнювання за X"
+
+#: src/hdy-squeezer.c:1144
+msgid "The horizontal alignment, from 0 (start) to 1 (end)"
+msgstr "Вирівнювання за горизонталлю, від 0 (початок) до 1 (кінець)"
+
+#: src/hdy-squeezer.c:1165
+msgid "Y align"
+msgstr "Вирівнювання за Y"
+
+#: src/hdy-squeezer.c:1166
+msgid "The vertical alignment, from 0 (top) to 1 (bottom)"
+msgstr "Вирівнювання за вертикаллю, від 0 (верх) до 1 (низ)"
+
+#: src/hdy-squeezer.c:1175 src/hdy-swipe-tracker.c:772
+msgid "Enabled"
+msgstr "Увімкнено"
+
+#: src/hdy-squeezer.c:1176
+msgid ""
+"Whether the child can be picked or should be ignored when looking for the "
+"child fitting the available size best"
+msgstr ""
+"Визначає, можна вибирати дочірній об'єкт чи його слід ігнорувати при пошуку "
+"найкращої відповідності розмірів дочірнього об'єкта"
+
+#: src/hdy-stackable-box.c:2951
+msgid "Horizontally homogeneous sizing when the widget is folded"
+msgstr "Однорідний за розміром горизонтально, якщо віджет згорнуто"
+
+#: src/hdy-stackable-box.c:2963
+msgid "Vertically homogeneous sizing when the widget is folded"
+msgstr "Однорідний за розміром вертикально, якщо віджет згорнуто"
+
+#: src/hdy-stackable-box.c:2975
+msgid "Horizontally homogeneous sizing when the widget is unfolded"
+msgstr "Однорідний за розміром горизонтально, якщо віджет розгорнуто"
+
+#: src/hdy-stackable-box.c:2987
+msgid "Vertically homogeneous sizing when the widget is unfolded"
+msgstr "Однорідний за розміром вертикально, якщо віджет розгорнуто"
+
+#: src/hdy-stackable-box.c:2994
+msgid "The widget currently visible when the widget is folded"
+msgstr "Віджет є видимим, коли віджет згорнуто"
+
+#: src/hdy-stackable-box.c:3084 src/hdy-stackable-box.c:3085
+msgid "Orientation"
+msgstr "Орієнтація"
+
+#: src/hdy-swipe-tracker.c:757
+msgid "Swipeable"
+msgstr "Змахуваний"
+
+#: src/hdy-swipe-tracker.c:758
+msgid "The swipeable the swipe tracker is attached to"
+msgstr "Змахуваний об'єкт, до якого долучено стеження за змахуванням"
+
+#: src/hdy-swipe-tracker.c:773
+msgid "Whether the swipe tracker processes events"
+msgstr "Визначає, чи стеження за змахуванням обробляє події"
+
+#: src/hdy-swipe-tracker.c:787
+msgid "Reversed"
+msgstr "Обернене"
+
+#: src/hdy-swipe-tracker.c:788
+msgid "Whether swipe direction is reversed"
+msgstr "Визначає, що напрям змахування є оберненим"
+
+#: src/hdy-title-bar.c:308
+msgid "Selection mode"
+msgstr "Режим вибирання"
+
+#: src/hdy-title-bar.c:309
+msgid "Whether or not the title bar is in selection mode"
+msgstr "Визначає, чи смужка заголовка перебуває у режимі вибору"
+
+#: src/hdy-value-object.c:191
+msgctxt "HdyValueObjectClass"
+msgid "Value"
+msgstr "Значення"
+
+#: src/hdy-value-object.c:192
+msgctxt "HdyValueObjectClass"
+msgid "The contained value"
+msgstr "Вміщене значення"
+
+#: src/hdy-view-switcher-bar.c:186 src/hdy-view-switcher.c:511
+#: src/hdy-view-switcher-title.c:237
+msgid "Policy"
+msgstr "Поведінка"
+
+#: src/hdy-view-switcher-bar.c:187 src/hdy-view-switcher.c:512
+#: src/hdy-view-switcher-title.c:238
+msgid "The policy to determine the mode to use"
+msgstr "Правила для визначення режиму, яким слід скористатися"
+
+#: src/hdy-view-switcher-bar.c:201 src/hdy-view-switcher-button.c:217
+#: src/hdy-view-switcher.c:526 src/hdy-view-switcher-title.c:252
+msgid "Icon Size"
+msgstr "Розмір піктограм"
+
+#: src/hdy-view-switcher-bar.c:202 src/hdy-view-switcher-button.c:218
+#: src/hdy-view-switcher.c:527 src/hdy-view-switcher-title.c:253
+msgid "Symbolic size to use for named icon"
+msgstr "Символьний розмір, використовуваний для іменованої піктограми"
+
+#: src/hdy-view-switcher-bar.c:215 src/hdy-view-switcher-bar.c:216
+#: src/hdy-view-switcher.c:561 src/hdy-view-switcher.c:562
+#: src/hdy-view-switcher-title.c:266 src/hdy-view-switcher-title.c:267
+msgid "Stack"
+msgstr "Стос"
+
+#: src/hdy-view-switcher-bar.c:229
+msgid "Reveal"
+msgstr "Показати"
+
+#: src/hdy-view-switcher-bar.c:230
+msgid "Whether the view switcher is revealed"
+msgstr "Визначає, чи показано перемикач перегляду"
+
+#: src/hdy-view-switcher-button.c:203
+msgid "Icon Name"
+msgstr "Назва значка"
+
+#: src/hdy-view-switcher-button.c:204
+msgid "Icon name for image"
+msgstr "Назва піктограми для зображення"
+
+#: src/hdy-view-switcher-button.c:234
+msgid "Needs attention"
+msgstr "Потребує уваги"
+
+#: src/hdy-view-switcher-button.c:235
+msgid "Hint the view needs attention"
+msgstr "Підказка про те, що перегляд потребує уваги"
+
+#: src/hdy-view-switcher.c:546
+msgid "Narrow ellipsize"
+msgstr "Багатокрапка при звуженні"
+
+#: src/hdy-view-switcher.c:547
+msgid ""
+"The preferred place to ellipsize the string, if the narrow mode label does "
+"not have enough room to display the entire string"
+msgstr ""
+"Бажане місце для обривання рядка багатокрапкою, якщо мітка у вузькому режимі "
+"не має достатньо місця для показу цілого рядка"
+
+#: src/hdy-view-switcher-title.c:308
+msgid "View switcher enabled"
+msgstr "Увімкнено перемикач перегляду"
+
+#: src/hdy-view-switcher-title.c:309
+msgid "Whether the view switcher is enabled"
+msgstr "Визначає, чи увімкнено перемикач перегляду"
+
+#: src/hdy-view-switcher-title.c:322
+msgid "Title visible"
+msgstr "Заголовок видимий"
+
+#: src/hdy-view-switcher-title.c:323
+msgid "Whether the title label is visible"
+msgstr "Визначає, чи видимою є мітка заголовка"
+
+#: src/hdy-window-handle-controller.c:259
+msgid "Move"
+msgstr "Пересунути"
+
+#: src/hdy-window-handle-controller.c:267
+msgid "Resize"
+msgstr "Змінити розмір"
+
+#: src/hdy-window-handle-controller.c:298
+msgid "Always on Top"
+msgstr "Завжди зверху"
+
+#~ msgid "Only Digits"
+#~ msgstr "Лише цифри"
+
+#~ msgid ""
+#~ "Whether the keypad should show only digits or also extra buttons for #, *"
+#~ msgstr ""
+#~ "Визначає, слід показувати на цифровій панелі лише цифри чи додаткові "
+#~ "кнопки #, *"
+
+#~ msgid "Entry widget"
+#~ msgstr "Віджет введення"
+
+#~ msgid "Right action widget"
+#~ msgstr "Віджет дії праворуч"
+
+#~ msgid "Left action widget"
+#~ msgstr "Віджет дії ліворуч"
diff --git a/subprojects/libhandy/run.in b/subprojects/libhandy/run.in
new file mode 100755
index 0000000..629897b
--- /dev/null
+++ b/subprojects/libhandy/run.in
@@ -0,0 +1,12 @@
+#!/bin/sh
+
+ABS_BUILDDIR='@ABS_BUILDDIR@'
+ABS_SRCDIR='@ABS_SRCDIR@'
+
+export GLADE_CATALOG_SEARCH_PATH="${ABS_SRCDIR}/glade/:${GLADE_CATALOG_SEARCH_PATH}"
+export GLADE_MODULE_SEARCH_PATH="${ABS_BUILDDIR}/glade:${GLADE_MODULE_SEARCH_PATH}"
+export GI_TYPELIB_PATH="${ABS_BUILDDIR}/src:$GI_TYPELIB_PATH"
+export LD_LIBRARY_PATH="${ABS_BUILDDIR}/src:${ABS_BUILDDIR}/glade:$LD_LIBRARY_PATH"
+export PKG_CONFIG_PATH="${ABS_BUILDDIR}/src:$PKG_CONFIG_PATH"
+
+exec "$@"
diff --git a/subprojects/libhandy/src/gen-public-types.sh b/subprojects/libhandy/src/gen-public-types.sh
new file mode 100644
index 0000000..036c336
--- /dev/null
+++ b/subprojects/libhandy/src/gen-public-types.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+set -e
+
+echo '/* This file was generated by gen-plublic-types.sh, do not edit it. */
+'
+
+for var in "$@"
+do
+ echo "#include \"$var\""
+done
+
+echo '#include "hdy-main-private.h"
+
+void
+hdy_init_public_types (void)
+{'
+
+sed -ne 's/^#define \{1,\}\(HDY_TYPE_[A-Z0-9_]\{1,\}\) \{1,\}.*/ g_type_ensure (\1);/p' "$@" | sort
+
+echo '}
+'
diff --git a/subprojects/libhandy/src/gtk-window-private.h b/subprojects/libhandy/src/gtk-window-private.h
new file mode 100644
index 0000000..e2f183e
--- /dev/null
+++ b/subprojects/libhandy/src/gtk-window-private.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void hdy_gtk_window_toggle_maximized (GtkWindow *window);
+GdkPixbuf *hdy_gtk_window_get_icon_for_size (GtkWindow *window,
+ gint size);
+GdkWindowState hdy_gtk_window_get_state (GtkWindow *window);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/gtk-window.c b/subprojects/libhandy/src/gtk-window.c
new file mode 100644
index 0000000..154acdb
--- /dev/null
+++ b/subprojects/libhandy/src/gtk-window.c
@@ -0,0 +1,169 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * 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 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
+ * file for a list of people on the GTK+ Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/* Bits taken from GTK 3.24 and tweaked to be used by libhandy. */
+
+#include "gtk-window-private.h"
+
+typedef struct
+{
+ GList *icon_list;
+ gchar *icon_name;
+ guint realized : 1;
+ guint using_default_icon : 1;
+ guint using_parent_icon : 1;
+ guint using_themed_icon : 1;
+} GtkWindowIconInfo;
+
+static GQuark quark_gtk_window_icon_info = 0;
+
+static void
+ensure_quarks (void)
+{
+ if (!quark_gtk_window_icon_info)
+ quark_gtk_window_icon_info = g_quark_from_static_string ("gtk-window-icon-info");
+}
+
+void
+hdy_gtk_window_toggle_maximized (GtkWindow *window)
+{
+ if (gtk_window_is_maximized (window))
+ gtk_window_unmaximize (window);
+ else
+ gtk_window_maximize (window);
+}
+
+static GtkWindowIconInfo*
+get_icon_info (GtkWindow *window)
+{
+ ensure_quarks ();
+
+ return g_object_get_qdata (G_OBJECT (window), quark_gtk_window_icon_info);
+}
+
+static void
+free_icon_info (GtkWindowIconInfo *info)
+{
+ g_free (info->icon_name);
+ g_slice_free (GtkWindowIconInfo, info);
+}
+
+static GtkWindowIconInfo*
+ensure_icon_info (GtkWindow *window)
+{
+ GtkWindowIconInfo *info;
+
+ ensure_quarks ();
+
+ info = get_icon_info (window);
+
+ if (info == NULL)
+ {
+ info = g_slice_new0 (GtkWindowIconInfo);
+ g_object_set_qdata_full (G_OBJECT (window),
+ quark_gtk_window_icon_info,
+ info,
+ (GDestroyNotify)free_icon_info);
+ }
+
+ return info;
+}
+
+static GdkPixbuf *
+icon_from_list (GList *list,
+ gint size)
+{
+ GdkPixbuf *best;
+ GdkPixbuf *pixbuf;
+ GList *l;
+
+ best = NULL;
+ for (l = list; l; l = l->next)
+ {
+ pixbuf = list->data;
+ if (gdk_pixbuf_get_width (pixbuf) <= size &&
+ gdk_pixbuf_get_height (pixbuf) <= size)
+ {
+ best = g_object_ref (pixbuf);
+ break;
+ }
+ }
+
+ if (best == NULL)
+ best = gdk_pixbuf_scale_simple (GDK_PIXBUF (list->data), size, size, GDK_INTERP_BILINEAR);
+
+ return best;
+}
+
+static GdkPixbuf *
+icon_from_name (const gchar *name,
+ gint size)
+{
+ return gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
+ name, size,
+ GTK_ICON_LOOKUP_FORCE_SIZE, NULL);
+}
+
+GdkPixbuf *
+hdy_gtk_window_get_icon_for_size (GtkWindow *window,
+ gint size)
+{
+ GtkWindowIconInfo *info;
+ const gchar *name;
+ g_autoptr (GList) default_icon_list = gtk_window_get_default_icon_list ();
+
+ info = ensure_icon_info (window);
+
+ if (info->icon_list != NULL)
+ return icon_from_list (info->icon_list, size);
+
+ name = gtk_window_get_icon_name (window);
+ if (name != NULL)
+ return icon_from_name (name, size);
+
+ if (gtk_window_get_transient_for (window) != NULL)
+ {
+ info = ensure_icon_info (gtk_window_get_transient_for (window));
+ if (info->icon_list)
+ return icon_from_list (info->icon_list, size);
+ }
+
+ if (default_icon_list != NULL)
+ return icon_from_list (default_icon_list, size);
+
+ if (gtk_window_get_default_icon_name () != NULL)
+ return icon_from_name (gtk_window_get_default_icon_name (), size);
+
+ return NULL;
+}
+
+GdkWindowState
+hdy_gtk_window_get_state (GtkWindow *window)
+{
+ GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (window));
+
+ return gdk_window ? gdk_window_get_state (gdk_window) : 0;
+}
diff --git a/subprojects/libhandy/src/gtkprogresstracker.c b/subprojects/libhandy/src/gtkprogresstracker.c
new file mode 100644
index 0000000..72d2013
--- /dev/null
+++ b/subprojects/libhandy/src/gtkprogresstracker.c
@@ -0,0 +1,248 @@
+/*
+ * Copyright © 2016 Endless Mobile Inc.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthew Watson <mattdangerw@gmail.com>
+ */
+
+#include "gtkprogresstrackerprivate.h"
+/* #include "gtkprivate.h" */
+/* #include "gtkcsseasevalueprivate.h" */
+
+#include <math.h>
+#include <string.h>
+
+#include "hdy-animation-private.h"
+
+/*
+ * Progress tracker is small helper for tracking progress through gtk
+ * animations. It's a simple zero-initable struct, meant to be thrown in a
+ * widget's private data without the need for setup or teardown.
+ *
+ * Progress tracker will handle translating frame clock timestamps to a
+ * fractional progress value for interpolating between animation targets.
+ *
+ * Progress tracker will use the GTK_SLOWDOWN environment variable to control
+ * the speed of animations. This can be useful for debugging.
+ */
+
+static gdouble gtk_slowdown = 1.0;
+
+/**
+ * gtk_progress_tracker_init_copy:
+ * @source: The source progress tracker
+ * @dest: The destination progress tracker
+ *
+ * Copy all progress tracker state from the source tracker to dest tracker.
+ **/
+void
+gtk_progress_tracker_init_copy (GtkProgressTracker *source,
+ GtkProgressTracker *dest)
+{
+ memcpy (dest, source, sizeof (GtkProgressTracker));
+}
+
+/**
+ * gtk_progress_tracker_start:
+ * @tracker: The progress tracker
+ * @duration: Animation duration in us
+ * @delay: Animation delay in us
+ * @iteration_count: Number of iterations to run the animation, must be >= 0
+ *
+ * Begins tracking progress for a new animation. Clears all previous state.
+ **/
+void
+gtk_progress_tracker_start (GtkProgressTracker *tracker,
+ guint64 duration,
+ gint64 delay,
+ gdouble iteration_count)
+{
+ tracker->is_running = TRUE;
+ tracker->last_frame_time = 0;
+ tracker->duration = duration;
+ tracker->iteration = - delay / (gdouble) duration;
+ tracker->iteration_count = iteration_count;
+}
+
+/**
+ * gtk_progress_tracker_finish:
+ * @tracker: The progress tracker
+ *
+ * Stops running the current animation.
+ **/
+void
+gtk_progress_tracker_finish (GtkProgressTracker *tracker)
+{
+ tracker->is_running = FALSE;
+}
+
+/**
+ * gtk_progress_tracker_advance_frame:
+ * @tracker: The progress tracker
+ * @frame_time: The current frame time, usually from the frame clock.
+ *
+ * Increments the progress of the animation forward a frame. If no animation has
+ * been started, does nothing.
+ **/
+void
+gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker,
+ guint64 frame_time)
+{
+ gdouble delta;
+
+ if (!tracker->is_running)
+ return;
+
+ if (tracker->last_frame_time == 0)
+ {
+ tracker->last_frame_time = frame_time;
+ return;
+ }
+
+ if (frame_time < tracker->last_frame_time)
+ {
+ g_warning ("Progress tracker frame set backwards, ignoring.");
+ return;
+ }
+
+ delta = (frame_time - tracker->last_frame_time) / gtk_slowdown / tracker->duration;
+ tracker->last_frame_time = frame_time;
+ tracker->iteration += delta;
+}
+
+/**
+ * gtk_progress_tracker_skip_frame:
+ * @tracker: The progress tracker
+ * @frame_time: The current frame time, usually from the frame clock.
+ *
+ * Does not update the progress of the animation forward, but records the frame
+ * to calculate future deltas. Calling this each frame will effectively pause
+ * the animation.
+ **/
+void
+gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker,
+ guint64 frame_time)
+{
+ if (!tracker->is_running)
+ return;
+
+ tracker->last_frame_time = frame_time;
+}
+
+/**
+ * gtk_progress_tracker_get_state:
+ * @tracker: The progress tracker
+ *
+ * Returns whether the tracker is before, during or after the currently started
+ * animation. The tracker will only ever be in the before state if the animation
+ * was started with a delay. If no animation has been started, returns
+ * %GTK_PROGRESS_STATE_AFTER.
+ *
+ * Returns: A GtkProgressState
+ **/
+GtkProgressState
+gtk_progress_tracker_get_state (GtkProgressTracker *tracker)
+{
+ if (!tracker->is_running || tracker->iteration > tracker->iteration_count)
+ return GTK_PROGRESS_STATE_AFTER;
+ if (tracker->iteration < 0)
+ return GTK_PROGRESS_STATE_BEFORE;
+ return GTK_PROGRESS_STATE_DURING;
+}
+
+/**
+ * gtk_progress_tracker_get_iteration:
+ * @tracker: The progress tracker
+ *
+ * Returns the fractional number of cycles the animation has completed. For
+ * example, it you started an animation with iteration-count of 2 and are half
+ * way through the second animation, this returns 1.5.
+ *
+ * Returns: The current iteration.
+ **/
+gdouble
+gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker)
+{
+ return tracker->is_running ? CLAMP (tracker->iteration, 0.0, tracker->iteration_count) : 1.0;
+}
+
+/**
+ * gtk_progress_tracker_get_iteration_cycle:
+ * @tracker: The progress tracker
+ *
+ * Returns an integer index of the current iteration cycle tracker is
+ * progressing through. Handles edge cases, such as an iteration value of 2.0
+ * which could be considered the end of the second iteration of the beginning of
+ * the third, in the same way as gtk_progress_tracker_get_progress().
+ *
+ * Returns: The integer count of the current animation cycle.
+ **/
+guint64
+gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker)
+{
+ gdouble iteration = gtk_progress_tracker_get_iteration (tracker);
+
+ /* Some complexity here. We want an iteration of 0.0 to always map to 0 (start
+ * of the first iteration), but an iteration of 1.0 to also map to 0 (end of
+ * first iteration) and 2.0 to 1 (end of the second iteration).
+ */
+ if (iteration == 0.0)
+ return 0;
+
+ return (guint64) ceil (iteration) - 1;
+}
+
+/**
+ * gtk_progress_tracker_get_progress:
+ * @tracker: The progress tracker
+ * @reversed: If progress should be reversed.
+ *
+ * Gets the progress through the current animation iteration, from [0, 1]. Use
+ * to interpolate between animation targets. If reverse is true each iteration
+ * will begin at 1 and end at 0.
+ *
+ * Returns: The progress value.
+ **/
+gdouble
+gtk_progress_tracker_get_progress (GtkProgressTracker *tracker,
+ gboolean reversed)
+{
+ gdouble progress, iteration;
+ guint64 iteration_cycle;
+
+ iteration = gtk_progress_tracker_get_iteration (tracker);
+ iteration_cycle = gtk_progress_tracker_get_iteration_cycle (tracker);
+
+ progress = iteration - iteration_cycle;
+ return reversed ? 1.0 - progress : progress;
+}
+
+/**
+ * gtk_progress_tracker_get_ease_out_cubic:
+ * @tracker: The progress tracker
+ * @reversed: If progress should be reversed before applying the ease function.
+ *
+ * Applies a simple ease out cubic function to the result of
+ * gtk_progress_tracker_get_progress().
+ *
+ * Returns: The eased progress value.
+ **/
+gdouble
+gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker,
+ gboolean reversed)
+{
+ gdouble progress = gtk_progress_tracker_get_progress (tracker, reversed);
+ return hdy_ease_out_cubic (progress);
+}
diff --git a/subprojects/libhandy/src/gtkprogresstrackerprivate.h b/subprojects/libhandy/src/gtkprogresstrackerprivate.h
new file mode 100644
index 0000000..fcce609
--- /dev/null
+++ b/subprojects/libhandy/src/gtkprogresstrackerprivate.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright © 2016 Endless Mobile Inc.
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ * Authors: Matthew Watson <mattdangerw@gmail.com>
+ */
+
+#ifndef __GTK_PROGRESS_TRACKER_PRIVATE_H__
+#define __GTK_PROGRESS_TRACKER_PRIVATE_H__
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+ GTK_PROGRESS_STATE_BEFORE,
+ GTK_PROGRESS_STATE_DURING,
+ GTK_PROGRESS_STATE_AFTER,
+} GtkProgressState;
+
+typedef struct _GtkProgressTracker GtkProgressTracker;
+
+struct _GtkProgressTracker
+{
+ gboolean is_running;
+ guint64 last_frame_time;
+ guint64 duration;
+ gdouble iteration;
+ gdouble iteration_count;
+};
+
+void gtk_progress_tracker_init_copy (GtkProgressTracker *source,
+ GtkProgressTracker *dest);
+
+void gtk_progress_tracker_start (GtkProgressTracker *tracker,
+ guint64 duration,
+ gint64 delay,
+ gdouble iteration_count);
+
+void gtk_progress_tracker_finish (GtkProgressTracker *tracker);
+
+void gtk_progress_tracker_advance_frame (GtkProgressTracker *tracker,
+ guint64 frame_time);
+
+void gtk_progress_tracker_skip_frame (GtkProgressTracker *tracker,
+ guint64 frame_time);
+
+GtkProgressState gtk_progress_tracker_get_state (GtkProgressTracker *tracker);
+
+gdouble gtk_progress_tracker_get_iteration (GtkProgressTracker *tracker);
+
+guint64 gtk_progress_tracker_get_iteration_cycle (GtkProgressTracker *tracker);
+
+gdouble gtk_progress_tracker_get_progress (GtkProgressTracker *tracker,
+ gboolean reverse);
+
+gdouble gtk_progress_tracker_get_ease_out_cubic (GtkProgressTracker *tracker,
+ gboolean reverse);
+
+G_END_DECLS
+
+#endif /* __GTK_PROGRESS_TRACKER_PRIVATE_H__ */
diff --git a/subprojects/libhandy/src/handy.gresources.xml b/subprojects/libhandy/src/handy.gresources.xml
new file mode 100644
index 0000000..b96444b
--- /dev/null
+++ b/subprojects/libhandy/src/handy.gresources.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+ <gresource prefix="/sm/puri/handy">
+ <file preprocess="xml-stripblanks">icons/avatar-default-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/hdy-expander-arrow-symbolic.svg</file>
+ <file compressed="true">themes/Adwaita.css</file>
+ <file compressed="true">themes/Adwaita-dark.css</file>
+ <file compressed="true">themes/fallback.css</file>
+ <file compressed="true">themes/HighContrast.css</file>
+ <file compressed="true">themes/HighContrastInverse.css</file>
+ <file compressed="true">themes/shared.css</file>
+ </gresource>
+ <gresource prefix="/sm/puri/handy/ui">
+ <file preprocess="xml-stripblanks">hdy-action-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-carousel.ui</file>
+ <file preprocess="xml-stripblanks">hdy-combo-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-expander-row.ui</file>
+ <file preprocess="xml-stripblanks">hdy-keypad.ui</file>
+ <file preprocess="xml-stripblanks">hdy-keypad-button.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-group.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-page.ui</file>
+ <file preprocess="xml-stripblanks">hdy-preferences-window.ui</file>
+ <file preprocess="xml-stripblanks">hdy-search-bar.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file>
+ <file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file>
+ </gresource>
+</gresources>
diff --git a/subprojects/libhandy/src/handy.h b/subprojects/libhandy/src/handy.h
new file mode 100644
index 0000000..1ea48a7
--- /dev/null
+++ b/subprojects/libhandy/src/handy.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#if !GTK_CHECK_VERSION(3, 22, 0)
+# error "libhandy requires gtk+-3.0 >= 3.22.0"
+#endif
+
+#if !GLIB_CHECK_VERSION(2, 50, 0)
+# error "libhandy requires glib-2.0 >= 2.50.0"
+#endif
+
+#define _HANDY_INSIDE
+
+#include "hdy-version.h"
+#include "hdy-action-row.h"
+#include "hdy-animation.h"
+#include "hdy-application-window.h"
+#include "hdy-avatar.h"
+#include "hdy-carousel.h"
+#include "hdy-carousel-indicator-dots.h"
+#include "hdy-carousel-indicator-lines.h"
+#include "hdy-clamp.h"
+#include "hdy-combo-row.h"
+#include "hdy-deck.h"
+#include "hdy-deprecation-macros.h"
+#include "hdy-enum-value-object.h"
+#include "hdy-expander-row.h"
+#include "hdy-header-bar.h"
+#include "hdy-header-group.h"
+#include "hdy-keypad.h"
+#include "hdy-leaflet.h"
+#include "hdy-main.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-preferences-group.h"
+#include "hdy-preferences-page.h"
+#include "hdy-preferences-row.h"
+#include "hdy-preferences-window.h"
+#include "hdy-search-bar.h"
+#include "hdy-squeezer.h"
+#include "hdy-swipe-group.h"
+#include "hdy-swipe-tracker.h"
+#include "hdy-swipeable.h"
+#include "hdy-title-bar.h"
+#include "hdy-types.h"
+#include "hdy-value-object.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-bar.h"
+#include "hdy-view-switcher-title.h"
+#include "hdy-window.h"
+#include "hdy-window-handle.h"
+
+#undef _HANDY_INSIDE
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-action-row.c b/subprojects/libhandy/src/hdy-action-row.c
new file mode 100644
index 0000000..95108ae
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.c
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-action-row.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-action-row
+ * @short_description: A #GtkListBox row used to present actions.
+ * @Title: HdyActionRow
+ *
+ * The #HdyActionRow widget can have a title, a subtitle and an icon. The row
+ * can receive additional widgets at its end, or prefix widgets at its start.
+ *
+ * It is convenient to present a preference and its related actions.
+ *
+ * #HdyActionRow is unactivatable by default, giving it an activatable widget
+ * will automatically make it activatable, but unsetting it won't change the
+ * row's activatability.
+ *
+ * # HdyActionRow as GtkBuildable
+ *
+ * The GtkWindow implementation of the GtkBuildable interface supports setting a
+ * child at its end by omitting the “type” attribute of a &lt;child&gt; element.
+ *
+ * It also supports setting a child as a prefix widget by specifying “prefix” as
+ * the “type” attribute of a &lt;child&gt; element.
+ *
+ * # CSS nodes
+ *
+ * #HdyActionRow has a main CSS node with name row.
+ *
+ * It contains the subnode box.header for its main horizontal box, and box.title
+ * for the vertical box containing the title and subtitle labels.
+ *
+ * It contains subnodes label.title and label.subtitle representing respectively
+ * the title label and subtitle label.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct
+{
+ GtkBox *header;
+ GtkImage *image;
+ GtkBox *prefixes;
+ GtkLabel *subtitle;
+ GtkBox *suffixes;
+ GtkLabel *title;
+ GtkBox *title_box;
+
+ GtkWidget *previous_parent;
+
+ gboolean use_underline;
+ GtkWidget *activatable_widget;
+} HdyActionRowPrivate;
+
+static void hdy_action_row_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyActionRow, hdy_action_row, HDY_TYPE_PREFERENCES_ROW,
+ G_ADD_PRIVATE (HdyActionRow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_action_row_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_ACTIVATABLE_WIDGET,
+ PROP_SUBTITLE,
+ PROP_USE_UNDERLINE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_ACTIVATED,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+row_activated_cb (HdyActionRow *self,
+ GtkListBoxRow *row)
+{
+ /* No need to use GTK_LIST_BOX_ROW() for a pointer comparison. */
+ if ((GtkListBoxRow *) self == row)
+ hdy_action_row_activate (self);
+}
+
+static void
+parent_cb (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self));
+
+ if (priv->previous_parent != NULL) {
+ g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self);
+ priv->previous_parent = NULL;
+ }
+
+ if (parent == NULL || !GTK_IS_LIST_BOX (parent))
+ return;
+
+ priv->previous_parent = parent;
+ g_signal_connect_swapped (parent, "row-activated", G_CALLBACK (row_activated_cb), self);
+}
+
+static void
+update_subtitle_visibility (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->subtitle),
+ gtk_label_get_text (priv->subtitle) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->subtitle), "") != 0);
+}
+
+static void
+hdy_action_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_action_row_get_icon_name (self));
+ break;
+ case PROP_ACTIVATABLE_WIDGET:
+ g_value_set_object (value, (GObject *) hdy_action_row_get_activatable_widget (self));
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_action_row_get_subtitle (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_action_row_get_use_underline (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_action_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_action_row_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_ACTIVATABLE_WIDGET:
+ hdy_action_row_set_activatable_widget (self, (GtkWidget*) g_value_get_object (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_action_row_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_action_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_action_row_dispose (GObject *object)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (object);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->previous_parent != NULL) {
+ g_signal_handlers_disconnect_by_func (priv->previous_parent, G_CALLBACK (row_activated_cb), self);
+ priv->previous_parent = NULL;
+ }
+
+ G_OBJECT_CLASS (hdy_action_row_parent_class)->dispose (object);
+}
+
+static void
+hdy_action_row_show_all (GtkWidget *widget)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (widget);
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->prefixes),
+ (GtkCallback) gtk_widget_show_all,
+ NULL);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->suffixes),
+ (GtkCallback) gtk_widget_show_all,
+ NULL);
+
+ GTK_WIDGET_CLASS (hdy_action_row_parent_class)->show_all (widget);
+}
+
+static void
+hdy_action_row_destroy (GtkWidget *widget)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (widget);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->header) {
+ gtk_widget_destroy (GTK_WIDGET (priv->header));
+ priv->header = NULL;
+ }
+
+ hdy_action_row_set_activatable_widget (self, NULL);
+
+ priv->prefixes = NULL;
+ priv->suffixes = NULL;
+
+ GTK_WIDGET_CLASS (hdy_action_row_parent_class)->destroy (widget);
+}
+
+static void
+hdy_action_row_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ /* When constructing the widget, we want the box to be added as the child of
+ * the GtkListBoxRow, as an implementation detail.
+ */
+ if (priv->header == NULL)
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->add (container, child);
+ else {
+ gtk_container_add (GTK_CONTAINER (priv->suffixes), child);
+ gtk_widget_show (GTK_WIDGET (priv->suffixes));
+ }
+}
+
+static void
+hdy_action_row_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->header))
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->remove (container, child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes))
+ gtk_container_remove (GTK_CONTAINER (priv->prefixes), child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->suffixes), child);
+}
+
+typedef struct {
+ HdyActionRow *row;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (data->row);
+
+ if (widget != (GtkWidget *) priv->image &&
+ widget != (GtkWidget *) priv->prefixes &&
+ widget != (GtkWidget *) priv->suffixes &&
+ widget != (GtkWidget *) priv->title_box)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_action_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (container);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_action_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.row = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ if (priv->prefixes)
+ GTK_CONTAINER_GET_CLASS (priv->prefixes)->forall (GTK_CONTAINER (priv->prefixes), include_internals, for_non_internal_child, &data);
+ if (priv->suffixes)
+ GTK_CONTAINER_GET_CLASS (priv->suffixes)->forall (GTK_CONTAINER (priv->suffixes), include_internals, for_non_internal_child, &data);
+ if (priv->header)
+ GTK_CONTAINER_GET_CLASS (priv->header)->forall (GTK_CONTAINER (priv->header), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_action_row_activate_real (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->activatable_widget)
+ gtk_widget_mnemonic_activate (priv->activatable_widget, FALSE);
+
+ g_signal_emit (self, signals[SIGNAL_ACTIVATED], 0);
+}
+
+static void
+hdy_action_row_class_init (HdyActionRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_action_row_get_property;
+ object_class->set_property = hdy_action_row_set_property;
+ object_class->dispose = hdy_action_row_dispose;
+
+ widget_class->destroy = hdy_action_row_destroy;
+ widget_class->show_all = hdy_action_row_show_all;
+
+ container_class->add = hdy_action_row_add;
+ container_class->remove = hdy_action_row_remove;
+ container_class->forall = hdy_action_row_forall;
+
+ klass->activate = hdy_action_row_activate_real;
+
+ /**
+ * HdyActionRow:icon-name:
+ *
+ * The icon name for this row.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyActionRow:activatable-widget:
+ *
+ * The activatable widget for this row.
+ *
+ * Since: 0.0.7
+ */
+ props[PROP_ACTIVATABLE_WIDGET] =
+ g_param_spec_object ("activatable-widget",
+ _("Activatable widget"),
+ _("The widget to be activated when the row is activated"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyActionRow:subtitle:
+ *
+ * The subtitle for this row.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("Subtitle"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyActionRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title and subtitle labels
+ * indicates a mnemonic.
+ *
+ * Since: 0.0.6
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyActionRow::activated:
+ * @self: The #HdyActionRow instance
+ *
+ * This signal is emitted after the row has been activated.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_ACTIVATED] =
+ g_signal_new ("activated",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-action-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, header);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, prefixes);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, subtitle);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, suffixes);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyActionRow, title_box);
+}
+
+static gboolean
+string_is_not_empty (GBinding *binding,
+ const GValue *from_value,
+ GValue *to_value,
+ gpointer user_data)
+{
+ const gchar *string = g_value_get_string (from_value);
+
+ g_value_set_boolean (to_value, string != NULL && g_strcmp0 (string, "") != 0);
+
+ return TRUE;
+}
+
+static void
+hdy_action_row_init (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ g_object_bind_property_full (self, "title", priv->title, "visible", G_BINDING_SYNC_CREATE,
+ string_is_not_empty, NULL, NULL, NULL);
+
+ update_subtitle_visibility (self);
+
+ g_signal_connect (self, "notify::parent", G_CALLBACK (parent_cb), NULL);
+
+}
+
+static void
+hdy_action_row_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (buildable);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->header == NULL || !type)
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+ else if (type && strcmp (type, "prefix") == 0)
+ hdy_action_row_add_prefix (self, GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type);
+}
+
+static void
+hdy_action_row_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+ iface->add_child = hdy_action_row_buildable_add_child;
+}
+
+/**
+ * hdy_action_row_new:
+ *
+ * Creates a new #HdyActionRow.
+ *
+ * Returns: a new #HdyActionRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_action_row_new (void)
+{
+ return g_object_new (HDY_TYPE_ACTION_ROW, NULL);
+}
+
+/**
+ * hdy_action_row_get_subtitle:
+ * @self: a #HdyActionRow
+ *
+ * Gets the subtitle for @self.
+ *
+ * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL.
+ *
+ * Since: 0.0.6
+ */
+const gchar *
+hdy_action_row_get_subtitle (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return gtk_label_get_text (priv->subtitle);
+}
+
+/**
+ * hdy_action_row_set_subtitle:
+ * @self: a #HdyActionRow
+ * @subtitle: (nullable): the subtitle
+ *
+ * Sets the subtitle for @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_subtitle (HdyActionRow *self,
+ const gchar *subtitle)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_text (priv->subtitle), subtitle) == 0)
+ return;
+
+ gtk_label_set_text (priv->subtitle, subtitle);
+ gtk_widget_set_visible (GTK_WIDGET (priv->subtitle),
+ subtitle != NULL && g_strcmp0 (subtitle, "") != 0);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_action_row_get_icon_name:
+ * @self: a #HdyActionRow
+ *
+ * Gets the icon name for @self.
+ *
+ * Returns: the icon name for @self.
+ *
+ * Since: 0.0.6
+ */
+const gchar *
+hdy_action_row_get_icon_name (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+ const gchar *icon_name;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_image_get_icon_name (priv->image, &icon_name, NULL);
+
+ return icon_name;
+}
+
+/**
+ * hdy_action_row_set_icon_name:
+ * @self: a #HdyActionRow
+ * @icon_name: the icon name
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_icon_name (HdyActionRow *self,
+ const gchar *icon_name)
+{
+ HdyActionRowPrivate *priv;
+ const gchar *old_icon_name;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_image_get_icon_name (priv->image, &old_icon_name, NULL);
+ if (g_strcmp0 (old_icon_name, icon_name) == 0)
+ return;
+
+ gtk_image_set_from_icon_name (priv->image, icon_name, GTK_ICON_SIZE_INVALID);
+ gtk_widget_set_visible (GTK_WIDGET (priv->image),
+ icon_name != NULL && g_strcmp0 (icon_name, "") != 0);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_action_row_get_activatable_widget:
+ * @self: a #HdyActionRow
+ *
+ * Gets the widget activated when @self is activated.
+ *
+ * Returns: (nullable) (transfer none): the widget activated when @self is
+ * activated, or %NULL if none has been set.
+ *
+ * Since: 0.0.7
+ */
+GtkWidget *
+hdy_action_row_get_activatable_widget (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), NULL);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return priv->activatable_widget;
+}
+
+static void
+activatable_widget_weak_notify (gpointer data,
+ GObject *where_the_object_was)
+{
+ HdyActionRow *self = HDY_ACTION_ROW (data);
+ HdyActionRowPrivate *priv = hdy_action_row_get_instance_private (self);
+
+ priv->activatable_widget = NULL;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]);
+}
+
+/**
+ * hdy_action_row_set_activatable_widget:
+ * @self: a #HdyActionRow
+ * @widget: (nullable): the target #GtkWidget, or %NULL to unset
+ *
+ * Sets the widget to activate when @self is activated, either by clicking
+ * on it, by calling hdy_action_row_activate(), or via mnemonics in the title or
+ * the subtitle. See the “use_underline” property to enable mnemonics.
+ *
+ * The target widget will be activated by emitting the
+ * GtkWidget::mnemonic-activate signal on it.
+ *
+ * Since: 0.0.7
+ */
+void
+hdy_action_row_set_activatable_widget (HdyActionRow *self,
+ GtkWidget *widget)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+ g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->activatable_widget == widget)
+ return;
+
+ if (priv->activatable_widget)
+ g_object_weak_unref (G_OBJECT (priv->activatable_widget),
+ activatable_widget_weak_notify,
+ self);
+
+ priv->activatable_widget = widget;
+
+ if (priv->activatable_widget != NULL) {
+ g_object_weak_ref (G_OBJECT (priv->activatable_widget),
+ activatable_widget_weak_notify,
+ self);
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), TRUE);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVATABLE_WIDGET]);
+}
+
+/**
+ * hdy_action_row_get_use_underline:
+ * @self: a #HdyActionRow
+ *
+ * Gets whether an embedded underline in the text of the title and subtitle
+ * labels indicates a mnemonic. See hdy_action_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title and subtitle labels
+ * indicates the mnemonic accelerator keys.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_action_row_get_use_underline (HdyActionRow *self)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_ACTION_ROW (self), FALSE);
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ return priv->use_underline;
+}
+
+/**
+ * hdy_action_row_set_use_underline:
+ * @self: a #HdyActionRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title and subtitle labels indicates
+ * the next character should be used for the mnemonic accelerator key.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_set_use_underline (HdyActionRow *self,
+ gboolean use_underline)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ if (priv->use_underline == !!use_underline)
+ return;
+
+ priv->use_underline = !!use_underline;
+ hdy_preferences_row_set_use_underline (HDY_PREFERENCES_ROW (self), priv->use_underline);
+ gtk_label_set_use_underline (priv->title, priv->use_underline);
+ gtk_label_set_use_underline (priv->subtitle, priv->use_underline);
+ gtk_label_set_mnemonic_widget (priv->title, GTK_WIDGET (self));
+ gtk_label_set_mnemonic_widget (priv->subtitle, GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]);
+}
+
+/**
+ * hdy_action_row_add_prefix:
+ * @self: a #HdyActionRow
+ * @widget: the prefix widget
+ *
+ * Adds a prefix widget to @self.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_action_row_add_prefix (HdyActionRow *self,
+ GtkWidget *widget)
+{
+ HdyActionRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (self));
+
+ priv = hdy_action_row_get_instance_private (self);
+
+ gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->prefixes));
+}
+
+void
+hdy_action_row_activate (HdyActionRow *self)
+{
+ g_return_if_fail (HDY_IS_ACTION_ROW (self));
+
+ HDY_ACTION_ROW_GET_CLASS (self)->activate (self);
+}
diff --git a/subprojects/libhandy/src/hdy-action-row.h b/subprojects/libhandy/src/hdy-action-row.h
new file mode 100644
index 0000000..7b5dd53
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include "hdy-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_ACTION_ROW (hdy_action_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyActionRow, hdy_action_row, HDY, ACTION_ROW, HdyPreferencesRow)
+
+/**
+ * HdyActionRowClass
+ * @parent_class: The parent class
+ * @activate: Activates the row to trigger its main action.
+ */
+struct _HdyActionRowClass
+{
+ GtkListBoxRowClass parent_class;
+
+ void (*activate) (HdyActionRow *self);
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_action_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_action_row_get_subtitle (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_subtitle (HdyActionRow *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_action_row_get_icon_name (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_icon_name (HdyActionRow *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_action_row_get_activatable_widget (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_activatable_widget (HdyActionRow *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_action_row_get_use_underline (HdyActionRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_set_use_underline (HdyActionRow *self,
+ gboolean use_underline);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_add_prefix (HdyActionRow *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_action_row_activate (HdyActionRow *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-action-row.ui b/subprojects/libhandy/src/hdy-action-row.ui
new file mode 100644
index 0000000..ff54c15
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-action-row.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyActionRow" parent="HdyPreferencesRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox" id="header">
+ <property name="can_focus">False</property>
+ <property name="spacing">12</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="header"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="prefixes">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="no_show_all">True</property>
+ <property name="pixel_size">32</property>
+ <property name="valign">center</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="title_box">
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="no_show_all">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="title"/>
+ </style>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="label" bind-source="HdyActionRow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="hexpand">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="subtitle"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="suffixes">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-animation-private.h b/subprojects/libhandy/src/hdy-animation-private.h
new file mode 100644
index 0000000..f31002a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation-private.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-animation.h"
+
+G_BEGIN_DECLS
+
+gdouble hdy_lerp (gdouble a, gdouble b, gdouble t);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-animation.c b/subprojects/libhandy/src/hdy-animation.c
new file mode 100644
index 0000000..ce5bf64
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-animation-private.h"
+
+/**
+ * SECTION:hdy-animation
+ * @short_description: Animation helpers
+ * @title: Animation Helpers
+ *
+ * Animation helpers.
+ *
+ * Since: 0.0.11
+ */
+
+/**
+ * hdy_get_enable_animations:
+ * @widget: a #GtkWidget
+ *
+ * Returns whether animations are enabled for that widget. This should be used
+ * when implementing an animated widget to know whether to animate it or not.
+ *
+ * Returns: %TRUE if animations are enabled for @widget.
+ *
+ * Since: 0.0.11
+ */
+gboolean
+hdy_get_enable_animations (GtkWidget *widget)
+{
+ gboolean enable_animations = TRUE;
+
+ g_assert (GTK_IS_WIDGET (widget));
+
+ g_object_get (gtk_widget_get_settings (widget),
+ "gtk-enable-animations", &enable_animations,
+ NULL);
+
+ return enable_animations;
+}
+
+/**
+ * hdy_lerp: (skip)
+ * @a: the start
+ * @b: the end
+ * @t: the interpolation rate
+ *
+ * Computes the linear interpolation between @a and @b for @t.
+ *
+ * Returns: the linear interpolation between @a and @b for @t.
+ *
+ * Since: 0.0.11
+ */
+gdouble
+hdy_lerp (gdouble a, gdouble b, gdouble t)
+{
+ return a * (1.0 - t) + b * t;
+}
+
+/* From clutter-easing.c, based on Robert Penner's
+ * infamous easing equations, MIT license.
+ */
+
+/**
+ * hdy_ease_out_cubic:
+ * @t: the term
+ *
+ * Computes the ease out for @t.
+ *
+ * Returns: the ease out for @t.
+ *
+ * Since: 0.0.11
+ */
+gdouble
+hdy_ease_out_cubic (gdouble t)
+{
+ gdouble p = t - 1;
+ return p * p * p + 1;
+}
diff --git a/subprojects/libhandy/src/hdy-animation.h b/subprojects/libhandy/src/hdy-animation.h
new file mode 100644
index 0000000..5af34c0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-animation.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_get_enable_animations (GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_ease_out_cubic (gdouble t);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-application-window.c b/subprojects/libhandy/src/hdy-application-window.c
new file mode 100644
index 0000000..d3979cf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-application-window.c
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-application-window.h"
+#include "hdy-window-mixin-private.h"
+
+/**
+ * SECTION:hdy-application-window
+ * @short_description: A freeform application window.
+ * @title: HdyApplicationWindow
+ * @See_also: #HdyHeaderBar, #HdyWindow, #HdyWindowHandle
+ *
+ * HdyApplicationWindow is a #GtkApplicationWindow subclass providing the same
+ * features as #HdyWindow.
+ *
+ * See #HdyWindow for details.
+ *
+ * Using gtk_application_set_app_menu() and gtk_application_set_menubar() is
+ * not supported and may result in visual glitches.
+ *
+ * Since: 1.0
+ */
+
+typedef struct
+{
+ HdyWindowMixin *mixin;
+} HdyApplicationWindowPrivate;
+
+static void hdy_application_window_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyApplicationWindow, hdy_application_window, GTK_TYPE_APPLICATION_WINDOW,
+ G_ADD_PRIVATE (HdyApplicationWindow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_application_window_buildable_init))
+
+#define HDY_GET_WINDOW_MIXIN(obj) (((HdyApplicationWindowPrivate *) hdy_application_window_get_instance_private (HDY_APPLICATION_WINDOW (obj)))->mixin)
+
+static void
+hdy_application_window_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_application_window_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_application_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container),
+ include_internals,
+ callback,
+ callback_data);
+}
+
+static gboolean
+hdy_application_window_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr);
+}
+
+static void
+hdy_application_window_destroy (GtkWidget *widget)
+{
+ hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget));
+}
+
+static void
+hdy_application_window_finalize (GObject *object)
+{
+ HdyApplicationWindow *self = (HdyApplicationWindow *)object;
+ HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self);
+
+ g_clear_object (&priv->mixin);
+
+ G_OBJECT_CLASS (hdy_application_window_parent_class)->finalize (object);
+}
+
+static void
+hdy_application_window_class_init (HdyApplicationWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_application_window_finalize;
+ widget_class->draw = hdy_application_window_draw;
+ widget_class->destroy = hdy_application_window_destroy;
+ container_class->add = hdy_application_window_add;
+ container_class->remove = hdy_application_window_remove;
+ container_class->forall = hdy_application_window_forall;
+}
+
+static void
+hdy_application_window_init (HdyApplicationWindow *self)
+{
+ HdyApplicationWindowPrivate *priv = hdy_application_window_get_instance_private (self);
+
+ priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self),
+ GTK_WINDOW_CLASS (hdy_application_window_parent_class));
+
+ gtk_application_window_set_show_menubar (GTK_APPLICATION_WINDOW (self), FALSE);
+}
+
+static void
+hdy_application_window_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable),
+ builder,
+ child,
+ type);
+}
+
+static void
+hdy_application_window_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_application_window_buildable_add_child;
+}
+
+/**
+ * hdy_application_window_new:
+ *
+ * Creates a new #HdyApplicationWindow.
+ *
+ * Returns: (transfer full): a newly created #HdyApplicationWindow
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_application_window_new (void)
+{
+ return g_object_new (HDY_TYPE_APPLICATION_WINDOW,
+ NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-application-window.h b/subprojects/libhandy/src/hdy-application-window.h
new file mode 100644
index 0000000..ed01eb1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-application-window.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_APPLICATION_WINDOW (hdy_application_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyApplicationWindow, hdy_application_window, HDY, APPLICATION_WINDOW, GtkApplicationWindow)
+
+struct _HdyApplicationWindowClass
+{
+ GtkApplicationWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_application_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-avatar.c b/subprojects/libhandy/src/hdy-avatar.c
new file mode 100644
index 0000000..9dcdcdf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-avatar.c
@@ -0,0 +1,811 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ * Copyright (C) 2020 Felipe Borges
+ *
+ * Authors:
+ * Felipe Borges <felipeborges@gnome.org>
+ * Julian Sparber <julian@sparber.net>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ */
+
+#include "config.h"
+#include <math.h>
+
+#include "hdy-avatar.h"
+#include "hdy-cairo-private.h"
+
+#define NUMBER_OF_COLORS 14
+/**
+ * SECTION:hdy-avatar
+ * @short_description: A widget displaying an image, with a generated fallback.
+ * @Title: HdyAvatar
+ *
+ * #HdyAvatar is a widget to display a round avatar.
+ * A provided image is made round before displaying, if no image is given this
+ * widget generates a round fallback with the initials of the #HdyAvatar:text
+ * on top of a colord background.
+ * The color is picked based on the hash of the #HdyAvatar:text.
+ * If #HdyAvatar:show-initials is set to %FALSE, `avatar-default-symbolic` is
+ * shown in place of the initials.
+ * Use hdy_avatar_set_image_load_func () to set a custom image.
+ * Create a #HdyAvatarImageLoadFunc similar to this example:
+ *
+ * |[<!-- language="C" -->
+ * static GdkPixbuf *
+ * image_load_func (gint size, gpointer user_data)
+ * {
+ * g_autoptr (GError) error = NULL;
+ * g_autoptr (GdkPixbuf) pixbuf = NULL;
+ * g_autofree gchar *file = gtk_file_chooser_get_filename ("avatar.png");
+ * gint width, height;
+ *
+ * gdk_pixbuf_get_file_info (file, &width, &height);
+ *
+ * pixbuf = gdk_pixbuf_new_from_file_at_scale (file,
+ * (width <= height) ? size : -1,
+ * (width >= height) ? size : -1,
+ * TRUE,
+ * error);
+ * if (error != NULL) {
+ * g_critical ("Failed to create pixbuf from file: %s", error->message);
+ *
+ * return NULL;
+ * }
+ *
+ * return pixbuf;
+ * }
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyAvatar has a single CSS node with name avatar.
+ *
+ */
+
+struct _HdyAvatar
+{
+ GtkDrawingArea parent_instance;
+
+ gchar *icon_name;
+ gchar *text;
+ PangoLayout *layout;
+ gboolean show_initials;
+ guint color_class;
+ gint size;
+ cairo_surface_t *round_image;
+
+ HdyAvatarImageLoadFunc load_image_func;
+ gpointer load_image_func_target;
+ GDestroyNotify load_image_func_target_destroy_notify;
+};
+
+G_DEFINE_TYPE (HdyAvatar, hdy_avatar, GTK_TYPE_DRAWING_AREA);
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_TEXT,
+ PROP_SHOW_INITIALS,
+ PROP_SIZE,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+static cairo_surface_t *
+round_image (GdkPixbuf *pixbuf,
+ gdouble size)
+{
+ g_autoptr (cairo_surface_t) surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
+ g_autoptr (cairo_t) cr = cairo_create (surface);
+
+ /* Clip a circle */
+ cairo_arc (cr, size / 2.0, size / 2.0, size / 2.0, 0, 2 * G_PI);
+ cairo_clip (cr);
+ cairo_new_path (cr);
+
+ gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0);
+ cairo_paint (cr);
+
+ return g_steal_pointer (&surface);
+}
+
+static gchar *
+extract_initials_from_text (const gchar *text)
+{
+ GString *initials;
+ g_autofree gchar *p = g_utf8_strup (text, -1);
+ g_autofree gchar *normalized = g_utf8_normalize (g_strstrip (p), -1, G_NORMALIZE_DEFAULT_COMPOSE);
+ gunichar unichar;
+ gchar *q = NULL;
+
+ if (normalized == NULL)
+ return NULL;
+
+ initials = g_string_new ("");
+
+ unichar = g_utf8_get_char (normalized);
+ g_string_append_unichar (initials, unichar);
+
+ q = g_utf8_strrchr (normalized, -1, ' ');
+ if (q != NULL && g_utf8_next_char (q) != NULL) {
+ q = g_utf8_next_char (q);
+
+ unichar = g_utf8_get_char (q);
+ g_string_append_unichar (initials, unichar);
+ }
+
+ return g_string_free (initials, FALSE);
+}
+
+static void
+update_custom_image (HdyAvatar *self)
+{
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ gint scale_factor;
+ gint size;
+ gboolean was_custom = FALSE;
+
+ if (self->round_image != NULL) {
+ g_clear_pointer (&self->round_image, cairo_surface_destroy);
+ was_custom = TRUE;
+ }
+
+ if (self->load_image_func != NULL) {
+ scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self));
+ size = MIN (gtk_widget_get_allocated_width (GTK_WIDGET (self)),
+ gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+ pixbuf = self->load_image_func (size * scale_factor, self->load_image_func_target);
+ if (pixbuf != NULL) {
+ self->round_image = round_image (pixbuf, (gdouble) size * scale_factor);
+ cairo_surface_set_device_scale (self->round_image, scale_factor, scale_factor);
+ }
+ }
+
+ if (was_custom || self->round_image != NULL)
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+set_class_color (HdyAvatar *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ g_autofree GRand *rand = NULL;
+ g_autofree gchar *new_class = NULL;
+ g_autofree gchar *old_class = g_strdup_printf ("color%d", self->color_class);
+
+ gtk_style_context_remove_class (context, old_class);
+
+ if (self->text == NULL || strlen (self->text) == 0) {
+ /* Use a random color if we don't have a text */
+ rand = g_rand_new ();
+ self->color_class = g_rand_int_range (rand, 1, NUMBER_OF_COLORS);
+ } else {
+ self->color_class = (g_str_hash (self->text) % NUMBER_OF_COLORS) + 1;
+ }
+
+ new_class = g_strdup_printf ("color%d", self->color_class);
+ gtk_style_context_add_class (context, new_class);
+}
+
+static void
+set_class_contrasted (HdyAvatar *self, gint size)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ if (size < 25)
+ gtk_style_context_add_class (context, "contrasted");
+ else
+ gtk_style_context_remove_class (context, "contrasted");
+}
+
+static void
+clear_pango_layout (HdyAvatar *self)
+{
+ g_clear_object (&self->layout);
+}
+
+static void
+ensure_pango_layout (HdyAvatar *self)
+{
+ g_autofree gchar *initials = NULL;
+
+ if (self->layout != NULL || self->text == NULL || strlen (self->text) == 0)
+ return;
+
+ initials = extract_initials_from_text (self->text);
+ self->layout = gtk_widget_create_pango_layout (GTK_WIDGET (self), initials);
+}
+
+static void
+set_font_size (HdyAvatar *self,
+ gint size)
+{
+ GtkStyleContext *context;
+ PangoFontDescription *font_desc;
+ gint width, height;
+ gdouble padding;
+ gdouble sqr_size;
+ gdouble max_size;
+ gdouble new_font_size;
+
+ if (self->round_image != NULL || self->layout == NULL)
+ return;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ gtk_style_context_get (context, gtk_style_context_get_state (context),
+ "font", &font_desc, NULL);
+
+ pango_layout_set_font_description (self->layout, font_desc);
+ pango_layout_get_pixel_size (self->layout, &width, &height);
+
+ /* This is the size of the biggest square fitting inside the circle */
+ sqr_size = (gdouble)size / 1.4142;
+ /* The padding has to be a function of the overall size.
+ * The 0.4 is how steep the linear function grows and the -5 is just
+ * an adjustment for smaller sizes which doesn't have a big impact on bigger sizes.
+ * Make also sure we don't have a negative padding */
+ padding = MAX (size * 0.4 - 5, 0);
+ max_size = sqr_size - padding;
+ new_font_size = (gdouble)height * (max_size / (gdouble)width);
+
+ font_desc = pango_font_description_copy (font_desc);
+ pango_font_description_set_absolute_size (font_desc,
+ CLAMP (new_font_size, 0, max_size) * PANGO_SCALE);
+ pango_layout_set_font_description (self->layout, font_desc);
+ pango_font_description_free (font_desc);
+}
+
+static void
+hdy_avatar_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ switch (property_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_avatar_get_icon_name (self));
+ break;
+
+ case PROP_TEXT:
+ g_value_set_string (value, hdy_avatar_get_text (self));
+ break;
+
+ case PROP_SHOW_INITIALS:
+ g_value_set_boolean (value, hdy_avatar_get_show_initials (self));
+ break;
+
+ case PROP_SIZE:
+ g_value_set_int (value, hdy_avatar_get_size (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_avatar_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ switch (property_id) {
+ case PROP_ICON_NAME:
+ hdy_avatar_set_icon_name (self, g_value_get_string (value));
+ break;
+
+ case PROP_TEXT:
+ hdy_avatar_set_text (self, g_value_get_string (value));
+ break;
+
+ case PROP_SHOW_INITIALS:
+ hdy_avatar_set_show_initials (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SIZE:
+ hdy_avatar_set_size (self, g_value_get_int (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_avatar_finalize (GObject *object)
+{
+ HdyAvatar *self = HDY_AVATAR (object);
+
+ g_clear_pointer (&self->icon_name, g_free);
+ g_clear_pointer (&self->text, g_free);
+ g_clear_pointer (&self->round_image, cairo_surface_destroy);
+ g_clear_object (&self->layout);
+
+ if (self->load_image_func_target_destroy_notify != NULL)
+ self->load_image_func_target_destroy_notify (self->load_image_func_target);
+
+ G_OBJECT_CLASS (hdy_avatar_parent_class)->finalize (object);
+}
+
+static gboolean
+hdy_avatar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyAvatar *self = HDY_AVATAR (widget);
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gint width = gtk_widget_get_allocated_width (widget);
+ gint height = gtk_widget_get_allocated_height (widget);
+ gint size = MIN (width, height);
+ gdouble x = (gdouble)(width - size) / 2.0;
+ gdouble y = (gdouble)(height - size) / 2.0;
+ const gchar *icon_name;
+ gint scale;
+ GdkRGBA color;
+ g_autoptr (GtkIconInfo) icon = NULL;
+ g_autoptr (GdkPixbuf) pixbuf = NULL;
+ g_autoptr (GError) error = NULL;
+ g_autoptr (cairo_surface_t) surface = NULL;
+
+ set_class_contrasted (HDY_AVATAR (widget), size);
+
+ gtk_render_frame (context, cr, x, y, size, size);
+
+ if (self->round_image) {
+ cairo_set_source_surface (cr, self->round_image, x, y);
+ cairo_paint (cr);
+
+ return FALSE;
+ }
+
+ gtk_render_background (context, cr, x, y, size, size);
+ ensure_pango_layout (HDY_AVATAR (widget));
+
+ if (self->show_initials && self->layout != NULL) {
+ set_font_size (HDY_AVATAR (widget), size);
+ pango_layout_get_pixel_size (self->layout, &width, &height);
+
+ gtk_render_layout (context, cr,
+ ((gdouble)(size - width) / 2.0) + x,
+ ((gdouble)(size - height) / 2.0) + y,
+ self->layout);
+
+ return FALSE;
+ }
+
+ icon_name = self->icon_name && *self->icon_name != '\0' ?
+ self->icon_name : "avatar-default-symbolic";
+ scale = gtk_widget_get_scale_factor (widget);
+ icon = gtk_icon_theme_lookup_icon_for_scale (gtk_icon_theme_get_default (),
+ icon_name,
+ size / 2, scale,
+ GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
+ if (icon == NULL) {
+ g_critical ("Failed to load icon `%s'", icon_name);
+
+ return FALSE;
+ }
+
+ gtk_style_context_get_color (context, gtk_style_context_get_state (context), &color);
+ pixbuf = gtk_icon_info_load_symbolic (icon, &color, NULL, NULL, NULL, NULL, &error);
+ if (error != NULL) {
+ g_critical ("Failed to load icon `%s': %s", icon_name, error->message);
+
+ return FALSE;
+ }
+
+ surface = gdk_cairo_surface_create_from_pixbuf (pixbuf, scale,
+ gtk_widget_get_window (widget));
+
+ width = cairo_image_surface_get_width (surface);
+ height = cairo_image_surface_get_height (surface);
+ gtk_render_icon_surface (context, cr, surface,
+ (((gdouble)size - ((gdouble)width / (gdouble)scale)) / 2.0) + x,
+ (((gdouble)size - ((gdouble)height / (gdouble)scale)) / 2.0) + y);
+
+ return FALSE;
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_avatar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdyAvatar *self = HDY_AVATAR (widget);
+
+ if (minimum)
+ *minimum = self->size;
+ if (natural)
+ *natural = self->size;
+}
+
+static void
+hdy_avatar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_avatar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_avatar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static GtkSizeRequestMode
+hdy_avatar_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+hdy_avatar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkAllocation clip;
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ GTK_WIDGET_CLASS (hdy_avatar_parent_class)->size_allocate (widget, allocation);
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_avatar_class_init (HdyAvatarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_avatar_finalize;
+
+ object_class->set_property = hdy_avatar_set_property;
+ object_class->get_property = hdy_avatar_get_property;
+
+ widget_class->draw = hdy_avatar_draw;
+ widget_class->get_request_mode = hdy_avatar_get_request_mode;
+ widget_class->get_preferred_width = hdy_avatar_get_preferred_width;
+ widget_class->get_preferred_height = hdy_avatar_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_avatar_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_avatar_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_avatar_size_allocate;
+
+ /**
+ * HdyAvatar:size:
+ *
+ * The avatar size of the avatar.
+ */
+ props[PROP_SIZE] =
+ g_param_spec_int ("size",
+ "Size",
+ "The size of the avatar",
+ -1, INT_MAX, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:icon-name:
+ *
+ * The name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ * If no name is set, the avatar-default-symbolic icon will be used.
+ * If the name doesn't match a valid icon, it is an error and no icon will be
+ * displayed.
+ * If the icon theme is changed, the image will be updated automatically.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ "Icon name",
+ "The name of the icon from the icon theme",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:text:
+ *
+ * The text used for the initials and for generating the color.
+ * If #HdyAvatar:show-initials is %FALSE it's only used to generate the color.
+ */
+ props[PROP_TEXT] =
+ g_param_spec_string ("text",
+ "Text",
+ "The text used to generate the color and the initials",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyAvatar:show_initials:
+ *
+ * Whether to show the initials or the fallback icon on the generated avatar.
+ */
+ props[PROP_SHOW_INITIALS] =
+ g_param_spec_boolean ("show-initials",
+ "Show initials",
+ "Whether to show the initials",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "avatar");
+}
+
+static void
+hdy_avatar_init (HdyAvatar *self)
+{
+ set_class_color (self);
+ g_signal_connect (self, "notify::scale-factor", G_CALLBACK (update_custom_image), NULL);
+ g_signal_connect (self, "size-allocate", G_CALLBACK (update_custom_image), NULL);
+ g_signal_connect (self, "screen-changed", G_CALLBACK (clear_pango_layout), NULL);
+}
+
+/**
+ * hdy_avatar_new:
+ * @size: The size of the avatar
+ * @text: (nullable): The text used to generate the color and initials if
+ * @show_initials is %TRUE. The color is selected at random if @text is empty.
+ * @show_initials: whether to show the initials or the fallback icon on
+ * top of the color generated based on @text.
+ *
+ * Creates a new #HdyAvatar.
+ *
+ * Returns: the newly created #HdyAvatar
+ */
+GtkWidget *
+hdy_avatar_new (gint size,
+ const gchar *text,
+ gboolean show_initials)
+{
+ return g_object_new (HDY_TYPE_AVATAR,
+ "size", size,
+ "text", text,
+ "show-initials", show_initials,
+ NULL);
+}
+
+/**
+ * hdy_avatar_get_icon_name:
+ * @self: a #HdyAvatar
+ *
+ * Gets the name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ *
+ * Returns: (nullable) (transfer none): the name of the icon from the icon theme.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_avatar_get_icon_name (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
+
+ return self->icon_name;
+}
+
+/**
+ * hdy_avatar_set_icon_name:
+ * @self: a #HdyAvatar
+ * @icon_name: (nullable): the name of the icon from the icon theme
+ *
+ * Sets the name of the icon in the icon theme to use when the icon should be
+ * displayed.
+ * If no name is set, the avatar-default-symbolic icon will be used.
+ * If the name doesn't match a valid icon, it is an error and no icon will be
+ * displayed.
+ * If the icon theme is changed, the image will be updated automatically.
+ *
+ * Since: 1.0
+ */
+void
+hdy_avatar_set_icon_name (HdyAvatar *self,
+ const gchar *icon_name)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (g_strcmp0 (self->icon_name, icon_name) == 0)
+ return;
+
+ g_clear_pointer (&self->icon_name, g_free);
+ self->icon_name = g_strdup (icon_name);
+
+ if (!self->round_image &&
+ (!self->show_initials || self->layout == NULL))
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_avatar_get_text:
+ * @self: a #HdyAvatar
+ *
+ * Get the text used to generate the fallback initials and color
+ *
+ * Returns: (nullable) (transfer none): returns the text used to generate
+ * the fallback initials. This is the internal string used by
+ * the #HdyAvatar, and must not be modified.
+ */
+const gchar *
+hdy_avatar_get_text (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), NULL);
+
+ return self->text;
+}
+
+/**
+ * hdy_avatar_set_text:
+ * @self: a #HdyAvatar
+ * @text: (nullable): the text used to get the initials and color
+ *
+ * Set the text used to generate the fallback initials color
+ */
+void
+hdy_avatar_set_text (HdyAvatar *self,
+ const gchar *text)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (g_strcmp0 (self->text, text) == 0)
+ return;
+
+ g_clear_pointer (&self->text, g_free);
+ self->text = g_strdup (text);
+
+ clear_pango_layout (self);
+ set_class_color (self);
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TEXT]);
+}
+
+/**
+ * hdy_avatar_get_show_initials:
+ * @self: a #HdyAvatar
+ *
+ * Returns whether initials are used for the fallback or the icon.
+ *
+ * Returns: %TRUE if the initials are used for the fallback.
+ */
+gboolean
+hdy_avatar_get_show_initials (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), FALSE);
+
+ return self->show_initials;
+}
+
+/**
+ * hdy_avatar_set_show_initials:
+ * @self: a #HdyAvatar
+ * @show_initials: whether the initials should be shown on the fallback avatar
+ * or the icon.
+ *
+ * Sets whether the initials should be shown on the fallback avatar or the icon.
+ */
+void
+hdy_avatar_set_show_initials (HdyAvatar *self,
+ gboolean show_initials)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+
+ if (self->show_initials == show_initials)
+ return;
+
+ self->show_initials = show_initials;
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_INITIALS]);
+}
+
+/**
+ * hdy_avatar_set_image_load_func:
+ * @self: a #HdyAvatar
+ * @load_image: (closure user_data) (nullable): callback to set a custom image
+ * @user_data: (nullable): user data passed to @load_image
+ * @destroy: (nullable): destroy notifier for @user_data
+ *
+ * A callback which is called when the custom image need to be reloaded for some
+ * reason (e.g. scale-factor changes).
+ */
+void
+hdy_avatar_set_image_load_func (HdyAvatar *self,
+ HdyAvatarImageLoadFunc load_image,
+ gpointer user_data,
+ GDestroyNotify destroy)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+ g_return_if_fail (user_data != NULL || (user_data == NULL && destroy == NULL));
+
+ if (self->load_image_func_target_destroy_notify != NULL)
+ self->load_image_func_target_destroy_notify (self->load_image_func_target);
+
+ self->load_image_func = load_image;
+ self->load_image_func_target = user_data;
+ self->load_image_func_target_destroy_notify = destroy;
+
+ update_custom_image (self);
+}
+
+/**
+ * hdy_avatar_get_size:
+ * @self: a #HdyAvatar
+ *
+ * Returns the size of the avatar.
+ *
+ * Returns: the size of the avatar.
+ */
+gint
+hdy_avatar_get_size (HdyAvatar *self)
+{
+ g_return_val_if_fail (HDY_IS_AVATAR (self), 0);
+
+ return self->size;
+}
+
+/**
+ * hdy_avatar_set_size:
+ * @self: a #HdyAvatar
+ * @size: The size to be used for the avatar
+ *
+ * Sets the size of the avatar.
+ */
+void
+hdy_avatar_set_size (HdyAvatar *self,
+ gint size)
+{
+ g_return_if_fail (HDY_IS_AVATAR (self));
+ g_return_if_fail (size >= -1);
+
+ if (self->size == size)
+ return;
+
+ self->size = size;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SIZE]);
+}
diff --git a/subprojects/libhandy/src/hdy-avatar.h b/subprojects/libhandy/src/hdy-avatar.h
new file mode 100644
index 0000000..54f3787
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-avatar.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_AVATAR (hdy_avatar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyAvatar, hdy_avatar, HDY, AVATAR, GtkDrawingArea)
+
+/**
+ * HdyAvatarImageLoadFunc:
+ * @size: the required size of the avatar
+ * @user_data: (closure): user data
+ *
+ * The returned #GdkPixbuf is expected to be square with width and height set
+ * to @size. The image is cropped to a circle without any scaling or transformation.
+ *
+ * Returns: (nullable) (transfer full): the #GdkPixbuf to use as a custom avatar
+ * or %NULL to fallback to the generated avatar.
+ */
+typedef GdkPixbuf *(*HdyAvatarImageLoadFunc) (gint size,
+ gpointer user_data);
+
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_avatar_new (gint size,
+ const gchar *text,
+ gboolean show_initials);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_avatar_get_icon_name (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_icon_name (HdyAvatar *self,
+ const gchar *icon_name);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_avatar_get_text (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_text (HdyAvatar *self,
+ const gchar *text);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_avatar_get_show_initials (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_show_initials (HdyAvatar *self,
+ gboolean show_initials);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_image_load_func (HdyAvatar *self,
+ HdyAvatarImageLoadFunc load_image,
+ gpointer user_data,
+ GDestroyNotify destroy);
+HDY_AVAILABLE_IN_ALL
+gint hdy_avatar_get_size (HdyAvatar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_avatar_set_size (HdyAvatar *self,
+ gint size);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-cairo-private.h b/subprojects/libhandy/src/hdy-cairo-private.h
new file mode 100644
index 0000000..d064f04
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-cairo-private.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib.h>
+#include <cairo/cairo.h>
+
+G_BEGIN_DECLS
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_t, cairo_destroy)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_surface_t, cairo_surface_destroy)
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-box-private.h b/subprojects/libhandy/src/hdy-carousel-box-private.h
new file mode 100644
index 0000000..98d3435
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-box-private.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_BOX (hdy_carousel_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyCarouselBox, hdy_carousel_box, HDY, CAROUSEL_BOX, GtkContainer)
+
+GtkWidget *hdy_carousel_box_new (void);
+
+void hdy_carousel_box_insert (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position);
+void hdy_carousel_box_reorder (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position);
+
+gboolean hdy_carousel_box_is_animating (HdyCarouselBox *self);
+void hdy_carousel_box_stop_animation (HdyCarouselBox *self);
+
+void hdy_carousel_box_scroll_to (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint64 duration);
+
+guint hdy_carousel_box_get_n_pages (HdyCarouselBox *self);
+gdouble hdy_carousel_box_get_distance (HdyCarouselBox *self);
+
+gdouble hdy_carousel_box_get_position (HdyCarouselBox *self);
+void hdy_carousel_box_set_position (HdyCarouselBox *self,
+ gdouble position);
+
+guint hdy_carousel_box_get_spacing (HdyCarouselBox *self);
+void hdy_carousel_box_set_spacing (HdyCarouselBox *self,
+ guint spacing);
+
+guint hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self);
+void hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self,
+ guint reveal_duration);
+
+GtkWidget *hdy_carousel_box_get_nth_child (HdyCarouselBox *self,
+ guint n);
+
+gdouble *hdy_carousel_box_get_snap_points (HdyCarouselBox *self,
+ gint *n_snap_points);
+void hdy_carousel_box_get_range (HdyCarouselBox *self,
+ gdouble *lower,
+ gdouble *upper);
+gdouble hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self);
+GtkWidget *hdy_carousel_box_get_page_at_position (HdyCarouselBox *self,
+ gdouble position);
+gint hdy_carousel_box_get_current_page_index (HdyCarouselBox *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-box.c b/subprojects/libhandy/src/hdy-carousel-box.c
new file mode 100644
index 0000000..1e0355f
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-box.c
@@ -0,0 +1,1768 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-carousel-box-private.h"
+
+#include <math.h>
+
+/**
+ * PRIVATE:hdy-carousel-box
+ * @short_description: Scrolling box used in #HdyCarousel
+ * @title: HdyCarouselBox
+ * @See_also: #HdyCarousel
+ * @stability: Private
+ *
+ * The #HdyCarouselBox object is meant to be used exclusively as part of the
+ * #HdyCarousel implementation.
+ *
+ * Since: 1.0
+ */
+
+typedef struct _HdyCarouselBoxAnimation HdyCarouselBoxAnimation;
+
+struct _HdyCarouselBoxAnimation
+{
+ gint64 start_time;
+ gint64 end_time;
+ gdouble start_value;
+ gdouble end_value;
+};
+
+typedef struct _HdyCarouselBoxChildInfo HdyCarouselBoxChildInfo;
+
+struct _HdyCarouselBoxChildInfo
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ gint position;
+ gboolean visible;
+ gdouble size;
+ gdouble snap_point;
+ gboolean adding;
+ gboolean removing;
+
+ gboolean shift_position;
+ HdyCarouselBoxAnimation resize_animation;
+
+ cairo_surface_t *surface;
+ cairo_region_t *dirty_region;
+};
+
+struct _HdyCarouselBox
+{
+ GtkContainer parent_instance;
+
+ HdyCarouselBoxAnimation animation;
+ HdyCarouselBoxChildInfo *destination_child;
+ GList *children;
+
+ gint child_width;
+ gint child_height;
+
+ gdouble distance;
+ gdouble position;
+ guint spacing;
+ GtkOrientation orientation;
+ guint reveal_duration;
+
+ guint tick_cb_id;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselBox, hdy_carousel_box, GTK_TYPE_CONTAINER,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+ PROP_0,
+ PROP_N_PAGES,
+ PROP_POSITION,
+ PROP_SPACING,
+ PROP_REVEAL_DURATION,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_REVEAL_DURATION + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_ANIMATION_STOPPED,
+ SIGNAL_POSITION_SHIFTED,
+ SIGNAL_LAST_SIGNAL,
+};
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static HdyCarouselBoxChildInfo *
+find_child_info (HdyCarouselBox *self,
+ GtkWidget *widget)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (widget == info->widget)
+ return info;
+ }
+
+ return NULL;
+}
+
+static gint
+find_child_index (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gboolean count_removing)
+{
+ GList *l;
+ gint i;
+
+ i = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->removing && !count_removing)
+ continue;
+
+ if (widget == info->widget)
+ return i;
+
+ i++;
+ }
+
+ return -1;
+}
+
+static GList *
+get_nth_link (HdyCarouselBox *self,
+ gint n)
+{
+
+ GList *l;
+ gint i;
+
+ i = n;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->removing)
+ continue;
+
+ if (i-- == 0)
+ return l;
+ }
+
+ return NULL;
+}
+
+static HdyCarouselBoxChildInfo *
+find_child_info_by_window (HdyCarouselBox *self,
+ GdkWindow *window)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (window == info->window)
+ return info;
+ }
+
+ return NULL;
+}
+
+static HdyCarouselBoxChildInfo *
+get_closest_child_at (HdyCarouselBox *self,
+ gdouble position,
+ gboolean count_adding,
+ gboolean count_removing)
+{
+ GList *l;
+ HdyCarouselBoxChildInfo *closest_child = NULL;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (child->adding && !count_adding)
+ continue;
+
+ if (child->removing && !count_removing)
+ continue;
+
+ if (!closest_child ||
+ ABS (closest_child->snap_point - position) >
+ ABS (child->snap_point - position))
+ closest_child = child;
+ }
+
+ return closest_child;
+}
+
+static void
+free_child_info (HdyCarouselBoxChildInfo *info)
+{
+ if (info->surface)
+ cairo_surface_destroy (info->surface);
+ if (info->dirty_region)
+ cairo_region_destroy (info->dirty_region);
+ g_free (info);
+}
+
+static void
+invalidate_handler_cb (GdkWindow *window,
+ cairo_region_t *region)
+{
+ gpointer user_data;
+ HdyCarouselBox *self;
+ HdyCarouselBoxChildInfo *info;
+
+ gdk_window_get_user_data (window, &user_data);
+ g_assert (HDY_IS_CAROUSEL_BOX (user_data));
+ self = HDY_CAROUSEL_BOX (user_data);
+
+ info = find_child_info_by_window (self, window);
+
+ if (!info->dirty_region)
+ info->dirty_region = cairo_region_create ();
+
+ cairo_region_union (info->dirty_region, region);
+}
+
+static void
+register_window (HdyCarouselBoxChildInfo *info,
+ HdyCarouselBox *self)
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ GdkWindowAttr attributes;
+ GtkAllocation allocation;
+ gint attributes_mask;
+
+ if (info->removing)
+ return;
+
+ widget = GTK_WIDGET (self);
+ gtk_widget_get_allocation (info->widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, window);
+ gtk_widget_set_parent_window (info->widget, window);
+
+ gdk_window_set_user_data (window, self);
+
+ gdk_window_show (window);
+
+ info->window = window;
+
+ gdk_window_set_invalidate_handler (window, invalidate_handler_cb);
+}
+
+static void
+unregister_window (HdyCarouselBoxChildInfo *info,
+ HdyCarouselBox *self)
+{
+ if (!info->widget)
+ return;
+
+ gtk_widget_set_parent_window (info->widget, NULL);
+ gtk_widget_unregister_window (GTK_WIDGET (self), info->window);
+ gdk_window_destroy (info->window);
+ info->window = NULL;
+}
+
+static gdouble
+get_animation_value (HdyCarouselBoxAnimation *animation,
+ GdkFrameClock *frame_clock)
+{
+ gint64 frame_time, duration;
+ gdouble t;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+ frame_time = MIN (frame_time, animation->end_time);
+
+ duration = animation->end_time - animation->start_time;
+ t = (gdouble) (frame_time - animation->start_time) / duration;
+ t = hdy_ease_out_cubic (t);
+
+ return hdy_lerp (animation->start_value, animation->end_value, t);
+}
+
+static gboolean
+animate_position (HdyCarouselBox *self,
+ GdkFrameClock *frame_clock)
+{
+ gint64 frame_time;
+ gdouble value;
+
+ if (!hdy_carousel_box_is_animating (self))
+ return G_SOURCE_REMOVE;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ self->animation.end_value = self->destination_child->snap_point;
+ value = get_animation_value (&self->animation, frame_clock);
+ hdy_carousel_box_set_position (self, value);
+
+ if (frame_time >= self->animation.end_time) {
+ self->animation.start_time = 0;
+ self->animation.end_time = 0;
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void update_windows (HdyCarouselBox *self);
+
+static void
+complete_child_animation (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ update_windows (self);
+
+ if (child->adding)
+ child->adding = FALSE;
+
+ if (child->removing) {
+ self->children = g_list_remove (self->children, child);
+
+ free_child_info (child);
+ }
+}
+
+static gboolean
+animate_child_size (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child,
+ GdkFrameClock *frame_clock,
+ gdouble *delta)
+{
+ gint64 frame_time;
+ gdouble d, new_value;
+
+ if (child->resize_animation.start_time == 0)
+ return G_SOURCE_REMOVE;
+
+ new_value = get_animation_value (&child->resize_animation, frame_clock);
+ d = new_value - child->size;
+
+ child->size += d;
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (delta)
+ *delta = d;
+
+ if (frame_time >= child->resize_animation.end_time) {
+ child->resize_animation.start_time = 0;
+ child->resize_animation.end_time = 0;
+ complete_child_animation (self, child);
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+set_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ gdouble lower, upper;
+
+ hdy_carousel_box_get_range (self, &lower, &upper);
+
+ position = CLAMP (position, lower, upper);
+
+ self->position = position;
+ update_windows (self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ g_autoptr (GList) children = NULL;
+ GList *l;
+ gboolean should_continue;
+ gdouble position_shift;
+
+ should_continue = G_SOURCE_REMOVE;
+
+ position_shift = 0;
+
+ children = g_list_copy (self->children);
+ for (l = children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+ gdouble delta;
+ gboolean shift;
+
+ delta = 0;
+ shift = child->shift_position;
+
+ should_continue |= animate_child_size (self, child, frame_clock, &delta);
+
+ if (shift)
+ position_shift += delta;
+ }
+
+ update_windows (self);
+
+ if (position_shift != 0) {
+ set_position (self, self->position + position_shift);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, position_shift);
+ }
+
+ should_continue |= animate_position (self, frame_clock);
+
+ update_windows (self);
+
+ if (!should_continue)
+ self->tick_cb_id = 0;
+
+ return should_continue;
+}
+
+static void
+update_shift_position_flag (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ HdyCarouselBoxChildInfo *closest_child;
+ gint animating_index, closest_index;
+
+ /* We want to still shift position when the active child is being removed */
+ closest_child = get_closest_child_at (self, self->position, FALSE, TRUE);
+
+ if (!closest_child)
+ return;
+
+ animating_index = g_list_index (self->children, child);
+ closest_index = g_list_index (self->children, closest_child);
+
+ child->shift_position = (closest_index >= animating_index);
+}
+
+static void
+animate_child (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child,
+ gdouble value,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (child->resize_animation.start_time > 0) {
+ child->resize_animation.start_time = 0;
+ child->resize_animation.end_time = 0;
+ }
+
+ update_shift_position_flag (self, child);
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)) ||
+ duration <= 0 ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gdouble delta = value - child->size;
+
+ child->size = value;
+
+ if (child->shift_position) {
+ set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+ }
+
+ complete_child_animation (self, child);
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gdouble delta = value - child->size;
+
+ child->size = value;
+
+ if (child->shift_position) {
+ set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+ }
+
+ complete_child_animation (self, child);
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ child->resize_animation.start_value = child->size;
+ child->resize_animation.end_value = value;
+
+ child->resize_animation.start_time = frame_time / 1000;
+ child->resize_animation.end_time = child->resize_animation.start_time + duration;
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL);
+}
+
+static gboolean
+hdy_carousel_box_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ if (info->adding || info->removing)
+ continue;
+
+ if (!info->visible)
+ continue;
+
+ if (info->dirty_region && !info->removing) {
+ g_autoptr (cairo_t) surface_cr = NULL;
+ GtkAllocation child_alloc;
+
+ if (!info->surface) {
+ gint width, height;
+
+ width = gdk_window_get_width (info->window);
+ height = gdk_window_get_height (info->window);
+
+ info->surface = gdk_window_create_similar_surface (info->window,
+ CAIRO_CONTENT_COLOR_ALPHA,
+ width, height);
+ }
+
+ gtk_widget_get_allocation (info->widget, &child_alloc);
+
+ surface_cr = cairo_create (info->surface);
+
+ gdk_cairo_region (surface_cr, info->dirty_region);
+ cairo_clip (surface_cr);
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ cairo_translate (surface_cr, 0, -info->position);
+ else
+ cairo_translate (surface_cr, -info->position, 0);
+
+ cairo_save (surface_cr);
+ cairo_set_source_rgba (surface_cr, 0, 0, 0, 0);
+ cairo_set_operator (surface_cr, CAIRO_OPERATOR_SOURCE);
+ cairo_paint (surface_cr);
+ cairo_restore (surface_cr);
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self), info->widget, surface_cr);
+
+ cairo_region_destroy (info->dirty_region);
+ info->dirty_region = NULL;
+ }
+
+ if (!info->surface)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ cairo_set_source_surface (cr, info->surface, 0, info->position);
+ else
+ cairo_set_source_surface (cr, info->surface, info->position, 0);
+ cairo_paint (cr);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ GList *children;
+
+ if (minimum)
+ *minimum = 0;
+ if (natural)
+ *natural = 0;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+ if (natural_baseline)
+ *natural_baseline = -1;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ gint child_min, child_nat;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ if (for_size < 0)
+ gtk_widget_get_preferred_height (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat);
+ } else {
+ if (for_size < 0)
+ gtk_widget_get_preferred_width (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat);
+ }
+
+ if (minimum)
+ *minimum = MAX (*minimum, child_min);
+ if (natural)
+ *natural = MAX (*natural, child_nat);
+ }
+}
+
+static void
+hdy_carousel_box_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_width_for_height (GtkWidget *widget,
+ gint for_height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ measure (widget, GTK_ORIENTATION_HORIZONTAL, for_height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_box_get_preferred_height_for_width (GtkWidget *widget,
+ gint for_width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ measure (widget, GTK_ORIENTATION_VERTICAL, for_width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+invalidate_cache_for_child (HdyCarouselBox *self,
+ HdyCarouselBoxChildInfo *child)
+{
+ cairo_rectangle_int_t rect;
+
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = self->child_width;
+ rect.height = self->child_height;
+
+ if (child->surface)
+ g_clear_pointer (&child->surface, cairo_surface_destroy);
+
+ if (child->dirty_region)
+ cairo_region_destroy (child->dirty_region);
+ child->dirty_region = cairo_region_create_rectangle (&rect);
+}
+
+static void
+invalidate_drawing_cache (HdyCarouselBox *self)
+{
+ GList *l;
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child_info = l->data;
+
+ invalidate_cache_for_child (self, child_info);
+ }
+}
+
+static void
+update_windows (HdyCarouselBox *self)
+{
+ GList *children;
+ GtkAllocation alloc;
+ gdouble x, y, offset;
+ gboolean is_rtl;
+ gdouble snap_point;
+
+ snap_point = 0;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ child_info->snap_point = snap_point + child_info->size - 1;
+
+ snap_point += child_info->size;
+ }
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+ return;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ x = alloc.x;
+ y = alloc.y;
+
+ is_rtl = (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL);
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ offset = (self->distance * self->position) - (alloc.height - self->child_height) / 2.0;
+ else if (is_rtl)
+ offset = -(self->distance * self->position) + (alloc.width - self->child_width) / 2.0;
+ else
+ offset = (self->distance * self->position) - (alloc.width - self->child_width) / 2.0;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ y -= offset;
+ else
+ x -= offset;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ if (!child_info->removing) {
+ if (!gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL) {
+ child_info->position = y;
+ child_info->visible = child_info->position < alloc.height &&
+ child_info->position + self->child_height > 0;
+ gdk_window_move (child_info->window, alloc.x, alloc.y + child_info->position);
+ } else {
+ child_info->position = x;
+ child_info->visible = child_info->position < alloc.width &&
+ child_info->position + self->child_width > 0;
+ gdk_window_move (child_info->window, alloc.x + child_info->position, alloc.y);
+ }
+
+ if (!child_info->visible)
+ invalidate_cache_for_child (self, child_info);
+ }
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL)
+ y += self->distance * child_info->size;
+ else if (is_rtl)
+ x -= self->distance * child_info->size;
+ else
+ x += self->distance * child_info->size;
+ }
+}
+
+static void
+hdy_carousel_box_map (GtkWidget *widget)
+{
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->map (widget);
+
+ gtk_widget_queue_draw (GTK_WIDGET (widget));
+}
+
+static void
+hdy_carousel_box_realize (GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->realize (widget);
+
+ g_list_foreach (self->children, (GFunc) register_window, self);
+
+ gtk_widget_queue_allocate (widget);
+}
+
+static void
+hdy_carousel_box_unrealize (GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+
+ g_list_foreach (self->children, (GFunc) unregister_window, self);
+
+ GTK_WIDGET_CLASS (hdy_carousel_box_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_carousel_box_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (widget);
+ gint size, width, height;
+ GList *children;
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ size = 0;
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ gint min, nat;
+ gint child_size;
+
+ if (child_info->removing)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gtk_widget_get_preferred_width_for_height (child, allocation->height,
+ &min, &nat);
+ if (gtk_widget_get_hexpand (child))
+ child_size = MAX (min, allocation->width);
+ else
+ child_size = MAX (min, nat);
+ } else {
+ gtk_widget_get_preferred_height_for_width (child, allocation->width,
+ &min, &nat);
+ if (gtk_widget_get_vexpand (child))
+ child_size = MAX (min, allocation->height);
+ else
+ child_size = MAX (min, nat);
+ }
+
+ size = MAX (size, child_size);
+ }
+
+ self->distance = size + self->spacing;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ width = size;
+ height = allocation->height;
+ } else {
+ width = allocation->width;
+ height = size;
+ }
+
+ if (width != self->child_width || height != self->child_height)
+ invalidate_drawing_cache (self);
+
+ self->child_width = width;
+ self->child_height = height;
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ if (!gtk_widget_get_realized (GTK_WIDGET (self)))
+ continue;
+
+ gdk_window_resize (child_info->window, width, height);
+ }
+
+ update_windows (self);
+
+ for (children = self->children; children; children = children->next) {
+ HdyCarouselBoxChildInfo *child_info = children->data;
+ GtkWidget *child = child_info->widget;
+ GtkAllocation alloc;
+
+ if (child_info->removing)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = width;
+ alloc.height = height;
+ gtk_widget_size_allocate (child, &alloc);
+ }
+
+ invalidate_drawing_cache (self);
+ gtk_widget_set_clip (widget, allocation);
+}
+
+static void
+hdy_carousel_box_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+
+ hdy_carousel_box_insert (self, widget, -1);
+}
+
+static void
+shift_position (HdyCarouselBox *self,
+ gdouble delta)
+{
+ hdy_carousel_box_set_position (self, self->position + delta);
+ g_signal_emit (self, signals[SIGNAL_POSITION_SHIFTED], 0, delta);
+}
+
+static void
+hdy_carousel_box_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+ HdyCarouselBoxChildInfo *info;
+
+ info = find_child_info (self, widget);
+ if (!info)
+ return;
+
+ info->removing = TRUE;
+
+ gtk_widget_unparent (widget);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (container)))
+ unregister_window (info, self);
+
+ info->widget = NULL;
+
+ if (!gtk_widget_in_destruction (GTK_WIDGET (container)))
+ animate_child (self, info, 0, self->reveal_duration);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+hdy_carousel_box_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (container);
+ g_autoptr (GList) children = NULL;
+ GList *l;
+
+ children = g_list_copy (self->children);
+ for (l = children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (!child->removing)
+ (* callback) (child->widget, callback_data);
+ }
+}
+
+static void
+hdy_carousel_box_finalize (GObject *object)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ if (self->tick_cb_id > 0)
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+
+ g_list_free_full (self->children, (GDestroyNotify) free_child_info);
+
+ G_OBJECT_CLASS (hdy_carousel_box_parent_class)->finalize (object);
+}
+
+static void
+hdy_carousel_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ switch (prop_id) {
+ case PROP_N_PAGES:
+ g_value_set_uint (value, hdy_carousel_box_get_n_pages (self));
+ break;
+
+ case PROP_POSITION:
+ g_value_set_double (value, hdy_carousel_box_get_position (self));
+ break;
+
+ case PROP_SPACING:
+ g_value_set_uint (value, hdy_carousel_box_get_spacing (self));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ g_value_set_uint (value, hdy_carousel_box_get_reveal_duration (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselBox *self = HDY_CAROUSEL_BOX (object);
+
+ switch (prop_id) {
+ case PROP_POSITION:
+ hdy_carousel_box_set_position (self, g_value_get_double (value));
+ break;
+
+ case PROP_SPACING:
+ hdy_carousel_box_set_spacing (self, g_value_get_uint (value));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ hdy_carousel_box_set_reveal_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_box_class_init (HdyCarouselBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_carousel_box_finalize;
+ object_class->get_property = hdy_carousel_box_get_property;
+ object_class->set_property = hdy_carousel_box_set_property;
+ widget_class->draw = hdy_carousel_box_draw;
+ widget_class->get_preferred_width = hdy_carousel_box_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_box_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_carousel_box_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_carousel_box_get_preferred_height_for_width;
+ widget_class->map = hdy_carousel_box_map;
+ widget_class->realize = hdy_carousel_box_realize;
+ widget_class->unrealize = hdy_carousel_box_unrealize;
+ widget_class->size_allocate = hdy_carousel_box_size_allocate;
+ container_class->add = hdy_carousel_box_add;
+ container_class->remove = hdy_carousel_box_remove;
+ container_class->forall = hdy_carousel_box_forall;
+
+ /**
+ * HdyCarouselBox:n-pages:
+ *
+ * The number of pages in a #HdyCarouselBox
+ *
+ * Since: 1.0
+ */
+ props[PROP_N_PAGES] =
+ g_param_spec_uint ("n-pages",
+ _("Number of pages"),
+ _("Number of pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:position:
+ *
+ * Current scrolling position, unitless. 1 matches 1 page.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POSITION] =
+ g_param_spec_double ("position",
+ _("Position"),
+ _("Current scrolling position"),
+ 0,
+ G_MAXDOUBLE,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:spacing:
+ *
+ * Spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SPACING] =
+ g_param_spec_uint ("spacing",
+ _("Spacing"),
+ _("Spacing between pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarouselBox:reveal-duration:
+ *
+ * Duration of the animation used when adding or removing pages, in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVEAL_DURATION] =
+ g_param_spec_uint ("reveal-duration",
+ _("Reveal duration"),
+ _("Page reveal duration"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyCarouselBox::animation-stopped:
+ * @self: The #HdyCarouselBox instance
+ *
+ * This signal is emitted after an animation has been stopped. If animations
+ * are disabled, the signal is emitted as well.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_ANIMATION_STOPPED] =
+ g_signal_new ("animation-stopped",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ /**
+ * HdyCarouselBox::position-shifted:
+ * @self: The #HdyCarouselBox instance
+ * @delta: The amount to shift the position by
+ *
+ * This signal is emitted when position has been programmatically shifted.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_POSITION_SHIFTED] =
+ g_signal_new ("position-shifted",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_DOUBLE);
+}
+
+static void
+hdy_carousel_box_init (HdyCarouselBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ self->orientation = GTK_ORIENTATION_HORIZONTAL;
+ self->reveal_duration = 0;
+
+ gtk_widget_set_has_window (widget, FALSE);
+}
+
+/**
+ * hdy_carousel_box_new:
+ *
+ * Create a new #HdyCarouselBox widget.
+ *
+ * Returns: The newly created #HdyCarouselBox widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_BOX, NULL);
+}
+
+/**
+ * hdy_carousel_box_insert:
+ * @self: a #HdyCarouselBox
+ * @widget: a widget to add
+ * @position: the position to insert @widget in.
+ *
+ * Inserts @widget into @self at position @position.
+ *
+ * If position is -1, or larger than the number of pages, @widget will be
+ * appended to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_insert (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyCarouselBoxChildInfo *info;
+ GList *prev_link;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ info = g_new0 (HdyCarouselBoxChildInfo, 1);
+ info->widget = widget;
+ info->size = 0;
+ info->adding = TRUE;
+
+ if (gtk_widget_get_realized (GTK_WIDGET (self)))
+ register_window (info, self);
+
+ if (position >= 0)
+ prev_link = get_nth_link (self, position);
+ else
+ prev_link = NULL;
+
+ self->children = g_list_insert_before (self->children, prev_link, info);
+
+ gtk_widget_set_parent (widget, GTK_WIDGET (self));
+
+ update_windows (self);
+
+ animate_child (self, info, 1, self->reveal_duration);
+
+ invalidate_drawing_cache (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+/**
+ * hdy_carousel_box_reorder:
+ * @self: a #HdyCarouselBox
+ * @widget: a widget to add
+ * @position: the position to move @widget to.
+ *
+ * Moves @widget into position @position.
+ *
+ * If position is -1, or larger than the number of pages, @widget will be moved
+ * to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_reorder (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyCarouselBoxChildInfo *info, *prev_info;
+ GList *link, *prev_link;
+ gint old_position;
+ gdouble closest_point, old_point, new_point;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ closest_point = hdy_carousel_box_get_closest_snap_point (self);
+
+ info = find_child_info (self, widget);
+ link = g_list_find (self->children, info);
+ old_position = g_list_position (self->children, link);
+
+ if (position == old_position)
+ return;
+
+ old_point = ((HdyCarouselBoxChildInfo *) link->data)->snap_point;
+
+ if (position < 0 || position >= hdy_carousel_box_get_n_pages (self))
+ prev_link = g_list_last (self->children);
+ else
+ prev_link = get_nth_link (self, position);
+
+ prev_info = prev_link->data;
+ new_point = prev_info->snap_point;
+ if (new_point > old_point)
+ new_point -= prev_info->size;
+
+ self->children = g_list_remove_link (self->children, link);
+ self->children = g_list_insert_before (self->children, prev_link, link->data);
+
+ if (closest_point == old_point)
+ shift_position (self, new_point - old_point);
+ else if (old_point > closest_point && closest_point >= new_point)
+ shift_position (self, info->size);
+ else if (new_point >= closest_point && closest_point > old_point)
+ shift_position (self, -info->size);
+}
+
+/**
+ * hdy_carousel_box_is_animating:
+ * @self: a #HdyCarouselBox
+ *
+ * Get whether @self is animating position.
+ *
+ * Returns: %TRUE if an animation is running
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_box_is_animating (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), FALSE);
+
+ return (self->animation.start_time != 0);
+}
+
+/**
+ * hdy_carousel_box_stop_animation:
+ * @self: a #HdyCarouselBox
+ *
+ * Stops a running animation. If there's no animation running, does nothing.
+ *
+ * It does not reset position to a non-transient value automatically.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_stop_animation (HdyCarouselBox *self)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->animation.start_time == 0)
+ return;
+
+ self->animation.start_time = 0;
+ self->animation.end_time = 0;
+}
+
+/**
+ * hdy_carousel_box_scroll_to:
+ * @self: a #HdyCarouselBox
+ * @widget: a child of @self
+ * @duration: animation duration in milliseconds
+ *
+ * Scrolls to @widget position over the next @duration milliseconds using
+ * easeOutCubic interpolator.
+ *
+ * If an animation was already running, it will be cancelled automatically.
+ *
+ * @duration can be 0, in that case the position will be
+ * changed immediately.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_scroll_to (HdyCarouselBox *self,
+ GtkWidget *widget,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+ gdouble position;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+ g_return_if_fail (duration >= 0);
+
+ child = find_child_info (self, widget);
+ position = child->snap_point;
+
+ hdy_carousel_box_stop_animation (self);
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ hdy_carousel_box_set_position (self, position);
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ hdy_carousel_box_set_position (self, position);
+ g_signal_emit (self, signals[SIGNAL_ANIMATION_STOPPED], 0);
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->destination_child = child;
+
+ self->animation.start_value = self->position;
+ self->animation.end_value = position;
+
+ self->animation.start_time = frame_time / 1000;
+ self->animation.end_time = self->animation.start_time + duration;
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), animation_cb, self, NULL);
+}
+
+/**
+ * hdy_carousel_box_get_n_pages:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the number of pages in @self.
+ *
+ * Returns: The number of pages in @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_n_pages (HdyCarouselBox *self)
+{
+ GList *l;
+ guint n_pages;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ n_pages = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (!child->removing)
+ n_pages++;
+ }
+
+ return n_pages;
+}
+
+/**
+ * hdy_carousel_box_get_distance:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets swiping distance between two adjacent children in pixels.
+ *
+ * Returns: The swiping distance in pixels
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_distance (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->distance;
+}
+
+/**
+ * hdy_carousel_box_get_position:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets current scroll position in @self. It's unitless, 1 matches 1 page.
+ *
+ * Returns: The scroll position
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_position (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->position;
+}
+
+/**
+ * hdy_carousel_box_set_position:
+ * @self: a #HdyCarouselBox
+ * @position: the new position value
+ *
+ * Sets current scroll position in @self, unitless, 1 matches 1 page.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ GList *l;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ set_position (self, position);
+
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *child = l->data;
+
+ if (child->adding || child->removing)
+ update_shift_position_flag (self, child);
+ }
+}
+
+/**
+ * hdy_carousel_box_get_spacing:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets spacing between pages in pixels.
+ *
+ * Returns: Spacing between pages
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_spacing (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->spacing;
+}
+
+/**
+ * hdy_carousel_box_set_spacing:
+ * @self: a #HdyCarouselBox
+ * @spacing: the new spacing value
+ *
+ * Sets spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_spacing (HdyCarouselBox *self,
+ guint spacing)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->spacing == spacing)
+ return;
+
+ self->spacing = spacing;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+/**
+ * hdy_carousel_box_get_reveal_duration:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Returns: Page reveal duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_box_get_reveal_duration (HdyCarouselBox *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ return self->reveal_duration;
+}
+
+/**
+ * hdy_carousel_box_set_reveal_duration:
+ * @self: a #HdyCarouselBox
+ * @reveal_duration: the new reveal duration value
+ *
+ * Sets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_set_reveal_duration (HdyCarouselBox *self,
+ guint reveal_duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ if (self->reveal_duration == reveal_duration)
+ return;
+
+ self->reveal_duration = reveal_duration;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]);
+}
+
+/**
+ * hdy_carousel_box_get_nth_child:
+ * @self: a #HdyCarouselBox
+ * @n: the child index
+ *
+ * Retrieves @n-th child widget of @self.
+ *
+ * Returns: The @n-th child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_get_nth_child (HdyCarouselBox *self,
+ guint n)
+{
+ HdyCarouselBoxChildInfo *info;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+ g_return_val_if_fail (n < hdy_carousel_box_get_n_pages (self), NULL);
+
+ info = get_nth_link (self, n)->data;
+
+ return info->widget;
+}
+
+/**
+ * hdy_carousel_box_get_snap_points:
+ * @self: a #HdyCarouselBox
+ * @n_snap_points: (out)
+ *
+ * Gets the snap points of @self, representing the points between each page,
+ * before the first page and after the last page.
+ *
+ * Returns: (array length=n_snap_points) (transfer full): the snap points of @self
+ *
+ * Since: 1.0
+ */
+gdouble *
+hdy_carousel_box_get_snap_points (HdyCarouselBox *self,
+ gint *n_snap_points)
+{
+ guint i, n_pages;
+ gdouble *points;
+ GList *l;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+
+ n_pages = MAX (g_list_length (self->children), 1);
+
+ points = g_new0 (gdouble, n_pages);
+
+ i = 0;
+ for (l = self->children; l; l = l->next) {
+ HdyCarouselBoxChildInfo *info = l->data;
+
+ points[i++] = info->snap_point;
+ }
+
+ if (n_snap_points)
+ *n_snap_points = n_pages;
+
+ return points;
+}
+
+/**
+ * hdy_carousel_box_get_range:
+ * @self: a #HdyCarouselBox
+ * @lower: (out) (optional): location to store the lowest possible position, or %NULL
+ * @upper: (out) (optional): location to store the maximum possible position, or %NULL
+ *
+ * Gets the range of possible positions.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_box_get_range (HdyCarouselBox *self,
+ gdouble *lower,
+ gdouble *upper)
+{
+ GList *l;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_if_fail (HDY_IS_CAROUSEL_BOX (self));
+
+ l = g_list_last (self->children);
+ child = l ? l->data : NULL;
+
+ if (lower)
+ *lower = 0;
+
+ if (upper)
+ *upper = child ? child->snap_point : 0;
+}
+
+/**
+ * hdy_carousel_box_get_closest_snap_point:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the snap point closest to the current position.
+ *
+ * Returns: the closest snap point.
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_box_get_closest_snap_point (HdyCarouselBox *self)
+{
+ HdyCarouselBoxChildInfo *closest_child;
+
+ closest_child = get_closest_child_at (self, self->position, TRUE, TRUE);
+
+ if (!closest_child)
+ return 0;
+
+ return closest_child->snap_point;
+}
+
+/**
+ * hdy_carousel_box_get_page_at_position:
+ * @self: a #HdyCarouselBox
+ * @position: a scroll position
+ *
+ * Gets the page closest to @position. For example, if @position matches
+ * the current position, the returned widget will match the currently
+ * displayed page.
+ *
+ * Returns: the closest page.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_box_get_page_at_position (HdyCarouselBox *self,
+ gdouble position)
+{
+ gdouble lower, upper;
+ HdyCarouselBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), NULL);
+
+ hdy_carousel_box_get_range (self, &lower, &upper);
+
+ position = CLAMP (position, lower, upper);
+
+ child = get_closest_child_at (self, position, TRUE, FALSE);
+
+ return child->widget;
+}
+
+/**
+ * hdy_carousel_box_get_current_page_index:
+ * @self: a #HdyCarouselBox
+ *
+ * Gets the index of the currently displayed page.
+ *
+ * Returns: the index of the current page.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_carousel_box_get_current_page_index (HdyCarouselBox *self)
+{
+ GtkWidget *child;
+
+ g_return_val_if_fail (HDY_IS_CAROUSEL_BOX (self), 0);
+
+ child = hdy_carousel_box_get_page_at_position (self, self->position);
+
+ return find_child_index (self, child, FALSE);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.c b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c
new file mode 100644
index 0000000..5bbc541
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.c
@@ -0,0 +1,486 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel-indicator-dots.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define DOTS_RADIUS 3
+#define DOTS_RADIUS_SELECTED 4
+#define DOTS_OPACITY 0.3
+#define DOTS_OPACITY_SELECTED 0.9
+#define DOTS_SPACING 7
+#define DOTS_MARGIN 6
+
+/**
+ * SECTION:hdy-carousel-indicator-dots
+ * @short_description: A dots indicator for #HdyCarousel
+ * @title: HdyCarouselIndicatorDots
+ * @See_also: #HdyCarousel, #HdyCarouselIndicatorLines
+ *
+ * The #HdyCarouselIndicatorDots widget can be used to show a set of dots for each
+ * page of a given #HdyCarousel. The dot representing the carousel's active page
+ * is larger and more opaque than the others, the transition to the active and
+ * inactive state is gradual to match the carousel's position.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarouselIndicatorDots has a single CSS node with name carouselindicatordots.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarouselIndicatorDots
+{
+ GtkDrawingArea parent_instance;
+
+ HdyCarousel *carousel;
+ GtkOrientation orientation;
+
+ guint tick_cb_id;
+ guint64 end_time;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, GTK_TYPE_DRAWING_AREA,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_CAROUSEL,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_CAROUSEL + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint64 frame_time;
+
+ g_assert (self->tick_cb_id > 0);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (frame_time >= self->end_time ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ self->tick_cb_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_animation (HdyCarouselIndicatorDots *self)
+{
+ if (self->tick_cb_id == 0)
+ return;
+
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+ self->tick_cb_id = 0;
+}
+
+static void
+animate (HdyCarouselIndicatorDots *self,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->end_time = MAX (self->end_time, frame_time / 1000 + duration);
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ animation_cb,
+ NULL, NULL);
+}
+
+static GdkRGBA
+get_color (GtkWidget *widget)
+{
+ GtkStyleContext *context;
+ GtkStateFlags flags;
+ GdkRGBA color;
+
+ context = gtk_widget_get_style_context (widget);
+ flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_color (context, flags, &color);
+
+ return color;
+}
+
+static void
+draw_dots (GtkWidget *widget,
+ cairo_t *cr,
+ GtkOrientation orientation,
+ gdouble position,
+ gdouble *sizes,
+ guint n_pages)
+{
+ GdkRGBA color;
+ gint i, widget_length, widget_thickness;
+ gdouble x, y, indicator_length, dot_size, full_size;
+ gdouble current_position, remaining_progress;
+
+ color = get_color (widget);
+ dot_size = 2 * DOTS_RADIUS_SELECTED + DOTS_SPACING;
+
+ indicator_length = 0;
+ for (i = 0; i < n_pages; i++)
+ indicator_length += dot_size * sizes[i];
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ widget_length = gtk_widget_get_allocated_width (widget);
+ widget_thickness = gtk_widget_get_allocated_height (widget);
+ } else {
+ widget_length = gtk_widget_get_allocated_height (widget);
+ widget_thickness = gtk_widget_get_allocated_width (widget);
+ }
+
+ /* Ensure the indicators are aligned to pixel grid when not animating */
+ full_size = round (indicator_length / dot_size) * dot_size;
+ if ((widget_length - (gint) full_size) % 2 == 0)
+ widget_length--;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_translate (cr, (widget_length - indicator_length) / 2.0, widget_thickness / 2);
+ else
+ cairo_translate (cr, widget_thickness / 2, (widget_length - indicator_length) / 2.0);
+
+ x = 0;
+ y = 0;
+
+ current_position = 0;
+ remaining_progress = 1;
+
+ for (i = 0; i < n_pages; i++) {
+ gdouble progress, radius, opacity;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ x += dot_size * sizes[i] / 2.0;
+ else
+ y += dot_size * sizes[i] / 2.0;
+
+ current_position += sizes[i];
+
+ progress = CLAMP (current_position - position, 0, remaining_progress);
+ remaining_progress -= progress;
+
+ radius = hdy_lerp (DOTS_RADIUS, DOTS_RADIUS_SELECTED, progress) * sizes[i];
+ opacity = hdy_lerp (DOTS_OPACITY, DOTS_OPACITY_SELECTED, progress) * sizes[i];
+
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * opacity);
+ cairo_arc (cr, x, y, radius, 0, 2 * G_PI);
+ cairo_fill (cr);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ x += dot_size * sizes[i] / 2.0;
+ else
+ y += dot_size * sizes[i] / 2.0;
+ }
+}
+
+static void
+n_pages_changed_cb (HdyCarouselIndicatorDots *self)
+{
+ animate (self, hdy_carousel_get_reveal_duration (self->carousel));
+}
+
+static void
+hdy_carousel_indicator_dots_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint size = 0;
+
+ if (orientation == self->orientation) {
+ gint n_pages = 0;
+ if (self->carousel)
+ n_pages = hdy_carousel_get_n_pages (self->carousel);
+
+ size = MAX (0, (2 * DOTS_RADIUS_SELECTED + DOTS_SPACING) * n_pages - DOTS_SPACING);
+ } else {
+ size = 2 * DOTS_RADIUS_SELECTED;
+ }
+
+ size += 2 * DOTS_MARGIN;
+
+ if (minimum)
+ *minimum = size;
+
+ if (natural)
+ *natural = size;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_carousel_indicator_dots_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_indicator_dots_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_carousel_indicator_dots_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static gboolean
+hdy_carousel_indicator_dots_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (widget);
+ gint i, n_points;
+ gdouble position;
+ g_autofree gdouble *points = NULL;
+ g_autofree gdouble *sizes = NULL;
+
+ if (!self->carousel)
+ return GDK_EVENT_PROPAGATE;
+
+ points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points);
+ position = hdy_carousel_get_position (self->carousel);
+
+ if (n_points < 2)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ position = points[n_points - 1] - position;
+
+ sizes = g_new0 (gdouble, n_points);
+
+ sizes[0] = points[0] + 1;
+ for (i = 1; i < n_points; i++)
+ sizes[i] = points[i] - points[i - 1];
+
+ draw_dots (widget, cr, self->orientation, position, sizes, n_points);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ hdy_carousel_indicator_dots_set_carousel (self, NULL);
+
+ G_OBJECT_CLASS (hdy_carousel_indicator_dots_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_indicator_dots_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ g_value_set_object (value, hdy_carousel_indicator_dots_get_carousel (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_dots_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorDots *self = HDY_CAROUSEL_INDICATOR_DOTS (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ hdy_carousel_indicator_dots_set_carousel (self, g_value_get_object (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_dots_class_init (HdyCarouselIndicatorDotsClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_indicator_dots_get_property;
+ object_class->set_property = hdy_carousel_indicator_dots_set_property;
+
+ widget_class->get_preferred_width = hdy_carousel_indicator_dots_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_indicator_dots_get_preferred_height;
+ widget_class->draw = hdy_carousel_indicator_dots_draw;
+
+ /**
+ * HdyCarouselIndicatorDots:carousel:
+ *
+ * The #HdyCarousel the indicator uses.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAROUSEL] =
+ g_param_spec_object ("carousel",
+ _("Carousel"),
+ _("Carousel"),
+ HDY_TYPE_CAROUSEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "carouselindicatordots");
+}
+
+static void
+hdy_carousel_indicator_dots_init (HdyCarouselIndicatorDots *self)
+{
+}
+
+/**
+ * hdy_carousel_indicator_dots_new:
+ *
+ * Create a new #HdyCarouselIndicatorDots widget.
+ *
+ * Returns: (transfer full): The newly created #HdyCarouselIndicatorDots widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_indicator_dots_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_DOTS, NULL);
+}
+
+/**
+ * hdy_carousel_indicator_dots_get_carousel:
+ * @self: a #HdyCarouselIndicatorDots
+ *
+ * Get the #HdyCarousel the indicator uses.
+ *
+ * See: hdy_carousel_indicator_dots_set_carousel()
+ *
+ * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+
+HdyCarousel *
+hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self), NULL);
+
+ return self->carousel;
+}
+
+/**
+ * hdy_carousel_indicator_dots_set_carousel:
+ * @self: a #HdyCarouselIndicatorDots
+ * @carousel: (nullable): a #HdyCarousel
+ *
+ * Sets the #HdyCarousel to use.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self,
+ HdyCarousel *carousel)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_DOTS (self));
+ g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL);
+
+ if (self->carousel == carousel)
+ return;
+
+ if (self->carousel) {
+ stop_animation (self);
+ g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self);
+ g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self);
+ }
+
+ g_set_object (&self->carousel, carousel);
+
+ if (self->carousel) {
+ g_signal_connect_object (self->carousel, "notify::position",
+ G_CALLBACK (gtk_widget_queue_draw), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->carousel, "notify::n-pages",
+ G_CALLBACK (n_pages_changed_cb), self,
+ G_CONNECT_SWAPPED);
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-dots.h b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h
new file mode 100644
index 0000000..032886e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-dots.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-carousel.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_INDICATOR_DOTS (hdy_carousel_indicator_dots_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorDots, hdy_carousel_indicator_dots, HDY, CAROUSEL_INDICATOR_DOTS, GtkDrawingArea)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_indicator_dots_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyCarousel *hdy_carousel_indicator_dots_get_carousel (HdyCarouselIndicatorDots *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_indicator_dots_set_carousel (HdyCarouselIndicatorDots *self,
+ HdyCarousel *carousel);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.c b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c
new file mode 100644
index 0000000..fba9b38
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.c
@@ -0,0 +1,485 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel-indicator-lines.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define LINE_WIDTH 3
+#define LINE_LENGTH 35
+#define LINE_SPACING 5
+#define LINE_OPACITY 0.3
+#define LINE_OPACITY_ACTIVE 0.9
+#define LINE_MARGIN 2
+
+/**
+ * SECTION:hdy-carousel-indicator-lines
+ * @short_description: A lines indicator for #HdyCarousel
+ * @title: HdyCarouselIndicatorLines
+ * @See_also: #HdyCarousel, #HdyCarouselIndicatorDots
+ *
+ * The #HdyCarouselIndicatorLines widget can be used to show a set of thin and long
+ * rectangles for each page of a given #HdyCarousel. The carousel's active page
+ * is shown with another rectangle that moves between them to match the
+ * carousel's position.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarouselIndicatorLines has a single CSS node with name carouselindicatorlines.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarouselIndicatorLines
+{
+ GtkDrawingArea parent_instance;
+
+ HdyCarousel *carousel;
+ GtkOrientation orientation;
+
+ guint tick_cb_id;
+ guint64 end_time;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, GTK_TYPE_DRAWING_AREA,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_CAROUSEL,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_CAROUSEL + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+animation_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint64 frame_time;
+
+ g_assert (self->tick_cb_id > 0);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+
+ if (frame_time >= self->end_time ||
+ !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ self->tick_cb_id = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_animation (HdyCarouselIndicatorLines *self)
+{
+ if (self->tick_cb_id == 0)
+ return;
+
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_cb_id);
+ self->tick_cb_id = 0;
+}
+
+static void
+animate (HdyCarouselIndicatorLines *self,
+ gint64 duration)
+{
+ GdkFrameClock *frame_clock;
+ gint64 frame_time;
+
+ if (duration <= 0 || !hdy_get_enable_animations (GTK_WIDGET (self))) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+ if (!frame_clock) {
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ return;
+ }
+
+ frame_time = gdk_frame_clock_get_frame_time (frame_clock);
+
+ self->end_time = MAX (self->end_time, frame_time / 1000 + duration);
+ if (self->tick_cb_id == 0)
+ self->tick_cb_id = gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ animation_cb,
+ NULL, NULL);
+}
+
+static GdkRGBA
+get_color (GtkWidget *widget)
+{
+ GtkStyleContext *context;
+ GtkStateFlags flags;
+ GdkRGBA color;
+
+ context = gtk_widget_get_style_context (widget);
+ flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_color (context, flags, &color);
+
+ return color;
+}
+
+static void
+draw_lines (GtkWidget *widget,
+ cairo_t *cr,
+ GtkOrientation orientation,
+ gdouble position,
+ gdouble *sizes,
+ guint n_pages)
+{
+ GdkRGBA color;
+ gint i, widget_length, widget_thickness;
+ gdouble indicator_length, full_size, line_size, pos;
+
+ color = get_color (widget);
+
+ line_size = LINE_LENGTH + LINE_SPACING;
+ indicator_length = 0;
+ for (i = 0; i < n_pages; i++)
+ indicator_length += line_size * sizes[i];
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ widget_length = gtk_widget_get_allocated_width (widget);
+ widget_thickness = gtk_widget_get_allocated_height (widget);
+ } else {
+ widget_length = gtk_widget_get_allocated_height (widget);
+ widget_thickness = gtk_widget_get_allocated_width (widget);
+ }
+
+ /* Ensure the indicators are aligned to pixel grid when not animating */
+ full_size = round (indicator_length / line_size) * line_size;
+ if ((widget_length - (gint) full_size) % 2 == 0)
+ widget_length--;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ cairo_translate (cr, (widget_length - indicator_length) / 2.0, (widget_thickness - LINE_WIDTH) / 2);
+ cairo_scale (cr, 1, LINE_WIDTH);
+ } else {
+ cairo_translate (cr, (widget_thickness - LINE_WIDTH) / 2, (widget_length - indicator_length) / 2.0);
+ cairo_scale (cr, LINE_WIDTH, 1);
+ }
+
+ pos = 0;
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * LINE_OPACITY);
+ for (i = 0; i < n_pages; i++) {
+ gdouble length;
+
+ length = (LINE_LENGTH + LINE_SPACING) * sizes[i] - LINE_SPACING;
+
+ if (length > 0) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_rectangle (cr, LINE_SPACING / 2.0 + pos, 0, length, 1);
+ else
+ cairo_rectangle (cr, 0, LINE_SPACING / 2.0 + pos, 1, length);
+ }
+
+ cairo_fill (cr);
+
+ pos += (LINE_LENGTH + LINE_SPACING) * sizes[i];
+ }
+
+ cairo_set_source_rgba (cr, color.red, color.green, color.blue,
+ color.alpha * LINE_OPACITY_ACTIVE);
+
+ pos = LINE_SPACING / 2.0 + position * (LINE_LENGTH + LINE_SPACING);
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ cairo_rectangle (cr, pos, 0, LINE_LENGTH, 1);
+ else
+ cairo_rectangle (cr, 0, pos, 1, LINE_LENGTH);
+ cairo_fill (cr);
+}
+
+static void
+n_pages_changed_cb (HdyCarouselIndicatorLines *self)
+{
+ animate (self, hdy_carousel_get_reveal_duration (self->carousel));
+}
+
+static void
+hdy_carousel_indicator_lines_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint size = 0;
+
+ if (orientation == self->orientation) {
+ gint n_pages = 0;
+ if (self->carousel)
+ n_pages = hdy_carousel_get_n_pages (self->carousel);
+
+ size = MAX (0, (LINE_LENGTH + LINE_SPACING) * n_pages - LINE_SPACING);
+ } else {
+ size = LINE_WIDTH;
+ }
+
+ size += 2 * LINE_MARGIN;
+
+ if (minimum)
+ *minimum = size;
+
+ if (natural)
+ *natural = size;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_carousel_indicator_lines_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_carousel_indicator_lines_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_carousel_indicator_lines_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static gboolean
+hdy_carousel_indicator_lines_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (widget);
+ gint i, n_points;
+ gdouble position;
+ g_autofree gdouble *points = NULL;
+ g_autofree gdouble *sizes = NULL;
+
+ if (!self->carousel)
+ return GDK_EVENT_PROPAGATE;
+
+ points = hdy_swipeable_get_snap_points (HDY_SWIPEABLE (self->carousel), &n_points);
+ position = hdy_carousel_get_position (self->carousel);
+
+ if (n_points < 2)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+ position = points[n_points - 1] - position;
+
+ sizes = g_new0 (gdouble, n_points);
+
+ sizes[0] = points[0] + 1;
+ for (i = 1; i < n_points; i++)
+ sizes[i] = points[i] - points[i - 1];
+
+ draw_lines (widget, cr, self->orientation, position, sizes, n_points);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ hdy_carousel_indicator_lines_set_carousel (self, NULL);
+
+ G_OBJECT_CLASS (hdy_carousel_indicator_lines_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_indicator_lines_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ g_value_set_object (value, hdy_carousel_indicator_lines_get_carousel (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_lines_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarouselIndicatorLines *self = HDY_CAROUSEL_INDICATOR_LINES (object);
+
+ switch (prop_id) {
+ case PROP_CAROUSEL:
+ hdy_carousel_indicator_lines_set_carousel (self, g_value_get_object (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_indicator_lines_class_init (HdyCarouselIndicatorLinesClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_indicator_lines_get_property;
+ object_class->set_property = hdy_carousel_indicator_lines_set_property;
+
+ widget_class->get_preferred_width = hdy_carousel_indicator_lines_get_preferred_width;
+ widget_class->get_preferred_height = hdy_carousel_indicator_lines_get_preferred_height;
+ widget_class->draw = hdy_carousel_indicator_lines_draw;
+
+ /**
+ * HdyCarouselIndicatorLines:carousel:
+ *
+ * The #HdyCarousel the indicator uses.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAROUSEL] =
+ g_param_spec_object ("carousel",
+ _("Carousel"),
+ _("Carousel"),
+ HDY_TYPE_CAROUSEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "carouselindicatorlines");
+}
+
+static void
+hdy_carousel_indicator_lines_init (HdyCarouselIndicatorLines *self)
+{
+}
+
+/**
+ * hdy_carousel_indicator_lines_new:
+ *
+ * Create a new #HdyCarouselIndicatorLines widget.
+ *
+ * Returns: (transfer full): The newly created #HdyCarouselIndicatorLines widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_indicator_lines_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL_INDICATOR_LINES, NULL);
+}
+
+/**
+ * hdy_carousel_indicator_lines_get_carousel:
+ * @self: a #HdyCarouselIndicatorLines
+ *
+ * Get the #HdyCarousel the indicator uses.
+ *
+ * See: hdy_carousel_indicator_lines_set_carousel()
+ *
+ * Returns: (nullable) (transfer none): the #HdyCarousel, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+
+HdyCarousel *
+hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self), NULL);
+
+ return self->carousel;
+}
+
+/**
+ * hdy_carousel_indicator_lines_set_carousel:
+ * @self: a #HdyCarouselIndicatorLines
+ * @carousel: (nullable): a #HdyCarousel
+ *
+ * Sets the #HdyCarousel to use.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self,
+ HdyCarousel *carousel)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL_INDICATOR_LINES (self));
+ g_return_if_fail (HDY_IS_CAROUSEL (carousel) || carousel == NULL);
+
+ if (self->carousel == carousel)
+ return;
+
+ if (self->carousel) {
+ stop_animation (self);
+ g_signal_handlers_disconnect_by_func (self->carousel, gtk_widget_queue_draw, self);
+ g_signal_handlers_disconnect_by_func (self->carousel, n_pages_changed_cb, self);
+ }
+
+ g_set_object (&self->carousel, carousel);
+
+ if (self->carousel) {
+ g_signal_connect_object (self->carousel, "notify::position",
+ G_CALLBACK (gtk_widget_queue_draw), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->carousel, "notify::n-pages",
+ G_CALLBACK (n_pages_changed_cb), self,
+ G_CONNECT_SWAPPED);
+ }
+
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAROUSEL]);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel-indicator-lines.h b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h
new file mode 100644
index 0000000..baae57d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel-indicator-lines.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-carousel.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL_INDICATOR_LINES (hdy_carousel_indicator_lines_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarouselIndicatorLines, hdy_carousel_indicator_lines, HDY, CAROUSEL_INDICATOR_LINES, GtkDrawingArea)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_indicator_lines_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyCarousel *hdy_carousel_indicator_lines_get_carousel (HdyCarouselIndicatorLines *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_indicator_lines_set_carousel (HdyCarouselIndicatorLines *self,
+ HdyCarousel *carousel);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel.c b/subprojects/libhandy/src/hdy-carousel.c
new file mode 100644
index 0000000..7d8db55
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.c
@@ -0,0 +1,1099 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-carousel.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-carousel-box-private.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker.h"
+#include "hdy-swipeable.h"
+
+#include <math.h>
+
+#define DEFAULT_DURATION 250
+
+/**
+ * SECTION:hdy-carousel
+ * @short_description: A paginated scrolling widget.
+ * @title: HdyCarousel
+ * @See_also: #HdyCarouselIndicatorDots, #HdyCarouselIndicatorLines
+ *
+ * The #HdyCarousel widget can be used to display a set of pages with
+ * swipe-based navigation between them.
+ *
+ * # CSS nodes
+ *
+ * #HdyCarousel has a single CSS node with name carousel.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyCarousel
+{
+ GtkEventBox parent_instance;
+
+ HdyCarouselBox *scrolling_box;
+
+ HdySwipeTracker *tracker;
+
+ GtkOrientation orientation;
+ guint animation_duration;
+
+ gulong scroll_timeout_id;
+ gboolean can_scroll;
+};
+
+static void hdy_carousel_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyCarousel, hdy_carousel, GTK_TYPE_EVENT_BOX,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_carousel_swipeable_init))
+
+enum {
+ PROP_0,
+ PROP_N_PAGES,
+ PROP_POSITION,
+ PROP_INTERACTIVE,
+ PROP_SPACING,
+ PROP_ANIMATION_DURATION,
+ PROP_ALLOW_MOUSE_DRAG,
+ PROP_REVEAL_DURATION,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_REVEAL_DURATION + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_PAGE_CHANGED,
+ SIGNAL_LAST_SIGNAL,
+};
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+
+static void
+hdy_carousel_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+ GtkWidget *child;
+
+ child = hdy_carousel_box_get_nth_child (self->scrolling_box, index);
+
+ hdy_carousel_box_scroll_to (self->scrolling_box, child, duration);
+}
+
+static void
+begin_swipe_cb (HdySwipeTracker *tracker,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdyCarousel *self)
+{
+ hdy_carousel_box_stop_animation (self->scrolling_box);
+}
+
+static void
+update_swipe_cb (HdySwipeTracker *tracker,
+ gdouble progress,
+ HdyCarousel *self)
+{
+ hdy_carousel_box_set_position (self->scrolling_box, progress);
+}
+
+static void
+end_swipe_cb (HdySwipeTracker *tracker,
+ gint64 duration,
+ gdouble to,
+ HdyCarousel *self)
+{
+ GtkWidget *child;
+
+ child = hdy_carousel_box_get_page_at_position (self->scrolling_box, to);
+ hdy_carousel_box_scroll_to (self->scrolling_box, child, duration);
+}
+
+static HdySwipeTracker *
+hdy_carousel_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return self->tracker;
+}
+
+static gdouble
+hdy_carousel_get_distance (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_distance (self->scrolling_box);
+}
+
+static gdouble *
+hdy_carousel_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_snap_points (self->scrolling_box,
+ n_snap_points);
+}
+
+static gdouble
+hdy_carousel_get_progress (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_get_position (self);
+}
+
+static gdouble
+hdy_carousel_get_cancel_progress (HdySwipeable *swipeable)
+{
+ HdyCarousel *self = HDY_CAROUSEL (swipeable);
+
+ return hdy_carousel_box_get_closest_snap_point (self->scrolling_box);
+}
+
+static void
+notify_n_pages_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+notify_position_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
+
+static void
+notify_spacing_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]);
+}
+
+static void
+notify_reveal_duration_cb (HdyCarousel *self,
+ GParamSpec *spec,
+ GObject *object)
+{
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL_DURATION]);
+}
+
+static void
+animation_stopped_cb (HdyCarousel *self,
+ HdyCarouselBox *box)
+{
+ gint index;
+
+ index = hdy_carousel_box_get_current_page_index (self->scrolling_box);
+
+ g_signal_emit (self, signals[SIGNAL_PAGE_CHANGED], 0, index);
+}
+
+static void
+position_shifted_cb (HdyCarousel *self,
+ gdouble delta,
+ HdyCarouselBox *box)
+{
+ hdy_swipe_tracker_shift_position (self->tracker, delta);
+}
+
+/* Copied from GtkOrientable. Orientable widgets are supposed
+ * to do this manually via a private GTK function. */
+static void
+set_orientable_style_classes (GtkOrientable *orientable)
+{
+ GtkStyleContext *context;
+ GtkOrientation orientation;
+
+ g_return_if_fail (GTK_IS_ORIENTABLE (orientable));
+ g_return_if_fail (GTK_IS_WIDGET (orientable));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (orientable));
+ orientation = gtk_orientable_get_orientation (orientable);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ {
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_HORIZONTAL);
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_VERTICAL);
+ }
+ else
+ {
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_VERTICAL);
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_HORIZONTAL);
+ }
+}
+
+static void
+update_orientation (HdyCarousel *self)
+{
+ gboolean reversed;
+
+ if (!self->scrolling_box)
+ return;
+
+ reversed = self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ g_object_set (self->scrolling_box, "orientation", self->orientation, NULL);
+ g_object_set (self->tracker, "orientation", self->orientation,
+ "reversed", reversed, NULL);
+
+ set_orientable_style_classes (GTK_ORIENTABLE (self));
+ set_orientable_style_classes (GTK_ORIENTABLE (self->scrolling_box));
+}
+
+static gboolean
+scroll_timeout_cb (HdyCarousel *self)
+{
+ self->can_scroll = TRUE;
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+scroll_event_cb (HdyCarousel *self,
+ GdkEvent *event)
+{
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ GdkScrollDirection direction;
+ gdouble dx, dy;
+ gint index;
+ gboolean allow_vertical;
+ GtkOrientation orientation;
+ guint duration;
+
+ if (!self->can_scroll)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!hdy_carousel_get_interactive (self))
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type != GDK_SCROLL)
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device (event);
+ input_source = gdk_device_get_source (source_device);
+ if (input_source == GDK_SOURCE_TOUCHPAD)
+ return GDK_EVENT_PROPAGATE;
+
+ /* Mice often don't have easily accessible horizontal scrolling,
+ * hence allow vertical mouse scrolling regardless of orientation */
+ allow_vertical = (input_source == GDK_SOURCE_MOUSE);
+
+ if (gdk_event_get_scroll_direction (event, &direction)) {
+ dx = 0;
+ dy = 0;
+
+ switch (direction) {
+ case GDK_SCROLL_UP:
+ dy = -1;
+ break;
+ case GDK_SCROLL_DOWN:
+ dy = 1;
+ break;
+ case GDK_SCROLL_LEFT:
+ dy = -1;
+ break;
+ case GDK_SCROLL_RIGHT:
+ dy = 1;
+ break;
+ case GDK_SCROLL_SMOOTH:
+ g_assert_not_reached ();
+ default:
+ return GDK_EVENT_PROPAGATE;
+ }
+ } else {
+ gdk_event_get_scroll_deltas (event, &dx, &dy);
+ }
+
+ orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (self));
+ index = 0;
+
+ if (orientation == GTK_ORIENTATION_VERTICAL || allow_vertical) {
+ if (dy > 0)
+ index++;
+ else if (dy < 0)
+ index--;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL && index == 0) {
+ if (dx > 0)
+ index++;
+ else if (dx < 0)
+ index--;
+ }
+
+ if (index == 0)
+ return GDK_EVENT_PROPAGATE;
+
+ index += hdy_carousel_box_get_current_page_index (self->scrolling_box);
+ index = CLAMP (index, 0, (gint) hdy_carousel_get_n_pages (self) - 1);
+
+ hdy_carousel_scroll_to (self, hdy_carousel_box_get_nth_child (self->scrolling_box, index));
+
+ /* Don't allow the delay to go lower than 250ms */
+ duration = MIN (self->animation_duration, DEFAULT_DURATION);
+
+ self->can_scroll = FALSE;
+ g_timeout_add (duration, (GSourceFunc) scroll_timeout_cb, self);
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+hdy_carousel_destroy (GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (widget);
+
+ if (self->scrolling_box) {
+ gtk_widget_destroy (GTK_WIDGET (self->scrolling_box));
+ self->scrolling_box = NULL;
+ }
+
+ GTK_WIDGET_CLASS (hdy_carousel_parent_class)->destroy (widget);
+}
+
+static void
+hdy_carousel_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ HdyCarousel *self = HDY_CAROUSEL (widget);
+
+ update_orientation (self);
+}
+
+static void
+hdy_carousel_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (self->scrolling_box)
+ gtk_container_add (GTK_CONTAINER (self->scrolling_box), widget);
+ else
+ GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->add (container, widget);
+}
+
+static void
+hdy_carousel_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (self->scrolling_box)
+ gtk_container_remove (GTK_CONTAINER (self->scrolling_box), widget);
+ else
+ GTK_CONTAINER_CLASS (hdy_carousel_parent_class)->remove (container, widget);
+}
+
+static void
+hdy_carousel_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyCarousel *self = HDY_CAROUSEL (container);
+
+ if (include_internals)
+ (* callback) (GTK_WIDGET (self->scrolling_box), callback_data);
+ else if (self->scrolling_box)
+ gtk_container_foreach (GTK_CONTAINER (self->scrolling_box),
+ callback, callback_data);
+}
+
+static void
+hdy_carousel_constructed (GObject *object)
+{
+ HdyCarousel *self = (HdyCarousel *)object;
+
+ update_orientation (self);
+
+ G_OBJECT_CLASS (hdy_carousel_parent_class)->constructed (object);
+}
+
+static void
+hdy_carousel_dispose (GObject *object)
+{
+ HdyCarousel *self = (HdyCarousel *)object;
+
+ g_clear_object (&self->tracker);
+
+ if (self->scroll_timeout_id != 0) {
+ g_source_remove (self->scroll_timeout_id);
+ self->scroll_timeout_id = 0;
+ }
+
+ G_OBJECT_CLASS (hdy_carousel_parent_class)->dispose (object);
+}
+
+static void
+hdy_carousel_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarousel *self = HDY_CAROUSEL (object);
+
+ switch (prop_id) {
+ case PROP_N_PAGES:
+ g_value_set_uint (value, hdy_carousel_get_n_pages (self));
+ break;
+
+ case PROP_POSITION:
+ g_value_set_double (value, hdy_carousel_get_position (self));
+ break;
+
+ case PROP_INTERACTIVE:
+ g_value_set_boolean (value, hdy_carousel_get_interactive (self));
+ break;
+
+ case PROP_SPACING:
+ g_value_set_uint (value, hdy_carousel_get_spacing (self));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ g_value_set_boolean (value, hdy_carousel_get_allow_mouse_drag (self));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ g_value_set_uint (value, hdy_carousel_get_reveal_duration (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ case PROP_ANIMATION_DURATION:
+ g_value_set_uint (value, hdy_carousel_get_animation_duration (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyCarousel *self = HDY_CAROUSEL (object);
+
+ switch (prop_id) {
+ case PROP_INTERACTIVE:
+ hdy_carousel_set_interactive (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SPACING:
+ hdy_carousel_set_spacing (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ANIMATION_DURATION:
+ hdy_carousel_set_animation_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_REVEAL_DURATION:
+ hdy_carousel_set_reveal_duration (self, g_value_get_uint (value));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ hdy_carousel_set_allow_mouse_drag (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = orientation;
+ update_orientation (self);
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_carousel_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_carousel_switch_child;
+ iface->get_swipe_tracker = hdy_carousel_get_swipe_tracker;
+ iface->get_distance = hdy_carousel_get_distance;
+ iface->get_snap_points = hdy_carousel_get_snap_points;
+ iface->get_progress = hdy_carousel_get_progress;
+ iface->get_cancel_progress = hdy_carousel_get_cancel_progress;
+}
+
+static void
+hdy_carousel_class_init (HdyCarouselClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->constructed = hdy_carousel_constructed;
+ object_class->dispose = hdy_carousel_dispose;
+ object_class->get_property = hdy_carousel_get_property;
+ object_class->set_property = hdy_carousel_set_property;
+ widget_class->destroy = hdy_carousel_destroy;
+ widget_class->direction_changed = hdy_carousel_direction_changed;
+ container_class->add = hdy_carousel_add;
+ container_class->remove = hdy_carousel_remove;
+ container_class->forall = hdy_carousel_forall;
+
+ /**
+ * HdyCarousel:n-pages:
+ *
+ * The number of pages in a #HdyCarousel
+ *
+ * Since: 1.0
+ */
+ props[PROP_N_PAGES] =
+ g_param_spec_uint ("n-pages",
+ _("Number of pages"),
+ _("Number of pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:position:
+ *
+ * Current scrolling position, unitless. 1 matches 1 page. Use
+ * hdy_carousel_scroll_to() for changing it.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POSITION] =
+ g_param_spec_double ("position",
+ _("Position"),
+ _("Current scrolling position"),
+ 0,
+ G_MAXDOUBLE,
+ 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:interactive:
+ *
+ * Whether the carousel can be navigated. This can be used to temporarily
+ * disable a #HdyCarousel to only allow navigating it in a certain state.
+ *
+ * Since: 1.0
+ */
+ props[PROP_INTERACTIVE] =
+ g_param_spec_boolean ("interactive",
+ _("Interactive"),
+ _("Whether the widget can be swiped"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:spacing:
+ *
+ * Spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SPACING] =
+ g_param_spec_uint ("spacing",
+ _("Spacing"),
+ _("Spacing between pages"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:animation-duration:
+ *
+ * Animation duration in milliseconds, used by hdy_carousel_scroll_to().
+ *
+ * Since: 1.0
+ */
+ props[PROP_ANIMATION_DURATION] =
+ g_param_spec_uint ("animation-duration",
+ _("Animation duration"),
+ _("Default animation duration"),
+ 0, G_MAXUINT, DEFAULT_DURATION,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:allow-mouse-drag:
+ *
+ * Sets whether the #HdyCarousel can be dragged with mouse pointer. If the
+ * value is %FALSE, dragging is only available on touch.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ALLOW_MOUSE_DRAG] =
+ g_param_spec_boolean ("allow-mouse-drag",
+ _("Allow mouse drag"),
+ _("Whether to allow dragging with mouse pointer"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyCarousel:reveal-duration:
+ *
+ * Page reveal duration in milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVEAL_DURATION] =
+ g_param_spec_uint ("reveal-duration",
+ _("Reveal duration"),
+ _("Page reveal duration"),
+ 0,
+ G_MAXUINT,
+ 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdyCarousel::page-changed:
+ * @self: The #HdyCarousel instance
+ * @index: Current page
+ *
+ * This signal is emitted after a page has been changed. This can be used to
+ * implement "infinite scrolling" by connecting to this signal and amending
+ * the pages.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_PAGE_CHANGED] =
+ g_signal_new ("page-changed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_UINT);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-carousel.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyCarousel, scrolling_box);
+ gtk_widget_class_bind_template_callback (widget_class, scroll_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_n_pages_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_position_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_spacing_cb);
+ gtk_widget_class_bind_template_callback (widget_class, notify_reveal_duration_cb);
+ gtk_widget_class_bind_template_callback (widget_class, animation_stopped_cb);
+ gtk_widget_class_bind_template_callback (widget_class, position_shifted_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "carousel");
+}
+
+static void
+hdy_carousel_init (HdyCarousel *self)
+{
+ g_type_ensure (HDY_TYPE_CAROUSEL_BOX);
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->animation_duration = DEFAULT_DURATION;
+
+ self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self));
+ hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, TRUE);
+
+ g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0);
+
+ self->can_scroll = TRUE;
+}
+
+/**
+ * hdy_carousel_new:
+ *
+ * Create a new #HdyCarousel widget.
+ *
+ * Returns: The newly created #HdyCarousel widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_carousel_new (void)
+{
+ return g_object_new (HDY_TYPE_CAROUSEL, NULL);
+}
+
+/**
+ * hdy_carousel_prepend:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ *
+ * Prepends @child to @self
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_prepend (HdyCarousel *self,
+ GtkWidget *widget)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_insert (self->scrolling_box, widget, 0);
+}
+
+/**
+ * hdy_carousel_insert:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ * @position: the position to insert @child in.
+ *
+ * Inserts @child into @self at position @position.
+ *
+ * If position is -1, or larger than the number of pages,
+ * @child will be appended to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_insert (HdyCarousel *self,
+ GtkWidget *widget,
+ gint position)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_insert (self->scrolling_box, widget, position);
+}
+/**
+ * hdy_carousel_reorder:
+ * @self: a #HdyCarousel
+ * @child: a widget to add
+ * @position: the position to move @child to.
+ *
+ * Moves @child into position @position.
+ *
+ * If position is -1, or larger than the number of pages, @child will be moved
+ * to the end.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_reorder (HdyCarousel *self,
+ GtkWidget *child,
+ gint position)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+
+ hdy_carousel_box_reorder (self->scrolling_box, child, position);
+}
+
+/**
+ * hdy_carousel_scroll_to:
+ * @self: a #HdyCarousel
+ * @widget: a child of @self
+ *
+ * Scrolls to @widget position with an animation.
+ * #HdyCarousel:animation-duration property can be used for controlling the
+ * duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_scroll_to (HdyCarousel *self,
+ GtkWidget *widget)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_scroll_to_full (self, widget, self->animation_duration);
+}
+
+/**
+ * hdy_carousel_scroll_to_full:
+ * @self: a #HdyCarousel
+ * @widget: a child of @self
+ * @duration: animation duration in milliseconds
+ *
+ * Scrolls to @widget position with an animation.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_scroll_to_full (HdyCarousel *self,
+ GtkWidget *widget,
+ gint64 duration)
+{
+ GList *children;
+ gint n;
+
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ children = gtk_container_get_children (GTK_CONTAINER (self->scrolling_box));
+ n = g_list_index (children, widget);
+ g_list_free (children);
+
+ hdy_carousel_box_scroll_to (self->scrolling_box, widget,
+ duration);
+ hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self), n, duration);
+}
+
+/**
+ * hdy_carousel_get_n_pages:
+ * @self: a #HdyCarousel
+ *
+ * Gets the number of pages in @self.
+ *
+ * Returns: The number of pages in @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_n_pages (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_n_pages (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_get_position:
+ * @self: a #HdyCarousel
+ *
+ * Gets current scroll position in @self. It's unitless, 1 matches 1 page.
+ *
+ * Returns: The scroll position
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_carousel_get_position (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_position (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_get_interactive
+ * @self: a #HdyCarousel
+ *
+ * Gets whether @self can be navigated.
+ *
+ * Returns: %TRUE if @self can be swiped
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_get_interactive (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE);
+
+ return hdy_swipe_tracker_get_enabled (self->tracker);
+}
+
+/**
+ * hdy_carousel_set_interactive
+ * @self: a #HdyCarousel
+ * @interactive: whether @self can be swiped.
+ *
+ * Sets whether @self can be navigated. This can be used to temporarily disable
+ * a #HdyCarousel to only allow swiping in a certain state.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_interactive (HdyCarousel *self,
+ gboolean interactive)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ interactive = !!interactive;
+
+ if (hdy_swipe_tracker_get_enabled (self->tracker) == interactive)
+ return;
+
+ hdy_swipe_tracker_set_enabled (self->tracker, interactive);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERACTIVE]);
+}
+
+/**
+ * hdy_carousel_get_spacing:
+ * @self: a #HdyCarousel
+ *
+ * Gets spacing between pages in pixels.
+ *
+ * Returns: Spacing between pages
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_spacing (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_spacing (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_set_spacing:
+ * @self: a #HdyCarousel
+ * @spacing: the new spacing value
+ *
+ * Sets spacing between pages in pixels.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_spacing (HdyCarousel *self,
+ guint spacing)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_set_spacing (self->scrolling_box, spacing);
+}
+
+/**
+ * hdy_carousel_get_animation_duration:
+ * @self: a #HdyCarousel
+ *
+ * Gets animation duration used by hdy_carousel_scroll_to().
+ *
+ * Returns: Animation duration in milliseconds
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_animation_duration (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return self->animation_duration;
+}
+
+/**
+ * hdy_carousel_set_animation_duration:
+ * @self: a #HdyCarousel
+ * @duration: animation duration in milliseconds
+ *
+ * Sets animation duration used by hdy_carousel_scroll_to().
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_animation_duration (HdyCarousel *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ if (self->animation_duration == duration)
+ return;
+
+ self->animation_duration = duration;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ANIMATION_DURATION]);
+}
+
+/**
+ * hdy_carousel_get_allow_mouse_drag:
+ * @self: a #HdyCarousel
+ *
+ * Sets whether @self can be dragged with mouse pointer
+ *
+ * Returns: %TRUE if @self can be dragged with mouse
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_carousel_get_allow_mouse_drag (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), FALSE);
+
+ return hdy_swipe_tracker_get_allow_mouse_drag (self->tracker);
+}
+
+/**
+ * hdy_carousel_set_allow_mouse_drag:
+ * @self: a #HdyCarousel
+ * @allow_mouse_drag: whether @self can be dragged with mouse pointer
+ *
+ * Sets whether @self can be dragged with mouse pointer. If @allow_mouse_drag
+ * is %FALSE, dragging is only available on touch.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_allow_mouse_drag (HdyCarousel *self,
+ gboolean allow_mouse_drag)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ allow_mouse_drag = !!allow_mouse_drag;
+
+ if (hdy_carousel_get_allow_mouse_drag (self) == allow_mouse_drag)
+ return;
+
+ hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, allow_mouse_drag);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * hdy_carousel_get_reveal_duration:
+ * @self: a #HdyCarousel
+ *
+ * Gets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Returns: Page reveal duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_carousel_get_reveal_duration (HdyCarousel *self)
+{
+ g_return_val_if_fail (HDY_IS_CAROUSEL (self), 0);
+
+ return hdy_carousel_box_get_reveal_duration (self->scrolling_box);
+}
+
+/**
+ * hdy_carousel_set_reveal_duration:
+ * @self: a #HdyCarousel
+ * @reveal_duration: the new reveal duration value
+ *
+ * Sets duration of the animation used when adding or removing pages in
+ * milliseconds.
+ *
+ * Since: 1.0
+ */
+void
+hdy_carousel_set_reveal_duration (HdyCarousel *self,
+ guint reveal_duration)
+{
+ g_return_if_fail (HDY_IS_CAROUSEL (self));
+
+ hdy_carousel_box_set_reveal_duration (self->scrolling_box, reveal_duration);
+}
diff --git a/subprojects/libhandy/src/hdy-carousel.h b/subprojects/libhandy/src/hdy-carousel.h
new file mode 100644
index 0000000..4318b65
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CAROUSEL (hdy_carousel_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyCarousel, hdy_carousel, HDY, CAROUSEL, GtkEventBox)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_carousel_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_prepend (HdyCarousel *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_insert (HdyCarousel *self,
+ GtkWidget *child,
+ gint position);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_reorder (HdyCarousel *self,
+ GtkWidget *child,
+ gint position);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_scroll_to (HdyCarousel *self,
+ GtkWidget *widget);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_scroll_to_full (HdyCarousel *self,
+ GtkWidget *widget,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_n_pages (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_carousel_get_position (HdyCarousel *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_carousel_get_interactive (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_interactive (HdyCarousel *self,
+ gboolean interactive);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_spacing (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_spacing (HdyCarousel *self,
+ guint spacing);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_animation_duration (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_animation_duration (HdyCarousel *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_carousel_get_allow_mouse_drag (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_allow_mouse_drag (HdyCarousel *self,
+ gboolean allow_mouse_drag);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_carousel_get_reveal_duration (HdyCarousel *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_carousel_set_reveal_duration (HdyCarousel *self,
+ guint reveal_duration);
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-carousel.ui b/subprojects/libhandy/src/hdy-carousel.ui
new file mode 100644
index 0000000..c9bf553
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-carousel.ui
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyCarousel" parent="GtkEventBox">
+ <property name="visible">True</property>
+ <property name="orientation">horizontal</property>
+ <signal name="scroll-event" handler="scroll_event_cb"/>
+ <child>
+ <object class="HdyCarouselBox" id="scrolling_box">
+ <property name="visible">True</property>
+ <property name="expand">True</property>
+ <signal name="notify::n-pages" handler="notify_n_pages_cb" swapped="true"/>
+ <signal name="notify::position" handler="notify_position_cb" swapped="true"/>
+ <signal name="notify::spacing" handler="notify_spacing_cb" swapped="true"/>
+ <signal name="notify::reveal-duration" handler="notify_reveal_duration_cb" swapped="true"/>
+ <signal name="animation-stopped" handler="animation_stopped_cb" swapped="true"/>
+ <signal name="position-shifted" handler="position_shifted_cb" swapped="true"/>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-clamp.c b/subprojects/libhandy/src/hdy-clamp.c
new file mode 100644
index 0000000..9cb9f23
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-clamp.c
@@ -0,0 +1,563 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-clamp.h"
+
+#include <glib/gi18n-lib.h>
+#include <math.h>
+
+#include "hdy-animation-private.h"
+
+/**
+ * SECTION:hdy-clamp
+ * @short_description: A container constraining its child to a given size.
+ * @Title: HdyClamp
+ *
+ * The #HdyClamp widget constraints the size of the widget it contains to a
+ * given maximum size. It will constrain the width if it is horizontal, or the
+ * height if it is vertical. The expansion of the child from its minimum to its
+ * maximum size is eased out for a smooth transition.
+ *
+ * If the child requires more than the requested maximum size, it will be
+ * allocated the minimum size it can fit in instead.
+ *
+ * # CSS nodes
+ *
+ * #HdyClamp has a single CSS node with name clamp. The node will get the style
+ * classes .large when its child reached its maximum size, .small when the clamp
+ * allocates its full size to its child, .medium in-between, or none if it
+ * didn't compute its size yet.
+ *
+ * Since: 1.0
+ */
+
+#define HDY_EASE_OUT_TAN_CUBIC 3
+
+enum {
+ PROP_0,
+ PROP_MAXIMUM_SIZE,
+ PROP_TIGHTENING_THRESHOLD,
+
+ /* Overridden properties */
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_TIGHTENING_THRESHOLD + 1,
+};
+
+struct _HdyClamp
+{
+ GtkBin parent_instance;
+
+ gint maximum_size;
+ gint tightening_threshold;
+
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdyClamp, hdy_clamp, GTK_TYPE_BIN,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static void
+set_orientation (HdyClamp *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_clamp_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyClamp *self = HDY_CLAMP (object);
+
+ switch (prop_id) {
+ case PROP_MAXIMUM_SIZE:
+ g_value_set_int (value, hdy_clamp_get_maximum_size (self));
+ break;
+ case PROP_TIGHTENING_THRESHOLD:
+ g_value_set_int (value, hdy_clamp_get_tightening_threshold (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_clamp_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyClamp *self = HDY_CLAMP (object);
+
+ switch (prop_id) {
+ case PROP_MAXIMUM_SIZE:
+ hdy_clamp_set_maximum_size (self, g_value_get_int (value));
+ break;
+ case PROP_TIGHTENING_THRESHOLD:
+ hdy_clamp_set_tightening_threshold (self, g_value_get_int (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+/**
+ * get_child_size:
+ * @self: a #HdyClamp
+ * @for_size: the size of the clamp
+ * @child_minimum: the minimum size reachable by the child, and hence by @self
+ * @child_maximum: the maximum size @self will ever allocate its child
+ * @lower_threshold: the threshold below which @self will allocate its full size to its child
+ * @upper_threshold: the threshold up from which @self will allocate its maximum size to its child
+ *
+ * Measures the child's extremes, the clamp's thresholds, and returns size to
+ * allocate to the child.
+ *
+ * If the clamp is horizontal, all values are widths, otherwise they are
+ * heights.
+ */
+static gint
+get_child_size (HdyClamp *self,
+ gint for_size,
+ gint *child_minimum,
+ gint *child_maximum,
+ gint *lower_threshold,
+ gint *upper_threshold)
+{
+ GtkBin *bin = GTK_BIN (self);
+ GtkWidget *child;
+ gint min = 0, max = 0, lower = 0, upper = 0;
+ gdouble amplitude, progress;
+
+ child = gtk_bin_get_child (bin);
+ if (child == NULL)
+ return 0;
+
+ if (gtk_widget_get_visible (child)) {
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+ gtk_widget_get_preferred_width (child, &min, NULL);
+ else
+ gtk_widget_get_preferred_height (child, &min, NULL);
+ }
+
+ lower = MAX (MIN (self->tightening_threshold, self->maximum_size), min);
+ max = MAX (lower, self->maximum_size);
+ amplitude = max - lower;
+ upper = HDY_EASE_OUT_TAN_CUBIC * amplitude + lower;
+
+ if (child_minimum)
+ *child_minimum = min;
+ if (child_maximum)
+ *child_maximum = max;
+ if (lower_threshold)
+ *lower_threshold = lower;
+ if (upper_threshold)
+ *upper_threshold = upper;
+
+ if (for_size < 0)
+ return 0;
+
+ if (for_size <= lower)
+ return for_size;
+
+ if (for_size >= upper)
+ return max;
+
+ progress = (double) (for_size - lower) / (double) (upper - lower);
+
+ return hdy_ease_out_cubic (progress) * amplitude + lower;
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_clamp_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+ GtkBin *bin = GTK_BIN (widget);
+ GtkWidget *child;
+ gint child_size;
+
+ if (minimum)
+ *minimum = 0;
+ if (natural)
+ *natural = 0;
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+ if (natural_baseline)
+ *natural_baseline = -1;
+
+ child = gtk_bin_get_child (bin);
+ if (!(child && gtk_widget_get_visible (child)))
+ return;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (self->orientation == orientation) {
+ gtk_widget_get_preferred_width (child, minimum, natural);
+
+ return;
+ }
+
+ child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL);
+
+ gtk_widget_get_preferred_width_for_height (child,
+ child_size,
+ minimum,
+ natural);
+ } else {
+ if (self->orientation == orientation) {
+ gtk_widget_get_preferred_height (child, minimum, natural);
+
+ return;
+ }
+
+ child_size = get_child_size (HDY_CLAMP (widget), for_size, NULL, NULL, NULL, NULL);
+
+ gtk_widget_get_preferred_height_and_baseline_for_width (child,
+ child_size,
+ minimum,
+ natural,
+ minimum_baseline,
+ natural_baseline);
+ }
+}
+
+static GtkSizeRequestMode
+hdy_clamp_get_request_mode (GtkWidget *widget)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+
+ return self->orientation == GTK_ORIENTATION_HORIZONTAL ?
+ GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH :
+ GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+}
+
+static void
+hdy_clamp_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_height_and_baseline_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_clamp_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_clamp_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyClamp *self = HDY_CLAMP (widget);
+ GtkBin *bin = GTK_BIN (widget);
+ GtkAllocation child_allocation;
+ gint baseline;
+ GtkWidget *child;
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gint child_maximum = 0, lower_threshold = 0;
+ gint child_clamped_size;
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ child = gtk_bin_get_child (bin);
+ if (!(child && gtk_widget_get_visible (child))) {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+
+ return;
+ }
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_allocation.width = get_child_size (self, allocation->width, NULL, &child_maximum, &lower_threshold, NULL);
+ child_allocation.height = allocation->height;
+
+ child_clamped_size = child_allocation.width;
+ }
+ else {
+ child_allocation.width = allocation->width;
+ child_allocation.height = get_child_size (self, allocation->height, NULL, &child_maximum, &lower_threshold, NULL);
+
+ child_clamped_size = child_allocation.height;
+ }
+
+ if (child_clamped_size >= child_maximum) {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_add_class (context, "large");
+ } else if (child_clamped_size <= lower_threshold) {
+ gtk_style_context_add_class (context, "small");
+ gtk_style_context_remove_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+ } else {
+ gtk_style_context_remove_class (context, "small");
+ gtk_style_context_add_class (context, "medium");
+ gtk_style_context_remove_class (context, "large");
+ }
+
+ if (!gtk_widget_get_has_window (widget)) {
+ /* This always center the child on the side of the orientation. */
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_allocation.x = allocation->x + (allocation->width - child_allocation.width) / 2;
+ child_allocation.y = allocation->y;
+ } else {
+ child_allocation.x = allocation->x;
+ child_allocation.y = allocation->y + (allocation->height - child_allocation.height) / 2;
+ }
+ }
+ else {
+ child_allocation.x = 0;
+ child_allocation.y = 0;
+ }
+
+ baseline = gtk_widget_get_allocated_baseline (widget);
+ gtk_widget_size_allocate_with_baseline (child, &child_allocation, baseline);
+}
+
+static void
+hdy_clamp_class_init (HdyClampClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_clamp_get_property;
+ object_class->set_property = hdy_clamp_set_property;
+
+ widget_class->get_request_mode = hdy_clamp_get_request_mode;
+ widget_class->get_preferred_width = hdy_clamp_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_clamp_get_preferred_width_for_height;
+ widget_class->get_preferred_height = hdy_clamp_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_clamp_get_preferred_height_for_width;
+ widget_class->get_preferred_height_and_baseline_for_width = hdy_clamp_get_preferred_height_and_baseline_for_width;
+ widget_class->size_allocate = hdy_clamp_size_allocate;
+
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyClamp:maximum-size:
+ *
+ * The maximum size to allocate to the child. It is the width if the clamp is
+ * horizontal, or the height if it is vertical.
+ *
+ * Since: 1.0
+ */
+ props[PROP_MAXIMUM_SIZE] =
+ g_param_spec_int ("maximum-size",
+ _("Maximum size"),
+ _("The maximum size allocated to the child"),
+ 0, G_MAXINT, 600,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyClamp:tightening-threshold:
+ *
+ * The size starting from which the clamp will tighten its grip on the child,
+ * slowly allocating less and less of the available size up to the maximum
+ * allocated size. Below that threshold and below the maximum width, the child
+ * will be allocated all the available size.
+ *
+ * If the threshold is greater than the maximum size to allocate to the child,
+ * the child will be allocated all the width up to the maximum.
+ * If the threshold is lower than the minimum size to allocate to the child,
+ * that size will be used as the tightening threshold.
+ *
+ * Effectively, tightening the grip on the child before it reaches its maximum
+ * size makes transitions to and from the maximum size smoother when resizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TIGHTENING_THRESHOLD] =
+ g_param_spec_int ("tightening-threshold",
+ _("Tightening threshold"),
+ _("The size from which the clamp will tighten its grip on the child"),
+ 0, G_MAXINT, 400,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "clamp");
+}
+
+static void
+hdy_clamp_init (HdyClamp *self)
+{
+ self->maximum_size = 600;
+ self->tightening_threshold = 400;
+}
+
+/**
+ * hdy_clamp_new:
+ *
+ * Creates a new #HdyClamp.
+ *
+ * Returns: a new #HdyClamp
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_clamp_new (void)
+{
+ return g_object_new (HDY_TYPE_CLAMP, NULL);
+}
+
+/**
+ * hdy_clamp_get_maximum_size:
+ * @self: a #HdyClamp
+ *
+ * Gets the maximum size to allocate to the contained child. It is the width if
+ * @self is horizontal, or the height if it is vertical.
+ *
+ * Returns: the maximum width to allocate to the contained child.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_clamp_get_maximum_size (HdyClamp *self)
+{
+ g_return_val_if_fail (HDY_IS_CLAMP (self), 0);
+
+ return self->maximum_size;
+}
+
+/**
+ * hdy_clamp_set_maximum_size:
+ * @self: a #HdyClamp
+ * @maximum_size: the maximum size
+ *
+ * Sets the maximum size to allocate to the contained child. It is the width if
+ * @self is horizontal, or the height if it is vertical.
+ *
+ * Since: 1.0
+ */
+void
+hdy_clamp_set_maximum_size (HdyClamp *self,
+ gint maximum_size)
+{
+ g_return_if_fail (HDY_IS_CLAMP (self));
+
+ if (self->maximum_size == maximum_size)
+ return;
+
+ self->maximum_size = maximum_size;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAXIMUM_SIZE]);
+}
+
+/**
+ * hdy_clamp_get_tightening_threshold:
+ * @self: a #HdyClamp
+ *
+ * Gets the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Returns: the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Since: 1.0
+ */
+gint
+hdy_clamp_get_tightening_threshold (HdyClamp *self)
+{
+ g_return_val_if_fail (HDY_IS_CLAMP (self), 0);
+
+ return self->tightening_threshold;
+}
+
+/**
+ * hdy_clamp_set_tightening_threshold:
+ * @self: a #HdyClamp
+ * @tightening_threshold: the tightening threshold
+ *
+ * Sets the size starting from which the clamp will tighten its grip on the
+ * child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_clamp_set_tightening_threshold (HdyClamp *self,
+ gint tightening_threshold)
+{
+ g_return_if_fail (HDY_IS_CLAMP (self));
+
+ if (self->tightening_threshold == tightening_threshold)
+ return;
+
+ self->tightening_threshold = tightening_threshold;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIGHTENING_THRESHOLD]);
+}
diff --git a/subprojects/libhandy/src/hdy-clamp.h b/subprojects/libhandy/src/hdy-clamp.h
new file mode 100644
index 0000000..46ad6dd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-clamp.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_CLAMP (hdy_clamp_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyClamp, hdy_clamp, HDY, CLAMP, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_clamp_new (void);
+HDY_AVAILABLE_IN_ALL
+gint hdy_clamp_get_maximum_size (HdyClamp *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_clamp_set_maximum_size (HdyClamp *self,
+ gint maximum_size);
+HDY_AVAILABLE_IN_ALL
+gint hdy_clamp_get_tightening_threshold (HdyClamp *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_clamp_set_tightening_threshold (HdyClamp *self,
+ gint tightening_threshold);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-combo-row.c b/subprojects/libhandy/src/hdy-combo-row.c
new file mode 100644
index 0000000..e9cc6bf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.c
@@ -0,0 +1,829 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-combo-row.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-combo-row
+ * @short_description: A #GtkListBox row used to choose from a list of items.
+ * @Title: HdyComboRow
+ *
+ * The #HdyComboRow widget allows the user to choose from a list of valid
+ * choices. The row displays the selected choice. When activated, the row
+ * displays a popover which allows the user to make a new choice.
+ *
+ * The #HdyComboRow uses the model-view pattern; the list of valid choices
+ * is specified in the form of a #GListModel, and the display of the choices can
+ * be adapted to the data in the model via widget creation functions.
+ *
+ * #HdyComboRow is #GtkListBoxRow:activatable if a model is set.
+ *
+ * # CSS nodes
+ *
+ * #HdyComboRow has a main CSS node with name row.
+ *
+ * Its popover has the node name popover with the .combo style class, it
+ * contains a #GtkScrolledWindow, which in turn contains a #GtkListBox, both are
+ * accessible via their regular nodes.
+ *
+ * A checkmark of node and style class image.checkmark in the popover denotes
+ * the current item.
+ *
+ * Since: 0.0.6
+ */
+
+/*
+ * This was mostly inspired by code from the display panel from GNOME Settings.
+ */
+
+typedef struct
+{
+ HdyComboRowGetNameFunc func;
+ gpointer func_data;
+ GDestroyNotify func_data_destroy;
+} HdyComboRowGetName;
+
+typedef struct
+{
+ GtkBox *current;
+ GtkImage *image;
+ GtkListBox *list;
+ GtkPopover *popover;
+ gint selected_index;
+ gboolean use_subtitle;
+ HdyComboRowGetName *get_name;
+
+ GListModel *bound_model;
+ GtkListBoxCreateWidgetFunc create_list_widget_func;
+ GtkListBoxCreateWidgetFunc create_current_widget_func;
+ gpointer create_widget_func_data;
+ GDestroyNotify create_widget_func_data_free_func;
+ /* This is owned by create_widget_func_data, which is ultimately owned by the
+ * list box, and hence should not be destroyed manually.
+ */
+ HdyComboRowGetName *get_name_internal;
+} HdyComboRowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyComboRow, hdy_combo_row, HDY_TYPE_ACTION_ROW)
+
+enum {
+ PROP_0,
+ PROP_SELECTED_INDEX,
+ PROP_USE_SUBTITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static GtkWidget *
+create_list_label (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data;
+ g_autofree gchar *name = get_name->func (item, get_name->func_data);
+
+ return g_object_new (GTK_TYPE_LABEL,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "label", name,
+ "max-width-chars", 20,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ "xalign", 0.0,
+ NULL);
+}
+
+static GtkWidget *
+create_current_label (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRowGetName *get_name = (HdyComboRowGetName *) user_data;
+ g_autofree gchar *name = NULL;
+
+ if (get_name->func)
+ name = get_name->func (item, get_name->func_data);
+
+ return g_object_new (GTK_TYPE_LABEL,
+ "ellipsize", PANGO_ELLIPSIZE_END,
+ "halign", GTK_ALIGN_END,
+ "label", name,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ "xalign", 0.0,
+ NULL);
+}
+
+static void
+create_list_widget_data_free (gpointer user_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (priv->create_widget_func_data_free_func)
+ priv->create_widget_func_data_free_func (priv->create_widget_func_data);
+}
+
+static GtkWidget *
+create_list_widget (gpointer item,
+ gpointer user_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+ GtkWidget *checkmark = g_object_new (GTK_TYPE_IMAGE,
+ "halign", GTK_ALIGN_START,
+ "icon-name", "emblem-ok-symbolic",
+ "valign", GTK_ALIGN_CENTER,
+ NULL);
+ GtkWidget *box = g_object_new (GTK_TYPE_BOX,
+ "child", priv->create_list_widget_func (item, priv->create_widget_func_data),
+ "child", checkmark,
+ "halign", GTK_ALIGN_START,
+ "spacing", 6,
+ "valign", GTK_ALIGN_CENTER,
+ "visible", TRUE,
+ NULL);
+ GtkStyleContext *checkmark_context = gtk_widget_get_style_context (checkmark);
+
+ gtk_style_context_add_class (checkmark_context, "checkmark");
+
+ g_object_set_data (G_OBJECT (box), "checkmark", checkmark);
+
+ return box;
+}
+
+static void
+get_name_free (HdyComboRowGetName *get_name)
+{
+ if (get_name == NULL)
+ return;
+
+ if (get_name->func_data_destroy)
+ get_name->func_data_destroy (get_name->func_data);
+ get_name->func = NULL;
+ get_name->func_data = NULL;
+ get_name->func_data_destroy = NULL;
+
+ g_free (get_name);
+}
+
+static void
+update (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+ g_autoptr(GObject) item = NULL;
+ g_autofree gchar *name = NULL;
+ GtkWidget *widget;
+ guint n_items = priv->bound_model ? g_list_model_get_n_items (priv->bound_model) : 0;
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->current), !priv->use_subtitle);
+ gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL);
+
+ gtk_widget_set_sensitive (GTK_WIDGET (self), n_items > 0);
+ gtk_widget_set_visible (GTK_WIDGET (priv->image), n_items > 1);
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (self), n_items > 1);
+
+ if (n_items == 0) {
+ g_assert (priv->selected_index == -1);
+
+ return;
+ }
+
+ g_assert (priv->selected_index >= 0 && priv->selected_index <= n_items);
+
+ {
+ g_autoptr (GList) rows = gtk_container_get_children (GTK_CONTAINER (priv->list));
+ GList *l;
+ int i = 0;
+
+ for (l = rows; l; l = l->next) {
+ GtkWidget *row = GTK_WIDGET (l->data);
+ GtkWidget *box = gtk_bin_get_child (GTK_BIN (row));
+
+ gtk_widget_set_visible (GTK_WIDGET (g_object_get_data (G_OBJECT (box), "checkmark")),
+ priv->selected_index == i++);
+ }
+ }
+
+ item = g_list_model_get_item (priv->bound_model, priv->selected_index);
+ if (priv->use_subtitle) {
+ if (priv->get_name != NULL && priv->get_name->func)
+ name = priv->get_name->func (item, priv->get_name->func_data);
+ else if (priv->get_name_internal != NULL && priv->get_name_internal->func)
+ name = priv->get_name_internal->func (item, priv->get_name_internal->func_data);
+ hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), name);
+ }
+ else {
+ widget = priv->create_current_widget_func (item, priv->create_widget_func_data);
+ gtk_container_add (GTK_CONTAINER (priv->current), widget);
+ }
+}
+
+static void
+bound_model_changed (GListModel *list,
+ guint index,
+ guint removed,
+ guint added,
+ gpointer user_data)
+{
+ gint new_idx;
+ HdyComboRow *self = HDY_COMBO_ROW (user_data);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ /* Selection is in front of insertion/removal point, nothing to do */
+ if (priv->selected_index > 0 && priv->selected_index < index)
+ return;
+
+ if (priv->selected_index < index + removed) {
+ /* The item selected item was removed (or none is selected) */
+ new_idx = -1;
+ } else {
+ /* The item selected item was behind the insertion/removal */
+ new_idx = priv->selected_index + added - removed;
+ }
+
+ /* Select the first item if none is selected. */
+ if (new_idx == -1 && g_list_model_get_n_items (list) > 0)
+ new_idx = 0;
+
+ hdy_combo_row_set_selected_index (self, new_idx);
+}
+
+static void
+row_activated_cb (HdyComboRow *self,
+ GtkListBoxRow *row)
+{
+ hdy_combo_row_set_selected_index (self, gtk_list_box_row_get_index (row));
+}
+
+static void
+hdy_combo_row_activate (HdyActionRow *row)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (row);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (gtk_widget_get_visible (GTK_WIDGET (priv->image)))
+ gtk_popover_popup (priv->popover);
+}
+
+static void
+destroy_model (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ if (!priv->bound_model)
+ return;
+
+ /* Disconnect the bound model *before* releasing it. */
+ g_signal_handlers_disconnect_by_func (priv->bound_model, bound_model_changed, self);
+
+ /* Destroy the model and the user data. */
+ if (priv->list)
+ gtk_list_box_bind_model (priv->list, NULL, NULL, NULL, NULL);
+
+ priv->bound_model = NULL;
+ priv->create_list_widget_func = NULL;
+ priv->create_current_widget_func = NULL;
+ priv->create_widget_func_data = NULL;
+ priv->create_widget_func_data_free_func = NULL;
+}
+
+static void
+hdy_combo_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SELECTED_INDEX:
+ g_value_set_int (value, hdy_combo_row_get_selected_index (self));
+ break;
+ case PROP_USE_SUBTITLE:
+ g_value_set_boolean (value, hdy_combo_row_get_use_subtitle (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_combo_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SELECTED_INDEX:
+ hdy_combo_row_set_selected_index (self, g_value_get_int (value));
+ break;
+ case PROP_USE_SUBTITLE:
+ hdy_combo_row_set_use_subtitle (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_combo_row_dispose (GObject *object)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (object);
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ destroy_model (self);
+ g_clear_pointer (&priv->get_name, get_name_free);
+
+ G_OBJECT_CLASS (hdy_combo_row_parent_class)->dispose (object);
+}
+
+typedef struct {
+ HdyComboRow *row;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (data->row);
+
+ if (widget != (GtkWidget *) priv->current &&
+ widget != (GtkWidget *) priv->image)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_combo_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyComboRow *self = HDY_COMBO_ROW (container);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.row = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ GTK_CONTAINER_CLASS (hdy_combo_row_parent_class)->forall (GTK_CONTAINER (self), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_combo_row_class_init (HdyComboRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+ HdyActionRowClass *row_class = HDY_ACTION_ROW_CLASS (klass);
+
+ object_class->get_property = hdy_combo_row_get_property;
+ object_class->set_property = hdy_combo_row_set_property;
+ object_class->dispose = hdy_combo_row_dispose;
+
+ container_class->forall = hdy_combo_row_forall;
+
+ row_class->activate = hdy_combo_row_activate;
+
+ /**
+ * HdyComboRow:selected-index:
+ *
+ * The index of the selected item in its #GListModel.
+ *
+ * Since: 0.0.7
+ */
+ props[PROP_SELECTED_INDEX] =
+ g_param_spec_int ("selected-index",
+ _("Selected index"),
+ _("The index of the selected item"),
+ -1, G_MAXINT, -1,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyComboRow:use-subtitle:
+ *
+ * %TRUE to set the current value as the subtitle.
+ *
+ * If you use a custom widget creation function, you will need to give the row
+ * a name conversion closure with hdy_combo_row_set_get_name_func().
+ *
+ * If %TRUE, you should not access HdyActionRow:subtitle.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_USE_SUBTITLE] =
+ g_param_spec_boolean ("use-subtitle",
+ _("Use subtitle"),
+ _("Set the current value as the subtitle"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-combo-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, current);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, list);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyComboRow, popover);
+}
+
+static void
+hdy_combo_row_init (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ priv->selected_index = -1;
+
+ g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (gtk_widget_hide),
+ priv->popover, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->list, "row-activated", G_CALLBACK (row_activated_cb),
+ self, G_CONNECT_SWAPPED);
+
+ update (self);
+}
+
+/**
+ * hdy_combo_row_new:
+ *
+ * Creates a new #HdyComboRow.
+ *
+ * Returns: a new #HdyComboRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_combo_row_new (void)
+{
+ return g_object_new (HDY_TYPE_COMBO_ROW, NULL);
+}
+
+/**
+ * hdy_combo_row_get_model:
+ * @self: a #HdyComboRow
+ *
+ * Gets the model bound to @self, or %NULL if none is bound.
+ *
+ * Returns: (transfer none) (nullable): the #GListModel bound to @self or %NULL
+ *
+ * Since: 0.0.6
+ */
+GListModel *
+hdy_combo_row_get_model (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), NULL);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->bound_model;
+}
+
+/**
+ * hdy_combo_row_bind_model:
+ * @self: a #HdyComboRow
+ * @model: (nullable): the #GListModel to be bound to @self
+ * @create_list_widget_func: (nullable) (scope call): a function that creates
+ * widgets for items to display in the list, or %NULL in case you also passed
+ * %NULL as @model
+ * @create_current_widget_func: (nullable) (scope call): a function that creates
+ * widgets for items to display as the selected item, or %NULL in case you
+ * also passed %NULL as @model
+ * @user_data: user data passed to @create_list_widget_func and
+ * @create_current_widget_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Binds @model to @self.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_bind_model (HdyComboRow *self,
+ GListModel *model,
+ GtkListBoxCreateWidgetFunc create_list_widget_func,
+ GtkListBoxCreateWidgetFunc create_current_widget_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+ g_return_if_fail (model == NULL || create_list_widget_func != NULL);
+ g_return_if_fail (model == NULL || create_current_widget_func != NULL);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ destroy_model (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->current), (GtkCallback) gtk_widget_destroy, NULL);
+ priv->selected_index = -1;
+
+ if (model == NULL) {
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+ return;
+ }
+
+ /* We don't need take a reference as the list box holds one for us. */
+ priv->bound_model = model;
+ priv->create_list_widget_func = create_list_widget_func;
+ priv->create_current_widget_func = create_current_widget_func;
+ priv->create_widget_func_data = user_data;
+ priv->create_widget_func_data_free_func = user_data_free_func;
+
+ g_signal_connect (priv->bound_model, "items-changed", G_CALLBACK (bound_model_changed), self);
+
+ if (g_list_model_get_n_items (priv->bound_model) > 0)
+ priv->selected_index = 0;
+
+ gtk_list_box_bind_model (priv->list, model, create_list_widget, self, create_list_widget_data_free);
+
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+}
+
+/**
+ * hdy_combo_row_bind_name_model:
+ * @self: a #HdyComboRow
+ * @model: (nullable): the #GListModel to be bound to @self
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Binds @model to @self.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * This is more convenient to use than hdy_combo_row_bind_model() if you want to
+ * represent items of the model with names.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_bind_name_model (HdyComboRow *self,
+ GListModel *model,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv = hdy_combo_row_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model));
+ g_return_if_fail (model == NULL || get_name_func != NULL);
+
+ priv->get_name_internal = g_new0 (HdyComboRowGetName, 1);
+ priv->get_name_internal->func = get_name_func;
+ priv->get_name_internal->func_data = user_data;
+ priv->get_name_internal->func_data_destroy = user_data_free_func;
+
+ hdy_combo_row_bind_model (self, model, create_list_label, create_current_label, priv->get_name_internal, (GDestroyNotify) get_name_free);
+}
+
+/**
+ * hdy_combo_row_set_for_enum:
+ * @self: a #HdyComboRow
+ * @enum_type: the enumeration #GType to be bound to @self
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Creates a model for @enum_type and binds it to @self. The items of the model
+ * will be #HdyEnumValueObject objects.
+ *
+ * If @self was already bound to a model, that previous binding is destroyed.
+ *
+ * The contents of @self are cleared and then filled with widgets that represent
+ * items from @model. @self is updated whenever @model changes. If @model is
+ * %NULL, @self is left empty.
+ *
+ * This is more convenient to use than hdy_combo_row_bind_name_model() if you
+ * want to represent values of an enumeration with names.
+ *
+ * See hdy_enum_value_row_name().
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_combo_row_set_for_enum (HdyComboRow *self,
+ GType enum_type,
+ HdyComboRowGetEnumValueNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ g_autoptr (GListStore) store = g_list_store_new (HDY_TYPE_ENUM_VALUE_OBJECT);
+ /* g_autoptr for GEnumClass would require glib > 2.56 */
+ GEnumClass *enum_class = NULL;
+ gsize i;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ enum_class = g_type_class_ref (enum_type);
+ for (i = 0; i < enum_class->n_values; i++)
+ {
+ g_autoptr(HdyEnumValueObject) obj = hdy_enum_value_object_new (&enum_class->values[i]);
+
+ g_list_store_append (store, obj);
+ }
+
+ hdy_combo_row_bind_name_model (self, G_LIST_MODEL (store), (HdyComboRowGetNameFunc) get_name_func, user_data, user_data_free_func);
+ g_type_class_unref (enum_class);
+}
+
+/**
+ * hdy_combo_row_get_selected_index:
+ * @self: a #GtkListBoxRow
+ *
+ * Gets the index of the selected item in its #GListModel.
+ *
+ * Returns: the index of the selected item, or -1 if no item is selected
+ *
+ * Since: 0.0.7
+ */
+gint
+hdy_combo_row_get_selected_index (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), -1);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->selected_index;
+}
+
+/**
+ * hdy_combo_row_set_selected_index:
+ * @self: a #HdyComboRow
+ * @selected_index: the index of the selected item
+ *
+ * Sets the index of the selected item in its #GListModel.
+ *
+ * Since: 0.0.7
+ */
+void
+hdy_combo_row_set_selected_index (HdyComboRow *self,
+ gint selected_index)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+ g_return_if_fail (selected_index >= -1);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ g_return_if_fail (selected_index >= 0 || priv->bound_model == NULL || g_list_model_get_n_items (priv->bound_model) == 0);
+ g_return_if_fail (selected_index == -1 || (priv->bound_model != NULL && selected_index < g_list_model_get_n_items (priv->bound_model)));
+
+ if (priv->selected_index == selected_index)
+ return;
+
+ priv->selected_index = selected_index;
+ update (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_INDEX]);
+}
+
+/**
+ * hdy_combo_row_get_use_subtitle:
+ * @self: a #GtkListBoxRow
+ *
+ * Gets whether the current value of @self should be displayed as its subtitle.
+ *
+ * Returns: whether the current value of @self should be displayed as its subtitle
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_combo_row_get_use_subtitle (HdyComboRow *self)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_COMBO_ROW (self), FALSE);
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ return priv->use_subtitle;
+}
+
+/**
+ * hdy_combo_row_set_use_subtitle:
+ * @self: a #HdyComboRow
+ * @use_subtitle: %TRUE to set the current value as the subtitle
+ *
+ * Sets whether the current value of @self should be displayed as its subtitle.
+ *
+ * If %TRUE, you should not access HdyActionRow:subtitle.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_combo_row_set_use_subtitle (HdyComboRow *self,
+ gboolean use_subtitle)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ use_subtitle = !!use_subtitle;
+
+ if (priv->use_subtitle == use_subtitle)
+ return;
+
+ priv->use_subtitle = use_subtitle;
+ update (self);
+ if (!use_subtitle)
+ hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_SUBTITLE]);
+}
+
+/**
+ * hdy_combo_row_set_get_name_func:
+ * @self: a #HdyComboRow
+ * @get_name_func: (nullable): a function that creates names for items, or %NULL
+ * in case you also passed %NULL as @model
+ * @user_data: user data passed to @get_name_func
+ * @user_data_free_func: function for freeing @user_data
+ *
+ * Sets a closure to convert items into names. See HdyComboRow:use-subtitle.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_combo_row_set_get_name_func (HdyComboRow *self,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func)
+{
+ HdyComboRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_COMBO_ROW (self));
+
+ priv = hdy_combo_row_get_instance_private (self);
+
+ get_name_free (priv->get_name);
+ priv->get_name = g_new0 (HdyComboRowGetName, 1);
+ priv->get_name->func = get_name_func;
+ priv->get_name->func_data = user_data;
+ priv->get_name->func_data_destroy = user_data_free_func;
+}
+
+/**
+ * hdy_enum_value_row_name:
+ * @value: the value from the enum from which to get a name
+ * @user_data: (closure): unused user data
+ *
+ * This is a default implementation of #HdyComboRowGetEnumValueNameFunc to be
+ * used with hdy_combo_row_set_for_enum(). If the enumeration has a nickname, it
+ * will return it, otherwise it will return its name.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @value
+ *
+ * Since: 0.0.6
+ */
+gchar *
+hdy_enum_value_row_name (HdyEnumValueObject *value,
+ gpointer user_data)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (value), NULL);
+
+ return g_strdup (hdy_enum_value_object_get_nick (value) != NULL ?
+ hdy_enum_value_object_get_nick (value) :
+ hdy_enum_value_object_get_name (value));
+}
diff --git a/subprojects/libhandy/src/hdy-combo-row.h b/subprojects/libhandy/src/hdy-combo-row.h
new file mode 100644
index 0000000..68657a0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.h
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enum-value-object.h"
+#include "hdy-action-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_COMBO_ROW (hdy_combo_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyComboRow, hdy_combo_row, HDY, COMBO_ROW, HdyActionRow)
+
+/**
+ * HdyComboRowGetNameFunc:
+ * @item: (type GObject): the item from the model from which to get a name
+ * @user_data: (closure): user data
+ *
+ * Called for combo rows that are bound to a #GListModel with
+ * hdy_combo_row_bind_name_model() for each item that gets added to the model.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @item
+ */
+typedef gchar * (*HdyComboRowGetNameFunc) (gpointer item,
+ gpointer user_data);
+
+/**
+ * HdyComboRowGetEnumValueNameFunc:
+ * @value: the value from the enum from which to get a name
+ * @user_data: (closure): user data
+ *
+ * Called for combo rows that are bound to an enumeration with
+ * hdy_combo_row_set_for_enum() for each value from that enumeration.
+ *
+ * Returns: (transfer full): a newly allocated displayable name that represents @value
+ */
+typedef gchar * (*HdyComboRowGetEnumValueNameFunc) (HdyEnumValueObject *value,
+ gpointer user_data);
+
+/**
+ * HdyComboRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyComboRowClass
+{
+ HdyActionRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_combo_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+GListModel *hdy_combo_row_get_model (HdyComboRow *self);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_bind_model (HdyComboRow *self,
+ GListModel *model,
+ GtkListBoxCreateWidgetFunc create_list_widget_func,
+ GtkListBoxCreateWidgetFunc create_current_widget_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_bind_name_model (HdyComboRow *self,
+ GListModel *model,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_for_enum (HdyComboRow *self,
+ GType enum_type,
+ HdyComboRowGetEnumValueNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+
+HDY_AVAILABLE_IN_ALL
+gint hdy_combo_row_get_selected_index (HdyComboRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_selected_index (HdyComboRow *self,
+ gint selected_index);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_combo_row_get_use_subtitle (HdyComboRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_use_subtitle (HdyComboRow *self,
+ gboolean use_subtitle);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_combo_row_set_get_name_func (HdyComboRow *self,
+ HdyComboRowGetNameFunc get_name_func,
+ gpointer user_data,
+ GDestroyNotify user_data_free_func);
+
+HDY_AVAILABLE_IN_ALL
+gchar *hdy_enum_value_row_name (HdyEnumValueObject *value,
+ gpointer user_data);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-combo-row.ui b/subprojects/libhandy/src/hdy-combo-row.ui
new file mode 100644
index 0000000..080a591
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-combo-row.ui
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyComboRow" parent="HdyActionRow">
+ <property name="activatable">False</property>
+ <child>
+ <object class="GtkBox" id="current">
+ <property name="halign">end</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="icon_name">pan-down-symbolic</property>
+ <property name="icon_size">1</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </template>
+ <object class="GtkPopover" id="popover">
+ <property name="position">bottom</property>
+ <property name="relative_to">image</property>
+ <style>
+ <class name="combo"/>
+ </style>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="hscrollbar_policy">never</property>
+ <property name="max_content_height">400</property>
+ <property name="propagate_natural_width">True</property>
+ <property name="propagate_natural_height">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="selection_mode">none</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-css-private.h b/subprojects/libhandy/src/hdy-css-private.h
new file mode 100644
index 0000000..d8190b5
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-css-private.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+void hdy_css_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural);
+
+void hdy_css_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-css.c b/subprojects/libhandy/src/hdy-css.c
new file mode 100644
index 0000000..7a056e2
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-css.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-css-private.h"
+
+void
+hdy_css_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ GtkStyleContext *style_context = gtk_widget_get_style_context (widget);
+ GtkStateFlags state_flags = gtk_widget_get_state_flags (widget);
+ GtkBorder border, margin, padding;
+ gint css_width, css_height;
+
+ /* Manually apply minimum sizes, the border, the padding and the margin as we
+ * can't use the private GtkGagdet.
+ */
+ gtk_style_context_get (style_context, state_flags,
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+ gtk_style_context_get_border (style_context, state_flags, &border);
+ gtk_style_context_get_margin (style_context, state_flags, &margin);
+ gtk_style_context_get_padding (style_context, state_flags, &padding);
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ *minimum = MAX (*minimum, css_height) +
+ border.top + margin.top + padding.top +
+ border.bottom + margin.bottom + padding.bottom;
+ *natural = MAX (*natural, css_height) +
+ border.top + margin.top + padding.top +
+ border.bottom + margin.bottom + padding.bottom;
+ } else {
+ *minimum = MAX (*minimum, css_width) +
+ border.left + margin.left + padding.left +
+ border.right + margin.right + padding.right;
+ *natural = MAX (*natural, css_width) +
+ border.left + margin.left + padding.left +
+ border.right + margin.right + padding.right;
+ }
+}
+
+void
+hdy_css_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkStyleContext *style_context;
+ GtkStateFlags state_flags;
+ GtkBorder border, margin, padding;
+
+ /* Manually apply the border, the padding and the margin as we can't use the
+ * private GtkGagdet.
+ */
+ style_context = gtk_widget_get_style_context (widget);
+ state_flags = gtk_widget_get_state_flags (widget);
+ gtk_style_context_get_border (style_context, state_flags, &border);
+ gtk_style_context_get_margin (style_context, state_flags, &margin);
+ gtk_style_context_get_padding (style_context, state_flags, &padding);
+ allocation->width -= border.left + border.right +
+ margin.left + margin.right +
+ padding.left + padding.right;
+ allocation->height -= border.top + border.bottom +
+ margin.top + margin.bottom +
+ padding.top + padding.bottom;
+ allocation->x += border.left + margin.left + padding.left;
+ allocation->y += border.top + margin.top + padding.top;
+}
diff --git a/subprojects/libhandy/src/hdy-deck.c b/subprojects/libhandy/src/hdy-deck.c
new file mode 100644
index 0000000..01dc45e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deck.c
@@ -0,0 +1,1103 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-deck.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-deck
+ * @short_description: A swipeable widget showing one of the visible children at a time.
+ * @Title: HdyDeck
+ *
+ * The #HdyDeck widget displays one of the visible children, similar to a
+ * #GtkStack. The children are strictly ordered and can be navigated using
+ * swipe gestures.
+ *
+ * The “over” and “under” stack the children one on top of the other, while the
+ * “slide” transition puts the children side by side. While navigating to a
+ * child on the side or below can be performed by swiping the current child
+ * away, navigating to an upper child requires dragging it from the edge where
+ * it resides. This doesn't affect non-dragging swipes.
+ *
+ * The “over” and “under” transitions can draw their shadow on top of the
+ * window's transparent areas, like the rounded corners. This is a side-effect
+ * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated
+ * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn
+ * beyond the rounded corners.
+ *
+ * # CSS nodes
+ *
+ * #HdyDeck has a single CSS node with name deck.
+ *
+ * Since: 1.0
+ */
+
+/**
+ * HdyDeckTransitionType:
+ * @HDY_DECK_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_DECK_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_DECK_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between children
+ * in a #HdyDeck widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_HHOMOGENEOUS,
+ PROP_VHOMOGENEOUS,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+
+ /* orientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ORIENTATION,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_NAME,
+ LAST_CHILD_PROP,
+};
+
+typedef struct
+{
+ HdyStackableBox *box;
+} HdyDeckPrivate;
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+static void hdy_deck_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyDeck, hdy_deck, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyDeck)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_deck_swipeable_init))
+
+#define HDY_GET_HELPER(obj) (((HdyDeckPrivate *) hdy_deck_get_instance_private (HDY_DECK (obj)))->box)
+
+/**
+ * hdy_deck_set_homogeneous:
+ * @self: a #HdyDeck
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyDeck to be homogeneous or not for the given orientation.
+ * If it is homogeneous, the #HdyDeck will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't, the deck may change width or height when a different child
+ * becomes visible.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_homogeneous (HdyDeck *self,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), TRUE, orientation, homogeneous);
+}
+
+/**
+ * hdy_deck_get_homogeneous:
+ * @self: a #HdyDeck
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given orientation.
+ * See hdy_deck_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given orientation.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_homogeneous (HdyDeck *self,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), TRUE, orientation);
+}
+
+/**
+ * hdy_deck_get_transition_type:
+ * @self: a #HdyDeck
+ *
+ * Gets the type of animation that will be used
+ * for transitions between children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 1.0
+ */
+HdyDeckTransitionType
+hdy_deck_get_transition_type (HdyDeck *self)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_val_if_fail (HDY_IS_DECK (self), HDY_DECK_TRANSITION_TYPE_OVER);
+
+ type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self));
+
+ switch (type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return HDY_DECK_TRANSITION_TYPE_OVER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return HDY_DECK_TRANSITION_TYPE_UNDER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ return HDY_DECK_TRANSITION_TYPE_SLIDE;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+/**
+ * hdy_deck_set_transition_type:
+ * @self: a #HdyDeck
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between children
+ * in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about to become
+ * current.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_transition_type (HdyDeck *self,
+ HdyDeckTransitionType transition)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_if_fail (HDY_IS_DECK (self));
+ g_return_if_fail (transition <= HDY_DECK_TRANSITION_TYPE_SLIDE);
+
+ switch (transition) {
+ case HDY_DECK_TRANSITION_TYPE_OVER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ break;
+
+ case HDY_DECK_TRANSITION_TYPE_UNDER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ break;
+
+ case HDY_DECK_TRANSITION_TYPE_SLIDE:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE;
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type);
+}
+
+/**
+ * hdy_deck_get_transition_duration:
+ * @self: a #HdyDeck
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_deck_get_transition_duration (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), 0);
+
+ return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_transition_duration:
+ * @self: a #HdyDeck
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_transition_duration (HdyDeck *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_deck_get_visible_child:
+ * @self: a #HdyDeck
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_visible_child (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_visible_child:
+ * @self: a #HdyDeck
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyDeck:transition-type and HdyDeck:transition-duration. The transition can
+ * be cancelled by the user, in which case visible child will change back to
+ * the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_visible_child (HdyDeck *self,
+ GtkWidget *visible_child)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child);
+}
+
+/**
+ * hdy_deck_get_visible_child_name:
+ * @self: a #HdyDeck
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_deck_get_visible_child_name (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_visible_child_name:
+ * @self: a #HdyDeck
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_deck_set_visible_child() for more details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_visible_child_name (HdyDeck *self,
+ const gchar *name)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name);
+}
+
+/**
+ * hdy_deck_get_transition_running:
+ * @self: a #HdyDeck
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_transition_running (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_interpolate_size:
+ * @self: a #HdyDeck
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyDeck:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_interpolate_size (HdyDeck *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size);
+}
+
+/**
+ * hdy_deck_get_interpolate_size:
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_interpolate_size (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_can_swipe_back:
+ * @self: a #HdyDeck
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_can_swipe_back (HdyDeck *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back);
+}
+
+/**
+ * hdy_deck_get_can_swipe_back
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_can_swipe_back (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_set_can_swipe_forward:
+ * @self: a #HdyDeck
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_deck_set_can_swipe_forward (HdyDeck *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_DECK (self));
+
+ hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward);
+}
+
+/**
+ * hdy_deck_get_can_swipe_forward
+ * @self: a #HdyDeck
+ *
+ * Returns whether the #HdyDeck allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_get_can_swipe_forward (HdyDeck *self)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_deck_get_adjacent_child
+ * @self: a #HdyDeck
+ * @direction: the direction
+ *
+ * Gets the previous or next child, or %NULL if it doesn't exist. This will be
+ * the same widget hdy_deck_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_adjacent_child (HdyDeck *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_deck_navigate
+ * @self: a #HdyDeck
+ * @direction: the direction
+ *
+ * Switches to the previous or next child, similar to performing a swipe
+ * gesture to go in @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_deck_navigate (HdyDeck *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), FALSE);
+
+ return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_deck_get_child_by_name:
+ * @self: a #HdyDeck
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_deck_get_child_by_name (HdyDeck *self,
+ const gchar *name)
+{
+ g_return_val_if_fail (HDY_IS_DECK (self), NULL);
+
+ return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name);
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_deck_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ hdy_stackable_box_measure (HDY_GET_HELPER (widget),
+ orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_deck_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_deck_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_deck_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_deck_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation);
+}
+
+static gboolean
+hdy_deck_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr);
+}
+
+static void
+hdy_deck_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction);
+}
+
+static void
+hdy_deck_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_add (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_deck_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_remove (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_deck_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data);
+}
+
+static void
+hdy_deck_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyDeck *self = HDY_DECK (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS:
+ g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS:
+ g_value_set_boolean (value, hdy_deck_get_homogeneous (self, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_deck_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_deck_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_deck_get_transition_type (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_deck_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_deck_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_deck_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_deck_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_deck_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_deck_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyDeck *self = HDY_DECK (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS:
+ hdy_deck_set_homogeneous (self, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS:
+ hdy_deck_set_homogeneous (self, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_deck_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_deck_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_deck_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_deck_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_deck_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_deck_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_deck_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_deck_finalize (GObject *object)
+{
+ HdyDeck *self = HDY_DECK (object);
+ HdyDeckPrivate *priv = hdy_deck_get_instance_private (self);
+
+ g_clear_object (&priv->box);
+
+ G_OBJECT_CLASS (hdy_deck_parent_class)->finalize (object);
+}
+
+static void
+hdy_deck_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_deck_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_deck_realize (GtkWidget *widget)
+{
+ hdy_stackable_box_realize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_unrealize (GtkWidget *widget)
+{
+ hdy_stackable_box_unrealize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_map (GtkWidget *widget)
+{
+ hdy_stackable_box_map (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_unmap (GtkWidget *widget)
+{
+ hdy_stackable_box_unmap (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_deck_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration);
+}
+
+static HdySwipeTracker *
+hdy_deck_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_deck_get_distance (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble *
+hdy_deck_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points);
+}
+
+static gdouble
+hdy_deck_get_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_deck_get_cancel_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable));
+}
+
+static void
+hdy_deck_get_swipe_area (HdySwipeable *swipeable,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect);
+}
+
+static void
+hdy_deck_class_init (HdyDeckClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = (GtkWidgetClass*) klass;
+ GtkContainerClass *container_class = (GtkContainerClass*) klass;
+
+ object_class->get_property = hdy_deck_get_property;
+ object_class->set_property = hdy_deck_set_property;
+ object_class->finalize = hdy_deck_finalize;
+
+ widget_class->realize = hdy_deck_realize;
+ widget_class->unrealize = hdy_deck_unrealize;
+ widget_class->map = hdy_deck_map;
+ widget_class->unmap = hdy_deck_unmap;
+ widget_class->get_preferred_width = hdy_deck_get_preferred_width;
+ widget_class->get_preferred_height = hdy_deck_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_deck_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_deck_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_deck_size_allocate;
+ widget_class->draw = hdy_deck_draw;
+ widget_class->direction_changed = hdy_deck_direction_changed;
+
+ container_class->add = hdy_deck_add;
+ container_class->remove = hdy_deck_remove;
+ container_class->forall = hdy_deck_forall;
+ container_class->set_child_property = hdy_deck_set_child_property;
+ container_class->get_child_property = hdy_deck_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyDeck:hhomogeneous:
+ *
+ * Horizontally homogeneous sizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_HHOMOGENEOUS] =
+ g_param_spec_boolean ("hhomogeneous",
+ _("Horizontally homogeneous"),
+ _("Horizontally homogeneous sizing"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:vhomogeneous:
+ *
+ * Vertically homogeneous sizing.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VHOMOGENEOUS] =
+ g_param_spec_boolean ("vhomogeneous",
+ _("Vertically homogeneous"),
+ _("Vertically homogeneous sizing"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:visible-child:
+ *
+ * The widget currently visible.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:visible-child-name:
+ *
+ * The name of the widget currently visible.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-type:
+ *
+ * The type of animation that will be used for transitions between
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about
+ * to become current.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between children"),
+ HDY_TYPE_DECK_TRANSITION_TYPE, HDY_DECK_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-duration:
+ *
+ * The transition animation duration, in milliseconds.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:transition-running:
+ *
+ * Whether or not the transition is currently running.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ /**
+ * HdyDeck:interpolate-size:
+ *
+ * Whether or not the size should smoothly change when changing between
+ * differently sized children.
+ *
+ * Since: 1.0
+ */
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:can-swipe-back:
+ *
+ * Whether or not the deck allows switching to the previous child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyDeck:can-swipe-forward:
+ *
+ * Whether or not the deck allows switching to the next child via a swipe
+ * gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_NAME] =
+ g_param_spec_string ("name",
+ _("Name"),
+ _("The name of the child page"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "deck");
+}
+
+GtkWidget *
+hdy_deck_new (void)
+{
+ return g_object_new (HDY_TYPE_DECK, NULL);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (HdyDeck *self) { \
+ g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \
+}
+
+NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS);
+NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS);
+NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD);
+NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME);
+NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE);
+NOTIFY (notify_child_transition_duration_cb, PROP_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_running_cb, PROP_TRANSITION_RUNNING);
+NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE);
+NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK);
+NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD);
+
+static void
+notify_orientation_cb (HdyDeck *self)
+{
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_deck_init (HdyDeck *self)
+{
+ HdyDeckPrivate *priv = hdy_deck_get_instance_private (self);
+
+ priv->box = hdy_stackable_box_new (GTK_CONTAINER (self),
+ GTK_CONTAINER_CLASS (hdy_deck_parent_class),
+ FALSE);
+
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_deck_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_deck_switch_child;
+ iface->get_swipe_tracker = hdy_deck_get_swipe_tracker;
+ iface->get_distance = hdy_deck_get_distance;
+ iface->get_snap_points = hdy_deck_get_snap_points;
+ iface->get_progress = hdy_deck_get_progress;
+ iface->get_cancel_progress = hdy_deck_get_cancel_progress;
+ iface->get_swipe_area = hdy_deck_get_swipe_area;
+}
diff --git a/subprojects/libhandy/src/hdy-deck.h b/subprojects/libhandy/src/hdy-deck.h
new file mode 100644
index 0000000..3c7da18
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deck.h
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_DECK (hdy_deck_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyDeck, hdy_deck, HDY, DECK, GtkContainer)
+
+typedef enum {
+ HDY_DECK_TRANSITION_TYPE_OVER,
+ HDY_DECK_TRANSITION_TYPE_UNDER,
+ HDY_DECK_TRANSITION_TYPE_SLIDE,
+} HdyDeckTransitionType;
+
+/**
+ * HdyDeckClass
+ * @parent_class: The parent class
+ */
+struct _HdyDeckClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_new (void);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_visible_child (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_visible_child (HdyDeck *self,
+ GtkWidget *visible_child);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_deck_get_visible_child_name (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_visible_child_name (HdyDeck *self,
+ const gchar *name);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_homogeneous (HdyDeck *self,
+ GtkOrientation orientation);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_homogeneous (HdyDeck *self,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HDY_AVAILABLE_IN_ALL
+HdyDeckTransitionType hdy_deck_get_transition_type (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_transition_type (HdyDeck *self,
+ HdyDeckTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_deck_get_transition_duration (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_transition_duration (HdyDeck *self,
+ guint duration);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_transition_running (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_interpolate_size (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_interpolate_size (HdyDeck *self,
+ gboolean interpolate_size);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_can_swipe_back (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_can_swipe_back (HdyDeck *self,
+ gboolean can_swipe_back);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_get_can_swipe_forward (HdyDeck *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_deck_set_can_swipe_forward (HdyDeck *self,
+ gboolean can_swipe_forward);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_adjacent_child (HdyDeck *self,
+ HdyNavigationDirection direction);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_deck_navigate (HdyDeck *self,
+ HdyNavigationDirection direction);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_deck_get_child_by_name (HdyDeck *self,
+ const gchar *name);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-deprecation-macros.h b/subprojects/libhandy/src/hdy-deprecation-macros.h
new file mode 100644
index 0000000..e889135
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-deprecation-macros.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#if defined(HDY_DISABLE_DEPRECATION_WARNINGS) || defined(HANDY_COMPILATION)
+# define _HDY_DEPRECATED
+# define _HDY_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_MACRO
+# define _HDY_DEPRECATED_MACRO_FOR(f)
+# define _HDY_DEPRECATED_ENUMERATOR
+# define _HDY_DEPRECATED_ENUMERATOR_FOR(f)
+# define _HDY_DEPRECATED_TYPE
+# define _HDY_DEPRECATED_TYPE_FOR(f)
+#else
+# define _HDY_DEPRECATED G_DEPRECATED
+# define _HDY_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_MACRO G_DEPRECATED
+# define _HDY_DEPRECATED_MACRO_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_ENUMERATOR G_DEPRECATED
+# define _HDY_DEPRECATED_ENUMERATOR_FOR(f) G_DEPRECATED_FOR(f)
+# define _HDY_DEPRECATED_TYPE G_DEPRECATED
+# define _HDY_DEPRECATED_TYPE_FOR(f) G_DEPRECATED_FOR(f)
+#endif
diff --git a/subprojects/libhandy/src/hdy-enum-value-object.c b/subprojects/libhandy/src/hdy-enum-value-object.c
new file mode 100644
index 0000000..58ca13a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enum-value-object.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-enum-value-object.h"
+
+/**
+ * SECTION:hdy-enum-value-object
+ * @short_description: An object representing a #GEnumValue.
+ * @Title: HdyEnumValueObject
+ *
+ * The #HdyEnumValueObject object represents a #GEnumValue, allowing it to be
+ * used with #GListModel.
+ *
+ * Since: 0.0.6
+ */
+
+struct _HdyEnumValueObject
+{
+ GObject parent_instance;
+
+ GEnumValue enum_value;
+};
+
+G_DEFINE_TYPE (HdyEnumValueObject, hdy_enum_value_object, G_TYPE_OBJECT)
+
+HdyEnumValueObject *
+hdy_enum_value_object_new (GEnumValue *enum_value)
+{
+ HdyEnumValueObject *self = g_object_new (HDY_TYPE_ENUM_VALUE_OBJECT, NULL);
+
+ self->enum_value = *enum_value;
+
+ return self;
+}
+
+static void
+hdy_enum_value_object_class_init (HdyEnumValueObjectClass *klass)
+{
+}
+
+static void
+hdy_enum_value_object_init (HdyEnumValueObject *self)
+{
+}
+
+gint
+hdy_enum_value_object_get_value (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), 0);
+
+ return self->enum_value.value;
+}
+
+const gchar *
+hdy_enum_value_object_get_name (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL);
+
+ return self->enum_value.value_name;
+}
+
+const gchar *
+hdy_enum_value_object_get_nick (HdyEnumValueObject *self)
+{
+ g_return_val_if_fail (HDY_IS_ENUM_VALUE_OBJECT (self), NULL);
+
+ return self->enum_value.value_nick;
+}
diff --git a/subprojects/libhandy/src/hdy-enum-value-object.h b/subprojects/libhandy/src/hdy-enum-value-object.h
new file mode 100644
index 0000000..b961a62
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enum-value-object.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_ENUM_VALUE_OBJECT (hdy_enum_value_object_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyEnumValueObject, hdy_enum_value_object, HDY, ENUM_VALUE_OBJECT, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdyEnumValueObject *hdy_enum_value_object_new (GEnumValue *enum_value);
+
+HDY_AVAILABLE_IN_ALL
+gint hdy_enum_value_object_get_value (HdyEnumValueObject *self);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_enum_value_object_get_name (HdyEnumValueObject *self);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_enum_value_object_get_nick (HdyEnumValueObject *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-enums-private.c.in b/subprojects/libhandy/src/hdy-enums-private.c.in
new file mode 100644
index 0000000..5a11cda
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums-private.c.in
@@ -0,0 +1,38 @@
+/*** BEGIN file-header ***/
+
+#include "config.h"
+#include "hdy-enums-private.h"
+#include "hdy-stackable-box-private.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType etype = 0;
+ if (G_UNLIKELY(etype == 0)) {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@, "@VALUENAME@", "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+ etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
+ }
+ return etype;
+}
+
+/*** END value-tail ***/
+
+/*** BEGIN file-tail ***/
+
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums-private.h.in b/subprojects/libhandy/src/hdy-enums-private.h.in
new file mode 100644
index 0000000..1955f4e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums-private.h.in
@@ -0,0 +1,27 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+
+/* enumerations from "@basename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType @enum_name@_get_type (void);
+#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ())
+/*** END value-header ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums.c.in b/subprojects/libhandy/src/hdy-enums.c.in
new file mode 100644
index 0000000..a630555
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums.c.in
@@ -0,0 +1,44 @@
+/*** BEGIN file-header ***/
+
+#include "config.h"
+#include "hdy-deck.h"
+#include "hdy-enums.h"
+#include "hdy-header-bar.h"
+#include "hdy-header-group.h"
+#include "hdy-leaflet.h"
+#include "hdy-navigation-direction.h"
+#include "hdy-squeezer.h"
+#include "hdy-view-switcher.h"
+
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+/* enumerations from "@filename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+GType
+@enum_name@_get_type (void)
+{
+ static GType etype = 0;
+ if (G_UNLIKELY(etype == 0)) {
+ static const G@Type@Value values[] = {
+/*** END value-header ***/
+
+/*** BEGIN value-production ***/
+ { @VALUENAME@, "@VALUENAME@", "@valuenick@" },
+/*** END value-production ***/
+
+/*** BEGIN value-tail ***/
+ { 0, NULL, NULL }
+ };
+ etype = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values);
+ }
+ return etype;
+}
+
+/*** END value-tail ***/
+
+/*** BEGIN file-tail ***/
+
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-enums.h.in b/subprojects/libhandy/src/hdy-enums.h.in
new file mode 100644
index 0000000..7b39850
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-enums.h.in
@@ -0,0 +1,28 @@
+/*** BEGIN file-header ***/
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+/*** END file-header ***/
+
+/*** BEGIN file-production ***/
+
+/* enumerations from "@basename@" */
+/*** END file-production ***/
+
+/*** BEGIN value-header ***/
+
+HDY_AVAILABLE_IN_ALL GType @enum_name@_get_type (void);
+#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ())
+/*** END value-header ***/
+
+/*** BEGIN file-tail ***/
+G_END_DECLS
+/*** END file-tail ***/
diff --git a/subprojects/libhandy/src/hdy-expander-row.c b/subprojects/libhandy/src/hdy-expander-row.c
new file mode 100644
index 0000000..55bf695
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.c
@@ -0,0 +1,762 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-expander-row.h"
+
+#include <glib/gi18n-lib.h>
+#include "hdy-action-row.h"
+
+/**
+ * SECTION:hdy-expander-row
+ * @short_description: A #GtkListBox row used to reveal widgets.
+ * @Title: HdyExpanderRow
+ *
+ * The #HdyExpanderRow allows the user to reveal or hide widgets below it. It
+ * also allows the user to enable the expansion of the row, allowing to disable
+ * all that the row contains.
+ *
+ * It also supports adding a child as an action widget by specifying “action” as
+ * the “type” attribute of a &lt;child&gt; element. It also supports setting a
+ * child as a prefix widget by specifying “prefix” as the “type” attribute of a
+ * &lt;child&gt; element.
+ *
+ * # CSS nodes
+ *
+ * #HdyExpanderRow has a main CSS node with name row, and the .expander style
+ * class. It has the .empty style class when it contains no children.
+ *
+ * It contains the subnodes row.header for its main embedded row, list.nested
+ * for the list it can expand, and image.expander-row-arrow for its arrow.
+ *
+ * When expanded, #HdyExpanderRow will add the
+ * .checked-expander-row-previous-sibling style class to its previous sibling,
+ * and remove it when retracted.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkBox *actions;
+ GtkBox *prefixes;
+ GtkListBox *list;
+ HdyActionRow *action_row;
+ GtkSwitch *enable_switch;
+ GtkImage *image;
+
+ gboolean expanded;
+ gboolean enable_expansion;
+ gboolean show_enable_switch;
+} HdyExpanderRowPrivate;
+
+static void hdy_expander_row_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyExpanderRow, hdy_expander_row, HDY_TYPE_PREFERENCES_ROW,
+ G_ADD_PRIVATE (HdyExpanderRow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_expander_row_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_SUBTITLE,
+ PROP_USE_UNDERLINE,
+ PROP_ICON_NAME,
+ PROP_EXPANDED,
+ PROP_ENABLE_EXPANSION,
+ PROP_SHOW_ENABLE_SWITCH,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_arrow (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (self));
+ GtkWidget *previous_sibling = NULL;
+
+ if (parent) {
+ g_autoptr (GList) siblings = gtk_container_get_children (GTK_CONTAINER (parent));
+ GList *l;
+
+ for (l = siblings; l != NULL && l->next != NULL && l->next->data != self; l = l->next);
+
+ if (l && l->next && l->next->data == self)
+ previous_sibling = l->data;
+ }
+
+ if (priv->expanded)
+ gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE);
+ else
+ gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED);
+
+ if (previous_sibling) {
+ GtkStyleContext *previous_sibling_context = gtk_widget_get_style_context (previous_sibling);
+
+ if (priv->expanded)
+ gtk_style_context_add_class (previous_sibling_context, "checked-expander-row-previous-sibling");
+ else
+ gtk_style_context_remove_class (previous_sibling_context, "checked-expander-row-previous-sibling");
+ }
+}
+
+static void
+hdy_expander_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_expander_row_get_subtitle (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_expander_row_get_use_underline (self));
+ break;
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_expander_row_get_icon_name (self));
+ break;
+ case PROP_EXPANDED:
+ g_value_set_boolean (value, hdy_expander_row_get_expanded (self));
+ break;
+ case PROP_ENABLE_EXPANSION:
+ g_value_set_boolean (value, hdy_expander_row_get_enable_expansion (self));
+ break;
+ case PROP_SHOW_ENABLE_SWITCH:
+ g_value_set_boolean (value, hdy_expander_row_get_show_enable_switch (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_expander_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (object);
+
+ switch (prop_id) {
+ case PROP_SUBTITLE:
+ hdy_expander_row_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_expander_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ case PROP_ICON_NAME:
+ hdy_expander_row_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_EXPANDED:
+ hdy_expander_row_set_expanded (self, g_value_get_boolean (value));
+ break;
+ case PROP_ENABLE_EXPANSION:
+ hdy_expander_row_set_enable_expansion (self, g_value_get_boolean (value));
+ break;
+ case PROP_SHOW_ENABLE_SWITCH:
+ hdy_expander_row_set_show_enable_switch (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_expander_row_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else {
+ if (priv->prefixes)
+ gtk_container_foreach (GTK_CONTAINER (priv->prefixes), callback, callback_data);
+ if (priv->actions)
+ gtk_container_foreach (GTK_CONTAINER (priv->actions), callback, callback_data);
+ if (priv->list)
+ gtk_container_foreach (GTK_CONTAINER (priv->list), callback, callback_data);
+ }
+}
+
+static void
+activate_cb (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_expander_row_set_expanded (self, !priv->expanded);
+}
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+list_children_changed_cb (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ gint count = 0;
+
+ gtk_container_foreach (GTK_CONTAINER (priv->list), (GtkCallback) count_children_cb, &count);
+
+ if (count == 0)
+ gtk_style_context_add_class (context, "empty");
+ else
+ gtk_style_context_remove_class (context, "empty");
+}
+
+static void
+hdy_expander_row_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ /* When constructing the widget, we want the box to be added as the child of
+ * the GtkListBoxRow, as an implementation detail.
+ */
+ if (priv->box == NULL)
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->add (container, child);
+ else
+ gtk_container_add (GTK_CONTAINER (priv->list), child);
+}
+
+static void
+hdy_expander_row_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (container);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->box))
+ GTK_CONTAINER_CLASS (hdy_expander_row_parent_class)->remove (container, child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->actions))
+ gtk_container_remove (GTK_CONTAINER (priv->actions), child);
+ else if (gtk_widget_get_parent (child) == GTK_WIDGET (priv->prefixes))
+ gtk_container_remove (GTK_CONTAINER (priv->prefixes), child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->list), child);
+}
+
+static void
+hdy_expander_row_class_init (HdyExpanderRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_expander_row_get_property;
+ object_class->set_property = hdy_expander_row_set_property;
+
+ container_class->add = hdy_expander_row_add;
+ container_class->remove = hdy_expander_row_remove;
+ container_class->forall = hdy_expander_row_forall;
+
+ /**
+ * HdyExpanderRow:subtitle:
+ *
+ * The subtitle for this row.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle for this row"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title and subtitle labels
+ * indicates a mnemonic.
+ *
+ * Since: 1.0
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:icon-name:
+ *
+ * The icon name for this row.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:expanded:
+ *
+ * %TRUE if the row is expanded.
+ */
+ props[PROP_EXPANDED] =
+ g_param_spec_boolean ("expanded",
+ _("Expanded"),
+ _("Whether the row is expanded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:enable-expansion:
+ *
+ * %TRUE if the expansion is enabled.
+ */
+ props[PROP_ENABLE_EXPANSION] =
+ g_param_spec_boolean ("enable-expansion",
+ _("Enable expansion"),
+ _("Whether the expansion is enabled"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyExpanderRow:show-enable-switch:
+ *
+ * %TRUE if the switch enabling the expansion is visible.
+ */
+ props[PROP_SHOW_ENABLE_SWITCH] =
+ g_param_spec_boolean ("show-enable-switch",
+ _("Show enable switch"),
+ _("Whether the switch enabling the expansion is visible"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-expander-row.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, action_row);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, actions);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, list);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, image);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyExpanderRow, enable_switch);
+ gtk_widget_class_bind_template_callback (widget_class, activate_cb);
+ gtk_widget_class_bind_template_callback (widget_class, list_children_changed_cb);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (gpointer this) { \
+ g_object_notify_by_pspec (G_OBJECT (this), props[prop]); \
+} \
+
+NOTIFY (notify_subtitle_cb, PROP_SUBTITLE);
+NOTIFY (notify_use_underline_cb, PROP_USE_UNDERLINE);
+NOTIFY (notify_icon_name_cb, PROP_ICON_NAME);
+
+static void
+hdy_expander_row_init (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ priv->prefixes = NULL;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ hdy_expander_row_set_enable_expansion (self, TRUE);
+ hdy_expander_row_set_expanded (self, FALSE);
+
+ g_signal_connect_object (priv->action_row, "notify::subtitle", G_CALLBACK (notify_subtitle_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->action_row, "notify::use-underline", G_CALLBACK (notify_use_underline_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->action_row, "notify::icon-name", G_CALLBACK (notify_icon_name_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_expander_row_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ HdyExpanderRow *self = HDY_EXPANDER_ROW (buildable);
+ HdyExpanderRowPrivate *priv = hdy_expander_row_get_instance_private (self);
+
+ if (priv->box == NULL || !type)
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+ else if (type && strcmp (type, "action") == 0)
+ hdy_expander_row_add_action (self, GTK_WIDGET (child));
+ else if (type && strcmp (type, "prefix") == 0)
+ hdy_expander_row_add_prefix (self, GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (self, type);
+}
+
+static void
+hdy_expander_row_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+ iface->add_child = hdy_expander_row_buildable_add_child;
+}
+
+/**
+ * hdy_expander_row_new:
+ *
+ * Creates a new #HdyExpanderRow.
+ *
+ * Returns: a new #HdyExpanderRow
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_expander_row_new (void)
+{
+ return g_object_new (HDY_TYPE_EXPANDER_ROW, NULL);
+}
+
+/**
+ * hdy_expander_row_get_subtitle:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets the subtitle for @self.
+ *
+ * Returns: (transfer none) (nullable): the subtitle for @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_expander_row_get_subtitle (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_subtitle (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_subtitle:
+ * @self: a #HdyExpanderRow
+ * @subtitle: (nullable): the subtitle
+ *
+ * Sets the subtitle for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_subtitle (HdyExpanderRow *self,
+ const gchar *subtitle)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_subtitle (priv->action_row, subtitle);
+}
+
+/**
+ * hdy_expander_row_get_use_underline:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether an embedded underline in the text of the title and subtitle
+ * labels indicates a mnemonic. See hdy_expander_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title and subtitle labels
+ * indicates the mnemonic accelerator keys.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_expander_row_get_use_underline (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_use_underline (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_use_underline:
+ * @self: a #HdyExpanderRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title and subtitle labels indicates
+ * the next character should be used for the mnemonic accelerator key.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_use_underline (HdyExpanderRow *self,
+ gboolean use_underline)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_use_underline (priv->action_row, use_underline);
+}
+
+/**
+ * hdy_expander_row_get_icon_name:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets the icon name for @self.
+ *
+ * Returns: the icon name for @self.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_expander_row_get_icon_name (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), NULL);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return hdy_action_row_get_icon_name (priv->action_row);
+}
+
+/**
+ * hdy_expander_row_set_icon_name:
+ * @self: a #HdyExpanderRow
+ * @icon_name: the icon name
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_set_icon_name (HdyExpanderRow *self,
+ const gchar *icon_name)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ hdy_action_row_set_icon_name (priv->action_row, icon_name);
+}
+
+gboolean
+hdy_expander_row_get_expanded (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->expanded;
+}
+
+void
+hdy_expander_row_set_expanded (HdyExpanderRow *self,
+ gboolean expanded)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ expanded = !!expanded && priv->enable_expansion;
+
+ if (priv->expanded == expanded)
+ return;
+
+ priv->expanded = expanded;
+
+ update_arrow (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPANDED]);
+}
+
+/**
+ * hdy_expander_row_get_enable_expansion:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether the expansion of @self is enabled.
+ *
+ * Returns: whether the expansion of @self is enabled.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_expander_row_get_enable_expansion (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->enable_expansion;
+}
+
+/**
+ * hdy_expander_row_set_enable_expansion:
+ * @self: a #HdyExpanderRow
+ * @enable_expansion: %TRUE to enable the expansion
+ *
+ * Sets whether the expansion of @self is enabled.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_expander_row_set_enable_expansion (HdyExpanderRow *self,
+ gboolean enable_expansion)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ enable_expansion = !!enable_expansion;
+
+ if (priv->enable_expansion == enable_expansion)
+ return;
+
+ priv->enable_expansion = enable_expansion;
+
+ hdy_expander_row_set_expanded (self, priv->enable_expansion);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLE_EXPANSION]);
+}
+
+/**
+ * hdy_expander_row_get_show_enable_switch:
+ * @self: a #HdyExpanderRow
+ *
+ * Gets whether the switch enabling the expansion of @self is visible.
+ *
+ * Returns: whether the switch enabling the expansion of @self is visible.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_EXPANDER_ROW (self), FALSE);
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ return priv->show_enable_switch;
+}
+
+/**
+ * hdy_expander_row_set_show_enable_switch:
+ * @self: a #HdyExpanderRow
+ * @show_enable_switch: %TRUE to show the switch enabling the expansion
+ *
+ * Sets whether the switch enabling the expansion of @self is visible.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self,
+ gboolean show_enable_switch)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ show_enable_switch = !!show_enable_switch;
+
+ if (priv->show_enable_switch == show_enable_switch)
+ return;
+
+ priv->show_enable_switch = show_enable_switch;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_ENABLE_SWITCH]);
+}
+
+/**
+ * hdy_expander_row_add_action:
+ * @self: a #HdyExpanderRow
+ * @widget: the action widget
+ *
+ * Adds an action widget to @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_add_action (HdyExpanderRow *self,
+ GtkWidget *widget)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (self));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ gtk_box_pack_start (priv->actions, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->actions));
+}
+
+/**
+ * hdy_expander_row_add_prefix:
+ * @self: a #HdyExpanderRow
+ * @widget: the prefix widget
+ *
+ * Adds a prefix widget to @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_expander_row_add_prefix (HdyExpanderRow *self,
+ GtkWidget *widget)
+{
+ HdyExpanderRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_EXPANDER_ROW (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget));
+
+ priv = hdy_expander_row_get_instance_private (self);
+
+ if (priv->prefixes == NULL) {
+ priv->prefixes = GTK_BOX (gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 12));
+ gtk_widget_set_no_show_all (GTK_WIDGET (priv->prefixes), TRUE);
+ gtk_widget_set_can_focus (GTK_WIDGET (priv->prefixes), FALSE);
+ hdy_action_row_add_prefix (HDY_ACTION_ROW (priv->action_row), GTK_WIDGET (priv->prefixes));
+ }
+ gtk_box_pack_start (priv->prefixes, widget, FALSE, TRUE, 0);
+ gtk_widget_show (GTK_WIDGET (priv->prefixes));
+}
diff --git a/subprojects/libhandy/src/hdy-expander-row.h b/subprojects/libhandy/src/hdy-expander-row.h
new file mode 100644
index 0000000..295f1d0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-preferences-row.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_EXPANDER_ROW (hdy_expander_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyExpanderRow, hdy_expander_row, HDY, EXPANDER_ROW, HdyPreferencesRow)
+
+/**
+ * HdyExpanderRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyExpanderRowClass
+{
+ HdyPreferencesRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_expander_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_expander_row_get_subtitle (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_subtitle (HdyExpanderRow *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_use_underline (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_use_underline (HdyExpanderRow *self,
+ gboolean use_underline);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_expander_row_get_icon_name (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_icon_name (HdyExpanderRow *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_expanded (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_expanded (HdyExpanderRow *self,
+ gboolean expanded);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_enable_expansion (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_enable_expansion (HdyExpanderRow *self,
+ gboolean enable_expansion);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_expander_row_get_show_enable_switch (HdyExpanderRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_set_show_enable_switch (HdyExpanderRow *self,
+ gboolean show_enable_switch);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_add_action (HdyExpanderRow *self,
+ GtkWidget *widget);
+HDY_AVAILABLE_IN_ALL
+void hdy_expander_row_add_prefix (HdyExpanderRow *self,
+ GtkWidget *widget);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-expander-row.ui b/subprojects/libhandy/src/hdy-expander-row.ui
new file mode 100644
index 0000000..54d2650
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-expander-row.ui
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdyExpanderRow" parent="HdyPreferencesRow">
+ <!-- The row must not be activatable, to be sure it doesn't conflict with
+ clicking nested rows. -->
+ <property name="activatable">False</property>
+ <!-- The row must be focusable for keyboard navigation to work as
+ expected. -->
+ <property name="can-focus">True</property>
+ <!-- The row is focusable and can still be activated via keyboard, despite
+ being marked as inactivatable. Activating the row should toggle its
+ expansion. -->
+ <signal name="activate" handler="activate_cb" after="yes"/>
+ <style>
+ <class name="empty"/>
+ <class name="expander"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="no-show-all">True</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox">
+ <property name="selection-mode">none</property>
+ <property name="visible">True</property>
+ <!-- The header row is focusable, activatable, and can be activated
+ by clicking it or via keyboard. Activating the row should
+ toggle its expansion. -->
+ <signal name="row-activated" handler="activate_cb" after="yes" swapped="yes"/>
+ <child>
+ <object class="HdyActionRow" id="action_row">
+ <!-- The header row must be activatable to toggle expansion by
+ clicking it or via keyboard activation. -->
+ <property name="activatable">True</property>
+ <!-- The header row must be focusable for keyboard navigation to
+ work as expected. -->
+ <property name="can-focus">True</property>
+ <property name="title" bind-source="HdyExpanderRow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="header"/>
+ </style>
+ <child>
+ <object class="GtkBox" id="actions">
+ <property name="can_focus">False</property>
+ <property name="no_show_all">True</property>
+ <property name="spacing">12</property>
+ <property name="visible">False</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkSwitch" id="enable_switch">
+ <property name="active" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="bidirectional|sync-create"/>
+ <property name="can-focus">True</property>
+ <property name="valign">center</property>
+ <property name="visible" bind-source="HdyExpanderRow" bind-property="show-enable-switch" bind-flags="bidirectional|sync-create"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage" id="image">
+ <property name="can-focus">False</property>
+ <property name="icon-name">hdy-expander-arrow-symbolic</property>
+ <property name="icon-size">1</property>
+ <property name="sensitive" bind-source="HdyExpanderRow" bind-property="enable-expansion" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <style>
+ <class name="expander-row-arrow"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkRevealer">
+ <property name="reveal-child" bind-source="HdyExpanderRow" bind-property="expanded" bind-flags="sync-create"/>
+ <property name="transition-type">slide-up</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="list">
+ <property name="selection-mode">none</property>
+ <property name="visible">True</property>
+ <signal name="add" handler="list_children_changed_cb" swapped="yes"/>
+ <signal name="remove" handler="list_children_changed_cb" swapped="yes"/>
+ <style>
+ <class name="nested"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-header-bar.c b/subprojects/libhandy/src/hdy-header-bar.c
new file mode 100644
index 0000000..c07a402
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-bar.c
@@ -0,0 +1,2868 @@
+/*
+ * Copyright (c) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * This program 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 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-header-bar.h"
+
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-css-private.h"
+#include "hdy-enums.h"
+#include "hdy-window-handle-controller-private.h"
+#include "gtkprogresstrackerprivate.h"
+#include "gtk-window-private.h"
+
+/**
+ * SECTION:hdy-header-bar
+ * @short_description: A box with a centered child.
+ * @Title: HdyHeaderBar
+ * @See_also: #GtkHeaderBar, #HdyApplicationWindow, #HdyTitleBar, #HdyViewSwitcher, #HdyWindow
+ *
+ * HdyHeaderBar is similar to #GtkHeaderBar but is designed to fix some of its
+ * shortcomings for adaptive applications.
+ *
+ * HdyHeaderBar doesn't force the custom title widget to be vertically centered,
+ * hence allowing it to fill up the whole height, which is e.g. needed for
+ * #HdyViewSwitcher.
+ *
+ * When used in a mobile dialog, HdyHeaderBar will replace its window
+ * decorations by a back button allowing to close it. It doesn't have to be its
+ * direct child and you can use any complex contraption you like as the dialog's
+ * titlebar.
+ *
+ * #HdyHeaderBar can be used in window's content area rather than titlebar, and
+ * will still be draggable and will handle right click, middle click and double
+ * click as expected from a titlebar. This is particularly useful with
+ * #HdyWindow or #HdyApplicationWindow.
+ *
+ * # CSS nodes
+ *
+ * #HdyHeaderBar has a single CSS node with name headerbar.
+ */
+
+/**
+ * HdyCenteringPolicy:
+ * @HDY_CENTERING_POLICY_LOOSE: Keep the title centered when possible
+ * @HDY_CENTERING_POLICY_STRICT: Keep the title centered at all cost
+ */
+
+#define DEFAULT_SPACING 6
+#define MIN_TITLE_CHARS 5
+
+#define MOBILE_WINDOW_WIDTH 480
+#define MOBILE_WINDOW_HEIGHT 800
+
+typedef struct {
+ gchar *title;
+ gchar *subtitle;
+ GtkWidget *title_label;
+ GtkWidget *subtitle_label;
+ GtkWidget *label_box;
+ GtkWidget *label_sizing_box;
+ GtkWidget *subtitle_sizing_label;
+ GtkWidget *custom_title;
+ gint spacing;
+ gboolean has_subtitle;
+
+ GList *children;
+
+ gboolean shows_wm_decorations;
+ gchar *decoration_layout;
+ gboolean decoration_layout_set;
+
+ GtkWidget *titlebar_start_box;
+ GtkWidget *titlebar_end_box;
+
+ GtkWidget *titlebar_start_separator;
+ GtkWidget *titlebar_end_separator;
+
+ GtkWidget *titlebar_icon;
+
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ HdyCenteringPolicy centering_policy;
+ guint transition_duration;
+ gboolean interpolate_size;
+
+ gboolean is_mobile_window;
+
+ gulong window_size_allocated_id;
+
+ HdyWindowHandleController *controller;
+} HdyHeaderBarPrivate;
+
+typedef struct _Child Child;
+struct _Child
+{
+ GtkWidget *widget;
+ GtkPackType pack_type;
+};
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_HAS_SUBTITLE,
+ PROP_CUSTOM_TITLE,
+ PROP_SPACING,
+ PROP_SHOW_CLOSE_BUTTON,
+ PROP_DECORATION_LAYOUT,
+ PROP_DECORATION_LAYOUT_SET,
+ PROP_CENTERING_POLICY,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ LAST_PROP
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_PACK_TYPE,
+ CHILD_PROP_POSITION
+};
+
+static GParamSpec *props[LAST_PROP] = { NULL, };
+
+static void hdy_header_bar_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyHeaderBar, hdy_header_bar, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyHeaderBar)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_header_bar_buildable_init));
+
+static gboolean
+hdy_header_bar_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->first_frame_skipped)
+ gtk_progress_tracker_advance_frame (&priv->tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ else
+ priv->first_frame_skipped = TRUE;
+
+ /* Finish the animation early if the widget isn't mapped anymore. */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&priv->tracker);
+
+ gtk_widget_queue_resize (widget);
+
+ if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ priv->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_header_bar_schedule_ticks (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->tick_id == 0) {
+ priv->tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_header_bar_transition_cb, self, NULL);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_header_bar_unschedule_ticks (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ if (priv->tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), priv->tick_id);
+ priv->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_header_bar_start_transition (HdyHeaderBar *self,
+ guint transition_duration)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ priv->interpolate_size &&
+ transition_duration != 0) {
+ priv->first_frame_skipped = FALSE;
+ hdy_header_bar_schedule_ticks (self);
+ gtk_progress_tracker_start (&priv->tracker,
+ priv->transition_duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ hdy_header_bar_unschedule_ticks (self);
+ gtk_progress_tracker_finish (&priv->tracker);
+ }
+
+ gtk_widget_queue_resize (widget);
+}
+
+static void
+init_sizing_box (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *w;
+ GtkStyleContext *context;
+
+ /* We use this box to always request size for the two labels (title
+ * and subtitle) as if they were always visible, but then allocate
+ * the real label box with its actual size, to keep it center-aligned
+ * in case we have only the title.
+ */
+ w = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_show (w);
+ priv->label_sizing_box = g_object_ref_sink (w);
+
+ w = gtk_label_new (NULL);
+ gtk_widget_show (w);
+ context = gtk_widget_get_style_context (w);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE);
+ gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0);
+ gtk_label_set_line_wrap (GTK_LABEL (w), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END);
+ gtk_label_set_width_chars (GTK_LABEL (w), MIN_TITLE_CHARS);
+
+ w = gtk_label_new (NULL);
+ context = gtk_widget_get_style_context (w);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE);
+ gtk_box_pack_start (GTK_BOX (priv->label_sizing_box), w, FALSE, FALSE, 0);
+ gtk_label_set_line_wrap (GTK_LABEL (w), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (w), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (w), PANGO_ELLIPSIZE_END);
+ gtk_widget_set_visible (w, priv->has_subtitle || (priv->subtitle && priv->subtitle[0]));
+ priv->subtitle_sizing_label = w;
+}
+
+static GtkWidget *
+create_title_box (const char *title,
+ const char *subtitle,
+ GtkWidget **ret_title_label,
+ GtkWidget **ret_subtitle_label)
+{
+ GtkWidget *label_box;
+ GtkWidget *title_label;
+ GtkWidget *subtitle_label;
+ GtkStyleContext *context;
+
+ label_box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
+ gtk_widget_set_valign (label_box, GTK_ALIGN_CENTER);
+ gtk_widget_show (label_box);
+
+ title_label = gtk_label_new (title);
+ context = gtk_widget_get_style_context (title_label);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_TITLE);
+ gtk_label_set_line_wrap (GTK_LABEL (title_label), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (title_label), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (title_label), PANGO_ELLIPSIZE_END);
+ gtk_box_pack_start (GTK_BOX (label_box), title_label, FALSE, FALSE, 0);
+ gtk_widget_show (title_label);
+ gtk_label_set_width_chars (GTK_LABEL (title_label), MIN_TITLE_CHARS);
+
+ subtitle_label = gtk_label_new (subtitle);
+ context = gtk_widget_get_style_context (subtitle_label);
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_SUBTITLE);
+ gtk_label_set_line_wrap (GTK_LABEL (subtitle_label), FALSE);
+ gtk_label_set_single_line_mode (GTK_LABEL (subtitle_label), TRUE);
+ gtk_label_set_ellipsize (GTK_LABEL (subtitle_label), PANGO_ELLIPSIZE_END);
+ gtk_box_pack_start (GTK_BOX (label_box), subtitle_label, FALSE, FALSE, 0);
+ gtk_widget_set_no_show_all (subtitle_label, TRUE);
+ gtk_widget_set_visible (subtitle_label, subtitle && subtitle[0]);
+
+ if (ret_title_label)
+ *ret_title_label = title_label;
+ if (ret_subtitle_label)
+ *ret_subtitle_label = subtitle_label;
+
+ return label_box;
+}
+
+static gboolean
+hdy_header_bar_update_window_icon (HdyHeaderBar *self,
+ GtkWindow *window)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GdkPixbuf *pixbuf;
+ gint scale;
+
+ if (priv->titlebar_icon == NULL)
+ return FALSE;
+
+ scale = gtk_widget_get_scale_factor (priv->titlebar_icon);
+ if (GTK_IS_BUTTON (gtk_widget_get_parent (priv->titlebar_icon)))
+ pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 16);
+ else
+ pixbuf = hdy_gtk_window_get_icon_for_size (window, scale * 20);
+
+ if (pixbuf) {
+ g_autoptr (cairo_surface_t) surface =
+ gdk_cairo_surface_create_from_pixbuf (pixbuf, scale, gtk_widget_get_window (priv->titlebar_icon));
+
+ gtk_image_set_from_surface (GTK_IMAGE (priv->titlebar_icon), surface);
+ g_object_unref (pixbuf);
+ gtk_widget_show (priv->titlebar_icon);
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static void
+_hdy_header_bar_update_separator_visibility (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gboolean have_visible_at_start = FALSE;
+ gboolean have_visible_at_end = FALSE;
+ GList *l;
+
+ for (l = priv->children; l != NULL; l = l->next) {
+ Child *child = l->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ if (child->pack_type == GTK_PACK_START)
+ have_visible_at_start = TRUE;
+ else
+ have_visible_at_end = TRUE;
+ }
+ }
+
+ if (priv->titlebar_start_separator != NULL)
+ gtk_widget_set_visible (priv->titlebar_start_separator, have_visible_at_start);
+
+ if (priv->titlebar_end_separator != NULL)
+ gtk_widget_set_visible (priv->titlebar_end_separator, have_visible_at_end);
+}
+
+static void
+hdy_header_bar_update_window_buttons (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *widget = GTK_WIDGET (self), *toplevel;
+ GtkWindow *window;
+ GtkTextDirection direction;
+ gchar *layout_desc;
+ gchar **tokens, **t;
+ gint i, j;
+ GMenuModel *menu;
+ gboolean shown_by_shell;
+ gboolean is_sovereign_window;
+ gboolean is_mobile_dialog;
+
+ toplevel = gtk_widget_get_toplevel (widget);
+ if (!gtk_widget_is_toplevel (toplevel))
+ return;
+
+ if (priv->titlebar_start_box) {
+ gtk_widget_unparent (priv->titlebar_start_box);
+ priv->titlebar_start_box = NULL;
+ priv->titlebar_start_separator = NULL;
+ }
+ if (priv->titlebar_end_box) {
+ gtk_widget_unparent (priv->titlebar_end_box);
+ priv->titlebar_end_box = NULL;
+ priv->titlebar_end_separator = NULL;
+ }
+
+ priv->titlebar_icon = NULL;
+
+ if (!priv->shows_wm_decorations)
+ return;
+
+ direction = gtk_widget_get_direction (widget);
+
+ g_object_get (gtk_widget_get_settings (widget),
+ "gtk-shell-shows-app-menu", &shown_by_shell,
+ "gtk-decoration-layout", &layout_desc,
+ NULL);
+
+ if (priv->decoration_layout_set) {
+ g_free (layout_desc);
+ layout_desc = g_strdup (priv->decoration_layout);
+ }
+
+ window = GTK_WINDOW (toplevel);
+
+ if (!shown_by_shell && gtk_window_get_application (window))
+ menu = gtk_application_get_app_menu (gtk_window_get_application (window));
+ else
+ menu = NULL;
+
+ is_sovereign_window = (!gtk_window_get_modal (window) &&
+ gtk_window_get_transient_for (window) == NULL &&
+ gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL);
+
+ is_mobile_dialog= (priv->is_mobile_window && !is_sovereign_window);
+
+ tokens = g_strsplit (layout_desc, ":", 2);
+ if (tokens) {
+ for (i = 0; i < 2; i++) {
+ GtkWidget *box;
+ GtkWidget *separator;
+ int n_children = 0;
+
+ if (tokens[i] == NULL)
+ break;
+
+ t = g_strsplit (tokens[i], ",", -1);
+
+ separator = gtk_separator_new (GTK_ORIENTATION_VERTICAL);
+ gtk_widget_set_no_show_all (separator, TRUE);
+ gtk_style_context_add_class (gtk_widget_get_style_context (separator), "titlebutton");
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, priv->spacing);
+
+ for (j = 0; t[j]; j++) {
+ GtkWidget *button = NULL;
+ GtkWidget *image = NULL;
+ AtkObject *accessible;
+
+ if (strcmp (t[j], "icon") == 0 &&
+ is_sovereign_window) {
+ button = gtk_image_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ priv->titlebar_icon = button;
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "icon");
+ gtk_widget_set_size_request (button, 20, 20);
+ gtk_widget_show (button);
+
+ if (!hdy_header_bar_update_window_icon (self, window))
+ {
+ gtk_widget_destroy (button);
+ priv->titlebar_icon = NULL;
+ button = NULL;
+ }
+ } else if (strcmp (t[j], "menu") == 0 &&
+ menu != NULL &&
+ is_sovereign_window) {
+ button = gtk_menu_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (button), menu);
+ gtk_menu_button_set_use_popover (GTK_MENU_BUTTON (button), TRUE);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "appmenu");
+ image = gtk_image_new ();
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Application menu"));
+
+ priv->titlebar_icon = image;
+ if (!hdy_header_bar_update_window_icon (self, window))
+ gtk_image_set_from_icon_name (GTK_IMAGE (priv->titlebar_icon),
+ "application-x-executable-symbolic", GTK_ICON_SIZE_MENU);
+ } else if (strcmp (t[j], "minimize") == 0 &&
+ is_sovereign_window) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "minimize");
+ image = gtk_image_new_from_icon_name ("window-minimize-symbolic", GTK_ICON_SIZE_MENU);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_iconify), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Minimize"));
+ } else if (strcmp (t[j], "maximize") == 0 &&
+ gtk_window_get_resizable (window) &&
+ is_sovereign_window) {
+ const gchar *icon_name;
+ gboolean maximized = gtk_window_is_maximized (window);
+
+ icon_name = maximized ? "window-restore-symbolic" : "window-maximize-symbolic";
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "maximize");
+ image = gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_MENU);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (hdy_gtk_window_toggle_maximized), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, maximized ? _("Restore") : _("Maximize"));
+ } else if (strcmp (t[j], "close") == 0 &&
+ gtk_window_get_deletable (window) &&
+ !is_mobile_dialog) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ image = gtk_image_new_from_icon_name ("window-close-symbolic", GTK_ICON_SIZE_MENU);
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "titlebutton");
+ gtk_style_context_add_class (gtk_widget_get_style_context (button), "close");
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, FALSE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_close), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Close"));
+ } else if (i == 0 && /* Only at the start. */
+ gtk_window_get_deletable (window) &&
+ is_mobile_dialog) {
+ button = gtk_button_new ();
+ gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
+ image = gtk_image_new_from_icon_name ("go-previous-symbolic", GTK_ICON_SIZE_BUTTON);
+ g_object_set (image, "use-fallback", TRUE, NULL);
+ gtk_container_add (GTK_CONTAINER (button), image);
+ gtk_widget_set_can_focus (button, TRUE);
+ gtk_widget_show_all (button);
+ g_signal_connect_swapped (button, "clicked",
+ G_CALLBACK (gtk_window_close), window);
+
+ accessible = gtk_widget_get_accessible (button);
+ if (GTK_IS_ACCESSIBLE (accessible))
+ atk_object_set_name (accessible, _("Back"));
+ }
+
+ if (button) {
+ gtk_box_pack_start (GTK_BOX (box), button, FALSE, FALSE, 0);
+ n_children ++;
+ }
+ }
+ g_strfreev (t);
+
+ if (n_children == 0) {
+ g_object_ref_sink (box);
+ g_object_unref (box);
+ g_object_ref_sink (separator);
+ g_object_unref (separator);
+ continue;
+ }
+
+ gtk_box_pack_start (GTK_BOX (box), separator, FALSE, FALSE, 0);
+ if (i == 1)
+ gtk_box_reorder_child (GTK_BOX (box), separator, 0);
+
+ if ((direction == GTK_TEXT_DIR_LTR && i == 0) ||
+ (direction == GTK_TEXT_DIR_RTL && i == 1))
+ gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_LEFT);
+ else
+ gtk_style_context_add_class (gtk_widget_get_style_context (box), GTK_STYLE_CLASS_RIGHT);
+
+ gtk_widget_show (box);
+ gtk_widget_set_parent (box, GTK_WIDGET (self));
+
+ if (i == 0) {
+ priv->titlebar_start_box = box;
+ priv->titlebar_start_separator = separator;
+ } else {
+ priv->titlebar_end_box = box;
+ priv->titlebar_end_separator = separator;
+ }
+ }
+ g_strfreev (tokens);
+ }
+ g_free (layout_desc);
+
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static gboolean
+compute_is_mobile_window (GtkWindow *window)
+{
+ gint window_width, window_height;
+
+ gtk_window_get_size (window, &window_width, &window_height);
+
+ if (window_width <= MOBILE_WINDOW_WIDTH &&
+ gtk_window_is_maximized (window))
+ return TRUE;
+
+ /* Mobile landscape mode. */
+ if (window_width <= MOBILE_WINDOW_HEIGHT &&
+ window_height <= MOBILE_WINDOW_WIDTH &&
+ gtk_window_is_maximized (window))
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+update_is_mobile_window (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+ gboolean was_mobile_window = priv->is_mobile_window;
+
+ if (!gtk_widget_is_toplevel (toplevel))
+ return;
+
+ priv->is_mobile_window = compute_is_mobile_window (GTK_WINDOW (toplevel));
+
+ if (priv->is_mobile_window != was_mobile_window)
+ hdy_header_bar_update_window_buttons (self);
+}
+
+static void
+construct_label_box (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_assert (priv->label_box == NULL);
+
+ priv->label_box = create_title_box (priv->title,
+ priv->subtitle,
+ &priv->title_label,
+ &priv->subtitle_label);
+ gtk_widget_set_parent (priv->label_box, GTK_WIDGET (self));
+}
+
+static gint
+count_visible_children (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint n;
+
+ n = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (gtk_widget_get_visible (child->widget))
+ n++;
+ }
+
+ return n;
+}
+
+static gint
+count_visible_children_for_pack_type (HdyHeaderBar *self, GtkPackType pack_type)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint n;
+
+ n = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (gtk_widget_get_visible (child->widget) && child->pack_type == pack_type)
+ n++;
+ }
+
+ return n;
+}
+
+static gboolean
+add_child_size (GtkWidget *child,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ gint child_minimum, child_natural;
+
+ if (!gtk_widget_get_visible (child))
+ return FALSE;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ gtk_widget_get_preferred_width (child, &child_minimum, &child_natural);
+ else
+ gtk_widget_get_preferred_height (child, &child_minimum, &child_natural);
+
+ if (GTK_ORIENTATION_HORIZONTAL == orientation) {
+ *minimum += child_minimum;
+ *natural += child_natural;
+ } else {
+ *minimum = MAX (*minimum, child_minimum);
+ *natural = MAX (*natural, child_natural);
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_header_bar_get_size (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint *minimum,
+ gint *natural)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ gint n_start_children = 0, n_end_children = 0;
+ gint start_min = 0, start_nat = 0;
+ gint end_min = 0, end_nat = 0;
+ gint center_min = 0, center_nat = 0;
+
+ for (l = priv->children; l; l = l->next) {
+ Child *child = l->data;
+
+ if (child->pack_type == GTK_PACK_START) {
+ if (add_child_size (child->widget, orientation, &start_min, &start_nat))
+ n_start_children += 1;
+ } else {
+ if (add_child_size (child->widget, orientation, &end_min, &end_nat))
+ n_end_children += 1;
+ }
+ }
+
+ if (priv->label_box != NULL) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ add_child_size (priv->label_box, orientation, &center_min, &center_nat);
+ else
+ add_child_size (priv->label_sizing_box, orientation, &center_min, &center_nat);
+ }
+
+ if (priv->custom_title != NULL)
+ add_child_size (priv->custom_title, orientation, &center_min, &center_nat);
+
+ if (priv->titlebar_start_box != NULL) {
+ if (add_child_size (priv->titlebar_start_box, orientation, &start_min, &start_nat))
+ n_start_children += 1;
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ if (add_child_size (priv->titlebar_end_box, orientation, &end_min, &end_nat))
+ n_end_children += 1;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gdouble strict_centering_t;
+ gint start_min_spaced = start_min + n_start_children * priv->spacing;
+ gint end_min_spaced = end_min + n_end_children * priv->spacing;
+ gint start_nat_spaced = start_nat + n_start_children * priv->spacing;
+ gint end_nat_spaced = end_nat + n_end_children * priv->spacing;
+
+ if (gtk_progress_tracker_get_state (&priv->tracker) != GTK_PROGRESS_STATE_AFTER) {
+ strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE);
+ if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT)
+ strict_centering_t = 1.0 - strict_centering_t;
+ } else
+ strict_centering_t = priv->centering_policy == HDY_CENTERING_POLICY_STRICT ? 1.0 : 0.0;
+
+ *minimum = center_min + n_start_children * priv->spacing +
+ hdy_lerp (start_min_spaced + end_min_spaced,
+ 2 * MAX (start_min_spaced, end_min_spaced),
+ strict_centering_t);
+ *natural = center_nat + n_start_children * priv->spacing +
+ hdy_lerp (start_nat_spaced + end_nat_spaced,
+ 2 * MAX (start_nat_spaced, end_nat_spaced),
+ strict_centering_t);
+ } else {
+ *minimum = MAX (MAX (start_min, end_min), center_min);
+ *natural = MAX (MAX (start_nat, end_nat), center_nat);
+ }
+}
+
+static void
+hdy_header_bar_compute_size_for_orientation (GtkWidget *widget,
+ gint avail_size,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *children;
+ gint required_size = 0;
+ gint required_natural = 0;
+ gint child_size;
+ gint child_natural;
+ gint nvis_children = 0;
+
+ for (children = priv->children; children != NULL; children = children->next) {
+ Child *child = children->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ avail_size, &child_size, &child_natural);
+
+ required_size += child_size;
+ required_natural += child_natural;
+
+ nvis_children += 1;
+ }
+ }
+
+ if (priv->label_box != NULL) {
+ gtk_widget_get_preferred_width (priv->label_sizing_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ }
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title)) {
+ gtk_widget_get_preferred_width (priv->custom_title,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ }
+
+ if (priv->titlebar_start_box != NULL) {
+ gtk_widget_get_preferred_width (priv->titlebar_start_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ nvis_children += 1;
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ gtk_widget_get_preferred_width (priv->titlebar_end_box,
+ &child_size, &child_natural);
+ required_size += child_size;
+ required_natural += child_natural;
+ nvis_children += 1;
+ }
+
+ required_size += nvis_children * priv->spacing;
+ required_natural += nvis_children * priv->spacing;
+
+ *minimum_size = required_size;
+ *natural_size = required_natural;
+}
+
+static void
+hdy_header_bar_compute_size_for_opposing_orientation (GtkWidget *widget,
+ gint avail_size,
+ gint *minimum_size,
+ gint *natural_size)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+ GList *children;
+ gint nvis_children;
+ gint computed_minimum = 0;
+ gint computed_natural = 0;
+ GtkRequestedSize *sizes;
+ GtkPackType packing;
+ gint i;
+ gint child_size;
+ gint child_minimum;
+ gint child_natural;
+ gint center_min, center_nat;
+
+ nvis_children = count_visible_children (self);
+
+ if (nvis_children <= 0)
+ return;
+
+ sizes = g_newa (GtkRequestedSize, nvis_children);
+
+ /* Retrieve desired size for visible children */
+ for (i = 0, children = priv->children; children; children = children->next) {
+ child = children->data;
+
+ if (gtk_widget_get_visible (child->widget)) {
+ gtk_widget_get_preferred_width (child->widget,
+ &sizes[i].minimum_size,
+ &sizes[i].natural_size);
+
+ sizes[i].data = child;
+ i += 1;
+ }
+ }
+
+ /* Bring children up to size first */
+ gtk_distribute_natural_allocation (MAX (0, avail_size), nvis_children, sizes);
+
+ /* Allocate child positions. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; ++packing) {
+ for (i = 0, children = priv->children; children; children = children->next) {
+ child = children->data;
+
+ /* If widget is not visible, skip it. */
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ /* If widget is packed differently skip it, but still increment i,
+ * since widget is visible and will be handled in next loop
+ * iteration.
+ */
+ if (child->pack_type != packing) {
+ i++;
+ continue;
+ }
+
+ child_size = sizes[i].minimum_size;
+
+ gtk_widget_get_preferred_height_for_width (child->widget,
+ child_size, &child_minimum, &child_natural);
+
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+ }
+
+ center_min = center_nat = 0;
+ if (priv->label_box != NULL) {
+ gtk_widget_get_preferred_height (priv->label_sizing_box,
+ &center_min, &center_nat);
+ }
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title)) {
+ gtk_widget_get_preferred_height (priv->custom_title,
+ &center_min, &center_nat);
+ }
+
+ if (priv->titlebar_start_box != NULL) {
+ gtk_widget_get_preferred_height (priv->titlebar_start_box,
+ &child_minimum, &child_natural);
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+
+ if (priv->titlebar_end_box != NULL) {
+ gtk_widget_get_preferred_height (priv->titlebar_end_box,
+ &child_minimum, &child_natural);
+ computed_minimum = MAX (computed_minimum, child_minimum);
+ computed_natural = MAX (computed_natural, child_natural);
+ }
+
+ *minimum_size = computed_minimum;
+ *natural_size = computed_natural;
+}
+
+static void
+hdy_header_bar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ gint css_width, css_height;
+
+ gtk_style_context_get (gtk_widget_get_style_context (widget),
+ gtk_widget_get_state_flags (widget),
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+
+ if (for_size < 0)
+ hdy_header_bar_get_size (widget, orientation, minimum, natural);
+ else if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ hdy_header_bar_compute_size_for_orientation (widget, MAX (for_size, css_height), minimum, natural);
+ else
+ hdy_header_bar_compute_size_for_opposing_orientation (widget, MAX (for_size, css_width), minimum, natural);
+
+ hdy_css_measure (widget, orientation, minimum, natural);
+}
+
+static void
+hdy_header_bar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_header_bar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_header_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static GtkWidget *
+get_title_size (HdyHeaderBar *self,
+ gint for_height,
+ GtkRequestedSize *size,
+ gint *expanded)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkWidget *title_widget;
+
+ if (priv->custom_title != NULL &&
+ gtk_widget_get_visible (priv->custom_title))
+ title_widget = priv->custom_title;
+ else if (priv->label_box != NULL)
+ title_widget = priv->label_box;
+ else
+ return NULL;
+
+ gtk_widget_get_preferred_width_for_height (title_widget,
+ for_height,
+ &(size->minimum_size),
+ &(size->natural_size));
+
+ *expanded = gtk_widget_compute_expand (title_widget, GTK_ORIENTATION_HORIZONTAL);
+
+ return title_widget;
+}
+
+static void
+children_allocate (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkRequestedSize *sizes,
+ gint decoration_width[2],
+ gint uniform_expand_bonus[2],
+ gint leftover_expand_bonus[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkPackType packing;
+ GtkAllocation child_allocation;
+ gint x;
+ gint i;
+ GList *l;
+ Child *child;
+ gint child_size;
+ /* GtkTextDirection direction; */
+
+ /* Allocate the children on both sides of the title. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ child_allocation.y = allocation->y;
+ child_allocation.height = allocation->height;
+ if (packing == GTK_PACK_START)
+ x = allocation->x + decoration_width[0];
+ else
+ x = allocation->x + allocation->width - decoration_width[1];
+
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type != packing)
+ goto next;
+
+ child_size = sizes[i].minimum_size;
+
+ /* If this child is expanded, give it extra space from the reserves. */
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL)) {
+ gint expand_bonus;
+
+ expand_bonus = uniform_expand_bonus[packing];
+
+ if (leftover_expand_bonus[packing] > 0) {
+ expand_bonus++;
+ leftover_expand_bonus[packing]--;
+ }
+
+ child_size += expand_bonus;
+ }
+
+ child_allocation.width = child_size;
+
+ if (packing == GTK_PACK_START) {
+ child_allocation.x = x;
+ x += child_size;
+ x += priv->spacing;
+ } else {
+ x -= child_size;
+ child_allocation.x = x;
+ x -= priv->spacing;
+ }
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ child_allocation.x = allocation->x + allocation->width - (child_allocation.x - allocation->x) - child_allocation.width;
+
+ (*allocations)[i] = child_allocation;
+
+ next:
+ i++;
+ }
+ }
+}
+
+static void
+get_loose_centering_allocations (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkAllocation *title_allocation,
+ gint decoration_width[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkRequestedSize *sizes;
+ gint width;
+ gint nvis_children;
+ GtkRequestedSize title_size = { 0 };
+ gboolean title_expands = FALSE;
+ gint side[2] = { 0 };
+ gint uniform_expand_bonus[2] = { 0 };
+ gint leftover_expand_bonus[2] = { 0 };
+ gint side_free_space[2] = { 0 };
+ gint center_free_space[2] = { 0 };
+ gint nexpand_children[2] = { 0 };
+ gint center_free_space_min;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkPackType packing;
+
+ nvis_children = count_visible_children (self);
+ sizes = g_newa (GtkRequestedSize, nvis_children);
+
+ width = allocation->width - nvis_children * priv->spacing;
+
+ i = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL))
+ nexpand_children[child->pack_type]++;
+
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ allocation->height,
+ &sizes[i].minimum_size,
+ &sizes[i].natural_size);
+ width -= sizes[i].minimum_size;
+ i++;
+ }
+
+ get_title_size (self, allocation->height, &title_size, &title_expands);
+ width -= title_size.minimum_size;
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type == packing)
+ side[packing] += sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+ }
+
+ /* Distribute the available space for natural expansion of the children. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++)
+ width -= decoration_width[packing];
+ width = gtk_distribute_natural_allocation (MAX (0, width), 1, &title_size);
+ width = gtk_distribute_natural_allocation (MAX (0, width), nvis_children, sizes);
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ i = 0;
+ side[packing] = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (child->pack_type == packing)
+ side[packing] += sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+ }
+
+ /* Figure out how much space is left on each side of the title, and earkmark
+ * that space for the expanded children.
+ *
+ * If the title itself is expanded, then it gets half the spoils from each
+ * side.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ side_free_space[packing] = MIN (MAX (allocation->width / 2 - title_size.natural_size / 2 - decoration_width[packing] - side[packing], 0), width);
+ if (title_expands)
+ center_free_space[packing] = nexpand_children[packing] > 0 ?
+ side_free_space[packing] / 2 :
+ side_free_space[packing];
+ }
+ center_free_space_min = MIN (center_free_space[0], center_free_space[1]);
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ center_free_space[packing] = center_free_space_min;
+ side_free_space[packing] -= center_free_space[packing];
+ width -= side_free_space[packing];
+
+ if (nexpand_children[packing] == 0)
+ continue;
+
+ uniform_expand_bonus[packing] = (side_free_space[packing]) / nexpand_children[packing];
+ leftover_expand_bonus[packing] = (side_free_space[packing]) % nexpand_children[packing];
+ }
+
+ children_allocate (self, allocation, allocations, sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus);
+
+ /* We don't enforce css borders on the center widget, to make title/subtitle
+ * combinations fit without growing the header.
+ */
+ title_allocation->y = allocation->y;
+ title_allocation->height = allocation->height;
+
+ title_allocation->width = MIN (allocation->width - decoration_width[0] - side[0] - decoration_width[1] - side[1],
+ title_size.natural_size);
+ title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2;
+
+ /* If the title widget is expanded, then grow it by all the available free
+ * space, and recenter it.
+ */
+ if (title_expands && width > 0) {
+ title_allocation->width += width;
+ title_allocation->x -= width / 2;
+ }
+
+ if (allocation->x + decoration_width[0] + side[0] > title_allocation->x)
+ title_allocation->x = allocation->x + decoration_width[0] + side[0];
+ else if (allocation->x + allocation->width - decoration_width[1] - side[1] < title_allocation->x + title_allocation->width)
+ title_allocation->x = allocation->x + allocation->width - decoration_width[1] - side[1] - title_allocation->width;
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width;
+}
+
+static void
+get_strict_centering_allocations (HdyHeaderBar *self,
+ GtkAllocation *allocation,
+ GtkAllocation **allocations,
+ GtkAllocation *title_allocation,
+ gint decoration_width[2])
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ GtkRequestedSize *children_sizes = { 0 };
+ GtkRequestedSize *children_sizes_for_side[2] = { 0 };
+ GtkRequestedSize side_size[2] = { 0 }; /* The size requested by each side. */
+ GtkRequestedSize title_size = { 0 }; /* The size requested by the title. */
+ GtkRequestedSize side_request = { 0 }; /* The maximum size requested by each side, decoration included. */
+ gint side_max; /* The maximum space allocatable to each side, decoration included. */
+ gint title_leftover; /* The or 0px or 1px leftover from ensuring each side is allocated the same size. */
+ /* The space available for expansion on each side, including for the title. */
+ gint free_space[2] = { 0 };
+ /* The space the title will take from the free space of each side for its expansion. */
+ gint title_expand_bonus = 0;
+ gint uniform_expand_bonus[2] = { 0 };
+ gint leftover_expand_bonus[2] = { 0 };
+
+ gint nvis_children, n_side_vis_children[2] = { 0 };
+ gint nexpand_children[2] = { 0 };
+ gboolean title_expands = FALSE;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkPackType packing;
+
+ get_title_size (self, allocation->height, &title_size, &title_expands);
+
+ nvis_children = count_visible_children (self);
+ children_sizes = g_newa (GtkRequestedSize, nvis_children);
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ n_side_vis_children[packing] = count_visible_children_for_pack_type (self, packing);
+ children_sizes_for_side[packing] = packing == 0 ? children_sizes : children_sizes + n_side_vis_children[packing - 1];
+ free_space[packing] = (allocation->width - title_size.minimum_size) / 2 - decoration_width[packing];
+ }
+
+ /* Compute the nominal size of the children filling up each side of the title
+ * in titlebar.
+ */
+ i = 0;
+ for (l = priv->children; l; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ if (gtk_widget_compute_expand (child->widget, GTK_ORIENTATION_HORIZONTAL))
+ nexpand_children[child->pack_type]++;
+
+ gtk_widget_get_preferred_width_for_height (child->widget,
+ allocation->height,
+ &children_sizes[i].minimum_size,
+ &children_sizes[i].natural_size);
+ side_size[child->pack_type].minimum_size += children_sizes[i].minimum_size + priv->spacing;
+ side_size[child->pack_type].natural_size += children_sizes[i].natural_size + priv->spacing;
+ free_space[child->pack_type] -= children_sizes[i].minimum_size + priv->spacing;
+
+ i++;
+ }
+
+ /* Figure out the space maximum size requests from each side to help centering
+ * the title.
+ */
+ side_request.minimum_size = MAX (side_size[GTK_PACK_START].minimum_size + decoration_width[GTK_PACK_START],
+ side_size[GTK_PACK_END].minimum_size + decoration_width[GTK_PACK_END]);
+ side_request.natural_size = MAX (side_size[GTK_PACK_START].natural_size + decoration_width[GTK_PACK_START],
+ side_size[GTK_PACK_END].natural_size + decoration_width[GTK_PACK_END]);
+ title_leftover = (allocation->width - title_size.natural_size) % 2;
+ side_max = MAX ((allocation->width - title_size.natural_size) / 2, side_request.minimum_size);
+
+ /* Distribute the available space for natural expansion of the children and
+ * figure out how much space is left on each side of the title, free to be
+ * used for expansion.
+ */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ gint leftovers = side_max - side_size[packing].minimum_size - decoration_width[packing];
+ free_space[packing] = gtk_distribute_natural_allocation (leftovers, n_side_vis_children[packing], children_sizes_for_side[packing]);
+ }
+
+ /* Compute how much of each side's free space should be distributed to the
+ * title for its expansion.
+ */
+ title_expand_bonus = !title_expands ? 0 :
+ MIN (nexpand_children[GTK_PACK_START] > 0 ? free_space[GTK_PACK_START] / 2 :
+ free_space[GTK_PACK_START],
+ nexpand_children[GTK_PACK_END] > 0 ? free_space[GTK_PACK_END] / 2 :
+ free_space[GTK_PACK_END]);
+
+ /* Remove the space the title takes from each side for its expansion. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++)
+ free_space[packing] -= title_expand_bonus;
+
+ /* Distribute the free space for expansion of the children. */
+ for (packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ if (nexpand_children[packing] == 0)
+ continue;
+
+ uniform_expand_bonus[packing] = free_space[packing] / nexpand_children[packing];
+ leftover_expand_bonus[packing] = free_space[packing] % nexpand_children[packing];
+ }
+
+ children_allocate (self, allocation, allocations, children_sizes, decoration_width, uniform_expand_bonus, leftover_expand_bonus);
+
+ /* We don't enforce css borders on the center widget, to make title/subtitle
+ * combinations fit without growing the header.
+ */
+ title_allocation->y = allocation->y;
+ title_allocation->height = allocation->height;
+
+ title_allocation->width = MIN (allocation->width - 2 * side_max + title_leftover,
+ title_size.natural_size);
+ title_allocation->x = allocation->x + (allocation->width - title_allocation->width) / 2;
+
+ /* If the title widget is expanded, then grow it by the free space available
+ * for it.
+ */
+ if (title_expands) {
+ title_allocation->width += 2 * title_expand_bonus;
+ title_allocation->x -= title_expand_bonus;
+ }
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ title_allocation->x = allocation->x + allocation->width - (title_allocation->x - allocation->x) - title_allocation->width;
+}
+
+static void
+hdy_header_bar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GtkAllocation *allocations;
+ GtkAllocation title_allocation;
+ GtkAllocation clip;
+ gint nvis_children;
+ GList *l;
+ gint i;
+ Child *child;
+ GtkAllocation child_allocation;
+ GtkTextDirection direction;
+ GtkWidget *decoration_box[2] = { priv->titlebar_start_box, priv->titlebar_end_box };
+ gint decoration_width[2] = { 0 };
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (gtk_widget_get_realized (widget))
+ gdk_window_move_resize (gtk_widget_get_window (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height);
+
+ hdy_css_size_allocate (widget, allocation);
+
+ direction = gtk_widget_get_direction (widget);
+ nvis_children = count_visible_children (self);
+ allocations = g_newa (GtkAllocation, nvis_children);
+
+ /* Get the decoration width. */
+ for (GtkPackType packing = GTK_PACK_START; packing <= GTK_PACK_END; packing++) {
+ gint min, nat;
+
+ if (decoration_box[packing] == NULL)
+ continue;
+
+ gtk_widget_get_preferred_width_for_height (decoration_box[packing],
+ allocation->height,
+ &min, &nat);
+ decoration_width[packing] = nat + priv->spacing;
+ }
+
+ /* Allocate the decoration widgets. */
+ child_allocation.y = allocation->y;
+ child_allocation.height = allocation->height;
+
+ if (priv->titlebar_start_box) {
+ if (direction == GTK_TEXT_DIR_LTR)
+ child_allocation.x = allocation->x;
+ else
+ child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_START] + priv->spacing;
+ child_allocation.width = decoration_width[GTK_PACK_START] - priv->spacing;
+ gtk_widget_size_allocate (priv->titlebar_start_box, &child_allocation);
+ }
+
+ if (priv->titlebar_end_box) {
+ if (direction != GTK_TEXT_DIR_LTR)
+ child_allocation.x = allocation->x;
+ else
+ child_allocation.x = allocation->x + allocation->width - decoration_width[GTK_PACK_END] + priv->spacing;
+ child_allocation.width = decoration_width[GTK_PACK_END] - priv->spacing;
+ gtk_widget_size_allocate (priv->titlebar_end_box, &child_allocation);
+ }
+
+ /* Get the allocation for widgets on both side of the title. */
+ if (gtk_progress_tracker_get_state (&priv->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (priv->centering_policy == HDY_CENTERING_POLICY_STRICT)
+ get_strict_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ else
+ get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ } else {
+ /* For memory usage optimisation's sake, we will use the allocations
+ * variable to store the loose centering allocations and the
+ * title_allocation variable to store the loose title allocation.
+ */
+ GtkAllocation *strict_allocations = g_newa (GtkAllocation, nvis_children);
+ GtkAllocation strict_title_allocation;
+ gdouble strict_centering_t = gtk_progress_tracker_get_ease_out_cubic (&priv->tracker, FALSE);
+
+ if (priv->centering_policy != HDY_CENTERING_POLICY_STRICT)
+ strict_centering_t = 1.0 - strict_centering_t;
+
+ get_loose_centering_allocations (self, allocation, &allocations, &title_allocation, decoration_width);
+ get_strict_centering_allocations (self, allocation, &strict_allocations, &strict_title_allocation, decoration_width);
+
+ for (i = 0; i < nvis_children; i++) {
+ allocations[i].x = hdy_lerp (allocations[i].x, strict_allocations[i].x, strict_centering_t);
+ allocations[i].y = hdy_lerp (allocations[i].y, strict_allocations[i].y, strict_centering_t);
+ allocations[i].width = hdy_lerp (allocations[i].width, strict_allocations[i].width, strict_centering_t);
+ allocations[i].height = hdy_lerp (allocations[i].height, strict_allocations[i].height, strict_centering_t);
+ }
+ title_allocation.x = hdy_lerp (title_allocation.x, strict_title_allocation.x, strict_centering_t);
+ title_allocation.y = hdy_lerp (title_allocation.y, strict_title_allocation.y, strict_centering_t);
+ title_allocation.width = hdy_lerp (title_allocation.width, strict_title_allocation.width, strict_centering_t);
+ title_allocation.height = hdy_lerp (title_allocation.height, strict_title_allocation.height, strict_centering_t);
+ }
+
+ /* Allocate the children on both sides of the title. */
+ i = 0;
+ for (l = priv->children; l != NULL; l = l->next) {
+ child = l->data;
+ if (!gtk_widget_get_visible (child->widget))
+ continue;
+
+ gtk_widget_size_allocate (child->widget, &allocations[i]);
+ i++;
+ }
+
+ /* Allocate the title widget. */
+ if (priv->custom_title != NULL && gtk_widget_get_visible (priv->custom_title))
+ gtk_widget_size_allocate (priv->custom_title, &title_allocation);
+ else if (priv->label_box != NULL)
+ gtk_widget_size_allocate (priv->label_box, &title_allocation);
+
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_header_bar_destroy (GtkWidget *widget)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (widget));
+
+ if (priv->label_sizing_box) {
+ gtk_widget_destroy (priv->label_sizing_box);
+ g_clear_object (&priv->label_sizing_box);
+ }
+
+ if (priv->custom_title) {
+ gtk_widget_unparent (priv->custom_title);
+ priv->custom_title = NULL;
+ }
+
+ if (priv->label_box) {
+ gtk_widget_unparent (priv->label_box);
+ priv->label_box = NULL;
+ }
+
+ if (priv->titlebar_start_box) {
+ gtk_widget_unparent (priv->titlebar_start_box);
+ priv->titlebar_start_box = NULL;
+ priv->titlebar_start_separator = NULL;
+ }
+
+ if (priv->titlebar_end_box) {
+ gtk_widget_unparent (priv->titlebar_end_box);
+ priv->titlebar_end_box = NULL;
+ priv->titlebar_end_separator = NULL;
+ }
+
+ GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->destroy (widget);
+}
+
+static void
+hdy_header_bar_finalize (GObject *object)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (object));
+
+ g_clear_pointer (&priv->title, g_free);
+ g_clear_pointer (&priv->subtitle, g_free);
+ g_clear_pointer (&priv->decoration_layout, g_free);
+ g_clear_object (&priv->controller);
+
+ G_OBJECT_CLASS (hdy_header_bar_parent_class)->finalize (object);
+}
+
+static void
+hdy_header_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (object);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, priv->title);
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, priv->subtitle);
+ break;
+ case PROP_CUSTOM_TITLE:
+ g_value_set_object (value, priv->custom_title);
+ break;
+ case PROP_SPACING:
+ g_value_set_int (value, priv->spacing);
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ g_value_set_boolean (value, hdy_header_bar_get_show_close_button (self));
+ break;
+ case PROP_HAS_SUBTITLE:
+ g_value_set_boolean (value, hdy_header_bar_get_has_subtitle (self));
+ break;
+ case PROP_DECORATION_LAYOUT:
+ g_value_set_string (value, hdy_header_bar_get_decoration_layout (self));
+ break;
+ case PROP_DECORATION_LAYOUT_SET:
+ g_value_set_boolean (value, priv->decoration_layout_set);
+ break;
+ case PROP_CENTERING_POLICY:
+ g_value_set_enum (value, hdy_header_bar_get_centering_policy (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_header_bar_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_header_bar_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_header_bar_get_interpolate_size (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_header_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (object);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ hdy_header_bar_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_header_bar_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_CUSTOM_TITLE:
+ hdy_header_bar_set_custom_title (self, g_value_get_object (value));
+ break;
+ case PROP_SPACING:
+ if (priv->spacing != g_value_get_int (value)) {
+ priv->spacing = g_value_get_int (value);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (object, pspec);
+ }
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ hdy_header_bar_set_show_close_button (self, g_value_get_boolean (value));
+ break;
+ case PROP_HAS_SUBTITLE:
+ hdy_header_bar_set_has_subtitle (self, g_value_get_boolean (value));
+ break;
+ case PROP_DECORATION_LAYOUT:
+ hdy_header_bar_set_decoration_layout (self, g_value_get_string (value));
+ break;
+ case PROP_DECORATION_LAYOUT_SET:
+ priv->decoration_layout_set = g_value_get_boolean (value);
+ break;
+ case PROP_CENTERING_POLICY:
+ hdy_header_bar_set_centering_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_header_bar_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_header_bar_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+notify_child_cb (GObject *child,
+ GParamSpec *pspec,
+ HdyHeaderBar *self)
+{
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static void
+hdy_header_bar_pack (HdyHeaderBar *self,
+ GtkWidget *widget,
+ GtkPackType pack_type)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+
+ g_return_if_fail (gtk_widget_get_parent (widget) == NULL);
+
+ child = g_new (Child, 1);
+ child->widget = widget;
+ child->pack_type = pack_type;
+
+ priv->children = g_list_append (priv->children, child);
+
+ gtk_widget_freeze_child_notify (widget);
+ gtk_widget_set_parent (widget, GTK_WIDGET (self));
+ g_signal_connect (widget, "notify::visible", G_CALLBACK (notify_child_cb), self);
+ gtk_widget_child_notify (widget, "pack-type");
+ gtk_widget_child_notify (widget, "position");
+ gtk_widget_thaw_child_notify (widget);
+
+ _hdy_header_bar_update_separator_visibility (self);
+}
+
+static void
+hdy_header_bar_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (HDY_HEADER_BAR (container), child, GTK_PACK_START);
+}
+
+static GList *
+find_child_link (HdyHeaderBar *self,
+ GtkWidget *widget,
+ gint *position)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+ gint i;
+
+ for (l = priv->children, i = 0; l; l = l->next, i++) {
+ child = l->data;
+ if (child->widget == widget) {
+ if (position)
+ *position = i;
+
+ return l;
+ }
+ }
+
+ return NULL;
+}
+
+static void
+hdy_header_bar_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l) {
+ child = l->data;
+ g_signal_handlers_disconnect_by_func (widget, notify_child_cb, self);
+ gtk_widget_unparent (child->widget);
+ priv->children = g_list_delete_link (priv->children, l);
+ g_free (child);
+ gtk_widget_queue_resize (GTK_WIDGET (container));
+ _hdy_header_bar_update_separator_visibility (self);
+ }
+}
+
+static void
+hdy_header_bar_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ Child *child;
+ GList *children;
+
+ if (include_internals && priv->titlebar_start_box != NULL)
+ (* callback) (priv->titlebar_start_box, callback_data);
+
+ children = priv->children;
+ while (children) {
+ child = children->data;
+ children = children->next;
+ if (child->pack_type == GTK_PACK_START)
+ (* callback) (child->widget, callback_data);
+ }
+
+ if (priv->custom_title != NULL)
+ (* callback) (priv->custom_title, callback_data);
+
+ if (include_internals && priv->label_box != NULL)
+ (* callback) (priv->label_box, callback_data);
+
+ children = priv->children;
+ while (children) {
+ child = children->data;
+ children = children->next;
+ if (child->pack_type == GTK_PACK_END)
+ (* callback) (child->widget, callback_data);
+ }
+
+ if (include_internals && priv->titlebar_end_box != NULL)
+ (* callback) (priv->titlebar_end_box, callback_data);
+}
+
+static void
+hdy_header_bar_reorder_child (HdyHeaderBar *self,
+ GtkWidget *widget,
+ gint position)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ gint old_position;
+ Child *child;
+
+ l = find_child_link (self, widget, &old_position);
+
+ if (l == NULL)
+ return;
+
+ if (old_position == position)
+ return;
+
+ child = l->data;
+ priv->children = g_list_delete_link (priv->children, l);
+
+ if (position < 0)
+ l = NULL;
+ else
+ l = g_list_nth (priv->children, position);
+
+ priv->children = g_list_insert_before (priv->children, l, child);
+ gtk_widget_child_notify (widget, "position");
+ gtk_widget_queue_resize (widget);
+}
+
+static GType
+hdy_header_bar_child_type (GtkContainer *container)
+{
+ return GTK_TYPE_WIDGET;
+}
+
+static void
+hdy_header_bar_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l == NULL) {
+ g_param_value_set_default (pspec, value);
+
+ return;
+ }
+
+ child = l->data;
+
+ switch (property_id) {
+ case CHILD_PROP_PACK_TYPE:
+ g_value_set_enum (value, child->pack_type);
+ break;
+
+ case CHILD_PROP_POSITION:
+ g_value_set_int (value, g_list_position (priv->children, l));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_header_bar_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (container);
+ GList *l;
+ Child *child;
+
+ l = find_child_link (self, widget, NULL);
+ if (l == NULL)
+ return;
+
+ child = l->data;
+
+ switch (property_id) {
+ case CHILD_PROP_PACK_TYPE:
+ child->pack_type = g_value_get_enum (value);
+ _hdy_header_bar_update_separator_visibility (self);
+ gtk_widget_queue_resize (widget);
+ break;
+
+ case CHILD_PROP_POSITION:
+ hdy_header_bar_reorder_child (self, widget, g_value_get_int (value));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static gboolean
+hdy_header_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ /* GtkWidget draws nothing by default so we have to render the background
+ * explicitly for HdyHederBar to render the typical titlebar background.
+ */
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ /* Ditto for the borders. */
+ gtk_render_frame (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+
+ return GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->draw (widget, cr);
+}
+
+static void
+hdy_header_bar_realize (GtkWidget *widget)
+{
+ GtkSettings *settings;
+ GtkAllocation allocation;
+ GdkWindowAttr attributes;
+ gint attributes_mask;
+ GdkWindow *window;
+
+ settings = gtk_widget_get_settings (widget);
+ g_signal_connect_swapped (settings, "notify::gtk-shell-shows-app-menu",
+ G_CALLBACK (hdy_header_bar_update_window_buttons), widget);
+ g_signal_connect_swapped (settings, "notify::gtk-decoration-layout",
+ G_CALLBACK (hdy_header_bar_update_window_buttons), widget);
+ update_is_mobile_window (HDY_HEADER_BAR (widget));
+ hdy_header_bar_update_window_buttons (HDY_HEADER_BAR (widget));
+
+ gtk_widget_get_allocation (widget, &allocation);
+ gtk_widget_set_realized (widget, TRUE);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes,
+ attributes_mask);
+ gtk_widget_set_window (widget, window);
+ gtk_widget_register_window (widget, window);
+}
+
+static void
+hdy_header_bar_unrealize (GtkWidget *widget)
+{
+ GtkSettings *settings;
+
+ settings = gtk_widget_get_settings (widget);
+
+ g_signal_handlers_disconnect_by_func (settings, hdy_header_bar_update_window_buttons, widget);
+
+ GTK_WIDGET_CLASS (hdy_header_bar_parent_class)->unrealize (widget);
+}
+
+static gboolean
+window_state_changed (GtkWidget *window,
+ GdkEventWindowState *event,
+ gpointer data)
+{
+ HdyHeaderBar *self = HDY_HEADER_BAR (data);
+
+ if (event->changed_mask & (GDK_WINDOW_STATE_FULLSCREEN |
+ GDK_WINDOW_STATE_MAXIMIZED |
+ GDK_WINDOW_STATE_TILED |
+ GDK_WINDOW_STATE_TOP_TILED |
+ GDK_WINDOW_STATE_RIGHT_TILED |
+ GDK_WINDOW_STATE_BOTTOM_TILED |
+ GDK_WINDOW_STATE_LEFT_TILED))
+ hdy_header_bar_update_window_buttons (self);
+
+ return FALSE;
+}
+
+static void
+hdy_header_bar_hierarchy_changed (GtkWidget *widget,
+ GtkWidget *previous_toplevel)
+{
+ GtkWidget *toplevel;
+ HdyHeaderBar *self = HDY_HEADER_BAR (widget);
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ toplevel = gtk_widget_get_toplevel (widget);
+
+ if (previous_toplevel)
+ g_signal_handlers_disconnect_by_func (previous_toplevel,
+ window_state_changed, widget);
+
+ if (toplevel)
+ g_signal_connect_after (toplevel, "window-state-event",
+ G_CALLBACK (window_state_changed), widget);
+
+ if (priv->window_size_allocated_id > 0) {
+ g_signal_handler_disconnect (previous_toplevel, priv->window_size_allocated_id);
+ priv->window_size_allocated_id = 0;
+ }
+
+ if (GTK_IS_WINDOW (toplevel))
+ priv->window_size_allocated_id =
+ g_signal_connect_swapped (toplevel, "size-allocate",
+ G_CALLBACK (update_is_mobile_window), self);
+
+ update_is_mobile_window (self);
+ hdy_header_bar_update_window_buttons (self);
+}
+
+static void
+hdy_header_bar_class_init (HdyHeaderBarClass *class)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (class);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (class);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (class);
+
+ object_class->finalize = hdy_header_bar_finalize;
+ object_class->get_property = hdy_header_bar_get_property;
+ object_class->set_property = hdy_header_bar_set_property;
+
+ widget_class->destroy = hdy_header_bar_destroy;
+ widget_class->size_allocate = hdy_header_bar_size_allocate;
+ widget_class->get_preferred_width = hdy_header_bar_get_preferred_width;
+ widget_class->get_preferred_height = hdy_header_bar_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_header_bar_get_preferred_height_for_width;
+ widget_class->get_preferred_width_for_height = hdy_header_bar_get_preferred_width_for_height;
+ widget_class->draw = hdy_header_bar_draw;
+ widget_class->realize = hdy_header_bar_realize;
+ widget_class->unrealize = hdy_header_bar_unrealize;
+ widget_class->hierarchy_changed = hdy_header_bar_hierarchy_changed;
+
+ container_class->add = hdy_header_bar_add;
+ container_class->remove = hdy_header_bar_remove;
+ container_class->forall = hdy_header_bar_forall;
+ container_class->child_type = hdy_header_bar_child_type;
+ container_class->set_child_property = hdy_header_bar_set_child_property;
+ container_class->get_child_property = hdy_header_bar_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ gtk_container_class_install_child_property (container_class,
+ CHILD_PROP_PACK_TYPE,
+ g_param_spec_enum ("pack-type",
+ _("Pack type"),
+ _("A GtkPackType indicating whether the child is packed with reference to the start or end of the parent"),
+ GTK_TYPE_PACK_TYPE, GTK_PACK_START,
+ G_PARAM_READWRITE));
+ gtk_container_class_install_child_property (container_class,
+ CHILD_PROP_POSITION,
+ g_param_spec_int ("position",
+ _("Position"),
+ _("The index of the child in the parent"),
+ -1, G_MAXINT, 0,
+ G_PARAM_READWRITE));
+
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title to display"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle to display"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ props[PROP_CUSTOM_TITLE] =
+ g_param_spec_object ("custom-title",
+ _("Custom Title"),
+ _("Custom title widget to display"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS);
+
+ props[PROP_SPACING] =
+ g_param_spec_int ("spacing",
+ _("Spacing"),
+ _("The amount of space between children"),
+ 0, G_MAXINT,
+ DEFAULT_SPACING,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyHeaderBar:show-close-button:
+ *
+ * Whether to show window decorations.
+ *
+ * Which buttons are actually shown and where is determined
+ * by the #HdyHeaderBar:decoration-layout property, and by
+ * the state of the window (e.g. a close button will not be
+ * shown if the window can't be closed).
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_SHOW_CLOSE_BUTTON] =
+ g_param_spec_boolean ("show-close-button",
+ _("Show decorations"),
+ _("Whether to show window decorations"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyHeaderBar:decoration-layout:
+ *
+ * The decoration layout for buttons. If this property is
+ * not set, the #GtkSettings:gtk-decoration-layout setting
+ * is used.
+ *
+ * See hdy_header_bar_set_decoration_layout() for information
+ * about the format of this string.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DECORATION_LAYOUT] =
+ g_param_spec_string ("decoration-layout",
+ _("Decoration Layout"),
+ _("The layout for window decorations"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyHeaderBar:decoration-layout-set:
+ *
+ * Set to %TRUE if #HdyHeaderBar:decoration-layout is set.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DECORATION_LAYOUT_SET] =
+ g_param_spec_boolean ("decoration-layout-set",
+ _("Decoration Layout Set"),
+ _("Whether the decoration-layout property has been set"),
+ FALSE,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyHeaderBar:has-subtitle:
+ *
+ * If %TRUE, reserve space for a subtitle, even if none
+ * is currently set.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_HAS_SUBTITLE] =
+ g_param_spec_boolean ("has-subtitle",
+ _("Has Subtitle"),
+ _("Whether to reserve space for a subtitle"),
+ TRUE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CENTERING_POLICY] =
+ g_param_spec_enum ("centering-policy",
+ _("Centering policy"),
+ _("The policy to horizontally align the center widget"),
+ HDY_TYPE_CENTERING_POLICY, HDY_CENTERING_POLICY_LOOSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "headerbar");
+}
+
+static void
+hdy_header_bar_init (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+ GtkStyleContext *context;
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ priv->title = NULL;
+ priv->subtitle = NULL;
+ priv->custom_title = NULL;
+ priv->children = NULL;
+ priv->spacing = DEFAULT_SPACING;
+ priv->has_subtitle = TRUE;
+ priv->decoration_layout = NULL;
+ priv->decoration_layout_set = FALSE;
+ priv->transition_duration = 200;
+
+ init_sizing_box (self);
+ construct_label_box (self);
+
+ priv->controller = hdy_window_handle_controller_new (GTK_WIDGET (self));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ /* Ensure the widget has the titlebar style class. */
+ gtk_style_context_add_class (context, "titlebar");
+}
+
+static void
+hdy_header_bar_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ if (type && strcmp (type, "title") == 0)
+ hdy_header_bar_set_custom_title (HDY_HEADER_BAR (buildable), GTK_WIDGET (child));
+ else if (!type)
+ gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (HDY_HEADER_BAR (buildable), type);
+}
+
+static void
+hdy_header_bar_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_header_bar_buildable_add_child;
+}
+
+/**
+ * hdy_header_bar_new:
+ *
+ * Creates a new #HdyHeaderBar widget.
+ *
+ * Returns: a new #HdyHeaderBar
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_header_bar_new (void)
+{
+ return GTK_WIDGET (g_object_new (HDY_TYPE_HEADER_BAR, NULL));
+}
+
+/**
+ * hdy_header_bar_pack_start:
+ * @self: A #HdyHeaderBar
+ * @child: the #GtkWidget to be added to @self:
+ *
+ * Adds @child to @self:, packed with reference to the
+ * start of the @self:.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_pack_start (HdyHeaderBar *self,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (self, child, GTK_PACK_START);
+}
+
+/**
+ * hdy_header_bar_pack_end:
+ * @self: A #HdyHeaderBar
+ * @child: the #GtkWidget to be added to @self:
+ *
+ * Adds @child to @self:, packed with reference to the
+ * end of the @self:.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_pack_end (HdyHeaderBar *self,
+ GtkWidget *child)
+{
+ hdy_header_bar_pack (self, child, GTK_PACK_END);
+}
+
+/**
+ * hdy_header_bar_set_title:
+ * @self: a #HdyHeaderBar
+ * @title: (nullable): a title, or %NULL
+ *
+ * Sets the title of the #HdyHeaderBar. The title should help a user
+ * identify the current view. A good title should not include the
+ * application name.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_title (HdyHeaderBar *self,
+ const gchar *title)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gchar *new_title;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ new_title = g_strdup (title);
+ g_free (priv->title);
+ priv->title = new_title;
+
+ if (priv->title_label != NULL) {
+ gtk_label_set_label (GTK_LABEL (priv->title_label), priv->title);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_header_bar_get_title:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the title of the header. See hdy_header_bar_set_title().
+ *
+ * Returns: (nullable): the title of the header, or %NULL if none has
+ * been set explicitly. The returned string is owned by the widget
+ * and must not be modified or freed.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_title (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->title;
+}
+
+/**
+ * hdy_header_bar_set_subtitle:
+ * @self: a #HdyHeaderBar
+ * @subtitle: (nullable): a subtitle, or %NULL
+ *
+ * Sets the subtitle of the #HdyHeaderBar. The title should give a user
+ * an additional detail to help them identify the current view.
+ *
+ * Note that HdyHeaderBar by default reserves room for the subtitle,
+ * even if none is currently set. If this is not desired, set the
+ * #HdyHeaderBar:has-subtitle property to %FALSE.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_subtitle (HdyHeaderBar *self,
+ const gchar *subtitle)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+ gchar *new_subtitle;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ new_subtitle = g_strdup (subtitle);
+ g_free (priv->subtitle);
+ priv->subtitle = new_subtitle;
+
+ if (priv->subtitle_label != NULL) {
+ gtk_label_set_label (GTK_LABEL (priv->subtitle_label), priv->subtitle);
+ gtk_widget_set_visible (priv->subtitle_label, priv->subtitle && priv->subtitle[0]);
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ }
+
+ gtk_widget_set_visible (priv->subtitle_sizing_label, priv->has_subtitle || (priv->subtitle && priv->subtitle[0]));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_header_bar_get_subtitle:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the subtitle of the header. See hdy_header_bar_set_subtitle().
+ *
+ * Returns: (nullable): the subtitle of the header, or %NULL if none has
+ * been set explicitly. The returned string is owned by the widget
+ * and must not be modified or freed.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_subtitle (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->subtitle;
+}
+
+/**
+ * hdy_header_bar_set_custom_title:
+ * @self: a #HdyHeaderBar
+ * @title_widget: (nullable): a custom widget to use for a title
+ *
+ * Sets a custom title for the #HdyHeaderBar.
+ *
+ * The title should help a user identify the current view. This
+ * supersedes any title set by hdy_header_bar_set_title() or
+ * hdy_header_bar_set_subtitle(). To achieve the same style as
+ * the builtin title and subtitle, use the “title” and “subtitle”
+ * style classes.
+ *
+ * You should set the custom title to %NULL, for the header title
+ * label to be visible again.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_custom_title (HdyHeaderBar *self,
+ GtkWidget *title_widget)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+ if (title_widget)
+ g_return_if_fail (GTK_IS_WIDGET (title_widget));
+
+ /* No need to do anything if the custom widget stays the same */
+ if (priv->custom_title == title_widget)
+ return;
+
+ if (priv->custom_title) {
+ GtkWidget *custom = priv->custom_title;
+
+ priv->custom_title = NULL;
+ gtk_widget_unparent (custom);
+ }
+
+ if (title_widget != NULL) {
+ priv->custom_title = title_widget;
+
+ gtk_widget_set_parent (priv->custom_title, GTK_WIDGET (self));
+
+ if (priv->label_box != NULL) {
+ GtkWidget *label_box = priv->label_box;
+
+ priv->label_box = NULL;
+ priv->title_label = NULL;
+ priv->subtitle_label = NULL;
+ gtk_widget_unparent (label_box);
+ }
+ } else {
+ if (priv->label_box == NULL)
+ construct_label_box (self);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CUSTOM_TITLE]);
+}
+
+/**
+ * hdy_header_bar_get_custom_title:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves the custom title widget of the header. See
+ * hdy_header_bar_set_custom_title().
+ *
+ * Returns: (nullable) (transfer none): the custom title widget
+ * of the header, or %NULL if none has been set explicitly.
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_header_bar_get_custom_title (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ return priv->custom_title;
+}
+
+/**
+ * hdy_header_bar_get_show_close_button:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns whether this header bar shows the standard window
+ * decorations.
+ *
+ * Returns: %TRUE if the decorations are shown
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_show_close_button (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->shows_wm_decorations;
+}
+
+/**
+ * hdy_header_bar_set_show_close_button:
+ * @self: a #HdyHeaderBar
+ * @setting: %TRUE to show standard window decorations
+ *
+ * Sets whether this header bar shows the standard window decorations,
+ * including close, maximize, and minimize.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_show_close_button (HdyHeaderBar *self,
+ gboolean setting)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ setting = setting != FALSE;
+
+ if (priv->shows_wm_decorations == setting)
+ return;
+
+ priv->shows_wm_decorations = setting;
+ hdy_header_bar_update_window_buttons (self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_CLOSE_BUTTON]);
+}
+
+/**
+ * hdy_header_bar_set_has_subtitle:
+ * @self: a #HdyHeaderBar
+ * @setting: %TRUE to reserve space for a subtitle
+ *
+ * Sets whether the header bar should reserve space
+ * for a subtitle, even if none is currently set.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_has_subtitle (HdyHeaderBar *self,
+ gboolean setting)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ setting = setting != FALSE;
+
+ if (priv->has_subtitle == setting)
+ return;
+
+ priv->has_subtitle = setting;
+ gtk_widget_set_visible (priv->subtitle_sizing_label, setting || (priv->subtitle && priv->subtitle[0]));
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_SUBTITLE]);
+}
+
+/**
+ * hdy_header_bar_get_has_subtitle:
+ * @self: a #HdyHeaderBar
+ *
+ * Retrieves whether the header bar reserves space for
+ * a subtitle, regardless if one is currently set or not.
+ *
+ * Returns: %TRUE if the header bar reserves space
+ * for a subtitle
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_has_subtitle (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->has_subtitle;
+}
+
+/**
+ * hdy_header_bar_set_decoration_layout:
+ * @self: a #HdyHeaderBar
+ * @layout: (nullable): a decoration layout, or %NULL to unset the layout
+ *
+ * Sets the decoration layout for this header bar, overriding
+ * the #GtkSettings:gtk-decoration-layout setting.
+ *
+ * There can be valid reasons for overriding the setting, such
+ * as a header bar design that does not allow for buttons to take
+ * room on the right, or only offers room for a single close button.
+ * Split header bars are another example for overriding the
+ * setting.
+ *
+ * The format of the string is button names, separated by commas.
+ * A colon separates the buttons that should appear on the left
+ * from those on the right. Recognized button names are minimize,
+ * maximize, close, icon (the window icon) and menu (a menu button
+ * for the fallback app menu).
+ *
+ * For example, “menu:minimize,maximize,close” specifies a menu
+ * on the left, and minimize, maximize and close buttons on the right.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_decoration_layout (HdyHeaderBar *self,
+ const gchar *layout)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ g_clear_pointer (&priv->decoration_layout, g_free);
+ priv->decoration_layout = g_strdup (layout);
+ priv->decoration_layout_set = (layout != NULL);
+
+ hdy_header_bar_update_window_buttons (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATION_LAYOUT_SET]);
+}
+
+/**
+ * hdy_header_bar_get_decoration_layout:
+ * @self: a #HdyHeaderBar
+ *
+ * Gets the decoration layout set with
+ * hdy_header_bar_set_decoration_layout().
+ *
+ * Returns: the decoration layout
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_header_bar_get_decoration_layout (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), NULL);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->decoration_layout;
+}
+
+/**
+ * hdy_header_bar_get_centering_policy:
+ * @self: a #HdyHeaderBar
+ *
+ * Gets the policy @self follows to horizontally align its center widget.
+ *
+ * Returns: the centering policy
+ *
+ * Since: 0.0.10
+ */
+HdyCenteringPolicy
+hdy_header_bar_get_centering_policy (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), HDY_CENTERING_POLICY_LOOSE);
+
+ return priv->centering_policy;
+}
+
+/**
+ * hdy_header_bar_set_centering_policy:
+ * @self: a #HdyHeaderBar
+ * @centering_policy: the centering policy
+ *
+ * Sets the policy @self must follow to horizontally align its center widget.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_centering_policy (HdyHeaderBar *self,
+ HdyCenteringPolicy centering_policy)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ if (priv->centering_policy == centering_policy)
+ return;
+
+ priv->centering_policy = centering_policy;
+ if (priv->interpolate_size)
+ hdy_header_bar_start_transition (self, priv->transition_duration);
+ else
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CENTERING_POLICY]);
+}
+
+/**
+ * hdy_header_bar_get_transition_duration:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between pages in @self will take.
+ *
+ * Returns: the transition duration
+ *
+ * Since: 0.0.10
+ */
+guint
+hdy_header_bar_get_transition_duration (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), 0);
+
+ return priv->transition_duration;
+}
+
+/**
+ * hdy_header_bar_set_transition_duration:
+ * @self: a #HdyHeaderBar
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between pages in @self
+ * will take.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_transition_duration (HdyHeaderBar *self,
+ guint duration)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ if (priv->transition_duration == duration)
+ return;
+
+ priv->transition_duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_header_bar_get_transition_running:
+ * @self: a #HdyHeaderBar
+ *
+ * Returns whether the @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_transition_running (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ return (priv->tick_id != 0);
+}
+
+/**
+ * hdy_header_bar_get_interpolate_size:
+ * @self: A #HdyHeaderBar
+ *
+ * Gets whether @self should interpolate its size on visible child change.
+ *
+ * See hdy_header_bar_set_interpolate_size().
+ *
+ * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_header_bar_get_interpolate_size (HdyHeaderBar *self)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (self), FALSE);
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ return priv->interpolate_size;
+}
+
+/**
+ * hdy_header_bar_set_interpolate_size:
+ * @self: A #HdyHeaderBar
+ * @interpolate_size: %TRUE to interpolate the size
+ *
+ * Sets whether or not @self will interpolate the size of its opposing
+ * orientation when changing the visible child. If %TRUE, @self will interpolate
+ * its size between the one of the previous visible child and the one of the new
+ * visible child, according to the set transition duration and the orientation,
+ * e.g. if @self is horizontal, it will interpolate the its height.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_header_bar_set_interpolate_size (HdyHeaderBar *self,
+ gboolean interpolate_size)
+{
+ HdyHeaderBarPrivate *priv;
+
+ g_return_if_fail (HDY_IS_HEADER_BAR (self));
+
+ priv = hdy_header_bar_get_instance_private (self);
+
+ interpolate_size = !!interpolate_size;
+
+ if (priv->interpolate_size == interpolate_size)
+ return;
+
+ priv->interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
diff --git a/subprojects/libhandy/src/hdy-header-bar.h b/subprojects/libhandy/src/hdy-header-bar.h
new file mode 100644
index 0000000..066d847
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-bar.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * This program 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 of the License, or (at your
+ * option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_HEADER_BAR (hdy_header_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyHeaderBar, hdy_header_bar, HDY, HEADER_BAR, GtkContainer)
+
+typedef enum {
+ HDY_CENTERING_POLICY_LOOSE,
+ HDY_CENTERING_POLICY_STRICT,
+} HdyCenteringPolicy;
+
+/**
+ * HdyHeaderBarClass
+ * @parent_class: The parent class
+ */
+struct _HdyHeaderBarClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_header_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_title (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_title (HdyHeaderBar *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_subtitle (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_subtitle (HdyHeaderBar *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_header_bar_get_custom_title (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_custom_title (HdyHeaderBar *self,
+ GtkWidget *title_widget);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_pack_start (HdyHeaderBar *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_pack_end (HdyHeaderBar *self,
+ GtkWidget *child);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_show_close_button (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_show_close_button (HdyHeaderBar *self,
+ gboolean setting);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_has_subtitle (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_has_subtitle (HdyHeaderBar *self,
+ gboolean setting);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_header_bar_get_decoration_layout (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_decoration_layout (HdyHeaderBar *self,
+ const gchar *layout);
+
+HDY_AVAILABLE_IN_ALL
+HdyCenteringPolicy hdy_header_bar_get_centering_policy (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_centering_policy (HdyHeaderBar *self,
+ HdyCenteringPolicy centering_policy);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_header_bar_get_transition_duration (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_transition_duration (HdyHeaderBar *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_transition_running (HdyHeaderBar *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_bar_get_interpolate_size (HdyHeaderBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_bar_set_interpolate_size (HdyHeaderBar *self,
+ gboolean interpolate_size);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-header-group.c b/subprojects/libhandy/src/hdy-header-group.c
new file mode 100644
index 0000000..e8287fa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-group.c
@@ -0,0 +1,1115 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-header-group.h"
+
+/**
+ * SECTION:hdy-header-group
+ * @short_description: An object handling composite title bars.
+ * @Title: HdyHeaderGroup
+ * @See_also: #GtkHeaderBar, #HdyHeaderBar, #HdyLeaflet
+ *
+ * The #HdyHeaderGroup object handles the header bars of a composite title bar.
+ * It splits the window decoration across the header bars, giving the left side
+ * of the decorations to the leftmost header bar, and the right side of the
+ * decorations to the rightmost header bar.
+ * See hdy_header_bar_set_decoration_layout().
+ *
+ * The #HdyHeaderGroup:decorate-all property can be used in conjunction with
+ * #HdyLeaflet:folded when the title bar is split across the pages of a
+ * #HdyLeaflet to automatically display the decorations on all the pages when
+ * the leaflet is folded.
+ *
+ * You can nest header groups, which is convenient when you nest leaflets too:
+ * |[
+ * <object class="HdyHeaderGroup" id="inner_header_group">
+ * <property name="decorate-all" bind-source="inner_leaflet" bind-property="folded" bind-flags="sync-create"/>
+ * <headerbars>
+ * <headerbar name="inner_header_bar_1"/>
+ * <headerbar name="inner_header_bar_2"/>
+ * </headerbars>
+ * </object>
+ * <object class="HdyHeaderGroup" id="outer_header_group">
+ * <property name="decorate-all" bind-source="outer_leaflet" bind-property="folded" bind-flags="sync-create"/>
+ * <headerbars>
+ * <headerbar name="inner_header_group"/>
+ * <headerbar name="outer_header_bar"/>
+ * </headerbars>
+ * </object>
+ * ]|
+ *
+ * Since: 0.0.4
+ */
+
+/**
+ * HdyHeaderGroupChildType:
+ * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR: The child is a #HdyHeaderBar
+ * @HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR: The child is a #GtkHeaderBar
+ * @HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP: The child is a #HdyHeaderGroup
+ *
+ * This enumeration value describes the child types handled by #HdyHeaderGroup.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyHeaderGroupChild
+{
+ GObject parent_instance;
+
+ HdyHeaderGroupChildType type;
+ GObject *object;
+};
+
+enum {
+ SIGNAL_UPDATE_DECORATION_LAYOUTS,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+G_DEFINE_TYPE (HdyHeaderGroupChild, hdy_header_group_child, G_TYPE_OBJECT)
+
+struct _HdyHeaderGroup
+{
+ GObject parent_instance;
+
+ GSList *children;
+ gboolean decorate_all;
+ gchar *layout;
+};
+
+static void hdy_header_group_buildable_init (GtkBuildableIface *iface);
+static gboolean hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *data);
+static void hdy_header_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data);
+
+G_DEFINE_TYPE_WITH_CODE (HdyHeaderGroup, hdy_header_group, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_header_group_buildable_init))
+
+enum {
+ PROP_0,
+ PROP_DECORATE_ALL,
+ N_PROPS
+};
+
+static GParamSpec *props [N_PROPS];
+
+static void update_decoration_layouts (HdyHeaderGroup *self);
+
+static void
+object_destroyed_cb (HdyHeaderGroupChild *self,
+ GObject *object)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ self->object = NULL;
+
+ g_object_unref (self);
+}
+
+static void
+forward_update_decoration_layouts (HdyHeaderGroupChild *self)
+{
+ HdyHeaderGroup *header_group;
+
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ header_group = HDY_HEADER_GROUP (g_object_get_data (G_OBJECT (self), "header-group"));
+
+ g_assert (HDY_IS_HEADER_GROUP (header_group));
+
+ g_signal_emit (header_group, signals[SIGNAL_UPDATE_DECORATION_LAYOUTS], 0);
+
+ update_decoration_layouts (header_group);
+}
+
+static void
+hdy_header_group_child_dispose (GObject *object)
+{
+ HdyHeaderGroupChild *self = (HdyHeaderGroupChild *)object;
+
+ if (self->object) {
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (object_destroyed_cb), self);
+ g_signal_handlers_disconnect_by_func (self->object, G_CALLBACK (forward_update_decoration_layouts), self);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ g_object_weak_unref (self->object, (GWeakNotify) object_destroyed_cb, self);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ self->object = NULL;
+ }
+
+ G_OBJECT_CLASS (hdy_header_group_child_parent_class)->dispose (object);
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_header_bar (HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *self;
+ gpointer header_group;
+
+ g_return_val_if_fail (HDY_IS_HEADER_BAR (header_bar), NULL);
+
+ header_group = g_object_get_data (G_OBJECT (header_bar), "header-group");
+
+ g_return_val_if_fail (header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR;
+ self->object = G_OBJECT (header_bar);
+
+ g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self);
+
+ g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self);
+ g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_gtk_header_bar (GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *self;
+ gpointer header_group;
+
+ g_return_val_if_fail (GTK_IS_HEADER_BAR (header_bar), NULL);
+
+ header_group = g_object_get_data (G_OBJECT (header_bar), "header-group");
+
+ g_return_val_if_fail (header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR;
+ self->object = G_OBJECT (header_bar);
+
+ g_signal_connect_swapped (header_bar, "destroy", G_CALLBACK (object_destroyed_cb), self);
+
+ g_signal_connect_swapped (header_bar, "map", G_CALLBACK (forward_update_decoration_layouts), self);
+ g_signal_connect_swapped (header_bar, "unmap", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static HdyHeaderGroupChild *
+hdy_header_group_child_new_for_header_group (HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *self;
+ gpointer parent_header_group;
+
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (header_group), NULL);
+
+ parent_header_group = g_object_get_data (G_OBJECT (header_group), "header-group");
+
+ g_return_val_if_fail (parent_header_group == NULL, NULL);
+
+ self = g_object_new (HDY_TYPE_HEADER_GROUP_CHILD, NULL);
+ self->type = HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP;
+ self->object = G_OBJECT (header_group);
+
+ g_object_weak_unref (G_OBJECT (header_group), (GWeakNotify) object_destroyed_cb, self);
+
+ g_signal_connect_swapped (header_group, "update-decoration-layouts", G_CALLBACK (forward_update_decoration_layouts), self);
+
+ return self;
+}
+
+static void
+hdy_header_group_child_class_init (HdyHeaderGroupChildClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_header_group_child_dispose;
+}
+
+static void
+hdy_header_group_child_init (HdyHeaderGroupChild *self)
+{
+}
+
+static void
+hdy_header_group_child_set_decoration_layout (HdyHeaderGroupChild *self,
+ const gchar *layout)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ hdy_header_bar_set_decoration_layout (HDY_HEADER_BAR (self->object), layout);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (self->object), layout);
+ break;
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ {
+ HdyHeaderGroup *group = HDY_HEADER_GROUP (self->object);
+
+ g_free (group->layout);
+ group->layout = g_strdup (layout);
+
+ update_decoration_layouts (group);
+ }
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static gboolean
+hdy_header_group_child_get_mapped (HdyHeaderGroupChild *self)
+{
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (self));
+
+ switch (self->type) {
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR:
+ case HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR:
+ return gtk_widget_get_mapped (GTK_WIDGET (self->object));
+ case HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP:
+ for (GSList *children = HDY_HEADER_GROUP (self->object)->children;
+ children != NULL;
+ children = children->next)
+ if (hdy_header_group_child_get_mapped (HDY_HEADER_GROUP_CHILD (children->data)))
+ return TRUE;
+
+ return FALSE;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static HdyHeaderGroupChild *
+get_child_for_object (HdyHeaderGroup *self,
+ gpointer object)
+{
+ GSList *children;
+
+ for (children = self->children; children != NULL; children = children->next) {
+ HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data);
+
+ g_assert (child);
+
+ if (child->object == object)
+ return child;
+ }
+
+ return NULL;
+}
+
+static void
+update_decoration_layouts (HdyHeaderGroup *self)
+{
+ GSList *children;
+ GtkSettings *settings;
+ HdyHeaderGroupChild *start_child = NULL, *end_child = NULL;
+ g_autofree gchar *layout = NULL;
+ g_autofree gchar *start_layout = NULL;
+ g_autofree gchar *end_layout = NULL;
+ g_auto(GStrv) ends = NULL;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+
+ children = self->children;
+
+ if (children == NULL)
+ return;
+
+ settings = gtk_settings_get_default ();
+ if (self->layout)
+ layout = g_strdup (self->layout);
+ else
+ g_object_get (G_OBJECT (settings), "gtk-decoration-layout", &layout, NULL);
+ if (layout == NULL)
+ layout = g_strdup (":");
+
+ if (self->decorate_all) {
+ for (; children != NULL; children = children->next)
+ hdy_header_group_child_set_decoration_layout (HDY_HEADER_GROUP_CHILD (children->data), layout);
+
+ return;
+ }
+
+ for (; children != NULL; children = children->next) {
+ HdyHeaderGroupChild *child = HDY_HEADER_GROUP_CHILD (children->data);
+
+ hdy_header_group_child_set_decoration_layout (child, ":");
+
+ if (!hdy_header_group_child_get_mapped (child))
+ continue;
+
+ /* The headerbars are in reverse order in the list. */
+ start_child = child;
+ if (end_child == NULL)
+ end_child = child;
+ }
+
+ if (start_child == NULL || end_child == NULL)
+ return;
+
+ if (start_child == end_child) {
+ hdy_header_group_child_set_decoration_layout (start_child, layout);
+
+ return;
+ }
+
+ ends = g_strsplit (layout, ":", 2);
+ if (g_strv_length (ends) >= 2) {
+ start_layout = g_strdup_printf ("%s:", ends[0]);
+ end_layout = g_strdup_printf (":%s", ends[1]);
+ } else {
+ start_layout = g_strdup (":");
+ end_layout = g_strdup (":");
+ }
+ hdy_header_group_child_set_decoration_layout (start_child, start_layout);
+ hdy_header_group_child_set_decoration_layout (end_child, end_layout);
+}
+
+static void
+child_destroyed_cb (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_assert (HDY_IS_HEADER_GROUP (self));
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_assert (g_slist_find (self->children, child) != NULL);
+
+ self->children = g_slist_remove (self->children, child);
+
+ g_object_unref (self);
+}
+
+HdyHeaderGroup *
+hdy_header_group_new (void)
+{
+ return g_object_new (HDY_TYPE_HEADER_GROUP, NULL);
+}
+
+static void
+hdy_header_group_add_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_assert (HDY_IS_HEADER_GROUP (self));
+ g_assert (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_assert (g_slist_find (self->children, child) == NULL);
+
+ self->children = g_slist_prepend (self->children, child);
+ g_object_weak_ref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self);
+ g_object_ref (self);
+
+ update_decoration_layouts (self);
+
+ g_object_set_data (G_OBJECT (child), "header-group", self);
+}
+
+/**
+ * hdy_header_group_add_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #HdyHeaderBar to add
+ *
+ * Adds @header_bar to @self.
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_BAR (header_bar));
+ g_return_if_fail (get_child_for_object (self, header_bar) == NULL);
+
+ child = hdy_header_group_child_new_for_header_bar (header_bar);
+ hdy_header_group_add_child (self, child);
+}
+
+/**
+ * hdy_header_group_add_gtk_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #GtkHeaderBar to add
+ *
+ * Adds @header_bar to @self.
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (GTK_IS_HEADER_BAR (header_bar));
+ g_return_if_fail (get_child_for_object (self, header_bar) == NULL);
+
+ child = hdy_header_group_child_new_for_gtk_header_bar (header_bar);
+ hdy_header_group_add_child (self, child);
+}
+
+/**
+ * hdy_header_group_add_header_group:
+ * @self: a #HdyHeaderGroup
+ * @header_group: the #HdyHeaderGroup to add
+ *
+ * Adds @header_group to @self.
+ * When the nested group is no longer referenced elsewhere, it will be removed
+ * from the header group.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_add_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP (header_group));
+ g_return_if_fail (get_child_for_object (self, header_group) == NULL);
+
+ child = hdy_header_group_child_new_for_header_group (header_group);
+ hdy_header_group_add_child (self, child);
+}
+
+typedef struct {
+ gchar *name;
+ gint line;
+ gint col;
+} ItemData;
+
+static void
+item_data_free (gpointer data)
+{
+ ItemData *item_data = data;
+
+ g_free (item_data->name);
+ g_free (item_data);
+}
+
+typedef struct {
+ GObject *object;
+ GtkBuilder *builder;
+ GSList *items;
+} GSListSubParserData;
+
+static void
+hdy_header_group_dispose (GObject *object)
+{
+ HdyHeaderGroup *self = (HdyHeaderGroup *)object;
+
+ g_slist_free_full (self->children, (GDestroyNotify) g_object_unref);
+ self->children = NULL;
+
+ G_OBJECT_CLASS (hdy_header_group_parent_class)->dispose (object);
+}
+
+static void
+hdy_header_group_finalize (GObject *object)
+{
+ HdyHeaderGroup *self = (HdyHeaderGroup *) object;
+
+ g_free (self->layout);
+
+ G_OBJECT_CLASS (hdy_header_group_parent_class)->finalize (object);
+}
+
+static void
+hdy_header_group_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderGroup *self = HDY_HEADER_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DECORATE_ALL:
+ g_value_set_boolean (value, hdy_header_group_get_decorate_all (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_header_group_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyHeaderGroup *self = HDY_HEADER_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DECORATE_ALL:
+ hdy_header_group_set_decorate_all (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+/*< private >
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @parent_name: the name of the expected parent element
+ * @error: return location for an error
+ *
+ * Checks that the parent element of the currently handled
+ * start tag is @parent_name and set @error if it isn't.
+ *
+ * This is intended to be called in start_element vfuncs to
+ * ensure that element nesting is as intended.
+ *
+ * Returns: %TRUE if @parent_name is the parent element
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static gboolean
+_gtk_builder_check_parent (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *parent_name,
+ GError **error)
+{
+ const GSList *stack;
+ gint line, col;
+ const gchar *parent;
+ const gchar *element;
+
+ stack = g_markup_parse_context_get_element_stack (context);
+
+ element = (const gchar *)stack->data;
+ parent = stack->next ? (const gchar *)stack->next->data : "";
+
+ if (g_str_equal (parent_name, parent) ||
+ (g_str_equal (parent_name, "object") && g_str_equal (parent, "template")))
+ return TRUE;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_INVALID_TAG,
+ ".:%d:%d Can't use <%s> here",
+ line, col, element);
+
+ return FALSE;
+}
+
+/*< private >
+ * _gtk_builder_prefix_error:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @error: an error
+ *
+ * Calls g_prefix_error() to prepend a filename:line:column marker
+ * to the given error. The filename is taken from @builder, and
+ * the line and column are obtained by calling
+ * g_markup_parse_context_get_position().
+ *
+ * This is intended to be called on errors returned by
+ * g_markup_collect_attributes() in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_prefix_error (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_prefix_error (error, ".:%d:%d ", line, col);
+}
+
+/*< private >
+ * _gtk_builder_error_unhandled_tag:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @object: name of the object that is being handled
+ * @element_name: name of the element whose start tag is being handled
+ * @error: return location for the error
+ *
+ * Sets @error to a suitable error indicating that an @element_name
+ * tag is not expected in the custom markup for @object.
+ *
+ * This is intended to be called in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *object,
+ const gchar *element_name,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_UNHANDLED_TAG,
+ ".:%d:%d Unsupported tag for %s: <%s>",
+ line, col,
+ object, element_name);
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+header_group_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **names,
+ const gchar **values,
+ gpointer user_data,
+ GError **error)
+{
+ GSListSubParserData *data = (GSListSubParserData*)user_data;
+
+ if (strcmp (element_name, "headerbar") == 0)
+ {
+ const gchar *name;
+ ItemData *item_data;
+
+ if (!_gtk_builder_check_parent (data->builder, context, "headerbars", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_STRING, "name", &name,
+ G_MARKUP_COLLECT_INVALID))
+ {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ item_data = g_new (ItemData, 1);
+ item_data->name = g_strdup (name);
+ g_markup_parse_context_get_position (context, &item_data->line, &item_data->col);
+ data->items = g_slist_prepend (data->items, item_data);
+ }
+ else if (strcmp (element_name, "headerbars") == 0)
+ {
+ if (!_gtk_builder_check_parent (data->builder, context, "object", error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_INVALID, NULL, NULL,
+ G_MARKUP_COLLECT_INVALID))
+ _gtk_builder_prefix_error (data->builder, context, error);
+ }
+ else
+ {
+ _gtk_builder_error_unhandled_tag (data->builder, context,
+ "HdyHeaderGroup", element_name,
+ error);
+ }
+}
+
+
+/* This has been copied and modified from gtksizegroup.c. */
+static const GMarkupParser header_group_parser =
+ {
+ header_group_start_element
+ };
+
+/* This has been copied and modified from gtksizegroup.c. */
+static gboolean
+hdy_header_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *parser_data)
+{
+ GSListSubParserData *data;
+
+ if (child)
+ return FALSE;
+
+ if (strcmp (tagname, "headerbars") == 0)
+ {
+ data = g_slice_new0 (GSListSubParserData);
+ data->items = NULL;
+ data->object = G_OBJECT (buildable);
+ data->builder = builder;
+
+ *parser = header_group_parser;
+ *parser_data = data;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+hdy_header_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data)
+{
+ GSList *l;
+ GSListSubParserData *data;
+
+ if (strcmp (tagname, "headerbars") != 0)
+ return;
+
+ data = (GSListSubParserData*)user_data;
+ data->items = g_slist_reverse (data->items);
+
+ for (l = data->items; l; l = l->next) {
+ ItemData *item_data = l->data;
+ GObject *object = gtk_builder_get_object (builder, item_data->name);
+
+ if (!object)
+ continue;
+
+ if (GTK_IS_HEADER_BAR (object))
+ hdy_header_group_add_gtk_header_bar (HDY_HEADER_GROUP (data->object),
+ GTK_HEADER_BAR (object));
+ else if (HDY_IS_HEADER_BAR (object))
+ hdy_header_group_add_header_bar (HDY_HEADER_GROUP (data->object),
+ HDY_HEADER_BAR (object));
+ else if (HDY_IS_HEADER_GROUP (object))
+ hdy_header_group_add_header_group (HDY_HEADER_GROUP (data->object),
+ HDY_HEADER_GROUP (object));
+ }
+
+ g_slist_free_full (data->items, item_data_free);
+ g_slice_free (GSListSubParserData, data);
+}
+
+static void
+hdy_header_group_class_init (HdyHeaderGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_header_group_dispose;
+ object_class->finalize = hdy_header_group_finalize;
+ object_class->get_property = hdy_header_group_get_property;
+ object_class->set_property = hdy_header_group_set_property;
+
+ /**
+ * HdyHeaderGroup:decorate-all:
+ *
+ * Whether the elements of the group should all receive the full decoration.
+ * This is useful in conjunction with #HdyLeaflet:folded when the leaflet
+ * contains the header bars of the group, as you want them all to display the
+ * complete decoration when the leaflet is folded.
+ *
+ * Since: 1.0
+ */
+ props[PROP_DECORATE_ALL] =
+ g_param_spec_boolean ("decorate-all",
+ _("Decorate all"),
+ _("Whether the elements of the group should all receive the full decoration"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, N_PROPS, props);
+
+ /**
+ * HdyHeaderGroup::update-decoration-layouts:
+ * @self: The #HdyHeaderGroup instance
+ *
+ * This signal is emitted before updating the decoration layouts.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_UPDATE_DECORATION_LAYOUTS] =
+ g_signal_new ("update-decoration-layouts",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+}
+
+static void
+hdy_header_group_init (HdyHeaderGroup *self)
+{
+ GtkSettings *settings = gtk_settings_get_default ();
+
+ g_signal_connect_swapped (settings, "notify::gtk-decoration-layout", G_CALLBACK (update_decoration_layouts), self);
+}
+
+static void
+hdy_header_group_buildable_init (GtkBuildableIface *iface)
+{
+ iface->custom_tag_start = hdy_header_group_buildable_custom_tag_start;
+ iface->custom_finished = hdy_header_group_buildable_custom_finished;
+}
+
+/**
+ * hdy_header_group_child_get_header_bar:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #HdyHeaderBar.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #HdyHeaderBar, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+HdyHeaderBar *
+hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR, NULL);
+
+ return HDY_HEADER_BAR (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_gtk_header_bar:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #GtkHeaderBar.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #GtkHeaderBar, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+GtkHeaderBar *
+hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR, NULL);
+
+ return GTK_HEADER_BAR (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_header_group:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child #HdyHeaderGroup.
+ * Use hdy_header_group_child_get_child_type() to check the child type.
+ *
+ * Returns: (transfer none): the child #HdyHeaderGroup, or %NULL in case of error.
+ *
+ * Since: 1.0
+ */
+HdyHeaderGroup *
+hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), NULL);
+ g_return_val_if_fail (self->type == HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP, NULL);
+
+ return HDY_HEADER_GROUP (self->object);
+}
+
+/**
+ * hdy_header_group_child_get_child_type:
+ * @self: a #HdyHeaderGroupChild
+ *
+ * Gets the child type.
+ *
+ * Returns: the child type.
+ *
+ * Since: 1.0
+ */
+HdyHeaderGroupChildType
+hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP_CHILD (self), HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR);
+
+ return self->type;
+}
+
+/**
+ * hdy_header_group_get_children:
+ * @self: a #HdyHeaderGroup
+ *
+ * Returns the list of children associated with @self.
+ *
+ * Returns: (element-type HdyHeaderGroupChild) (transfer none): the #GSList of
+ * children. The list is owned by libhandy and should not be modified.
+ *
+ * Since: 1.0
+ */
+GSList *
+hdy_header_group_get_children (HdyHeaderGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), NULL);
+
+ return self->children;
+}
+
+static void
+remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ self->children = g_slist_remove (self->children, child);
+
+ g_object_weak_unref (G_OBJECT (child), (GWeakNotify) child_destroyed_cb, self);
+
+ g_object_unref (self);
+ g_object_unref (child);
+}
+
+/**
+ * hdy_header_group_remove_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #HdyHeaderBar to remove
+ *
+ * Removes @header_bar from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_BAR (header_bar));
+
+ child = get_child_for_object (self, header_bar);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_gtk_header_bar:
+ * @self: a #HdyHeaderGroup
+ * @header_bar: the #GtkHeaderBar to remove
+ *
+ * Removes @header_bar from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (GTK_IS_HEADER_BAR (header_bar));
+
+ child = get_child_for_object (self, header_bar);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_header_group:
+ * @self: a #HdyHeaderGroup
+ * @header_group: the #HdyHeaderGroup to remove
+ *
+ * Removes a nested #HdyHeaderGroup from a #HdyHeaderGroup
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group)
+{
+ HdyHeaderGroupChild *child;
+
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP (header_group));
+
+ child = get_child_for_object (self, header_group);
+
+ g_return_if_fail (child != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_remove_child:
+ * @self: a #HdyHeaderGroup
+ * @child: the #HdyHeaderGroupChild to remove
+ *
+ * Removes @child from @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child)
+{
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+ g_return_if_fail (HDY_IS_HEADER_GROUP_CHILD (child));
+ g_return_if_fail (g_slist_find (self->children, child) != NULL);
+
+ remove_child (self, child);
+}
+
+/**
+ * hdy_header_group_set_decorate_all:
+ * @self: a #HdyHeaderGroup
+ * @decorate_all: whether the elements of the group should all receive the full decoration
+ *
+ * Sets whether the elements of the group should all receive the full decoration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_header_group_set_decorate_all (HdyHeaderGroup *self,
+ gboolean decorate_all)
+{
+ g_return_if_fail (HDY_IS_HEADER_GROUP (self));
+
+ decorate_all = !!decorate_all;
+
+ if (self->decorate_all == decorate_all)
+ return;
+
+ self->decorate_all = decorate_all;
+
+ update_decoration_layouts (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DECORATE_ALL]);
+}
+
+/**
+ * hdy_header_group_get_decorate_all:
+ * @self: a #HdyHeaderGroup
+ *
+ * Gets whether the elements of the group should all receive the full decoration.
+ *
+ * Returns: %TRUE if the elements of the group should all receive the full
+ * decoration, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_header_group_get_decorate_all (HdyHeaderGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_HEADER_GROUP (self), FALSE);
+
+ return self->decorate_all;
+}
diff --git a/subprojects/libhandy/src/hdy-header-group.h b/subprojects/libhandy/src/hdy-header-group.h
new file mode 100644
index 0000000..dc20a76
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-header-group.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-header-bar.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_HEADER_GROUP_CHILD (hdy_header_group_child_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyHeaderGroupChild, hdy_header_group_child, HDY, HEADER_GROUP_CHILD, GObject)
+
+#define HDY_TYPE_HEADER_GROUP (hdy_header_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyHeaderGroup, hdy_header_group, HDY, HEADER_GROUP, GObject)
+
+typedef enum {
+ HDY_HEADER_GROUP_CHILD_TYPE_HEADER_BAR,
+ HDY_HEADER_GROUP_CHILD_TYPE_GTK_HEADER_BAR,
+ HDY_HEADER_GROUP_CHILD_TYPE_HEADER_GROUP,
+} HdyHeaderGroupChildType;
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderBar *hdy_header_group_child_get_header_bar (HdyHeaderGroupChild *self);
+HDY_AVAILABLE_IN_ALL
+GtkHeaderBar *hdy_header_group_child_get_gtk_header_bar (HdyHeaderGroupChild *self);
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroup *hdy_header_group_child_get_header_group (HdyHeaderGroupChild *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroupChildType hdy_header_group_child_get_child_type (HdyHeaderGroupChild *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyHeaderGroup *hdy_header_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_add_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group);
+
+HDY_AVAILABLE_IN_ALL
+GSList *hdy_header_group_get_children (HdyHeaderGroup *self);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_header_bar (HdyHeaderGroup *self,
+ HdyHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_gtk_header_bar (HdyHeaderGroup *self,
+ GtkHeaderBar *header_bar);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_header_group (HdyHeaderGroup *self,
+ HdyHeaderGroup *header_group);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_remove_child (HdyHeaderGroup *self,
+ HdyHeaderGroupChild *child);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_header_group_get_decorate_all (HdyHeaderGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_header_group_set_decorate_all (HdyHeaderGroup *self,
+ gboolean decorate_all);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad-button-private.h b/subprojects/libhandy/src/hdy-keypad-button-private.h
new file mode 100644
index 0000000..723526a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button-private.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_KEYPAD_BUTTON (hdy_keypad_button_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyKeypadButton, hdy_keypad_button, HDY, KEYPAD_BUTTON, GtkButton)
+
+struct _HdyKeypadButtonClass
+{
+ GtkButtonClass parent_class;
+};
+
+GtkWidget *hdy_keypad_button_new (const gchar *symbols);
+gchar hdy_keypad_button_get_digit (HdyKeypadButton *self);
+const gchar *hdy_keypad_button_get_symbols (HdyKeypadButton *self);
+void hdy_keypad_button_show_symbols (HdyKeypadButton *self,
+ gboolean visible);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad-button.c b/subprojects/libhandy/src/hdy-keypad-button.c
new file mode 100644
index 0000000..436555d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button.c
@@ -0,0 +1,331 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-keypad-button-private.h"
+
+/**
+ * PRIVATE:hdy-keypad-button
+ * @short_description: A button on a #HdyKeypad keypad
+ * @Title: HdyKeypadButton
+ *
+ * The #HdyKeypadButton widget is a single button on an #HdyKeypad. It
+ * can represent a single symbol (typically a digit) plus an arbitrary
+ * number of symbols that are displayed below it.
+ */
+
+enum {
+ PROP_0,
+ PROP_DIGIT,
+ PROP_SYMBOLS,
+ PROP_SHOW_SYMBOLS,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+struct _HdyKeypadButton
+{
+ GtkButton parent_instance;
+
+ GtkLabel *label, *secondary_label;
+ gchar *symbols;
+};
+
+G_DEFINE_TYPE (HdyKeypadButton, hdy_keypad_button, GTK_TYPE_BUTTON)
+
+static void
+format_label(HdyKeypadButton *self)
+{
+ g_autofree gchar *text = NULL;
+ gchar *secondary_text = NULL;
+
+ if (self->symbols != NULL && *(self->symbols) != '\0') {
+ secondary_text = g_utf8_find_next_char (self->symbols, NULL);
+ text = g_strndup (self->symbols, 1);
+ }
+
+ gtk_label_set_label (self->label, text);
+ gtk_label_set_label (self->secondary_label, secondary_text);
+}
+
+static void
+hdy_keypad_button_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ switch (property_id) {
+ case PROP_SYMBOLS:
+ if (g_strcmp0 (self->symbols, g_value_get_string (value)) != 0) {
+ g_free (self->symbols);
+ self->symbols = g_value_dup_string (value);
+ format_label(self);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS]);
+ }
+ break;
+
+ case PROP_SHOW_SYMBOLS:
+ hdy_keypad_button_show_symbols (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_keypad_button_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ switch (property_id) {
+ case PROP_DIGIT:
+ g_value_set_schar (value, hdy_keypad_button_get_digit (self));
+ break;
+
+ case PROP_SYMBOLS:
+ g_value_set_string (value, hdy_keypad_button_get_symbols (self));
+ break;
+
+ case PROP_SHOW_SYMBOLS:
+ g_value_set_boolean (value, gtk_widget_is_visible (GTK_WIDGET (self->secondary_label)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_keypad_button_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class);
+ gint min1, min2, nat1, nat2;
+
+ if (for_size < 0) {
+ widget_class->get_preferred_width (widget, &min1, &nat1);
+ widget_class->get_preferred_height (widget, &min2, &nat2);
+ }
+ else {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ widget_class->get_preferred_width_for_height (widget, for_size, &min1, &nat1);
+ else
+ widget_class->get_preferred_height_for_width (widget, for_size, &min1, &nat1);
+ min2 = nat2 = for_size;
+ }
+
+ if (minimum)
+ *minimum = MAX (min1, min2);
+ if (natural)
+ *natural = MAX (nat1, nat2);
+}
+
+static GtkSizeRequestMode
+hdy_keypad_button_get_request_mode (GtkWidget *widget)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (hdy_keypad_button_parent_class);
+ gint min1, min2;
+ widget_class->get_preferred_width (widget, &min1, NULL);
+ widget_class->get_preferred_height (widget, &min2, NULL);
+ if (min1 < min2)
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+ else
+ return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT;
+}
+
+static void
+hdy_keypad_button_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_keypad_button_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_keypad_button_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_keypad_button_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_keypad_button_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ *minimum_width = height;
+ *natural_width = height;
+}
+
+static void
+hdy_keypad_button_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ *minimum_height = width;
+ *natural_height = width;
+}
+
+
+static void
+hdy_keypad_button_finalize (GObject *object)
+{
+ HdyKeypadButton *self = HDY_KEYPAD_BUTTON (object);
+
+ g_clear_pointer (&self->symbols, g_free);
+ G_OBJECT_CLASS (hdy_keypad_button_parent_class)->finalize (object);
+}
+
+
+static void
+hdy_keypad_button_class_init (HdyKeypadButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->set_property = hdy_keypad_button_set_property;
+ object_class->get_property = hdy_keypad_button_get_property;
+
+ object_class->finalize = hdy_keypad_button_finalize;
+
+ widget_class->get_request_mode = hdy_keypad_button_get_request_mode;
+ widget_class->get_preferred_width = hdy_keypad_button_get_preferred_width;
+ widget_class->get_preferred_height = hdy_keypad_button_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_keypad_button_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_keypad_button_get_preferred_height_for_width;
+
+ props[PROP_DIGIT] =
+ g_param_spec_int ("digit",
+ _("Digit"),
+ _("The keypad digit of the button"),
+ -1, INT_MAX, 0,
+ G_PARAM_READABLE);
+
+ props[PROP_SYMBOLS] =
+ g_param_spec_string ("symbols",
+ _("Symbols"),
+ _("The keypad symbols of the button. The first symbol is used as the digit"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_SHOW_SYMBOLS] =
+ g_param_spec_boolean ("show-symbols",
+ _("Show symbols"),
+ _("Whether the second line of symbols should be shown or not"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-keypad-button.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, label);
+ gtk_widget_class_bind_template_child (widget_class, HdyKeypadButton, secondary_label);
+}
+
+static void
+hdy_keypad_button_init (HdyKeypadButton *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->symbols = NULL;
+}
+
+/**
+ * hdy_keypad_button_new:
+ * @symbols: (nullable): the symbols displayed on the #HdyKeypadButton
+ *
+ * Create a new #HdyKeypadButton which displays @symbols,
+ * where the first char is used as the main and the other symbols are shown below
+ *
+ * Returns: the newly created #HdyKeypadButton widget
+ */
+GtkWidget *
+hdy_keypad_button_new (const gchar *symbols)
+{
+ return g_object_new (HDY_TYPE_KEYPAD_BUTTON, "symbols", symbols, NULL);
+}
+
+/**
+ * hdy_keypad_button_get_digit:
+ * @self: a #HdyKeypadButton
+ *
+ * Get the #HdyKeypadButton's digit.
+ *
+ * Returns: the button's digit
+ */
+char
+hdy_keypad_button_get_digit (HdyKeypadButton *self)
+{
+ g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), '\0');
+
+ if (self->symbols == NULL)
+ return ('\0');
+
+ return *(self->symbols);
+}
+
+/**
+ * hdy_keypad_button_get_symbols:
+ * @self: a #HdyKeypadButton
+ *
+ * Get the #HdyKeypadButton's symbols.
+ *
+ * Returns: the button's symbols including the digit.
+ */
+const char*
+hdy_keypad_button_get_symbols (HdyKeypadButton *self)
+{
+ g_return_val_if_fail (HDY_IS_KEYPAD_BUTTON (self), NULL);
+
+ return self->symbols;
+}
+
+/**
+ * hdy_keypad_button_show_symbols:
+ * @self: a #HdyKeypadButton
+ * @visible: whether the second line should be shown or not
+ *
+ * Sets the visibility of the second line of symbols for #HdyKeypadButton
+ *
+ */
+void
+hdy_keypad_button_show_symbols (HdyKeypadButton *self, gboolean visible)
+{
+ gboolean old_visible;
+
+ g_return_if_fail (HDY_IS_KEYPAD_BUTTON (self));
+
+ old_visible = gtk_widget_get_visible (GTK_WIDGET (self->secondary_label));
+
+ if (old_visible != visible) {
+ gtk_widget_set_visible (GTK_WIDGET (self->secondary_label), visible);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_SYMBOLS]);
+ }
+}
diff --git a/subprojects/libhandy/src/hdy-keypad-button.ui b/subprojects/libhandy/src/hdy-keypad-button.ui
new file mode 100644
index 0000000..27a53dd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad-button.ui
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.20.1 -->
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyKeypadButton" parent="GtkButton">
+ <property name="can_focus">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="margin">6</property>
+ <child>
+ <object class="GtkLabel" id="label">
+ <property name="visible">True</property>
+ <property name="label"></property>
+ <style>
+ <class name="digit"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="secondary_label">
+ <property name="visible">True</property>
+ <property name="no_show_all">True</property>
+ <property name="label"></property>
+ <style>
+ <class name="dim-label"/>
+ <class name="letters"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-keypad.c b/subprojects/libhandy/src/hdy-keypad.c
new file mode 100644
index 0000000..1714d9a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.c
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-keypad.h"
+#include "hdy-keypad-button-private.h"
+
+/**
+ * SECTION:hdy-keypad
+ * @short_description: A keypad for dialing numbers
+ * @Title: HdyKeypad
+ *
+ * The #HdyKeypad widget is a keypad for entering numbers such as phone numbers
+ * or PIN codes.
+ *
+ * # CSS nodes
+ *
+ * #HdyKeypad has a single CSS node with name keypad.
+ *
+ * Since: 0.0.12
+ */
+
+typedef struct
+{
+ GtkEntry *entry;
+ GtkWidget *grid;
+ GtkWidget *label_asterisk;
+ GtkWidget *label_hash;
+ GtkGesture *long_press_zero_gesture;
+ guint16 row_spacing;
+ guint16 column_spacing;
+ gboolean symbols_visible;
+ gboolean letters_visible;
+} HdyKeypadPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyKeypad, hdy_keypad, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_ROW_SPACING,
+ PROP_COLUMN_SPACING,
+ PROP_LETTERS_VISIBLE,
+ PROP_SYMBOLS_VISIBLE,
+ PROP_ENTRY,
+ PROP_END_ACTION,
+ PROP_START_ACTION,
+ PROP_LAST_PROP,
+};
+static GParamSpec *props[PROP_LAST_PROP];
+
+static void
+symbol_clicked (HdyKeypad *self,
+ gchar symbol)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+ g_autofree gchar *string = g_strdup_printf ("%c", symbol);
+
+ if (!priv->entry)
+ return;
+
+ g_signal_emit_by_name (priv->entry, "insert-at-cursor", string, NULL);
+ /* Set focus to the entry only when it can get focus
+ * https://gitlab.gnome.org/GNOME/gtk/issues/2204
+ */
+ if (gtk_widget_get_can_focus (GTK_WIDGET (priv->entry)))
+ gtk_entry_grab_focus_without_selecting (priv->entry);
+}
+
+
+static void
+button_clicked_cb (HdyKeypad *self,
+ HdyKeypadButton *btn)
+{
+ gchar digit = hdy_keypad_button_get_digit (btn);
+ symbol_clicked (self, digit);
+ g_debug ("Button with number %c was pressed", digit);
+}
+
+
+static void
+asterisk_button_clicked_cb (HdyKeypad *self,
+ GtkWidget *btn)
+{
+ symbol_clicked (self, '*');
+ g_debug ("Button with * was pressed");
+}
+
+
+static void
+hash_button_clicked_cb (HdyKeypad *self,
+ GtkWidget *btn)
+{
+ symbol_clicked (self, '#');
+ g_debug ("Button with # was pressed");
+}
+
+
+static void
+insert_text_cb (HdyKeypad *self,
+ gchar *text,
+ gint length,
+ gpointer position,
+ GtkEditable *editable)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_assert (length == 1);
+
+ if (g_ascii_isdigit (*text))
+ return;
+
+ if (!priv->symbols_visible && strchr ("#*+", *text))
+ return;
+
+ g_signal_stop_emission_by_name (editable, "insert-text");
+}
+
+
+static void
+long_press_zero_cb (HdyKeypad *self,
+ gdouble x,
+ gdouble y,
+ GtkGesture *gesture)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->symbols_visible)
+ return;
+
+ g_debug ("Long press on zero button");
+ symbol_clicked (self, '+');
+ gtk_gesture_set_state (GTK_GESTURE (gesture), GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+
+static void
+hdy_keypad_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypad *self = HDY_KEYPAD (object);
+
+ switch (property_id) {
+ case PROP_ROW_SPACING:
+ hdy_keypad_set_row_spacing (self, g_value_get_uint (value));
+ break;
+ case PROP_COLUMN_SPACING:
+ hdy_keypad_set_column_spacing (self, g_value_get_uint (value));
+ break;
+ case PROP_LETTERS_VISIBLE:
+ hdy_keypad_set_letters_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_SYMBOLS_VISIBLE:
+ hdy_keypad_set_symbols_visible (self, g_value_get_boolean (value));
+ break;
+ case PROP_ENTRY:
+ hdy_keypad_set_entry (self, g_value_get_object (value));
+ break;
+ case PROP_END_ACTION:
+ hdy_keypad_set_end_action (self, g_value_get_object (value));
+ break;
+ case PROP_START_ACTION:
+ hdy_keypad_set_start_action (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+hdy_keypad_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyKeypad *self = HDY_KEYPAD (object);
+
+ switch (property_id) {
+ case PROP_ROW_SPACING:
+ g_value_set_uint (value, hdy_keypad_get_row_spacing (self));
+ break;
+ case PROP_COLUMN_SPACING:
+ g_value_set_uint (value, hdy_keypad_get_column_spacing (self));
+ break;
+ case PROP_LETTERS_VISIBLE:
+ g_value_set_boolean (value, hdy_keypad_get_letters_visible (self));
+ break;
+ case PROP_SYMBOLS_VISIBLE:
+ g_value_set_boolean (value, hdy_keypad_get_symbols_visible (self));
+ break;
+ case PROP_ENTRY:
+ g_value_set_object (value, hdy_keypad_get_entry (self));
+ break;
+ case PROP_START_ACTION:
+ g_value_set_object (value, hdy_keypad_get_start_action (self));
+ break;
+ case PROP_END_ACTION:
+ g_value_set_object (value, hdy_keypad_get_end_action (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+
+static void
+hdy_keypad_finalize (GObject *object)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (HDY_KEYPAD (object));
+
+ if (priv->long_press_zero_gesture != NULL)
+ g_object_unref (priv->long_press_zero_gesture);
+
+ G_OBJECT_CLASS (hdy_keypad_parent_class)->finalize (object);
+}
+
+
+static void
+hdy_keypad_class_init (HdyKeypadClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_keypad_finalize;
+
+ object_class->set_property = hdy_keypad_set_property;
+ object_class->get_property = hdy_keypad_get_property;
+
+ /**
+ * HdyKeypad:row-spacing:
+ *
+ * The amount of space between two consecutive rows.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ROW_SPACING] =
+ g_param_spec_uint ("row-spacing",
+ _("Row spacing"),
+ _("The amount of space between two consecutive rows"),
+ 0, G_MAXINT16, 6,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:column-spacing:
+ *
+ * The amount of space between two consecutive columns.
+ *
+ * Since: 1.0
+ */
+ props[PROP_COLUMN_SPACING] =
+ g_param_spec_uint ("column-spacing",
+ _("Column spacing"),
+ _("The amount of space between two consecutive columns"),
+ 0, G_MAXINT16, 6,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:letters-visible:
+ *
+ * Whether the keypad should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Since: 1.0
+ */
+ props[PROP_LETTERS_VISIBLE] =
+ g_param_spec_boolean ("letters-visible",
+ _("Letters visible"),
+ _("Whether the letters below the digits should be visible"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:symbols-visible:
+ *
+ * Whether the keypad should display the hash and asterisk buttons, and should
+ * display the plus symbol at the bottom of its 0 button.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SYMBOLS_VISIBLE] =
+ g_param_spec_boolean ("symbols-visible",
+ _("Symbols visible"),
+ _("Whether the hash, plus, and asterisk symbols should be visible"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:entry:
+ *
+ * The entry widget connected to the keypad. See hdy_keypad_set_entry() for
+ * details.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ENTRY] =
+ g_param_spec_object ("entry",
+ _("Entry"),
+ _("The entry widget connected to the keypad"),
+ GTK_TYPE_ENTRY,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:end-action:
+ *
+ * The widget for the lower end corner of @self.
+ *
+ * Since: 1.0
+ */
+ props[PROP_END_ACTION] =
+ g_param_spec_object ("end-action",
+ _("End action"),
+ _("The end action widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyKeypad:start-action:
+ *
+ * The widget for the lower start corner of @self.
+ *
+ * Since: 1.0
+ */
+ props[PROP_START_ACTION] =
+ g_param_spec_object ("start-action",
+ _("Start action"),
+ _("The start action widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, PROP_LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-keypad.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, grid);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_asterisk);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, label_hash);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyKeypad, long_press_zero_gesture);
+
+ gtk_widget_class_bind_template_callback (widget_class, button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, asterisk_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, hash_button_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, long_press_zero_cb);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_DIAL);
+ gtk_widget_class_set_css_name (widget_class, "keypad");
+}
+
+
+static void
+hdy_keypad_init (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ priv->row_spacing = 6;
+ priv->column_spacing = 6;
+ priv->letters_visible = TRUE;
+ priv->symbols_visible = TRUE;
+
+ g_type_ensure (HDY_TYPE_KEYPAD_BUTTON);
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+
+/**
+ * hdy_keypad_new:
+ * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible
+ * @letters_visible: whether the letters below the digits should be visible
+ *
+ * Create a new #HdyKeypad widget.
+ *
+ * Returns: the newly created #HdyKeypad widget
+ *
+ * Since: 0.0.12
+ */
+GtkWidget *
+hdy_keypad_new (gboolean symbols_visible,
+ gboolean letters_visible)
+{
+ return g_object_new (HDY_TYPE_KEYPAD,
+ "symbols-visible", symbols_visible,
+ "letters-visible", letters_visible,
+ NULL);
+}
+
+/**
+ * hdy_keypad_set_row_spacing:
+ * @self: a #HdyKeypad
+ * @spacing: the amount of space to insert between rows
+ *
+ * Sets the amount of space between rows of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_row_spacing (HdyKeypad *self,
+ guint spacing)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (spacing <= G_MAXINT16);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->row_spacing == spacing)
+ return;
+
+ priv->row_spacing = spacing;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ROW_SPACING]);
+}
+
+
+/**
+ * hdy_keypad_get_row_spacing:
+ * @self: a #HdyKeypad
+ *
+ * Returns the amount of space between the rows of @self.
+ *
+ * Returns: the row spacing of @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_keypad_get_row_spacing (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), 0);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->row_spacing;
+}
+
+
+/**
+ * hdy_keypad_set_column_spacing:
+ * @self: a #HdyKeypad
+ * @spacing: the amount of space to insert between columns
+ *
+ * Sets the amount of space between columns of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_column_spacing (HdyKeypad *self,
+ guint spacing)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (spacing <= G_MAXINT16);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (priv->column_spacing == spacing)
+ return;
+
+ priv->column_spacing = spacing;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COLUMN_SPACING]);
+}
+
+
+/**
+ * hdy_keypad_get_column_spacing:
+ * @self: a #HdyKeypad
+ *
+ * Returns the amount of space between the columns of @self.
+ *
+ * Returns: the column spacing of @self
+ *
+ * Since: 1.0
+ */
+guint
+hdy_keypad_get_column_spacing (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), 0);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->column_spacing;
+}
+
+
+/**
+ * hdy_keypad_set_letters_visible:
+ * @self: a #HdyKeypad
+ * @letters_visible: whether the letters below the digits should be visible
+ *
+ * Sets whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_letters_visible (HdyKeypad *self,
+ gboolean letters_visible)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+
+ letters_visible = !!letters_visible;
+
+ if (priv->letters_visible == letters_visible)
+ return;
+
+ priv->letters_visible = letters_visible;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LETTERS_VISIBLE]);
+}
+
+
+/**
+ * hdy_keypad_get_letters_visible:
+ * @self: a #HdyKeypad
+ *
+ * Returns whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Returns: whether the letters below the digits should be visible
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_keypad_get_letters_visible (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->letters_visible;
+}
+
+
+/**
+ * hdy_keypad_set_symbols_visible:
+ * @self: a #HdyKeypad
+ * @symbols_visible: whether the hash, plus, and asterisk symbols should be visible
+ *
+ * Sets whether @self should display the hash and asterisk buttons, and should
+ * display the plus symbol at the bottom of its 0 button.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_symbols_visible (HdyKeypad *self,
+ gboolean symbols_visible)
+{
+ HdyKeypadPrivate *priv = hdy_keypad_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+
+ symbols_visible = !!symbols_visible;
+
+ if (priv->symbols_visible == symbols_visible)
+ return;
+
+ priv->symbols_visible = symbols_visible;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SYMBOLS_VISIBLE]);
+}
+
+
+/**
+ * hdy_keypad_get_symbols_visible:
+ * @self: a #HdyKeypad
+ *
+ * Returns whether @self should display the standard letters below the digits on
+ * its buttons.
+ *
+ * Returns Whether @self should display the hash and asterisk buttons, and
+ * should display the plus symbol at the bottom of its 0 button.
+ *
+ * Returns: whether the hash, plus, and asterisk symbols should be visible
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_keypad_get_symbols_visible (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), FALSE);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->symbols_visible;
+}
+
+
+/**
+ * hdy_keypad_set_entry:
+ * @self: a #HdyKeypad
+ * @entry: (nullable): a #GtkEntry
+ *
+ * Binds @entry to @self and blocks any input which wouldn't be possible to type
+ * with with the keypad.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_keypad_set_entry (HdyKeypad *self,
+ GtkEntry *entry)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ if (entry == priv->entry)
+ return;
+
+ g_clear_object (&priv->entry);
+
+ if (entry) {
+ priv->entry = g_object_ref (entry);
+
+ gtk_widget_show (GTK_WIDGET (priv->entry));
+ /* Workaround: To keep the osk closed
+ * https://gitlab.gnome.org/GNOME/gtk/merge_requests/978#note_546576 */
+ g_object_set (priv->entry, "im-module", "gtk-im-context-none", NULL);
+
+ g_signal_connect_swapped (G_OBJECT (priv->entry),
+ "insert-text",
+ G_CALLBACK (insert_text_cb),
+ self);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]);
+}
+
+
+/**
+ * hdy_keypad_get_entry:
+ * @self: a #HdyKeypad
+ *
+ * Get the connected entry. See hdy_keypad_set_entry() for details.
+ *
+ * Returns: (transfer none): the set #GtkEntry or %NULL if no widget was set
+ *
+ * Since: 1.0
+ */
+GtkEntry *
+hdy_keypad_get_entry (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return priv->entry;
+}
+
+
+/**
+ * hdy_keypad_set_start_action:
+ * @self: a #HdyKeypad
+ * @start_action: (nullable): the start action widget
+ *
+ * Sets the widget for the lower left corner (or right, in RTL locales) of
+ * @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_start_action (HdyKeypad *self,
+ GtkWidget *start_action)
+{
+ HdyKeypadPrivate *priv;
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (start_action == NULL || GTK_IS_WIDGET (start_action));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3);
+
+ if (old_widget == start_action)
+ return;
+
+ if (old_widget != NULL)
+ gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget);
+
+ if (start_action != NULL)
+ gtk_grid_attach (GTK_GRID (priv->grid), start_action, 0, 3, 1, 1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION]);
+}
+
+
+/**
+ * hdy_keypad_get_start_action:
+ * @self: a #HdyKeypad
+ *
+ * Returns the widget for the lower left corner (or right, in RTL locales) of
+ * @self.
+ *
+ * Returns: (transfer none) (nullable): the start action widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_keypad_get_start_action (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return gtk_grid_get_child_at (GTK_GRID (priv->grid), 0, 3);
+}
+
+
+/**
+ * hdy_keypad_set_end_action:
+ * @self: a #HdyKeypad
+ * @end_action: (nullable): the end action widget
+ *
+ * Sets the widget for the lower right corner (or left, in RTL locales) of
+ * @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_keypad_set_end_action (HdyKeypad *self,
+ GtkWidget *end_action)
+{
+ HdyKeypadPrivate *priv;
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_KEYPAD (self));
+ g_return_if_fail (end_action == NULL || GTK_IS_WIDGET (end_action));
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ old_widget = gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3);
+
+ if (old_widget == end_action)
+ return;
+
+ if (old_widget != NULL)
+ gtk_container_remove (GTK_CONTAINER (priv->grid), old_widget);
+
+ if (end_action != NULL)
+ gtk_grid_attach (GTK_GRID (priv->grid), end_action, 2, 3, 1, 1);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION]);
+}
+
+
+/**
+ * hdy_keypad_get_end_action:
+ * @self: a #HdyKeypad
+ *
+ * Returns the widget for the lower right corner (or left, in RTL locales) of
+ * @self.
+ *
+ * Returns: (transfer none) (nullable): the end action widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_keypad_get_end_action (HdyKeypad *self)
+{
+ HdyKeypadPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_KEYPAD (self), NULL);
+
+ priv = hdy_keypad_get_instance_private (self);
+
+ return gtk_grid_get_child_at (GTK_GRID (priv->grid), 2, 3);
+}
diff --git a/subprojects/libhandy/src/hdy-keypad.h b/subprojects/libhandy/src/hdy-keypad.h
new file mode 100644
index 0000000..267feea
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_KEYPAD (hdy_keypad_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyKeypad, hdy_keypad, HDY, KEYPAD, GtkBin)
+
+/**
+ * HdyKeypadClass:
+ * @parent_class: The parent class
+ */
+struct _HdyKeypadClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_new (gboolean symbols_visible,
+ gboolean letters_visible);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_row_spacing (HdyKeypad *self,
+ guint spacing);
+HDY_AVAILABLE_IN_ALL
+guint hdy_keypad_get_row_spacing (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_column_spacing (HdyKeypad *self,
+ guint spacing);
+HDY_AVAILABLE_IN_ALL
+guint hdy_keypad_get_column_spacing (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_letters_visible (HdyKeypad *self,
+ gboolean letters_visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_keypad_get_letters_visible (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_symbols_visible (HdyKeypad *self,
+ gboolean symbols_visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_keypad_get_symbols_visible (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_entry (HdyKeypad *self,
+ GtkEntry *entry);
+HDY_AVAILABLE_IN_ALL
+GtkEntry *hdy_keypad_get_entry (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_start_action (HdyKeypad *self,
+ GtkWidget *start_action);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_get_start_action (HdyKeypad *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_keypad_set_end_action (HdyKeypad *self,
+ GtkWidget *end_action);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_keypad_get_end_action (HdyKeypad *self);
+
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-keypad.ui b/subprojects/libhandy/src/hdy-keypad.ui
new file mode 100644
index 0000000..3f04532
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-keypad.ui
@@ -0,0 +1,216 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.20"/>
+ <template class="HdyKeypad" parent="GtkBin">
+ <child>
+ <object class="GtkGrid" id="grid">
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="hexpand">False</property>
+ <property name="vexpand">False</property>
+ <property name="row-spacing" bind-source="HdyKeypad" bind-property="row-spacing" bind-flags="sync-create"/>
+ <property name="column-spacing" bind-source="HdyKeypad" bind-property="column-spacing" bind-flags="sync-create"/>
+ <property name="column_homogeneous">True</property>
+ <property name="column_homogeneous">True</property>
+ <child>
+ <object class="HdyKeypadButton" id="btn_1">
+ <property name="symbols">1</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_2">
+ <property name="symbols">2ABC</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_3">
+ <property name="symbols">3DEF</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">0</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_4">
+ <property name="symbols">4GHI</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_5">
+ <property name="symbols">5JKL</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_6">
+ <property name="symbols">6MNO</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">1</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_7">
+ <property name="symbols">7PQRS</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_8">
+ <property name="symbols">8TUV</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_9">
+ <property name="symbols">9WXYZ</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="letters-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">2</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_asterisk">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="asterisk_button_clicked_cb" swapped="true"/>
+ <child>
+ <object class="GtkLabel" id="label_asterisk">
+ <property name="visible">True</property>
+ <property name="label">∗</property>
+ <style>
+ <class name="symbol"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">0</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyKeypadButton" id="btn_0">
+ <property name="symbols">0+</property>
+ <property name="visible">True</property>
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="show-symbols" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="button_clicked_cb" swapped="true"/>
+ </object>
+ <packing>
+ <property name="left_attach">1</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkButton" id="btn_hash">
+ <property name="can_focus">True</property>
+ <property name="receives_default">True</property>
+ <property name="focus_on_click">False</property>
+ <property name="visible" bind-source="HdyKeypad" bind-property="symbols-visible" bind-flags="sync-create"/>
+ <signal name="clicked" handler="hash_button_clicked_cb" swapped="true"/>
+ <child>
+ <object class="GtkLabel" id="label_hash">
+ <property name="visible">True</property>
+ <property name="label">#</property>
+ <style>
+ <class name="symbol"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="left_attach">2</property>
+ <property name="top_attach">3</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkGestureLongPress" id="long_press_zero_gesture">
+ <property name="widget">btn_0</property>
+ <signal name="pressed" handler="long_press_zero_cb" object="HdyKeypad" swapped="true"/>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-leaflet.c b/subprojects/libhandy/src/hdy-leaflet.c
new file mode 100644
index 0000000..8c1ba2a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-leaflet.c
@@ -0,0 +1,1209 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-leaflet.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-leaflet
+ * @short_description: An adaptive container acting like a box or a stack.
+ * @Title: HdyLeaflet
+ *
+ * The #HdyLeaflet widget can display its children like a #GtkBox does or
+ * like a #GtkStack does, adapting to size changes by switching between
+ * the two modes.
+ *
+ * When there is enough space the children are displayed side by side, otherwise
+ * only one is displayed and the leaflet is said to be “folded”.
+ * The threshold is dictated by the preferred minimum sizes of the children.
+ * When a leaflet is folded, the children can be navigated using swipe gestures.
+ *
+ * The “over” and “under” stack the children one on top of the other, while the
+ * “slide” transition puts the children side by side. While navigating to a
+ * child on the side or below can be performed by swiping the current child
+ * away, navigating to an upper child requires dragging it from the edge where
+ * it resides. This doesn't affect non-dragging swipes.
+ *
+ * The “over” and “under” transitions can draw their shadow on top of the
+ * window's transparent areas, like the rounded corners. This is a side-effect
+ * of allowing shadows to be drawn on top of OpenGL areas. It can be mitigated
+ * by using #HdyWindow or #HdyApplicationWindow as they will crop anything drawn
+ * beyond the rounded corners.
+ *
+ * # CSS nodes
+ *
+ * #HdyLeaflet has a single CSS node with name leaflet. The node will get the
+ * style classes .folded when it is folded, .unfolded when it's not, or none if
+ * it didn't compute its fold yet.
+ */
+
+/**
+ * HdyLeafletTransitionType:
+ * @HDY_LEAFLET_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_LEAFLET_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_LEAFLET_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between modes and
+ * children in a #HdyLeaflet widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 0.0.12
+ */
+
+enum {
+ PROP_0,
+ PROP_FOLDED,
+ PROP_HHOMOGENEOUS_FOLDED,
+ PROP_VHOMOGENEOUS_FOLDED,
+ PROP_HHOMOGENEOUS_UNFOLDED,
+ PROP_VHOMOGENEOUS_UNFOLDED,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_MODE_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+
+ /* orientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ORIENTATION,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_NAME,
+ CHILD_PROP_NAVIGATABLE,
+ LAST_CHILD_PROP,
+};
+
+typedef struct
+{
+ HdyStackableBox *box;
+} HdyLeafletPrivate;
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+static void hdy_leaflet_swipeable_init (HdySwipeableInterface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyLeaflet, hdy_leaflet, GTK_TYPE_CONTAINER,
+ G_ADD_PRIVATE (HdyLeaflet)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
+ G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, hdy_leaflet_swipeable_init))
+
+#define HDY_GET_HELPER(obj) (((HdyLeafletPrivate *) hdy_leaflet_get_instance_private (HDY_LEAFLET (obj)))->box)
+
+/**
+ * hdy_leaflet_get_folded:
+ * @self: a #HdyLeaflet
+ *
+ * Gets whether @self is folded.
+ *
+ * Returns: whether @self is folded.
+ */
+gboolean
+hdy_leaflet_get_folded (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_folded (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_homogeneous:
+ * @self: a #HdyLeaflet
+ * @folded: the fold
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyLeaflet to be homogeneous or not for the given fold and orientation.
+ * If it is homogeneous, the #HdyLeaflet will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't and it is folded, the leaflet may change width or height
+ * when a different child becomes visible.
+ */
+void
+hdy_leaflet_set_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_homogeneous (HDY_GET_HELPER (self), folded, orientation, homogeneous);
+}
+
+/**
+ * hdy_leaflet_get_homogeneous:
+ * @self: a #HdyLeaflet
+ * @folded: the fold
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given fold and orientation.
+ * See hdy_leaflet_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given fold and orientation.
+ */
+gboolean
+hdy_leaflet_get_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_homogeneous (HDY_GET_HELPER (self), folded, orientation);
+}
+
+/**
+ * hdy_leaflet_get_transition_type:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the type of animation that will be used
+ * for transitions between modes and children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 0.0.12
+ */
+HdyLeafletTransitionType
+hdy_leaflet_get_transition_type (HdyLeaflet *self)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), HDY_LEAFLET_TRANSITION_TYPE_OVER);
+
+ type = hdy_stackable_box_get_transition_type (HDY_GET_HELPER (self));
+
+ switch (type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return HDY_LEAFLET_TRANSITION_TYPE_OVER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return HDY_LEAFLET_TRANSITION_TYPE_UNDER;
+
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ return HDY_LEAFLET_TRANSITION_TYPE_SLIDE;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+/**
+ * hdy_leaflet_set_transition_type:
+ * @self: a #HdyLeaflet
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between modes
+ * and children in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about to
+ * become current.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_transition_type (HdyLeaflet *self,
+ HdyLeafletTransitionType transition)
+{
+ HdyStackableBoxTransitionType type;
+
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+ g_return_if_fail (transition <= HDY_LEAFLET_TRANSITION_TYPE_SLIDE);
+
+ switch (transition) {
+ case HDY_LEAFLET_TRANSITION_TYPE_OVER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ break;
+
+ case HDY_LEAFLET_TRANSITION_TYPE_UNDER:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ break;
+
+ case HDY_LEAFLET_TRANSITION_TYPE_SLIDE:
+ type = HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE;
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ hdy_stackable_box_set_transition_type (HDY_GET_HELPER (self), type);
+}
+
+/**
+ * hdy_leaflet_get_mode_transition_duration:
+ * @self: a #HdyLeaflet
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between modes in @self will take.
+ *
+ * Returns: the mode transition duration
+ */
+guint
+hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), 0);
+
+ return hdy_stackable_box_get_mode_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_mode_transition_duration:
+ * @self: a #HdyLeaflet
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between modes in @self
+ * will take.
+ */
+void
+hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_mode_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_leaflet_get_child_transition_duration:
+ * @self: a #HdyLeaflet
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ */
+guint
+hdy_leaflet_get_child_transition_duration (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), 0);
+
+ return hdy_stackable_box_get_child_transition_duration (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_child_transition_duration:
+ * @self: a #HdyLeaflet
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ */
+void
+hdy_leaflet_set_child_transition_duration (HdyLeaflet *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_child_transition_duration (HDY_GET_HELPER (self), duration);
+}
+
+/**
+ * hdy_leaflet_get_visible_child:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ */
+GtkWidget *
+hdy_leaflet_get_visible_child (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_visible_child (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_visible_child:
+ * @self: a #HdyLeaflet
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyLeaflet:transition-type and HdyLeaflet:child-transition-duration. The
+ * transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ */
+void
+hdy_leaflet_set_visible_child (HdyLeaflet *self,
+ GtkWidget *visible_child)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_visible_child (HDY_GET_HELPER (self), visible_child);
+}
+
+/**
+ * hdy_leaflet_get_visible_child_name:
+ * @self: a #HdyLeaflet
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ */
+const gchar *
+hdy_leaflet_get_visible_child_name (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_visible_child_name (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_visible_child_name:
+ * @self: a #HdyLeaflet
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_leaflet_set_visible_child() for more details.
+ */
+void
+hdy_leaflet_set_visible_child_name (HdyLeaflet *self,
+ const gchar *name)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_visible_child_name (HDY_GET_HELPER (self), name);
+}
+
+/**
+ * hdy_leaflet_get_child_transition_running:
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ */
+gboolean
+hdy_leaflet_get_child_transition_running (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_child_transition_running (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_interpolate_size:
+ * @self: a #HdyLeaflet
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyLeaflet:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ */
+void
+hdy_leaflet_set_interpolate_size (HdyLeaflet *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_interpolate_size (HDY_GET_HELPER (self), interpolate_size);
+}
+
+/**
+ * hdy_leaflet_get_interpolate_size:
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ */
+gboolean
+hdy_leaflet_get_interpolate_size (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_interpolate_size (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_can_swipe_back:
+ * @self: a #HdyLeaflet
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_can_swipe_back (HdyLeaflet *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_can_swipe_back (HDY_GET_HELPER (self), can_swipe_back);
+}
+
+/**
+ * hdy_leaflet_get_can_swipe_back
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 0.0.12
+ */
+gboolean
+hdy_leaflet_get_can_swipe_back (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_back (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_set_can_swipe_forward:
+ * @self: a #HdyLeaflet
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_LEAFLET (self));
+
+ hdy_stackable_box_set_can_swipe_forward (HDY_GET_HELPER (self), can_swipe_forward);
+}
+
+/**
+ * hdy_leaflet_get_can_swipe_forward
+ * @self: a #HdyLeaflet
+ *
+ * Returns whether the #HdyLeaflet allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 0.0.12
+ */
+gboolean
+hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_get_can_swipe_forward (HDY_GET_HELPER (self));
+}
+
+/**
+ * hdy_leaflet_get_adjacent_child
+ * @self: a #HdyLeaflet
+ * @direction: the direction
+ *
+ * Gets the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, or %NULL if it doesn't exist. This will be the same
+ * widget hdy_leaflet_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_leaflet_get_adjacent_child (HdyLeaflet *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_adjacent_child (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_leaflet_navigate
+ * @self: a #HdyLeaflet
+ * @direction: the direction
+ *
+ * Switches to the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, similar to performing a swipe gesture to go in
+ * @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_leaflet_navigate (HdyLeaflet *self,
+ HdyNavigationDirection direction)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), FALSE);
+
+ return hdy_stackable_box_navigate (HDY_GET_HELPER (self), direction);
+}
+
+/**
+ * hdy_leaflet_get_child_by_name:
+ * @self: a #HdyLeaflet
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_leaflet_get_child_by_name (HdyLeaflet *self,
+ const gchar *name)
+{
+ g_return_val_if_fail (HDY_IS_LEAFLET (self), NULL);
+
+ return hdy_stackable_box_get_child_by_name (HDY_GET_HELPER (self), name);
+}
+
+/* This private method is prefixed by the call name because it will be a virtual
+ * method in GTK 4.
+ */
+static void
+hdy_leaflet_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ hdy_stackable_box_measure (HDY_GET_HELPER (widget),
+ orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+}
+
+static void
+hdy_leaflet_get_preferred_width (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_height (GtkWidget *widget,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum_width, natural_width, NULL, NULL);
+}
+
+static void
+hdy_leaflet_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum_height,
+ gint *natural_height)
+{
+ hdy_leaflet_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum_height, natural_height, NULL, NULL);
+}
+
+static void
+hdy_leaflet_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ hdy_stackable_box_size_allocate (HDY_GET_HELPER (widget), allocation);
+}
+
+static gboolean
+hdy_leaflet_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_stackable_box_draw (HDY_GET_HELPER (widget), cr);
+}
+
+static void
+hdy_leaflet_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ hdy_stackable_box_direction_changed (HDY_GET_HELPER (widget), previous_direction);
+}
+
+static void
+hdy_leaflet_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_add (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_leaflet_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_stackable_box_remove (HDY_GET_HELPER (container), widget);
+}
+
+static void
+hdy_leaflet_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_stackable_box_forall (HDY_GET_HELPER (container), include_internals, callback, callback_data);
+}
+
+static void
+hdy_leaflet_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+
+ switch (prop_id) {
+ case PROP_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_folded (self));
+ break;
+ case PROP_HHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_leaflet_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_leaflet_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_leaflet_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_leaflet_get_transition_type (self));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_leaflet_get_mode_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_leaflet_get_child_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_leaflet_get_child_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_leaflet_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_leaflet_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_leaflet_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (HDY_GET_HELPER (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_leaflet_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS_FOLDED:
+ hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ hdy_leaflet_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ hdy_leaflet_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_leaflet_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_leaflet_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_leaflet_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ hdy_leaflet_set_mode_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ hdy_leaflet_set_child_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_leaflet_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_leaflet_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_leaflet_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (HDY_GET_HELPER (self), g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_leaflet_finalize (GObject *object)
+{
+ HdyLeaflet *self = HDY_LEAFLET (object);
+ HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self);
+
+ g_clear_object (&priv->box);
+
+ G_OBJECT_CLASS (hdy_leaflet_parent_class)->finalize (object);
+}
+
+static void
+hdy_leaflet_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_child_name (HDY_GET_HELPER (container), widget));
+ break;
+
+ case CHILD_PROP_NAVIGATABLE:
+ g_value_set_boolean (value, hdy_stackable_box_get_child_navigatable (HDY_GET_HELPER (container), widget));
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_leaflet_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ switch (property_id) {
+ case CHILD_PROP_NAME:
+ hdy_stackable_box_set_child_name (HDY_GET_HELPER (container), widget, g_value_get_string (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ case CHILD_PROP_NAVIGATABLE:
+ hdy_stackable_box_set_child_navigatable (HDY_GET_HELPER (container), widget, g_value_get_boolean (value));
+ gtk_container_child_notify_by_pspec (container, widget, pspec);
+ break;
+
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_leaflet_realize (GtkWidget *widget)
+{
+ hdy_stackable_box_realize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_unrealize (GtkWidget *widget)
+{
+ hdy_stackable_box_unrealize (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_map (GtkWidget *widget)
+{
+ hdy_stackable_box_map (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_unmap (GtkWidget *widget)
+{
+ hdy_stackable_box_unmap (HDY_GET_HELPER (widget));
+}
+
+static void
+hdy_leaflet_switch_child (HdySwipeable *swipeable,
+ guint index,
+ gint64 duration)
+{
+ hdy_stackable_box_switch_child (HDY_GET_HELPER (swipeable), index, duration);
+}
+
+static HdySwipeTracker *
+hdy_leaflet_get_swipe_tracker (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_swipe_tracker (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_leaflet_get_distance (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_distance (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble *
+hdy_leaflet_get_snap_points (HdySwipeable *swipeable,
+ gint *n_snap_points)
+{
+ return hdy_stackable_box_get_snap_points (HDY_GET_HELPER (swipeable), n_snap_points);
+}
+
+static gdouble
+hdy_leaflet_get_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_progress (HDY_GET_HELPER (swipeable));
+}
+
+static gdouble
+hdy_leaflet_get_cancel_progress (HdySwipeable *swipeable)
+{
+ return hdy_stackable_box_get_cancel_progress (HDY_GET_HELPER (swipeable));
+}
+
+static void
+hdy_leaflet_get_swipe_area (HdySwipeable *swipeable,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ hdy_stackable_box_get_swipe_area (HDY_GET_HELPER (swipeable), navigation_direction, is_drag, rect);
+}
+
+static void
+hdy_leaflet_class_init (HdyLeafletClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = (GtkWidgetClass*) klass;
+ GtkContainerClass *container_class = (GtkContainerClass*) klass;
+
+ object_class->get_property = hdy_leaflet_get_property;
+ object_class->set_property = hdy_leaflet_set_property;
+ object_class->finalize = hdy_leaflet_finalize;
+
+ widget_class->realize = hdy_leaflet_realize;
+ widget_class->unrealize = hdy_leaflet_unrealize;
+ widget_class->map = hdy_leaflet_map;
+ widget_class->unmap = hdy_leaflet_unmap;
+ widget_class->get_preferred_width = hdy_leaflet_get_preferred_width;
+ widget_class->get_preferred_height = hdy_leaflet_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_leaflet_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_leaflet_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_leaflet_size_allocate;
+ widget_class->draw = hdy_leaflet_draw;
+ widget_class->direction_changed = hdy_leaflet_direction_changed;
+
+ container_class->add = hdy_leaflet_add;
+ container_class->remove = hdy_leaflet_remove;
+ container_class->forall = hdy_leaflet_forall;
+ container_class->set_child_property = hdy_leaflet_set_child_property;
+ container_class->get_child_property = hdy_leaflet_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyLeaflet:folded:
+ *
+ * %TRUE if the leaflet is folded.
+ *
+ * The leaflet will be folded if the size allocated to it is smaller than the
+ * sum of the natural size of its children, it will be unfolded otherwise.
+ */
+ props[PROP_FOLDED] =
+ g_param_spec_boolean ("folded",
+ _("Folded"),
+ _("Whether the widget is folded"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:hhomogeneous_folded:
+ *
+ * %TRUE if the leaflet allocates the same width for all children when folded.
+ */
+ props[PROP_HHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("hhomogeneous-folded",
+ _("Horizontally homogeneous folded"),
+ _("Horizontally homogeneous sizing when the leaflet is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:vhomogeneous_folded:
+ *
+ * %TRUE if the leaflet allocates the same height for all children when folded.
+ */
+ props[PROP_VHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("vhomogeneous-folded",
+ _("Vertically homogeneous folded"),
+ _("Vertically homogeneous sizing when the leaflet is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:hhomogeneous_unfolded:
+ *
+ * %TRUE if the leaflet allocates the same width for all children when unfolded.
+ */
+ props[PROP_HHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("hhomogeneous-unfolded",
+ _("Box horizontally homogeneous"),
+ _("Horizontally homogeneous sizing when the leaflet is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:vhomogeneous_unfolded:
+ *
+ * %TRUE if the leaflet allocates the same height for all children when unfolded.
+ */
+ props[PROP_VHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("vhomogeneous-unfolded",
+ _("Box vertically homogeneous"),
+ _("Vertically homogeneous sizing when the leaflet is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible when the leaflet is folded"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible when the children are stacked"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:transition-type:
+ *
+ * The type of animation that will be used for transitions between modes and
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about
+ * to become current.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between modes and children"),
+ HDY_TYPE_LEAFLET_TRANSITION_TYPE, HDY_LEAFLET_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_MODE_TRANSITION_DURATION] =
+ g_param_spec_uint ("mode-transition-duration",
+ _("Mode transition duration"),
+ _("The mode transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 250,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_DURATION] =
+ g_param_spec_uint ("child-transition-duration",
+ _("Child transition duration"),
+ _("The child transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("child-transition-running",
+ _("Child transition running"),
+ _("Whether or not the child transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:can-swipe-back:
+ *
+ * Whether or not the leaflet allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyLeaflet:can-swipe-forward:
+ *
+ * Whether or not the leaflet allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 0.0.12
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_NAME] =
+ g_param_spec_string ("name",
+ _("Name"),
+ _("The name of the child page"),
+ NULL,
+ G_PARAM_READWRITE);
+
+ /**
+ * HdyLeaflet:navigatable:
+ *
+ * Whether the child can be navigated to when folded.
+ * If %FALSE, the child will be ignored by hdy_leaflet_get_adjacent_child(),
+ * hdy_leaflet_navigate(), and swipe gestures.
+ *
+ * This can be used used to prevent switching to widgets like separators.
+ *
+ * Since: 1.0
+ */
+ child_props[CHILD_PROP_NAVIGATABLE] =
+ g_param_spec_boolean ("navigatable",
+ _("Navigatable"),
+ _("Whether the child can be navigated to"),
+ TRUE,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_PANEL);
+ gtk_widget_class_set_css_name (widget_class, "leaflet");
+}
+
+GtkWidget *
+hdy_leaflet_new (void)
+{
+ return g_object_new (HDY_TYPE_LEAFLET, NULL);
+}
+
+#define NOTIFY(func, prop) \
+static void \
+func (HdyLeaflet *self) { \
+ g_object_notify_by_pspec (G_OBJECT (self), props[prop]); \
+}
+
+NOTIFY (notify_folded_cb, PROP_FOLDED);
+NOTIFY (notify_hhomogeneous_folded_cb, PROP_HHOMOGENEOUS_FOLDED);
+NOTIFY (notify_vhomogeneous_folded_cb, PROP_VHOMOGENEOUS_FOLDED);
+NOTIFY (notify_hhomogeneous_unfolded_cb, PROP_HHOMOGENEOUS_UNFOLDED);
+NOTIFY (notify_vhomogeneous_unfolded_cb, PROP_VHOMOGENEOUS_UNFOLDED);
+NOTIFY (notify_visible_child_cb, PROP_VISIBLE_CHILD);
+NOTIFY (notify_visible_child_name_cb, PROP_VISIBLE_CHILD_NAME);
+NOTIFY (notify_transition_type_cb, PROP_TRANSITION_TYPE);
+NOTIFY (notify_mode_transition_duration_cb, PROP_MODE_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_duration_cb, PROP_CHILD_TRANSITION_DURATION);
+NOTIFY (notify_child_transition_running_cb, PROP_CHILD_TRANSITION_RUNNING);
+NOTIFY (notify_interpolate_size_cb, PROP_INTERPOLATE_SIZE);
+NOTIFY (notify_can_swipe_back_cb, PROP_CAN_SWIPE_BACK);
+NOTIFY (notify_can_swipe_forward_cb, PROP_CAN_SWIPE_FORWARD);
+
+static void
+notify_orientation_cb (HdyLeaflet *self)
+{
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static void
+hdy_leaflet_init (HdyLeaflet *self)
+{
+ HdyLeafletPrivate *priv = hdy_leaflet_get_instance_private (self);
+
+ priv->box = hdy_stackable_box_new (GTK_CONTAINER (self),
+ GTK_CONTAINER_CLASS (hdy_leaflet_parent_class),
+ TRUE);
+
+ g_signal_connect_object (priv->box, "notify::folded", G_CALLBACK (notify_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-folded", G_CALLBACK (notify_hhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-folded", G_CALLBACK (notify_vhomogeneous_folded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::hhomogeneous-unfolded", G_CALLBACK (notify_hhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::vhomogeneous-unfolded", G_CALLBACK (notify_vhomogeneous_unfolded_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child", G_CALLBACK (notify_visible_child_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::visible-child-name", G_CALLBACK (notify_visible_child_name_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::transition-type", G_CALLBACK (notify_transition_type_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::mode-transition-duration", G_CALLBACK (notify_mode_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-duration", G_CALLBACK (notify_child_transition_duration_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::child-transition-running", G_CALLBACK (notify_child_transition_running_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::interpolate-size", G_CALLBACK (notify_interpolate_size_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-back", G_CALLBACK (notify_can_swipe_back_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::can-swipe-forward", G_CALLBACK (notify_can_swipe_forward_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->box, "notify::orientation", G_CALLBACK (notify_orientation_cb), self, G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_leaflet_swipeable_init (HdySwipeableInterface *iface)
+{
+ iface->switch_child = hdy_leaflet_switch_child;
+ iface->get_swipe_tracker = hdy_leaflet_get_swipe_tracker;
+ iface->get_distance = hdy_leaflet_get_distance;
+ iface->get_snap_points = hdy_leaflet_get_snap_points;
+ iface->get_progress = hdy_leaflet_get_progress;
+ iface->get_cancel_progress = hdy_leaflet_get_cancel_progress;
+ iface->get_swipe_area = hdy_leaflet_get_swipe_area;
+}
diff --git a/subprojects/libhandy/src/hdy-leaflet.h b/subprojects/libhandy/src/hdy-leaflet.h
new file mode 100644
index 0000000..4d9324d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-leaflet.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+#include "hdy-navigation-direction.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_LEAFLET (hdy_leaflet_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyLeaflet, hdy_leaflet, HDY, LEAFLET, GtkContainer)
+
+typedef enum {
+ HDY_LEAFLET_TRANSITION_TYPE_OVER,
+ HDY_LEAFLET_TRANSITION_TYPE_UNDER,
+ HDY_LEAFLET_TRANSITION_TYPE_SLIDE,
+} HdyLeafletTransitionType;
+
+/**
+ * HdyLeafletClass
+ * @parent_class: The parent class
+ */
+struct _HdyLeafletClass
+{
+ GtkContainerClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_new (void);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_folded (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_visible_child (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_visible_child (HdyLeaflet *self,
+ GtkWidget *visible_child);
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_leaflet_get_visible_child_name (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_visible_child_name (HdyLeaflet *self,
+ const gchar *name);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_homogeneous (HdyLeaflet *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HDY_AVAILABLE_IN_ALL
+HdyLeafletTransitionType hdy_leaflet_get_transition_type (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_transition_type (HdyLeaflet *self,
+ HdyLeafletTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_leaflet_get_mode_transition_duration (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_mode_transition_duration (HdyLeaflet *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_leaflet_get_child_transition_duration (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_child_transition_duration (HdyLeaflet *self,
+ guint duration);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_child_transition_running (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_interpolate_size (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_interpolate_size (HdyLeaflet *self,
+ gboolean interpolate_size);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_can_swipe_back (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_can_swipe_back (HdyLeaflet *self,
+ gboolean can_swipe_back);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_get_can_swipe_forward (HdyLeaflet *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_leaflet_set_can_swipe_forward (HdyLeaflet *self,
+ gboolean can_swipe_forward);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_adjacent_child (HdyLeaflet *self,
+ HdyNavigationDirection direction);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_leaflet_navigate (HdyLeaflet *self,
+ HdyNavigationDirection direction);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_leaflet_get_child_by_name (HdyLeaflet *self,
+ const gchar *name);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-main-private.h b/subprojects/libhandy/src/hdy-main-private.h
new file mode 100644
index 0000000..3ad6ad1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main-private.h
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-main.h"
+
+G_BEGIN_DECLS
+
+/* Initializes the public GObject types, which is needed to ensure they are
+ * discoverable, for example so they can easily be used with GtkBuilder.
+ *
+ * The function is implemented in hdy-public-types.c which is generated at
+ * compile time by gen-public-types.sh
+ */
+void hdy_init_public_types (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-main.c b/subprojects/libhandy/src/hdy-main.c
new file mode 100644
index 0000000..6c5df7b
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main.c
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2018-2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#include "config.h"
+#include "hdy-main-private.h"
+#include <gio/gio.h>
+#include <glib/gi18n-lib.h>
+#include <gtk/gtk.h>
+
+static gint hdy_initialized = FALSE;
+
+/**
+ * SECTION:hdy-main
+ * @short_description: Library initialization.
+ * @Title: hdy-main
+ *
+ * Before using the Handy library you should initialize it by calling the
+ * hdy_init() function.
+ * This makes sure translations, types, themes, and icons for the Handy library
+ * are set up properly.
+ */
+
+/* The style provider priority to use for libhandy widgets custom styling. It is
+ * higher than themes and settings, allowing to override theme defaults, but
+ * lower than applications and user provided styles, so application developers
+ * can nonetheless apply custom styling on top of it. */
+#define HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE (GTK_STYLE_PROVIDER_PRIORITY_SETTINGS + 1)
+
+#define HDY_THEMES_PATH "/sm/puri/handy/themes/"
+
+static inline gboolean
+hdy_resource_exists (const gchar *resource_path)
+{
+ return g_resources_get_info (resource_path, G_RESOURCE_LOOKUP_FLAGS_NONE, NULL, NULL, NULL);
+}
+
+static gchar *
+hdy_themes_get_theme_name (gboolean *prefer_dark_theme)
+{
+ gchar *theme_name = NULL;
+ gchar *p;
+
+ g_assert (prefer_dark_theme);
+
+ theme_name = g_strdup (g_getenv ("GTK_THEME"));
+
+ if (theme_name == NULL) {
+ g_object_get (gtk_settings_get_default (),
+ "gtk-theme-name", &theme_name,
+ "gtk-application-prefer-dark-theme", prefer_dark_theme,
+ NULL);
+
+ return theme_name;
+ }
+
+ /* Theme variants are specified with the syntax
+ * "<theme>:<variant>" e.g. "Adwaita:dark" */
+ if (NULL != (p = strrchr (theme_name, ':'))) {
+ *p = '\0';
+ p++;
+ *prefer_dark_theme = g_strcmp0 (p, "dark") == 0;
+ }
+
+ return theme_name;
+}
+
+static void
+hdy_themes_update (GtkCssProvider *css_provider)
+{
+ g_autofree gchar *theme_name = NULL;
+ g_autofree gchar *resource_path = NULL;
+ gboolean prefer_dark_theme = FALSE;
+
+ g_assert (GTK_IS_CSS_PROVIDER (css_provider));
+
+ theme_name = hdy_themes_get_theme_name (&prefer_dark_theme);
+
+ /* First check with full path to theme+variant */
+ resource_path = g_strdup_printf (HDY_THEMES_PATH"%s%s.css",
+ theme_name, prefer_dark_theme ? "-dark" : "");
+
+ if (!hdy_resource_exists (resource_path)) {
+ /* Now try without the theme variant */
+ g_free (resource_path);
+ resource_path = g_strdup_printf (HDY_THEMES_PATH"%s.css", theme_name);
+
+ if (!hdy_resource_exists (resource_path)) {
+ /* Now fallback to shared styling */
+ g_free (resource_path);
+ resource_path = g_strdup (HDY_THEMES_PATH"shared.css");
+
+ g_assert (hdy_resource_exists (resource_path));
+ }
+ }
+
+ gtk_css_provider_load_from_resource (css_provider, resource_path);
+}
+
+static void
+load_fallback_style (void)
+{
+ g_autoptr (GtkCssProvider) css_provider = NULL;
+
+ css_provider = gtk_css_provider_new ();
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (css_provider),
+ GTK_STYLE_PROVIDER_PRIORITY_FALLBACK);
+
+ gtk_css_provider_load_from_resource (css_provider, HDY_THEMES_PATH"fallback.css");
+}
+
+/**
+ * hdy_style_init:
+ *
+ * Initializes the style classes. This must be called once GTK has been
+ * initialized.
+ *
+ * Since: 1.0
+ */
+static void
+hdy_style_init (void)
+{
+ static volatile gsize guard = 0;
+ g_autoptr (GtkCssProvider) css_provider = NULL;
+ GtkSettings *settings;
+
+ if (!g_once_init_enter (&guard))
+ return;
+
+ css_provider = gtk_css_provider_new ();
+ gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
+ GTK_STYLE_PROVIDER (css_provider),
+ HDY_STYLE_PROVIDER_PRIORITY_OVERRIDE);
+
+ settings = gtk_settings_get_default ();
+ g_signal_connect_swapped (settings,
+ "notify::gtk-theme-name",
+ G_CALLBACK (hdy_themes_update),
+ css_provider);
+ g_signal_connect_swapped (settings,
+ "notify::gtk-application-prefer-dark-theme",
+ G_CALLBACK (hdy_themes_update),
+ css_provider);
+
+ hdy_themes_update (css_provider);
+
+ load_fallback_style ();
+
+ g_once_init_leave (&guard, 1);
+}
+
+/**
+ * hdy_icons_init:
+ *
+ * Initializes the embedded icons. This must be called once GTK has been
+ * initialized.
+ *
+ * Since: 1.0
+ */
+static void
+hdy_icons_init (void)
+{
+ static volatile gsize guard = 0;
+
+ if (!g_once_init_enter (&guard))
+ return;
+
+ gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (),
+ "/sm/puri/handy/icons");
+
+ g_once_init_leave (&guard, 1);
+}
+
+/**
+ * hdy_init:
+ *
+ * Call this function just after initializing GTK, if you are using
+ * #GtkApplication it means it must be called when the #GApplication::startup
+ * signal is emitted. If libhandy has already been initialized, the function
+ * will simply return.
+ *
+ * This makes sure translations, types, themes, and icons for the Handy library
+ * are set up properly.
+ */
+void
+hdy_init (void)
+{
+ if (hdy_initialized)
+ return;
+
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ hdy_init_public_types ();
+
+ hdy_style_init ();
+ hdy_icons_init ();
+
+ hdy_initialized = TRUE;
+}
diff --git a/subprojects/libhandy/src/hdy-main.h b/subprojects/libhandy/src/hdy-main.h
new file mode 100644
index 0000000..f960a69
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-main.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+HDY_AVAILABLE_IN_ALL
+void hdy_init (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-navigation-direction.c b/subprojects/libhandy/src/hdy-navigation-direction.c
new file mode 100644
index 0000000..b4a2d23
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-navigation-direction.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-navigation-direction.h"
+
+/**
+ * SECTION:hdy-navigation-direction
+ * @short_description: Swipe navigation directions.
+ * @title: HdyNavigationDirection
+ * @See_also: #HdyDeck, #HdyLeaflet
+ */
+
+/**
+ * HdyNavigationDirection:
+ * @HDY_NAVIGATION_DIRECTION_BACK: Corresponds to start or top, depending on orientation and text direction
+ * @HDY_NAVIGATION_DIRECTION_FORWARD: Corresponds to end or bottom, depending on orientation and text direction
+ *
+ * Represents direction of a swipe navigation gesture in #HdyDeck and
+ * #HdyLeaflet.
+ *
+ * Since: 1.0
+ */
diff --git a/subprojects/libhandy/src/hdy-navigation-direction.h b/subprojects/libhandy/src/hdy-navigation-direction.h
new file mode 100644
index 0000000..ea63ef5
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-navigation-direction.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <glib-object.h>
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ HDY_NAVIGATION_DIRECTION_BACK,
+ HDY_NAVIGATION_DIRECTION_FORWARD,
+} HdyNavigationDirection;
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-nothing-private.h b/subprojects/libhandy/src/hdy-nothing-private.h
new file mode 100644
index 0000000..19d35c9
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-nothing-private.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_NOTHING (hdy_nothing_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyNothing, hdy_nothing, HDY, NOTHING, GtkWidget)
+
+GtkWidget *hdy_nothing_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-nothing.c b/subprojects/libhandy/src/hdy-nothing.c
new file mode 100644
index 0000000..036537a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-nothing.c
@@ -0,0 +1,47 @@
+#include "hdy-nothing-private.h"
+
+/**
+ * PRIVATE:hdy-nothing
+ * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow
+ * @title: HdyNothing
+ * @See_also: #HdyApplicationWindow, #HdyWindow, #HdyWindowMixin
+ * @stability: Private
+ *
+ * The HdyNothing widget does nothing. It's used as the titlebar for
+ * #HdyWindow and #HdyApplicationWindow.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyNothing
+{
+ GtkWidget parent_instance;
+};
+
+G_DEFINE_TYPE (HdyNothing, hdy_nothing, GTK_TYPE_WIDGET)
+
+static void
+hdy_nothing_class_init (HdyNothingClass *klass)
+{
+}
+
+static void
+hdy_nothing_init (HdyNothing *self)
+{
+}
+
+/**
+ * hdy_nothing_new:
+ *
+ * Creates a new #HdyNothing.
+ *
+ * Returns: (transfer full): a newly created #HdyNothing
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_nothing_new (void)
+{
+ return g_object_new (HDY_TYPE_NOTHING, NULL);
+}
+
diff --git a/subprojects/libhandy/src/hdy-preferences-group-private.h b/subprojects/libhandy/src/hdy-preferences-group-private.h
new file mode 100644
index 0000000..731e2fa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group-private.h
@@ -0,0 +1,16 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include "hdy-preferences-group.h"
+
+G_BEGIN_DECLS
+
+void hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self,
+ GListStore *model);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-group.c b/subprojects/libhandy/src/hdy-preferences-group.c
new file mode 100644
index 0000000..8000627
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.c
@@ -0,0 +1,449 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-group-private.h"
+
+#include "hdy-preferences-row.h"
+
+/**
+ * SECTION:hdy-preferences-group
+ * @short_description: A group gathering preferences rows.
+ * @Title: HdyPreferencesGroup
+ *
+ * A #HdyPreferencesGroup represents a group or tightly related preferences,
+ * which in turn are represented by HdyPreferencesRow.
+ *
+ * To summarize the role of the preferences it gathers, a group can have both a
+ * title and a description. The title will be used by #HdyPreferencesWindow to
+ * let the user look for a preference.
+ *
+ * # CSS nodes
+ *
+ * #HdyPreferencesGroup has a single CSS node with name preferencesgroup.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkLabel *description;
+ GtkListBox *listbox;
+ GtkBox *listbox_box;
+ GtkLabel *title;
+} HdyPreferencesGroupPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesGroup, hdy_preferences_group, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_DESCRIPTION,
+ PROP_TITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_title_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ /* Show the listbox only if it has children to avoid having showing the
+ * listbox as an empty frame, parasiting the look of non-GtkListBoxRow
+ * children.
+ */
+ gtk_widget_set_visible (GTK_WIDGET (priv->title),
+ gtk_label_get_text (priv->title) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->title), "") != 0);
+}
+
+static void
+update_description_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->description),
+ gtk_label_get_text (priv->description) != NULL &&
+ g_strcmp0 (gtk_label_get_text (priv->description), "") != 0);
+}
+
+static void
+update_listbox_visibility (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+ g_autoptr(GList) children = NULL;
+
+ /* We must wait until listob has been built and added. */
+ if (priv->listbox == NULL)
+ return;
+
+ children = gtk_container_get_children (GTK_CONTAINER (priv->listbox));
+
+ gtk_widget_set_visible (GTK_WIDGET (priv->listbox), children != NULL);
+}
+
+typedef struct {
+ HdyPreferencesGroup *group;
+ GtkCallback callback;
+ gpointer callback_data;
+} ForallData;
+
+static void
+for_non_internal_child (GtkWidget *widget,
+ gpointer callback_data)
+{
+ ForallData *data = callback_data;
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (data->group);
+
+ if (widget != (GtkWidget *) priv->listbox)
+ data->callback (widget, data->callback_data);
+}
+
+static void
+hdy_preferences_group_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+ ForallData data;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->forall (GTK_CONTAINER (self), include_internals, callback, callback_data);
+
+ return;
+ }
+
+ data.group = self;
+ data.callback = callback;
+ data.callback_data = callback_data;
+
+ if (priv->listbox)
+ GTK_CONTAINER_GET_CLASS (priv->listbox)->forall (GTK_CONTAINER (priv->listbox), include_internals, callback, callback_data);
+ if (priv->listbox_box)
+ GTK_CONTAINER_GET_CLASS (priv->listbox_box)->forall (GTK_CONTAINER (priv->listbox_box), include_internals, for_non_internal_child, &data);
+}
+
+static void
+hdy_preferences_group_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, hdy_preferences_group_get_description (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_group_get_title (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_group_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+
+ switch (prop_id) {
+ case PROP_DESCRIPTION:
+ hdy_preferences_group_set_description (self, g_value_get_string (value));
+ break;
+ case PROP_TITLE:
+ hdy_preferences_group_set_title (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_group_dispose (GObject *object)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (object);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ /*
+ * Since we overload forall(), the inherited destroy() won't work as normal.
+ * Remove internal widgets ourself.
+ */
+ g_clear_pointer ((GtkWidget **) &priv->description, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->listbox, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->listbox_box, gtk_widget_destroy);
+ g_clear_pointer ((GtkWidget **) &priv->title, gtk_widget_destroy);
+
+ G_OBJECT_CLASS (hdy_preferences_group_parent_class)->dispose (object);
+}
+
+static void
+hdy_preferences_group_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ if (priv->title == NULL || priv->description == NULL || priv->listbox_box == NULL) {
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->add (container, child);
+
+ return;
+ }
+
+ if (HDY_IS_PREFERENCES_ROW (child))
+ gtk_container_add (GTK_CONTAINER (priv->listbox), child);
+ else
+ gtk_container_add (GTK_CONTAINER (priv->listbox_box), child);
+}
+
+static void
+hdy_preferences_group_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesGroup *self = HDY_PREFERENCES_GROUP (container);
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->box))
+ GTK_CONTAINER_CLASS (hdy_preferences_group_parent_class)->remove (container, child);
+ else if (HDY_IS_PREFERENCES_ROW (child))
+ gtk_container_remove (GTK_CONTAINER (priv->listbox), child);
+ else if (child != GTK_WIDGET (priv->listbox))
+ gtk_container_remove (GTK_CONTAINER (priv->listbox_box), child);
+}
+
+static void
+hdy_preferences_group_class_init (HdyPreferencesGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_group_get_property;
+ object_class->set_property = hdy_preferences_group_set_property;
+ object_class->dispose = hdy_preferences_group_dispose;
+
+ container_class->add = hdy_preferences_group_add;
+ container_class->remove = hdy_preferences_group_remove;
+ container_class->forall = hdy_preferences_group_forall;
+
+ /**
+ * HdyPreferencesGroup:description:
+ *
+ * The description for this group of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_DESCRIPTION] =
+ g_param_spec_string ("description",
+ _("Description"),
+ _("Description"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyPreferencesGroup:title:
+ *
+ * The title for this group of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("Title"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "preferencesgroup");
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-group.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, description);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, listbox_box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesGroup, title);
+ gtk_widget_class_bind_template_callback (widget_class, update_listbox_visibility);
+}
+
+static void
+hdy_preferences_group_init (HdyPreferencesGroup *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ update_description_visibility (self);
+ update_title_visibility (self);
+ update_listbox_visibility (self);
+}
+
+/**
+ * hdy_preferences_group_new:
+ *
+ * Creates a new #HdyPreferencesGroup.
+ *
+ * Returns: a new #HdyPreferencesGroup
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_group_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_GROUP, NULL);
+}
+
+/**
+ * hdy_preferences_group_get_title:
+ * @self: a #HdyPreferencesGroup
+ *
+ * Gets the title of @self.
+ *
+ * Returns: the title of @self.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_group_get_title (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL);
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ return gtk_label_get_text (priv->title);
+}
+
+/**
+ * hdy_preferences_group_set_title:
+ * @self: a #HdyPreferencesGroup
+ * @title: the title
+ *
+ * Sets the title for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_set_title (HdyPreferencesGroup *self,
+ const gchar *title)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_label (priv->title), title) == 0)
+ return;
+
+ gtk_label_set_label (priv->title, title);
+ update_title_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_preferences_group_get_description:
+ * @self: a #HdyPreferencesGroup
+ *
+ *
+ * Returns: the description of @self.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_group_get_description (HdyPreferencesGroup *self)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_GROUP (self), NULL);
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ return gtk_label_get_text (priv->description);
+}
+
+/**
+ * hdy_preferences_group_set_description:
+ * @self: a #HdyPreferencesGroup
+ * @description: the description
+ *
+ * Sets the description for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_set_description (HdyPreferencesGroup *self,
+ const gchar *description)
+{
+ HdyPreferencesGroupPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+
+ priv = hdy_preferences_group_get_instance_private (self);
+
+ if (g_strcmp0 (gtk_label_get_label (priv->description), description) == 0)
+ return;
+
+ gtk_label_set_label (priv->description, description);
+ update_description_visibility (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DESCRIPTION]);
+}
+
+static void
+add_preferences_to_model (HdyPreferencesRow *row,
+ GListStore *model)
+{
+ const gchar *title;
+
+ g_assert (HDY_IS_PREFERENCES_ROW (row));
+ g_assert (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (row)))
+ return;
+
+ title = hdy_preferences_row_get_title (row);
+
+ if (!title || !*title)
+ return;
+
+ g_list_store_append (model, row);
+}
+
+/**
+ * hdy_preferences_group_add_preferences_to_model: (skip)
+ * @self: a #HdyPreferencesGroup
+ * @model: the model
+ *
+ * Add preferences from @self to the model.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_group_add_preferences_to_model (HdyPreferencesGroup *self,
+ GListStore *model)
+{
+ HdyPreferencesGroupPrivate *priv = hdy_preferences_group_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_PREFERENCES_GROUP (self));
+ g_return_if_fail (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+ return;
+
+ gtk_container_foreach (GTK_CONTAINER (priv->listbox), (GtkCallback) add_preferences_to_model, model);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-group.h b/subprojects/libhandy/src/hdy-preferences-group.h
new file mode 100644
index 0000000..2a7952c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_GROUP (hdy_preferences_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesGroup, hdy_preferences_group, HDY, PREFERENCES_GROUP, GtkBin)
+
+/**
+ * HdyPreferencesGroupClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesGroupClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_group_get_title (HdyPreferencesGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_group_set_title (HdyPreferencesGroup *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_group_get_description (HdyPreferencesGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_group_set_description (HdyPreferencesGroup *self,
+ const gchar *description);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-group.ui b/subprojects/libhandy/src/hdy-preferences-group.ui
new file mode 100644
index 0000000..60a5ae1
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-group.ui
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesGroup" parent="GtkBin">
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">12</property>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="can_focus">False</property>
+ <property name="ellipsize">end</property>
+ <property name="halign">start</property>
+ <property name="xalign">0</property>
+ <style>
+ <!-- Requires Adwaita from GTK 3.24.14. -->
+ <class name="heading"/>
+ <!-- Matching elementary class. -->
+ <class name="h4"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="description">
+ <property name="can_focus">False</property>
+ <property name="halign">start</property>
+ <property name="wrap">True</property>
+ <property name="xalign">0</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="listbox_box">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="listbox">
+ <property name="selection_mode">none</property>
+ <property name="visible">True</property>
+ <signal name="add" handler="update_listbox_visibility" after="yes" swapped="yes"/>
+ <signal name="remove" handler="update_listbox_visibility" after="yes" swapped="yes"/>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-preferences-page-private.h b/subprojects/libhandy/src/hdy-preferences-page-private.h
new file mode 100644
index 0000000..a93ccfd
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page-private.h
@@ -0,0 +1,18 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include "hdy-preferences-page.h"
+
+G_BEGIN_DECLS
+
+GtkAdjustment *hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self);
+
+void hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self,
+ GListStore *model);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-page.c b/subprojects/libhandy/src/hdy-preferences-page.c
new file mode 100644
index 0000000..51fdc0d
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.c
@@ -0,0 +1,365 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-page-private.h"
+
+#include "hdy-preferences-group-private.h"
+
+/**
+ * SECTION:hdy-preferences-page
+ * @short_description: A page from the preferences window.
+ * @Title: HdyPreferencesPage
+ *
+ * The #HdyPreferencesPage widget gathers preferences groups into a single page
+ * of a preferences window.
+ *
+ * # CSS nodes
+ *
+ * #HdyPreferencesPage has a single CSS node with name preferencespage.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ GtkBox *box;
+ GtkScrolledWindow *scrolled_window;
+
+ gchar *icon_name;
+ gchar *title;
+} HdyPreferencesPagePrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesPage, hdy_preferences_page, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_ICON_NAME,
+ PROP_TITLE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+typedef struct {
+ HdyPreferencesPage *preferences_page;
+ GtkCallback callback;
+ gpointer data;
+} CallbackData;
+
+static void
+hdy_preferences_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_preferences_page_get_icon_name (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_page_get_title (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_preferences_page_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_TITLE:
+ hdy_preferences_page_set_title (self, g_value_get_string (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_page_finalize (GObject *object)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (object);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ g_clear_pointer (&priv->icon_name, g_free);
+ g_clear_pointer (&priv->title, g_free);
+
+ G_OBJECT_CLASS (hdy_preferences_page_parent_class)->finalize (object);
+}
+
+static void
+hdy_preferences_page_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (priv->scrolled_window == NULL)
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->add (container, child);
+ else if (HDY_IS_PREFERENCES_GROUP (child))
+ gtk_container_add (GTK_CONTAINER (priv->box), child);
+ else
+ g_warning ("Can't add children of type %s to %s",
+ G_OBJECT_TYPE_NAME (child),
+ G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+hdy_preferences_page_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->scrolled_window))
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->remove (container, child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->box), child);
+}
+
+static void
+hdy_preferences_page_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesPage *self = HDY_PREFERENCES_PAGE (container);
+ HdyPreferencesPagePrivate *priv = hdy_preferences_page_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_preferences_page_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else if (priv->box)
+ gtk_container_foreach (GTK_CONTAINER (priv->box), callback, callback_data);
+}
+
+static void
+hdy_preferences_page_class_init (HdyPreferencesPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_page_get_property;
+ object_class->set_property = hdy_preferences_page_set_property;
+ object_class->finalize = hdy_preferences_page_finalize;
+
+ container_class->add = hdy_preferences_page_add;
+ container_class->remove = hdy_preferences_page_remove;
+ container_class->forall = hdy_preferences_page_forall;
+
+ /**
+ * HdyPreferencesPage:icon-name:
+ *
+ * The icon name for this page of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon name"),
+ _("Icon name"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesPage:title:
+ *
+ * The title for this page of preferences.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("Title"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-page.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesPage, scrolled_window);
+
+ gtk_widget_class_set_css_name (widget_class, "preferencespage");
+}
+
+static void
+hdy_preferences_page_init (HdyPreferencesPage *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_preferences_page_new:
+ *
+ * Creates a new #HdyPreferencesPage.
+ *
+ * Returns: a new #HdyPreferencesPage
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_page_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_PAGE, NULL);
+}
+
+/**
+ * hdy_preferences_page_get_icon_name:
+ * @self: a #HdyPreferencesPage
+ *
+ * Gets the icon name for @self, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): the icon name for @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_page_get_icon_name (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return priv->icon_name;
+}
+
+/**
+ * hdy_preferences_page_set_icon_name:
+ * @self: a #HdyPreferencesPage
+ * @icon_name: (nullable): the icon name, or %NULL
+ *
+ * Sets the icon name for @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_set_icon_name (HdyPreferencesPage *self,
+ const gchar *icon_name)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ if (g_strcmp0 (priv->icon_name, icon_name) == 0)
+ return;
+
+ g_clear_pointer (&priv->icon_name, g_free);
+ priv->icon_name = g_strdup (icon_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_preferences_page_get_title:
+ * @self: a #HdyPreferencesPage
+ *
+ * Gets the title of @self, or %NULL.
+ *
+ * Returns: (transfer none) (nullable): the title of the @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_page_get_title (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return priv->title;
+}
+
+/**
+ * hdy_preferences_page_set_title:
+ * @self: a #HdyPreferencesPage
+ * @title: (nullable): the title of the page, or %NULL
+ *
+ * Sets the title of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_set_title (HdyPreferencesPage *self,
+ const gchar *title)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ if (g_strcmp0 (priv->title, title) == 0)
+ return;
+
+ g_clear_pointer (&priv->title, g_free);
+ priv->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+GtkAdjustment *
+hdy_preferences_page_get_vadjustment (HdyPreferencesPage *self)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_PAGE (self), NULL);
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ return gtk_scrolled_window_get_vadjustment (priv->scrolled_window);
+}
+
+/**
+ * hdy_preferences_page_add_preferences_to_model: (skip)
+ * @self: a #HdyPreferencesPage
+ * @model: the model
+ *
+ * Add preferences from @self to the model.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_page_add_preferences_to_model (HdyPreferencesPage *self,
+ GListStore *model)
+{
+ HdyPreferencesPagePrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_PAGE (self));
+ g_return_if_fail (G_IS_LIST_STORE (model));
+
+ if (!gtk_widget_get_visible (GTK_WIDGET (self)))
+ return;
+
+ priv = hdy_preferences_page_get_instance_private (self);
+
+ gtk_container_foreach (GTK_CONTAINER (priv->box), (GtkCallback) hdy_preferences_group_add_preferences_to_model, model);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-page.h b/subprojects/libhandy/src/hdy-preferences-page.h
new file mode 100644
index 0000000..158c18c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_PAGE (hdy_preferences_page_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesPage, hdy_preferences_page, HDY, PREFERENCES_PAGE, GtkBin)
+
+/**
+ * HdyPreferencesPageClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesPageClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_page_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_page_get_icon_name (HdyPreferencesPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_page_set_icon_name (HdyPreferencesPage *self,
+ const gchar *icon_name);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_page_get_title (HdyPreferencesPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_page_set_title (HdyPreferencesPage *self,
+ const gchar *title);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-page.ui b/subprojects/libhandy/src/hdy-preferences-page.ui
new file mode 100644
index 0000000..809dee7
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-page.ui
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesPage" parent="GtkBin">
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="visible">True</property>
+ <property name="hscrollbar-policy">never</property>
+ <child>
+ <object class="GtkViewport">
+ <property name="shadow-type">none</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="margin-bottom">18</property>
+ <property name="margin-end">12</property>
+ <property name="margin-start">12</property>
+ <property name="margin-top">18</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="box">
+ <property name="orientation">vertical</property>
+ <property name="spacing">18</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-preferences-row.c b/subprojects/libhandy/src/hdy-preferences-row.c
new file mode 100644
index 0000000..9327509
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-row.c
@@ -0,0 +1,259 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-row.h"
+
+/**
+ * SECTION:hdy-preferences-row
+ * @short_description: A #GtkListBox row used to present preferences.
+ * @Title: HdyPreferencesRow
+ *
+ * The #HdyPreferencesRow widget has a title that #HdyPreferencesWindow will use
+ * to let the user look for a preference. It doesn't present the title in any
+ * way and it lets you present the preference as you please.
+ *
+ * #HdyActionRow and its derivatives are convenient to use as preference rows as
+ * they take care of presenting the preference's title while letting you compose
+ * the inputs of the preference around it.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ gchar *title;
+
+ gboolean use_underline;
+} HdyPreferencesRowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesRow, hdy_preferences_row, GTK_TYPE_LIST_BOX_ROW)
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_USE_UNDERLINE,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+hdy_preferences_row_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_preferences_row_get_title (self));
+ break;
+ case PROP_USE_UNDERLINE:
+ g_value_set_boolean (value, hdy_preferences_row_get_use_underline (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_row_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ hdy_preferences_row_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_USE_UNDERLINE:
+ hdy_preferences_row_set_use_underline (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_row_finalize (GObject *object)
+{
+ HdyPreferencesRow *self = HDY_PREFERENCES_ROW (object);
+ HdyPreferencesRowPrivate *priv = hdy_preferences_row_get_instance_private (self);
+
+ g_free (priv->title);
+
+ G_OBJECT_CLASS (hdy_preferences_row_parent_class)->finalize (object);
+}
+
+static void
+hdy_preferences_row_class_init (HdyPreferencesRowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_row_get_property;
+ object_class->set_property = hdy_preferences_row_set_property;
+ object_class->finalize = hdy_preferences_row_finalize;
+
+ /**
+ * HdyPreferencesRow:title:
+ *
+ * The title of the preference represented by this row.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title of the preference"),
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesRow:use-underline:
+ *
+ * Whether an embedded underline in the text of the title indicates a
+ * mnemonic.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_USE_UNDERLINE] =
+ g_param_spec_boolean ("use-underline",
+ _("Use underline"),
+ _("If set, an underline in the text indicates the next character should be used for the mnemonic accelerator key"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+hdy_preferences_row_init (HdyPreferencesRow *self)
+{
+}
+
+/**
+ * hdy_preferences_row_new:
+ *
+ * Creates a new #HdyPreferencesRow.
+ *
+ * Returns: a new #HdyPreferencesRow
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_row_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_ROW, NULL);
+}
+
+/**
+ * hdy_preferences_row_get_title:
+ * @self: a #HdyPreferencesRow
+ *
+ * Gets the title of the preference represented by @self.
+ *
+ * Returns: (transfer none) (nullable): the title of the preference represented
+ * by @self, or %NULL.
+ *
+ * Since: 0.0.10
+ */
+const gchar *
+hdy_preferences_row_get_title (HdyPreferencesRow *self)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), NULL);
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ return priv->title;
+}
+
+/**
+ * hdy_preferences_row_set_title:
+ * @self: a #HdyPreferencesRow
+ * @title: (nullable): the title, or %NULL.
+ *
+ * Sets the title of the preference represented by @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_row_set_title (HdyPreferencesRow *self,
+ const gchar *title)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_ROW (self));
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ if (g_strcmp0 (priv->title, title) == 0)
+ return;
+
+ g_free (priv->title);
+ priv->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_preferences_row_get_use_underline:
+ * @self: a #HdyPreferencesRow
+ *
+ * Gets whether an embedded underline in the text of the title indicates a
+ * mnemonic. See hdy_preferences_row_set_use_underline().
+ *
+ * Returns: %TRUE if an embedded underline in the title indicates the mnemonic
+ * accelerator keys.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_preferences_row_get_use_underline (HdyPreferencesRow *self)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_ROW (self), FALSE);
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ return priv->use_underline;
+}
+
+/**
+ * hdy_preferences_row_set_use_underline:
+ * @self: a #HdyPreferencesRow
+ * @use_underline: %TRUE if underlines in the text indicate mnemonics
+ *
+ * If true, an underline in the text of the title indicates the next character
+ * should be used for the mnemonic accelerator key.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_preferences_row_set_use_underline (HdyPreferencesRow *self,
+ gboolean use_underline)
+{
+ HdyPreferencesRowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_ROW (self));
+
+ priv = hdy_preferences_row_get_instance_private (self);
+
+ if (priv->use_underline == !!use_underline)
+ return;
+
+ priv->use_underline = !!use_underline;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_UNDERLINE]);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-row.h b/subprojects/libhandy/src/hdy-preferences-row.h
new file mode 100644
index 0000000..f5e926b
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-row.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_ROW (hdy_preferences_row_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesRow, hdy_preferences_row, HDY, PREFERENCES_ROW, GtkListBoxRow)
+
+/**
+ * HdyPreferencesRowClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesRowClass
+{
+ GtkListBoxRowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_row_new (void);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_preferences_row_get_title (HdyPreferencesRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_row_set_title (HdyPreferencesRow *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_row_get_use_underline (HdyPreferencesRow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_row_set_use_underline (HdyPreferencesRow *self,
+ gboolean use_underline);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-window.c b/subprojects/libhandy/src/hdy-preferences-window.c
new file mode 100644
index 0000000..3618d58
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.c
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-preferences-window.h"
+
+#include "hdy-animation.h"
+#include "hdy-action-row.h"
+#include "hdy-deck.h"
+#include "hdy-preferences-group-private.h"
+#include "hdy-preferences-page-private.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-bar.h"
+#include "hdy-view-switcher-title.h"
+
+/**
+ * SECTION:hdy-preferences-window
+ * @short_description: A window to present an application's preferences.
+ * @Title: HdyPreferencesWindow
+ *
+ * The #HdyPreferencesWindow widget presents an application's preferences
+ * gathered into pages and groups. The preferences are searchable by the user.
+ *
+ * Since: 0.0.10
+ */
+
+typedef struct
+{
+ HdyDeck *subpages_deck;
+ GtkWidget *preferences;
+ GtkStack *content_stack;
+ GtkStack *pages_stack;
+ GtkToggleButton *search_button;
+ GtkSearchEntry *search_entry;
+ GtkListBox *search_results;
+ GtkStack *search_stack;
+ GtkStack *title_stack;
+ HdyViewSwitcherBar *view_switcher_bar;
+ HdyViewSwitcherTitle *view_switcher_title;
+
+ gboolean search_enabled;
+ gboolean can_swipe_back;
+ gint n_last_search_results;
+ GtkWidget *subpage;
+} HdyPreferencesWindowPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdyPreferencesWindow, hdy_preferences_window, HDY_TYPE_WINDOW)
+
+enum {
+ PROP_0,
+ PROP_SEARCH_ENABLED,
+ PROP_CAN_SWIPE_BACK,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gboolean
+filter_search_results (HdyActionRow *row,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ g_autofree gchar *text = g_utf8_casefold (gtk_entry_get_text (GTK_ENTRY (priv->search_entry)), -1);
+ g_autofree gchar *title = g_utf8_casefold (hdy_preferences_row_get_title (HDY_PREFERENCES_ROW (row)), -1);
+ g_autofree gchar *subtitle = NULL;
+
+ /* The CSS engine works in such a way that invisible children are treated as
+ * visible widgets, which breaks the expectations of the .preferences style
+ * class when filtering a row, leading to straight corners when the first row
+ * or last row are filtered out.
+ *
+ * This works around it by explicitly toggling the row's visibility, while
+ * keeping GtkListBox's filtering logic.
+ *
+ * See https://gitlab.gnome.org/GNOME/libhandy/-/merge_requests/424
+ */
+
+ if (strstr (title, text)) {
+ priv->n_last_search_results++;
+ gtk_widget_show (GTK_WIDGET (row));
+
+ return TRUE;
+ }
+
+ subtitle = g_utf8_casefold (hdy_action_row_get_subtitle (row), -1);
+
+ if (!!strstr (subtitle, text)) {
+ priv->n_last_search_results++;
+ gtk_widget_show (GTK_WIDGET (row));
+
+ return TRUE;
+ }
+
+ gtk_widget_hide (GTK_WIDGET (row));
+
+ return FALSE;
+}
+
+static GtkWidget *
+new_search_row_for_preference (HdyPreferencesRow *row,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ HdyActionRow *widget;
+ HdyPreferencesGroup *group;
+ HdyPreferencesPage *page;
+ const gchar *group_title, *page_title;
+ GtkWidget *parent;
+
+ g_assert (HDY_IS_PREFERENCES_ROW (row));
+
+ widget = HDY_ACTION_ROW (hdy_action_row_new ());
+ gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (widget), TRUE);
+ g_object_bind_property (row, "title", widget, "title", G_BINDING_SYNC_CREATE);
+ g_object_bind_property (row, "use-underline", widget, "use-underline", G_BINDING_SYNC_CREATE);
+
+ for (parent = gtk_widget_get_parent (GTK_WIDGET (row));
+ parent != NULL && !HDY_IS_PREFERENCES_GROUP (parent);
+ parent = gtk_widget_get_parent (parent));
+ group = parent != NULL ? HDY_PREFERENCES_GROUP (parent) : NULL;
+ group_title = group != NULL ? hdy_preferences_group_get_title (group) : NULL;
+ if (g_strcmp0 (group_title, "") == 0)
+ group_title = NULL;
+
+ for (parent = gtk_widget_get_parent (GTK_WIDGET (group));
+ parent != NULL && !HDY_IS_PREFERENCES_PAGE (parent);
+ parent = gtk_widget_get_parent (parent));
+ page = parent != NULL ? HDY_PREFERENCES_PAGE (parent) : NULL;
+ page_title = page != NULL ? hdy_preferences_page_get_title (page) : NULL;
+ if (g_strcmp0 (page_title, "") == 0)
+ page_title = NULL;
+
+ if (group_title && !hdy_view_switcher_title_get_title_visible (priv->view_switcher_title))
+ hdy_action_row_set_subtitle (widget, group_title);
+ if (group_title) {
+ g_autofree gchar *subtitle = g_strdup_printf ("%s → %s", page_title != NULL ? page_title : _("Untitled page"), group_title);
+ hdy_action_row_set_subtitle (widget, subtitle);
+ } else if (page_title)
+ hdy_action_row_set_subtitle (widget, page_title);
+
+ gtk_widget_show (GTK_WIDGET (widget));
+
+ g_object_set_data (G_OBJECT (widget), "page", page);
+ g_object_set_data (G_OBJECT (widget), "row", row);
+
+ return GTK_WIDGET (widget);
+}
+
+static void
+update_search_results (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ g_autoptr (GListStore) model;
+
+ model = g_list_store_new (HDY_TYPE_PREFERENCES_ROW);
+ gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), (GtkCallback) hdy_preferences_page_add_preferences_to_model, model);
+ gtk_container_foreach (GTK_CONTAINER (priv->search_results), (GtkCallback) gtk_widget_destroy, NULL);
+ for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (model)); i++)
+ gtk_container_add (GTK_CONTAINER (priv->search_results),
+ new_search_row_for_preference ((HdyPreferencesRow *) g_list_model_get_item (G_LIST_MODEL (model), i), self));
+}
+
+static void
+search_result_activated_cb (HdyPreferencesWindow *self,
+ HdyActionRow *widget)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ HdyPreferencesPage *page;
+ HdyPreferencesRow *row;
+ GtkAdjustment *adjustment;
+ GtkAllocation allocation;
+ gint y = 0;
+
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+ page = HDY_PREFERENCES_PAGE (g_object_get_data (G_OBJECT (widget), "page"));
+ row = HDY_PREFERENCES_ROW (g_object_get_data (G_OBJECT (widget), "row"));
+
+ g_assert (page != NULL);
+ g_assert (row != NULL);
+
+ adjustment = hdy_preferences_page_get_vadjustment (page);
+
+ g_assert (adjustment != NULL);
+
+ gtk_stack_set_visible_child (priv->pages_stack, GTK_WIDGET (page));
+ gtk_widget_set_can_focus (GTK_WIDGET (row), TRUE);
+ gtk_widget_grab_focus (GTK_WIDGET (row));
+
+ if (!gtk_widget_translate_coordinates (GTK_WIDGET (row), GTK_WIDGET (page), 0, 0, NULL, &y))
+ return;
+
+ gtk_container_set_focus_child (GTK_CONTAINER (page), GTK_WIDGET (row));
+ y += gtk_adjustment_get_value (adjustment);
+ gtk_widget_get_allocation (GTK_WIDGET (row), &allocation);
+ gtk_adjustment_clamp_page (adjustment, y, y + allocation.height);
+}
+
+static gboolean
+key_press_event_cb (GtkWidget *sender,
+ GdkEvent *event,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+ GdkModifierType default_modifiers = gtk_accelerator_get_default_mod_mask ();
+ guint keyval;
+ GdkModifierType state;
+
+ if (priv->subpage)
+ return GDK_EVENT_PROPAGATE;
+
+ gdk_event_get_keyval (event, &keyval);
+ gdk_event_get_state (event, &state);
+
+ if (priv->search_enabled &&
+ (keyval == GDK_KEY_f || keyval == GDK_KEY_F) &&
+ (state & default_modifiers) == GDK_CONTROL_MASK) {
+ gtk_toggle_button_set_active (priv->search_button, TRUE);
+
+ return GDK_EVENT_STOP;
+ }
+
+ if (priv->search_enabled &&
+ gtk_search_entry_handle_event (priv->search_entry, event)) {
+ gtk_toggle_button_set_active (priv->search_button, TRUE);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+try_remove_subpages (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (hdy_deck_get_transition_running (priv->subpages_deck))
+ return;
+
+ if (hdy_deck_get_visible_child (priv->subpages_deck) == priv->preferences)
+ priv->subpage = NULL;
+
+ for (GList *child = gtk_container_get_children (GTK_CONTAINER (priv->subpages_deck));
+ child;
+ child = child->next)
+ if (child->data != priv->preferences && child->data != priv->subpage)
+ gtk_container_remove (GTK_CONTAINER (priv->subpages_deck), child->data);
+}
+
+static void
+subpages_deck_transition_running_cb (HdyPreferencesWindow *self)
+{
+ try_remove_subpages (self);
+}
+
+static void
+subpages_deck_visible_child_cb (HdyPreferencesWindow *self)
+{
+ try_remove_subpages (self);
+}
+
+static void
+header_bar_size_allocate_cb (HdyPreferencesWindow *self,
+ GdkRectangle *allocation)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ hdy_view_switcher_title_set_view_switcher_enabled (priv->view_switcher_title, allocation->width > 360);
+}
+
+static void
+title_stack_notify_transition_running_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (gtk_stack_get_transition_running (priv->title_stack) ||
+ gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title))
+ return;
+
+ gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+}
+
+static void
+title_stack_notify_visible_child_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (hdy_get_enable_animations (GTK_WIDGET (priv->title_stack)) ||
+ gtk_stack_get_visible_child (priv->title_stack) != GTK_WIDGET (priv->view_switcher_title))
+ return;
+
+ gtk_entry_set_text (GTK_ENTRY (priv->search_entry), "");
+}
+
+
+static void
+search_button_notify_active_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (gtk_toggle_button_get_active (priv->search_button)) {
+ update_search_results (self);
+ gtk_stack_set_visible_child_name (priv->title_stack, "search");
+ gtk_stack_set_visible_child_name (priv->content_stack, "search");
+ gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->search_entry));
+ /* Grabbing without selecting puts the cursor at the start of the buffer, so
+ * for "type to search" to work we must move the cursor at the end. We can't
+ * use GTK_MOVEMENT_BUFFER_ENDS because it causes a sound to be played.
+ */
+ g_signal_emit_by_name (priv->search_entry, "move-cursor",
+ GTK_MOVEMENT_LOGICAL_POSITIONS, G_MAXINT, FALSE, NULL);
+ } else {
+ gtk_stack_set_visible_child_name (priv->title_stack, "pages");
+ gtk_stack_set_visible_child_name (priv->content_stack, "pages");
+ }
+}
+
+static void
+search_changed_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ priv->n_last_search_results = 0;
+ gtk_list_box_invalidate_filter (priv->search_results);
+ gtk_stack_set_visible_child_name (priv->search_stack,
+ priv->n_last_search_results > 0 ? "results" : "no-results");
+}
+
+static void
+on_page_icon_name_changed (HdyPreferencesPage *page,
+ GParamSpec *pspec,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
+ "icon-name", hdy_preferences_page_get_icon_name (page),
+ NULL);
+}
+
+static void
+stop_search_cb (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+}
+
+static void
+on_page_title_changed (HdyPreferencesPage *page,
+ GParamSpec *pspec,
+ HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ gtk_container_child_set (GTK_CONTAINER (priv->pages_stack), GTK_WIDGET (page),
+ "title", hdy_preferences_page_get_title (page),
+ NULL);
+}
+
+static void
+hdy_preferences_window_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_ENABLED:
+ g_value_set_boolean (value, hdy_preferences_window_get_search_enabled (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_preferences_window_get_can_swipe_back (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_window_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_ENABLED:
+ hdy_preferences_window_set_search_enabled (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_preferences_window_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_preferences_window_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->content_stack == NULL)
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->add (container, child);
+ else if (HDY_IS_PREFERENCES_PAGE (child)) {
+ gtk_container_add (GTK_CONTAINER (priv->pages_stack), child);
+ on_page_icon_name_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
+ on_page_title_changed (HDY_PREFERENCES_PAGE (child), NULL, self);
+ g_signal_connect (child, "notify::icon-name",
+ G_CALLBACK (on_page_icon_name_changed), self);
+ g_signal_connect (child, "notify::title",
+ G_CALLBACK (on_page_title_changed), self);
+ } else
+ g_warning ("Can't add children of type %s to %s",
+ G_OBJECT_TYPE_NAME (child),
+ G_OBJECT_TYPE_NAME (container));
+}
+
+static void
+hdy_preferences_window_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (child == GTK_WIDGET (priv->content_stack))
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->remove (container, child);
+ else
+ gtk_container_remove (GTK_CONTAINER (priv->pages_stack), child);
+}
+
+static void
+hdy_preferences_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyPreferencesWindow *self = HDY_PREFERENCES_WINDOW (container);
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ if (include_internals)
+ GTK_CONTAINER_CLASS (hdy_preferences_window_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+ else if (priv->pages_stack)
+ gtk_container_foreach (GTK_CONTAINER (priv->pages_stack), callback, callback_data);
+}
+
+static void
+hdy_preferences_window_class_init (HdyPreferencesWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_preferences_window_get_property;
+ object_class->set_property = hdy_preferences_window_set_property;
+
+ container_class->add = hdy_preferences_window_add;
+ container_class->remove = hdy_preferences_window_remove;
+ container_class->forall = hdy_preferences_window_forall;
+
+ /**
+ * HdyPreferencesWindow:search-enabled:
+ *
+ * Whether search is enabled.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SEARCH_ENABLED] =
+ g_param_spec_boolean ("search-enabled",
+ _("Search enabled"),
+ _("Whether search is enabled"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyPreferencesWindow:can-swipe-back:
+ *
+ * Whether or not the window allows closing the subpage via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch from a subpage to the preferences"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-preferences-window.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, subpages_deck);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, preferences);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, content_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, pages_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_button);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_entry);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_results);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, search_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, title_stack);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_bar);
+ gtk_widget_class_bind_template_child_private (widget_class, HdyPreferencesWindow, view_switcher_title);
+ gtk_widget_class_bind_template_callback (widget_class, subpages_deck_transition_running_cb);
+ gtk_widget_class_bind_template_callback (widget_class, subpages_deck_visible_child_cb);
+ gtk_widget_class_bind_template_callback (widget_class, header_bar_size_allocate_cb);
+ gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_transition_running_cb);
+ gtk_widget_class_bind_template_callback (widget_class, title_stack_notify_visible_child_cb);
+ gtk_widget_class_bind_template_callback (widget_class, key_press_event_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_button_notify_active_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_changed_cb);
+ gtk_widget_class_bind_template_callback (widget_class, search_result_activated_cb);
+ gtk_widget_class_bind_template_callback (widget_class, stop_search_cb);
+}
+
+static void
+hdy_preferences_window_init (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv = hdy_preferences_window_get_instance_private (self);
+
+ priv->search_enabled = TRUE;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_list_box_set_filter_func (priv->search_results, (GtkListBoxFilterFunc) filter_search_results, self, NULL);
+}
+
+/**
+ * hdy_preferences_window_new:
+ *
+ * Creates a new #HdyPreferencesWindow.
+ *
+ * Returns: a new #HdyPreferencesWindow
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_preferences_window_new (void)
+{
+ return g_object_new (HDY_TYPE_PREFERENCES_WINDOW, NULL);
+}
+
+/**
+ * hdy_preferences_window_get_search_enabled:
+ * @self: a #HdyPreferencesWindow
+ *
+ * Gets whether search is enabled for @self.
+ *
+ * Returns: whether search is enabled for @self.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE);
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ return priv->search_enabled;
+}
+
+/**
+ * hdy_preferences_window_set_search_enabled:
+ * @self: a #HdyPreferencesWindow
+ * @search_enabled: %TRUE to enable search, %FALSE to disable it
+ *
+ * Sets whether search is enabled for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self,
+ gboolean search_enabled)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ search_enabled = !!search_enabled;
+
+ if (priv->search_enabled == search_enabled)
+ return;
+
+ priv->search_enabled = search_enabled;
+ gtk_widget_set_visible (GTK_WIDGET (priv->search_button), search_enabled);
+ if (!search_enabled)
+ gtk_toggle_button_set_active (priv->search_button, FALSE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_ENABLED]);
+}
+
+/**
+ * hdy_preferences_window_set_can_swipe_back:
+ * @self: a #HdyPreferencesWindow
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching from a subpage to the preferences
+ * via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self,
+ gboolean can_swipe_back)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ can_swipe_back = !!can_swipe_back;
+
+ if (priv->can_swipe_back == can_swipe_back)
+ return;
+
+ priv->can_swipe_back = can_swipe_back;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]);
+}
+
+/**
+ * hdy_preferences_window_get_can_swipe_back
+ * @self: a #HdyPreferencesWindow
+ *
+ * Returns whether or not @self allows switching from a subpage to the
+ * preferences via a swipe gesture.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_val_if_fail (HDY_IS_PREFERENCES_WINDOW (self), FALSE);
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ return priv->can_swipe_back;
+}
+
+/**
+ * hdy_preferences_window_present_subpage:
+ * @self: a #HdyPreferencesWindow
+ * @subpage: the subpage
+ *
+ * Sets @subpage as the window's subpage and present it.
+ * The transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_present_subpage (HdyPreferencesWindow *self,
+ GtkWidget *subpage)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+ g_return_if_fail (GTK_IS_WIDGET (subpage));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->subpage == subpage)
+ return;
+
+ priv->subpage = subpage;
+
+ /* The check below avoids a warning when re-entering a subpage during the
+ * transition between the that subpage to the preferences.
+ */
+ if (gtk_widget_get_parent (subpage) != GTK_WIDGET (priv->subpages_deck))
+ gtk_container_add (GTK_CONTAINER (priv->subpages_deck), subpage);
+
+ hdy_deck_set_visible_child (priv->subpages_deck, subpage);
+}
+
+/**
+ * hdy_preferences_window_close_subpage:
+ * @self: a #HdyPreferencesWindow
+ *
+ * Closes the current subpage to return back to the preferences, if there is no
+ * presented subpage, this does nothing.
+ *
+ * Since: 1.0
+ */
+void
+hdy_preferences_window_close_subpage (HdyPreferencesWindow *self)
+{
+ HdyPreferencesWindowPrivate *priv;
+
+ g_return_if_fail (HDY_IS_PREFERENCES_WINDOW (self));
+
+ priv = hdy_preferences_window_get_instance_private (self);
+
+ if (priv->subpage == NULL)
+ return;
+
+ hdy_deck_set_visible_child (priv->subpages_deck, priv->preferences);
+}
diff --git a/subprojects/libhandy/src/hdy-preferences-window.h b/subprojects/libhandy/src/hdy-preferences-window.h
new file mode 100644
index 0000000..427a94a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-window.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_PREFERENCES_WINDOW (hdy_preferences_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyPreferencesWindow, hdy_preferences_window, HDY, PREFERENCES_WINDOW, HdyWindow)
+
+/**
+ * HdyPreferencesWindowClass
+ * @parent_class: The parent class
+ */
+struct _HdyPreferencesWindowClass
+{
+ HdyWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_preferences_window_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_window_get_search_enabled (HdyPreferencesWindow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_set_search_enabled (HdyPreferencesWindow *self,
+ gboolean search_enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_preferences_window_get_can_swipe_back (HdyPreferencesWindow *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_set_can_swipe_back (HdyPreferencesWindow *self,
+ gboolean can_swipe_back);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_present_subpage (HdyPreferencesWindow *self,
+ GtkWidget *subpage);
+HDY_AVAILABLE_IN_ALL
+void hdy_preferences_window_close_subpage (HdyPreferencesWindow *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-preferences-window.ui b/subprojects/libhandy/src/hdy-preferences-window.ui
new file mode 100644
index 0000000..5f764fc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-preferences-window.ui
@@ -0,0 +1,248 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyPreferencesWindow" parent="HdyWindow">
+ <property name="modal">True</property>
+ <property name="window_position">center</property>
+ <property name="destroy_with_parent">True</property>
+ <property name="icon_name">gtk-preferences</property>
+ <property name="title" translatable="yes">Preferences</property>
+ <property name="type_hint">dialog</property>
+ <property name="default-width">640</property>
+ <property name="default-height">576</property>
+ <signal name="key-press-event" handler="key_press_event_cb" after="yes" swapped="no"/>
+ <child>
+ <object class="HdyDeck" id="subpages_deck">
+ <property name="can-swipe-back" bind-source="HdyPreferencesWindow" bind-property="can-swipe-back" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ <property name="width-request">360</property>
+ <signal name="notify::transition-running" handler="subpages_deck_transition_running_cb" swapped="yes"/>
+ <signal name="notify::visible-child" handler="subpages_deck_visible_child_cb" swapped="yes"/>
+ <child>
+ <object class="GtkBox" id="preferences">
+ <property name="visible">True</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="HdyHeaderBar">
+ <property name="centering_policy">strict</property>
+ <property name="show_close_button">True</property>
+ <property name="visible">True</property>
+ <signal name="size-allocate" handler="header_bar_size_allocate_cb" swapped="yes"/>
+ <child type="title">
+ <object class="GtkStack" id="title_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="visible">True</property>
+ <signal name="notify::visible-child" handler="title_stack_notify_visible_child_cb" swapped="true"/>
+ <signal name="notify::transition-running" handler="title_stack_notify_transition_running_cb" swapped="true"/>
+ <child>
+ <object class="HdyViewSwitcherTitle" id="view_switcher_title">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="stack">pages_stack</property>
+ <property name="title" bind-source="HdyPreferencesWindow" bind-property="title" bind-flags="sync-create"/>
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">pages</property>
+ </packing>
+ </child>
+ <child>
+ <object class="HdyClamp">
+ <property name="tightening-threshold">300</property>
+ <property name="maximum-size">400</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkSearchEntry" id="search_entry">
+ <property name="hexpand">True</property>
+ <property name="visible">True</property>
+ <signal name="search-changed" handler="search_changed_cb" swapped="yes"/>
+ <signal name="stop-search" handler="stop_search_cb" swapped="yes"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkToggleButton" id="search_button">
+ <property name="can_focus">True</property>
+ <property name="receives_default">False</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <signal name="notify::active" handler="search_button_notify_active_cb" swapped="yes"/>
+ <style>
+ <class name="image-button"/>
+ </style>
+ <child internal-child="accessible">
+ <object class="AtkObject" id="a11y-search">
+ <property name="accessible-name" translatable="yes">Search</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkImage">
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">1</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="content_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="vhomogeneous">False</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkStack" id="pages_stack">
+ <property name="transition-type">crossfade</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="HdyViewSwitcherBar" id="view_switcher_bar">
+ <property name="visible">True</property>
+ <property name="can_focus">False</property>
+ <property name="stack">pages_stack</property>
+ <property name="reveal" bind-source="view_switcher_title" bind-property="title-visible" bind-flags="sync-create"/>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">pages</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="search_stack">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyClamp">
+ <property name="margin_bottom">18</property>
+ <property name="margin_end">12</property>
+ <property name="margin_start">12</property>
+ <property name="margin_top">18</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkListBox" id="search_results">
+ <property name="selection-mode">none</property>
+ <property name="valign">start</property>
+ <property name="visible">True</property>
+ <signal name="row-activated" handler="search_result_activated_cb" swapped="yes"/>
+ <style>
+ <class name="content"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">results</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow">
+ <property name="can_focus">False</property>
+ <property name="expand">True</property>
+ <property name="hscrollbar_policy">never</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="valign">center</property>
+ <property name="vexpand">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkImage">
+ <property name="can_focus">False</property>
+ <property name="icon_name">edit-find-symbolic</property>
+ <property name="icon_size">0</property>
+ <property name="margin_bottom">18</property>
+ <property name="pixel_size">128</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="dim-label"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="can_focus">False</property>
+ <property name="margin_end">12</property>
+ <property name="margin_start">12</property>
+ <property name="orientation">vertical</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel">
+ <property name="can_focus">False</property>
+ <property name="halign">center</property>
+ <property name="justify">center</property>
+ <property name="label" translatable="yes">No Results Found</property>
+ <property name="margin_bottom">12</property>
+ <property name="opacity">0.5</property>
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ <attributes>
+ <attribute name="scale" value="2"/>
+ <attribute name="weight" value="bold"/>
+ </attributes>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel">
+ <property name="can_focus">False</property>
+ <property name="justify">center</property>
+ <property name="label" translatable="yes">Try a different search</property>
+ <property name="margin_bottom">6</property>
+ <property name="opacity">0.5</property>
+ <property name="use_markup">True</property>
+ <property name="visible">True</property>
+ <property name="wrap">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="name">no-results</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">search</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-search-bar.c b/subprojects/libhandy/src/hdy-search-bar.c
new file mode 100644
index 0000000..decbda4
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.c
@@ -0,0 +1,659 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 2013 Red Hat, Inc.
+ * Copyright (C) 2018 Purism SPC
+ *
+ * Authors:
+ * - Bastien Nocera <bnocera@redhat.com>
+ * - Adrien Plazas <adrien.plazas@puri.sm>
+ *
+ * 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 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 2013. See the AUTHORS
+ * file for a list of people on the GTK Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/*
+ * Forked from the GTK+ 3.94.0 GtkSearchBar widget and modified for libhandy by
+ * Adrien Plazas on behalf of Purism SPC 2018.
+ *
+ * The AUTHORS file referenced above is part of GTK and not present in
+ * libhandy. At the time of the fork it was available here:
+ * https://gitlab.gnome.org/GNOME/gtk/blob/faba0f0145b1281facba20fb90699e3db594fbb0/AUTHORS
+ *
+ * The ChangeLog file referenced above was not present in GTK+ at the time of
+ * the fork.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-search-bar.h"
+
+/**
+ * SECTION:hdy-search-bar
+ * @short_description: A toolbar to integrate a search entry with.
+ * @Title: HdySearchBar
+ *
+ * #HdySearchBar is a container made to have a search entry (possibly
+ * with additional connex widgets, such as drop-down menus, or buttons)
+ * built-in. The search bar would appear when a search is started through
+ * typing on the keyboard, or the application’s search mode is toggled on.
+ *
+ * For keyboard presses to start a search, events will need to be
+ * forwarded from the top-level window that contains the search bar.
+ * See hdy_search_bar_handle_event() for example code. Common shortcuts
+ * such as Ctrl+F should be handled as an application action, or through
+ * the menu items.
+ *
+ * You will also need to tell the search bar about which entry you
+ * are using as your search entry using hdy_search_bar_connect_entry().
+ * The following example shows you how to create a more complex search
+ * entry.
+ *
+ * HdySearchBar is very similar to #GtkSearchBar, the main difference being that
+ * it allows the search entry to fill all the available space. This allows you
+ * to control your search entry's width with a #HdyClamp.
+ *
+ * # CSS nodes
+ *
+ * #HdySearchBar has a single CSS node with name searchbar.
+ *
+ * Since: 0.0.6
+ */
+
+typedef struct {
+ /* Template widgets */
+ GtkWidget *revealer;
+ GtkWidget *tool_box;
+ GtkWidget *start;
+ GtkWidget *end;
+ GtkWidget *close_button;
+
+ GtkWidget *entry;
+ gboolean reveal_child;
+ gboolean show_close_button;
+} HdySearchBarPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (HdySearchBar, hdy_search_bar, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_SEARCH_MODE_ENABLED,
+ PROP_SHOW_CLOSE_BUTTON,
+ LAST_PROPERTY
+};
+
+static GParamSpec *props[LAST_PROPERTY] = { NULL, };
+
+/* This comes from gtksearchentry.c in GTK. */
+static gboolean
+gtk_search_entry_is_keynav_event (GdkEvent *event)
+{
+ GdkModifierType state = 0;
+ guint keyval;
+
+ if (!gdk_event_get_keyval (event, &keyval))
+ return FALSE;
+
+ gdk_event_get_state (event, &state);
+
+ if (keyval == GDK_KEY_Tab || keyval == GDK_KEY_KP_Tab ||
+ keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up ||
+ keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down ||
+ keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left ||
+ keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right ||
+ keyval == GDK_KEY_Home || keyval == GDK_KEY_KP_Home ||
+ keyval == GDK_KEY_End || keyval == GDK_KEY_KP_End ||
+ keyval == GDK_KEY_Page_Up || keyval == GDK_KEY_KP_Page_Up ||
+ keyval == GDK_KEY_Page_Down || keyval == GDK_KEY_KP_Page_Down ||
+ ((state & (GDK_CONTROL_MASK | GDK_MOD1_MASK)) != 0))
+ return TRUE;
+
+ /* Other navigation events should get automatically
+ * ignored as they will not change the content of the entry
+ */
+ return FALSE;
+}
+
+static void
+stop_search_cb (GtkWidget *entry,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE);
+}
+
+static gboolean
+entry_key_pressed_event_cb (GtkWidget *widget,
+ GdkEvent *event,
+ HdySearchBar *self)
+{
+ if (event->key.keyval == GDK_KEY_Escape) {
+ stop_search_cb (widget, self);
+
+ return GDK_EVENT_STOP;
+ } else {
+ return GDK_EVENT_PROPAGATE;
+ }
+}
+
+static void
+preedit_changed_cb (GtkEntry *entry,
+ GtkWidget *popup,
+ gboolean *preedit_changed)
+{
+ *preedit_changed = TRUE;
+}
+
+static gboolean
+hdy_search_bar_handle_event_for_entry (HdySearchBar *self,
+ GdkEvent *event)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean handled;
+ gboolean preedit_changed;
+ guint preedit_change_id;
+ gboolean res;
+ char *old_text, *new_text;
+
+ if (gtk_search_entry_is_keynav_event (event) ||
+ event->key.keyval == GDK_KEY_space ||
+ event->key.keyval == GDK_KEY_Menu)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!gtk_widget_get_realized (priv->entry))
+ gtk_widget_realize (priv->entry);
+
+ handled = GDK_EVENT_PROPAGATE;
+ preedit_changed = FALSE;
+ preedit_change_id = g_signal_connect (priv->entry, "preedit-changed",
+ G_CALLBACK (preedit_changed_cb), &preedit_changed);
+
+ old_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry)));
+ res = gtk_widget_event (priv->entry, event);
+ new_text = g_strdup (gtk_entry_get_text (GTK_ENTRY (priv->entry)));
+
+ g_signal_handler_disconnect (priv->entry, preedit_change_id);
+
+ if ((res && g_strcmp0 (new_text, old_text) != 0) || preedit_changed)
+ handled = GDK_EVENT_STOP;
+
+ g_free (old_text);
+ g_free (new_text);
+
+ return handled;
+}
+
+/**
+ * hdy_search_bar_handle_event:
+ * @self: a #HdySearchBar
+ * @event: a #GdkEvent containing key press events
+ *
+ * This function should be called when the top-level
+ * window which contains the search bar received a key event.
+ *
+ * If the key event is handled by the search bar, the bar will
+ * be shown, the entry populated with the entered text and %GDK_EVENT_STOP
+ * will be returned. The caller should ensure that events are
+ * not propagated further.
+ *
+ * If no entry has been connected to the search bar, using
+ * hdy_search_bar_connect_entry(), this function will return
+ * immediately with a warning.
+ *
+ * ## Showing the search bar on key presses
+ *
+ * |[<!-- language="C" -->
+ * static gboolean
+ * on_key_press_event (GtkWidget *widget,
+ * GdkEvent *event,
+ * gpointer user_data)
+ * {
+ * HdySearchBar *bar = HDY_SEARCH_BAR (user_data);
+ * return hdy_search_bar_handle_event (self, event);
+ * }
+ *
+ * static void
+ * create_toplevel (void)
+ * {
+ * GtkWidget *window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+ * GtkWindow *search_bar = hdy_search_bar_new ();
+ *
+ * // Add more widgets to the window...
+ *
+ * g_signal_connect (window,
+ * "key-press-event",
+ * G_CALLBACK (on_key_press_event),
+ * search_bar);
+ * }
+ * ]|
+ *
+ * Returns: %GDK_EVENT_STOP if the key press event resulted
+ * in text being entered in the search entry (and revealing
+ * the search bar if necessary), %GDK_EVENT_PROPAGATE otherwise.
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_handle_event (HdySearchBar *self,
+ GdkEvent *event)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean handled;
+
+ if (priv->reveal_child)
+ return GDK_EVENT_PROPAGATE;
+
+ if (priv->entry == NULL) {
+ g_warning ("The search bar does not have an entry connected to it. Call hdy_search_bar_connect_entry() to connect one.");
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ handled = gtk_search_entry_handle_event (GTK_SEARCH_ENTRY (priv->entry), event);
+ else
+ handled = hdy_search_bar_handle_event_for_entry (self, event);
+
+ if (handled == GDK_EVENT_STOP)
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), TRUE);
+
+ return handled;
+}
+
+static void
+reveal_child_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean reveal_child;
+
+ g_object_get (object, "reveal-child", &reveal_child, NULL);
+ if (reveal_child)
+ gtk_widget_set_child_visible (priv->revealer, TRUE);
+
+ if (reveal_child == priv->reveal_child)
+ return;
+
+ priv->reveal_child = reveal_child;
+
+ if (priv->entry) {
+ if (reveal_child)
+ gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->entry));
+ else
+ gtk_entry_set_text (GTK_ENTRY (priv->entry), "");
+ }
+
+ g_object_notify (G_OBJECT (self), "search-mode-enabled");
+}
+
+static void
+child_revealed_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+ gboolean val;
+
+ g_object_get (object, "child-revealed", &val, NULL);
+ if (!val)
+ gtk_widget_set_child_visible (priv->revealer, FALSE);
+}
+
+static void
+close_button_clicked_cb (GtkWidget *button,
+ HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), FALSE);
+}
+
+static void
+hdy_search_bar_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (container);
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ if (priv->revealer == NULL) {
+ GTK_CONTAINER_CLASS (hdy_search_bar_parent_class)->add (container, child);
+ } else {
+ gtk_box_set_center_widget (GTK_BOX (priv->tool_box), child);
+ gtk_container_child_set (GTK_CONTAINER (priv->tool_box), child,
+ "expand", TRUE,
+ NULL);
+ /* If an entry is the only child, save the developer a couple of
+ * lines of code
+ */
+ if (GTK_IS_ENTRY (child))
+ hdy_search_bar_connect_entry (self, GTK_ENTRY (child));
+ }
+}
+
+static void
+hdy_search_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SEARCH_MODE_ENABLED:
+ hdy_search_bar_set_search_mode (self, g_value_get_boolean (value));
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ hdy_search_bar_set_show_close_button (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_search_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ switch (prop_id) {
+ case PROP_SEARCH_MODE_ENABLED:
+ g_value_set_boolean (value, priv->reveal_child);
+ break;
+ case PROP_SHOW_CLOSE_BUTTON:
+ g_value_set_boolean (value, hdy_search_bar_get_show_close_button (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void hdy_search_bar_set_entry (HdySearchBar *self,
+ GtkEntry *entry);
+
+static void
+hdy_search_bar_dispose (GObject *object)
+{
+ HdySearchBar *self = HDY_SEARCH_BAR (object);
+
+ hdy_search_bar_set_entry (self, NULL);
+
+ G_OBJECT_CLASS (hdy_search_bar_parent_class)->dispose (object);
+}
+
+static gboolean
+hdy_search_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ gint width, height;
+ GtkStyleContext *context;
+
+ width = gtk_widget_get_allocated_width (widget);
+ height = gtk_widget_get_allocated_height (widget);
+ context = gtk_widget_get_style_context (widget);
+
+ gtk_render_background (context, cr, 0, 0, width, height);
+ gtk_render_frame (context, cr, 0, 0, width, height);
+
+ GTK_WIDGET_CLASS (hdy_search_bar_parent_class)->draw (widget, cr);
+
+ return FALSE;
+}
+
+static void
+hdy_search_bar_class_init (HdySearchBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->dispose = hdy_search_bar_dispose;
+ object_class->set_property = hdy_search_bar_set_property;
+ object_class->get_property = hdy_search_bar_get_property;
+ widget_class->draw = hdy_search_bar_draw;
+
+ container_class->add = hdy_search_bar_add;
+
+ /**
+ * HdySearchBar:search-mode-enabled:
+ *
+ * Whether the search mode is on and the search bar shown.
+ *
+ * See hdy_search_bar_set_search_mode() for details.
+ */
+ props[PROP_SEARCH_MODE_ENABLED] =
+ g_param_spec_boolean ("search-mode-enabled",
+ _("Search Mode Enabled"),
+ _("Whether the search mode is on and the search bar shown"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySearchBar:show-close-button:
+ *
+ * Whether to show the close button in the toolbar.
+ */
+ props[PROP_SHOW_CLOSE_BUTTON] =
+ g_param_spec_boolean ("show-close-button",
+ _("Show Close Button"),
+ _("Whether to show the close button in the toolbar"),
+ FALSE,
+ G_PARAM_READWRITE|G_PARAM_CONSTRUCT|G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROPERTY, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-search-bar.ui");
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, tool_box);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, revealer);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, start);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, end);
+ gtk_widget_class_bind_template_child_private (widget_class, HdySearchBar, close_button);
+
+ gtk_widget_class_set_css_name (widget_class, "searchbar");
+}
+
+static void
+hdy_search_bar_init (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ /* We use child-visible to avoid the unexpanded revealer
+ * peaking out by 1 pixel
+ */
+ gtk_widget_set_child_visible (priv->revealer, FALSE);
+
+ g_signal_connect (priv->revealer, "notify::reveal-child",
+ G_CALLBACK (reveal_child_changed_cb), self);
+ g_signal_connect (priv->revealer, "notify::child-revealed",
+ G_CALLBACK (child_revealed_changed_cb), self);
+
+ gtk_widget_set_no_show_all (priv->start, TRUE);
+ gtk_widget_set_no_show_all (priv->end, TRUE);
+ g_signal_connect (priv->close_button, "clicked",
+ G_CALLBACK (close_button_clicked_cb), self);
+};
+
+/**
+ * hdy_search_bar_new:
+ *
+ * Creates a #HdySearchBar. You will need to tell it about
+ * which widget is going to be your text entry using
+ * hdy_search_bar_connect_entry().
+ *
+ * Returns: a new #HdySearchBar
+ *
+ * Since: 0.0.6
+ */
+GtkWidget *
+hdy_search_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_SEARCH_BAR, NULL);
+}
+
+static void
+hdy_search_bar_set_entry (HdySearchBar *self,
+ GtkEntry *entry)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ if (priv->entry != NULL) {
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ g_signal_handlers_disconnect_by_func (priv->entry, stop_search_cb, self);
+ else
+ g_signal_handlers_disconnect_by_func (priv->entry, entry_key_pressed_event_cb, self);
+ g_object_remove_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry);
+ }
+
+ priv->entry = GTK_WIDGET (entry);
+
+ if (priv->entry != NULL) {
+ g_object_add_weak_pointer (G_OBJECT (priv->entry), (gpointer *) &priv->entry);
+ if (GTK_IS_SEARCH_ENTRY (priv->entry))
+ g_signal_connect (priv->entry, "stop-search",
+ G_CALLBACK (stop_search_cb), self);
+ else
+ g_signal_connect (priv->entry, "key-press-event",
+ G_CALLBACK (entry_key_pressed_event_cb), self);
+ }
+}
+
+/**
+ * hdy_search_bar_connect_entry:
+ * @self: a #HdySearchBar
+ * @entry: a #GtkEntry
+ *
+ * Connects the #GtkEntry widget passed as the one to be used in
+ * this search bar. The entry should be a descendant of the search bar.
+ * This is only required if the entry isn’t the direct child of the
+ * search bar (as in our main example).
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_connect_entry (HdySearchBar *self,
+ GtkEntry *entry)
+{
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+ g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry));
+
+ hdy_search_bar_set_entry (self, entry);
+}
+
+/**
+ * hdy_search_bar_get_search_mode:
+ * @self: a #HdySearchBar
+ *
+ * Returns whether the search mode is on or off.
+ *
+ * Returns: whether search mode is toggled on
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_get_search_mode (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE);
+
+ return priv->reveal_child;
+}
+
+/**
+ * hdy_search_bar_set_search_mode:
+ * @self: a #HdySearchBar
+ * @search_mode: the new state of the search mode
+ *
+ * Switches the search mode on or off.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_set_search_mode (HdySearchBar *self,
+ gboolean search_mode)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer), search_mode);
+}
+
+/**
+ * hdy_search_bar_get_show_close_button:
+ * @self: a #HdySearchBar
+ *
+ * Returns whether the close button is shown.
+ *
+ * Returns: whether the close button is shown
+ *
+ * Since: 0.0.6
+ */
+gboolean
+hdy_search_bar_get_show_close_button (HdySearchBar *self)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_val_if_fail (HDY_IS_SEARCH_BAR (self), FALSE);
+
+ return priv->show_close_button;
+}
+
+/**
+ * hdy_search_bar_set_show_close_button:
+ * @self: a #HdySearchBar
+ * @visible: whether the close button will be shown or not
+ *
+ * Shows or hides the close button. Applications that
+ * already have a “search” toggle button should not show a close
+ * button in their search bar, as it duplicates the role of the
+ * toggle button.
+ *
+ * Since: 0.0.6
+ */
+void
+hdy_search_bar_set_show_close_button (HdySearchBar *self,
+ gboolean visible)
+{
+ HdySearchBarPrivate *priv = hdy_search_bar_get_instance_private (self);
+
+ g_return_if_fail (HDY_IS_SEARCH_BAR (self));
+
+ visible = visible != FALSE;
+
+ if (priv->show_close_button == visible)
+ return;
+
+ priv->show_close_button = visible;
+ gtk_widget_set_visible (priv->start, visible);
+ gtk_widget_set_visible (priv->end, visible);
+ g_object_notify (G_OBJECT (self), "show-close-button");
+}
diff --git a/subprojects/libhandy/src/hdy-search-bar.h b/subprojects/libhandy/src/hdy-search-bar.h
new file mode 100644
index 0000000..fc6aa72
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SEARCH_BAR (hdy_search_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdySearchBar, hdy_search_bar, HDY, SEARCH_BAR, GtkBin)
+
+struct _HdySearchBarClass
+{
+ GtkBinClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_search_bar_new (void);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_connect_entry (HdySearchBar *self,
+ GtkEntry *entry);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_get_search_mode (HdySearchBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_set_search_mode (HdySearchBar *self,
+ gboolean search_mode);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_get_show_close_button (HdySearchBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_search_bar_set_show_close_button (HdySearchBar *self,
+ gboolean visible);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_search_bar_handle_event (HdySearchBar *self,
+ GdkEvent *event);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-search-bar.ui b/subprojects/libhandy/src/hdy-search-bar.ui
new file mode 100644
index 0000000..5e79042
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-search-bar.ui
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <template class="HdySearchBar" parent="GtkBin">
+ <child>
+ <object class="GtkRevealer" id="revealer">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkBox" id="tool_box">
+ <property name="visible">True</property>
+ <property name="border-width">6</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkBox" id="start">
+ <property name="visible">False</property>
+ <property name="halign">start</property>
+ <property name="orientation">vertical</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="end">
+ <property name="visible">False</property>
+ <property name="halign">end</property>
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkButton" id="close_button">
+ <property name="visible">True</property>
+ <property name="can-focus">True</property>
+ <property name="receives-default">1</property>
+ <property name="relief">none</property>
+ <style>
+ <class name="close"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="close_image">
+ <property name="visible">True</property>
+ <property name="icon-size">1</property>
+ <property name="icon-name">window-close-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="expand">True</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="pack_type">end</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+ <object class="GtkSizeGroup">
+ <property name="mode">horizontal</property>
+ <widgets>
+ <widget name="start"/>
+ <widget name="end"/>
+ </widgets>
+ </object>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-shadow-helper-private.h b/subprojects/libhandy/src/hdy-shadow-helper-private.h
new file mode 100644
index 0000000..4d96e11
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-shadow-helper-private.h
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SHADOW_HELPER (hdy_shadow_helper_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyShadowHelper, hdy_shadow_helper, HDY, SHADOW_HELPER, GObject)
+
+HdyShadowHelper *hdy_shadow_helper_new (GtkWidget *widget);
+
+void hdy_shadow_helper_clear_cache (HdyShadowHelper *self);
+
+void hdy_shadow_helper_draw_shadow (HdyShadowHelper *self,
+ cairo_t *cr,
+ gint width,
+ gint height,
+ gdouble progress,
+ GtkPanDirection direction);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-shadow-helper.c b/subprojects/libhandy/src/hdy-shadow-helper.c
new file mode 100644
index 0000000..929f04a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-shadow-helper.c
@@ -0,0 +1,445 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-cairo-private.h"
+#include "hdy-shadow-helper-private.h"
+
+#include <math.h>
+
+/**
+ * PRIVATE:hdy-shadow-helper
+ * @short_description: Shadow helper used in #HdyLeaflet
+ * @title: HdyShadowHelper
+ * @See_also: #HdyLeaflet
+ * @stability: Private
+ *
+ * A helper class for drawing #HdyLeaflet transition shadow.
+ *
+ * Since: 0.0.12
+ */
+
+struct _HdyShadowHelper
+{
+ GObject parent_instance;
+
+ GtkWidget *widget;
+
+ gboolean is_cache_valid;
+
+ cairo_pattern_t *dimming_pattern;
+ cairo_pattern_t *shadow_pattern;
+ cairo_pattern_t *border_pattern;
+ cairo_pattern_t *outline_pattern;
+ gint shadow_size;
+ gint border_size;
+ gint outline_size;
+
+ GtkPanDirection last_direction;
+ gint last_width;
+ gint last_height;
+ gint last_scale;
+};
+
+G_DEFINE_TYPE (HdyShadowHelper, hdy_shadow_helper, G_TYPE_OBJECT);
+
+enum {
+ PROP_0,
+ PROP_WIDGET,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+
+static GtkStyleContext *
+create_context (HdyShadowHelper *self,
+ const gchar *name,
+ GtkPanDirection direction)
+{
+ g_autoptr(GtkWidgetPath) path = NULL;
+ GtkStyleContext *context;
+ gint pos;
+ const gchar *direction_name;
+ GEnumClass *enum_class;
+
+ enum_class = g_type_class_ref (GTK_TYPE_PAN_DIRECTION);
+ direction_name = g_enum_get_value (enum_class, direction)->value_nick;
+
+ path = gtk_widget_path_copy (gtk_widget_get_path (self->widget));
+
+ pos = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET);
+ gtk_widget_path_iter_set_object_name (path, pos, name);
+
+ gtk_widget_path_iter_add_class (path, pos, direction_name);
+
+ context = gtk_style_context_new ();
+ gtk_style_context_set_path (context, path);
+
+ g_type_class_unref (enum_class);
+
+ return context;
+}
+
+static gint
+get_element_size (GtkStyleContext *context,
+ GtkPanDirection direction)
+{
+ gint width, height;
+
+ gtk_style_context_get (context,
+ gtk_style_context_get_state (context),
+ "min-width", &width,
+ "min-height", &height,
+ NULL);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_RIGHT:
+ return width;
+ case GTK_PAN_DIRECTION_UP:
+ case GTK_PAN_DIRECTION_DOWN:
+ return height;
+ default:
+ g_assert_not_reached ();
+ }
+
+ return 0;
+}
+
+static cairo_pattern_t *
+create_element_pattern (GtkStyleContext *context,
+ gint width,
+ gint height)
+{
+ g_autoptr (cairo_surface_t) surface =
+ cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width, height);
+ g_autoptr (cairo_t) cr = cairo_create (surface);
+ cairo_pattern_t *pattern;
+
+ gtk_render_background (context, cr, 0, 0, width, height);
+ gtk_render_frame (context, cr, 0, 0, width, height);
+
+ pattern = cairo_pattern_create_for_surface (surface);
+
+ return pattern;
+}
+
+static void
+cache_shadow (HdyShadowHelper *self,
+ gint width,
+ gint height,
+ GtkPanDirection direction)
+{
+ g_autoptr(GtkStyleContext) dim_context = NULL;
+ g_autoptr(GtkStyleContext) shadow_context = NULL;
+ g_autoptr(GtkStyleContext) border_context = NULL;
+ g_autoptr(GtkStyleContext) outline_context = NULL;
+ gint shadow_size, border_size, outline_size, scale;
+
+ scale = gtk_widget_get_scale_factor (self->widget);
+
+ if (self->last_direction == direction &&
+ self->last_width == width &&
+ self->last_height == height &&
+ self->last_scale == scale &&
+ self->is_cache_valid)
+ return;
+
+ hdy_shadow_helper_clear_cache (self);
+
+ dim_context = create_context (self, "dimming", direction);
+ shadow_context = create_context (self, "shadow", direction);
+ border_context = create_context (self, "border", direction);
+ outline_context = create_context (self, "outline", direction);
+
+ shadow_size = get_element_size (shadow_context, direction);
+ border_size = get_element_size (border_context, direction);
+ outline_size = get_element_size (outline_context, direction);
+
+ self->dimming_pattern = create_element_pattern (dim_context, width, height);
+ if (direction == GTK_PAN_DIRECTION_LEFT || direction == GTK_PAN_DIRECTION_RIGHT) {
+ self->shadow_pattern = create_element_pattern (shadow_context, shadow_size, height);
+ self->border_pattern = create_element_pattern (border_context, border_size, height);
+ self->outline_pattern = create_element_pattern (outline_context, outline_size, height);
+ } else {
+ self->shadow_pattern = create_element_pattern (shadow_context, width, shadow_size);
+ self->border_pattern = create_element_pattern (border_context, width, border_size);
+ self->outline_pattern = create_element_pattern (outline_context, width, outline_size);
+ }
+
+ self->border_size = border_size;
+ self->shadow_size = shadow_size;
+ self->outline_size = outline_size;
+
+ self->is_cache_valid = TRUE;
+ self->last_direction = direction;
+ self->last_width = width;
+ self->last_height = height;
+ self->last_scale = scale;
+}
+
+static void
+hdy_shadow_helper_dispose (GObject *object)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ hdy_shadow_helper_clear_cache (self);
+
+ if (self->widget)
+ g_clear_object (&self->widget);
+
+ G_OBJECT_CLASS (hdy_shadow_helper_parent_class)->dispose (object);
+}
+
+static void
+hdy_shadow_helper_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ switch (prop_id) {
+ case PROP_WIDGET:
+ g_value_set_object (value, self->widget);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_shadow_helper_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
+
+ switch (prop_id) {
+ case PROP_WIDGET:
+ self->widget = GTK_WIDGET (g_object_ref (g_value_get_object (value)));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_shadow_helper_class_init (HdyShadowHelperClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_shadow_helper_dispose;
+ object_class->get_property = hdy_shadow_helper_get_property;
+ object_class->set_property = hdy_shadow_helper_set_property;
+
+ /**
+ * HdyShadowHelper:widget:
+ *
+ * The widget the shadow will be drawn for. Must not be %NULL
+ *
+ * Since: 0.0.11
+ */
+ props[PROP_WIDGET] =
+ g_param_spec_object ("widget",
+ _("Widget"),
+ _("The widget the shadow will be drawn for"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+static void
+hdy_shadow_helper_init (HdyShadowHelper *self)
+{
+}
+
+/**
+ * hdy_shadow_helper_new:
+ *
+ * Creates a new #HdyShadowHelper object.
+ *
+ * Returns: The newly created #HdyShadowHelper object
+ *
+ * Since: 0.0.12
+ */
+HdyShadowHelper *
+hdy_shadow_helper_new (GtkWidget *widget)
+{
+ return g_object_new (HDY_TYPE_SHADOW_HELPER,
+ "widget", widget,
+ NULL);
+}
+
+/**
+ * hdy_shadow_helper_clear_cache:
+ * @self: a #HdyShadowHelper
+ *
+ * Clears shadow cache. This should be used after a transition is done.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_shadow_helper_clear_cache (HdyShadowHelper *self)
+{
+ if (!self->is_cache_valid)
+ return;
+
+ cairo_pattern_destroy (self->dimming_pattern);
+ cairo_pattern_destroy (self->shadow_pattern);
+ cairo_pattern_destroy (self->border_pattern);
+ cairo_pattern_destroy (self->outline_pattern);
+ self->border_size = 0;
+ self->shadow_size = 0;
+ self->outline_size = 0;
+
+ self->last_direction = 0;
+ self->last_width = 0;
+ self->last_height = 0;
+ self->last_scale = 0;
+
+ self->is_cache_valid = FALSE;
+}
+
+/**
+ * hdy_shadow_helper_draw_shadow:
+ * @self: a #HdyShadowHelper
+ * @cr: a Cairo context to draw to
+ * @width: the width of the shadow rectangle
+ * @height: the height of the shadow rectangle
+ * @progress: transition progress, changes from 0 to 1
+ * @direction: shadow direction
+ *
+ * Draws a transition shadow. For caching to work, @width, @height and
+ * @direction shouldn't change between calls.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_shadow_helper_draw_shadow (HdyShadowHelper *self,
+ cairo_t *cr,
+ gint width,
+ gint height,
+ gdouble progress,
+ GtkPanDirection direction)
+{
+ gdouble remaining_distance, shadow_opacity;
+ gint shadow_size, border_size, outline_size, distance;
+
+ if (progress <= 0 || progress >= 1)
+ return;
+
+ cache_shadow (self, width, height, direction);
+
+ shadow_size = self->shadow_size;
+ border_size = self->border_size;
+ outline_size = self->outline_size;
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_RIGHT:
+ distance = width;
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ case GTK_PAN_DIRECTION_DOWN:
+ distance = height;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ remaining_distance = (1 - progress) * (gdouble) distance;
+ shadow_opacity = 1;
+ if (remaining_distance < shadow_size)
+ shadow_opacity = (remaining_distance / shadow_size);
+
+ cairo_save (cr);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_LEFT:
+ cairo_rectangle (cr, -outline_size, 0, width + outline_size, height);
+ break;
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_rectangle (cr, 0, 0, width + outline_size, height);
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ cairo_rectangle (cr, 0, -outline_size, width, height + outline_size);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_rectangle (cr, 0, 0, width, height + outline_size);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+ cairo_clip (cr);
+ gdk_window_mark_paint_from_clip (gtk_widget_get_window (self->widget), cr);
+
+ cairo_set_source (cr, self->dimming_pattern);
+ cairo_paint_with_alpha (cr, 1 - progress);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, width - shadow_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, height - shadow_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_UP:
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->shadow_pattern);
+ cairo_paint_with_alpha (cr, shadow_opacity);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, shadow_size - border_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, shadow_size - border_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ case GTK_PAN_DIRECTION_UP:
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->border_pattern);
+ cairo_paint (cr);
+
+ switch (direction) {
+ case GTK_PAN_DIRECTION_RIGHT:
+ cairo_translate (cr, border_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ cairo_translate (cr, 0, border_size);
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ cairo_translate (cr, -outline_size, 0);
+ break;
+ case GTK_PAN_DIRECTION_UP:
+ cairo_translate (cr, 0, -outline_size);
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ cairo_set_source (cr, self->outline_pattern);
+ cairo_paint (cr);
+
+ cairo_restore (cr);
+}
diff --git a/subprojects/libhandy/src/hdy-squeezer.c b/subprojects/libhandy/src/hdy-squeezer.c
new file mode 100644
index 0000000..1995661
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-squeezer.c
@@ -0,0 +1,1576 @@
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ * Copyright (C) 2019 Purism SPC
+ *
+ * Author: Alexander Larsson <alexl@redhat.com>
+ * Author: Adrien Plazas <adrien.plazas@puri.sm>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Forked from the GTK+ 3.24.2 GtkStack widget initially written by Alexander
+ * Larsson, and heavily modified for libhandy by Adrien Plazas on behalf of
+ * Purism SPC 2019.
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-squeezer.h"
+
+#include "gtkprogresstrackerprivate.h"
+#include "hdy-animation-private.h"
+#include "hdy-cairo-private.h"
+#include "hdy-css-private.h"
+
+/**
+ * SECTION:hdy-squeezer
+ * @short_description: A best fit container.
+ * @Title: HdySqueezer
+ *
+ * The HdySqueezer widget is a container which only shows the first of its
+ * children that fits in the available size. It is convenient to offer different
+ * widgets to represent the same data with different levels of detail, making
+ * the widget seem to squeeze itself to fit in the available space.
+ *
+ * Transitions between children can be animated as fades. This can be controlled
+ * with hdy_squeezer_set_transition_type().
+ *
+ * # CSS nodes
+ *
+ * #HdySqueezer has a single CSS node with name squeezer.
+ */
+
+/**
+ * HdySqueezerTransitionType:
+ * @HDY_SQUEEZER_TRANSITION_TYPE_NONE: No transition
+ * @HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE: A cross-fade
+ *
+ * These enumeration values describe the possible transitions between children
+ * in a #HdySqueezer widget.
+ */
+
+enum {
+ PROP_0,
+ PROP_HOMOGENEOUS,
+ PROP_VISIBLE_CHILD,
+ PROP_TRANSITION_DURATION,
+ PROP_TRANSITION_TYPE,
+ PROP_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_XALIGN,
+ PROP_YALIGN,
+
+ /* Overridden properties */
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_YALIGN + 1,
+};
+
+enum {
+ CHILD_PROP_0,
+ CHILD_PROP_ENABLED,
+
+ LAST_CHILD_PROP,
+};
+
+typedef struct {
+ GtkWidget *widget;
+ gboolean enabled;
+ GtkWidget *last_focus;
+} HdySqueezerChildInfo;
+
+struct _HdySqueezer
+{
+ GtkContainer parent_instance;
+
+ GList *children;
+
+ GdkWindow* bin_window;
+ GdkWindow* view_window;
+
+ HdySqueezerChildInfo *visible_child;
+
+ gboolean homogeneous;
+
+ HdySqueezerTransitionType transition_type;
+ guint transition_duration;
+
+ HdySqueezerChildInfo *last_visible_child;
+ cairo_surface_t *last_visible_surface;
+ GtkAllocation last_visible_surface_allocation;
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ gint last_visible_widget_width;
+ gint last_visible_widget_height;
+
+ HdySqueezerTransitionType active_transition_type;
+
+ gboolean interpolate_size;
+
+ gfloat xalign;
+ gfloat yalign;
+
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+static GParamSpec *child_props[LAST_CHILD_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdySqueezer, hdy_squeezer, GTK_TYPE_CONTAINER,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static GtkOrientation
+get_orientation (HdySqueezer *self)
+{
+ return self->orientation;
+}
+
+static void
+set_orientation (HdySqueezer *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+static HdySqueezerChildInfo *
+find_child_info_for_widget (HdySqueezer *self,
+ GtkWidget *child)
+{
+ HdySqueezerChildInfo *info;
+ GList *l;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ if (info->widget == child)
+ return info;
+ }
+
+ return NULL;
+}
+
+static void
+hdy_squeezer_progress_updated (HdySqueezer *self)
+{
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+
+ if (!self->homogeneous)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (self->last_visible_surface != NULL) {
+ cairo_surface_destroy (self->last_visible_surface);
+ self->last_visible_surface = NULL;
+ }
+
+ if (self->last_visible_child != NULL) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+ }
+}
+
+static gboolean
+hdy_squeezer_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ if (self->first_frame_skipped) {
+ gtk_progress_tracker_advance_frame (&self->tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ } else {
+ self->first_frame_skipped = TRUE;
+ }
+
+ /* Finish the animation early if the widget isn't mapped anymore. */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&self->tracker);
+
+ hdy_squeezer_progress_updated (HDY_SQUEEZER (widget));
+
+ if (gtk_progress_tracker_get_state (&self->tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_squeezer_schedule_ticks (HdySqueezer *self)
+{
+ if (self->tick_id == 0) {
+ self->tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self), hdy_squeezer_transition_cb, self, NULL);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_squeezer_unschedule_ticks (HdySqueezer *self)
+{
+ if (self->tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), self->tick_id);
+ self->tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_squeezer_start_transition (HdySqueezer *self,
+ HdySqueezerTransitionType transition_type,
+ guint transition_duration)
+{
+ GtkWidget *widget = GTK_WIDGET (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ hdy_get_enable_animations (widget) &&
+ transition_type != HDY_SQUEEZER_TRANSITION_TYPE_NONE &&
+ transition_duration != 0 &&
+ self->last_visible_child != NULL) {
+ self->active_transition_type = transition_type;
+ self->first_frame_skipped = FALSE;
+ hdy_squeezer_schedule_ticks (self);
+ gtk_progress_tracker_start (&self->tracker,
+ self->transition_duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ hdy_squeezer_unschedule_ticks (self);
+ self->active_transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE;
+ gtk_progress_tracker_finish (&self->tracker);
+ }
+
+ hdy_squeezer_progress_updated (HDY_SQUEEZER (widget));
+}
+
+static void
+set_visible_child (HdySqueezer *self,
+ HdySqueezerChildInfo *child_info,
+ HdySqueezerTransitionType transition_type,
+ guint transition_duration)
+{
+ HdySqueezerChildInfo *info;
+ GtkWidget *widget = GTK_WIDGET (self);
+ GList *l;
+ GtkWidget *toplevel;
+ GtkWidget *focus;
+ gboolean contains_focus = FALSE;
+
+ /* If we are being destroyed, do not bother with transitions and
+ * notifications.
+ */
+ if (gtk_widget_in_destruction (widget))
+ return;
+
+ /* If none, pick the first visible. */
+ if (child_info == NULL) {
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ if (gtk_widget_get_visible (info->widget)) {
+ child_info = info;
+ break;
+ }
+ }
+ }
+
+ if (child_info == self->visible_child)
+ return;
+
+ toplevel = gtk_widget_get_toplevel (widget);
+ if (GTK_IS_WINDOW (toplevel)) {
+ focus = gtk_window_get_focus (GTK_WINDOW (toplevel));
+ if (focus &&
+ self->visible_child &&
+ self->visible_child->widget &&
+ gtk_widget_is_ancestor (focus, self->visible_child->widget)) {
+ contains_focus = TRUE;
+
+ if (self->visible_child->last_focus)
+ g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus),
+ (gpointer *)&self->visible_child->last_focus);
+ self->visible_child->last_focus = focus;
+ g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus),
+ (gpointer *)&self->visible_child->last_focus);
+ }
+ }
+
+ if (self->last_visible_child != NULL)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+
+ if (self->last_visible_surface != NULL)
+ cairo_surface_destroy (self->last_visible_surface);
+ self->last_visible_surface = NULL;
+
+ if (self->visible_child && self->visible_child->widget) {
+ if (gtk_widget_is_visible (widget)) {
+ GtkAllocation allocation;
+
+ self->last_visible_child = self->visible_child;
+ gtk_widget_get_allocated_size (self->last_visible_child->widget, &allocation, NULL);
+ self->last_visible_widget_width = allocation.width;
+ self->last_visible_widget_height = allocation.height;
+ } else {
+ gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
+ }
+ }
+
+ self->visible_child = child_info;
+
+ if (child_info) {
+ gtk_widget_set_child_visible (child_info->widget, TRUE);
+
+ if (contains_focus) {
+ if (child_info->last_focus)
+ gtk_widget_grab_focus (child_info->last_focus);
+ else
+ gtk_widget_child_focus (child_info->widget, GTK_DIR_TAB_FORWARD);
+ }
+ }
+
+ if (self->homogeneous)
+ gtk_widget_queue_allocate (widget);
+ else
+ gtk_widget_queue_resize (widget);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+
+ hdy_squeezer_start_transition (self, transition_type, transition_duration);
+}
+
+static void
+stack_child_visibility_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (user_data);
+ GtkWidget *child = GTK_WIDGET (obj);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, child);
+
+ if (self->visible_child == NULL &&
+ gtk_widget_get_visible (child))
+ set_visible_child (self, child_info, self->transition_type, self->transition_duration);
+ else if (self->visible_child == child_info &&
+ !gtk_widget_get_visible (child))
+ set_visible_child (self, NULL, self->transition_type, self->transition_duration);
+
+ if (child_info == self->last_visible_child) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+}
+
+static void
+hdy_squeezer_add (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ g_return_if_fail (child != NULL);
+
+ child_info = g_slice_new (HdySqueezerChildInfo);
+ child_info->widget = child;
+ child_info->enabled = TRUE;
+ child_info->last_focus = NULL;
+
+ self->children = g_list_append (self->children, child_info);
+
+ gtk_widget_set_child_visible (child, FALSE);
+ gtk_widget_set_parent_window (child, self->bin_window);
+ gtk_widget_set_parent (child, GTK_WIDGET (self));
+
+ if (self->bin_window != NULL) {
+ gdk_window_set_events (self->bin_window,
+ gdk_window_get_events (self->bin_window) |
+ gtk_widget_get_events (child));
+ }
+
+ g_signal_connect (child, "notify::visible",
+ G_CALLBACK (stack_child_visibility_notify_cb), self);
+
+ if (self->visible_child == NULL &&
+ gtk_widget_get_visible (child))
+ set_visible_child (self, child_info, self->transition_type, self->transition_duration);
+
+ if (self->visible_child == child_info)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_squeezer_remove (GtkContainer *container,
+ GtkWidget *child)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+ gboolean was_visible;
+
+ child_info = find_child_info_for_widget (self, child);
+ if (child_info == NULL)
+ return;
+
+ self->children = g_list_remove (self->children, child_info);
+
+ g_signal_handlers_disconnect_by_func (child,
+ stack_child_visibility_notify_cb,
+ self);
+
+ was_visible = gtk_widget_get_visible (child);
+
+ child_info->widget = NULL;
+
+ if (self->visible_child == child_info)
+ set_visible_child (self, NULL, self->transition_type, self->transition_duration);
+
+ if (self->last_visible_child == child_info)
+ self->last_visible_child = NULL;
+
+ gtk_widget_unparent (child);
+
+ if (child_info->last_focus)
+ g_object_remove_weak_pointer (G_OBJECT (child_info->last_focus),
+ (gpointer *)&child_info->last_focus);
+
+ g_slice_free (HdySqueezerChildInfo, child_info);
+
+ if (self->homogeneous && was_visible)
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_squeezer_get_property (GObject *object,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ switch (property_id) {
+ case PROP_HOMOGENEOUS:
+ g_value_set_boolean (value, hdy_squeezer_get_homogeneous (self));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_squeezer_get_visible_child (self));
+ break;
+ case PROP_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_squeezer_get_transition_duration (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_squeezer_get_transition_type (self));
+ break;
+ case PROP_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_squeezer_get_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_squeezer_get_interpolate_size (self));
+ break;
+ case PROP_XALIGN:
+ g_value_set_float (value, hdy_squeezer_get_xalign (self));
+ break;
+ case PROP_YALIGN:
+ g_value_set_float (value, hdy_squeezer_get_yalign (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_set_property (GObject *object,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ switch (property_id) {
+ case PROP_HOMOGENEOUS:
+ hdy_squeezer_set_homogeneous (self, g_value_get_boolean (value));
+ break;
+ case PROP_TRANSITION_DURATION:
+ hdy_squeezer_set_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_squeezer_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_squeezer_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_XALIGN:
+ hdy_squeezer_set_xalign (self, g_value_get_float (value));
+ break;
+ case PROP_YALIGN:
+ hdy_squeezer_set_yalign (self, g_value_get_float (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_realize (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+ HdySqueezerChildInfo *info;
+ GList *l;
+
+ gtk_widget_set_realized (widget, TRUE);
+ gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget)));
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask =
+ gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ self->view_window =
+ gdk_window_new (gtk_widget_get_window (GTK_WIDGET (self)),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->view_window);
+
+ attributes.x = 0;
+ attributes.y = 0;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+ attributes.event_mask |= gtk_widget_get_events (info->widget);
+ }
+
+ self->bin_window =
+ gdk_window_new (self->view_window, &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->bin_window);
+
+ for (l = self->children; l != NULL; l = l->next) {
+ info = l->data;
+
+ gtk_widget_set_parent_window (info->widget, self->bin_window);
+ }
+
+ gdk_window_show (self->bin_window);
+}
+
+static void
+hdy_squeezer_unrealize (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ gtk_widget_unregister_window (widget, self->bin_window);
+ gdk_window_destroy (self->bin_window);
+ self->bin_window = NULL;
+ gtk_widget_unregister_window (widget, self->view_window);
+ gdk_window_destroy (self->view_window);
+ self->view_window = NULL;
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_squeezer_map (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->map (widget);
+
+ gdk_window_show (self->view_window);
+}
+
+static void
+hdy_squeezer_unmap (GtkWidget *widget)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ gdk_window_hide (self->view_window);
+
+ GTK_WIDGET_CLASS (hdy_squeezer_parent_class)->unmap (widget);
+}
+
+static void
+hdy_squeezer_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+ GList *l;
+
+ l = self->children;
+ while (l) {
+ child_info = l->data;
+ l = l->next;
+
+ (* callback) (child_info->widget, callback_data);
+ }
+}
+
+static void
+hdy_squeezer_compute_expand (GtkWidget *widget,
+ gboolean *hexpand_p,
+ gboolean *vexpand_p)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ gboolean hexpand, vexpand;
+ HdySqueezerChildInfo *child_info;
+ GtkWidget *child;
+ GList *l;
+
+ hexpand = FALSE;
+ vexpand = FALSE;
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (!hexpand &&
+ gtk_widget_compute_expand (child, GTK_ORIENTATION_HORIZONTAL))
+ hexpand = TRUE;
+
+ if (!vexpand &&
+ gtk_widget_compute_expand (child, GTK_ORIENTATION_VERTICAL))
+ vexpand = TRUE;
+
+ if (hexpand && vexpand)
+ break;
+ }
+
+ *hexpand_p = hexpand;
+ *vexpand_p = vexpand;
+}
+
+static void
+hdy_squeezer_draw_crossfade (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ gdouble progress = gtk_progress_tracker_get_progress (&self->tracker, FALSE);
+
+ cairo_push_group (cr);
+ gtk_container_propagate_draw (GTK_CONTAINER (self),
+ self->visible_child->widget,
+ cr);
+ cairo_save (cr);
+
+ /* Multiply alpha by progress. */
+ cairo_set_source_rgba (cr, 1, 1, 1, progress);
+ cairo_set_operator (cr, CAIRO_OPERATOR_DEST_IN);
+ cairo_paint (cr);
+
+ if (self->last_visible_surface != NULL) {
+ gint width_diff = gtk_widget_get_allocated_width (widget) - self->last_visible_surface_allocation.width;
+ gint height_diff = gtk_widget_get_allocated_height (widget) - self->last_visible_surface_allocation.height;
+
+ cairo_set_source_surface (cr, self->last_visible_surface,
+ width_diff * self->xalign,
+ height_diff * self->yalign);
+ cairo_set_operator (cr, CAIRO_OPERATOR_ADD);
+ cairo_paint_with_alpha (cr, MAX (1.0 - progress, 0));
+ }
+
+ cairo_restore (cr);
+
+ cairo_pop_group_to_source (cr);
+ cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
+ cairo_paint (cr);
+}
+
+static gboolean
+hdy_squeezer_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+
+ if (gtk_cairo_should_draw_window (cr, self->view_window)) {
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ }
+
+ if (self->visible_child) {
+ if (gtk_progress_tracker_get_state (&self->tracker) != GTK_PROGRESS_STATE_AFTER) {
+ if (self->last_visible_surface == NULL &&
+ self->last_visible_child != NULL) {
+ g_autoptr (cairo_t) pattern_cr = NULL;
+
+ gtk_widget_get_allocation (self->last_visible_child->widget,
+ &self->last_visible_surface_allocation);
+ self->last_visible_surface =
+ gdk_window_create_similar_surface (gtk_widget_get_window (widget),
+ CAIRO_CONTENT_COLOR_ALPHA,
+ self->last_visible_surface_allocation.width,
+ self->last_visible_surface_allocation.height);
+ pattern_cr = cairo_create (self->last_visible_surface);
+ /* We don't use propagate_draw here, because we don't want to apply the
+ * bin_window offset.
+ */
+ gtk_widget_draw (self->last_visible_child->widget, pattern_cr);
+ }
+
+ cairo_rectangle (cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+ cairo_clip (cr);
+
+ switch (self->active_transition_type) {
+ case HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE:
+ if (gtk_cairo_should_draw_window (cr, self->bin_window))
+ hdy_squeezer_draw_crossfade (widget, cr);
+ break;
+ case HDY_SQUEEZER_TRANSITION_TYPE_NONE:
+ default:
+ g_assert_not_reached ();
+ }
+
+ } else if (gtk_cairo_should_draw_window (cr, self->bin_window))
+ gtk_container_propagate_draw (GTK_CONTAINER (self),
+ self->visible_child->widget,
+ cr);
+ }
+
+ return FALSE;
+}
+
+static void
+hdy_squeezer_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ HdySqueezerChildInfo *child_info = NULL;
+ GtkWidget *child = NULL;
+ gint child_min;
+ GList *l;
+ GtkAllocation child_allocation;
+
+ hdy_css_size_allocate (widget, allocation);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ if (!child_info->enabled)
+ continue;
+
+ if (self->orientation == GTK_ORIENTATION_VERTICAL) {
+ if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH)
+ gtk_widget_get_preferred_height (child, &child_min, NULL);
+ else
+ gtk_widget_get_preferred_height_for_width (child, allocation->width, &child_min, NULL);
+
+ if (child_min <= allocation->height)
+ break;
+ } else {
+ if (gtk_widget_get_request_mode (child) != GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT)
+ gtk_widget_get_preferred_width (child, &child_min, NULL);
+ else
+ gtk_widget_get_preferred_width_for_height (child, allocation->height, &child_min, NULL);
+
+ if (child_min <= allocation->width)
+ break;
+ }
+ }
+
+ set_visible_child (self, child_info,
+ self->transition_type,
+ self->transition_duration);
+
+ child_allocation.x = 0;
+ child_allocation.y = 0;
+
+ if (gtk_widget_get_realized (widget)) {
+ gdk_window_move_resize (self->view_window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+ gdk_window_move_resize (self->bin_window,
+ 0, 0,
+ allocation->width, allocation->height);
+ }
+
+ if (self->last_visible_child != NULL) {
+ int min, nat;
+ gtk_widget_get_preferred_width (self->last_visible_child->widget, &min, &nat);
+ child_allocation.width = MAX (min, allocation->width);
+ gtk_widget_get_preferred_height_for_width (self->last_visible_child->widget,
+ child_allocation.width,
+ &min, &nat);
+ child_allocation.height = MAX (min, allocation->height);
+
+ gtk_widget_size_allocate (self->last_visible_child->widget, &child_allocation);
+ }
+
+ child_allocation.width = allocation->width;
+ child_allocation.height = allocation->height;
+
+ if (self->visible_child) {
+ int min, nat;
+ GtkAlign valign;
+
+ gtk_widget_get_preferred_height_for_width (self->visible_child->widget,
+ allocation->width,
+ &min, &nat);
+ if (self->interpolate_size) {
+ valign = gtk_widget_get_valign (self->visible_child->widget);
+ child_allocation.height = MAX (nat, allocation->height);
+ if (valign == GTK_ALIGN_END &&
+ child_allocation.height > allocation->height)
+ child_allocation.y -= nat - allocation->height;
+ else if (valign == GTK_ALIGN_CENTER &&
+ child_allocation.height > allocation->height)
+ child_allocation.y -= (nat - allocation->height) / 2;
+ }
+
+ gtk_widget_size_allocate (self->visible_child->widget, &child_allocation);
+ }
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_squeezer_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ HdySqueezer *self = HDY_SQUEEZER (widget);
+ HdySqueezerChildInfo *child_info;
+ GtkWidget *child;
+ gint child_min, child_nat;
+ GList *l;
+
+ *minimum = 0;
+ *natural = 0;
+
+ for (l = self->children; l != NULL; l = l->next) {
+ child_info = l->data;
+ child = child_info->widget;
+
+ if (self->orientation != orientation && !self->homogeneous &&
+ self->visible_child != child_info)
+ continue;
+
+ if (!gtk_widget_get_visible (child))
+ continue;
+
+ /* Disabled children are taken into account when measuring the widget, to
+ * keep its size request and allocation consistent. This avoids the
+ * appearant size and position of a child to changes suddenly when a larger
+ * child gets enabled/disabled.
+ */
+
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ if (for_size < 0)
+ gtk_widget_get_preferred_height (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_height_for_width (child, for_size, &child_min, &child_nat);
+ } else {
+ if (for_size < 0)
+ gtk_widget_get_preferred_width (child, &child_min, &child_nat);
+ else
+ gtk_widget_get_preferred_width_for_height (child, for_size, &child_min, &child_nat);
+ }
+
+ if (self->orientation == orientation)
+ *minimum = *minimum == 0 ? child_min : MIN (*minimum, child_min);
+ else
+ *minimum = MAX (*minimum, child_min);
+ *natural = MAX (*natural, child_nat);
+ }
+
+ if (self->orientation != orientation && !self->homogeneous &&
+ self->interpolate_size &&
+ self->last_visible_child != NULL) {
+ gdouble t = gtk_progress_tracker_get_ease_out_cubic (&self->tracker, FALSE);
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ *minimum = hdy_lerp (self->last_visible_widget_height, *minimum, t);
+ *natural = hdy_lerp (self->last_visible_widget_height, *natural, t);
+ } else {
+ *minimum = hdy_lerp (self->last_visible_widget_width, *minimum, t);
+ *natural = hdy_lerp (self->last_visible_widget_width, *natural, t);
+ }
+ }
+
+ hdy_css_measure (widget, orientation, minimum, natural);
+}
+
+static void
+hdy_squeezer_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_squeezer_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_squeezer_get_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+ if (child_info == NULL) {
+ g_param_value_set_default (pspec, value);
+
+ return;
+ }
+
+ switch (property_id) {
+ case CHILD_PROP_ENABLED:
+ g_value_set_boolean (value, hdy_squeezer_get_child_enabled (self, widget));
+ break;
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_set_child_property (GtkContainer *container,
+ GtkWidget *widget,
+ guint property_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySqueezer *self = HDY_SQUEEZER (container);
+ HdySqueezerChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+ if (child_info == NULL)
+ return;
+
+ switch (property_id) {
+ case CHILD_PROP_ENABLED:
+ hdy_squeezer_set_child_enabled (self, widget, g_value_get_boolean (value));
+ break;
+ default:
+ GTK_CONTAINER_WARN_INVALID_CHILD_PROPERTY_ID (container, property_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_squeezer_dispose (GObject *object)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ self->visible_child = NULL;
+
+ G_OBJECT_CLASS (hdy_squeezer_parent_class)->dispose (object);
+}
+
+static void
+hdy_squeezer_finalize (GObject *object)
+{
+ HdySqueezer *self = HDY_SQUEEZER (object);
+
+ hdy_squeezer_unschedule_ticks (self);
+
+ if (self->last_visible_surface != NULL)
+ cairo_surface_destroy (self->last_visible_surface);
+
+ G_OBJECT_CLASS (hdy_squeezer_parent_class)->finalize (object);
+}
+
+static void
+hdy_squeezer_class_init (HdySqueezerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_squeezer_get_property;
+ object_class->set_property = hdy_squeezer_set_property;
+ object_class->dispose = hdy_squeezer_dispose;
+ object_class->finalize = hdy_squeezer_finalize;
+
+ widget_class->size_allocate = hdy_squeezer_size_allocate;
+ widget_class->draw = hdy_squeezer_draw;
+ widget_class->realize = hdy_squeezer_realize;
+ widget_class->unrealize = hdy_squeezer_unrealize;
+ widget_class->map = hdy_squeezer_map;
+ widget_class->unmap = hdy_squeezer_unmap;
+ widget_class->get_preferred_height = hdy_squeezer_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_squeezer_get_preferred_height_for_width;
+ widget_class->get_preferred_width = hdy_squeezer_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_squeezer_get_preferred_width_for_height;
+ widget_class->compute_expand = hdy_squeezer_compute_expand;
+
+ container_class->add = hdy_squeezer_add;
+ container_class->remove = hdy_squeezer_remove;
+ container_class->forall = hdy_squeezer_forall;
+ container_class->set_child_property = hdy_squeezer_set_child_property;
+ container_class->get_child_property = hdy_squeezer_get_child_property;
+ gtk_container_class_handle_border_width (container_class);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ props[PROP_HOMOGENEOUS] =
+ g_param_spec_boolean ("homogeneous",
+ _("Homogeneous"),
+ _("Homogeneous sizing"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible in the squeezer"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_DURATION] =
+ g_param_spec_uint ("transition-duration",
+ _("Transition duration"),
+ _("The animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition"),
+ HDY_TYPE_SQUEEZER_TRANSITION_TYPE,
+ HDY_SQUEEZER_TRANSITION_TYPE_NONE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("transition-running",
+ _("Transition running"),
+ _("Whether or not the transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySqueezer:xalign:
+ *
+ * The xalign property determines the horizontal aligment of the children
+ * inside the squeezer's size allocation.
+ * Compare this to #GtkWidget:halign, which determines how the squeezer's size
+ * allocation is positioned in the space available for the squeezer.
+ * The range goes from 0 (start) to 1 (end).
+ *
+ * This will affect the position of children too wide to fit in the squeezer
+ * as they are fading out.
+ *
+ * Since: 1.0
+ */
+ props[PROP_XALIGN] =
+ g_param_spec_float ("xalign",
+ _("X align"),
+ _("The horizontal alignment, from 0 (start) to 1 (end)"),
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySqueezer:yalign:
+ *
+ * The yalign property determines the vertical aligment of the children inside
+ * the squeezer's size allocation.
+ * Compare this to #GtkWidget:valign, which determines how the squeezer's size
+ * allocation is positioned in the space available for the squeezer.
+ * The range goes from 0 (top) to 1 (bottom).
+ *
+ * This will affect the position of children too tall to fit in the squeezer
+ * as they are fading out.
+ *
+ * Since: 1.0
+ */
+ props[PROP_YALIGN] =
+ g_param_spec_float ("yalign",
+ _("Y align"),
+ _("The vertical alignment, from 0 (top) to 1 (bottom)"),
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ child_props[CHILD_PROP_ENABLED] =
+ g_param_spec_boolean ("enabled",
+ _("Enabled"),
+ _("Whether the child can be picked or should be ignored when looking for the child fitting the available size best"),
+ TRUE,
+ G_PARAM_READWRITE);
+
+ gtk_container_class_install_child_properties (container_class, LAST_CHILD_PROP, child_props);
+
+ gtk_widget_class_set_css_name (widget_class, "squeezer");
+}
+
+static void
+hdy_squeezer_init (HdySqueezer *self)
+{
+
+ gtk_widget_set_has_window (GTK_WIDGET (self), FALSE);
+
+ self->homogeneous = TRUE;
+ self->transition_duration = 200;
+ self->transition_type = HDY_SQUEEZER_TRANSITION_TYPE_NONE;
+ self->xalign = 0.5;
+ self->yalign = 0.5;
+}
+
+/**
+ * hdy_squeezer_new:
+ *
+ * Creates a new #HdySqueezer container.
+ *
+ * Returns: a new #HdySqueezer
+ */
+GtkWidget *
+hdy_squeezer_new (void)
+{
+ return g_object_new (HDY_TYPE_SQUEEZER, NULL);
+}
+
+/**
+ * hdy_squeezer_get_homogeneous:
+ * @self: a #HdySqueezer
+ *
+ * Gets whether @self is homogeneous.
+ *
+ * See hdy_squeezer_set_homogeneous().
+ *
+ * Returns: %TRUE if @self is homogeneous, %FALSE is not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_squeezer_get_homogeneous (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return self->homogeneous;
+}
+
+/**
+ * hdy_squeezer_set_homogeneous:
+ * @self: a #HdySqueezer
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets @self to be homogeneous or not. If it is homogeneous, @self will request
+ * the same size for all its children for its opposite orientation, e.g. if
+ * @self is oriented horizontally and is homogeneous, it will request the same
+ * height for all its children. If it isn't, @self may change size when a
+ * different child becomes visible.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_squeezer_set_homogeneous (HdySqueezer *self,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ homogeneous = !!homogeneous;
+
+ if (self->homogeneous == homogeneous)
+ return;
+
+ self->homogeneous = homogeneous;
+
+ if (gtk_widget_get_visible (GTK_WIDGET(self)))
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOMOGENEOUS]);
+}
+
+/**
+ * hdy_squeezer_get_transition_duration:
+ * @self: a #HdySqueezer
+ *
+ * Gets the amount of time (in milliseconds) that transitions between children
+ * in @self will take.
+ *
+ * Returns: the transition duration
+ */
+guint
+hdy_squeezer_get_transition_duration (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0);
+
+ return self->transition_duration;
+}
+
+/**
+ * hdy_squeezer_set_transition_duration:
+ * @self: a #HdySqueezer
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self will take.
+ */
+void
+hdy_squeezer_set_transition_duration (HdySqueezer *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ if (self->transition_duration == duration)
+ return;
+
+ self->transition_duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_squeezer_get_transition_type:
+ * @self: a #HdySqueezer
+ *
+ * Gets the type of animation that will be used for transitions between children
+ * in @self.
+ *
+ * Returns: the current transition type of @self
+ */
+HdySqueezerTransitionType
+hdy_squeezer_get_transition_type (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), HDY_SQUEEZER_TRANSITION_TYPE_NONE);
+
+ return self->transition_type;
+}
+
+/**
+ * hdy_squeezer_set_transition_type:
+ * @self: a #HdySqueezer
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between children
+ * in @self. Available types include various kinds of fades and slides.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the child that is about to become
+ * current.
+ */
+void
+hdy_squeezer_set_transition_type (HdySqueezer *self,
+ HdySqueezerTransitionType transition)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ if (self->transition_type == transition)
+ return;
+
+ self->transition_type = transition;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSITION_TYPE]);
+}
+
+/**
+ * hdy_squeezer_get_transition_running:
+ * @self: a #HdySqueezer
+ *
+ * Gets whether @self is currently in a transition from one child to another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ */
+gboolean
+hdy_squeezer_get_transition_running (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return (self->tick_id != 0);
+}
+
+/**
+ * hdy_squeezer_get_interpolate_size:
+ * @self: A #HdySqueezer
+ *
+ * Gets whether @self should interpolate its size on visible child change.
+ *
+ * See hdy_squeezer_set_interpolate_size().
+ *
+ * Returns: %TRUE if @self interpolates its size on visible child change, %FALSE if not
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_squeezer_get_interpolate_size (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+
+ return self->interpolate_size;
+}
+
+/**
+ * hdy_squeezer_set_interpolate_size:
+ * @self: A #HdySqueezer
+ * @interpolate_size: %TRUE to interpolate the size
+ *
+ * Sets whether or not @self will interpolate the size of its opposing
+ * orientation when changing the visible child. If %TRUE, @self will interpolate
+ * its size between the one of the previous visible child and the one of the new
+ * visible child, according to the set transition duration and the orientation,
+ * e.g. if @self is horizontal, it will interpolate the its height.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_squeezer_set_interpolate_size (HdySqueezer *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ interpolate_size = !!interpolate_size;
+
+ if (self->interpolate_size == interpolate_size)
+ return;
+
+ self->interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
+
+/**
+ * hdy_squeezer_get_visible_child:
+ * @self: a #HdySqueezer
+ *
+ * Gets the currently visible child of @self, or %NULL if there are no visible
+ * children.
+ *
+ * Returns: (transfer none) (nullable): the visible child of the #HdySqueezer
+ */
+GtkWidget *
+hdy_squeezer_get_visible_child (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), NULL);
+
+ return self->visible_child ? self->visible_child->widget : NULL;
+}
+
+/**
+ * hdy_squeezer_get_child_enabled:
+ * @self: a #HdySqueezer
+ * @child: a child of @self
+ *
+ * Gets whether @child is enabled.
+ *
+ * See hdy_squeezer_set_child_enabled().
+ *
+ * Returns: %TRUE if @child is enabled, %FALSE otherwise.
+ */
+gboolean
+hdy_squeezer_get_child_enabled (HdySqueezer *self,
+ GtkWidget *child)
+{
+ HdySqueezerChildInfo *child_info;
+
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), FALSE);
+ g_return_val_if_fail (GTK_IS_WIDGET (child), FALSE);
+
+ child_info = find_child_info_for_widget (self, child);
+
+ g_return_val_if_fail (child_info != NULL, FALSE);
+
+ return child_info->enabled;
+}
+
+/**
+ * hdy_squeezer_set_child_enabled:
+ * @self: a #HdySqueezer
+ * @child: a child of @self
+ * @enabled: %TRUE to enable the child, %FALSE to disable it
+ *
+ * Make @self enable or disable @child. If a child is disabled, it will be
+ * ignored when looking for the child fitting the available size best. This
+ * allows to programmatically and prematurely hide a child of @self even if it
+ * fits in the available space.
+ *
+ * This can be used e.g. to ensure a certain child is hidden below a certain
+ * window width, or any other constraint you find suitable.
+ */
+void
+hdy_squeezer_set_child_enabled (HdySqueezer *self,
+ GtkWidget *child,
+ gboolean enabled)
+{
+ HdySqueezerChildInfo *child_info;
+
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+ g_return_if_fail (GTK_IS_WIDGET (child));
+
+ child_info = find_child_info_for_widget (self, child);
+
+ g_return_if_fail (child_info != NULL);
+
+ enabled = !!enabled;
+
+ if (child_info->enabled == enabled)
+ return;
+
+ child_info->enabled = enabled;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_squeezer_get_xalign:
+ * @self: a #HdySqueezer
+ *
+ * Gets the #HdySqueezer:xalign property for @self.
+ *
+ * Returns: the xalign property
+ *
+ * Since: 1.0
+ */
+gfloat
+hdy_squeezer_get_xalign (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5);
+
+ return self->xalign;
+}
+
+/**
+ * hdy_squeezer_set_xalign:
+ * @self: a #HdySqueezer
+ * @xalign: the new xalign value, between 0 and 1
+ *
+ * Sets the #HdySqueezer:xalign property for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_squeezer_set_xalign (HdySqueezer *self,
+ gfloat xalign)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ xalign = CLAMP (xalign, 0.0, 1.0);
+
+ if (self->xalign == xalign)
+ return;
+
+ self->xalign = xalign;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_XALIGN]);
+}
+
+/**
+ * hdy_squeezer_get_yalign:
+ * @self: a #HdySqueezer
+ *
+ * Gets the #HdySqueezer:yalign property for @self.
+ *
+ * Returns: the yalign property
+ *
+ * Since: 1.0
+ */
+gfloat
+hdy_squeezer_get_yalign (HdySqueezer *self)
+{
+ g_return_val_if_fail (HDY_IS_SQUEEZER (self), 0.5);
+
+ return self->yalign;
+}
+
+/**
+ * hdy_squeezer_set_yalign:
+ * @self: a #HdySqueezer
+ * @yalign: the new yalign value, between 0 and 1
+ *
+ * Sets the #HdySqueezer:yalign property for @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_squeezer_set_yalign (HdySqueezer *self,
+ gfloat yalign)
+{
+ g_return_if_fail (HDY_IS_SQUEEZER (self));
+
+ yalign = CLAMP (yalign, 0.0, 1.0);
+
+ if (self->yalign == yalign)
+ return;
+
+ self->yalign = yalign;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_YALIGN]);
+}
diff --git a/subprojects/libhandy/src/hdy-squeezer.h b/subprojects/libhandy/src/hdy-squeezer.h
new file mode 100644
index 0000000..9b98116
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-squeezer.h
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SQUEEZER (hdy_squeezer_get_type ())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySqueezer, hdy_squeezer, HDY, SQUEEZER, GtkContainer)
+
+typedef enum {
+ HDY_SQUEEZER_TRANSITION_TYPE_NONE,
+ HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE,
+} HdySqueezerTransitionType;
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_squeezer_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_homogeneous (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_homogeneous (HdySqueezer *self,
+ gboolean homogeneous);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_squeezer_get_transition_duration (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_transition_duration (HdySqueezer *self,
+ guint duration);
+
+HDY_AVAILABLE_IN_ALL
+HdySqueezerTransitionType hdy_squeezer_get_transition_type (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_transition_type (HdySqueezer *self,
+ HdySqueezerTransitionType transition);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_transition_running (HdySqueezer *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_interpolate_size (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_interpolate_size (HdySqueezer *self,
+ gboolean interpolate_size);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_squeezer_get_visible_child (HdySqueezer *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_squeezer_get_child_enabled (HdySqueezer *self,
+ GtkWidget *child);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_child_enabled (HdySqueezer *self,
+ GtkWidget *child,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gfloat hdy_squeezer_get_xalign (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_xalign (HdySqueezer *self,
+ gfloat xalign);
+
+HDY_AVAILABLE_IN_ALL
+gfloat hdy_squeezer_get_yalign (HdySqueezer *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_squeezer_set_yalign (HdySqueezer *self,
+ gfloat yalign);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-stackable-box-private.h b/subprojects/libhandy/src/hdy-stackable-box-private.h
new file mode 100644
index 0000000..d72c75a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-stackable-box-private.h
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_STACKABLE_BOX (hdy_stackable_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyStackableBox, hdy_stackable_box, HDY, STACKABLE_BOX, GObject)
+
+typedef enum {
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER,
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER,
+ HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE,
+} HdyStackableBoxTransitionType;
+
+HdyStackableBox *hdy_stackable_box_new (GtkContainer *container,
+ GtkContainerClass *klass,
+ gboolean can_unfold);
+gboolean hdy_stackable_box_get_folded (HdyStackableBox *self);
+GtkWidget *hdy_stackable_box_get_visible_child (HdyStackableBox *self);
+void hdy_stackable_box_set_visible_child (HdyStackableBox *self,
+ GtkWidget *visible_child);
+const gchar *hdy_stackable_box_get_visible_child_name (HdyStackableBox *self);
+void hdy_stackable_box_set_visible_child_name (HdyStackableBox *self,
+ const gchar *name);
+gboolean hdy_stackable_box_get_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation);
+void hdy_stackable_box_set_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous);
+HdyStackableBoxTransitionType hdy_stackable_box_get_transition_type (HdyStackableBox *self);
+void hdy_stackable_box_set_transition_type (HdyStackableBox *self,
+ HdyStackableBoxTransitionType transition);
+
+guint hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self);
+void hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self,
+ guint duration);
+
+guint hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self);
+void hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self,
+ guint duration);
+gboolean hdy_stackable_box_get_child_transition_running (HdyStackableBox *self);
+gboolean hdy_stackable_box_get_interpolate_size (HdyStackableBox *self);
+void hdy_stackable_box_set_interpolate_size (HdyStackableBox *self,
+ gboolean interpolate_size);
+gboolean hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self);
+void hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self,
+ gboolean can_swipe_back);
+gboolean hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self);
+void hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self,
+ gboolean can_swipe_forward);
+
+GtkWidget *hdy_stackable_box_get_adjacent_child (HdyStackableBox *self,
+ HdyNavigationDirection direction);
+gboolean hdy_stackable_box_navigate (HdyStackableBox *self,
+ HdyNavigationDirection direction);
+
+GtkWidget *hdy_stackable_box_get_child_by_name (HdyStackableBox *self,
+ const gchar *name);
+
+GtkOrientation hdy_stackable_box_get_orientation (HdyStackableBox *self);
+void hdy_stackable_box_set_orientation (HdyStackableBox *self,
+ GtkOrientation orientation);
+
+const gchar *hdy_stackable_box_get_child_name (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_set_child_name (HdyStackableBox *self,
+ GtkWidget *widget,
+ const gchar *name);
+gboolean hdy_stackable_box_get_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_set_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget,
+ gboolean navigatable);
+
+void hdy_stackable_box_switch_child (HdyStackableBox *self,
+ guint index,
+ gint64 duration);
+
+HdySwipeTracker *hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self);
+gdouble hdy_stackable_box_get_distance (HdyStackableBox *self);
+gdouble *hdy_stackable_box_get_snap_points (HdyStackableBox *self,
+ gint *n_snap_points);
+gdouble hdy_stackable_box_get_progress (HdyStackableBox *self);
+gdouble hdy_stackable_box_get_cancel_progress (HdyStackableBox *self);
+void hdy_stackable_box_get_swipe_area (HdyStackableBox *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+void hdy_stackable_box_add (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_remove (HdyStackableBox *self,
+ GtkWidget *widget);
+void hdy_stackable_box_forall (HdyStackableBox *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data);
+
+void hdy_stackable_box_measure (HdyStackableBox *self,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline);
+void hdy_stackable_box_size_allocate (HdyStackableBox *self,
+ GtkAllocation *allocation);
+gboolean hdy_stackable_box_draw (HdyStackableBox *self,
+ cairo_t *cr);
+void hdy_stackable_box_realize (HdyStackableBox *self);
+void hdy_stackable_box_unrealize (HdyStackableBox *self);
+void hdy_stackable_box_map (HdyStackableBox *self);
+void hdy_stackable_box_unmap (HdyStackableBox *self);
+void hdy_stackable_box_direction_changed (HdyStackableBox *self,
+ GtkTextDirection previous_direction);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-stackable-box.c b/subprojects/libhandy/src/hdy-stackable-box.c
new file mode 100644
index 0000000..4eb8fa3
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-stackable-box.c
@@ -0,0 +1,3151 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "gtkprogresstrackerprivate.h"
+#include "hdy-animation-private.h"
+#include "hdy-enums-private.h"
+#include "hdy-stackable-box-private.h"
+#include "hdy-shadow-helper-private.h"
+#include "hdy-swipeable.h"
+
+/**
+ * PRIVATE:hdy-stackable-box
+ * @short_description: An adaptive container acting like a box or a stack.
+ * @Title: HdyStackableBox
+ * @stability: Private
+ * @See_also: #HdyDeck, #HdyLeaflet
+ *
+ * The #HdyStackableBox object can arrange the widgets it manages like #GtkBox
+ * does or like a #GtkStack does, adapting to size changes by switching between
+ * the two modes. These modes are named respectively “unfoled” and “folded”.
+ *
+ * When there is enough space the children are displayed side by side, otherwise
+ * only one is displayed. The threshold is dictated by the preferred minimum
+ * sizes of the children.
+ *
+ * #HdyStackableBox is used as an internal implementation of #HdyDeck and
+ * #HdyLeaflet.
+ *
+ * Since: 1.0
+ */
+
+/**
+ * HdyStackableBoxTransitionType:
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER: Cover the old page or uncover the new page, sliding from or towards the end according to orientation, text direction and children order
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER: Uncover the new page or cover the old page, sliding from or towards the start according to orientation, text direction and children order
+ * @HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE: Slide from left, right, up or down according to the orientation, text direction and the children order
+ *
+ * This enumeration value describes the possible transitions between modes and
+ * children in a #HdyStackableBox widget.
+ *
+ * New values may be added to this enumeration over time.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_FOLDED,
+ PROP_HHOMOGENEOUS_FOLDED,
+ PROP_VHOMOGENEOUS_FOLDED,
+ PROP_HHOMOGENEOUS_UNFOLDED,
+ PROP_VHOMOGENEOUS_UNFOLDED,
+ PROP_VISIBLE_CHILD,
+ PROP_VISIBLE_CHILD_NAME,
+ PROP_TRANSITION_TYPE,
+ PROP_MODE_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_DURATION,
+ PROP_CHILD_TRANSITION_RUNNING,
+ PROP_INTERPOLATE_SIZE,
+ PROP_CAN_SWIPE_BACK,
+ PROP_CAN_SWIPE_FORWARD,
+ PROP_ORIENTATION,
+ LAST_PROP,
+};
+
+#define HDY_FOLD_UNFOLDED FALSE
+#define HDY_FOLD_FOLDED TRUE
+#define HDY_FOLD_MAX 2
+#define GTK_ORIENTATION_MAX 2
+#define HDY_SWIPE_BORDER 16
+
+typedef struct _HdyStackableBoxChildInfo HdyStackableBoxChildInfo;
+
+struct _HdyStackableBoxChildInfo
+{
+ GtkWidget *widget;
+ GdkWindow *window;
+ gchar *name;
+ gboolean navigatable;
+
+ /* Convenience storage for per-child temporary frequently computed values. */
+ GtkAllocation alloc;
+ GtkRequisition min;
+ GtkRequisition nat;
+ gboolean visible;
+};
+
+struct _HdyStackableBox
+{
+ GObject parent;
+
+ GtkContainer *container;
+ GtkContainerClass *klass;
+ gboolean can_unfold;
+
+ GList *children;
+ /* It is probably cheaper to store and maintain a reversed copy of the
+ * children list that to reverse the list every time we need to allocate or
+ * draw children for RTL languages on a horizontal widget.
+ */
+ GList *children_reversed;
+ HdyStackableBoxChildInfo *visible_child;
+ HdyStackableBoxChildInfo *last_visible_child;
+
+ GdkWindow* view_window;
+
+ gboolean folded;
+
+ gboolean homogeneous[HDY_FOLD_MAX][GTK_ORIENTATION_MAX];
+
+ GtkOrientation orientation;
+
+ HdyStackableBoxTransitionType transition_type;
+
+ HdySwipeTracker *tracker;
+
+ struct {
+ guint duration;
+
+ gdouble current_pos;
+ gdouble source_pos;
+ gdouble target_pos;
+
+ gdouble start_progress;
+ gdouble end_progress;
+ guint tick_id;
+ GtkProgressTracker tracker;
+ } mode_transition;
+
+ /* Child transition variables. */
+ struct {
+ guint duration;
+
+ gdouble progress;
+ gdouble start_progress;
+ gdouble end_progress;
+
+ gboolean is_gesture_active;
+ gboolean is_cancelled;
+
+ guint tick_id;
+ GtkProgressTracker tracker;
+ gboolean first_frame_skipped;
+
+ gboolean interpolate_size;
+ gboolean can_swipe_back;
+ gboolean can_swipe_forward;
+
+ GtkPanDirection active_direction;
+ gboolean is_direct_swipe;
+ gint swipe_direction;
+ } child_transition;
+
+ HdyShadowHelper *shadow_helper;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static gint HOMOGENEOUS_PROP[HDY_FOLD_MAX][GTK_ORIENTATION_MAX] = {
+ { PROP_HHOMOGENEOUS_UNFOLDED, PROP_VHOMOGENEOUS_UNFOLDED},
+ { PROP_HHOMOGENEOUS_FOLDED, PROP_VHOMOGENEOUS_FOLDED},
+};
+
+G_DEFINE_TYPE (HdyStackableBox, hdy_stackable_box, G_TYPE_OBJECT);
+
+static void
+free_child_info (HdyStackableBoxChildInfo *child_info)
+{
+ g_free (child_info->name);
+ g_free (child_info);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (HdyStackableBoxChildInfo, free_child_info)
+
+static HdyStackableBoxChildInfo *
+find_child_info_for_widget (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info->widget == widget)
+ return child_info;
+ }
+
+ return NULL;
+}
+
+static HdyStackableBoxChildInfo *
+find_child_info_for_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (g_strcmp0 (child_info->name, name) == 0)
+ return child_info;
+ }
+
+ return NULL;
+}
+
+static GList *
+get_directed_children (HdyStackableBox *self)
+{
+ return self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL ?
+ self->children_reversed : self->children;
+}
+
+static GtkPanDirection
+get_pan_direction (HdyStackableBox *self,
+ gboolean new_child_first)
+{
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL)
+ return new_child_first ? GTK_PAN_DIRECTION_LEFT : GTK_PAN_DIRECTION_RIGHT;
+ else
+ return new_child_first ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT;
+ }
+ else
+ return new_child_first ? GTK_PAN_DIRECTION_DOWN : GTK_PAN_DIRECTION_UP;
+}
+
+static gint
+get_child_window_x (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child_info,
+ gint width)
+{
+ gboolean is_rtl;
+ gint rtl_multiplier;
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ if (self->child_transition.active_direction != GTK_PAN_DIRECTION_LEFT &&
+ self->child_transition.active_direction != GTK_PAN_DIRECTION_RIGHT)
+ return 0;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+ rtl_multiplier = is_rtl ? -1 : 1;
+
+ if ((self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT) == is_rtl) {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return width * (1 - self->child_transition.progress) * rtl_multiplier;
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return -width * self->child_transition.progress * rtl_multiplier;
+ } else {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return -width * (1 - self->child_transition.progress) * rtl_multiplier;
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return width * self->child_transition.progress * rtl_multiplier;
+ }
+
+ return 0;
+}
+
+static gint
+get_child_window_y (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child_info,
+ gint height)
+{
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ if (self->child_transition.active_direction != GTK_PAN_DIRECTION_UP &&
+ self->child_transition.active_direction != GTK_PAN_DIRECTION_DOWN)
+ return 0;
+
+ if (self->child_transition.active_direction == GTK_PAN_DIRECTION_UP) {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return height * (1 - self->child_transition.progress);
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return -height * self->child_transition.progress;
+ } else {
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->visible_child)
+ return -height * (1 - self->child_transition.progress);
+
+ if ((self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE) &&
+ child_info == self->last_visible_child)
+ return height * self->child_transition.progress;
+ }
+
+ return 0;
+}
+
+static void
+hdy_stackable_box_child_progress_updated (HdyStackableBox *self)
+{
+ gtk_widget_queue_draw (GTK_WIDGET (self->container));
+
+ if (!self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] ||
+ !self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL])
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+ else
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ if (self->child_transition.is_cancelled) {
+ if (self->last_visible_child != NULL) {
+ if (self->folded) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, TRUE);
+ gtk_widget_set_child_visible (self->visible_child->widget, FALSE);
+ }
+ self->visible_child = self->last_visible_child;
+ self->last_visible_child = NULL;
+ }
+
+ self->child_transition.is_cancelled = FALSE;
+
+ g_object_freeze_notify (G_OBJECT (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]);
+ g_object_thaw_notify (G_OBJECT (self));
+ } else {
+ if (self->last_visible_child != NULL) {
+ if (self->folded)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+ self->child_transition.swipe_direction = 0;
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+ }
+}
+
+static gboolean
+hdy_stackable_box_child_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ gdouble progress;
+
+ if (self->child_transition.first_frame_skipped) {
+ gtk_progress_tracker_advance_frame (&self->child_transition.tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ progress = gtk_progress_tracker_get_ease_out_cubic (&self->child_transition.tracker, FALSE);
+ self->child_transition.progress =
+ hdy_lerp (self->child_transition.start_progress,
+ self->child_transition.end_progress, progress);
+ } else
+ self->child_transition.first_frame_skipped = TRUE;
+
+ /* Finish animation early if not mapped anymore */
+ if (!gtk_widget_get_mapped (widget))
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+
+ hdy_stackable_box_child_progress_updated (self);
+
+ if (gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->child_transition.tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_stackable_box_schedule_child_ticks (HdyStackableBox *self)
+{
+ if (self->child_transition.tick_id == 0) {
+ self->child_transition.tick_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self->container),
+ hdy_stackable_box_child_transition_cb,
+ self, NULL);
+ if (!self->child_transition.is_gesture_active)
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_stackable_box_unschedule_child_ticks (HdyStackableBox *self)
+{
+ if (self->child_transition.tick_id != 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->child_transition.tick_id);
+ self->child_transition.tick_id = 0;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+}
+
+static void
+hdy_stackable_box_stop_child_transition (HdyStackableBox *self)
+{
+ hdy_stackable_box_unschedule_child_ticks (self);
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ if (self->last_visible_child != NULL) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, FALSE);
+ self->last_visible_child = NULL;
+ }
+
+ self->child_transition.swipe_direction = 0;
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+}
+
+static void
+hdy_stackable_box_start_child_transition (HdyStackableBox *self,
+ guint transition_duration,
+ GtkPanDirection transition_direction)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (gtk_widget_get_mapped (widget) &&
+ ((hdy_get_enable_animations (widget) &&
+ transition_duration != 0) ||
+ self->child_transition.is_gesture_active) &&
+ self->last_visible_child != NULL &&
+ /* Don't animate child transition when a mode transition is ongoing. */
+ self->mode_transition.tick_id == 0) {
+ self->child_transition.active_direction = transition_direction;
+ self->child_transition.first_frame_skipped = FALSE;
+ self->child_transition.start_progress = 0;
+ self->child_transition.end_progress = 1;
+ self->child_transition.progress = 0;
+ self->child_transition.is_cancelled = FALSE;
+
+ if (!self->child_transition.is_gesture_active) {
+ hdy_stackable_box_schedule_child_ticks (self);
+ gtk_progress_tracker_start (&self->child_transition.tracker,
+ transition_duration * 1000,
+ 0,
+ 1.0);
+ }
+ }
+ else {
+ hdy_stackable_box_unschedule_child_ticks (self);
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ }
+
+ hdy_stackable_box_child_progress_updated (self);
+}
+
+static void
+set_visible_child_info (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *new_visible_child,
+ HdyStackableBoxTransitionType transition_type,
+ guint transition_duration,
+ gboolean emit_child_switched)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+ GtkPanDirection transition_direction = GTK_PAN_DIRECTION_LEFT;
+
+ /* If we are being destroyed, do not bother with transitions and
+ * notifications.
+ */
+ if (gtk_widget_in_destruction (widget))
+ return;
+
+ /* If none, pick first visible. */
+ if (new_visible_child == NULL) {
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (gtk_widget_get_visible (child_info->widget)) {
+ new_visible_child = child_info;
+
+ break;
+ }
+ }
+ }
+
+ if (new_visible_child == self->visible_child)
+ return;
+
+ /* FIXME Probably copied from Gtk Stack, should check whether it's needed. */
+ /* toplevel = gtk_widget_get_toplevel (widget); */
+ /* if (GTK_IS_WINDOW (toplevel)) { */
+ /* focus = gtk_window_get_focus (GTK_WINDOW (toplevel)); */
+ /* if (focus && */
+ /* self->visible_child && */
+ /* self->visible_child->widget && */
+ /* gtk_widget_is_ancestor (focus, self->visible_child->widget)) { */
+ /* contains_focus = TRUE; */
+
+ /* if (self->visible_child->last_focus) */
+ /* g_object_remove_weak_pointer (G_OBJECT (self->visible_child->last_focus), */
+ /* (gpointer *)&self->visible_child->last_focus); */
+ /* self->visible_child->last_focus = focus; */
+ /* g_object_add_weak_pointer (G_OBJECT (self->visible_child->last_focus), */
+ /* (gpointer *)&self->visible_child->last_focus); */
+ /* } */
+ /* } */
+
+ if (self->last_visible_child)
+ gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded);
+ self->last_visible_child = NULL;
+
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+
+ if (self->visible_child && self->visible_child->widget) {
+ if (gtk_widget_is_visible (widget))
+ self->last_visible_child = self->visible_child;
+ else
+ gtk_widget_set_child_visible (self->visible_child->widget, !self->folded);
+ }
+
+ /* FIXME This comes from GtkStack and should be adapted. */
+ /* hdy_stackable_box_accessible_update_visible_child (stack, */
+ /* self->visible_child ? self->visible_child->widget : NULL, */
+ /* new_visible_child ? new_visible_child->widget : NULL); */
+
+ self->visible_child = new_visible_child;
+
+ if (new_visible_child) {
+ gtk_widget_set_child_visible (new_visible_child->widget, TRUE);
+
+ /* FIXME This comes from GtkStack and should be adapted. */
+ /* if (contains_focus) { */
+ /* if (new_visible_child->last_focus) */
+ /* gtk_widget_grab_focus (new_visible_child->last_focus); */
+ /* else */
+ /* gtk_widget_child_focus (new_visible_child->widget, GTK_DIR_TAB_FORWARD); */
+ /* } */
+ }
+
+ if (new_visible_child == NULL || self->last_visible_child == NULL)
+ transition_duration = 0;
+ else {
+ gboolean new_first = FALSE;
+ for (children = self->children; children; children = children->next) {
+ if (new_visible_child == children->data) {
+ new_first = TRUE;
+
+ break;
+ }
+ if (self->last_visible_child == children->data)
+ break;
+ }
+
+ transition_direction = get_pan_direction (self, new_first);
+ }
+
+ if (self->folded) {
+ if (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] &&
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL])
+ gtk_widget_queue_allocate (widget);
+ else
+ gtk_widget_queue_resize (widget);
+
+ hdy_stackable_box_start_child_transition (self, transition_duration, transition_direction);
+ }
+
+ if (emit_child_switched) {
+ gint index = 0;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->navigatable)
+ continue;
+
+ if (child_info == new_visible_child)
+ break;
+
+ index++;
+ }
+
+ hdy_swipeable_emit_child_switched (HDY_SWIPEABLE (self->container), index,
+ transition_duration);
+ }
+
+ g_object_freeze_notify (G_OBJECT (self));
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD]);
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE_CHILD_NAME]);
+ g_object_thaw_notify (G_OBJECT (self));
+}
+
+static void
+hdy_stackable_box_set_position (HdyStackableBox *self,
+ gdouble pos)
+{
+ self->mode_transition.current_pos = pos;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self->container));
+}
+
+static void
+hdy_stackable_box_mode_progress_updated (HdyStackableBox *self)
+{
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ hdy_shadow_helper_clear_cache (self->shadow_helper);
+}
+
+static gboolean
+hdy_stackable_box_mode_transition_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ gdouble ease;
+
+ gtk_progress_tracker_advance_frame (&self->mode_transition.tracker,
+ gdk_frame_clock_get_frame_time (frame_clock));
+ ease = gtk_progress_tracker_get_ease_out_cubic (&self->mode_transition.tracker, FALSE);
+ hdy_stackable_box_set_position (self,
+ self->mode_transition.source_pos + (ease * (self->mode_transition.target_pos - self->mode_transition.source_pos)));
+
+ hdy_stackable_box_mode_progress_updated (self);
+
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) == GTK_PROGRESS_STATE_AFTER) {
+ self->mode_transition.tick_id = 0;
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static void
+hdy_stackable_box_start_mode_transition (HdyStackableBox *self,
+ gdouble target)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (self->mode_transition.target_pos == target)
+ return;
+
+ self->mode_transition.target_pos = target;
+ /* FIXME PROP_REVEAL_CHILD needs to be implemented. */
+ /* g_object_notify_by_pspec (G_OBJECT (revealer), props[PROP_REVEAL_CHILD]); */
+
+ hdy_stackable_box_stop_child_transition (self);
+
+ if (gtk_widget_get_mapped (widget) &&
+ self->mode_transition.duration != 0 &&
+ hdy_get_enable_animations (widget) &&
+ self->can_unfold) {
+ self->mode_transition.source_pos = self->mode_transition.current_pos;
+ if (self->mode_transition.tick_id == 0)
+ self->mode_transition.tick_id = gtk_widget_add_tick_callback (widget, hdy_stackable_box_mode_transition_cb, self, NULL);
+ gtk_progress_tracker_start (&self->mode_transition.tracker,
+ self->mode_transition.duration * 1000,
+ 0,
+ 1.0);
+ }
+ else
+ hdy_stackable_box_set_position (self, target);
+}
+
+/* FIXME Use this to stop the mode transition animation when it makes sense (see *
+ * GtkRevealer for exmples).
+ */
+/* static void */
+/* hdy_stackable_box_stop_mode_animation (HdyStackableBox *self) */
+/* { */
+/* if (self->mode_transition.current_pos != self->mode_transition.target_pos) { */
+/* self->mode_transition.current_pos = self->mode_transition.target_pos; */
+ /* g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_REVEALED]); */
+/* } */
+/* if (self->mode_transition.tick_id != 0) { */
+/* gtk_widget_remove_tick_callback (GTK_WIDGET (self->container), self->mode_transition.tick_id); */
+/* self->mode_transition.tick_id = 0; */
+/* } */
+/* } */
+
+/**
+ * hdy_stackable_box_get_folded:
+ * @self: a #HdyStackableBox
+ *
+ * Gets whether @self is folded.
+ *
+ * Returns: whether @self is folded.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_folded (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->folded;
+}
+
+static void
+hdy_stackable_box_set_folded (HdyStackableBox *self,
+ gboolean folded)
+{
+ GtkStyleContext *context;
+
+ if (self->folded == folded)
+ return;
+
+ self->folded = folded;
+
+ hdy_stackable_box_start_mode_transition (self, folded ? 0.0 : 1.0);
+
+ if (self->can_unfold) {
+ context = gtk_widget_get_style_context (GTK_WIDGET (self->container));
+ if (folded) {
+ gtk_style_context_add_class (context, "folded");
+ gtk_style_context_remove_class (context, "unfolded");
+ } else {
+ gtk_style_context_remove_class (context, "folded");
+ gtk_style_context_add_class (context, "unfolded");
+ }
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_FOLDED]);
+}
+
+/**
+ * hdy_stackable_box_set_homogeneous:
+ * @self: a #HdyStackableBox
+ * @folded: the fold
+ * @orientation: the orientation
+ * @homogeneous: %TRUE to make @self homogeneous
+ *
+ * Sets the #HdyStackableBox to be homogeneous or not for the given fold and orientation.
+ * If it is homogeneous, the #HdyStackableBox will request the same
+ * width or height for all its children depending on the orientation.
+ * If it isn't and it is folded, the widget may change width or height
+ * when a different child becomes visible.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation,
+ gboolean homogeneous)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ folded = !!folded;
+ homogeneous = !!homogeneous;
+
+ if (self->homogeneous[folded][orientation] == homogeneous)
+ return;
+
+ self->homogeneous[folded][orientation] = homogeneous;
+
+ if (gtk_widget_get_visible (GTK_WIDGET (self->container)))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[HOMOGENEOUS_PROP[folded][orientation]]);
+}
+
+/**
+ * hdy_stackable_box_get_homogeneous:
+ * @self: a #HdyStackableBox
+ * @folded: the fold
+ * @orientation: the orientation
+ *
+ * Gets whether @self is homogeneous for the given fold and orientation.
+ * See hdy_stackable_box_set_homogeneous().
+ *
+ * Returns: whether @self is homogeneous for the given fold and orientation.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_homogeneous (HdyStackableBox *self,
+ gboolean folded,
+ GtkOrientation orientation)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ folded = !!folded;
+
+ return self->homogeneous[folded][orientation];
+}
+
+/**
+ * hdy_stackable_box_get_transition_type:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the type of animation that will be used
+ * for transitions between modes and children in @self.
+ *
+ * Returns: the current transition type of @self
+ *
+ * Since: 1.0
+ */
+HdyStackableBoxTransitionType
+hdy_stackable_box_get_transition_type (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER);
+
+ return self->transition_type;
+}
+
+/**
+ * hdy_stackable_box_set_transition_type:
+ * @self: a #HdyStackableBox
+ * @transition: the new transition type
+ *
+ * Sets the type of animation that will be used for transitions between modes
+ * and children in @self.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about to
+ * become current.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_transition_type (HdyStackableBox *self,
+ HdyStackableBoxTransitionType transition)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->transition_type == transition)
+ return;
+
+ self->transition_type = transition;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_TRANSITION_TYPE]);
+}
+
+/**
+ * hdy_stackable_box_get_mode_transition_duration:
+ * @self: a #HdyStackableBox
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between modes in @self will take.
+ *
+ * Returns: the mode transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_stackable_box_get_mode_transition_duration (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0);
+
+ return self->mode_transition.duration;
+}
+
+/**
+ * hdy_stackable_box_set_mode_transition_duration:
+ * @self: a #HdyStackableBox
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between modes in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_mode_transition_duration (HdyStackableBox *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->mode_transition.duration == duration)
+ return;
+
+ self->mode_transition.duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_MODE_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_stackable_box_get_child_transition_duration:
+ * @self: a #HdyStackableBox
+ *
+ * Returns the amount of time (in milliseconds) that
+ * transitions between children in @self will take.
+ *
+ * Returns: the child transition duration
+ *
+ * Since: 1.0
+ */
+guint
+hdy_stackable_box_get_child_transition_duration (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), 0);
+
+ return self->child_transition.duration;
+}
+
+/**
+ * hdy_stackable_box_set_child_transition_duration:
+ * @self: a #HdyStackableBox
+ * @duration: the new duration, in milliseconds
+ *
+ * Sets the duration that transitions between children in @self
+ * will take.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_child_transition_duration (HdyStackableBox *self,
+ guint duration)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ if (self->child_transition.duration == duration)
+ return;
+
+ self->child_transition.duration = duration;
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_CHILD_TRANSITION_DURATION]);
+}
+
+/**
+ * hdy_stackable_box_get_visible_child:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the visible child widget.
+ *
+ * Returns: (transfer none): the visible child widget
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_visible_child (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ if (self->visible_child == NULL)
+ return NULL;
+
+ return self->visible_child->widget;
+}
+
+/**
+ * hdy_stackable_box_set_visible_child:
+ * @self: a #HdyStackableBox
+ * @visible_child: the new child
+ *
+ * Makes @visible_child visible using a transition determined by
+ * HdyStackableBox:transition-type and HdyStackableBox:child-transition-duration.
+ * The transition can be cancelled by the user, in which case visible child will
+ * change back to the previously visible child.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_visible_child (HdyStackableBox *self,
+ GtkWidget *visible_child)
+{
+ HdyStackableBoxChildInfo *child_info;
+ gboolean contains_child;
+
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+ g_return_if_fail (GTK_IS_WIDGET (visible_child));
+
+ child_info = find_child_info_for_widget (self, visible_child);
+ contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+/**
+ * hdy_stackable_box_get_visible_child_name:
+ * @self: a #HdyStackableBox
+ *
+ * Gets the name of the currently visible child widget.
+ *
+ * Returns: (transfer none): the name of the visible child
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_stackable_box_get_visible_child_name (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ if (self->visible_child == NULL)
+ return NULL;
+
+ return self->visible_child->name;
+}
+
+/**
+ * hdy_stackable_box_set_visible_child_name:
+ * @self: a #HdyStackableBox
+ * @name: the name of a child
+ *
+ * Makes the child with the name @name visible.
+ *
+ * See hdy_stackable_box_set_visible_child() for more details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_visible_child_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+ gboolean contains_child;
+
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+ g_return_if_fail (name != NULL);
+
+ child_info = find_child_info_for_name (self, name);
+ contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+/**
+ * hdy_stackable_box_get_child_transition_running:
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether @self is currently in a transition from one page to
+ * another.
+ *
+ * Returns: %TRUE if the transition is currently running, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_child_transition_running (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return (self->child_transition.tick_id != 0 ||
+ self->child_transition.is_gesture_active);
+}
+
+/**
+ * hdy_stackable_box_set_interpolate_size:
+ * @self: a #HdyStackableBox
+ * @interpolate_size: the new value
+ *
+ * Sets whether or not @self will interpolate its size when
+ * changing the visible child. If the #HdyStackableBox:interpolate-size
+ * property is set to %TRUE, @self will interpolate its size between
+ * the current one and the one it'll take after changing the
+ * visible child, according to the set transition duration.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_interpolate_size (HdyStackableBox *self,
+ gboolean interpolate_size)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ interpolate_size = !!interpolate_size;
+
+ if (self->child_transition.interpolate_size == interpolate_size)
+ return;
+
+ self->child_transition.interpolate_size = interpolate_size;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INTERPOLATE_SIZE]);
+}
+
+/**
+ * hdy_stackable_box_get_interpolate_size:
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox is set up to interpolate between
+ * the sizes of children on page switch.
+ *
+ * Returns: %TRUE if child sizes are interpolated
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_interpolate_size (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.interpolate_size;
+}
+
+/**
+ * hdy_stackable_box_set_can_swipe_back:
+ * @self: a #HdyStackableBox
+ * @can_swipe_back: the new value
+ *
+ * Sets whether or not @self allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_can_swipe_back (HdyStackableBox *self,
+ gboolean can_swipe_back)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ can_swipe_back = !!can_swipe_back;
+
+ if (self->child_transition.can_swipe_back == can_swipe_back)
+ return;
+
+ self->child_transition.can_swipe_back = can_swipe_back;
+ hdy_swipe_tracker_set_enabled (self->tracker, can_swipe_back || self->child_transition.can_swipe_forward);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_BACK]);
+}
+
+/**
+ * hdy_stackable_box_get_can_swipe_back
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox allows swiping to the previous child.
+ *
+ * Returns: %TRUE if back swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_can_swipe_back (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.can_swipe_back;
+}
+
+/**
+ * hdy_stackable_box_set_can_swipe_forward:
+ * @self: a #HdyStackableBox
+ * @can_swipe_forward: the new value
+ *
+ * Sets whether or not @self allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_stackable_box_set_can_swipe_forward (HdyStackableBox *self,
+ gboolean can_swipe_forward)
+{
+ g_return_if_fail (HDY_IS_STACKABLE_BOX (self));
+
+ can_swipe_forward = !!can_swipe_forward;
+
+ if (self->child_transition.can_swipe_forward == can_swipe_forward)
+ return;
+
+ self->child_transition.can_swipe_forward = can_swipe_forward;
+ hdy_swipe_tracker_set_enabled (self->tracker, self->child_transition.can_swipe_back || can_swipe_forward);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SWIPE_FORWARD]);
+}
+
+/**
+ * hdy_stackable_box_get_can_swipe_forward
+ * @self: a #HdyStackableBox
+ *
+ * Returns whether the #HdyStackableBox allows swiping to the next child.
+ *
+ * Returns: %TRUE if forward swipe is enabled.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_get_can_swipe_forward (HdyStackableBox *self)
+{
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ return self->child_transition.can_swipe_forward;
+}
+
+static HdyStackableBoxChildInfo *
+find_swipeable_child (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child = NULL;
+
+ children = g_list_find (self->children, self->visible_child);
+ do {
+ children = (direction == HDY_NAVIGATION_DIRECTION_BACK) ? children->prev : children->next;
+
+ if (children == NULL)
+ break;
+
+ child = children->data;
+ } while (child && !child->navigatable);
+
+ return child;
+}
+
+/**
+ * hdy_stackable_box_get_adjacent_child
+ * @self: a #HdyStackableBox
+ * @direction: the direction
+ *
+ * Gets the previous or next child that doesn't have 'navigatable' child
+ * property set to %FALSE, or %NULL if it doesn't exist. This will be the same
+ * widget hdy_stackable_box_navigate() will navigate to.
+ *
+ * Returns: (nullable) (transfer none): the previous or next child, or
+ * %NULL if it doesn't exist.
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_adjacent_child (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ HdyStackableBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+
+ child = find_swipeable_child (self, direction);
+
+ if (!child)
+ return NULL;
+
+ return child->widget;
+}
+
+/**
+ * hdy_stackable_box_navigate
+ * @self: a #HdyStackableBox
+ * @direction: the direction
+ *
+ * Switches to the previous or next child that doesn't have 'navigatable'
+ * child property set to %FALSE, similar to performing a swipe gesture to go
+ * in @direction.
+ *
+ * Returns: %TRUE if visible child was changed, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_stackable_box_navigate (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ HdyStackableBoxChildInfo *child;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), FALSE);
+
+ child = find_swipeable_child (self, direction);
+
+ if (!child)
+ return FALSE;
+
+ set_visible_child_info (self, child, self->transition_type, self->child_transition.duration, TRUE);
+
+ return TRUE;
+}
+
+/**
+ * hdy_stackable_box_get_child_by_name:
+ * @self: a #HdyStackableBox
+ * @name: the name of the child to find
+ *
+ * Finds the child of @self with the name given as the argument. Returns %NULL
+ * if there is no child with this name.
+ *
+ * Returns: (transfer none) (nullable): the requested child of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_stackable_box_get_child_by_name (HdyStackableBox *self,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ g_return_val_if_fail (HDY_IS_STACKABLE_BOX (self), NULL);
+ g_return_val_if_fail (name != NULL, NULL);
+
+ child_info = find_child_info_for_name (self, name);
+
+ return child_info ? child_info->widget : NULL;
+}
+
+static void
+get_preferred_size (gint *min,
+ gint *nat,
+ gboolean same_orientation,
+ gboolean homogeneous_folded,
+ gboolean homogeneous_unfolded,
+ gint visible_children,
+ gdouble visible_child_progress,
+ gint sum_nat,
+ gint max_min,
+ gint max_nat,
+ gint visible_min,
+ gint last_visible_min)
+{
+ if (same_orientation) {
+ *min = homogeneous_folded ?
+ max_min :
+ hdy_lerp (last_visible_min, visible_min, visible_child_progress);
+ *nat = homogeneous_unfolded ?
+ max_nat * visible_children :
+ sum_nat;
+ }
+ else {
+ *min = homogeneous_folded ?
+ max_min :
+ hdy_lerp (last_visible_min, visible_min, visible_child_progress);
+ *nat = max_nat;
+ }
+}
+
+void
+hdy_stackable_box_measure (HdyStackableBox *self,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+ gint visible_children;
+ gdouble visible_child_progress;
+ gint child_min, max_min, visible_min, last_visible_min;
+ gint child_nat, max_nat, sum_nat;
+ void (*get_preferred_size_static) (GtkWidget *widget,
+ gint *minimum_width,
+ gint *natural_width);
+ void (*get_preferred_size_for_size) (GtkWidget *widget,
+ gint height,
+ gint *minimum_width,
+ gint *natural_width);
+
+ get_preferred_size_static = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ gtk_widget_get_preferred_width :
+ gtk_widget_get_preferred_height;
+ get_preferred_size_for_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ gtk_widget_get_preferred_width_for_height :
+ gtk_widget_get_preferred_height_for_width;
+
+ visible_children = 0;
+ child_min = max_min = visible_min = last_visible_min = 0;
+ child_nat = max_nat = sum_nat = 0;
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info->widget == NULL || !gtk_widget_get_visible (child_info->widget))
+ continue;
+
+ visible_children++;
+ if (for_size < 0)
+ get_preferred_size_static (child_info->widget,
+ &child_min, &child_nat);
+ else
+ get_preferred_size_for_size (child_info->widget, for_size,
+ &child_min, &child_nat);
+
+ max_min = MAX (max_min, child_min);
+ max_nat = MAX (max_nat, child_nat);
+ sum_nat += child_nat;
+ }
+
+ if (self->visible_child != NULL) {
+ if (for_size < 0)
+ get_preferred_size_static (self->visible_child->widget,
+ &visible_min, NULL);
+ else
+ get_preferred_size_for_size (self->visible_child->widget, for_size,
+ &visible_min, NULL);
+ }
+
+ if (self->last_visible_child != NULL) {
+ if (for_size < 0)
+ get_preferred_size_static (self->last_visible_child->widget,
+ &last_visible_min, NULL);
+ else
+ get_preferred_size_for_size (self->last_visible_child->widget, for_size,
+ &last_visible_min, NULL);
+ }
+
+ visible_child_progress = self->child_transition.interpolate_size ? self->child_transition.progress : 1.0;
+
+ get_preferred_size (minimum, natural,
+ gtk_orientable_get_orientation (GTK_ORIENTABLE (self->container)) == orientation,
+ self->homogeneous[HDY_FOLD_FOLDED][orientation],
+ self->homogeneous[HDY_FOLD_UNFOLDED][orientation],
+ visible_children, visible_child_progress,
+ sum_nat, max_min, max_nat, visible_min, last_visible_min);
+}
+
+static void
+hdy_stackable_box_size_allocate_folded (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info, *visible_child;
+ gint start_size, end_size, visible_size;
+ gint remaining_start_size, remaining_end_size, remaining_size;
+ gint current_pad;
+ gint max_child_size = 0;
+ gint start_position, end_position;
+ gboolean box_homogeneous;
+ HdyStackableBoxTransitionType mode_transition_type;
+ GtkTextDirection direction;
+ gboolean under;
+
+ directed_children = get_directed_children (self);
+ visible_child = self->visible_child;
+
+ if (!visible_child)
+ return;
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->widget == visible_child->widget)
+ continue;
+
+ if (self->last_visible_child &&
+ child_info->widget == self->last_visible_child->widget)
+ continue;
+
+ child_info->visible = FALSE;
+ }
+
+ if (visible_child->widget == NULL)
+ return;
+
+ /* FIXME is this needed? */
+ if (!gtk_widget_get_visible (visible_child->widget)) {
+ visible_child->visible = FALSE;
+
+ return;
+ }
+
+ visible_child->visible = TRUE;
+
+ mode_transition_type = self->transition_type;
+
+ /* Avoid useless computations and allow visible child transitions. */
+ if (self->mode_transition.current_pos <= 0.0) {
+ /* Child transitions should be applied only when folded and when no mode
+ * transition is ongoing.
+ */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info != visible_child &&
+ child_info != self->last_visible_child) {
+ child_info->visible = FALSE;
+
+ continue;
+ }
+
+ child_info->alloc.x = get_child_window_x (self, child_info, allocation->width);
+ child_info->alloc.y = get_child_window_y (self, child_info, allocation->height);
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = allocation->height;
+ child_info->visible = TRUE;
+ }
+
+ return;
+ }
+
+ /* Compute visible child size. */
+ visible_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ MIN (allocation->width, MAX (visible_child->nat.width, (gint) (allocation->width * (1.0 - self->mode_transition.current_pos)))) :
+ MIN (allocation->height, MAX (visible_child->nat.height, (gint) (allocation->height * (1.0 - self->mode_transition.current_pos))));
+
+ /* Compute homogeneous box child size. */
+ box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) ||
+ (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL);
+ if (box_homogeneous) {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ max_child_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ MAX (max_child_size, child_info->nat.width) :
+ MAX (max_child_size, child_info->nat.height);
+ }
+ }
+
+ /* Compute the start size. */
+ start_size = 0;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ start_size += orientation == GTK_ORIENTATION_HORIZONTAL ?
+ (box_homogeneous ? max_child_size : child_info->nat.width) :
+ (box_homogeneous ? max_child_size : child_info->nat.height);
+ }
+
+ /* Compute the end size. */
+ end_size = 0;
+ for (children = g_list_last (directed_children); children; children = children->prev) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ end_size += orientation == GTK_ORIENTATION_HORIZONTAL ?
+ (box_homogeneous ? max_child_size : child_info->nat.width) :
+ (box_homogeneous ? max_child_size : child_info->nat.height);
+ }
+
+ /* Compute pads. */
+ remaining_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ allocation->width - visible_size :
+ allocation->height - visible_size;
+ remaining_start_size = (gint) (remaining_size * ((gdouble) start_size / (gdouble) (start_size + end_size)));
+ remaining_end_size = remaining_size - remaining_start_size;
+
+ /* Store start and end allocations. */
+ switch (orientation) {
+ case GTK_ORIENTATION_HORIZONTAL:
+ direction = gtk_widget_get_direction (GTK_WIDGET (self->container));
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL);
+ start_position = under ? 0 : remaining_start_size - start_size;
+ self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1;
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL);
+ end_position = under ? allocation->width - end_size : remaining_start_size + visible_size;
+ self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1;
+ break;
+ case GTK_ORIENTATION_VERTICAL:
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ start_position = under ? 0 : remaining_start_size - start_size;
+ self->mode_transition.start_progress = under ? (gdouble) remaining_size / start_size : 1;
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ end_position = remaining_start_size + visible_size;
+ self->mode_transition.end_progress = under ? (gdouble) remaining_end_size / end_size : 1;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ /* Allocate visible child. */
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ visible_child->alloc.width = visible_size;
+ visible_child->alloc.height = allocation->height;
+ visible_child->alloc.x = remaining_start_size;
+ visible_child->alloc.y = 0;
+ visible_child->visible = TRUE;
+ }
+ else {
+ visible_child->alloc.width = allocation->width;
+ visible_child->alloc.height = visible_size;
+ visible_child->alloc.x = 0;
+ visible_child->alloc.y = remaining_start_size;
+ visible_child->visible = TRUE;
+ }
+
+ /* Allocate starting children. */
+ current_pad = start_position;
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_info->alloc.width = box_homogeneous ?
+ max_child_size :
+ child_info->nat.width;
+ child_info->alloc.height = allocation->height;
+ child_info->alloc.x = current_pad;
+ child_info->alloc.y = 0;
+ child_info->visible = child_info->alloc.x + child_info->alloc.width > 0;
+
+ current_pad += child_info->alloc.width;
+ }
+ else {
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = box_homogeneous ?
+ max_child_size :
+ child_info->nat.height;
+ child_info->alloc.x = 0;
+ child_info->alloc.y = current_pad;
+ child_info->visible = child_info->alloc.y + child_info->alloc.height > 0;
+
+ current_pad += child_info->alloc.height;
+ }
+ }
+
+ /* Allocate ending children. */
+ current_pad = end_position;
+
+ if (!children || !children->next)
+ return;
+
+ for (children = children->next; children; children = children->next) {
+ child_info = children->data;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ child_info->alloc.width = box_homogeneous ?
+ max_child_size :
+ child_info->nat.width;
+ child_info->alloc.height = allocation->height;
+ child_info->alloc.x = current_pad;
+ child_info->alloc.y = 0;
+ child_info->visible = child_info->alloc.x < allocation->width;
+
+ current_pad += child_info->alloc.width;
+ }
+ else {
+ child_info->alloc.width = allocation->width;
+ child_info->alloc.height = box_homogeneous ?
+ max_child_size :
+ child_info->nat.height;
+ child_info->alloc.x = 0;
+ child_info->alloc.y = current_pad;
+ child_info->visible = child_info->alloc.y < allocation->height;
+
+ current_pad += child_info->alloc.height;
+ }
+ }
+}
+
+static void
+hdy_stackable_box_size_allocate_unfolded (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GtkAllocation remaining_alloc;
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info, *visible_child;
+ gint homogeneous_size = 0, min_size, extra_size;
+ gint per_child_extra, n_extra_widgets;
+ gint n_visible_children, n_expand_children;
+ gint start_pad = 0, end_pad = 0;
+ gboolean box_homogeneous;
+ HdyStackableBoxTransitionType mode_transition_type;
+ GtkTextDirection direction;
+ gboolean under;
+
+ directed_children = get_directed_children (self);
+ visible_child = self->visible_child;
+
+ box_homogeneous = (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] && orientation == GTK_ORIENTATION_HORIZONTAL) ||
+ (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] && orientation == GTK_ORIENTATION_VERTICAL);
+
+ n_visible_children = n_expand_children = 0;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ child_info->visible = child_info->widget != NULL && gtk_widget_get_visible (child_info->widget);
+
+ if (child_info->visible) {
+ n_visible_children++;
+ if (gtk_widget_compute_expand (child_info->widget, orientation))
+ n_expand_children++;
+ }
+ else {
+ child_info->min.width = child_info->min.height = 0;
+ child_info->nat.width = child_info->nat.height = 0;
+ }
+ }
+
+ /* Compute repartition of extra space. */
+
+ if (box_homogeneous) {
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ homogeneous_size = n_visible_children > 0 ? allocation->width / n_visible_children : 0;
+ n_expand_children = n_visible_children > 0 ? allocation->width % n_visible_children : 0;
+ min_size = allocation->width - n_expand_children;
+ }
+ else {
+ homogeneous_size = n_visible_children > 0 ? allocation->height / n_visible_children : 0;
+ n_expand_children = n_visible_children > 0 ? allocation->height % n_visible_children : 0;
+ min_size = allocation->height - n_expand_children;
+ }
+ }
+ else {
+ min_size = 0;
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ min_size += child_info->nat.width;
+ }
+ }
+ else {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ min_size += child_info->nat.height;
+ }
+ }
+ }
+
+ remaining_alloc.x = 0;
+ remaining_alloc.y = 0;
+ remaining_alloc.width = allocation->width;
+ remaining_alloc.height = allocation->height;
+
+ extra_size = orientation == GTK_ORIENTATION_HORIZONTAL ?
+ remaining_alloc.width - min_size :
+ remaining_alloc.height - min_size;
+
+ per_child_extra = 0, n_extra_widgets = 0;
+ if (n_expand_children > 0) {
+ per_child_extra = extra_size / n_expand_children;
+ n_extra_widgets = extra_size % n_expand_children;
+ }
+
+ /* Compute children allocation */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->visible)
+ continue;
+
+ child_info->alloc.x = remaining_alloc.x;
+ child_info->alloc.y = remaining_alloc.y;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (box_homogeneous) {
+ child_info->alloc.width = homogeneous_size;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.width++;
+ n_extra_widgets--;
+ }
+ }
+ else {
+ child_info->alloc.width = child_info->nat.width;
+ if (gtk_widget_compute_expand (child_info->widget, orientation)) {
+ child_info->alloc.width += per_child_extra;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.width++;
+ n_extra_widgets--;
+ }
+ }
+ }
+ child_info->alloc.height = remaining_alloc.height;
+
+ remaining_alloc.x += child_info->alloc.width;
+ remaining_alloc.width -= child_info->alloc.width;
+ }
+ else {
+ if (box_homogeneous) {
+ child_info->alloc.height = homogeneous_size;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.height++;
+ n_extra_widgets--;
+ }
+ }
+ else {
+ child_info->alloc.height = child_info->nat.height;
+ if (gtk_widget_compute_expand (child_info->widget, orientation)) {
+ child_info->alloc.height += per_child_extra;
+ if (n_extra_widgets > 0) {
+ child_info->alloc.height++;
+ n_extra_widgets--;
+ }
+ }
+ }
+ child_info->alloc.width = remaining_alloc.width;
+
+ remaining_alloc.y += child_info->alloc.height;
+ remaining_alloc.height -= child_info->alloc.height;
+ }
+ }
+
+ /* Apply animations. */
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ start_pad = (gint) ((visible_child->alloc.x) * (1.0 - self->mode_transition.current_pos));
+ end_pad = (gint) ((allocation->width - (visible_child->alloc.x + visible_child->alloc.width)) * (1.0 - self->mode_transition.current_pos));
+ }
+ else {
+ start_pad = (gint) ((visible_child->alloc.y) * (1.0 - self->mode_transition.current_pos));
+ end_pad = (gint) ((allocation->height - (visible_child->alloc.y + visible_child->alloc.height)) * (1.0 - self->mode_transition.current_pos));
+ }
+
+ mode_transition_type = self->transition_type;
+ direction = gtk_widget_get_direction (GTK_WIDGET (self->container));
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_RTL);
+ else
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (!child_info->visible)
+ continue;
+
+ if (under)
+ continue;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ child_info->alloc.x -= start_pad;
+ else
+ child_info->alloc.y -= start_pad;
+ }
+
+ self->mode_transition.start_progress = under ? self->mode_transition.current_pos : 1;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ under = (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER && direction == GTK_TEXT_DIR_LTR) ||
+ (mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER && direction == GTK_TEXT_DIR_RTL);
+ else
+ under = mode_transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER;
+ for (children = g_list_last (directed_children); children; children = children->prev) {
+ child_info = children->data;
+
+ if (child_info == visible_child)
+ break;
+
+ if (!child_info->visible)
+ continue;
+
+ if (under)
+ continue;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ child_info->alloc.x += end_pad;
+ else
+ child_info->alloc.y += end_pad;
+ }
+
+ self->mode_transition.end_progress = under ? self->mode_transition.current_pos : 1;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ visible_child->alloc.x -= start_pad;
+ visible_child->alloc.width += start_pad + end_pad;
+ }
+ else {
+ visible_child->alloc.y -= start_pad;
+ visible_child->alloc.height += start_pad + end_pad;
+ }
+}
+
+static HdyStackableBoxChildInfo *
+get_top_overlap_child (HdyStackableBox *self)
+{
+ gboolean is_rtl, start;
+
+ if (!self->last_visible_child)
+ return self->visible_child;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ start = (self->child_transition.active_direction == GTK_PAN_DIRECTION_LEFT && !is_rtl) ||
+ (self->child_transition.active_direction == GTK_PAN_DIRECTION_RIGHT && is_rtl) ||
+ self->child_transition.active_direction == GTK_PAN_DIRECTION_UP;
+
+ switch (self->transition_type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ // Nothing overlaps in this case
+ return NULL;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ return start ? self->visible_child : self->last_visible_child;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ return start ? self->last_visible_child : self->visible_child;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+static void
+restack_windows (HdyStackableBox *self)
+{
+ HdyStackableBoxChildInfo *child_info, *overlap_child;
+ GList *l;
+
+ overlap_child = get_top_overlap_child (self);
+
+ switch (self->transition_type) {
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE:
+ // Nothing overlaps in this case
+ return;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER:
+ for (l = g_list_last (self->children); l; l = l->prev) {
+ child_info = l->data;
+
+ if (child_info->window)
+ gdk_window_raise (child_info->window);
+
+ if (child_info == overlap_child)
+ break;
+ }
+
+ break;
+ case HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER:
+ for (l = self->children; l; l = l->next) {
+ child_info = l->data;
+
+ if (child_info->window)
+ gdk_window_raise (child_info->window);
+
+ if (child_info == overlap_child)
+ break;
+ }
+
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+void
+hdy_stackable_box_size_allocate (HdyStackableBox *self,
+ GtkAllocation *allocation)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkOrientation orientation = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget));
+ GList *directed_children, *children;
+ HdyStackableBoxChildInfo *child_info;
+ gboolean folded;
+
+ directed_children = get_directed_children (self);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (gtk_widget_get_realized (widget)) {
+ gdk_window_move_resize (self->view_window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+ }
+
+ /* Prepare children information. */
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ gtk_widget_get_preferred_size (child_info->widget, &child_info->min, &child_info->nat);
+ child_info->alloc.x = child_info->alloc.y = child_info->alloc.width = child_info->alloc.height = 0;
+ child_info->visible = FALSE;
+ }
+
+ /* Check whether the children should be stacked or not. */
+ if (self->can_unfold) {
+ gint nat_box_size = 0, nat_max_size = 0, visible_children = 0;
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ /* FIXME Check the child is visible. */
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->nat.width <= 0)
+ continue;
+
+ nat_box_size += child_info->nat.width;
+ nat_max_size = MAX (nat_max_size, child_info->nat.width);
+ visible_children++;
+ }
+ if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL])
+ nat_box_size = nat_max_size * visible_children;
+ folded = visible_children > 1 && allocation->width < nat_box_size;
+ }
+ else {
+ for (children = directed_children; children; children = children->next) {
+ child_info = children->data;
+
+ /* FIXME Check the child is visible. */
+ if (!child_info->widget)
+ continue;
+
+ if (child_info->nat.height <= 0)
+ continue;
+
+ nat_box_size += child_info->nat.height;
+ nat_max_size = MAX (nat_max_size, child_info->nat.height);
+ visible_children++;
+ }
+ if (self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL])
+ nat_box_size = nat_max_size * visible_children;
+ folded = visible_children > 1 && allocation->height < nat_box_size;
+ }
+ } else {
+ folded = TRUE;
+ }
+
+ hdy_stackable_box_set_folded (self, folded);
+
+ /* Allocate size to the children. */
+ if (folded)
+ hdy_stackable_box_size_allocate_folded (self, allocation);
+ else
+ hdy_stackable_box_size_allocate_unfolded (self, allocation);
+
+ /* Apply visibility and allocation. */
+ for (children = directed_children; children; children = children->next) {
+ GtkAllocation alloc;
+
+ child_info = children->data;
+
+ gtk_widget_set_child_visible (child_info->widget, child_info->visible);
+
+ if (child_info->window &&
+ child_info->visible != gdk_window_is_visible (child_info->window)) {
+ if (child_info->visible)
+ gdk_window_show (child_info->window);
+ else
+ gdk_window_hide (child_info->window);
+ }
+
+ if (!child_info->visible)
+ continue;
+
+ if (child_info->window)
+ gdk_window_move_resize (child_info->window,
+ child_info->alloc.x,
+ child_info->alloc.y,
+ child_info->alloc.width,
+ child_info->alloc.height);
+
+ alloc.x = 0;
+ alloc.y = 0;
+ alloc.width = child_info->alloc.width;
+ alloc.height = child_info->alloc.height;
+ gtk_widget_size_allocate (child_info->widget, &alloc);
+
+ if (gtk_widget_get_realized (widget))
+ gtk_widget_show (child_info->widget);
+ }
+
+ restack_windows (self);
+}
+
+gboolean
+hdy_stackable_box_draw (HdyStackableBox *self,
+ cairo_t *cr)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *stacked_children, *l;
+ HdyStackableBoxChildInfo *child_info, *overlap_child;
+ gboolean is_transition;
+ gboolean is_vertical;
+ gboolean is_rtl;
+ gboolean is_over;
+ GtkAllocation shadow_rect;
+ gdouble shadow_progress, mode_progress;
+ GtkPanDirection shadow_direction;
+
+ overlap_child = get_top_overlap_child (self);
+
+ is_transition = self->child_transition.is_gesture_active ||
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER ||
+ gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER;
+
+ if (!is_transition ||
+ self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE ||
+ !overlap_child) {
+ for (l = self->children; l; l = l->next) {
+ child_info = l->data;
+
+ if (!gtk_cairo_should_draw_window (cr, child_info->window))
+ continue;
+
+ gtk_container_propagate_draw (self->container,
+ child_info->widget,
+ cr);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ stacked_children = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER ?
+ self->children_reversed : self->children;
+
+ is_vertical = gtk_orientable_get_orientation (GTK_ORIENTABLE (widget)) == GTK_ORIENTATION_VERTICAL;
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+ is_over = self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+
+ cairo_save (cr);
+
+ shadow_rect.x = 0;
+ shadow_rect.y = 0;
+ shadow_rect.width = gtk_widget_get_allocated_width (widget);
+ shadow_rect.height = gtk_widget_get_allocated_height (widget);
+
+ if (is_vertical) {
+ if (!is_over) {
+ shadow_rect.y = overlap_child->alloc.y + overlap_child->alloc.height;
+ shadow_rect.height -= shadow_rect.y;
+ shadow_direction = GTK_PAN_DIRECTION_UP;
+ mode_progress = self->mode_transition.end_progress;
+ } else {
+ shadow_rect.height = overlap_child->alloc.y;
+ shadow_direction = GTK_PAN_DIRECTION_DOWN;
+ mode_progress = self->mode_transition.start_progress;
+ }
+ } else {
+ if (is_over == is_rtl) {
+ shadow_rect.x = overlap_child->alloc.x + overlap_child->alloc.width;
+ shadow_rect.width -= shadow_rect.x;
+ shadow_direction = GTK_PAN_DIRECTION_LEFT;
+ mode_progress = self->mode_transition.end_progress;
+ } else {
+ shadow_rect.width = overlap_child->alloc.x;
+ shadow_direction = GTK_PAN_DIRECTION_RIGHT;
+ mode_progress = self->mode_transition.start_progress;
+ }
+ }
+
+ if (gtk_progress_tracker_get_state (&self->mode_transition.tracker) != GTK_PROGRESS_STATE_AFTER) {
+ shadow_progress = mode_progress;
+ } else {
+ GtkPanDirection direction = self->child_transition.active_direction;
+ GtkPanDirection left_or_right = is_rtl ? GTK_PAN_DIRECTION_RIGHT : GTK_PAN_DIRECTION_LEFT;
+ gint width = gtk_widget_get_allocated_width (widget);
+ gint height = gtk_widget_get_allocated_height (widget);
+
+ if (direction == GTK_PAN_DIRECTION_UP || direction == left_or_right)
+ shadow_progress = self->child_transition.progress;
+ else
+ shadow_progress = 1 - self->child_transition.progress;
+
+ if (is_over)
+ shadow_progress = 1 - shadow_progress;
+
+ /* Normalize the shadow rect size so that we can cache the shadow */
+ if (shadow_direction == GTK_PAN_DIRECTION_RIGHT)
+ shadow_rect.x -= (width - shadow_rect.width);
+ else if (shadow_direction == GTK_PAN_DIRECTION_DOWN)
+ shadow_rect.y -= (height - shadow_rect.height);
+
+ shadow_rect.width = width;
+ shadow_rect.height = height;
+ }
+
+ cairo_rectangle (cr, shadow_rect.x, shadow_rect.y, shadow_rect.width, shadow_rect.height);
+ cairo_clip (cr);
+
+ for (l = stacked_children; l; l = l->next) {
+ child_info = l->data;
+
+ if (!gtk_cairo_should_draw_window (cr, child_info->window))
+ continue;
+
+ if (child_info == overlap_child)
+ cairo_restore (cr);
+
+ gtk_container_propagate_draw (self->container,
+ child_info->widget,
+ cr);
+ }
+
+ cairo_save (cr);
+ cairo_translate (cr, shadow_rect.x, shadow_rect.y);
+ hdy_shadow_helper_draw_shadow (self->shadow_helper, cr,
+ shadow_rect.width, shadow_rect.height,
+ shadow_progress, shadow_direction);
+ cairo_restore (cr);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+update_tracker_orientation (HdyStackableBox *self)
+{
+ gboolean reverse;
+
+ reverse = (self->orientation == GTK_ORIENTATION_HORIZONTAL &&
+ gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL);
+
+ g_object_set (self->tracker,
+ "orientation", self->orientation,
+ "reversed", reverse,
+ NULL);
+}
+
+void
+hdy_stackable_box_direction_changed (HdyStackableBox *self,
+ GtkTextDirection previous_direction)
+{
+ update_tracker_orientation (self);
+}
+
+static void
+hdy_stackable_box_child_visibility_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (user_data);
+ GtkWidget *widget = GTK_WIDGET (obj);
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ if (self->visible_child == NULL && gtk_widget_get_visible (widget))
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, TRUE);
+ else if (self->visible_child == child_info && !gtk_widget_get_visible (widget))
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+
+ if (child_info == self->last_visible_child) {
+ gtk_widget_set_child_visible (self->last_visible_child->widget, !self->folded);
+ self->last_visible_child = NULL;
+ }
+}
+
+static void
+register_window (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+
+ attributes.x = child->alloc.x;
+ attributes.y = child->alloc.y;
+ attributes.width = child->alloc.width;
+ attributes.height = child->alloc.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ attributes.event_mask = gtk_widget_get_events (widget) |
+ gtk_widget_get_events (child->widget);
+
+ child->window = gdk_window_new (self->view_window, &attributes, attributes_mask);
+ gtk_widget_register_window (widget, child->window);
+
+ gtk_widget_set_parent_window (child->widget, child->window);
+
+ gdk_window_show (child->window);
+}
+
+static void
+unregister_window (HdyStackableBox *self,
+ HdyStackableBoxChildInfo *child)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+
+ if (!child->window)
+ return;
+
+ gtk_widget_unregister_window (widget, child->window);
+ gdk_window_destroy (child->window);
+ child->window = NULL;
+}
+
+void
+hdy_stackable_box_add (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ g_return_if_fail (gtk_widget_get_parent (widget) == NULL);
+
+ child_info = g_new0 (HdyStackableBoxChildInfo, 1);
+ child_info->widget = widget;
+ child_info->navigatable = TRUE;
+
+ self->children = g_list_append (self->children, child_info);
+ self->children_reversed = g_list_prepend (self->children_reversed, child_info);
+
+ if (gtk_widget_get_realized (GTK_WIDGET (self->container)))
+ register_window (self, child_info);
+
+ gtk_widget_set_child_visible (widget, FALSE);
+ gtk_widget_set_parent (widget, GTK_WIDGET (self->container));
+
+ g_signal_connect (widget, "notify::visible",
+ G_CALLBACK (hdy_stackable_box_child_visibility_notify_cb), self);
+
+ if (hdy_stackable_box_get_visible_child (self) == NULL &&
+ gtk_widget_get_visible (widget)) {
+ set_visible_child_info (self, child_info, self->transition_type, self->child_transition.duration, FALSE);
+ }
+
+ if (!self->folded ||
+ (self->folded && (self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] ||
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] ||
+ self->visible_child == child_info)))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+}
+
+void
+hdy_stackable_box_remove (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ g_autoptr (HdyStackableBoxChildInfo) child_info = find_child_info_for_widget (self, widget);
+ gboolean contains_child = child_info != NULL;
+
+ g_return_if_fail (contains_child);
+
+ self->children = g_list_remove (self->children, child_info);
+ self->children_reversed = g_list_remove (self->children_reversed, child_info);
+
+ g_signal_handlers_disconnect_by_func (widget,
+ hdy_stackable_box_child_visibility_notify_cb,
+ self);
+
+ if (hdy_stackable_box_get_visible_child (self) == widget)
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+
+ if (child_info == self->last_visible_child)
+ self->last_visible_child = NULL;
+
+ if (gtk_widget_get_visible (widget))
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+
+ unregister_window (self, child_info);
+
+ gtk_widget_unparent (widget);
+}
+
+void
+hdy_stackable_box_forall (HdyStackableBox *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ /* This shallow copy is needed when the callback changes the list while we are
+ * looping through it, for example by calling hdy_stackable_box_remove() on all
+ * children when destroying the HdyStackableBox_private_offset.
+ */
+ g_autoptr (GList) children_copy = g_list_copy (self->children);
+ GList *children;
+ HdyStackableBoxChildInfo *child_info;
+
+ for (children = children_copy; children; children = children->next) {
+ child_info = children->data;
+
+ (* callback) (child_info->widget, callback_data);
+ }
+
+ g_list_free (self->children_reversed);
+ self->children_reversed = g_list_copy (self->children);
+ self->children_reversed = g_list_reverse (self->children_reversed);
+}
+
+static void
+hdy_stackable_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ switch (prop_id) {
+ case PROP_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_folded (self));
+ break;
+ case PROP_HHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ g_value_set_boolean (value, hdy_stackable_box_get_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL));
+ break;
+ case PROP_VISIBLE_CHILD:
+ g_value_set_object (value, hdy_stackable_box_get_visible_child (self));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ g_value_set_string (value, hdy_stackable_box_get_visible_child_name (self));
+ break;
+ case PROP_TRANSITION_TYPE:
+ g_value_set_enum (value, hdy_stackable_box_get_transition_type (self));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_stackable_box_get_mode_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ g_value_set_uint (value, hdy_stackable_box_get_child_transition_duration (self));
+ break;
+ case PROP_CHILD_TRANSITION_RUNNING:
+ g_value_set_boolean (value, hdy_stackable_box_get_child_transition_running (self));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ g_value_set_boolean (value, hdy_stackable_box_get_interpolate_size (self));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_back (self));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ g_value_set_boolean (value, hdy_stackable_box_get_can_swipe_forward (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, hdy_stackable_box_get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_stackable_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ switch (prop_id) {
+ case PROP_HHOMOGENEOUS_FOLDED:
+ hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_FOLDED:
+ hdy_stackable_box_set_homogeneous (self, TRUE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_HHOMOGENEOUS_UNFOLDED:
+ hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_HORIZONTAL, g_value_get_boolean (value));
+ break;
+ case PROP_VHOMOGENEOUS_UNFOLDED:
+ hdy_stackable_box_set_homogeneous (self, FALSE, GTK_ORIENTATION_VERTICAL, g_value_get_boolean (value));
+ break;
+ case PROP_VISIBLE_CHILD:
+ hdy_stackable_box_set_visible_child (self, g_value_get_object (value));
+ break;
+ case PROP_VISIBLE_CHILD_NAME:
+ hdy_stackable_box_set_visible_child_name (self, g_value_get_string (value));
+ break;
+ case PROP_TRANSITION_TYPE:
+ hdy_stackable_box_set_transition_type (self, g_value_get_enum (value));
+ break;
+ case PROP_MODE_TRANSITION_DURATION:
+ hdy_stackable_box_set_mode_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_CHILD_TRANSITION_DURATION:
+ hdy_stackable_box_set_child_transition_duration (self, g_value_get_uint (value));
+ break;
+ case PROP_INTERPOLATE_SIZE:
+ hdy_stackable_box_set_interpolate_size (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_BACK:
+ hdy_stackable_box_set_can_swipe_back (self, g_value_get_boolean (value));
+ break;
+ case PROP_CAN_SWIPE_FORWARD:
+ hdy_stackable_box_set_can_swipe_forward (self, g_value_get_boolean (value));
+ break;
+ case PROP_ORIENTATION:
+ hdy_stackable_box_set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_stackable_box_finalize (GObject *object)
+{
+ HdyStackableBox *self = HDY_STACKABLE_BOX (object);
+
+ self->visible_child = NULL;
+
+ if (self->shadow_helper)
+ g_clear_object (&self->shadow_helper);
+
+ hdy_stackable_box_unschedule_child_ticks (self);
+
+ G_OBJECT_CLASS (hdy_stackable_box_parent_class)->finalize (object);
+}
+
+void
+hdy_stackable_box_realize (HdyStackableBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes = { 0 };
+ GdkWindowAttributesType attributes_mask;
+ GList *children;
+
+ gtk_widget_set_realized (widget, TRUE);
+ gtk_widget_set_window (widget, g_object_ref (gtk_widget_get_parent_window (widget)));
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = (GDK_WA_X | GDK_WA_Y) | GDK_WA_VISUAL;
+
+ self->view_window = gdk_window_new (gtk_widget_get_window (widget),
+ &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->view_window);
+
+ for (children = self->children; children != NULL; children = children->next)
+ register_window (self, children->data);
+}
+
+void
+hdy_stackable_box_unrealize (HdyStackableBox *self)
+{
+ GtkWidget *widget = GTK_WIDGET (self->container);
+ GList *children;
+
+ for (children = self->children; children != NULL; children = children->next)
+ unregister_window (self, children->data);
+
+ gtk_widget_unregister_window (widget, self->view_window);
+ gdk_window_destroy (self->view_window);
+ self->view_window = NULL;
+
+ GTK_WIDGET_CLASS (self->klass)->unrealize (widget);
+}
+
+void
+hdy_stackable_box_map (HdyStackableBox *self)
+{
+ GTK_WIDGET_CLASS (self->klass)->map (GTK_WIDGET (self->container));
+
+ gdk_window_show (self->view_window);
+}
+
+void
+hdy_stackable_box_unmap (HdyStackableBox *self)
+{
+ gdk_window_hide (self->view_window);
+
+ GTK_WIDGET_CLASS (self->klass)->unmap (GTK_WIDGET (self->container));
+}
+
+HdySwipeTracker *
+hdy_stackable_box_get_swipe_tracker (HdyStackableBox *self)
+{
+ return self->tracker;
+}
+
+gdouble
+hdy_stackable_box_get_distance (HdyStackableBox *self)
+{
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
+ return gtk_widget_get_allocated_width (GTK_WIDGET (self->container));
+ else
+ return gtk_widget_get_allocated_height (GTK_WIDGET (self->container));
+}
+
+static gboolean
+can_swipe_in_direction (HdyStackableBox *self,
+ HdyNavigationDirection direction)
+{
+ switch (direction) {
+ case HDY_NAVIGATION_DIRECTION_BACK:
+ return self->child_transition.can_swipe_back;
+ case HDY_NAVIGATION_DIRECTION_FORWARD:
+ return self->child_transition.can_swipe_forward;
+ default:
+ g_assert_not_reached ();
+ }
+}
+
+gdouble *
+hdy_stackable_box_get_snap_points (HdyStackableBox *self,
+ gint *n_snap_points)
+{
+ gint n;
+ gdouble *points, lower, upper;
+
+ if (self->child_transition.tick_id > 0 ||
+ self->child_transition.is_gesture_active) {
+ gint current_direction;
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ switch (self->child_transition.active_direction) {
+ case GTK_PAN_DIRECTION_UP:
+ current_direction = 1;
+ break;
+ case GTK_PAN_DIRECTION_DOWN:
+ current_direction = -1;
+ break;
+ case GTK_PAN_DIRECTION_LEFT:
+ current_direction = is_rtl ? -1 : 1;
+ break;
+ case GTK_PAN_DIRECTION_RIGHT:
+ current_direction = is_rtl ? 1 : -1;
+ break;
+ default:
+ g_assert_not_reached ();
+ }
+
+ lower = MIN (0, current_direction);
+ upper = MAX (0, current_direction);
+ } else {
+ HdyStackableBoxChildInfo *child = NULL;
+
+ if ((can_swipe_in_direction (self, self->child_transition.swipe_direction) ||
+ !self->child_transition.is_direct_swipe) && self->folded)
+ child = find_swipeable_child (self, self->child_transition.swipe_direction);
+
+ lower = MIN (0, child ? self->child_transition.swipe_direction : 0);
+ upper = MAX (0, child ? self->child_transition.swipe_direction : 0);
+ }
+
+ n = (lower != upper) ? 2 : 1;
+
+ points = g_new0 (gdouble, n);
+ points[0] = lower;
+ points[n - 1] = upper;
+
+ if (n_snap_points)
+ *n_snap_points = n;
+
+ return points;
+}
+
+gdouble
+hdy_stackable_box_get_progress (HdyStackableBox *self)
+{
+ gboolean new_first = FALSE;
+ GList *children;
+
+ if (!self->child_transition.is_gesture_active &&
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) == GTK_PROGRESS_STATE_AFTER)
+ return 0;
+
+ for (children = self->children; children; children = children->next) {
+ if (self->last_visible_child == children->data) {
+ new_first = TRUE;
+
+ break;
+ }
+ if (self->visible_child == children->data)
+ break;
+ }
+
+ return self->child_transition.progress * (new_first ? 1 : -1);
+}
+
+gdouble
+hdy_stackable_box_get_cancel_progress (HdyStackableBox *self)
+{
+ return 0;
+}
+
+void
+hdy_stackable_box_get_swipe_area (HdyStackableBox *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ gint width = gtk_widget_get_allocated_width (GTK_WIDGET (self->container));
+ gint height = gtk_widget_get_allocated_height (GTK_WIDGET (self->container));
+ gdouble progress = 0;
+
+ rect->x = 0;
+ rect->y = 0;
+ rect->width = width;
+ rect->height = height;
+
+ if (!is_drag)
+ return;
+
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_SLIDE)
+ return;
+
+ if (self->child_transition.is_gesture_active ||
+ gtk_progress_tracker_get_state (&self->child_transition.tracker) != GTK_PROGRESS_STATE_AFTER)
+ progress = self->child_transition.progress;
+
+ if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self->container)) == GTK_TEXT_DIR_RTL;
+
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) {
+ rect->width = MAX (progress * width, HDY_SWIPE_BORDER);
+ rect->x = is_rtl ? 0 : width - rect->width;
+ } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) {
+ rect->width = MAX (progress * width, HDY_SWIPE_BORDER);
+ rect->x = is_rtl ? width - rect->width : 0;
+ }
+ } else {
+ if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_FORWARD) {
+ rect->height = MAX (progress * height, HDY_SWIPE_BORDER);
+ rect->y = height - rect->height;
+ } else if (self->transition_type == HDY_STACKABLE_BOX_TRANSITION_TYPE_UNDER &&
+ navigation_direction == HDY_NAVIGATION_DIRECTION_BACK) {
+ rect->height = MAX (progress * height, HDY_SWIPE_BORDER);
+ rect->y = 0;
+ }
+ }
+}
+
+void
+hdy_stackable_box_switch_child (HdyStackableBox *self,
+ guint index,
+ gint64 duration)
+{
+ HdyStackableBoxChildInfo *child_info = NULL;
+ GList *children;
+ guint i = 0;
+
+ for (children = self->children; children; children = children->next) {
+ child_info = children->data;
+
+ if (!child_info->navigatable)
+ continue;
+
+ if (i == index)
+ break;
+
+ i++;
+ }
+
+ if (child_info == NULL) {
+ g_critical ("Couldn't find eligible child with index %u", index);
+ return;
+ }
+
+ set_visible_child_info (self, child_info, self->transition_type,
+ duration, FALSE);
+}
+
+static void
+begin_swipe_cb (HdySwipeTracker *tracker,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdyStackableBox *self)
+{
+ self->child_transition.is_direct_swipe = direct;
+ self->child_transition.swipe_direction = direction;
+
+ if (self->child_transition.tick_id > 0) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self->container),
+ self->child_transition.tick_id);
+ self->child_transition.tick_id = 0;
+ self->child_transition.is_gesture_active = TRUE;
+ self->child_transition.is_cancelled = FALSE;
+ } else {
+ HdyStackableBoxChildInfo *child;
+
+ if ((can_swipe_in_direction (self, direction) || !direct) && self->folded)
+ child = find_swipeable_child (self, direction);
+ else
+ child = NULL;
+
+ if (child) {
+ self->child_transition.is_gesture_active = TRUE;
+ set_visible_child_info (self, child, self->transition_type,
+ self->child_transition.duration, FALSE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD_TRANSITION_RUNNING]);
+ }
+ }
+}
+
+static void
+update_swipe_cb (HdySwipeTracker *tracker,
+ gdouble progress,
+ HdyStackableBox *self)
+{
+ self->child_transition.progress = ABS (progress);
+ hdy_stackable_box_child_progress_updated (self);
+}
+
+static void
+end_swipe_cb (HdySwipeTracker *tracker,
+ gint64 duration,
+ gdouble to,
+ HdyStackableBox *self)
+{
+ if (!self->child_transition.is_gesture_active)
+ return;
+
+ self->child_transition.start_progress = self->child_transition.progress;
+ self->child_transition.end_progress = ABS (to);
+ self->child_transition.is_cancelled = (to == 0);
+ self->child_transition.first_frame_skipped = TRUE;
+
+ hdy_stackable_box_schedule_child_ticks (self);
+ if (hdy_get_enable_animations (GTK_WIDGET (self->container)) && duration != 0) {
+ gtk_progress_tracker_start (&self->child_transition.tracker,
+ duration * 1000,
+ 0,
+ 1.0);
+ } else {
+ self->child_transition.progress = self->child_transition.end_progress;
+ gtk_progress_tracker_finish (&self->child_transition.tracker);
+ }
+
+ self->child_transition.is_gesture_active = FALSE;
+ hdy_stackable_box_child_progress_updated (self);
+
+ gtk_widget_queue_draw (GTK_WIDGET (self->container));
+}
+
+GtkOrientation
+hdy_stackable_box_get_orientation (HdyStackableBox *self)
+{
+ return self->orientation;
+}
+
+void
+hdy_stackable_box_set_orientation (HdyStackableBox *self,
+ GtkOrientation orientation)
+{
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+ update_tracker_orientation (self);
+ gtk_widget_queue_resize (GTK_WIDGET (self->container));
+ g_object_notify (G_OBJECT (self), "orientation");
+}
+
+const gchar *
+hdy_stackable_box_get_child_name (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_val_if_fail (child_info != NULL, NULL);
+
+ return child_info->name;
+}
+
+void
+hdy_stackable_box_set_child_name (HdyStackableBox *self,
+ GtkWidget *widget,
+ const gchar *name)
+{
+ HdyStackableBoxChildInfo *child_info;
+ HdyStackableBoxChildInfo *child_info2;
+ GList *children;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_if_fail (child_info != NULL);
+
+ for (children = self->children; children; children = children->next) {
+ child_info2 = children->data;
+
+ if (child_info == child_info2)
+ continue;
+ if (g_strcmp0 (child_info2->name, name) == 0) {
+ g_warning ("Duplicate child name in HdyStackableBox: %s", name);
+
+ break;
+ }
+ }
+
+ g_free (child_info->name);
+ child_info->name = g_strdup (name);
+
+ if (self->visible_child == child_info)
+ g_object_notify_by_pspec (G_OBJECT (self),
+ props[PROP_VISIBLE_CHILD_NAME]);
+}
+
+gboolean
+hdy_stackable_box_get_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_val_if_fail (child_info != NULL, FALSE);
+
+ return child_info->navigatable;
+}
+
+void
+hdy_stackable_box_set_child_navigatable (HdyStackableBox *self,
+ GtkWidget *widget,
+ gboolean navigatable)
+{
+ HdyStackableBoxChildInfo *child_info;
+
+ child_info = find_child_info_for_widget (self, widget);
+
+ g_return_if_fail (child_info != NULL);
+
+ child_info->navigatable = navigatable;
+
+ if (!child_info->navigatable &&
+ hdy_stackable_box_get_visible_child (self) == widget)
+ set_visible_child_info (self, NULL, self->transition_type, self->child_transition.duration, TRUE);
+}
+
+static void
+hdy_stackable_box_class_init (HdyStackableBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = hdy_stackable_box_get_property;
+ object_class->set_property = hdy_stackable_box_set_property;
+ object_class->finalize = hdy_stackable_box_finalize;
+
+ /**
+ * HdyStackableBox:folded:
+ *
+ * %TRUE if the widget is folded.
+ *
+ * The #HdyStackableBox will be folded if the size allocated to it is smaller
+ * than the sum of the natural size of its children, it will be unfolded
+ * otherwise.
+ */
+ props[PROP_FOLDED] =
+ g_param_spec_boolean ("folded",
+ _("Folded"),
+ _("Whether the widget is folded"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:hhomogeneous_folded:
+ *
+ * %TRUE if the widget allocates the same width for all children when folded.
+ */
+ props[PROP_HHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("hhomogeneous-folded",
+ _("Horizontally homogeneous folded"),
+ _("Horizontally homogeneous sizing when the widget is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:vhomogeneous_folded:
+ *
+ * %TRUE if the widget allocates the same height for all children when folded.
+ */
+ props[PROP_VHOMOGENEOUS_FOLDED] =
+ g_param_spec_boolean ("vhomogeneous-folded",
+ _("Vertically homogeneous folded"),
+ _("Vertically homogeneous sizing when the widget is folded"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:hhomogeneous_unfolded:
+ *
+ * %TRUE if the widget allocates the same width for all children when unfolded.
+ */
+ props[PROP_HHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("hhomogeneous-unfolded",
+ _("Box horizontally homogeneous"),
+ _("Horizontally homogeneous sizing when the widget is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:vhomogeneous_unfolded:
+ *
+ * %TRUE if the widget allocates the same height for all children when unfolded.
+ */
+ props[PROP_VHOMOGENEOUS_UNFOLDED] =
+ g_param_spec_boolean ("vhomogeneous-unfolded",
+ _("Box vertically homogeneous"),
+ _("Vertically homogeneous sizing when the widget is unfolded"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD] =
+ g_param_spec_object ("visible-child",
+ _("Visible child"),
+ _("The widget currently visible when the widget is folded"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_VISIBLE_CHILD_NAME] =
+ g_param_spec_string ("visible-child-name",
+ _("Name of visible child"),
+ _("The name of the widget currently visible when the children are stacked"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:transition-type:
+ *
+ * The type of animation that will be used for transitions between modes and
+ * children.
+ *
+ * The transition type can be changed without problems at runtime, so it is
+ * possible to change the animation based on the mode or child that is about
+ * to become current.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TRANSITION_TYPE] =
+ g_param_spec_enum ("transition-type",
+ _("Transition type"),
+ _("The type of animation used to transition between modes and children"),
+ HDY_TYPE_STACKABLE_BOX_TRANSITION_TYPE, HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_MODE_TRANSITION_DURATION] =
+ g_param_spec_uint ("mode-transition-duration",
+ _("Mode transition duration"),
+ _("The mode transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 250,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_DURATION] =
+ g_param_spec_uint ("child-transition-duration",
+ _("Child transition duration"),
+ _("The child transition animation duration, in milliseconds"),
+ 0, G_MAXUINT, 200,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_CHILD_TRANSITION_RUNNING] =
+ g_param_spec_boolean ("child-transition-running",
+ _("Child transition running"),
+ _("Whether or not the child transition is currently running"),
+ FALSE,
+ G_PARAM_READABLE);
+
+ props[PROP_INTERPOLATE_SIZE] =
+ g_param_spec_boolean ("interpolate-size",
+ _("Interpolate size"),
+ _("Whether or not the size should smoothly change when changing between differently sized children"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:can-swipe-back:
+ *
+ * Whether or not the widget allows switching to the previous child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_BACK] =
+ g_param_spec_boolean ("can-swipe-back",
+ _("Can swipe back"),
+ _("Whether or not swipe gesture can be used to switch to the previous child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyStackableBox:can-swipe-forward:
+ *
+ * Whether or not the widget allows switching to the next child that has
+ * 'navigatable' child property set to %TRUE via a swipe gesture.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CAN_SWIPE_FORWARD] =
+ g_param_spec_boolean ("can-swipe-forward",
+ _("Can swipe forward"),
+ _("Whether or not swipe gesture can be used to switch to the next child"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_ORIENTATION] =
+ g_param_spec_enum ("orientation",
+ _("Orientation"),
+ _("Orientation"),
+ GTK_TYPE_ORIENTATION,
+ GTK_ORIENTATION_HORIZONTAL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+}
+
+HdyStackableBox *
+hdy_stackable_box_new (GtkContainer *container,
+ GtkContainerClass *klass,
+ gboolean can_unfold)
+{
+ GtkWidget *widget;
+ HdyStackableBox *self;
+
+ g_return_val_if_fail (GTK_IS_CONTAINER (container), NULL);
+ g_return_val_if_fail (GTK_IS_ORIENTABLE (container), NULL);
+ g_return_val_if_fail (GTK_IS_CONTAINER_CLASS (klass), NULL);
+
+ widget = GTK_WIDGET (container);
+ self = g_object_new (HDY_TYPE_STACKABLE_BOX, NULL);
+
+ self->container = container;
+ self->klass = klass;
+ self->can_unfold = can_unfold;
+
+ self->children = NULL;
+ self->children_reversed = NULL;
+ self->visible_child = NULL;
+ self->folded = FALSE;
+ self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_HORIZONTAL] = FALSE;
+ self->homogeneous[HDY_FOLD_UNFOLDED][GTK_ORIENTATION_VERTICAL] = FALSE;
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_HORIZONTAL] = TRUE;
+ self->homogeneous[HDY_FOLD_FOLDED][GTK_ORIENTATION_VERTICAL] = TRUE;
+ self->transition_type = HDY_STACKABLE_BOX_TRANSITION_TYPE_OVER;
+ self->mode_transition.duration = 250;
+ self->child_transition.duration = 200;
+ self->mode_transition.current_pos = 1.0;
+ self->mode_transition.target_pos = 1.0;
+
+ self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self->container));
+
+ g_object_set (self->tracker, "orientation", self->orientation, "enabled", FALSE, NULL);
+
+ g_signal_connect_object (self->tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self, 0);
+ g_signal_connect_object (self->tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self, 0);
+
+ self->shadow_helper = hdy_shadow_helper_new (widget);
+
+ gtk_widget_set_has_window (widget, FALSE);
+ gtk_widget_set_can_focus (widget, FALSE);
+ gtk_widget_set_redraw_on_allocate (widget, FALSE);
+
+ if (can_unfold) {
+ GtkStyleContext *context = gtk_widget_get_style_context (widget);
+ gtk_style_context_add_class (context, "unfolded");
+ }
+
+ return self;
+}
+
+static void
+hdy_stackable_box_init (HdyStackableBox *self)
+{
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-group.c b/subprojects/libhandy/src/hdy-swipe-group.c
new file mode 100644
index 0000000..2779a31
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-group.c
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-swipe-group.h"
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-swipe-tracker-private.h"
+
+#define BUILDABLE_TAG_OBJECT "object"
+#define BUILDABLE_TAG_SWIPEABLE "swipeable"
+#define BUILDABLE_TAG_SWIPEABLES "swipeables"
+#define BUILDABLE_TAG_TEMPLATE "template"
+
+/**
+ * SECTION:hdy-swipe-group
+ * @short_description: An object for syncing swipeable widgets.
+ * @title: HdySwipeGroup
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable
+ *
+ * The #HdySwipeGroup object can be used to sync multiple swipeable widgets
+ * that implement the #HdySwipeable interface, such as #HdyCarousel, so that
+ * animating one of them also animates all the other widgets in the group.
+ *
+ * This can be useful for syncing widgets between a window's titlebar and
+ * content area.
+ *
+ * # #HdySwipeGroup as #GtkBuildable
+ *
+ * #HdySwipeGroup can be created in an UI definition. The list of swipeable
+ * widgets is specified with a &lt;swipeables&gt; element containing multiple
+ * &lt;swipeable&gt; elements with their ”name” attribute specifying the id of
+ * the widgets.
+ *
+ * |[
+ * <object class="HdySwipeGroup">
+ * <swipeables>
+ * <swipeable name="carousel1"/>
+ * <swipeable name="carousel2"/>
+ * </swipeables>
+ * </object>
+ * ]|
+ *
+ * Since: 0.0.12
+ */
+
+struct _HdySwipeGroup
+{
+ GObject parent_instance;
+
+ GSList *swipeables;
+ HdySwipeable *current;
+ gboolean block;
+};
+
+static void hdy_swipe_group_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdySwipeGroup, hdy_swipe_group, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_swipe_group_buildable_init))
+
+static gboolean
+contains (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ GSList *swipeables;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data == swipeable)
+ return TRUE;
+
+ return FALSE;
+}
+
+static void
+swipeable_destroyed (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+
+ self->swipeables = g_slist_remove (self->swipeables, swipeable);
+
+ g_object_unref (self);
+}
+
+/**
+ * hdy_swipe_group_new:
+ *
+ * Create a new #HdySwipeGroup object.
+ *
+ * Returns: The newly created #HdySwipeGroup object
+ *
+ * Since: 0.0.12
+ */
+HdySwipeGroup *
+hdy_swipe_group_new (void)
+{
+ return g_object_new (HDY_TYPE_SWIPE_GROUP, NULL);
+}
+
+static void
+child_switched_cb (HdySwipeGroup *self,
+ guint index,
+ gint64 duration,
+ HdySwipeable *swipeable)
+{
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ if (self->current != NULL && self->current != swipeable)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipeable_switch_child (swipeables->data, index, duration);
+
+ self->block = FALSE;
+}
+
+static void
+begin_swipe_cb (HdySwipeGroup *self,
+ HdyNavigationDirection direction,
+ gboolean direct,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (self->current != NULL && self->current != swipeable)
+ return;
+
+ self->current = swipeable;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_begin_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ direction, FALSE);
+
+ self->block = FALSE;
+}
+
+static void
+update_swipe_cb (HdySwipeGroup *self,
+ gdouble progress,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (swipeable != self->current)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_update_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ progress);
+
+ self->block = FALSE;
+}
+
+static void
+end_swipe_cb (HdySwipeGroup *self,
+ gint64 duration,
+ gdouble to,
+ HdySwipeTracker *tracker)
+{
+ HdySwipeable *swipeable;
+ GSList *swipeables;
+
+ if (self->block)
+ return;
+
+ swipeable = hdy_swipe_tracker_get_swipeable (tracker);
+
+ if (swipeable != self->current)
+ return;
+
+ self->block = TRUE;
+
+ for (swipeables = self->swipeables; swipeables != NULL; swipeables = swipeables->next)
+ if (swipeables->data != swipeable)
+ hdy_swipe_tracker_emit_end_swipe (hdy_swipeable_get_swipe_tracker (swipeables->data),
+ duration, to);
+
+ self->current = NULL;
+
+ self->block = FALSE;
+}
+
+/**
+ * hdy_swipe_group_add_swipeable:
+ * @self: a #HdySwipeGroup
+ * @swipeable: the #HdySwipeable to add
+ *
+ * When the widget is destroyed or no longer referenced elsewhere, it will
+ * be removed from the swipe group.
+ *
+ * Since: 0.0.12
+ */
+void
+hdy_swipe_group_add_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ HdySwipeTracker *tracker;
+
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+ g_return_if_fail (HDY_IS_SWIPEABLE (swipeable));
+
+ tracker = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (tracker));
+
+ g_signal_connect_swapped (swipeable, "child-switched", G_CALLBACK (child_switched_cb), self);
+ g_signal_connect_swapped (tracker, "begin-swipe", G_CALLBACK (begin_swipe_cb), self);
+ g_signal_connect_swapped (tracker, "update-swipe", G_CALLBACK (update_swipe_cb), self);
+ g_signal_connect_swapped (tracker, "end-swipe", G_CALLBACK (end_swipe_cb), self);
+
+ self->swipeables = g_slist_prepend (self->swipeables, swipeable);
+
+ g_object_ref (self);
+
+ g_signal_connect_swapped (swipeable, "destroy", G_CALLBACK (swipeable_destroyed), self);
+}
+
+
+/**
+ * hdy_swipe_group_remove_swipeable:
+ * @self: a #HdySwipeGroup
+ * @swipeable: the #HdySwipeable to remove
+ *
+ * Removes a widget from a #HdySwipeGroup.
+ *
+ * Since: 0.0.12
+ **/
+void
+hdy_swipe_group_remove_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable)
+{
+ HdySwipeTracker *tracker;
+
+ g_return_if_fail (HDY_IS_SWIPE_GROUP (self));
+ g_return_if_fail (HDY_IS_SWIPEABLE (swipeable));
+ g_return_if_fail (contains (self, swipeable));
+
+ tracker = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ self->swipeables = g_slist_remove (self->swipeables, swipeable);
+
+ g_signal_handlers_disconnect_by_data (swipeable, self);
+ g_signal_handlers_disconnect_by_data (tracker, self);
+
+ g_object_unref (self);
+}
+
+
+/**
+ * hdy_swipe_group_get_swipeables:
+ * @self: a #HdySwipeGroup
+ *
+ * Returns the list of swipeables associated with @self.
+ *
+ * Returns: (element-type HdySwipeable) (transfer none): a #GSList of
+ * swipeables. The list is owned by libhandy and should not be modified.
+ *
+ * Since: 0.0.12
+ **/
+GSList *
+hdy_swipe_group_get_swipeables (HdySwipeGroup *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_GROUP (self), NULL);
+
+ return self->swipeables;
+}
+
+typedef struct {
+ gchar *name;
+ gint line;
+ gint col;
+} ItemData;
+
+static void
+item_data_free (gpointer data)
+{
+ ItemData *item_data = data;
+
+ g_free (item_data->name);
+ g_free (item_data);
+}
+
+typedef struct {
+ GObject *object;
+ GtkBuilder *builder;
+ GSList *items;
+} GSListSubParserData;
+
+static void
+hdy_swipe_group_dispose (GObject *object)
+{
+ HdySwipeGroup *self = (HdySwipeGroup *)object;
+
+ g_slist_free_full (self->swipeables, (GDestroyNotify) g_object_unref);
+ self->swipeables = NULL;
+
+ G_OBJECT_CLASS (hdy_swipe_group_parent_class)->dispose (object);
+}
+
+/*< private >
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @parent_name: the name of the expected parent element
+ * @error: return location for an error
+ *
+ * Checks that the parent element of the currently handled
+ * start tag is @parent_name and set @error if it isn't.
+ *
+ * This is intended to be called in start_element vfuncs to
+ * ensure that element nesting is as intended.
+ *
+ * Returns: %TRUE if @parent_name is the parent element
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static gboolean
+_gtk_builder_check_parent (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *parent_name,
+ GError **error)
+{
+ const GSList *stack;
+ gint line, col;
+ const gchar *parent;
+ const gchar *element;
+
+ stack = g_markup_parse_context_get_element_stack (context);
+
+ element = (const gchar *)stack->data;
+ parent = stack->next ? (const gchar *)stack->next->data : "";
+
+ if (g_str_equal (parent_name, parent) ||
+ (g_str_equal (parent_name, BUILDABLE_TAG_OBJECT) &&
+ g_str_equal (parent, BUILDABLE_TAG_TEMPLATE)))
+ return TRUE;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_INVALID_TAG,
+ ".:%d:%d Can't use <%s> here",
+ line, col, element);
+
+ return FALSE;
+}
+
+/*< private >
+ * _gtk_builder_prefix_error:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @error: an error
+ *
+ * Calls g_prefix_error() to prepend a filename:line:column marker
+ * to the given error. The filename is taken from @builder, and
+ * the line and column are obtained by calling
+ * g_markup_parse_context_get_position().
+ *
+ * This is intended to be called on errors returned by
+ * g_markup_collect_attributes() in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_prefix_error (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_prefix_error (error, ".:%d:%d ", line, col);
+}
+
+/*< private >
+ * _gtk_builder_error_unhandled_tag:
+ * @builder: a #GtkBuilder
+ * @context: the #GMarkupParseContext
+ * @object: name of the object that is being handled
+ * @element_name: name of the element whose start tag is being handled
+ * @error: return location for the error
+ *
+ * Sets @error to a suitable error indicating that an @element_name
+ * tag is not expected in the custom markup for @object.
+ *
+ * This is intended to be called in a start_element vfunc.
+ */
+/* This has been copied and modified from gtkbuilder.c. */
+static void
+_gtk_builder_error_unhandled_tag (GtkBuilder *builder,
+ GMarkupParseContext *context,
+ const gchar *object,
+ const gchar *element_name,
+ GError **error)
+{
+ gint line, col;
+
+ g_markup_parse_context_get_position (context, &line, &col);
+ g_set_error (error,
+ GTK_BUILDER_ERROR,
+ GTK_BUILDER_ERROR_UNHANDLED_TAG,
+ ".:%d:%d Unsupported tag for %s: <%s>",
+ line, col,
+ object, element_name);
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+swipe_group_start_element (GMarkupParseContext *context,
+ const gchar *element_name,
+ const gchar **names,
+ const gchar **values,
+ gpointer user_data,
+ GError **error)
+{
+ GSListSubParserData *data = (GSListSubParserData*)user_data;
+
+ if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLE) == 0)
+ {
+ const gchar *name;
+ ItemData *item_data;
+
+ if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_SWIPEABLES, error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_STRING, "name", &name,
+ G_MARKUP_COLLECT_INVALID))
+ {
+ _gtk_builder_prefix_error (data->builder, context, error);
+ return;
+ }
+
+ item_data = g_new (ItemData, 1);
+ item_data->name = g_strdup (name);
+ g_markup_parse_context_get_position (context, &item_data->line, &item_data->col);
+ data->items = g_slist_prepend (data->items, item_data);
+ }
+ else if (strcmp (element_name, BUILDABLE_TAG_SWIPEABLES) == 0)
+ {
+ if (!_gtk_builder_check_parent (data->builder, context, BUILDABLE_TAG_OBJECT, error))
+ return;
+
+ if (!g_markup_collect_attributes (element_name, names, values, error,
+ G_MARKUP_COLLECT_INVALID, NULL, NULL,
+ G_MARKUP_COLLECT_INVALID))
+ _gtk_builder_prefix_error (data->builder, context, error);
+ }
+ else
+ {
+ _gtk_builder_error_unhandled_tag (data->builder, context,
+ "HdySwipeGroup", element_name,
+ error);
+ }
+}
+
+
+/* This has been copied and modified from gtksizegroup.c. */
+static const GMarkupParser swipe_group_parser =
+ {
+ swipe_group_start_element
+ };
+
+/* This has been copied and modified from gtksizegroup.c. */
+static gboolean
+hdy_swipe_group_buildable_custom_tag_start (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ GMarkupParser *parser,
+ gpointer *parser_data)
+{
+ GSListSubParserData *data;
+
+ if (child)
+ return FALSE;
+
+ if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) == 0)
+ {
+ data = g_slice_new0 (GSListSubParserData);
+ data->items = NULL;
+ data->object = G_OBJECT (buildable);
+ data->builder = builder;
+
+ *parser = swipe_group_parser;
+ *parser_data = data;
+
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+/* This has been copied and modified from gtksizegroup.c. */
+static void
+hdy_swipe_group_buildable_custom_finished (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *tagname,
+ gpointer user_data)
+{
+ GSList *l;
+ GSListSubParserData *data;
+ GObject *object;
+
+ if (strcmp (tagname, BUILDABLE_TAG_SWIPEABLES) != 0)
+ return;
+
+ data = (GSListSubParserData*)user_data;
+ data->items = g_slist_reverse (data->items);
+
+ for (l = data->items; l; l = l->next)
+ {
+ ItemData *item_data = l->data;
+ object = gtk_builder_get_object (builder, item_data->name);
+ if (!object)
+ continue;
+ hdy_swipe_group_add_swipeable (HDY_SWIPE_GROUP (data->object), HDY_SWIPEABLE (object));
+ }
+ g_slist_free_full (data->items, item_data_free);
+ g_slice_free (GSListSubParserData, data);
+}
+
+static void
+hdy_swipe_group_class_init (HdySwipeGroupClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_swipe_group_dispose;
+}
+
+static void
+hdy_swipe_group_init (HdySwipeGroup *self)
+{
+}
+
+static void
+hdy_swipe_group_buildable_init (GtkBuildableIface *iface)
+{
+ iface->custom_tag_start = hdy_swipe_group_buildable_custom_tag_start;
+ iface->custom_finished = hdy_swipe_group_buildable_custom_finished;
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-group.h b/subprojects/libhandy/src/hdy-swipe-group.h
new file mode 100644
index 0000000..791962e
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-group.h
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <glib-object.h>
+#include "hdy-swipeable.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPE_GROUP (hdy_swipe_group_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySwipeGroup, hdy_swipe_group, HDY, SWIPE_GROUP, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeGroup *hdy_swipe_group_new (void);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_group_add_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable);
+HDY_AVAILABLE_IN_ALL
+GSList * hdy_swipe_group_get_swipeables (HdySwipeGroup *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_group_remove_swipeable (HdySwipeGroup *self,
+ HdySwipeable *swipeable);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker-private.h b/subprojects/libhandy/src/hdy-swipe-tracker-private.h
new file mode 100644
index 0000000..d4b5541
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-swipe-tracker.h"
+
+G_BEGIN_DECLS
+
+void hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean direct);
+void hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self,
+ gdouble progress);
+void hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self,
+ gint64 duration,
+ gdouble to);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.c b/subprojects/libhandy/src/hdy-swipe-tracker.c
new file mode 100644
index 0000000..0cbf4a4
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker.c
@@ -0,0 +1,1113 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-swipe-tracker-private.h"
+#include "hdy-navigation-direction.h"
+
+#include <math.h>
+
+#define TOUCHPAD_BASE_DISTANCE_H 400
+#define TOUCHPAD_BASE_DISTANCE_V 300
+#define SCROLL_MULTIPLIER 10
+#define MIN_ANIMATION_DURATION 100
+#define MAX_ANIMATION_DURATION 400
+#define VELOCITY_THRESHOLD 0.4
+#define DURATION_MULTIPLIER 3
+#define ANIMATION_BASE_VELOCITY 0.002
+#define DRAG_THRESHOLD_DISTANCE 5
+
+/**
+ * SECTION:hdy-swipe-tracker
+ * @short_description: Swipe tracker used in #HdyCarousel and #HdyLeaflet
+ * @title: HdySwipeTracker
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeable
+ *
+ * The HdySwipeTracker object can be used for implementing widgets with swipe
+ * gestures. It supports touch-based swipes, pointer dragging, and touchpad
+ * scrolling.
+ *
+ * The widgets will probably want to expose #HdySwipeTracker:enabled property.
+ * If they expect to use horizontal orientation, #HdySwipeTracker:reversed
+ * property can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+
+typedef enum {
+ HDY_SWIPE_TRACKER_STATE_NONE,
+ HDY_SWIPE_TRACKER_STATE_PENDING,
+ HDY_SWIPE_TRACKER_STATE_SCROLLING,
+ HDY_SWIPE_TRACKER_STATE_FINISHING,
+ HDY_SWIPE_TRACKER_STATE_REJECTED,
+} HdySwipeTrackerState;
+
+struct _HdySwipeTracker
+{
+ GObject parent_instance;
+
+ HdySwipeable *swipeable;
+ gboolean enabled;
+ gboolean reversed;
+ gboolean allow_mouse_drag;
+ GtkOrientation orientation;
+
+ gint start_x;
+ gint start_y;
+
+ guint32 prev_time;
+ gdouble velocity;
+
+ gdouble initial_progress;
+ gdouble progress;
+ gboolean cancelled;
+
+ gdouble prev_offset;
+
+ gboolean is_scrolling;
+
+ HdySwipeTrackerState state;
+ GtkGesture *touch_gesture;
+};
+
+G_DEFINE_TYPE_WITH_CODE (HdySwipeTracker, hdy_swipe_tracker, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+ PROP_0,
+ PROP_SWIPEABLE,
+ PROP_ENABLED,
+ PROP_REVERSED,
+ PROP_ALLOW_MOUSE_DRAG,
+
+ /* GtkOrientable */
+ PROP_ORIENTATION,
+ LAST_PROP = PROP_ALLOW_MOUSE_DRAG + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_BEGIN_SWIPE,
+ SIGNAL_UPDATE_SWIPE,
+ SIGNAL_END_SWIPE,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+reset (HdySwipeTracker *self)
+{
+ self->state = HDY_SWIPE_TRACKER_STATE_NONE;
+
+ self->prev_offset = 0;
+
+ self->initial_progress = 0;
+ self->progress = 0;
+
+ self->start_x = 0;
+ self->start_y = 0;
+
+ self->prev_time = 0;
+ self->velocity = 0;
+
+ self->cancelled = FALSE;
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+}
+
+static void
+get_range (HdySwipeTracker *self,
+ gdouble *first,
+ gdouble *last)
+{
+ g_autofree gdouble *points = NULL;
+ gint n;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ *first = points[0];
+ *last = points[n - 1];
+}
+
+static void
+gesture_prepare (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean is_drag)
+{
+ GdkRectangle rect;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ hdy_swipeable_get_swipe_area (self->swipeable, direction, is_drag, &rect);
+
+ if (self->start_x < rect.x ||
+ self->start_x >= rect.x + rect.width ||
+ self->start_y < rect.y ||
+ self->start_y >= rect.y + rect.height) {
+ self->state = HDY_SWIPE_TRACKER_STATE_REJECTED;
+
+ return;
+ }
+
+ hdy_swipe_tracker_emit_begin_swipe (self, direction, TRUE);
+
+ self->initial_progress = hdy_swipeable_get_progress (self->swipeable);
+ self->progress = self->initial_progress;
+ self->velocity = 0;
+ self->state = HDY_SWIPE_TRACKER_STATE_PENDING;
+}
+
+static void
+gesture_begin (HdySwipeTracker *self)
+{
+ GdkEvent *event;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING)
+ return;
+
+ event = gtk_get_current_event ();
+ self->prev_time = gdk_event_get_time (event);
+ self->state = HDY_SWIPE_TRACKER_STATE_SCROLLING;
+
+ gtk_grab_add (GTK_WIDGET (self->swipeable));
+}
+
+static void
+gesture_update (HdySwipeTracker *self,
+ gdouble delta)
+{
+ GdkEvent *event;
+ guint32 time;
+ gdouble progress;
+ gdouble first_point, last_point;
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ event = gtk_get_current_event ();
+ time = gdk_event_get_time (event);
+ if (time != self->prev_time)
+ self->velocity = delta / (time - self->prev_time);
+
+ get_range (self, &first_point, &last_point);
+
+ progress = self->progress + delta;
+ progress = CLAMP (progress, first_point, last_point);
+
+ /* FIXME: this is a hack to prevent swiping more than 1 page at once */
+ progress = CLAMP (progress, self->initial_progress - 1, self->initial_progress + 1);
+
+ self->progress = progress;
+
+ hdy_swipe_tracker_emit_update_swipe (self, progress);
+
+ self->prev_time = time;
+}
+
+static void
+get_closest_snap_points (HdySwipeTracker *self,
+ gdouble *upper,
+ gdouble *lower)
+{
+ gint i, n;
+ gdouble *points;
+
+ *upper = 0;
+ *lower = 0;
+
+ points = hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+ for (i = 0; i < n; i++) {
+ if (points[i] >= self->progress) {
+ *upper = points[i];
+ break;
+ }
+ }
+
+ for (i = n - 1; i >= 0; i--) {
+ if (points[i] <= self->progress) {
+ *lower = points[i];
+ break;
+ }
+ }
+
+ g_free (points);
+}
+
+static gdouble
+get_end_progress (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble upper, lower, middle;
+
+ if (self->cancelled)
+ return hdy_swipeable_get_cancel_progress (self->swipeable);
+
+ get_closest_snap_points (self, &upper, &lower);
+ middle = (upper + lower) / 2;
+
+ if (self->progress > middle)
+ return (self->velocity * distance > -VELOCITY_THRESHOLD ||
+ self->initial_progress > upper) ? upper : lower;
+
+ return (self->velocity * distance < VELOCITY_THRESHOLD ||
+ self->initial_progress < lower) ? lower : upper;
+}
+
+static void
+gesture_end (HdySwipeTracker *self,
+ gdouble distance)
+{
+ gdouble end_progress, velocity;
+ gint64 duration;
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE)
+ return;
+
+ end_progress = get_end_progress (self, distance);
+
+ velocity = ANIMATION_BASE_VELOCITY;
+ if ((end_progress - self->progress) * self->velocity > 0)
+ velocity = self->velocity;
+
+ duration = ABS ((self->progress - end_progress) / velocity * DURATION_MULTIPLIER);
+ if (self->progress != end_progress)
+ duration = CLAMP (duration, MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION);
+
+ hdy_swipe_tracker_emit_end_swipe (self, duration, end_progress);
+
+ if (self->cancelled)
+ reset (self);
+ else
+ self->state = HDY_SWIPE_TRACKER_STATE_FINISHING;
+}
+
+static void
+gesture_cancel (HdySwipeTracker *self,
+ gdouble distance)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->cancelled = TRUE;
+ gesture_end (self, distance);
+}
+
+static void
+drag_begin_cb (HdySwipeTracker *self,
+ gdouble start_x,
+ gdouble start_y,
+ GtkGestureDrag *gesture)
+{
+ if (self->state != HDY_SWIPE_TRACKER_STATE_NONE)
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ self->start_x = start_x;
+ self->start_y = start_y;
+}
+
+static void
+drag_update_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble offset, distance;
+ gboolean is_vertical, is_offset_vertical;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ if (is_vertical)
+ offset = -offset_y / distance;
+ else
+ offset = -offset_x / distance;
+
+ if (self->reversed)
+ offset = -offset;
+
+ is_offset_vertical = (ABS (offset_y) > ABS (offset_x));
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (is_vertical == is_offset_vertical)
+ gesture_prepare (self, offset > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, TRUE);
+ else
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gdouble drag_distance;
+ gdouble first_point, last_point;
+ gboolean is_overshooting;
+
+ get_range (self, &first_point, &last_point);
+
+ drag_distance = sqrt (offset_x * offset_x + offset_y * offset_y);
+ is_overshooting = (offset < 0 && self->progress <= first_point) ||
+ (offset > 0 && self->progress >= last_point);
+
+ if (drag_distance >= DRAG_THRESHOLD_DISTANCE) {
+ if ((is_vertical == is_offset_vertical) && !is_overshooting) {
+ gesture_begin (self);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ } else {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ }
+ }
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_update (self, offset - self->prev_offset);
+ self->prev_offset = offset;
+ }
+}
+
+static void
+drag_end_cb (HdySwipeTracker *self,
+ gdouble offset_x,
+ gdouble offset_y,
+ GtkGestureDrag *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ reset (self);
+ return;
+ }
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ gesture_end (self, distance);
+}
+
+static void
+drag_cancel_cb (HdySwipeTracker *self,
+ GdkEventSequence *sequence,
+ GtkGesture *gesture)
+{
+ gdouble distance;
+
+ distance = hdy_swipeable_get_distance (self->swipeable);
+
+ gesture_cancel (self, distance);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static gboolean
+handle_scroll_event (HdySwipeTracker *self,
+ GdkEvent *event,
+ gboolean capture)
+{
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ gdouble dx, dy, delta, distance;
+ gboolean is_vertical;
+ gboolean is_delta_vertical;
+
+ is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+ distance = is_vertical ? TOUCHPAD_BASE_DISTANCE_V : TOUCHPAD_BASE_DISTANCE_H;
+
+ if (gdk_event_get_scroll_direction (event, NULL))
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device (event);
+ input_source = gdk_device_get_source (source_device);
+ if (input_source != GDK_SOURCE_TOUCHPAD)
+ return GDK_EVENT_PROPAGATE;
+
+ gdk_event_get_scroll_deltas (event, &dx, &dy);
+ delta = is_vertical ? dy : dx;
+ if (self->reversed)
+ delta = -delta;
+
+ is_delta_vertical = (ABS (dy) > ABS (dx));
+
+ if (self->is_scrolling) {
+ gesture_cancel (self, distance);
+
+ if (gdk_event_is_scroll_stop_event (event))
+ self->is_scrolling = FALSE;
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_REJECTED) {
+ if (gdk_event_is_scroll_stop_event (event))
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_NONE) {
+ if (gdk_event_is_scroll_stop_event (event))
+ return GDK_EVENT_PROPAGATE;
+
+ if (is_vertical == is_delta_vertical) {
+ if (!capture) {
+ GtkWidget *widget = gtk_get_event_widget (event);
+ gdouble event_x, event_y;
+
+ gdk_event_get_coords (event, &event_x, &event_y);
+ gtk_widget_translate_coordinates (widget, GTK_WIDGET (self->swipeable),
+ event_x, event_y,
+ &self->start_x, &self->start_y);
+
+ gesture_prepare (self, delta > 0 ? HDY_NAVIGATION_DIRECTION_FORWARD : HDY_NAVIGATION_DIRECTION_BACK, FALSE);
+ }
+ } else {
+ self->is_scrolling = TRUE;
+ return GDK_EVENT_PROPAGATE;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_PENDING) {
+ gboolean is_overshooting;
+ gdouble first_point, last_point;
+
+ get_range (self, &first_point, &last_point);
+
+ is_overshooting = (delta < 0 && self->progress <= first_point) ||
+ (delta > 0 && self->progress >= last_point);
+
+ if ((is_vertical == is_delta_vertical) && !is_overshooting)
+ gesture_begin (self);
+ else
+ gesture_cancel (self, distance);
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ if (gdk_event_is_scroll_stop_event (event)) {
+ gesture_end (self, distance);
+ } else {
+ gesture_update (self, delta / distance * SCROLL_MULTIPLIER);
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ if (!capture && self->state == HDY_SWIPE_TRACKER_STATE_FINISHING)
+ reset (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+is_window_handle (GtkWidget *widget)
+{
+ gboolean window_dragging;
+ GtkWidget *parent, *window, *titlebar;
+
+ gtk_widget_style_get (widget, "window-dragging", &window_dragging, NULL);
+
+ if (window_dragging)
+ return TRUE;
+
+ /* Window titlebar area is always draggable, so check if we're inside. */
+ window = gtk_widget_get_toplevel (widget);
+ if (!GTK_IS_WINDOW (window))
+ return FALSE;
+
+ titlebar = gtk_window_get_titlebar (GTK_WINDOW (window));
+ if (!titlebar)
+ return FALSE;
+
+ parent = widget;
+ while (parent && parent != titlebar)
+ parent = gtk_widget_get_parent (parent);
+
+ return parent == titlebar;
+}
+
+static gboolean
+handle_event_cb (HdySwipeTracker *self,
+ GdkEvent *event)
+{
+ GdkEventSequence *sequence;
+ gboolean retval;
+ GtkEventSequenceState state;
+ GtkWidget *widget;
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type == GDK_SCROLL)
+ return handle_scroll_event (self, event, FALSE);
+
+ if (event->type != GDK_BUTTON_PRESS &&
+ event->type != GDK_BUTTON_RELEASE &&
+ event->type != GDK_MOTION_NOTIFY &&
+ event->type != GDK_TOUCH_BEGIN &&
+ event->type != GDK_TOUCH_END &&
+ event->type != GDK_TOUCH_UPDATE &&
+ event->type != GDK_TOUCH_CANCEL)
+ return GDK_EVENT_PROPAGATE;
+
+ widget = gtk_get_event_widget (event);
+ if (is_window_handle (widget))
+ return GDK_EVENT_PROPAGATE;
+
+ sequence = gdk_event_get_event_sequence (event);
+ retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event);
+ state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence);
+
+ if (state == GTK_EVENT_SEQUENCE_DENIED) {
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture));
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ if (self->state == HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+ return GDK_EVENT_STOP;
+ } else if (self->state == HDY_SWIPE_TRACKER_STATE_FINISHING) {
+ reset (self);
+ return GDK_EVENT_STOP;
+ }
+ return retval;
+}
+
+static gboolean
+captured_event_cb (HdySwipeable *swipeable,
+ GdkEvent *event)
+{
+ HdySwipeTracker *self = hdy_swipeable_get_swipe_tracker (swipeable);
+
+ g_assert (HDY_IS_SWIPE_TRACKER (self));
+
+ if (!self->enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return GDK_EVENT_PROPAGATE;
+
+ if (event->type != GDK_SCROLL)
+ return GDK_EVENT_PROPAGATE;
+
+ return handle_scroll_event (self, event, TRUE);
+}
+
+static void
+hdy_swipe_tracker_constructed (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ g_assert (self->swipeable);
+
+ gtk_widget_add_events (GTK_WIDGET (self->swipeable),
+ GDK_SMOOTH_SCROLL_MASK |
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_TOUCH_MASK);
+
+ self->touch_gesture = g_object_new (GTK_TYPE_GESTURE_DRAG,
+ "widget", self->swipeable,
+ "propagation-phase", GTK_PHASE_NONE,
+ "touch-only", !self->allow_mouse_drag,
+ NULL);
+
+ g_signal_connect_swapped (self->touch_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-update", G_CALLBACK (drag_update_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "drag-end", G_CALLBACK (drag_end_cb), self);
+ g_signal_connect_swapped (self->touch_gesture, "cancel", G_CALLBACK (drag_cancel_cb), self);
+
+ g_signal_connect_object (self->swipeable, "event", G_CALLBACK (handle_event_cb), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->swipeable, "unrealize", G_CALLBACK (reset), self, G_CONNECT_SWAPPED);
+
+ /*
+ * HACK: GTK3 has no other way to get events on capture phase.
+ * This is a reimplementation of _gtk_widget_set_captured_event_handler(),
+ * which is private. In GTK4 it can be replaced with GtkEventControllerLegacy
+ * with capture propagation phase
+ */
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", captured_event_cb);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->constructed (object);
+}
+
+static void
+hdy_swipe_tracker_dispose (GObject *object)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ if (self->swipeable)
+ gtk_grab_remove (GTK_WIDGET (self->swipeable));
+
+ if (self->touch_gesture)
+ g_signal_handlers_disconnect_by_data (self->touch_gesture, self);
+
+ g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", NULL);
+
+ g_clear_object (&self->touch_gesture);
+ g_clear_object (&self->swipeable);
+
+ G_OBJECT_CLASS (hdy_swipe_tracker_parent_class)->dispose (object);
+}
+
+static void
+hdy_swipe_tracker_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ g_value_set_object (value, hdy_swipe_tracker_get_swipeable (self));
+ break;
+
+ case PROP_ENABLED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_enabled (self));
+ break;
+
+ case PROP_REVERSED:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_reversed (self));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ g_value_set_boolean (value, hdy_swipe_tracker_get_allow_mouse_drag (self));
+ break;
+
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, self->orientation);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdySwipeTracker *self = HDY_SWIPE_TRACKER (object);
+
+ switch (prop_id) {
+ case PROP_SWIPEABLE:
+ self->swipeable = HDY_SWIPEABLE (g_object_ref (g_value_get_object (value)));
+ break;
+
+ case PROP_ENABLED:
+ hdy_swipe_tracker_set_enabled (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_REVERSED:
+ hdy_swipe_tracker_set_reversed (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ALLOW_MOUSE_DRAG:
+ hdy_swipe_tracker_set_allow_mouse_drag (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_ORIENTATION:
+ {
+ GtkOrientation orientation = g_value_get_enum (value);
+ if (orientation != self->orientation) {
+ self->orientation = g_value_get_enum (value);
+ g_object_notify (G_OBJECT (self), "orientation");
+ }
+ }
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_swipe_tracker_class_init (HdySwipeTrackerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->constructed = hdy_swipe_tracker_constructed;
+ object_class->dispose = hdy_swipe_tracker_dispose;
+ object_class->get_property = hdy_swipe_tracker_get_property;
+ object_class->set_property = hdy_swipe_tracker_set_property;
+
+ /**
+ * HdySwipeTracker:swipeable:
+ *
+ * The widget the swipe tracker is attached to. Must not be %NULL.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SWIPEABLE] =
+ g_param_spec_object ("swipeable",
+ _("Swipeable"),
+ _("The swipeable the swipe tracker is attached to"),
+ HDY_TYPE_SWIPEABLE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ /**
+ * HdySwipeTracker:enabled:
+ *
+ * Whether the swipe tracker is enabled. When it's not enabled, no events
+ * will be processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ENABLED] =
+ g_param_spec_boolean ("enabled",
+ _("Enabled"),
+ _("Whether the swipe tracker processes events"),
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:reversed:
+ *
+ * Whether to reverse the swipe direction. If the swipe tracker is horizontal,
+ * it can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+ props[PROP_REVERSED] =
+ g_param_spec_boolean ("reversed",
+ _("Reversed"),
+ _("Whether swipe direction is reversed"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdySwipeTracker:allow-mouse-drag:
+ *
+ * Whether to allow dragging with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+ props[PROP_ALLOW_MOUSE_DRAG] =
+ g_param_spec_boolean ("allow-mouse-drag",
+ _("Allow mouse drag"),
+ _("Whether to allow dragging with mouse pointer"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * HdySwipeTracker::begin-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @direction: The direction of the swipe
+ * @direct: %TRUE if the swipe is directly triggered by a gesture,
+ * %FALSE if it's triggered via a #HdySwipeGroup
+ *
+ * This signal is emitted when a possible swipe is detected.
+ *
+ * The @direction value can be used to restrict the swipe to a certain
+ * direction.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_BEGIN_SWIPE] =
+ g_signal_new ("begin-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ HDY_TYPE_NAVIGATION_DIRECTION, G_TYPE_BOOLEAN);
+
+ /**
+ * HdySwipeTracker::update-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @progress: The current animation progress value
+ *
+ * This signal is emitted every time the progress value changes.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_UPDATE_SWIPE] =
+ g_signal_new ("update-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_DOUBLE);
+
+ /**
+ * HdySwipeTracker::end-swipe:
+ * @self: The #HdySwipeTracker instance
+ * @duration: Snap-back animation duration in milliseconds
+ * @to: The progress value to animate to
+ *
+ * This signal is emitted as soon as the gesture has stopped.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_END_SWIPE] =
+ g_signal_new ("end-swipe",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_INT64, G_TYPE_DOUBLE);
+}
+
+static void
+hdy_swipe_tracker_init (HdySwipeTracker *self)
+{
+ reset (self);
+ self->orientation = GTK_ORIENTATION_HORIZONTAL;
+ self->enabled = TRUE;
+}
+
+/**
+ * hdy_swipe_tracker_new:
+ * @swipeable: a #GtkWidget to add the tracker on
+ *
+ * Create a new #HdySwipeTracker object on @widget.
+ *
+ * Returns: the newly created #HdySwipeTracker object
+ *
+ * Since: 1.0
+ */
+HdySwipeTracker *
+hdy_swipe_tracker_new (HdySwipeable *swipeable)
+{
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (swipeable), NULL);
+
+ return g_object_new (HDY_TYPE_SWIPE_TRACKER,
+ "swipeable", swipeable,
+ NULL);
+}
+
+/**
+ * hdy_swipe_tracker_get_swipeable:
+ * @self: a #HdySwipeTracker
+ *
+ * Get @self's swipeable widget.
+ *
+ * Returns: (transfer none): the swipeable widget
+ *
+ * Since: 1.0
+ */
+HdySwipeable *
+hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), NULL);
+
+ return self->swipeable;
+}
+
+/**
+ * hdy_swipe_tracker_get_enabled:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is enabled. When it's not enabled, no events will be
+ * processed. Generally widgets will want to expose this via a property.
+ *
+ * Returns: %TRUE if @self is enabled
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_enabled (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->enabled;
+}
+
+/**
+ * hdy_swipe_tracker_set_enabled:
+ * @self: a #HdySwipeTracker
+ * @enabled: whether to enable to swipe tracker
+ *
+ * Set whether @self is enabled. When it's not enabled, no events will be
+ * processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_enabled (HdySwipeTracker *self,
+ gboolean enabled)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ enabled = !!enabled;
+
+ if (self->enabled == enabled)
+ return;
+
+ self->enabled = enabled;
+
+ if (!enabled && self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ reset (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_reversed:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self is reversing the swipe direction.
+ *
+ * Returns: %TRUE is the direction is reversed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_reversed (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->reversed;
+}
+
+/**
+ * hdy_swipe_tracker_set_reversed:
+ * @self: a #HdySwipeTracker
+ * @reversed: whether to reverse the swipe direction
+ *
+ * Set whether to reverse the swipe direction. If @self is horizontal,
+ * can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_reversed (HdySwipeTracker *self,
+ gboolean reversed)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ reversed = !!reversed;
+
+ if (self->reversed == reversed)
+ return;
+
+ self->reversed = reversed;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVERSED]);
+}
+
+/**
+ * hdy_swipe_tracker_get_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ *
+ * Get whether @self can be dragged with mouse pointer.
+ *
+ * Returns: %TRUE is mouse dragging is allowed
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self)
+{
+ g_return_val_if_fail (HDY_IS_SWIPE_TRACKER (self), FALSE);
+
+ return self->allow_mouse_drag;
+}
+
+/**
+ * hdy_swipe_tracker_set_allow_mouse_drag:
+ * @self: a #HdySwipeTracker
+ * @allow_mouse_drag: whether to allow mouse dragging
+ *
+ * Set whether @self can be dragged with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self,
+ gboolean allow_mouse_drag)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ allow_mouse_drag = !!allow_mouse_drag;
+
+ if (self->allow_mouse_drag == allow_mouse_drag)
+ return;
+
+ self->allow_mouse_drag = allow_mouse_drag;
+
+ if (self->touch_gesture)
+ g_object_set (self->touch_gesture, "touch-only", !allow_mouse_drag, NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * hdy_swipe_tracker_shift_position:
+ * @self: a #HdySwipeTracker
+ * @delta: the position delta
+ *
+ * Move the current progress value by @delta. This can be used to adjust the
+ * current position if snap points move during the gesture.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipe_tracker_shift_position (HdySwipeTracker *self,
+ gdouble delta)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ if (self->state != HDY_SWIPE_TRACKER_STATE_PENDING &&
+ self->state != HDY_SWIPE_TRACKER_STATE_SCROLLING)
+ return;
+
+ self->progress += delta;
+ self->initial_progress += delta;
+}
+
+void
+hdy_swipe_tracker_emit_begin_swipe (HdySwipeTracker *self,
+ HdyNavigationDirection direction,
+ gboolean direct)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_BEGIN_SWIPE], 0, direction, direct);
+}
+
+void
+hdy_swipe_tracker_emit_update_swipe (HdySwipeTracker *self,
+ gdouble progress)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_UPDATE_SWIPE], 0, progress);
+}
+
+void
+hdy_swipe_tracker_emit_end_swipe (HdySwipeTracker *self,
+ gint64 duration,
+ gdouble to)
+{
+ g_return_if_fail (HDY_IS_SWIPE_TRACKER (self));
+
+ g_signal_emit (self, signals[SIGNAL_END_SWIPE], 0, duration, to);
+}
diff --git a/subprojects/libhandy/src/hdy-swipe-tracker.h b/subprojects/libhandy/src/hdy-swipe-tracker.h
new file mode 100644
index 0000000..20fe751
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipe-tracker.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-swipeable.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPE_TRACKER (hdy_swipe_tracker_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdySwipeTracker, hdy_swipe_tracker, HDY, SWIPE_TRACKER, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeTracker *hdy_swipe_tracker_new (HdySwipeable *swipeable);
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeable *hdy_swipe_tracker_get_swipeable (HdySwipeTracker *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_enabled (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_enabled (HdySwipeTracker *self,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_reversed (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_reversed (HdySwipeTracker *self,
+ gboolean reversed);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_swipe_tracker_get_allow_mouse_drag (HdySwipeTracker *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_set_allow_mouse_drag (HdySwipeTracker *self,
+ gboolean allow_mouse_drag);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipe_tracker_shift_position (HdySwipeTracker *self,
+ gdouble delta);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-swipeable.c b/subprojects/libhandy/src/hdy-swipeable.c
new file mode 100644
index 0000000..6be1713
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipeable.c
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-swipeable.h"
+
+/**
+ * SECTION:hdy-swipeable
+ * @short_description: An interface for swipeable widgets.
+ * @title: HdySwipeable
+ * @See_also: #HdyCarousel, #HdyDeck, #HdyLeaflet, #HdySwipeGroup
+ *
+ * The #HdySwipeable interface is implemented by all swipeable widgets. They
+ * can be synced using #HdySwipeGroup.
+ *
+ * See #HdySwipeTracker for details about implementing it.
+ *
+ * Since: 0.0.12
+ */
+
+G_DEFINE_INTERFACE (HdySwipeable, hdy_swipeable, GTK_TYPE_WIDGET)
+
+enum {
+ SIGNAL_CHILD_SWITCHED,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+hdy_swipeable_default_init (HdySwipeableInterface *iface)
+{
+ /**
+ * HdySwipeable::child-switched:
+ * @self: The #HdySwipeable instance
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * This signal should be emitted when the widget's visible child is changed.
+ *
+ * @duration can be 0 if the child is switched without animation.
+ *
+ * This is used by #HdySwipeGroup, applications should not connect to it.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_CHILD_SWITCHED] =
+ g_signal_new ("child-switched",
+ G_TYPE_FROM_INTERFACE (iface),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_UINT, G_TYPE_INT64);
+}
+
+/**
+ * hdy_swipeable_switch_child:
+ * @self: a #HdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * See HdySwipeable::child-switched.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_switch_child (HdySwipeable *self,
+ guint index,
+ gint64 duration)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_if_fail (iface->switch_child != NULL);
+
+ iface->switch_child (self, index, duration);
+}
+
+/**
+ * hdy_swipeable_emit_child_switched:
+ * @self: a #HdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * Emits HdySwipeable::child-switched signal. This should be called when the
+ * widget switches visible child widget.
+ *
+ * @duration can be 0 if the child is switched without animation.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_emit_child_switched (HdySwipeable *self,
+ guint index,
+ gint64 duration)
+{
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+
+ g_signal_emit (self, signals[SIGNAL_CHILD_SWITCHED], 0, index, duration);
+}
+
+/**
+ * hdy_swipeable_get_swipe_tracker:
+ * @self: a #HdySwipeable
+ *
+ * Gets the #HdySwipeTracker used by this swipeable widget.
+ *
+ * Returns: (transfer none): the swipe tracker
+ *
+ * Since: 1.0
+ */
+HdySwipeTracker *
+hdy_swipeable_get_swipe_tracker (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_swipe_tracker != NULL, NULL);
+
+ return iface->get_swipe_tracker (self);
+}
+
+/**
+ * hdy_swipeable_get_distance:
+ * @self: a #HdySwipeable
+ *
+ * Gets the swipe distance of @self. This corresponds to how many pixels
+ * 1 unit represents.
+ *
+ * Returns: the swipe distance in pixels
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_distance (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_distance != NULL, 0);
+
+ return iface->get_distance (self);
+}
+
+/**
+ * hdy_swipeable_get_snap_points: (virtual get_snap_points)
+ * @self: a #HdySwipeable
+ * @n_snap_points: (out): location to return the number of the snap points
+ *
+ * Gets the snap points of @self. Each snap point represents a progress value
+ * that is considered acceptable to end the swipe on.
+ *
+ * Returns: (array length=n_snap_points) (transfer full): the snap points of
+ * @self. The array must be freed with g_free().
+ *
+ * Since: 1.0
+ */
+gdouble *
+hdy_swipeable_get_snap_points (HdySwipeable *self,
+ gint *n_snap_points)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_snap_points != NULL, NULL);
+
+ return iface->get_snap_points (self, n_snap_points);
+}
+
+/**
+ * hdy_swipeable_get_progress:
+ * @self: a #HdySwipeable
+ *
+ * Gets the current progress of @self
+ *
+ * Returns: the current progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_progress (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_progress != NULL, 0);
+
+ return iface->get_progress (self);
+}
+
+/**
+ * hdy_swipeable_get_cancel_progress:
+ * @self: a #HdySwipeable
+ *
+ * Gets the progress @self will snap back to after the gesture is canceled.
+ *
+ * Returns: the cancel progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+hdy_swipeable_get_cancel_progress (HdySwipeable *self)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_val_if_fail (HDY_IS_SWIPEABLE (self), 0);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+ g_return_val_if_fail (iface->get_cancel_progress != NULL, 0);
+
+ return iface->get_cancel_progress (self);
+}
+
+/**
+ * hdy_swipeable_get_swipe_area:
+ * @self: a #HdySwipeable
+ * @navigation_direction: the direction of the swipe
+ * @is_drag: whether the swipe is caused by a dragging gesture
+ * @rect: (out): a pointer to a #GdkRectangle to store the swipe area
+ *
+ * Gets the area @self can start a swipe from for the given direction and
+ * gesture type.
+ * This can be used to restrict swipes to only be possible from a certain area,
+ * for example, to only allow edge swipes, or to have a draggable element and
+ * ignore swipes elsewhere.
+ *
+ * Swipe area is only considered for direct swipes (as in, not initiated by
+ * #HdySwipeGroup).
+ *
+ * If not implemented, the default implementation returns the allocation of
+ * @self, allowing swipes from anywhere.
+ *
+ * Since: 1.0
+ */
+void
+hdy_swipeable_get_swipe_area (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect)
+{
+ HdySwipeableInterface *iface;
+
+ g_return_if_fail (HDY_IS_SWIPEABLE (self));
+ g_return_if_fail (rect != NULL);
+
+ iface = HDY_SWIPEABLE_GET_IFACE (self);
+
+ if (iface->get_swipe_area) {
+ iface->get_swipe_area (self, navigation_direction, is_drag, rect);
+ return;
+ }
+
+ rect->x = 0;
+ rect->y = 0;
+ rect->width = gtk_widget_get_allocated_width (GTK_WIDGET (self));
+ rect->height = gtk_widget_get_allocated_height (GTK_WIDGET (self));
+}
diff --git a/subprojects/libhandy/src/hdy-swipeable.h b/subprojects/libhandy/src/hdy-swipeable.h
new file mode 100644
index 0000000..9cb6cde
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-swipeable.h
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-navigation-direction.h"
+#include "hdy-types.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_SWIPEABLE (hdy_swipeable_get_type ())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_INTERFACE (HdySwipeable, hdy_swipeable, HDY, SWIPEABLE, GtkWidget)
+
+/**
+ * HdySwipeableInterface:
+ * @parent: The parent interface.
+ * @switch_child: Switches visible child.
+ * @get_swipe_tracker: Gets the swipe tracker.
+ * @get_distance: Gets the swipe distance.
+ * @get_snap_points: Gets the snap points
+ * @get_progress: Gets the current progress.
+ * @get_cancel_progress: Gets the cancel progress.
+ * @get_swipe_area: Gets the swipeable rectangle.
+ *
+ * An interface for swipeable widgets.
+ *
+ * Since: 1.0
+ **/
+struct _HdySwipeableInterface
+{
+ GTypeInterface parent;
+
+ void (*switch_child) (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+ HdySwipeTracker * (*get_swipe_tracker) (HdySwipeable *self);
+ gdouble (*get_distance) (HdySwipeable *self);
+ gdouble * (*get_snap_points) (HdySwipeable *self,
+ gint *n_snap_points);
+ gdouble (*get_progress) (HdySwipeable *self);
+ gdouble (*get_cancel_progress) (HdySwipeable *self);
+ void (*get_swipe_area) (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_switch_child (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_emit_child_switched (HdySwipeable *self,
+ guint index,
+ gint64 duration);
+
+HDY_AVAILABLE_IN_ALL
+HdySwipeTracker *hdy_swipeable_get_swipe_tracker (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_distance (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble *hdy_swipeable_get_snap_points (HdySwipeable *self,
+ gint *n_snap_points);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_progress (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+gdouble hdy_swipeable_get_cancel_progress (HdySwipeable *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_swipeable_get_swipe_area (HdySwipeable *self,
+ HdyNavigationDirection navigation_direction,
+ gboolean is_drag,
+ GdkRectangle *rect);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-title-bar.c b/subprojects/libhandy/src/hdy-title-bar.c
new file mode 100644
index 0000000..fd5371a
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-title-bar.c
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include "hdy-title-bar.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * SECTION:hdy-title-bar
+ * @short_description: A simple title bar container.
+ * @Title: HdyTitleBar
+ *
+ * HdyTitleBar is meant to be used as the top-level widget of your window's
+ * title bar. It will be drawn with the same style as a GtkHeaderBar but it
+ * won't force a widget layout on you: you can put whatever widget you want in
+ * it, including a GtkHeaderBar.
+ *
+ * HdyTitleBar becomes really useful when you want to animate header bars, like
+ * an adaptive application using #HdyLeaflet would do.
+ *
+ * # CSS nodes
+ *
+ * #HdyTitleBar has a single CSS node with name headerbar.
+ */
+
+enum {
+ PROP_0,
+ PROP_SELECTION_MODE,
+ LAST_PROP,
+};
+
+struct _HdyTitleBar
+{
+ GtkBin parent_instance;
+
+ gboolean selection_mode;
+};
+
+G_DEFINE_TYPE (HdyTitleBar, hdy_title_bar, GTK_TYPE_BIN)
+
+static GParamSpec *props[LAST_PROP];
+
+/**
+ * hdy_title_bar_set_selection_mode:
+ * @self: a #HdyTitleBar
+ * @selection_mode: %TRUE to enable the selection mode
+ *
+ * Sets whether @self is in selection mode.
+ */
+void
+hdy_title_bar_set_selection_mode (HdyTitleBar *self,
+ gboolean selection_mode)
+{
+ GtkStyleContext *context;
+
+ g_return_if_fail (HDY_IS_TITLE_BAR (self));
+
+ selection_mode = !!selection_mode;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ if (self->selection_mode == selection_mode)
+ return;
+
+ self->selection_mode = selection_mode;
+
+ if (selection_mode)
+ gtk_style_context_add_class (context, "selection-mode");
+ else
+ gtk_style_context_remove_class (context, "selection-mode");
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]);
+}
+
+/**
+ * hdy_title_bar_get_selection_mode:
+ * @self: a #HdyTitleBar
+ *
+ * Returns whether whether @self is in selection mode.
+ *
+ * Returns: %TRUE if the title bar is in selection mode
+ */
+gboolean
+hdy_title_bar_get_selection_mode (HdyTitleBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TITLE_BAR (self), FALSE);
+
+ return self->selection_mode;
+}
+
+static void
+style_updated_cb (HdyTitleBar *self)
+{
+ GtkStyleContext *context;
+ gboolean selection_mode;
+
+ g_assert (HDY_IS_TITLE_BAR (self));
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ selection_mode = gtk_style_context_has_class (context, "selection-mode");
+
+ if (self->selection_mode == selection_mode)
+ return;
+
+ self->selection_mode = selection_mode;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTION_MODE]);
+}
+
+static void
+hdy_title_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTitleBar *self = HDY_TITLE_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SELECTION_MODE:
+ g_value_set_boolean (value, hdy_title_bar_get_selection_mode (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_title_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTitleBar *self = HDY_TITLE_BAR (object);
+
+ switch (prop_id) {
+ case PROP_SELECTION_MODE:
+ hdy_title_bar_set_selection_mode (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static gboolean
+hdy_title_bar_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (widget);
+ /* GtkWidget draws nothing by default so we have to render the background
+ * explicitly for HdyTitleBar to render the typical titlebar background.
+ */
+ gtk_render_background (context,
+ cr,
+ 0, 0,
+ gtk_widget_get_allocated_width (widget),
+ gtk_widget_get_allocated_height (widget));
+
+ return GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->draw (widget, cr);
+}
+
+/* This private method is prefixed by the class name because it will be a
+ * virtual method in GTK 4.
+ */
+static void
+hdy_title_bar_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ GtkWidget *child;
+ gint parent_min, parent_nat;
+ gint css_width, css_height, css_min;
+
+ child = gtk_bin_get_child (GTK_BIN (widget));
+
+ gtk_style_context_get (gtk_widget_get_style_context (widget),
+ gtk_widget_get_state_flags (widget),
+ "min-width", &css_width,
+ "min-height", &css_height,
+ NULL);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ css_min = css_width;
+ else
+ css_min = css_height;
+
+ if (child)
+ if (orientation == GTK_ORIENTATION_HORIZONTAL)
+ if (for_size != 1)
+ gtk_widget_get_preferred_width_for_height (child,
+ MAX (for_size, css_height),
+ &parent_min, &parent_nat);
+ else
+ gtk_widget_get_preferred_width (child, &parent_min, &parent_nat);
+ else
+ if (for_size != 1)
+ gtk_widget_get_preferred_height_for_width (child,
+ MAX (for_size, css_width),
+ &parent_min, &parent_nat);
+ else
+ gtk_widget_get_preferred_height (child, &parent_min, &parent_nat);
+ else {
+ parent_min = 0;
+ parent_nat = 0;
+ }
+
+ if (minimum)
+ *minimum = MAX (parent_min, css_min);
+
+ if (natural)
+ *natural = MAX (parent_nat, css_min);
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_title_bar_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_title_bar_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural, NULL, NULL);
+}
+
+static void
+hdy_title_bar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkAllocation clip;
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x,
+ allocation->y,
+ allocation->width,
+ allocation->height,
+ &clip);
+
+ GTK_WIDGET_CLASS (hdy_title_bar_parent_class)->size_allocate (widget, allocation);
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_title_bar_class_init (HdyTitleBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->get_property = hdy_title_bar_get_property;
+ object_class->set_property = hdy_title_bar_set_property;
+
+ widget_class->draw = hdy_title_bar_draw;
+ widget_class->get_preferred_width = hdy_title_bar_get_preferred_width;
+ widget_class->get_preferred_width_for_height = hdy_title_bar_get_preferred_width_for_height;
+ widget_class->get_preferred_height = hdy_title_bar_get_preferred_height;
+ widget_class->get_preferred_height_for_width = hdy_title_bar_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_title_bar_size_allocate;
+
+ /**
+ * HdyTitleBar:selection_mode:
+ *
+ * %TRUE if the title bar is in selection mode.
+ */
+ props[PROP_SELECTION_MODE] =
+ g_param_spec_boolean ("selection-mode",
+ _("Selection mode"),
+ _("Whether or not the title bar is in selection mode"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_TITLE_BAR);
+ /* Adwaita states it expects a headerbar to be the top-level titlebar widget,
+ * so style-wise HdyTitleBar pretends to be one as its role is to be the
+ * top-level titlebar widget.
+ */
+ gtk_widget_class_set_css_name (widget_class, "headerbar");
+ gtk_container_class_handle_border_width (container_class);
+}
+
+static void
+hdy_title_bar_init (HdyTitleBar *self)
+{
+ GtkStyleContext *context;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ /* Ensure the widget has the titlebar style class. */
+ gtk_style_context_add_class (context, "titlebar");
+
+ g_signal_connect (self, "style-updated", G_CALLBACK (style_updated_cb), NULL);
+}
+
+/**
+ * hdy_title_bar_new:
+ *
+ * Creates a new #HdyTitleBar.
+ *
+ * Returns: a new #HdyTitleBar
+ */
+GtkWidget *
+hdy_title_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_TITLE_BAR, NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-title-bar.h b/subprojects/libhandy/src/hdy-title-bar.h
new file mode 100644
index 0000000..275f4ea
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-title-bar.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TITLE_BAR (hdy_title_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTitleBar, hdy_title_bar, HDY, TITLE_BAR, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_title_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_title_bar_get_selection_mode (HdyTitleBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_title_bar_set_selection_mode (HdyTitleBar *self,
+ gboolean selection_mode);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-types.h b/subprojects/libhandy/src/hdy-types.h
new file mode 100644
index 0000000..56a3763
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-types.h
@@ -0,0 +1,17 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+G_BEGIN_DECLS
+
+typedef struct _HdySwipeTracker HdySwipeTracker;
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-value-object.c b/subprojects/libhandy/src/hdy-value-object.c
new file mode 100644
index 0000000..485d4b3
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-value-object.c
@@ -0,0 +1,267 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+#include <gobject/gvaluecollector.h>
+#include "hdy-value-object.h"
+
+/**
+ * SECTION:hdy-value-object
+ * @short_description: An object representing a #GValue.
+ * @Title: HdyValueObject
+ *
+ * The #HdyValueObject object represents a #GValue, allowing it to be
+ * used with #GListModel.
+ *
+ * Since: 0.0.8
+ */
+
+struct _HdyValueObject
+{
+ GObject parent_instance;
+
+ GValue value;
+};
+
+G_DEFINE_TYPE (HdyValueObject, hdy_value_object, G_TYPE_OBJECT)
+
+enum {
+ PROP_0,
+ PROP_VALUE,
+ N_PROPS
+};
+
+static GParamSpec *props [N_PROPS];
+
+/**
+ * hdy_value_object_new:
+ * @value: the #GValue to store
+ *
+ * Create a new #HdyValueObject.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject *
+hdy_value_object_new (const GValue *value)
+{
+ return g_object_new (HDY_TYPE_VALUE_OBJECT,
+ "value", value,
+ NULL);
+}
+
+/**
+ * hdy_value_object_new_collect: (skip)
+ * @type: the #GType of the value
+ * @...: the value to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method which uses
+ * the G_VALUE_COLLECT() macro internally.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_collect (GType type, ...)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+ g_autofree gchar *error = NULL;
+ va_list var_args;
+
+ va_start (var_args, type);
+
+ G_VALUE_COLLECT_INIT (&value, type, var_args, 0, &error);
+
+ va_end (var_args);
+
+ if (error)
+ g_critical ("%s: %s", G_STRFUNC, error);
+
+ return g_object_new (HDY_TYPE_VALUE_OBJECT,
+ "value", &value,
+ NULL);
+}
+
+/**
+ * hdy_value_object_new_string: (skip)
+ * @string: (transfer none): the string to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method to create a
+ * #HdyValueObject that stores a string.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_string (const gchar *string)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+
+ g_value_init (&value, G_TYPE_STRING);
+ g_value_set_string (&value, string);
+ return hdy_value_object_new (&value);
+}
+
+/**
+ * hdy_value_object_new_take_string: (skip)
+ * @string: (transfer full): the string to store
+ *
+ * Creates a new #HdyValueObject. This is a convenience method to create a
+ * #HdyValueObject that stores a string taking ownership of it.
+ *
+ * Returns: a new #HdyValueObject
+ * Since: 0.0.8
+ */
+HdyValueObject*
+hdy_value_object_new_take_string (gchar *string)
+{
+ g_auto(GValue) value = G_VALUE_INIT;
+
+ g_value_init (&value, G_TYPE_STRING);
+ g_value_take_string (&value, string);
+ return hdy_value_object_new (&value);
+}
+
+static void
+hdy_value_object_finalize (GObject *object)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+
+ g_value_unset (&self->value);
+
+ G_OBJECT_CLASS (hdy_value_object_parent_class)->finalize (object);
+}
+
+static void
+hdy_value_object_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+
+ switch (prop_id)
+ {
+ case PROP_VALUE:
+ g_value_set_boxed (value, &self->value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_value_object_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyValueObject *self = HDY_VALUE_OBJECT (object);
+ GValue *real_value;
+
+ switch (prop_id)
+ {
+ case PROP_VALUE:
+ /* construct only */
+ real_value = g_value_get_boxed (value);
+ g_value_init (&self->value, real_value->g_type);
+ g_value_copy (real_value, &self->value);
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_value_object_class_init (HdyValueObjectClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_value_object_finalize;
+ object_class->get_property = hdy_value_object_get_property;
+ object_class->set_property = hdy_value_object_set_property;
+
+ props[PROP_VALUE] =
+ g_param_spec_boxed ("value", C_("HdyValueObjectClass", "Value"),
+ C_("HdyValueObjectClass", "The contained value"),
+ G_TYPE_VALUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class,
+ N_PROPS,
+ props);
+}
+
+static void
+hdy_value_object_init (HdyValueObject *self)
+{
+}
+
+/**
+ * hdy_value_object_get_value:
+ * @value: the #HdyValueObject
+ *
+ * Return the contained value.
+ *
+ * Returns: (transfer none): the contained #GValue
+ * Since: 0.0.8
+ */
+const GValue*
+hdy_value_object_get_value (HdyValueObject *value)
+{
+ return &value->value;
+}
+
+/**
+ * hdy_value_object_copy_value:
+ * @value: the #HdyValueObject
+ * @dest: #GValue with correct type to copy into
+ *
+ * Copy data from the contained #GValue into @dest.
+ *
+ * Since: 0.0.8
+ */
+void
+hdy_value_object_copy_value (HdyValueObject *value,
+ GValue *dest)
+{
+ g_value_copy (&value->value, dest);
+}
+
+/**
+ * hdy_value_object_get_string:
+ * @value: the #HdyValueObject
+ *
+ * Returns the contained string if the value is of type #G_TYPE_STRING.
+ *
+ * Returns: (transfer none): the contained string
+ * Since: 0.0.8
+ */
+const gchar*
+hdy_value_object_get_string (HdyValueObject *value)
+{
+ return g_value_get_string (&value->value);
+}
+
+/**
+ * hdy_value_object_dup_string:
+ * @value: the #HdyValueObject
+ *
+ * Returns a copy of the contained string if the value is of type
+ * #G_TYPE_STRING.
+ *
+ * Returns: (transfer full): a copy of the contained string
+ * Since: 0.0.8
+ */
+gchar*
+hdy_value_object_dup_string (HdyValueObject *value)
+{
+ return g_value_dup_string (&value->value);
+}
+
diff --git a/subprojects/libhandy/src/hdy-value-object.h b/subprojects/libhandy/src/hdy-value-object.h
new file mode 100644
index 0000000..b44f106
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-value-object.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gio/gio.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VALUE_OBJECT (hdy_value_object_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyValueObject, hdy_value_object, HDY, VALUE_OBJECT, GObject)
+
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new (const GValue *value);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_collect (GType type,
+ ...);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_string (const gchar *string);
+HDY_AVAILABLE_IN_ALL
+HdyValueObject *hdy_value_object_new_take_string (gchar *string);
+
+HDY_AVAILABLE_IN_ALL
+const GValue* hdy_value_object_get_value (HdyValueObject *value);
+HDY_AVAILABLE_IN_ALL
+void hdy_value_object_copy_value (HdyValueObject *value,
+ GValue *dest);
+HDY_AVAILABLE_IN_ALL
+const gchar* hdy_value_object_get_string (HdyValueObject *value);
+HDY_AVAILABLE_IN_ALL
+gchar* hdy_value_object_dup_string (HdyValueObject *value);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-version.h.in b/subprojects/libhandy/src/hdy-version.h.in
new file mode 100644
index 0000000..0cb923f
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-version.h.in
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+/**
+ * SECTION:hdy-version
+ * @short_description: Handy version checking.
+ *
+ * Handy provides macros to check the version of the library at compile-time.
+ */
+
+/**
+ * HDY_MAJOR_VERSION:
+ *
+ * Hdy major version component (e.g. 1 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MAJOR_VERSION (@HDY_MAJOR_VERSION@)
+
+/**
+ * HDY_MINOR_VERSION:
+ *
+ * Hdy minor version component (e.g. 2 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MINOR_VERSION (@HDY_MINOR_VERSION@)
+
+/**
+ * HDY_MICRO_VERSION:
+ *
+ * Hdy micro version component (e.g. 3 if %HDY_VERSION is 1.2.3)
+ */
+#define HDY_MICRO_VERSION (@HDY_MICRO_VERSION@)
+
+/**
+ * HDY_VERSION
+ *
+ * Hdy version.
+ */
+#define HDY_VERSION (@HDY_VERSION@)
+
+/**
+ * HDY_VERSION_S:
+ *
+ * Handy version, encoded as a string, useful for printing and
+ * concatenation.
+ */
+#define HDY_VERSION_S "@HDY_VERSION@"
+
+#define HDY_ENCODE_VERSION(major,minor,micro) \
+ ((major) << 24 | (minor) << 16 | (micro) << 8)
+
+/**
+ * HDY_VERSION_HEX:
+ *
+ * Handy version, encoded as an hexadecimal number, useful for
+ * integer comparisons.
+ */
+#define HDY_VERSION_HEX \
+ (HDY_ENCODE_VERSION (HDY_MAJOR_VERSION, HDY_MINOR_VERSION, HDY_MICRO_VERSION))
+
+/**
+ * HDY_CHECK_VERSION:
+ * @major: required major version
+ * @minor: required minor version
+ * @micro: required micro version
+ *
+ * Compile-time version checking. Evaluates to %TRUE if the version
+ * of handy is greater than the required one.
+ */
+#define HDY_CHECK_VERSION(major,minor,micro) \
+ (HDY_MAJOR_VERSION > (major) || \
+ (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION > (minor)) || \
+ (HDY_MAJOR_VERSION == (major) && HDY_MINOR_VERSION == (minor) && \
+ HDY_MICRO_VERSION >= (micro)))
+
+#ifndef _HDY_EXTERN
+#define _HDY_EXTERN extern
+#endif
+
+#define HDY_AVAILABLE_IN_ALL _HDY_EXTERN
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.c b/subprojects/libhandy/src/hdy-view-switcher-bar.c
new file mode 100644
index 0000000..111d3e6
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.c
@@ -0,0 +1,392 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-enums.h"
+#include "hdy-view-switcher-bar.h"
+
+/**
+ * SECTION:hdy-view-switcher-bar
+ * @short_description: A view switcher action bar.
+ * @title: HdyViewSwitcherBar
+ * @See_also: #HdyViewSwitcher, #HdyViewSwitcherTitle
+ *
+ * An action bar letting you switch between multiple views offered by a
+ * #GtkStack, via an #HdyViewSwitcher. It is designed to be put at the bottom of
+ * a window and to be revealed only on really narrow windows e.g. on mobile
+ * phones. It can't be revealed if there are less than two pages.
+ *
+ * You can conveniently bind the #HdyViewSwitcherBar:reveal property to
+ * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher
+ * bar when the title label is displayed in place of the view switcher.
+ *
+ * An example of the UI definition for a common use case:
+ * |[
+ * <object class="GtkWindow"/>
+ * <child type="titlebar">
+ * <object class="HdyHeaderBar">
+ * <property name="centering-policy">strict</property>
+ * <child type="title">
+ * <object class="HdyViewSwitcherTitle"
+ * id="view_switcher_title">
+ * <property name="stack">stack</property>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * <child>
+ * <object class="GtkBox">
+ * <child>
+ * <object class="GtkStack" id="stack"/>
+ * </child>
+ * <child>
+ * <object class="HdyViewSwitcherBar">
+ * <property name="stack">stack</property>
+ * <property name="reveal"
+ * bind-source="view_switcher_title"
+ * bind-property="title-visible"
+ * bind-flags="sync-create"/>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcherBar has a single CSS node with name viewswitcherbar.
+ *
+ * Since: 0.0.10
+ */
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_STACK,
+ PROP_REVEAL,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcherBar
+{
+ GtkBin parent_instance;
+
+ GtkActionBar *action_bar;
+ GtkRevealer *revealer;
+ HdyViewSwitcher *view_switcher;
+
+ HdyViewSwitcherPolicy policy;
+ gboolean reveal;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, GTK_TYPE_BIN)
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+update_bar_revealed (HdyViewSwitcherBar *self) {
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+ gint count = 0;
+
+ if (self->reveal && stack)
+ gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count);
+
+ gtk_revealer_set_reveal_child (self->revealer, count > 1);
+}
+
+static void
+hdy_view_switcher_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_bar_get_policy (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_bar_get_stack (self));
+ break;
+ case PROP_REVEAL:
+ g_value_set_boolean (value, hdy_view_switcher_bar_get_reveal (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherBar *self = HDY_VIEW_SWITCHER_BAR (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_bar_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_bar_set_stack (self, g_value_get_object (value));
+ break;
+ case PROP_REVEAL:
+ hdy_view_switcher_bar_set_reveal (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_bar_class_init (HdyViewSwitcherBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_bar_get_property;
+ object_class->set_property = hdy_view_switcher_bar_set_property;
+
+ /**
+ * HdyViewSwitcherBar:policy:
+ *
+ * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine
+ * which mode to use.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_NARROW,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherBar:stack:
+ *
+ * The #GtkStack the #HdyViewSwitcher controls.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherBar:reveal:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_REVEAL] =
+ g_param_spec_boolean ("reveal",
+ _("Reveal"),
+ _("Whether the view switcher is revealed"),
+ FALSE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitcherbar");
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-bar.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, action_bar);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherBar, view_switcher);
+}
+
+static void
+hdy_view_switcher_bar_init (HdyViewSwitcherBar *self)
+{
+ /* This must be initialized before the template so the embedded view switcher
+ * can pick up the correct default value.
+ */
+ self->policy = HDY_VIEW_SWITCHER_POLICY_NARROW;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ self->revealer = GTK_REVEALER (gtk_bin_get_child (GTK_BIN (self->action_bar)));
+ update_bar_revealed (self);
+ gtk_revealer_set_transition_type (self->revealer, GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
+}
+
+/**
+ * hdy_view_switcher_bar_new:
+ *
+ * Creates a new #HdyViewSwitcherBar widget.
+ *
+ * Returns: a new #HdyViewSwitcherBar
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_BAR, NULL);
+}
+
+/**
+ * hdy_view_switcher_bar_get_policy:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 0.0.10
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ return self->policy;
+}
+
+/**
+ * hdy_view_switcher_bar_set_policy:
+ * @self: a #HdyViewSwitcherBar
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+
+ if (self->policy == policy)
+ return;
+
+ self->policy = policy;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_bar_get_stack:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 0.0.10
+ */
+GtkStack *
+hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), NULL);
+
+ return hdy_view_switcher_get_stack (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_bar_set_stack:
+ * @self: a #HdyViewSwitcherBar
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self,
+ GtkStack *stack)
+{
+ GtkStack *previous_stack;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ previous_stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (previous_stack == stack)
+ return;
+
+ if (previous_stack)
+ g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_bar_revealed), self);
+
+ hdy_view_switcher_set_stack (self->view_switcher, stack);
+
+ if (stack) {
+ g_signal_connect_swapped (stack, "add", G_CALLBACK (update_bar_revealed), self);
+ g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_bar_revealed), self);
+ }
+
+ update_bar_revealed (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
+
+/**
+ * hdy_view_switcher_bar_get_reveal:
+ * @self: a #HdyViewSwitcherBar
+ *
+ * Gets whether @self should be revealed or not.
+ *
+ * Returns: %TRUE if @self is revealed, %FALSE if not.
+ *
+ * Since: 0.0.10
+ */
+gboolean
+hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self), FALSE);
+
+ return self->reveal;
+}
+
+/**
+ * hdy_view_switcher_bar_set_reveal:
+ * @self: a #HdyViewSwitcherBar
+ * @reveal: %TRUE to reveal @self
+ *
+ * Sets whether @self should be revealed or not.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self,
+ gboolean reveal)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BAR (self));
+
+ reveal = !!reveal;
+
+ if (self->reveal == reveal)
+ return;
+
+ self->reveal = reveal;
+ update_bar_revealed (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEAL]);
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.h b/subprojects/libhandy/src/hdy-view-switcher-bar.h
new file mode 100644
index 0000000..be2db35
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+#include "hdy-view-switcher.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherBar, hdy_view_switcher_bar, HDY, VIEW_SWITCHER_BAR, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_view_switcher_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_bar_get_policy (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_policy (HdyViewSwitcherBar *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_bar_get_stack (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_stack (HdyViewSwitcherBar *self,
+ GtkStack *stack);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_bar_get_reveal (HdyViewSwitcherBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_bar_set_reveal (HdyViewSwitcherBar *self,
+ gboolean reveal);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-bar.ui b/subprojects/libhandy/src/hdy-view-switcher-bar.ui
new file mode 100644
index 0000000..a2b1266
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-bar.ui
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherBar" parent="GtkBin">
+ <child>
+ <object class="GtkActionBar" id="action_bar">
+ <property name="visible">True</property>
+ <child type="center">
+ <object class="HdyViewSwitcher" id="view_switcher">
+ <property name="margin-start">10</property>
+ <property name="margin-end">10</property>
+ <property name="narrow-ellipsize">end</property>
+ <property name="policy" bind-source="HdyViewSwitcherBar" bind-property="policy" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button-private.h b/subprojects/libhandy/src/hdy-view-switcher-button-private.h
new file mode 100644
index 0000000..5f85c52
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button-private.h
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherButton, hdy_view_switcher_button, HDY, VIEW_SWITCHER_BUTTON, GtkRadioButton)
+
+GtkWidget *hdy_view_switcher_button_new (void);
+
+const gchar *hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self,
+ const gchar *icon_name);
+
+GtkIconSize hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self,
+ GtkIconSize icon_size);
+
+gboolean hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self,
+ gboolean needs_attention);
+
+const gchar *hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self);
+void hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self,
+ const gchar *label);
+
+void hdy_view_switcher_button_set_narrow_ellipsize (HdyViewSwitcherButton *self,
+ PangoEllipsizeMode mode);
+
+void hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self,
+ gint *h_min_width,
+ gint *h_nat_width,
+ gint *v_min_width,
+ gint *v_nat_width);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.c b/subprojects/libhandy/src/hdy-view-switcher-button.c
new file mode 100644
index 0000000..20b57b0
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button.c
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-view-switcher-button-private.h"
+
+/**
+ * PRIVATE:hdy-view-switcher-button
+ * @short_description: Button used in #HdyViewSwitcher.
+ * @title: HdyViewSwitcherButton
+ * @See_also: #HdyViewSwitcher
+ * @stability: Private
+ *
+ * #HdyViewSwitcherButton represents an application's view. It is designed to be
+ * used exclusively internally by #HdyViewSwitcher.
+ *
+ * Since: 0.0.10
+ */
+
+enum {
+ PROP_0,
+ PROP_ICON_SIZE,
+ PROP_ICON_NAME,
+ PROP_NEEDS_ATTENTION,
+
+ /* Overridden properties */
+ PROP_LABEL,
+ PROP_ORIENTATION,
+
+ LAST_PROP = PROP_NEEDS_ATTENTION + 1,
+};
+
+struct _HdyViewSwitcherButton
+{
+ GtkRadioButton parent_instance;
+
+ GtkBox *horizontal_box;
+ GtkImage *horizontal_image;
+ GtkLabel *horizontal_label_active;
+ GtkLabel *horizontal_label_inactive;
+ GtkStack *horizontal_label_stack;
+ GtkStack *stack;
+ GtkBox *vertical_box;
+ GtkImage *vertical_image;
+ GtkLabel *vertical_label_active;
+ GtkLabel *vertical_label_inactive;
+ GtkStack *vertical_label_stack;
+
+ gchar *icon_name;
+ GtkIconSize icon_size;
+ gchar *label;
+ GtkOrientation orientation;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE_WITH_CODE (HdyViewSwitcherButton, hdy_view_switcher_button, GTK_TYPE_RADIO_BUTTON,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL))
+
+static void
+on_active_changed (HdyViewSwitcherButton *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self))) {
+ gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_active));
+ gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_active));
+ } else {
+ gtk_stack_set_visible_child (self->horizontal_label_stack, GTK_WIDGET (self->horizontal_label_inactive));
+ gtk_stack_set_visible_child (self->vertical_label_stack, GTK_WIDGET (self->vertical_label_inactive));
+ }
+}
+
+static GtkOrientation
+get_orientation (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ORIENTATION_HORIZONTAL);
+
+ return self->orientation;
+}
+
+static void
+set_orientation (HdyViewSwitcherButton *self,
+ GtkOrientation orientation)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (self->orientation == orientation)
+ return;
+
+ self->orientation = orientation;
+
+ gtk_stack_set_visible_child (self->stack,
+ GTK_WIDGET (self->orientation == GTK_ORIENTATION_VERTICAL ?
+ self->vertical_box :
+ self->horizontal_box));
+}
+
+static void
+hdy_view_switcher_button_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ g_value_set_string (value, hdy_view_switcher_button_get_icon_name (self));
+ break;
+ case PROP_ICON_SIZE:
+ g_value_set_int (value, hdy_view_switcher_button_get_icon_size (self));
+ break;
+ case PROP_NEEDS_ATTENTION:
+ g_value_set_boolean (value, hdy_view_switcher_button_get_needs_attention (self));
+ break;
+ case PROP_LABEL:
+ g_value_set_string (value, hdy_view_switcher_button_get_label (self));
+ break;
+ case PROP_ORIENTATION:
+ g_value_set_enum (value, get_orientation (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_button_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ switch (prop_id) {
+ case PROP_ICON_NAME:
+ hdy_view_switcher_button_set_icon_name (self, g_value_get_string (value));
+ break;
+ case PROP_ICON_SIZE:
+ hdy_view_switcher_button_set_icon_size (self, g_value_get_int (value));
+ break;
+ case PROP_NEEDS_ATTENTION:
+ hdy_view_switcher_button_set_needs_attention (self, g_value_get_boolean (value));
+ break;
+ case PROP_LABEL:
+ hdy_view_switcher_button_set_label (self, g_value_get_string (value));
+ break;
+ case PROP_ORIENTATION:
+ set_orientation (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_button_finalize (GObject *object)
+{
+ HdyViewSwitcherButton *self = HDY_VIEW_SWITCHER_BUTTON (object);
+
+ g_free (self->icon_name);
+ g_free (self->label);
+
+ G_OBJECT_CLASS (hdy_view_switcher_button_parent_class)->finalize (object);
+}
+
+static void
+hdy_view_switcher_button_class_init (HdyViewSwitcherButtonClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_button_get_property;
+ object_class->set_property = hdy_view_switcher_button_set_property;
+ object_class->finalize = hdy_view_switcher_button_finalize;
+
+ g_object_class_override_property (object_class,
+ PROP_LABEL,
+ "label");
+
+ g_object_class_override_property (object_class,
+ PROP_ORIENTATION,
+ "orientation");
+
+ /**
+ * HdyViewSwitcherButton:icon-name:
+ *
+ * The icon name representing the view, or %NULL for no icon.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_NAME] =
+ g_param_spec_string ("icon-name",
+ _("Icon Name"),
+ _("Icon name for image"),
+ "text-x-generic-symbolic",
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ /**
+ * HdyViewSwitcherButton:icon-size:
+ *
+ * The icon size.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_ICON_SIZE] =
+ g_param_spec_int ("icon-size",
+ _("Icon Size"),
+ _("Symbolic size to use for named icon"),
+ 0, G_MAXINT, GTK_ICON_SIZE_BUTTON,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ /**
+ * HdyViewSwitcherButton:needs-attention:
+ *
+ * Sets a flag specifying whether the view requires the user attention. This
+ * is used by the HdyViewSwitcher to change the appearance of the
+ * corresponding button when a view needs attention and it is not the current
+ * one.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_NEEDS_ATTENTION] =
+ g_param_spec_boolean ("needs-attention",
+ _("Needs attention"),
+ _("Hint the view needs attention"),
+ FALSE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /* We probably should set the class's CSS name to "viewswitcherbutton"
+ * here, but it doesn't work because GtkCheckButton hardcodes it to "button"
+ * on instantiation, and the functions required to override it are private.
+ * In the meantime, we can use the "viewswitcher > button" CSS selector as
+ * a fairly safe fallback.
+ */
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-button.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_image);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_active);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_inactive);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, horizontal_label_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_image);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_active);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_inactive);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherButton, vertical_label_stack);
+ gtk_widget_class_bind_template_callback (widget_class, on_active_changed);
+}
+
+static void
+hdy_view_switcher_button_init (HdyViewSwitcherButton *self)
+{
+ self->icon_size = GTK_ICON_SIZE_BUTTON;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_stack_set_visible_child (GTK_STACK (self->stack), GTK_WIDGET (self->horizontal_box));
+
+ gtk_widget_set_focus_on_click (GTK_WIDGET (self), FALSE);
+ /* Make the button look like a regular button and not a radio button. */
+ gtk_toggle_button_set_mode (GTK_TOGGLE_BUTTON (self), FALSE);
+
+ on_active_changed (self);
+}
+
+/**
+ * hdy_view_switcher_button_new:
+ *
+ * Creates a new #HdyViewSwitcherButton widget.
+ *
+ * Returns: a new #HdyViewSwitcherButton
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_button_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_BUTTON, NULL);
+}
+
+/**
+ * hdy_view_switcher_button_get_icon_name:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the icon name representing the view, or %NULL is no icon is set.
+ *
+ * Returns: (transfer none) (nullable): the icon name, or %NULL
+ *
+ * Since: 0.0.10
+ **/
+const gchar *
+hdy_view_switcher_button_get_icon_name (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL);
+
+ return self->icon_name;
+}
+
+/**
+ * hdy_view_switcher_button_set_icon_name:
+ * @self: a #HdyViewSwitcherButton
+ * @icon_name: (nullable): an icon name or %NULL
+ *
+ * Sets the icon name representing the view, or %NULL to disable the icon.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_icon_name (HdyViewSwitcherButton *self,
+ const gchar *icon_name)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (!g_strcmp0 (self->icon_name, icon_name))
+ return;
+
+ g_free (self->icon_name);
+ self->icon_name = g_strdup (icon_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]);
+}
+
+/**
+ * hdy_view_switcher_button_get_icon_size:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the icon size used by @self.
+ *
+ * Returns: the icon size used by @self
+ *
+ * Since: 0.0.10
+ **/
+GtkIconSize
+hdy_view_switcher_button_get_icon_size (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), GTK_ICON_SIZE_INVALID);
+
+ return self->icon_size;
+}
+
+/**
+ * hdy_view_switcher_button_set_icon_size:
+ * @self: a #HdyViewSwitcherButton
+ * @icon_size: the new icon size
+ *
+ * Sets the icon size used by @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_set_icon_size (HdyViewSwitcherButton *self,
+ GtkIconSize icon_size)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (self->icon_size == icon_size)
+ return;
+
+ self->icon_size = icon_size;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_SIZE]);
+}
+
+/**
+ * hdy_view_switcher_button_get_needs_attention:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets whether the view represented by @self requires the user attention.
+ *
+ * Returns: %TRUE if the view represented by @self requires the user attention, %FALSE otherwise
+ *
+ * Since: 0.0.10
+ **/
+gboolean
+hdy_view_switcher_button_get_needs_attention (HdyViewSwitcherButton *self)
+{
+ GtkStyleContext *context;
+
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), FALSE);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ return gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+}
+
+/**
+ * hdy_view_switcher_button_set_needs_attention:
+ * @self: a #HdyViewSwitcherButton
+ * @needs_attention: the new icon size
+ *
+ * Sets whether the view represented by @self requires the user attention.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_set_needs_attention (HdyViewSwitcherButton *self,
+ gboolean needs_attention)
+{
+ GtkStyleContext *context;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ needs_attention = !!needs_attention;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ if (gtk_style_context_has_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION) == needs_attention)
+ return;
+
+ if (needs_attention)
+ gtk_style_context_add_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+ else
+ gtk_style_context_remove_class (context, GTK_STYLE_CLASS_NEEDS_ATTENTION);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION]);
+}
+
+/**
+ * hdy_view_switcher_button_get_label:
+ * @self: a #HdyViewSwitcherButton
+ *
+ * Gets the label representing the view.
+ *
+ * Returns: (transfer none) (nullable): the label, or %NULL
+ *
+ * Since: 0.0.10
+ **/
+const gchar *
+hdy_view_switcher_button_get_label (HdyViewSwitcherButton *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self), NULL);
+
+ return self->label;
+}
+
+/**
+ * hdy_view_switcher_button_set_label:
+ * @self: a #HdyViewSwitcherButton
+ * @label: (nullable): a label or %NULL
+ *
+ * Sets the label representing the view.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_label (HdyViewSwitcherButton *self,
+ const gchar *label)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+
+ if (!g_strcmp0 (self->label, label))
+ return;
+
+ g_free (self->label);
+ self->label = g_strdup (label);
+
+ g_object_notify (G_OBJECT (self), "label");
+}
+
+/**
+ * hdy_view_switcher_button_set_narrow_ellipsize:
+ * @self: a #HdyViewSwitcherButton
+ * @mode: a #PangoEllipsizeMode
+ *
+ * Set the mode used to ellipsize the text in narrow mode if there is not
+ * enough space to render the entire string.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_button_set_narrow_ellipsize (HdyViewSwitcherButton *self,
+ PangoEllipsizeMode mode)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_BUTTON (self));
+ g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END);
+
+ gtk_label_set_ellipsize (self->vertical_label_active, mode);
+ gtk_label_set_ellipsize (self->vertical_label_inactive, mode);
+}
+
+/**
+ * hdy_view_switcher_button_get_size:
+ * @self: a #HdyViewSwitcherButton
+ * @h_min_width: (out) (nullable): the minimum width when horizontal
+ * @h_nat_width: (out) (nullable): the natural width when horizontal
+ * @v_min_width: (out) (nullable): the minimum width when vertical
+ * @v_nat_width: (out) (nullable): the natural width when vertical
+ *
+ * Measure the size requests in both horizontal and vertical modes.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_button_get_size (HdyViewSwitcherButton *self,
+ gint *h_min_width,
+ gint *h_nat_width,
+ gint *v_min_width,
+ gint *v_nat_width)
+{
+ GtkStyleContext *context;
+ GtkStateFlags state;
+ GtkBorder border;
+
+ /* gtk_widget_get_preferred_width() doesn't accept both its out parameters to
+ * be NULL, so we must have guards.
+ */
+ if (h_min_width != NULL || h_nat_width != NULL)
+ gtk_widget_get_preferred_width (GTK_WIDGET (self->horizontal_box), h_min_width, h_nat_width);
+ if (v_min_width != NULL || v_nat_width != NULL)
+ gtk_widget_get_preferred_width (GTK_WIDGET (self->vertical_box), v_min_width, v_nat_width);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ state = gtk_style_context_get_state (context);
+ gtk_style_context_get_border (context, state, &border);
+ if (h_min_width != NULL)
+ *h_min_width += border.left + border.right;
+ if (h_nat_width != NULL)
+ *h_nat_width += border.left + border.right;
+ if (v_min_width != NULL)
+ *v_min_width += border.left + border.right;
+ if (v_nat_width != NULL)
+ *v_nat_width += border.left + border.right;
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-button.ui b/subprojects/libhandy/src/hdy-view-switcher-button.ui
new file mode 100644
index 0000000..018b6cc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-button.ui
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherButton" parent="GtkRadioButton">
+ <signal name="notify::active" handler="on_active_changed" after="yes"/>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="hhomogeneous">False</property>
+ <property name="transition-type">crossfade</property>
+ <property name="vhomogeneous">True</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkBox" id="horizontal_box">
+ <property name="halign">center</property>
+ <property name="orientation">horizontal</property>
+ <property name="spacing">8</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="wide"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="horizontal_image">
+ <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" />
+ <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="horizontal_label_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="horizontal_label_inactive">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">inactive</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="horizontal_label_active">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ <style>
+ <class name="active"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">active</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="vertical_box">
+ <property name="halign">center</property>
+ <property name="orientation">vertical</property>
+ <property name="spacing">4</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="narrow"/>
+ </style>
+ <child>
+ <object class="GtkImage" id="vertical_image">
+ <property name="icon-name" bind-source="HdyViewSwitcherButton" bind-property="icon-name" bind-flags="sync-create" />
+ <property name="icon-size" bind-source="HdyViewSwitcherButton" bind-property="icon-size" bind-flags="sync-create" />
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStack" id="vertical_label_stack">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="vertical_label_inactive">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ </object>
+ <packing>
+ <property name="name">inactive</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkLabel" id="vertical_label_active">
+ <property name="label" bind-source="HdyViewSwitcherButton" bind-property="label" bind-flags="sync-create|bidirectional" />
+ <property name="visible">True</property>
+ <style>
+ <class name="active"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">active</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.c b/subprojects/libhandy/src/hdy-view-switcher-title.c
new file mode 100644
index 0000000..39bdafa
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.c
@@ -0,0 +1,600 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-view-switcher-title.h"
+#include "hdy-squeezer.h"
+
+/**
+ * SECTION:hdy-view-switcher-title
+ * @short_description: A view switcher title.
+ * @title: HdyViewSwitcherTitle
+ * @See_also: #HdyHeaderBar, #HdyViewSwitcher, #HdyViewSwitcherBar
+ *
+ * A widget letting you switch between multiple views offered by a #GtkStack,
+ * via an #HdyViewSwitcher. It is designed to be used as the title widget of a
+ * #HdyHeaderBar, and will display the window's title when the window is too
+ * narrow to fit the view switcher e.g. on mobile phones, or if there are less
+ * than two views.
+ *
+ * You can conveniently bind the #HdyViewSwitcherBar:reveal property to
+ * #HdyViewSwitcherTitle:title-visible to automatically reveal the view switcher
+ * bar when the title label is displayed in place of the view switcher.
+ *
+ * An example of the UI definition for a common use case:
+ * |[
+ * <object class="GtkWindow"/>
+ * <child type="titlebar">
+ * <object class="HdyHeaderBar">
+ * <property name="centering-policy">strict</property>
+ * <child type="title">
+ * <object class="HdyViewSwitcherTitle"
+ * id="view_switcher_title">
+ * <property name="stack">stack</property>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * <child>
+ * <object class="GtkBox">
+ * <child>
+ * <object class="GtkStack" id="stack"/>
+ * </child>
+ * <child>
+ * <object class="HdyViewSwitcherBar">
+ * <property name="stack">stack</property>
+ * <property name="reveal"
+ * bind-source="view_switcher_title"
+ * bind-property="title-visible"
+ * bind-flags="sync-create"/>
+ * </object>
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcherTitle has a single CSS node with name viewswitchertitle.
+ *
+ * Since: 1.0
+ */
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_STACK,
+ PROP_TITLE,
+ PROP_SUBTITLE,
+ PROP_VIEW_SWITCHER_ENABLED,
+ PROP_TITLE_VISIBLE,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcherTitle
+{
+ GtkBin parent_instance;
+
+ HdySqueezer *squeezer;
+ GtkLabel *subtitle_label;
+ GtkBox *title_box;
+ GtkLabel *title_label;
+ HdyViewSwitcher *view_switcher;
+
+ gboolean view_switcher_enabled;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, GTK_TYPE_BIN)
+
+static void
+update_subtitle_label (HdyViewSwitcherTitle *self)
+{
+ const gchar *subtitle = gtk_label_get_label (self->subtitle_label);
+
+ gtk_widget_set_visible (GTK_WIDGET (self->subtitle_label), subtitle && subtitle[0]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+count_children_cb (GtkWidget *widget,
+ gint *count)
+{
+ (*count)++;
+}
+
+static void
+update_view_switcher_visible (HdyViewSwitcherTitle *self)
+{
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+ gint count = 0;
+
+ if (self->view_switcher_enabled && stack)
+ gtk_container_foreach (GTK_CONTAINER (stack), (GtkCallback) count_children_cb, &count);
+
+ hdy_squeezer_set_child_enabled (self->squeezer, GTK_WIDGET (self->view_switcher), count > 1);
+}
+
+static void
+notify_squeezer_visible_child_cb (GObject *self)
+{
+ g_object_notify_by_pspec (self, props[PROP_TITLE_VISIBLE]);
+}
+
+static void
+hdy_view_switcher_title_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_title_get_policy (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_title_get_stack (self));
+ break;
+ case PROP_TITLE:
+ g_value_set_string (value, hdy_view_switcher_title_get_title (self));
+ break;
+ case PROP_SUBTITLE:
+ g_value_set_string (value, hdy_view_switcher_title_get_subtitle (self));
+ break;
+ case PROP_VIEW_SWITCHER_ENABLED:
+ g_value_set_boolean (value, hdy_view_switcher_title_get_view_switcher_enabled (self));
+ break;
+ case PROP_TITLE_VISIBLE:
+ g_value_set_boolean (value, hdy_view_switcher_title_get_title_visible (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_title_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcherTitle *self = HDY_VIEW_SWITCHER_TITLE (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_title_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_title_set_stack (self, g_value_get_object (value));
+ break;
+ case PROP_TITLE:
+ hdy_view_switcher_title_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_SUBTITLE:
+ hdy_view_switcher_title_set_subtitle (self, g_value_get_string (value));
+ break;
+ case PROP_VIEW_SWITCHER_ENABLED:
+ hdy_view_switcher_title_set_view_switcher_enabled (self, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_title_dispose (GObject *object) {
+ HdyViewSwitcherTitle *self = (HdyViewSwitcherTitle *)object;
+
+ if (self->view_switcher) {
+ GtkStack *stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (stack)
+ g_signal_handlers_disconnect_by_func (stack, G_CALLBACK (update_view_switcher_visible), self);
+ }
+
+ G_OBJECT_CLASS (hdy_view_switcher_title_parent_class)->dispose (object);
+}
+
+static void
+hdy_view_switcher_title_class_init (HdyViewSwitcherTitleClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = hdy_view_switcher_title_dispose;
+ object_class->get_property = hdy_view_switcher_title_get_property;
+ object_class->set_property = hdy_view_switcher_title_set_property;
+
+ /**
+ * HdyViewSwitcherTitle:policy:
+ *
+ * The #HdyViewSwitcherPolicy the #HdyViewSwitcher should use to determine
+ * which mode to use.
+ *
+ * Since: 1.0
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:stack:
+ *
+ * The #GtkStack the #HdyViewSwitcher controls.
+ *
+ * Since: 1.0
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:title:
+ *
+ * The title of the #HdyViewSwitcher.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("The title to display"),
+ NULL,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:subtitle:
+ *
+ * The subtitle of the #HdyViewSwitcher.
+ *
+ * Since: 1.0
+ */
+ props[PROP_SUBTITLE] =
+ g_param_spec_string ("subtitle",
+ _("Subtitle"),
+ _("The subtitle to display"),
+ NULL,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:view-switcher-enabled:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 1.0
+ */
+ props[PROP_VIEW_SWITCHER_ENABLED] =
+ g_param_spec_boolean ("view-switcher-enabled",
+ _("View switcher enabled"),
+ _("Whether the view switcher is enabled"),
+ TRUE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcherTitle:title-visible:
+ *
+ * Whether the bar should be revealed or hidden.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TITLE_VISIBLE] =
+ g_param_spec_boolean ("title-visible",
+ _("Title visible"),
+ _("Whether the title label is visible"),
+ TRUE,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitchertitle");
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-view-switcher-title.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, squeezer);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, subtitle_label);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, title_label);
+ gtk_widget_class_bind_template_child (widget_class, HdyViewSwitcherTitle, view_switcher);
+ gtk_widget_class_bind_template_callback (widget_class, notify_squeezer_visible_child_cb);
+}
+
+static void
+hdy_view_switcher_title_init (HdyViewSwitcherTitle *self)
+{
+ /* This must be initialized before the template so the embedded view switcher
+ * can pick up the correct default value.
+ */
+ self->view_switcher_enabled = TRUE;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ update_subtitle_label (self);
+ update_view_switcher_visible (self);
+}
+
+/**
+ * hdy_view_switcher_title_new:
+ *
+ * Creates a new #HdyViewSwitcherTitle widget.
+ *
+ * Returns: a new #HdyViewSwitcherTitle
+ *
+ * Since: 1.0
+ */
+HdyViewSwitcherTitle *
+hdy_view_switcher_title_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER_TITLE, NULL);
+}
+
+/**
+ * hdy_view_switcher_title_get_policy:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 1.0
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ return hdy_view_switcher_get_policy (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_title_set_policy:
+ * @self: a #HdyViewSwitcherTitle
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (hdy_view_switcher_get_policy (self->view_switcher) == policy)
+ return;
+
+ hdy_view_switcher_set_policy (self->view_switcher, policy);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_title_get_stack:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 1.0
+ */
+GtkStack *
+hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return hdy_view_switcher_get_stack (self->view_switcher);
+}
+
+/**
+ * hdy_view_switcher_title_set_stack:
+ * @self: a #HdyViewSwitcherTitle
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self,
+ GtkStack *stack)
+{
+ GtkStack *previous_stack;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ previous_stack = hdy_view_switcher_get_stack (self->view_switcher);
+
+ if (previous_stack == stack)
+ return;
+
+ if (previous_stack)
+ g_signal_handlers_disconnect_by_func (previous_stack, G_CALLBACK (update_view_switcher_visible), self);
+
+ hdy_view_switcher_set_stack (self->view_switcher, stack);
+
+ if (stack) {
+ g_signal_connect_swapped (stack, "add", G_CALLBACK (update_view_switcher_visible), self);
+ g_signal_connect_swapped (stack, "remove", G_CALLBACK (update_view_switcher_visible), self);
+ }
+
+ update_view_switcher_visible (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
+
+/**
+ * hdy_view_switcher_title_get_title:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the title of @self. See hdy_view_switcher_title_set_title().
+ *
+ * Returns: (transfer none) (nullable): the title of @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return gtk_label_get_label (self->title_label);
+}
+
+/**
+ * hdy_view_switcher_title_set_title:
+ * @self: a #HdyViewSwitcherTitle
+ * @title: (nullable): a title, or %NULL
+ *
+ * Sets the title of @self. The title should give a user additional details. A
+ * good title should not include the application name.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self,
+ const gchar *title)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (g_strcmp0 (gtk_label_get_label (self->title_label), title) == 0)
+ return;
+
+ gtk_label_set_label (self->title_label, title);
+ gtk_widget_set_visible (GTK_WIDGET (self->title_label), title && title[0]);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * hdy_view_switcher_title_get_subtitle:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets the subtitle of @self. See hdy_view_switcher_title_set_subtitle().
+ *
+ * Returns: (transfer none) (nullable): the subtitle of @self, or %NULL.
+ *
+ * Since: 1.0
+ */
+const gchar *
+hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), NULL);
+
+ return gtk_label_get_label (self->subtitle_label);
+}
+
+/**
+ * hdy_view_switcher_title_set_subtitle:
+ * @self: a #HdyViewSwitcherTitle
+ * @subtitle: (nullable): a subtitle, or %NULL
+ *
+ * Sets the subtitle of @self. The subtitle should give a user additional
+ * details.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self,
+ const gchar *subtitle)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ if (g_strcmp0 (gtk_label_get_label (self->subtitle_label), subtitle) == 0)
+ return;
+
+ gtk_label_set_label (self->subtitle_label, subtitle);
+ update_subtitle_label (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUBTITLE]);
+}
+
+/**
+ * hdy_view_switcher_title_get_view_switcher_enabled:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Gets whether @self's view switcher is enabled.
+ *
+ * See hdy_view_switcher_title_set_view_switcher_enabled().
+ *
+ * Returns: %TRUE if the view switcher is enabled, %FALSE otherwise.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
+
+ return self->view_switcher_enabled;
+}
+
+/**
+ * hdy_view_switcher_title_set_view_switcher_enabled:
+ * @self: a #HdyViewSwitcherTitle
+ * @enabled: %TRUE to enable the view switcher, %FALSE to disable it
+ *
+ * Make @self enable or disable its view switcher. If it is disabled, the title
+ * will be displayed instead. This allows to programmatically and prematurely
+ * hide the view switcher of @self even if it fits in the available space.
+ *
+ * This can be used e.g. to ensure the view switcher is hidden below a certain
+ * window width, or any other constraint you find suitable.
+ *
+ * Since: 1.0
+ */
+void
+hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self,
+ gboolean enabled)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self));
+
+ enabled = !!enabled;
+
+ if (self->view_switcher_enabled == enabled)
+ return;
+
+ self->view_switcher_enabled = enabled;
+ update_view_switcher_visible (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW_SWITCHER_ENABLED]);
+}
+
+/**
+ * hdy_view_switcher_title_get_title_visible:
+ * @self: a #HdyViewSwitcherTitle
+ *
+ * Get whether the title label of @self is visible.
+ *
+ * Returns: %TRUE if the title label of @self is visible, %FALSE if not.
+ *
+ * Since: 1.0
+ */
+gboolean
+hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER_TITLE (self), FALSE);
+
+ return hdy_squeezer_get_visible_child (self->squeezer) == (GtkWidget *) self->title_box;
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.h b/subprojects/libhandy/src/hdy-view-switcher-title.h
new file mode 100644
index 0000000..2540396
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+#include "hdy-view-switcher.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER_TITLE (hdy_view_switcher_title_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcherTitle, hdy_view_switcher_title, HDY, VIEW_SWITCHER_TITLE, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherTitle *hdy_view_switcher_title_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_title_get_policy (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_policy (HdyViewSwitcherTitle *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_title_get_stack (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_stack (HdyViewSwitcherTitle *self,
+ GtkStack *stack);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_view_switcher_title_get_title (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_title (HdyViewSwitcherTitle *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_view_switcher_title_get_subtitle (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_subtitle (HdyViewSwitcherTitle *self,
+ const gchar *subtitle);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_title_get_view_switcher_enabled (HdyViewSwitcherTitle *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_title_set_view_switcher_enabled (HdyViewSwitcherTitle *self,
+ gboolean enabled);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_view_switcher_title_get_title_visible (HdyViewSwitcherTitle *self);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-view-switcher-title.ui b/subprojects/libhandy/src/hdy-view-switcher-title.ui
new file mode 100644
index 0000000..1c706bc
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher-title.ui
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.0"/>
+ <template class="HdyViewSwitcherTitle" parent="GtkBin">
+ <child>
+ <object class="HdySqueezer" id="squeezer">
+ <property name="transition-type">crossfade</property>
+ <property name="visible">True</property>
+ <property name="no-show-all">True</property>
+ <signal name="notify::visible-child" handler="notify_squeezer_visible_child_cb" swapped="yes"/>
+ <child>
+ <object class="HdyViewSwitcher" id="view_switcher">
+ <property name="visible">True</property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="title_box">
+ <property name="orientation">vertical</property>
+ <property name="halign">center</property>
+ <property name="valign">center</property>
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkLabel" id="title_label">
+ <property name="ellipsize">end</property>
+ <property name="halign">center</property>
+ <property name="wrap">False</property>
+ <property name="single-line-mode">True</property>
+ <property name="visible">True</property>
+ <property name="width-chars">5</property>
+ <style>
+ <class name="title"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="subtitle_label">
+ <property name="ellipsize">end</property>
+ <property name="halign">center</property>
+ <property name="wrap">False</property>
+ <property name="single-line-mode">True</property>
+ <property name="visible">True</property>
+ <style>
+ <class name="subtitle"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/subprojects/libhandy/src/hdy-view-switcher.c b/subprojects/libhandy/src/hdy-view-switcher.c
new file mode 100644
index 0000000..26c5bcf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher.c
@@ -0,0 +1,734 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * Based on gtkstackswitcher.c, Copyright (c) 2013 Red Hat, Inc.
+ * https://gitlab.gnome.org/GNOME/gtk/blob/a0129f556b1fd655215165739d0277d7f7a2c1a8/gtk/gtkstackswitcher.c
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-css-private.h"
+#include "hdy-enums.h"
+#include "hdy-view-switcher.h"
+#include "hdy-view-switcher-button-private.h"
+
+/**
+ * SECTION:hdy-view-switcher
+ * @short_description: An adaptive view switcher.
+ * @title: HdyViewSwitcher
+ *
+ * An adaptive view switcher, designed to switch between multiple views in a
+ * similar fashion than a #GtkStackSwitcher.
+ *
+ * Depending on the available width, the view switcher can adapt from a wide
+ * mode showing the view's icon and title side by side, to a narrow mode showing
+ * the view's icon and title one on top of the other, in a more compact way.
+ * This can be controlled via the policy property.
+ *
+ * To look good in a header bar, an #HdyViewSwitcher requires to fill its full
+ * height. Contrary to #GtkHeaderBar, #HdyHeaderBar doesn't force a vertical
+ * alignment on its title widget, so we recommend it over #GtkHeaderBar.
+ *
+ * # CSS nodes
+ *
+ * #HdyViewSwitcher has a single CSS node with name viewswitcher.
+ *
+ * Since: 0.0.10
+ */
+
+/**
+ * HdyViewSwitcherPolicy:
+ * @HDY_VIEW_SWITCHER_POLICY_AUTO: Automatically adapt to the best fitting mode
+ * @HDY_VIEW_SWITCHER_POLICY_NARROW: Force the narrow mode
+ * @HDY_VIEW_SWITCHER_POLICY_WIDE: Force the wide mode
+ */
+
+#define MIN_NAT_BUTTON_WIDTH 100
+#define TIMEOUT_EXPAND 500
+
+enum {
+ PROP_0,
+ PROP_POLICY,
+ PROP_NARROW_ELLIPSIZE,
+ PROP_STACK,
+ LAST_PROP,
+};
+
+struct _HdyViewSwitcher
+{
+ GtkBin parent_instance;
+
+ GtkWidget *box;
+ GHashTable *buttons;
+ gboolean in_child_changed;
+ GtkWidget *switch_button;
+ guint switch_timer;
+
+ HdyViewSwitcherPolicy policy;
+ PangoEllipsizeMode narrow_ellipsize;
+ GtkStack *stack;
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (HdyViewSwitcher, hdy_view_switcher, GTK_TYPE_BIN)
+
+static void
+set_visible_stack_child_for_button (HdyViewSwitcher *self,
+ HdyViewSwitcherButton *button)
+{
+ if (self->in_child_changed)
+ return;
+
+ gtk_stack_set_visible_child (self->stack, GTK_WIDGET (g_object_get_data (G_OBJECT (button), "stack-child")));
+}
+
+static void
+update_button (HdyViewSwitcher *self,
+ GtkWidget *widget,
+ HdyViewSwitcherButton *button)
+{
+ g_autofree gchar *title = NULL;
+ g_autofree gchar *icon_name = NULL;
+ gboolean needs_attention;
+
+ gtk_container_child_get (GTK_CONTAINER (self->stack), widget,
+ "title", &title,
+ "icon-name", &icon_name,
+ "needs-attention", &needs_attention,
+ NULL);
+
+ g_object_set (G_OBJECT (button),
+ "icon-name", icon_name,
+ "icon-size", GTK_ICON_SIZE_BUTTON,
+ "label", title,
+ "needs-attention", needs_attention,
+ NULL);
+
+ gtk_widget_set_visible (GTK_WIDGET (button),
+ gtk_widget_get_visible (widget) && (title != NULL || icon_name != NULL));
+}
+
+static void
+on_stack_child_updated (GtkWidget *widget,
+ GParamSpec *pspec,
+ HdyViewSwitcher *self)
+{
+ update_button (self, widget, g_hash_table_lookup (self->buttons, widget));
+}
+
+static void
+on_position_updated (GtkWidget *widget,
+ GParamSpec *pspec,
+ HdyViewSwitcher *self)
+{
+ GtkWidget *button = g_hash_table_lookup (self->buttons, widget);
+ gint position;
+
+ gtk_container_child_get (GTK_CONTAINER (self->stack), widget,
+ "position", &position,
+ NULL);
+ gtk_box_reorder_child (GTK_BOX (self->box), button, position);
+}
+
+static void
+remove_switch_timer (HdyViewSwitcher *self)
+{
+ if (!self->switch_timer)
+ return;
+
+ g_source_remove (self->switch_timer);
+ self->switch_timer = 0;
+}
+
+static gboolean
+hdy_view_switcher_switch_timeout (gpointer data)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (data);
+ GtkWidget *button = self->switch_button;
+
+ self->switch_timer = 0;
+ self->switch_button = NULL;
+
+ if (button)
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
+
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+hdy_view_switcher_drag_motion (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+ GtkAllocation allocation;
+ GtkWidget *button;
+ GHashTableIter iter;
+ gpointer value;
+ gboolean retval = FALSE;
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ x += allocation.x;
+ y += allocation.y;
+
+ button = NULL;
+ g_hash_table_iter_init (&iter, self->buttons);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ gtk_widget_get_allocation (GTK_WIDGET (value), &allocation);
+ if (x >= allocation.x && x <= allocation.x + allocation.width &&
+ y >= allocation.y && y <= allocation.y + allocation.height) {
+ button = GTK_WIDGET (value);
+ retval = TRUE;
+
+ break;
+ }
+ }
+
+ if (button != self->switch_button)
+ remove_switch_timer (self);
+
+ self->switch_button = button;
+
+ if (button && !self->switch_timer) {
+ self->switch_timer = gdk_threads_add_timeout (TIMEOUT_EXPAND,
+ hdy_view_switcher_switch_timeout,
+ self);
+ g_source_set_name_by_id (self->switch_timer, "[gtk+] hdy_view_switcher_switch_timeout");
+ }
+
+ return retval;
+}
+
+static void
+hdy_view_switcher_drag_leave (GtkWidget *widget,
+ GdkDragContext *context,
+ guint time)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+
+ remove_switch_timer (self);
+}
+
+static void
+add_button_for_stack_child (HdyViewSwitcher *self,
+ GtkWidget *stack_child)
+{
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ HdyViewSwitcherButton *button = HDY_VIEW_SWITCHER_BUTTON (hdy_view_switcher_button_new ());
+
+ g_object_set_data (G_OBJECT (button), "stack-child", stack_child);
+ hdy_view_switcher_button_set_narrow_ellipsize (button, self->narrow_ellipsize);
+
+ update_button (self, stack_child, button);
+
+ if (children != NULL)
+ gtk_radio_button_join_group (GTK_RADIO_BUTTON (button), GTK_RADIO_BUTTON (children->data));
+
+ gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (button));
+
+ g_signal_connect_swapped (button, "clicked", G_CALLBACK (set_visible_stack_child_for_button), self);
+ g_signal_connect (stack_child, "notify::visible", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::title", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::icon-name", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::needs-attention", G_CALLBACK (on_stack_child_updated), self);
+ g_signal_connect (stack_child, "child-notify::position", G_CALLBACK (on_position_updated), self);
+
+ g_hash_table_insert (self->buttons, stack_child, button);
+}
+
+static void
+add_button_for_stack_child_cb (GtkWidget *stack_child,
+ HdyViewSwitcher *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (GTK_IS_WIDGET (stack_child));
+
+ add_button_for_stack_child (self, stack_child);
+}
+
+static void
+remove_button_for_stack_child (HdyViewSwitcher *self,
+ GtkWidget *stack_child)
+{
+ g_signal_handlers_disconnect_by_func (stack_child, on_stack_child_updated, self);
+ g_signal_handlers_disconnect_by_func (stack_child, on_position_updated, self);
+ gtk_container_remove (GTK_CONTAINER (self->box), g_hash_table_lookup (self->buttons, stack_child));
+ g_hash_table_remove (self->buttons, stack_child);
+}
+
+static void
+remove_button_for_stack_child_cb (GtkWidget *stack_child,
+ HdyViewSwitcher *self)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (GTK_IS_WIDGET (stack_child));
+
+ remove_button_for_stack_child (self, stack_child);
+}
+
+static void
+update_active_button_for_visible_stack_child (HdyViewSwitcher *self)
+{
+ GtkWidget *visible_stack_child = gtk_stack_get_visible_child (self->stack);
+ GtkWidget *button = g_hash_table_lookup (self->buttons, visible_stack_child);
+
+ if (button == NULL)
+ return;
+
+ self->in_child_changed = TRUE;
+ gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (button), TRUE);
+ self->in_child_changed = FALSE;
+}
+
+static void
+disconnect_stack_signals (HdyViewSwitcher *self)
+{
+ g_signal_handlers_disconnect_by_func (self->stack, add_button_for_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, remove_button_for_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, update_active_button_for_visible_stack_child, self);
+ g_signal_handlers_disconnect_by_func (self->stack, disconnect_stack_signals, self);
+}
+
+static void
+connect_stack_signals (HdyViewSwitcher *self)
+{
+ g_signal_connect_object (self->stack, "add",
+ G_CALLBACK (add_button_for_stack_child), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "remove",
+ G_CALLBACK (remove_button_for_stack_child), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "notify::visible-child",
+ G_CALLBACK (update_active_button_for_visible_stack_child), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->stack, "destroy",
+ G_CALLBACK (disconnect_stack_signals), self,
+ G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_view_switcher_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ g_value_set_enum (value, hdy_view_switcher_get_policy (self));
+ break;
+ case PROP_NARROW_ELLIPSIZE:
+ g_value_set_enum (value, hdy_view_switcher_get_narrow_ellipsize (self));
+ break;
+ case PROP_STACK:
+ g_value_set_object (value, hdy_view_switcher_get_stack (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ switch (prop_id) {
+ case PROP_POLICY:
+ hdy_view_switcher_set_policy (self, g_value_get_enum (value));
+ break;
+ case PROP_NARROW_ELLIPSIZE:
+ hdy_view_switcher_set_narrow_ellipsize (self, g_value_get_enum (value));
+ break;
+ case PROP_STACK:
+ hdy_view_switcher_set_stack (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+hdy_view_switcher_dispose (GObject *object)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ remove_switch_timer (self);
+ hdy_view_switcher_set_stack (self, NULL);
+
+ G_OBJECT_CLASS (hdy_view_switcher_parent_class)->dispose (object);
+}
+
+static void
+hdy_view_switcher_finalize (GObject *object)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (object);
+
+ g_hash_table_destroy (self->buttons);
+
+ G_OBJECT_CLASS (hdy_view_switcher_parent_class)->finalize (object);
+}
+
+static void
+hdy_view_switcher_get_preferred_width (GtkWidget *widget,
+ gint *min,
+ gint *nat)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ gint max_h_min = 0, max_h_nat = 0, max_v_min = 0, max_v_nat = 0;
+ gint n_children = 0;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l)) {
+ gint h_min = 0, h_nat = 0, v_min = 0, v_nat = 0;
+
+ if (!gtk_widget_get_visible (l->data))
+ continue;
+
+ hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, &h_nat, &v_min, &v_nat);
+ max_h_min = MAX (h_min, max_h_min);
+ max_h_nat = MAX (h_nat, max_h_nat);
+ max_v_min = MAX (v_min, max_v_min);
+ max_v_nat = MAX (v_nat, max_v_nat);
+
+ n_children++;
+ }
+
+ /* Make the buttons ask at least a minimum arbitrary size for their natural
+ * width. This prevents them from looking terribly narrow in a very wide bar.
+ */
+ max_h_nat = MAX (max_h_nat, MIN_NAT_BUTTON_WIDTH);
+ max_v_nat = MAX (max_v_nat, MIN_NAT_BUTTON_WIDTH);
+
+ switch (self->policy) {
+ case HDY_VIEW_SWITCHER_POLICY_NARROW:
+ *min = max_v_min * n_children;
+ *nat = max_v_nat * n_children;
+ break;
+ case HDY_VIEW_SWITCHER_POLICY_WIDE:
+ *min = max_h_min * n_children;
+ *nat = max_h_nat * n_children;
+ break;
+ case HDY_VIEW_SWITCHER_POLICY_AUTO:
+ default:
+ *min = max_v_min * n_children;
+ *nat = max_h_nat * n_children;
+ break;
+ }
+
+ hdy_css_measure (widget, GTK_ORIENTATION_HORIZONTAL, min, nat);
+}
+
+static gint
+is_narrow (HdyViewSwitcher *self,
+ gint width)
+{
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ gint max_h_min = 0;
+ gint n_children = 0;
+
+ if (self->policy == HDY_VIEW_SWITCHER_POLICY_NARROW)
+ return TRUE;
+
+ if (self->policy == HDY_VIEW_SWITCHER_POLICY_WIDE)
+ return FALSE;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l)) {
+ gint h_min = 0;
+
+ hdy_view_switcher_button_get_size (HDY_VIEW_SWITCHER_BUTTON (l->data), &h_min, NULL, NULL, NULL);
+ max_h_min = MAX (max_h_min, h_min);
+
+ n_children++;
+ }
+
+ return (max_h_min * n_children) > width;
+}
+
+static void
+hdy_view_switcher_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyViewSwitcher *self = HDY_VIEW_SWITCHER (widget);
+
+ g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->box));
+ GtkOrientation orientation;
+
+ hdy_css_size_allocate (widget, allocation);
+
+ orientation = is_narrow (HDY_VIEW_SWITCHER (widget), allocation->width) ?
+ GTK_ORIENTATION_VERTICAL :
+ GTK_ORIENTATION_HORIZONTAL;
+
+ for (GList *l = children; l != NULL; l = g_list_next (l))
+ gtk_orientable_set_orientation (GTK_ORIENTABLE (l->data), orientation);
+
+ GTK_WIDGET_CLASS (hdy_view_switcher_parent_class)->size_allocate (widget, allocation);
+}
+
+static void
+hdy_view_switcher_class_init (HdyViewSwitcherClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = hdy_view_switcher_get_property;
+ object_class->set_property = hdy_view_switcher_set_property;
+ object_class->dispose = hdy_view_switcher_dispose;
+ object_class->finalize = hdy_view_switcher_finalize;
+
+ widget_class->size_allocate = hdy_view_switcher_size_allocate;
+ widget_class->get_preferred_width = hdy_view_switcher_get_preferred_width;
+ widget_class->drag_motion = hdy_view_switcher_drag_motion;
+ widget_class->drag_leave = hdy_view_switcher_drag_leave;
+
+ /**
+ * HdyViewSwitcher:policy:
+ *
+ * The #HdyViewSwitcherPolicy the view switcher should use to determine which
+ * mode to use.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_POLICY] =
+ g_param_spec_enum ("policy",
+ _("Policy"),
+ _("The policy to determine the mode to use"),
+ HDY_TYPE_VIEW_SWITCHER_POLICY, HDY_VIEW_SWITCHER_POLICY_AUTO,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ /**
+ * HdyViewSwitcher:narrow-ellipsize:
+ *
+ * The preferred place to ellipsize the string, if the narrow mode label does
+ * not have enough room to display the entire string, specified as a
+ * #PangoEllipsizeMode.
+ *
+ * Note that setting this property to a value other than %PANGO_ELLIPSIZE_NONE
+ * has the side-effect that the label requests only enough space to display
+ * the ellipsis.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_NARROW_ELLIPSIZE] =
+ g_param_spec_enum ("narrow-ellipsize",
+ _("Narrow ellipsize"),
+ _("The preferred place to ellipsize the string, if the narrow mode label does not have enough room to display the entire string"),
+ PANGO_TYPE_ELLIPSIZE_MODE,
+ PANGO_ELLIPSIZE_NONE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * HdyViewSwitcher:stack:
+ *
+ * The #GtkStack the view switcher controls.
+ *
+ * Since: 0.0.10
+ */
+ props[PROP_STACK] =
+ g_param_spec_object ("stack",
+ _("Stack"),
+ _("Stack"),
+ GTK_TYPE_STACK,
+ G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "viewswitcher");
+}
+
+static void
+hdy_view_switcher_init (HdyViewSwitcher *self)
+{
+ self->box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ gtk_widget_show (self->box);
+ gtk_box_set_homogeneous (GTK_BOX (self->box), TRUE);
+ gtk_container_add (GTK_CONTAINER (self), self->box);
+
+ self->buttons = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+ gtk_widget_set_valign (GTK_WIDGET (self), GTK_ALIGN_FILL);
+
+ gtk_drag_dest_set (GTK_WIDGET (self), 0, NULL, 0, 0);
+ gtk_drag_dest_set_track_motion (GTK_WIDGET (self), TRUE);
+}
+
+/**
+ * hdy_view_switcher_new:
+ *
+ * Creates a new #HdyViewSwitcher widget.
+ *
+ * Returns: a new #HdyViewSwitcher
+ *
+ * Since: 0.0.10
+ */
+GtkWidget *
+hdy_view_switcher_new (void)
+{
+ return g_object_new (HDY_TYPE_VIEW_SWITCHER, NULL);
+}
+
+/**
+ * hdy_view_switcher_get_policy:
+ * @self: a #HdyViewSwitcher
+ *
+ * Gets the policy of @self.
+ *
+ * Returns: the policy of @self
+ *
+ * Since: 0.0.10
+ */
+HdyViewSwitcherPolicy
+hdy_view_switcher_get_policy (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), HDY_VIEW_SWITCHER_POLICY_AUTO);
+
+ return self->policy;
+}
+
+/**
+ * hdy_view_switcher_set_policy:
+ * @self: a #HdyViewSwitcher
+ * @policy: the new policy
+ *
+ * Sets the policy of @self.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_set_policy (HdyViewSwitcher *self,
+ HdyViewSwitcherPolicy policy)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+
+ if (self->policy == policy)
+ return;
+
+ self->policy = policy;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POLICY]);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_view_switcher_get_narrow_ellipsize:
+ * @self: a #HdyViewSwitcher
+ *
+ * Get the ellipsizing position of the narrow mode label. See
+ * hdy_view_switcher_set_narrow_ellipsize().
+ *
+ * Returns: #PangoEllipsizeMode
+ *
+ * Since: 0.0.10
+ **/
+PangoEllipsizeMode
+hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), PANGO_ELLIPSIZE_NONE);
+
+ return self->narrow_ellipsize;
+}
+
+/**
+ * hdy_view_switcher_set_narrow_ellipsize:
+ * @self: a #HdyViewSwitcher
+ * @mode: a #PangoEllipsizeMode
+ *
+ * Set the mode used to ellipsize the text in narrow mode if there is not
+ * enough space to render the entire string.
+ *
+ * Since: 0.0.10
+ **/
+void
+hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self,
+ PangoEllipsizeMode mode)
+{
+ GHashTableIter iter;
+ gpointer button;
+
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (mode >= PANGO_ELLIPSIZE_NONE && mode <= PANGO_ELLIPSIZE_END);
+
+ if ((PangoEllipsizeMode) self->narrow_ellipsize == mode)
+ return;
+
+ self->narrow_ellipsize = mode;
+
+ g_hash_table_iter_init (&iter, self->buttons);
+ while (g_hash_table_iter_next (&iter, NULL, &button))
+ hdy_view_switcher_button_set_narrow_ellipsize (HDY_VIEW_SWITCHER_BUTTON (button), mode);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NARROW_ELLIPSIZE]);
+}
+
+/**
+ * hdy_view_switcher_get_stack:
+ * @self: a #HdyViewSwitcher
+ *
+ * Get the #GtkStack being controlled by the #HdyViewSwitcher.
+ *
+ * See: hdy_view_switcher_set_stack()
+ *
+ * Returns: (nullable) (transfer none): the #GtkStack, or %NULL if none has been set
+ *
+ * Since: 0.0.10
+ */
+GtkStack *
+hdy_view_switcher_get_stack (HdyViewSwitcher *self)
+{
+ g_return_val_if_fail (HDY_IS_VIEW_SWITCHER (self), NULL);
+
+ return self->stack;
+}
+
+/**
+ * hdy_view_switcher_set_stack:
+ * @self: a #HdyViewSwitcher
+ * @stack: (nullable): a #GtkStack
+ *
+ * Sets the #GtkStack to control.
+ *
+ * Since: 0.0.10
+ */
+void
+hdy_view_switcher_set_stack (HdyViewSwitcher *self,
+ GtkStack *stack)
+{
+ g_return_if_fail (HDY_IS_VIEW_SWITCHER (self));
+ g_return_if_fail (stack == NULL || GTK_IS_STACK (stack));
+
+ if (self->stack == stack)
+ return;
+
+ if (self->stack) {
+ disconnect_stack_signals (self);
+ gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) remove_button_for_stack_child_cb, self);
+ }
+
+ g_set_object (&self->stack, stack);
+
+ if (self->stack) {
+ gtk_container_foreach (GTK_CONTAINER (self->stack), (GtkCallback) add_button_for_stack_child_cb, self);
+ update_active_button_for_visible_stack_child (self);
+ connect_stack_signals (self);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STACK]);
+}
diff --git a/subprojects/libhandy/src/hdy-view-switcher.h b/subprojects/libhandy/src/hdy-view-switcher.h
new file mode 100644
index 0000000..3ec02f6
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-view-switcher.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 Zander Brown <zbrown@gnome.org>
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_VIEW_SWITCHER (hdy_view_switcher_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyViewSwitcher, hdy_view_switcher, HDY, VIEW_SWITCHER, GtkBin)
+
+typedef enum {
+ HDY_VIEW_SWITCHER_POLICY_AUTO,
+ HDY_VIEW_SWITCHER_POLICY_NARROW,
+ HDY_VIEW_SWITCHER_POLICY_WIDE,
+} HdyViewSwitcherPolicy;
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_view_switcher_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyViewSwitcherPolicy hdy_view_switcher_get_policy (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_policy (HdyViewSwitcher *self,
+ HdyViewSwitcherPolicy policy);
+
+HDY_AVAILABLE_IN_ALL
+PangoEllipsizeMode hdy_view_switcher_get_narrow_ellipsize (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_narrow_ellipsize (HdyViewSwitcher *self,
+ PangoEllipsizeMode mode);
+
+HDY_AVAILABLE_IN_ALL
+GtkStack *hdy_view_switcher_get_stack (HdyViewSwitcher *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_view_switcher_set_stack (HdyViewSwitcher *self,
+ GtkStack *stack);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-handle-controller-private.h b/subprojects/libhandy/src/hdy-window-handle-controller-private.h
new file mode 100644
index 0000000..0a19251
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle-controller-private.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_HANDLE_CONTROLLER (hdy_window_handle_controller_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyWindowHandleController, hdy_window_handle_controller, HDY, WINDOW_HANDLE_CONTROLLER, GObject)
+
+HdyWindowHandleController *hdy_window_handle_controller_new (GtkWidget *widget);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-handle-controller.c b/subprojects/libhandy/src/hdy-window-handle-controller.c
new file mode 100644
index 0000000..d668745
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle-controller.c
@@ -0,0 +1,515 @@
+/* GTK - The GIMP Toolkit
+ * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald
+ *
+ * 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 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 <http://www.gnu.org/licenses/>.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/*
+ * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS
+ * file for a list of people on the GTK+ Team. See the ChangeLog
+ * files for a list of changes. These files are distributed with
+ * GTK+ at ftp://ftp.gtk.org/pub/gtk/.
+ */
+
+/* Most of the file is based on bits of code from GtkWindow */
+
+#include "config.h"
+
+#include "gtk-window-private.h"
+#include "hdy-window-handle-controller-private.h"
+
+#include <glib/gi18n-lib.h>
+
+/**
+ * PRIVATE:hdy-window-handle-controller
+ * @short_description: An oblect that makes widgets behave like titlebars.
+ * @Title: HdyWindowHandleController
+ * @See_also: #HdyHeaderBar, #HdyWindowHandle
+ * @stability: Private
+ *
+ * When HdyWindowHandleController is added to the widget, dragging that widget
+ * will move the window, and right click, double click and middle click will be
+ * handled as if that widget was a titlebar. Currently it's used to implement
+ * these properties in #HdyWindowHandle and #HdyHeaderBar
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowHandleController
+{
+ GObject parent;
+
+ GtkWidget *widget;
+ GtkGesture *multipress_gesture;
+ GtkWidget *fallback_menu;
+ gboolean keep_above;
+};
+
+G_DEFINE_TYPE (HdyWindowHandleController, hdy_window_handle_controller, G_TYPE_OBJECT);
+
+static GtkWindow *
+get_window (HdyWindowHandleController *self)
+{
+ GtkWidget *toplevel = gtk_widget_get_toplevel (self->widget);
+
+ if (GTK_IS_WINDOW (toplevel))
+ return GTK_WINDOW (toplevel);
+
+ return NULL;
+}
+
+static void
+popup_menu_detach (GtkWidget *widget,
+ GtkMenu *menu)
+{
+ HdyWindowHandleController *self;
+
+ self = g_object_steal_data (G_OBJECT (menu), "hdywindowhandlecontroller");
+
+ self->fallback_menu = NULL;
+}
+
+static void
+restore_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+ GdkWindowState state;
+
+ if (!window)
+ return;
+
+ if (gtk_window_is_maximized (window)) {
+ gtk_window_unmaximize (window);
+ return;
+ }
+
+ state = hdy_gtk_window_get_state (window);
+
+ if (state & GDK_WINDOW_STATE_ICONIFIED)
+ gtk_window_deiconify (window);
+}
+
+static void
+move_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_begin_move_drag (window,
+ 0, /* 0 means "use keyboard" */
+ 0, 0,
+ GDK_CURRENT_TIME);
+}
+
+static void
+resize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_begin_resize_drag (window,
+ 0,
+ 0, /* 0 means "use keyboard" */
+ 0, 0,
+ GDK_CURRENT_TIME);
+}
+
+static void
+minimize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ /* Turns out, we can't iconify a maximized window */
+ if (gtk_window_is_maximized (window))
+ gtk_window_unmaximize (window);
+
+ gtk_window_iconify (window);
+}
+
+static void
+maximize_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+ GdkWindowState state;
+
+ if (!window)
+ return;
+
+ state = hdy_gtk_window_get_state (window);
+
+ if (state & GDK_WINDOW_STATE_ICONIFIED)
+ gtk_window_deiconify (window);
+
+ gtk_window_maximize (window);
+}
+
+static void
+ontop_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ /*
+ * FIXME: It will go out of sync if something else calls
+ * gtk_window_set_keep_above(), so we need to actually track it.
+ * For some reason this doesn't seem to be reflected in the
+ * window state.
+ */
+ self->keep_above = !self->keep_above;
+ gtk_window_set_keep_above (window, self->keep_above);
+}
+
+static void
+close_window_cb (GtkMenuItem *menuitem,
+ HdyWindowHandleController *self)
+{
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return;
+
+ gtk_window_close (window);
+}
+
+static void
+do_popup (HdyWindowHandleController *self,
+ GdkEventButton *event)
+{
+ GtkWindow *window = get_window (self);
+ GtkWidget *menuitem;
+ GdkWindowState state;
+ gboolean maximized, iconified, resizable;
+ GdkWindowTypeHint type_hint;
+
+ if (!window)
+ return;
+
+ if (gdk_window_show_window_menu (gtk_widget_get_window (GTK_WIDGET (window)),
+ (GdkEvent *) event))
+ return;
+
+ if (self->fallback_menu)
+ gtk_widget_destroy (self->fallback_menu);
+
+ state = hdy_gtk_window_get_state (window);
+
+ iconified = (state & GDK_WINDOW_STATE_ICONIFIED) == GDK_WINDOW_STATE_ICONIFIED;
+ maximized = gtk_window_is_maximized (window) && !iconified;
+ resizable = gtk_window_get_resizable (window);
+ type_hint = gtk_window_get_type_hint (window);
+
+ self->fallback_menu = gtk_menu_new ();
+ gtk_style_context_add_class (gtk_widget_get_style_context (self->fallback_menu),
+ GTK_STYLE_CLASS_CONTEXT_MENU);
+
+ /* We can't pass self to popup_menu_detach, so will have to use custom data */
+ g_object_set_data (G_OBJECT (self->fallback_menu),
+ "hdywindowhandlecontroller", self);
+
+ gtk_menu_attach_to_widget (GTK_MENU (self->fallback_menu),
+ self->widget,
+ popup_menu_detach);
+
+ menuitem = gtk_menu_item_new_with_label (_("Restore"));
+ gtk_widget_show (menuitem);
+ /* "Restore" means "Unmaximize" or "Unminimize"
+ * (yes, some WMs allow window menu to be shown for minimized windows).
+ * Not restorable:
+ * - visible windows that are not maximized or minimized
+ * - non-resizable windows that are not minimized
+ * - non-normal windows
+ */
+ if ((gtk_widget_is_visible (GTK_WIDGET (window)) &&
+ !(maximized || iconified)) ||
+ (!iconified && !resizable) ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (restore_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Move"));
+ gtk_widget_show (menuitem);
+ if (maximized || iconified)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (move_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Resize"));
+ gtk_widget_show (menuitem);
+ if (!resizable || maximized || iconified)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (resize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Minimize"));
+ gtk_widget_show (menuitem);
+ if (iconified ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (minimize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Maximize"));
+ gtk_widget_show (menuitem);
+ if (maximized ||
+ !resizable ||
+ type_hint != GDK_WINDOW_TYPE_HINT_NORMAL)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (maximize_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_separator_menu_item_new ();
+ gtk_widget_show (menuitem);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_check_menu_item_new_with_label (_("Always on Top"));
+ gtk_check_menu_item_set_active (GTK_CHECK_MENU_ITEM (menuitem), self->keep_above);
+ if (maximized)
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ gtk_widget_show (menuitem);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (ontop_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_separator_menu_item_new ();
+ gtk_widget_show (menuitem);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+
+ menuitem = gtk_menu_item_new_with_label (_("Close"));
+ gtk_widget_show (menuitem);
+ if (!gtk_window_get_deletable (window))
+ gtk_widget_set_sensitive (menuitem, FALSE);
+ g_signal_connect (G_OBJECT (menuitem), "activate",
+ G_CALLBACK (close_window_cb), self);
+ gtk_menu_shell_append (GTK_MENU_SHELL (self->fallback_menu), menuitem);
+ gtk_menu_popup_at_pointer (GTK_MENU (self->fallback_menu), (GdkEvent *) event);
+}
+
+static gboolean
+titlebar_action (HdyWindowHandleController *self,
+ const GdkEvent *event,
+ guint button)
+{
+ GtkSettings *settings;
+ g_autofree gchar *action = NULL;
+ GtkWindow *window = get_window (self);
+
+ if (!window)
+ return FALSE;
+
+ settings = gtk_widget_get_settings (GTK_WIDGET (window));
+
+ switch (button) {
+ case GDK_BUTTON_PRIMARY:
+ g_object_get (settings, "gtk-titlebar-double-click", &action, NULL);
+ break;
+
+ case GDK_BUTTON_MIDDLE:
+ g_object_get (settings, "gtk-titlebar-middle-click", &action, NULL);
+ break;
+
+ case GDK_BUTTON_SECONDARY:
+ g_object_get (settings, "gtk-titlebar-right-click", &action, NULL);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ if (action == NULL)
+ return FALSE;
+
+ if (g_str_equal (action, "none"))
+ return FALSE;
+
+ if (g_str_has_prefix (action, "toggle-maximize")) {
+ /*
+ * gtk header bar won't show the maximize button if the following
+ * properties are not met, apply the same to title bar actions for
+ * consistency.
+ */
+ if (gtk_window_get_resizable (window) &&
+ gtk_window_get_type_hint (window) == GDK_WINDOW_TYPE_HINT_NORMAL)
+ hdy_gtk_window_toggle_maximized (window);
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "lower")) {
+ gdk_window_lower (gtk_widget_get_window (GTK_WIDGET (window)));
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "minimize")) {
+ gdk_window_iconify (gtk_widget_get_window (GTK_WIDGET (window)));
+
+ return TRUE;
+ }
+
+ if (g_str_equal (action, "menu")) {
+ do_popup (self, (GdkEventButton*) event);
+
+ return TRUE;
+ }
+
+ g_warning ("Unsupported titlebar action %s", action);
+
+ return FALSE;
+}
+
+static void
+pressed_cb (GtkGestureMultiPress *gesture,
+ gint n_press,
+ gdouble x,
+ gdouble y,
+ HdyWindowHandleController *self)
+{
+ GtkWidget *window = gtk_widget_get_toplevel (self->widget);
+ GdkEventSequence *sequence =
+ gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+ const GdkEvent *event =
+ gtk_gesture_get_last_event (GTK_GESTURE (gesture), sequence);
+ guint button =
+ gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+ if (!event)
+ return;
+
+ if (gdk_display_device_is_grabbed (gtk_widget_get_display (window),
+ gtk_gesture_get_device (GTK_GESTURE (gesture))))
+ return;
+
+ switch (button) {
+ case GDK_BUTTON_PRIMARY:
+ gdk_window_raise (gtk_widget_get_window (window));
+
+ if (n_press == 2)
+ titlebar_action (self, event, button);
+
+ if (gtk_widget_has_grab (window))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ break;
+
+ case GDK_BUTTON_SECONDARY:
+ if (titlebar_action (self, event, button))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
+ break;
+
+ case GDK_BUTTON_MIDDLE:
+ if (titlebar_action (self, event, button))
+ gtk_gesture_set_sequence_state (GTK_GESTURE (gesture),
+ sequence, GTK_EVENT_SEQUENCE_CLAIMED);
+ break;
+
+ default:
+ break;
+ }
+}
+
+static void
+hdy_window_handle_controller_finalize (GObject *object)
+{
+ HdyWindowHandleController *self = (HdyWindowHandleController *)object;
+
+ self->widget = NULL;
+ g_clear_object (&self->multipress_gesture);
+ g_clear_object (&self->fallback_menu);
+
+ G_OBJECT_CLASS (hdy_window_handle_controller_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_handle_controller_class_init (HdyWindowHandleControllerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_window_handle_controller_finalize;
+}
+
+static void
+hdy_window_handle_controller_init (HdyWindowHandleController *self)
+{
+}
+
+/**
+ * hdy_window_handle_controller_new:
+ * @widget: The widget to create a controller for
+ *
+ * Creates a new #HdyWindowHandleController for @widget.
+ *
+ * Returns: (transfer full): a newly created #HdyWindowHandleController
+ *
+ * Since: 1.0
+ */
+HdyWindowHandleController *
+hdy_window_handle_controller_new (GtkWidget *widget)
+{
+ HdyWindowHandleController *self;
+
+ g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);
+
+ self = g_object_new (HDY_TYPE_WINDOW_HANDLE_CONTROLLER, NULL);
+
+ /* The object is intended to have the same life cycle as the widget,
+ * so we don't ref it. */
+ self->widget = widget;
+ self->multipress_gesture = g_object_new (GTK_TYPE_GESTURE_MULTI_PRESS,
+ "widget", widget,
+ "button", 0,
+ NULL);
+ g_signal_connect_object (self->multipress_gesture,
+ "pressed",
+ G_CALLBACK (pressed_cb),
+ self,
+ 0);
+
+ gtk_widget_add_events (widget,
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_TOUCH_MASK);
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (widget),
+ "windowhandle");
+
+ return self;
+}
diff --git a/subprojects/libhandy/src/hdy-window-handle.c b/subprojects/libhandy/src/hdy-window-handle.c
new file mode 100644
index 0000000..30cc855
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-window-handle.h"
+#include "hdy-window-handle-controller-private.h"
+
+/**
+ * SECTION:hdy-window-handle
+ * @short_description: A bin that acts like a titlebar.
+ * @Title: HdyWindowHandle
+ * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindow
+ *
+ * HdyWindowHandle is a #GtkBin subclass that can be dragged to move its
+ * #GtkWindow, and handles right click, middle click and double click as
+ * expected from a titlebar. This is particularly useful with #HdyWindow or
+ * #HdyApplicationWindow.
+ *
+ * It isn't necessary to use #HdyWindowHandle if you use #HdyHeaderBar.
+ *
+ * It can be safely nested or used in the actual window titlebar.
+ *
+ * # CSS nodes
+ *
+ * #HdyWindowHandle has a single CSS node with name windowhandle.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowHandle
+{
+ GtkEventBox parent_instance;
+
+ HdyWindowHandleController *controller;
+};
+
+G_DEFINE_TYPE (HdyWindowHandle, hdy_window_handle, GTK_TYPE_EVENT_BOX)
+
+static void
+hdy_window_handle_finalize (GObject *object)
+{
+ HdyWindowHandle *self = (HdyWindowHandle *)object;
+
+ g_clear_object (&self->controller);
+
+ G_OBJECT_CLASS (hdy_window_handle_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_handle_class_init (HdyWindowHandleClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->finalize = hdy_window_handle_finalize;
+
+ gtk_widget_class_set_css_name (widget_class, "windowhandle");
+}
+
+static void
+hdy_window_handle_init (HdyWindowHandle *self)
+{
+ self->controller = hdy_window_handle_controller_new (GTK_WIDGET (self));
+}
+
+/**
+ * hdy_window_handle_new:
+ *
+ * Creates a new #HdyWindowHandle.
+ *
+ * Returns: (transfer full): a newly created #HdyWindowHandle
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_window_handle_new (void)
+{
+ return g_object_new (HDY_TYPE_WINDOW_HANDLE, NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-window-handle.h b/subprojects/libhandy/src/hdy-window-handle.h
new file mode 100644
index 0000000..d7835f9
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-handle.h
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_HANDLE (hdy_window_handle_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyWindowHandle, hdy_window_handle, HDY, WINDOW_HANDLE, GtkEventBox)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_window_handle_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-mixin-private.h b/subprojects/libhandy/src/hdy-window-mixin-private.h
new file mode 100644
index 0000000..27ce713
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-mixin-private.h
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW_MIXIN (hdy_window_mixin_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyWindowMixin, hdy_window_mixin, HDY, WINDOW_MIXIN, GObject)
+
+HdyWindowMixin *hdy_window_mixin_new (GtkWindow *window,
+ GtkWindowClass *klass);
+
+void hdy_window_mixin_add (HdyWindowMixin *self,
+ GtkWidget *widget);
+void hdy_window_mixin_remove (HdyWindowMixin *self,
+ GtkWidget *widget);
+void hdy_window_mixin_forall (HdyWindowMixin *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data);
+
+gboolean hdy_window_mixin_draw (HdyWindowMixin *self,
+ cairo_t *cr);
+void hdy_window_mixin_destroy (HdyWindowMixin *self);
+
+void hdy_window_mixin_buildable_add_child (HdyWindowMixin *self,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/hdy-window-mixin.c b/subprojects/libhandy/src/hdy-window-mixin.c
new file mode 100644
index 0000000..d55536c
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window-mixin.c
@@ -0,0 +1,583 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-cairo-private.h"
+#include "hdy-deck.h"
+#include "hdy-nothing-private.h"
+#include "hdy-window-mixin-private.h"
+
+typedef enum {
+ HDY_CORNER_TOP_LEFT,
+ HDY_CORNER_TOP_RIGHT,
+ HDY_CORNER_BOTTOM_LEFT,
+ HDY_CORNER_BOTTOM_RIGHT,
+ HDY_N_CORNERS,
+} HdyCorner;
+
+/**
+ * PRIVATE:hdy-window-mixin
+ * @short_description: A helper object for #HdyWindow and #HdyApplicationWindow
+ * @title: HdyWindowMixin
+ * @See_also: #HdyApplicationWindow, #HdyWindow
+ * @stability: Private
+ *
+ * The HdyWindowMixin object contains the implementation of the HdyWindow and
+ * HdyApplicationWindow classes, providing a way to make a GtkWindow subclass
+ * that has masked window corners on all sides and no titlebar by default,
+ * allowing for more freedom with how to handle the titlebar for applications.
+ *
+ * Since: 1.0
+ */
+
+struct _HdyWindowMixin
+{
+ GObject parent;
+
+ GtkWindow *window;
+ GtkWindowClass *klass;
+
+ GtkWidget *content;
+ GtkWidget *titlebar;
+ cairo_surface_t *masks[HDY_N_CORNERS];
+ gint last_border_radius;
+
+ GtkStyleContext *decoration_context;
+ GtkStyleContext *overlay_context;
+
+ GtkWidget *child;
+};
+
+G_DEFINE_TYPE (HdyWindowMixin, hdy_window_mixin, G_TYPE_OBJECT)
+
+static GtkStyleContext *
+create_child_context (HdyWindowMixin *self)
+{
+ GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ GtkStyleContext *child = gtk_style_context_new ();
+
+ gtk_style_context_set_parent (child, parent);
+ gtk_style_context_set_screen (child, gtk_style_context_get_screen (parent));
+ gtk_style_context_set_frame_clock (child, gtk_style_context_get_frame_clock (parent));
+
+ g_signal_connect_object (child,
+ "changed",
+ G_CALLBACK (gtk_widget_queue_draw),
+ self->window,
+ G_CONNECT_SWAPPED);
+
+ return child;
+}
+
+static void
+update_child_context (HdyWindowMixin *self,
+ GtkStyleContext *context,
+ const gchar *name)
+{
+ g_autoptr (GtkWidgetPath) path = gtk_widget_path_new ();
+ GtkStyleContext *parent = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ gint position;
+
+ gtk_widget_path_append_for_widget (path, GTK_WIDGET (self->window));
+ position = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET);
+ gtk_widget_path_iter_set_object_name (path, position, name);
+
+ gtk_style_context_set_path (context, path);
+ gtk_style_context_set_state (context, gtk_style_context_get_state (parent));
+}
+
+static void
+style_changed_cb (HdyWindowMixin *self)
+{
+ update_child_context (self, self->decoration_context, "decoration");
+ update_child_context (self, self->overlay_context, "decoration-overlay");
+}
+
+static gboolean
+window_state_event_cb (HdyWindowMixin *self,
+ GdkEvent *event,
+ GtkWidget *widget)
+{
+ style_changed_cb (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+size_allocate_cb (HdyWindowMixin *self,
+ GtkAllocation *alloc)
+{
+ /* We don't want to allow any other titlebar */
+ if (gtk_window_get_titlebar (self->window) != self->titlebar)
+ g_error ("gtk_window_set_titlebar() is not supported for HdyWindow");
+}
+
+static gboolean
+is_fullscreen (HdyWindowMixin *self)
+{
+ GdkWindow *window = gtk_widget_get_window (GTK_WIDGET (self->window));
+
+ return !!(gdk_window_get_state (window) & GDK_WINDOW_STATE_FULLSCREEN);
+}
+
+static gboolean
+supports_client_shadow (HdyWindowMixin *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+
+ /*
+ * GtkWindow adds this when it can't draw proper decorations, e.g. on a
+ * non-composited WM on X11. This is documented, so we can rely on this
+ * instead of copying the (pretty extensive) check.
+ */
+ return !gtk_style_context_has_class (context, "solid-csd");
+}
+
+static void
+max_borders (GtkBorder *one,
+ GtkBorder *two)
+{
+ one->top = MAX (one->top, two->top);
+ one->right = MAX (one->right, two->right);
+ one->bottom = MAX (one->bottom, two->bottom);
+ one->left = MAX (one->left, two->left);
+}
+
+static void
+get_shadow_width (HdyWindowMixin *self,
+ GtkStyleContext *context,
+ GtkBorder *shadow_width)
+{
+ GtkStateFlags state;
+ GtkBorder margin = { 0 };
+ GtkAllocation content_alloc, alloc;
+ GtkWidget *titlebar;
+
+ *shadow_width = margin;
+
+ if (!gtk_window_get_decorated (self->window))
+ return;
+
+ if (gtk_window_is_maximized (self->window) ||
+ is_fullscreen (self))
+ return;
+
+ if (!gtk_widget_is_toplevel (GTK_WIDGET (self->window)))
+ return;
+
+ state = gtk_style_context_get_state (context);
+
+ gtk_style_context_get_margin (context, state, &margin);
+
+ gtk_widget_get_allocation (GTK_WIDGET (self->window), &alloc);
+ gtk_widget_get_allocation (self->content, &content_alloc);
+
+ titlebar = gtk_window_get_titlebar (self->window);
+ if (titlebar && gtk_widget_get_visible (titlebar)) {
+ GtkAllocation titlebar_alloc;
+
+ gtk_widget_get_allocation (titlebar, &titlebar_alloc);
+
+ content_alloc.y = titlebar_alloc.y;
+ content_alloc.height += titlebar_alloc.height;
+ }
+
+ /*
+ * Since we can't get shadow extents the normal way,
+ * we have to compare window and content allocation instead.
+ */
+ shadow_width->left = content_alloc.x - alloc.x;
+ shadow_width->right = alloc.width - content_alloc.width - content_alloc.x;
+ shadow_width->top = content_alloc.y - alloc.y;
+ shadow_width->bottom = alloc.height - content_alloc.height - content_alloc.y;
+
+ max_borders (shadow_width, &margin);
+}
+
+static void
+create_masks (HdyWindowMixin *self,
+ cairo_t *cr,
+ gint border_radius)
+{
+ gint scale_factor = gtk_widget_get_scale_factor (GTK_WIDGET (self->window));
+ gdouble radius_correction = 0.5 / scale_factor;
+ gdouble r = border_radius - radius_correction;
+ gint i;
+
+ for (i = 0; i < HDY_N_CORNERS; i++)
+ g_clear_pointer (&self->masks[i], cairo_surface_destroy);
+
+ if (r <= 0)
+ return;
+
+ for (i = 0; i < HDY_N_CORNERS; i++) {
+ g_autoptr (cairo_t) mask_cr = NULL;
+
+ self->masks[i] =
+ cairo_surface_create_similar_image (cairo_get_target (cr),
+ CAIRO_FORMAT_A8,
+ border_radius * scale_factor,
+ border_radius * scale_factor);
+
+ mask_cr = cairo_create (self->masks[i]);
+
+ cairo_scale (mask_cr, scale_factor, scale_factor);
+ cairo_set_source_rgb (mask_cr, 0, 0, 0);
+ cairo_arc (mask_cr,
+ (i % 2 == 0) ? r : radius_correction,
+ (i / 2 == 0) ? r : radius_correction,
+ r,
+ 0, G_PI * 2);
+ cairo_fill (mask_cr);
+ }
+}
+
+void
+hdy_window_mixin_add (HdyWindowMixin *self,
+ GtkWidget *widget)
+{
+ if (GTK_IS_POPOVER (widget))
+ GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window),
+ widget);
+ else {
+ g_return_if_fail (self->child == NULL);
+
+ self->child = widget;
+ gtk_container_add (GTK_CONTAINER (self->content), widget);
+ }
+}
+
+void
+hdy_window_mixin_remove (HdyWindowMixin *self,
+ GtkWidget *widget)
+{
+ GtkWidget *titlebar = gtk_window_get_titlebar (self->window);
+
+ if (widget == self->content ||
+ widget == titlebar ||
+ GTK_IS_POPOVER (widget))
+ GTK_CONTAINER_CLASS (self->klass)->remove (GTK_CONTAINER (self->window),
+ widget);
+ else if (widget == self->child) {
+ self->child = NULL;
+ gtk_container_remove (GTK_CONTAINER (self->content), widget);
+ }
+}
+
+void
+hdy_window_mixin_forall (HdyWindowMixin *self,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (self->klass)->forall (GTK_CONTAINER (self->window),
+ include_internals,
+ callback,
+ callback_data);
+
+ return;
+ }
+
+ if (self->child)
+ (*callback) (self->child, callback_data);
+}
+
+typedef struct {
+ HdyWindowMixin *self;
+ cairo_t *cr;
+} HdyWindowMixinDrawData;
+
+static void
+draw_popover_cb (GtkWidget *child,
+ HdyWindowMixinDrawData *data)
+{
+ HdyWindowMixin *self = data->self;
+ GdkWindow *window;
+ cairo_t *cr = data->cr;
+
+ if (child == self->content ||
+ child == gtk_window_get_titlebar (self->window) ||
+ !gtk_widget_get_visible (child) ||
+ !gtk_widget_get_child_visible (child))
+ return;
+
+ window = gtk_widget_get_window (child);
+
+ if (gtk_widget_get_has_window (child))
+ window = gdk_window_get_parent (window);
+
+ if (!gtk_cairo_should_draw_window (cr, window))
+ return;
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), child, cr);
+}
+
+static inline void
+mask_corner (HdyWindowMixin *self,
+ cairo_t *cr,
+ gint scale_factor,
+ gint corner,
+ gint x,
+ gint y)
+{
+ cairo_save (cr);
+ cairo_scale (cr, 1.0 / scale_factor, 1.0 / scale_factor);
+ cairo_mask_surface (cr,
+ self->masks[corner],
+ x * scale_factor,
+ y * scale_factor);
+ cairo_restore (cr);
+}
+
+gboolean
+hdy_window_mixin_draw (HdyWindowMixin *self,
+ cairo_t *cr)
+{
+ HdyWindowMixinDrawData data;
+ GtkWidget *widget = GTK_WIDGET (self->window);
+ GdkWindow *window = gtk_widget_get_window (widget);
+
+ if (gtk_cairo_should_draw_window (cr, window)) {
+ GtkStyleContext *context;
+ gboolean should_mask_corners;
+ GdkRectangle clip = { 0 };
+ gint width, height, x, y, w, h, r, scale_factor;
+ GtkWidget *titlebar;
+ g_autoptr (cairo_surface_t) surface = NULL;
+ g_autoptr (cairo_t) surface_cr = NULL;
+ GtkBorder shadow;
+
+ /* Use the parent drawing unless we have a reason to use masking */
+ if (!gtk_window_get_decorated (self->window) ||
+ !supports_client_shadow (self) ||
+ is_fullscreen (self))
+ return GTK_WIDGET_CLASS (self->klass)->draw (GTK_WIDGET (self->window), cr);
+
+ context = gtk_widget_get_style_context (widget);
+
+ get_shadow_width (self, self->decoration_context, &shadow);
+
+ width = gtk_widget_get_allocated_width (widget);
+ height = gtk_widget_get_allocated_height (widget);
+
+ x = shadow.left;
+ y = shadow.top;
+ w = width - shadow.left - shadow.right;
+ h = height - shadow.top - shadow.bottom;
+
+ gtk_style_context_get (context,
+ gtk_style_context_get_state (self->decoration_context),
+ GTK_STYLE_PROPERTY_BORDER_RADIUS, &r,
+ NULL);
+
+ r = CLAMP (r, 0, MIN (w / 2, h / 2));
+
+ if (!gdk_cairo_get_clip_rectangle (cr, &clip)) {
+ clip.x = 0;
+ clip.y = 0;
+ clip.width = w;
+ clip.height = h;
+ }
+
+ gtk_render_background (self->decoration_context, cr, x, y, w, h);
+ gtk_render_frame (self->decoration_context, cr, x, y, w, h);
+
+ cairo_save (cr);
+
+ scale_factor = gtk_widget_get_scale_factor (widget);
+
+ if (r * scale_factor != self->last_border_radius) {
+ create_masks (self, cr, r);
+ self->last_border_radius = r * scale_factor;
+ }
+
+ should_mask_corners = !gtk_window_is_maximized (self->window) &&
+ r > 0 &&
+ ((clip.x < x + r && clip.y < y + r) ||
+ (clip.x < x + r && clip.y + clip.height > y + h - r) ||
+ (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r) ||
+ (clip.x + clip.width > x + w - r && clip.y < y + r));
+
+
+ if (should_mask_corners) {
+ surface = gdk_window_create_similar_surface (window,
+ CAIRO_CONTENT_COLOR_ALPHA,
+ MAX (clip.width, 1),
+ MAX (clip.height, 1));
+ surface_cr = cairo_create (surface);
+ cairo_surface_set_device_offset (surface, -clip.x * scale_factor, -clip.y * scale_factor);
+ } else {
+ surface_cr = cairo_reference (cr);
+ }
+
+ if (!gtk_widget_get_app_paintable (widget)) {
+ gtk_render_background (context, surface_cr, x, y, w, h);
+ gtk_render_frame (context, surface_cr, x, y, w, h);
+ }
+
+ titlebar = gtk_window_get_titlebar (self->window);
+
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), self->content, surface_cr);
+ gtk_container_propagate_draw (GTK_CONTAINER (self->window), titlebar, surface_cr);
+
+ gtk_render_background (self->overlay_context, surface_cr, x, y, w, h);
+ gtk_render_frame (self->overlay_context, surface_cr, x, y, w, h);
+
+ if (should_mask_corners) {
+ cairo_set_source_surface (cr, surface, 0, 0);
+
+ cairo_rectangle (cr, x + r, y, w - r * 2, r);
+ cairo_rectangle (cr, x + r, y + h - r, w - r * 2, r);
+ cairo_rectangle (cr, x, y + r, w, h - r * 2);
+ cairo_fill (cr);
+
+ if (clip.x < x + r && clip.y < y + r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_TOP_LEFT, x, y);
+
+ if (clip.x + clip.width > x + w - r && clip.y < y + r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_TOP_RIGHT, x + w - r, y);
+
+ if (clip.x < x + r && clip.y + clip.height > y + h - r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_BOTTOM_LEFT, x, y + h - r);
+
+ if (clip.x + clip.width > x + w - r && clip.y + clip.height > y + h - r)
+ mask_corner (self, cr, scale_factor,
+ HDY_CORNER_BOTTOM_RIGHT, x + w - r, y + h - r);
+
+ cairo_surface_flush (surface);
+ }
+
+ cairo_restore (cr);
+ }
+
+ data.self = self;
+ data.cr = cr;
+ gtk_container_forall (GTK_CONTAINER (self->window),
+ (GtkCallback) draw_popover_cb,
+ &data);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+void
+hdy_window_mixin_destroy (HdyWindowMixin *self)
+{
+ if (self->titlebar) {
+ hdy_window_mixin_remove (self, self->titlebar);
+ self->titlebar = NULL;
+ }
+
+ if (self->content) {
+ hdy_window_mixin_remove (self, self->content);
+ self->content = NULL;
+ self->child = NULL;
+ }
+
+ GTK_WIDGET_CLASS (self->klass)->destroy (GTK_WIDGET (self->window));
+}
+
+void
+hdy_window_mixin_buildable_add_child (HdyWindowMixin *self,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ GtkBuildable *buildable = GTK_BUILDABLE (self->window);
+
+ if (!type)
+ gtk_container_add (GTK_CONTAINER (buildable), GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (buildable, type);
+}
+
+static void
+hdy_window_mixin_finalize (GObject *object)
+{
+ HdyWindowMixin *self = (HdyWindowMixin *)object;
+ gint i;
+
+ for (i = 0; i < HDY_N_CORNERS; i++)
+ g_clear_pointer (&self->masks[i], cairo_surface_destroy);
+ g_clear_object (&self->decoration_context);
+ g_clear_object (&self->overlay_context);
+
+ G_OBJECT_CLASS (hdy_window_mixin_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_mixin_class_init (HdyWindowMixinClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = hdy_window_mixin_finalize;
+}
+
+static void
+hdy_window_mixin_init (HdyWindowMixin *self)
+{
+}
+
+HdyWindowMixin *
+hdy_window_mixin_new (GtkWindow *window,
+ GtkWindowClass *klass)
+{
+ HdyWindowMixin *self;
+ GtkStyleContext *context;
+
+ g_return_val_if_fail (GTK_IS_WINDOW (window), NULL);
+ g_return_val_if_fail (GTK_IS_WINDOW_CLASS (klass), NULL);
+ g_return_val_if_fail (GTK_IS_BUILDABLE (window), NULL);
+
+ self = g_object_new (HDY_TYPE_WINDOW_MIXIN, NULL);
+
+ self->window = window;
+ self->klass = klass;
+
+ gtk_widget_add_events (GTK_WIDGET (window), GDK_STRUCTURE_MASK);
+
+ g_signal_connect_object (window,
+ "style-updated",
+ G_CALLBACK (style_changed_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ g_signal_connect_object (window,
+ "window-state-event",
+ G_CALLBACK (window_state_event_cb),
+ self,
+ G_CONNECT_SWAPPED | G_CONNECT_AFTER);
+
+ g_signal_connect_object (window,
+ "size-allocate",
+ G_CALLBACK (size_allocate_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ self->decoration_context = create_child_context (self);
+ self->overlay_context = create_child_context (self);
+
+ style_changed_cb (self);
+
+ self->content = hdy_deck_new ();
+ gtk_widget_set_vexpand (self->content, TRUE);
+ gtk_widget_show (self->content);
+ GTK_CONTAINER_CLASS (self->klass)->add (GTK_CONTAINER (self->window),
+ self->content);
+
+ self->titlebar = hdy_nothing_new ();
+ gtk_widget_set_no_show_all (self->titlebar, TRUE);
+ gtk_window_set_titlebar (self->window, self->titlebar);
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self->window));
+ gtk_style_context_add_class (context, "unified");
+
+ return self;
+}
diff --git a/subprojects/libhandy/src/hdy-window.c b/subprojects/libhandy/src/hdy-window.c
new file mode 100644
index 0000000..1a868d7
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window.c
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-window.h"
+#include "hdy-window-mixin-private.h"
+
+/**
+ * SECTION:hdy-window
+ * @short_description: A freeform window.
+ * @title: HdyWindow
+ * @See_also: #HdyApplicationWindow, #HdyHeaderBar, #HdyWindowHandle
+ *
+ * The HdyWindow widget is a subclass of #GtkWindow which has no titlebar area
+ * and provides rounded corners on all sides, ensuring they can never be
+ * overlapped by the content. This makes it safe to use headerbars in the
+ * content area as follows:
+ *
+ * |[
+ * <object class="HdyWindow"/>
+ * <child>
+ * <object class="GtkBox">
+ * <property name="visible">True</property>
+ * <property name="orientation">vertical</property>
+ * <child>
+ * <object class="HdyHeaderBar">
+ * <property name="visible">True</property>
+ * <property name="show-close-button">True</property>
+ * </object>
+ * </child>
+ * <child>
+ * ...
+ * </child>
+ * </object>
+ * </child>
+ * </object>
+ * ]|
+ *
+ * It's recommended to use #HdyHeaderBar with #HdyWindow, as unlike
+ * #GtkHeaderBar it remains draggable inside the window. Otherwise,
+ * #HdyWindowHandle can be used.
+ *
+ * #HdyWindow allows to easily implement titlebar autohiding by putting the
+ * headerbar inside a #GtkRevealer, and to show titlebar above content by
+ * putting it into a #GtkOverlay instead of #GtkBox.
+ *
+ * if the window has a #GtkGLArea, it may bring a slight performance regression
+ * when the window is not fullscreen, tiled or maximized.
+ *
+ * Using gtk_window_get_titlebar() and gtk_window_set_titlebar() is not
+ * supported and will result in a crash.
+ *
+ * # CSS nodes
+ *
+ * #HdyWindow has a main CSS node with the name window and style classes
+ * .background, .csd and .unified.
+ *
+ * The .solid-csd style class on the main node is used for client-side
+ * decorations without invisible borders.
+ *
+ * #HdyWindow also represents window states with the following
+ * style classes on the main node: .tiled, .maximized, .fullscreen.
+ *
+ * It contains the subnodes decoration for window shadow and/or border,
+ * decoration-overlay for the sheen on top of the window, widget.titlebar, and
+ * deck, which contains the child inside the window.
+ *
+ * Since: 1.0
+ */
+
+typedef struct
+{
+ HdyWindowMixin *mixin;
+} HdyWindowPrivate;
+
+static void hdy_window_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyWindow, hdy_window, GTK_TYPE_WINDOW,
+ G_ADD_PRIVATE (HdyWindow)
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, hdy_window_buildable_init))
+
+#define HDY_GET_WINDOW_MIXIN(obj) (((HdyWindowPrivate *) hdy_window_get_instance_private (HDY_WINDOW (obj)))->mixin)
+
+static void
+hdy_window_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_add (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_window_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ hdy_window_mixin_remove (HDY_GET_WINDOW_MIXIN (container), widget);
+}
+
+static void
+hdy_window_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ hdy_window_mixin_forall (HDY_GET_WINDOW_MIXIN (container),
+ include_internals,
+ callback,
+ callback_data);
+}
+
+static gboolean
+hdy_window_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ return hdy_window_mixin_draw (HDY_GET_WINDOW_MIXIN (widget), cr);
+}
+
+static void
+hdy_window_destroy (GtkWidget *widget)
+{
+ hdy_window_mixin_destroy (HDY_GET_WINDOW_MIXIN (widget));
+}
+
+static void
+hdy_window_finalize (GObject *object)
+{
+ HdyWindow *self = (HdyWindow *)object;
+ HdyWindowPrivate *priv = hdy_window_get_instance_private (self);
+
+ g_clear_object (&priv->mixin);
+
+ G_OBJECT_CLASS (hdy_window_parent_class)->finalize (object);
+}
+
+static void
+hdy_window_class_init (HdyWindowClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->finalize = hdy_window_finalize;
+ widget_class->draw = hdy_window_draw;
+ widget_class->destroy = hdy_window_destroy;
+ container_class->add = hdy_window_add;
+ container_class->remove = hdy_window_remove;
+ container_class->forall = hdy_window_forall;
+}
+
+static void
+hdy_window_init (HdyWindow *self)
+{
+ HdyWindowPrivate *priv = hdy_window_get_instance_private (self);
+
+ priv->mixin = hdy_window_mixin_new (GTK_WINDOW (self),
+ GTK_WINDOW_CLASS (hdy_window_parent_class));
+}
+
+static void
+hdy_window_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ hdy_window_mixin_buildable_add_child (HDY_GET_WINDOW_MIXIN (buildable),
+ builder,
+ child,
+ type);
+}
+
+static void
+hdy_window_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_window_buildable_add_child;
+}
+
+/**
+ * hdy_window_new:
+ *
+ * Creates a new #HdyWindow.
+ *
+ * Returns: (transfer full): a newly created #HdyWindow
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+hdy_window_new (void)
+{
+ return g_object_new (HDY_TYPE_WINDOW,
+ "type", GTK_WINDOW_TOPLEVEL,
+ NULL);
+}
diff --git a/subprojects/libhandy/src/hdy-window.h b/subprojects/libhandy/src/hdy-window.h
new file mode 100644
index 0000000..51099cf
--- /dev/null
+++ b/subprojects/libhandy/src/hdy-window.h
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_WINDOW (hdy_window_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_DERIVABLE_TYPE (HdyWindow, hdy_window, HDY, WINDOW, GtkWindow)
+
+struct _HdyWindowClass
+{
+ GtkWindowClass parent_class;
+
+ /*< private >*/
+ gpointer padding[4];
+};
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_window_new (void);
+
+G_END_DECLS
diff --git a/subprojects/libhandy/src/icons/avatar-default-symbolic.svg b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg
new file mode 100644
index 0000000..ec0905d
--- /dev/null
+++ b/subprojects/libhandy/src/icons/avatar-default-symbolic.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <path d="M8 1a3 3 0 100 6 3 3 0 000-6zM6.5 8A4.49 4.49 0 002 12.5V14c0 1 1 1 1 1h10s1 0 1-1v-1.5A4.49 4.49 0 009.5 8z" style="marker:none" color="#bebebe" overflow="visible" fill="#2e3436"/>
+</svg>
diff --git a/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg
new file mode 100644
index 0000000..78ab0be
--- /dev/null
+++ b/subprojects/libhandy/src/icons/hdy-expander-arrow-symbolic.svg
@@ -0,0 +1,7 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16">
+ <g color="#000" fill="#474747">
+ <path d="M3.707 5.293L2.293 6.707 8 12.414l5.707-5.707-1.414-1.414L8 9.586z" style="line-height:normal;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000;text-transform:none;text-orientation:mixed;shape-padding:0;isolation:auto;mix-blend-mode:normal;marker:none" font-weight="400" font-family="sans-serif" white-space="normal" overflow="visible"/>
+ <path d="M13 6V5h1v1zM2 6V5h1v1z" style="marker:none" overflow="visible"/>
+ <path d="M2 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1zM12 6c0-.554.446-1 1-1s1 .446 1 1-.446 1-1 1-1-.446-1-1z" style="marker:none" overflow="visible"/>
+ </g>
+</svg>
diff --git a/subprojects/libhandy/src/meson.build b/subprojects/libhandy/src/meson.build
new file mode 100644
index 0000000..11d4100
--- /dev/null
+++ b/subprojects/libhandy/src/meson.build
@@ -0,0 +1,298 @@
+libhandy_header_subdir = package_subdir / package_api_name
+libhandy_header_dir = get_option('includedir') / libhandy_header_subdir
+libhandy_resources = gnome.compile_resources(
+ 'hdy-resources',
+ 'handy.gresources.xml',
+
+ c_name: 'hdy',
+)
+
+hdy_public_enum_headers = [
+ 'hdy-deck.h',
+ 'hdy-header-bar.h',
+ 'hdy-header-group.h',
+ 'hdy-leaflet.h',
+ 'hdy-navigation-direction.h',
+ 'hdy-squeezer.h',
+ 'hdy-view-switcher.h',
+]
+
+hdy_private_enum_headers = [
+ 'hdy-stackable-box-private.h',
+]
+
+version_data = configuration_data()
+version_data.set('HDY_MAJOR_VERSION', handy_version_major)
+version_data.set('HDY_MINOR_VERSION', handy_version_minor)
+version_data.set('HDY_MICRO_VERSION', handy_version_micro)
+version_data.set('HDY_VERSION', meson.project_version())
+
+hdy_version_h = configure_file(
+ input: 'hdy-version.h.in',
+ output: 'hdy-version.h',
+ install_dir: libhandy_header_dir,
+ install: true,
+ configuration: version_data)
+
+libhandy_generated_headers = [
+]
+
+install_headers(['handy.h'],
+ subdir: libhandy_header_subdir)
+
+# Filled out in the subdirs
+libhandy_public_headers = []
+libhandy_public_sources = []
+libhandy_private_sources = []
+
+hdy_public_enums = gnome.mkenums('hdy-enums',
+ h_template: 'hdy-enums.h.in',
+ c_template: 'hdy-enums.c.in',
+ sources: hdy_public_enum_headers,
+ install_header: true,
+ install_dir: libhandy_header_dir,
+)
+
+hdy_private_enums = gnome.mkenums('hdy-enums-private',
+ h_template: 'hdy-enums-private.h.in',
+ c_template: 'hdy-enums-private.c.in',
+ sources: hdy_private_enum_headers,
+ install_header: false,
+)
+
+libhandy_public_sources += [hdy_public_enums[0]]
+libhandy_private_sources += [hdy_private_enums[0]]
+libhandy_generated_headers += [hdy_public_enums[1]]
+
+src_headers = [
+ 'hdy-action-row.h',
+ 'hdy-animation.h',
+ 'hdy-application-window.h',
+ 'hdy-avatar.h',
+ 'hdy-carousel.h',
+ 'hdy-carousel-indicator-dots.h',
+ 'hdy-carousel-indicator-lines.h',
+ 'hdy-clamp.h',
+ 'hdy-combo-row.h',
+ 'hdy-deck.h',
+ 'hdy-deprecation-macros.h',
+ 'hdy-enum-value-object.h',
+ 'hdy-expander-row.h',
+ 'hdy-header-bar.h',
+ 'hdy-header-group.h',
+ 'hdy-keypad.h',
+ 'hdy-leaflet.h',
+ 'hdy-main.h',
+ 'hdy-navigation-direction.h',
+ 'hdy-preferences-group.h',
+ 'hdy-preferences-page.h',
+ 'hdy-preferences-row.h',
+ 'hdy-preferences-window.h',
+ 'hdy-search-bar.h',
+ 'hdy-squeezer.h',
+ 'hdy-swipe-group.h',
+ 'hdy-swipe-tracker.h',
+ 'hdy-swipeable.h',
+ 'hdy-title-bar.h',
+ 'hdy-types.h',
+ 'hdy-value-object.h',
+ 'hdy-view-switcher.h',
+ 'hdy-view-switcher-bar.h',
+ 'hdy-view-switcher-title.h',
+ 'hdy-window.h',
+ 'hdy-window-handle.h',
+]
+
+sed = find_program('sed', required: true)
+gen_public_types = find_program('gen-public-types.sh', required: true)
+
+libhandy_init_public_types = custom_target('hdy-public-types.c',
+ output: 'hdy-public-types.c',
+ input: [src_headers, libhandy_generated_headers],
+ command: [gen_public_types, '@INPUT@'],
+ capture: true,
+)
+
+src_sources = [
+ 'gtkprogresstracker.c',
+ 'gtk-window.c',
+ 'hdy-action-row.c',
+ 'hdy-animation.c',
+ 'hdy-application-window.c',
+ 'hdy-avatar.c',
+ 'hdy-carousel.c',
+ 'hdy-carousel-box.c',
+ 'hdy-carousel-indicator-dots.c',
+ 'hdy-carousel-indicator-lines.c',
+ 'hdy-clamp.c',
+ 'hdy-combo-row.c',
+ 'hdy-css.c',
+ 'hdy-deck.c',
+ 'hdy-enum-value-object.c',
+ 'hdy-expander-row.c',
+ 'hdy-header-bar.c',
+ 'hdy-header-group.c',
+ 'hdy-keypad-button.c',
+ 'hdy-keypad.c',
+ 'hdy-leaflet.c',
+ 'hdy-main.c',
+ 'hdy-navigation-direction.c',
+ 'hdy-nothing.c',
+ 'hdy-preferences-group.c',
+ 'hdy-preferences-page.c',
+ 'hdy-preferences-row.c',
+ 'hdy-preferences-window.c',
+ 'hdy-search-bar.c',
+ 'hdy-shadow-helper.c',
+ 'hdy-squeezer.c',
+ 'hdy-stackable-box.c',
+ 'hdy-swipe-group.c',
+ 'hdy-swipe-tracker.c',
+ 'hdy-swipeable.c',
+ 'hdy-title-bar.c',
+ 'hdy-value-object.c',
+ 'hdy-view-switcher.c',
+ 'hdy-view-switcher-bar.c',
+ 'hdy-view-switcher-button.c',
+ 'hdy-view-switcher-title.c',
+ 'hdy-window.c',
+ 'hdy-window-handle.c',
+ 'hdy-window-handle-controller.c',
+ 'hdy-window-mixin.c',
+]
+
+libhandy_public_headers += files(src_headers)
+libhandy_public_sources += files(src_sources)
+
+install_headers(src_headers, subdir: libhandy_header_subdir)
+
+
+libhandy_sources = [
+ libhandy_generated_headers,
+ libhandy_public_sources,
+ libhandy_private_sources,
+ libhandy_resources,
+ libhandy_init_public_types,
+]
+
+glib_min_version = '>= 2.44'
+
+libhandy_deps = [
+ dependency('glib-2.0', version: glib_min_version),
+ dependency('gio-2.0', version: glib_min_version),
+ dependency('gmodule-2.0', version: glib_min_version),
+ dependency('gtk+-3.0', version: '>= 3.24.1'),
+ cc.find_library('m', required: false),
+ cc.find_library('rt', required: false),
+]
+
+libhandy_c_args = [
+ '-DG_LOG_DOMAIN="Handy"',
+]
+
+config_h = configuration_data()
+config_h.set_quoted('GETTEXT_PACKAGE', 'libhandy')
+config_h.set_quoted('LOCALEDIR', get_option('prefix') / get_option('localedir'))
+
+# Symbol visibility
+if target_system == 'windows'
+ config_h.set('DLL_EXPORT', true)
+ config_h.set('_HDY_EXTERN', '__declspec(dllexport) extern')
+ if cc.get_id() != 'msvc'
+ libhandy_c_args += ['-fvisibility=hidden']
+ endif
+else
+ config_h.set('_HDY_EXTERN', '__attribute__((visibility("default"))) extern')
+ libhandy_c_args += ['-fvisibility=hidden']
+endif
+
+configure_file(
+ output: 'config.h',
+ configuration: config_h,
+)
+
+libhandy_link_args = []
+libhandy_symbols_file = 'libhandy.syms'
+
+# Check linker flags
+ld_version_script_arg = '-Wl,--version-script,@0@/@1@'.format(meson.source_root(),
+ libhandy_symbols_file)
+if cc.links('int main() { return 0; }', args : ld_version_script_arg, name : 'ld_supports_version_script')
+ libhandy_link_args += [ld_version_script_arg]
+endif
+
+# set default libdir on win32 for libhandy target to keep MinGW compatibility
+if target_system == 'windows'
+ handy_libdir = [true]
+else
+ handy_libdir = libdir
+endif
+
+libhandy = shared_library(
+ 'handy-' + apiversion,
+ libhandy_sources,
+
+ soversion: soversion,
+ c_args: libhandy_c_args,
+ dependencies: libhandy_deps,
+ include_directories: [ root_inc, src_inc ],
+ install: true,
+ link_args: libhandy_link_args,
+ install_dir: handy_libdir,
+)
+
+libhandy_dep = declare_dependency(
+ sources: libhandy_generated_headers,
+ dependencies: libhandy_deps,
+ link_with: libhandy,
+ include_directories: include_directories('.'),
+)
+
+if introspection
+
+ libhandy_gir_extra_args = [
+ '--c-include=handy.h',
+ '--quiet',
+ '-DHANDY_COMPILATION',
+ ]
+
+ libhandy_gir = gnome.generate_gir(libhandy,
+ sources: libhandy_generated_headers + libhandy_public_headers + libhandy_public_sources,
+ nsversion: apiversion,
+ namespace: 'Handy',
+ export_packages: package_api_name,
+ symbol_prefix: 'hdy',
+ identifier_prefix: 'Hdy',
+ link_with: libhandy,
+ includes: ['Gio-2.0', 'Gtk-3.0'],
+ install: true,
+ install_dir_gir: girdir,
+ install_dir_typelib: typelibdir,
+ extra_args: libhandy_gir_extra_args,
+ )
+
+ if get_option('vapi')
+
+ libhandy_vapi = gnome.generate_vapi(package_api_name,
+ sources: libhandy_gir[0],
+ packages: [ 'gio-2.0', 'gtk+-3.0' ],
+ install: true,
+ install_dir: vapidir,
+ metadata_dirs: [ meson.current_source_dir() ],
+ )
+
+ endif
+endif
+
+pkgg = import('pkgconfig')
+
+pkgg.generate(
+ libraries: [libhandy],
+ subdirs: libhandy_header_subdir,
+ version: meson.project_version(),
+ name: 'Handy',
+ filebase: package_api_name,
+ description: 'Handy Mobile widgets',
+ requires: 'gtk+-3.0',
+ install_dir: libdir / 'pkgconfig',
+)
diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.css b/subprojects/libhandy/src/themes/Adwaita-dark.css
new file mode 100644
index 0000000..e553fac
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita-dark.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#353535, 0.5); border-color: alpha(#1b1b1b, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #15539e; }
+
+row.expander image.expander-row-arrow:disabled { color: #919190; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.2); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.05); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#1b1b1b, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#353535)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#1b1b1b, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#1b1b1b, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#353535, 0.7), 0.99) 2px, alpha(#353535, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #1b1b1b; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #1b1b1b; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#353535); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#1b1b1b, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/Adwaita-dark.scss b/subprojects/libhandy/src/themes/Adwaita-dark.scss
new file mode 100644
index 0000000..918f489
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita-dark.scss
@@ -0,0 +1,5 @@
+$variant: 'dark';
+$high_contrast: false;
+
+@import 'colors';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/Adwaita.css b/subprojects/libhandy/src/themes/Adwaita.css
new file mode 100644
index 0000000..acb7f27
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; }
+
+row.expander image.expander-row-arrow:disabled { color: #929595; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#cdc7c2, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#f6f5f4)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#cdc7c2, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#cdc7c2, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#f6f5f4, 0.7), 0.96) 2px, alpha(#f6f5f4, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #cdc7c2; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #cdc7c2; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#f6f5f4); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#cdc7c2, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/Adwaita.scss b/subprojects/libhandy/src/themes/Adwaita.scss
new file mode 100644
index 0000000..5ded9f6
--- /dev/null
+++ b/subprojects/libhandy/src/themes/Adwaita.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/HighContrast.css b/subprojects/libhandy/src/themes/HighContrast.css
new file mode 100644
index 0000000..f1d1eda
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrast.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#fdfdfc, 0.5); border-color: alpha(#877b6e, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #1b6acb; }
+
+row.expander image.expander-row-arrow:disabled { color: #929495; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #877b6e; }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#877b6e, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#fdfdfc)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#877b6e, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#877b6e, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#fdfdfc, 0.7), 0.96) 2px, alpha(#fdfdfc, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #877b6e; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #877b6e; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#fdfdfc); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#877b6e, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0.7); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.34); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/HighContrast.scss b/subprojects/libhandy/src/themes/HighContrast.scss
new file mode 100644
index 0000000..4456428
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrast.scss
@@ -0,0 +1,6 @@
+$variant: 'light';
+$high_contrast: true;
+
+@import 'colors';
+@import 'colors-hc';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.css b/subprojects/libhandy/src/themes/HighContrastInverse.css
new file mode 100644
index 0000000..fd5b01b
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrastInverse.css
@@ -0,0 +1,197 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#303030, 0.5); border-color: alpha(#686868, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #0f3b71; }
+
+row.expander image.expander-row-arrow:disabled { color: #919191; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.24); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: #686868; }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.02) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.06), rgba(0, 0, 0, 0.02) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: transparent; }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
+
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
+
+popover.combo { padding: 0px; }
+
+popover.combo list { border-style: none; background-color: transparent; }
+
+popover.combo list > row { padding: 0px 12px 0px 12px; min-height: 50px; }
+
+popover.combo list > row:not(:last-child) { border-bottom: 1px solid alpha(#686868, 0.5); }
+
+popover.combo list > row:first-child { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo list > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo overshoot.top { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+popover.combo overshoot.bottom { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical { padding-top: 2px; padding-bottom: 2px; }
+
+popover.combo scrollbar.vertical:dir(ltr) { border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+popover.combo scrollbar.vertical:dir(rtl) { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; }
+
+row.expander { padding: 0px; }
+
+row.expander image.expander-row-arrow:dir(ltr) { margin-left: 6px; }
+
+row.expander image.expander-row-arrow:dir(rtl) { margin-right: 6px; }
+
+keypad .digit { font-size: 200%; font-weight: bold; }
+
+keypad .letters { font-size: 70%; }
+
+keypad .symbol { font-size: 160%; }
+
+viewswitcher, viewswitcher button { margin: 0; padding: 0; }
+
+viewswitcher button { border-radius: 0; border-top: 0; border-bottom: 0; box-shadow: none; font-size: 1rem; }
+
+viewswitcher button:not(:checked):not(:hover) { background: transparent; }
+
+viewswitcher button:not(:only-child):not(:last-child) { border-right-width: 0px; }
+
+viewswitcher button:not(only-child):first-child:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):not(:hover) { border-left-color: transparent; }
+
+viewswitcher button:not(only-child):last-child:not(:checked):not(:hover) { border-right-color: transparent; }
+
+viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: image(lighter(#303030)); }
+
+viewswitcher button:not(only-child):first-child:not(:checked):hover, viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: shade(#686868, 1.15); }
+
+viewswitcher button:not(only-child):last-child:not(:checked):hover { border-right-color: shade(#686868, 1.15); }
+
+viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); }
+
+headerbar viewswitcher button:not(:checked):hover:not(:backdrop) { background-image: linear-gradient(to top, shade(alpha(#303030, 0.7), 0.99) 2px, alpha(#303030, 0.7)); }
+
+headerbar viewswitcher button:not(:checked):not(only-child):first-child:hover, headerbar viewswitcher button:not(:checked):hover + button:not(:checked):not(:hover), headerbar viewswitcher button:not(:checked):not(:hover) + button:not(:checked):hover { border-left-color: #686868; }
+
+headerbar viewswitcher button:not(:checked):not(only-child):last-child:hover { border-right-color: #686868; }
+
+headerbar viewswitcher button:not(:checked):hover:backdrop { background-image: image(#303030); }
+
+viewswitcher button > stack > box.narrow { font-size: 0.75rem; padding-top: 7px; padding-bottom: 5px; }
+
+viewswitcher button > stack > box.narrow image, viewswitcher button > stack > box.narrow label { padding-left: 8px; padding-right: 8px; }
+
+viewswitcher button > stack > box.wide { padding: 8px 12px; }
+
+viewswitcher button > stack > box.wide label:dir(ltr) { padding-right: 7px; }
+
+viewswitcher button > stack > box.wide label:dir(rtl) { padding-left: 7px; }
+
+viewswitcher button > stack > box label.active { font-weight: bold; }
+
+viewswitcher button.needs-attention:active > stack > box label, viewswitcher button.needs-attention:checked > stack > box label { animation: none; background-image: none; }
+
+viewswitcher button.needs-attention > stack > box label { animation: needs_attention 150ms ease-in; background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent)); background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; background-position: right 0px, right 1px; }
+
+viewswitcher button.needs-attention > stack > box label:backdrop { background-size: 6px 6px, 0 0; }
+
+viewswitcher button.needs-attention > stack > box label:dir(rtl) { background-position: left 0px, left 1px; }
+
+viewswitcherbar actionbar > revealer > box { padding: 0; }
+
+list.content, list.content list { background-color: transparent; }
+
+list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); }
+
+list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+
+list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander row.header:not(:active):not(:hover):not(:selected), list.content > row.expander row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
+
+list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+
+list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid; transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+list.content > row:not(:last-child) { border-width: 1px 1px 0px 1px; }
+
+list.content > row:first-child, list.content > row.expander:first-child row.header, list.content > row.expander:checked, list.content > row.expander:checked row.header, list.content > row.expander:checked + row, list.content > row.expander:checked + row.expander row.header { border-top-left-radius: 8px; -gtk-outline-top-left-radius: 7px; border-top-right-radius: 8px; -gtk-outline-top-right-radius: 7px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked { border-width: 1px; }
+
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; -gtk-outline-bottom-right-radius: 7px; }
+
+list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { margin-top: 6px; }
+
+button.list-button:not(:active):not(:checked):not(:hover) { background: none; border: 1px solid alpha(#686868, 0.5); box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar { box-shadow: inset 0 1px rgba(255, 255, 255, 0); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { box-shadow: none; }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 255, 255, 0.065); }
+
+window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized), window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration, window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) > decoration-overlay { border-radius: 8px; }
diff --git a/subprojects/libhandy/src/themes/HighContrastInverse.scss b/subprojects/libhandy/src/themes/HighContrastInverse.scss
new file mode 100644
index 0000000..a49c0e1
--- /dev/null
+++ b/subprojects/libhandy/src/themes/HighContrastInverse.scss
@@ -0,0 +1,6 @@
+$variant: 'dark';
+$high_contrast: true;
+
+@import 'colors';
+@import 'colors-hc';
+@import 'Adwaita-base';
diff --git a/subprojects/libhandy/src/themes/_Adwaita-base.scss b/subprojects/libhandy/src/themes/_Adwaita-base.scss
new file mode 100644
index 0000000..cc0b754
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_Adwaita-base.scss
@@ -0,0 +1,336 @@
+// Include base styling.
+@import 'fallback-base';
+@import 'shared-base';
+
+// HdyComboRow
+
+popover.combo {
+ padding: 0px;
+
+ list {
+ border-style: none;
+ background-color: transparent;
+
+ > row {
+ padding: 0px 12px 0px 12px;
+ min-height: 50px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid hdyalpha($borders_color, 0.5)
+ }
+
+ &:first-child {
+ @include rounded-border(top);
+ }
+
+ &:last-child {
+ @include rounded-border(bottom);
+ }
+ }
+ }
+
+ @each $border in top, bottom {
+ overshoot.#{$border} {
+ @include rounded-border($border);
+ }
+ }
+
+ scrollbar.vertical {
+ padding-top: 2px;
+ padding-bottom: 2px;
+
+ &:dir(ltr) {
+ @include rounded-border(right);
+ }
+
+ &:dir(rtl) {
+ @include rounded-border(left);
+ }
+ }
+}
+
+// HdyExpanderRow
+
+row.expander {
+ padding: 0px;
+
+ image.expander-row-arrow {
+ @include margin-start(6px);
+ }
+}
+
+// HdyKeypad
+
+keypad {
+ .digit {
+ font-size: 200%;
+ font-weight: bold;
+ }
+
+ .letters {
+ font-size: 70%;
+ }
+
+ .symbol {
+ font-size: 160%;
+ }
+}
+
+// HdyViewSwitcher
+
+viewswitcher {
+ &, & button {
+ margin: 0;
+ padding: 0;
+ }
+
+ button {
+ border-radius: 0;
+ border-top: 0;
+ border-bottom: 0;
+ box-shadow: none;
+ font-size: 1rem;
+
+ &:not(:checked):not(:hover) {
+ background: transparent;
+ }
+
+ &:not(:only-child):not(:last-child) {
+ border-right-width: 0px;
+ }
+
+ &:not(only-child):first-child:not(:checked):not(:hover),
+ &:not(:checked):not(:hover) + button:not(:checked):not(:hover) {
+ border-left-color: transparent;
+ }
+
+ &:not(only-child):last-child:not(:checked):not(:hover) {
+ border-right-color: transparent;
+ }
+
+ &:not(:checked):hover:not(:backdrop) {
+ background-image: image(lighter($bg_color));
+ }
+
+ &:not(only-child):first-child:not(:checked):hover,
+ &:not(:checked):hover + button:not(:checked):not(:hover),
+ &:not(:checked):not(:hover) + button:not(:checked):hover {
+ border-left-color: shade($borders_color, 1.15);
+ }
+
+ &:not(only-child):last-child:not(:checked):hover {
+ border-right-color: shade($borders_color, 1.15);
+ }
+
+ &:not(:checked):hover:backdrop {
+ background-image: image($bg_color);
+ }
+
+ // View switcher in a header bar
+ headerbar &:not(:checked) {
+ &:hover:not(:backdrop) {
+ // Reimplementation of $button_fill from Adwaita. The colors are made
+ // only 70% visible to avoid the highlight to be too strong.
+ $c: hdyalpha($bg_color, 0.7);
+ $button_fill: if($variant == 'light', linear-gradient(to top, shade($c, 0.96) 2px, $c),
+ linear-gradient(to top, shade($c, 0.99) 2px, $c)) !global;
+ background-image: $button_fill;
+ }
+
+ &:not(only-child):first-child:hover,
+ &:hover + button:not(:checked):not(:hover),
+ &:not(:hover) + button:not(:checked):hover {
+ border-left-color: $borders_color;
+ }
+
+ &:not(only-child):last-child:hover {
+ border-right-color: $borders_color;
+ }
+
+ &:hover:backdrop {
+ background-image: image($bg_color);
+ }
+ }
+
+ // View switcher button
+ > stack > box {
+ &.narrow {
+ font-size: 0.75rem;
+ padding-top: 7px;
+ padding-bottom: 5px;
+
+ image,
+ label {
+ padding-left: 8px;
+ padding-right: 8px;
+ }
+ }
+
+ &.wide {
+ padding: 8px 12px;
+
+ label {
+ &:dir(ltr) {
+ padding-right: 7px;
+ }
+
+ &:dir(rtl) {
+ padding-left: 7px;
+ }
+ }
+ }
+
+ label.active {
+ font-weight: bold;
+ }
+ }
+
+ &.needs-attention {
+ &:active > stack > box label,
+ &:checked > stack > box label {
+ animation: none;
+ background-image: none;
+ }
+
+ > stack > box label {
+ animation: needs_attention 150ms ease-in;
+ background-image: -gtk-gradient(radial, center center, 0, center center, 0.5, to(#3584e4), to(transparent)), -gtk-gradient(radial, center center, 0, center center, 0.5, to(rgba(255, 255, 255, 0.769231)), to(transparent));
+ background-size: 6px 6px, 6px 6px;
+ background-repeat: no-repeat;
+ background-position: right 0px, right 1px;
+
+ &:backdrop {
+ background-size: 6px 6px, 0 0;
+ }
+
+ &:dir(rtl) {
+ background-position: left 0px, left 1px;
+ }
+ }
+ }
+ }
+}
+
+// HdyViewSwitcherBar
+
+viewswitcherbar actionbar > revealer > box {
+ padding: 0;
+}
+
+// Content list
+
+list.content {
+ &,
+ list {
+ background-color: transparent;
+ }
+
+ // Nested rows background
+ list.nested > row:not(:active) {
+ &:not(:hover):not(:selected),
+ &:hover:not(.activatable):not(:selected) {
+ background-color: hdymix($bg_color, $base_color, 0.5);
+ }
+
+ &:hover.activatable:not(:selected) {
+ background-color: hdymix($fg_color, $base_color, 0.95);
+ }
+ }
+
+ > row {
+ // Regular rows and expander header rows background
+ &:not(.expander):not(:active):not(:hover):not(:selected),
+ &:not(.expander):not(:active):hover:not(.activatable):not(:selected),
+ &.expander row.header:not(:active):not(:hover):not(:selected),
+ &.expander row.header:not(:active):hover:not(.activatable):not(:selected) {
+ background-color: $base_color;
+ }
+
+ &:not(.expander):not(:active):hover.activatable:not(:selected),
+ &.expander row.header:not(:active):hover.activatable:not(:selected) {
+ background-color: hdymix($fg_color, $base_color, 0.95);
+ }
+
+ &,
+ list > row {
+ border-color: hdyalpha($borders_color, 0.7);
+ border-style: solid;
+ transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ // Top border
+ &:not(:last-child) {
+ border-width: 1px 1px 0px 1px;
+ }
+
+ // Rounded top
+ &:first-child,
+ &.expander:first-child row.header,
+ &.expander:checked,
+ &.expander:checked row.header,
+ &.expander:checked + row,
+ &.expander:checked + row.expander row.header {
+ @include rounded-border(top);
+ }
+
+ // Bottom border
+ &:last-child,
+ &.checked-expander-row-previous-sibling,
+ &.expander:checked {
+ border-width: 1px;
+ }
+
+ // Rounded bottom
+ &:last-child,
+ &.checked-expander-row-previous-sibling,
+ &.expander:checked,
+ &.expander:not(:checked):last-child row.header,
+ &.expander:not(:checked).checked-expander-row-previous-sibling row.header,
+ &.expander.empty:checked row.header,
+ &.expander list.nested > row:last-child {
+ @include rounded-border(bottom);
+ }
+
+ // Add space around expanded rows
+ &.expander:checked:not(:first-child),
+ &.expander:checked + row {
+ margin-top: 6px;
+ }
+ }
+}
+
+// List button
+
+button.list-button:not(:active):not(:checked):not(:hover) {
+ background: none;
+ border: 1px solid hdyalpha($borders_color, 0.5);
+ box-shadow: none;
+}
+
+// Unified window
+
+window.csd.unified:not(.solid-csd):not(.fullscreen) {
+ // Remove the sheen on headerbar...
+ headerbar {
+ box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.7, 0));
+
+ &.selection-mode {
+ box-shadow: none;
+ }
+ }
+
+ // ...and add it on the window itself
+ > decoration-overlay {
+ // Use a white sheen instead of @borders, as it has to be neutral enough
+ // for any content and not just headerbar background
+ box-shadow: inset 0 1px rgba(255, 255, 255, if($variant == 'light', 0.34, 0.065));
+ }
+
+ &:not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized) {
+ &,
+ > decoration,
+ > decoration-overlay {
+ border-radius: 8px;
+ }
+ }
+}
diff --git a/subprojects/libhandy/src/themes/_definitions.scss b/subprojects/libhandy/src/themes/_definitions.scss
new file mode 100644
index 0000000..ed427a4
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_definitions.scss
@@ -0,0 +1,66 @@
+@import 'drawing';
+
+@function hdyalpha($c, $a) {
+ @return unquote("alpha(#{$c}, #{$a})");
+}
+
+@function hdymix($c1, $c2, $r) {
+ @return unquote("mix(#{$c1}, #{$c2}, #{$r})");
+}
+
+$leaflet_dimming: rgba(0, 0, 0, if($variant == 'light', 0.12, 0.24));
+$leaflet_border: rgba(0, 0, 0, if($variant == 'light', 0.05, 0.2));
+$leaflet_outline: rgba(255, 255, 255, if($variant == 'light', 0.2, 0.05));
+
+@if $high_contrast {
+ $leaflet_border: $borders_color;
+ $leaflet_outline: transparent;
+}
+
+@mixin background-shadow($direction) {
+ background-image:
+ linear-gradient($direction,
+ rgba(0, 0, 0, if($variant == 'light', 0.05, 0.1)),
+ rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 40px,
+ rgba(0, 0, 0, 0) 56px),
+ linear-gradient($direction,
+ rgba(0, 0, 0, if($variant == 'light', 0.03, 0.06)),
+ rgba(0, 0, 0, if($variant == 'light', 0.01, 0.02)) 7px,
+ rgba(0, 0, 0, 0) 24px);
+}
+
+// Makes the corners of the given border rounded.
+// $border must be top, bottom, left, or right.
+@mixin rounded-border($border) {
+ // The floors (top, bottom) and walls (left, right) of the corners matching
+ // $border. This is needed to easily form floor-wall pairs regardless of
+ // whether $border is a floor or a wall.
+ $corners: (
+ 'top': (('top'), ('left', 'right')),
+ 'bottom': (('bottom'), ('left', 'right')),
+ 'left': (('top', 'bottom'), ('left')),
+ 'right': (('top', 'bottom'), ('right')),
+ );
+
+ @if not map-get($corners, $border) {
+ @error "Unknown border type: #{$border}";
+ }
+
+ // Loop through the floors and walls of the corners of $border.
+ @each $floor in nth(map-get($corners, $border), 1) {
+ @each $wall in nth(map-get($corners, $border), 2) {
+ border-#{$floor}-#{$wall}-radius: 8px;
+ -gtk-outline-#{$floor}-#{$wall}-radius: 7px;
+ }
+ }
+}
+
+@mixin margin-start($margin) {
+ &:dir(ltr) {
+ margin-left: $margin;
+ }
+
+ &:dir(rtl) {
+ margin-right: $margin;
+ }
+}
diff --git a/subprojects/libhandy/src/themes/_fallback-base.scss b/subprojects/libhandy/src/themes/_fallback-base.scss
new file mode 100644
index 0000000..b821d95
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_fallback-base.scss
@@ -0,0 +1,146 @@
+@import 'definitions';
+
+// HdyActionRow
+
+row {
+ label.subtitle {
+ font-size: smaller;
+ opacity: 0.55;
+ text-shadow: none;
+ }
+
+ > box.header {
+ margin-left: 12px;
+ margin-right: 12px;
+ min-height: 50px;
+
+ > box.title {
+ margin-top: 8px;
+ margin-bottom: 8px;
+ }
+ }
+}
+
+// HdyExpanderRow
+
+row.expander {
+ // Drop transparent background on expander rows to let nested rows handle it,
+ // avoiding double highlights.
+ background-color: transparent;
+
+ list.nested > row {
+ background-color: hdyalpha($bg_color, 0.5);
+ border-color: hdyalpha($borders_color, 0.7);
+ border-style: solid;
+ border-width: 1px 0px 0px 0px;
+ }
+
+ // HdyExpanderRow arrow rotation
+
+ image.expander-row-arrow {
+ transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
+ }
+
+ &:checked image.expander-row-arrow {
+ -gtk-icon-transform: rotate(0turn);
+ }
+
+ &:not(:checked) image.expander-row-arrow {
+ opacity: 0.55;
+ text-shadow: none;
+
+ &:dir(ltr) {
+ -gtk-icon-transform: rotate(-0.25turn);
+ }
+
+ &:dir(rtl) {
+ -gtk-icon-transform: rotate(0.25turn);
+ }
+ }
+
+ &:checked image.expander-row-arrow:not(:disabled) {
+ color: $selected_bg_color;
+ }
+
+ & image.expander-row-arrow:disabled {
+ color: $insensitive_fg_color;
+ }
+}
+
+// Shadows
+
+deck,
+leaflet {
+ > dimming {
+ background: $leaflet_dimming;
+ }
+
+ > border {
+ min-width: 1px;
+ min-height: 1px;
+ background: $leaflet_border;
+ }
+
+ > shadow {
+ min-width: 56px;
+ min-height: 56px;
+
+ &.left { @include background-shadow(to right); }
+ &.right { @include background-shadow(to left); }
+ &.up { @include background-shadow(to bottom); }
+ &.down { @include background-shadow(to top); }
+ }
+
+ > outline {
+ min-width: 1px;
+ min-height: 1px;
+ background: $leaflet_outline;
+ }
+}
+
+// Avatar
+
+avatar {
+ border-radius: 9999px;
+ -gtk-outline-radius: 9999px;
+ font-weight: bold;
+
+ // The list of colors to generate avatars.
+ // Each avatar color is represented by a font color, a gradient start color and a gradient stop color.
+ // There are 8 different colors for avtars in the list if you change the number of them you
+ // need to update the NUMBER_OF_COLORS in src/hdy-avatar.c.
+ // The 2D list has this form: ((font-color, gradient-top-color, gradient-bottom-color)).
+ $avatarcolorlist: (
+ (#cfe1f5, #83b6ec, #337fdc), // blue
+ (#caeaf2, #7ad9f1, #0f9ac8), // cyan
+ (#cef8d8, #8de6b1, #29ae74), // green
+ (#e6f9d7, #b5e98a, #6ab85b), // lime
+ (#f9f4e1, #f8e359, #d29d09), // yellow
+ (#ffead1, #ffcb62, #d68400), // gold
+ (#ffe5c5, #ffa95a, #ed5b00), // orange
+ (#f8d2ce, #f78773, #e62d42), // raspberry
+ (#fac7de, #e973ab, #e33b6a), // magenta
+ (#e7c2e8, #cb78d4, #9945b5), // purple
+ (#d5d2f5, #9e91e8, #7a59ca), // violet
+ (#f2eade, #e3cf9c, #b08952), // beige
+ (#e5d6ca, #be916d, #785336), // brown
+ (#d8d7d3, #c0bfbc, #6e6d71), // gray
+ );
+
+ @for $i from 1 through length($avatarcolorlist) {
+ &.color#{$i} {
+ $avatarcolor: nth($avatarcolorlist, $i);
+ background-image: linear-gradient(nth($avatarcolor, 2), nth($avatarcolor, 3));
+ color: nth($avatarcolor, 1);
+ }
+ }
+
+ &.contrasted { color: #fff; }
+}
+
+// HdyViewSwitcherTitle
+
+viewswitchertitle viewswitcher {
+ margin-left: 12px;
+ margin-right: 12px;
+}
diff --git a/subprojects/libhandy/src/themes/_shared-base.scss b/subprojects/libhandy/src/themes/_shared-base.scss
new file mode 100644
index 0000000..934eeda
--- /dev/null
+++ b/subprojects/libhandy/src/themes/_shared-base.scss
@@ -0,0 +1,21 @@
+@import 'definitions';
+
+// HdyComboRow
+
+popover.combo list {
+ min-width: 200px;
+}
+
+window.csd.unified:not(.solid-csd) {
+ // Since corners are masked, there's no need for round corners anymore
+ &, headerbar {
+ border-radius: 0;
+ }
+}
+
+.windowhandle {
+ &, & * {
+ // This is the most reliable way to enable window dragging
+ -GtkWidget-window-dragging: true;
+ }
+}
diff --git a/subprojects/libhandy/src/themes/fallback.css b/subprojects/libhandy/src/themes/fallback.css
new file mode 100644
index 0000000..8c1d89b
--- /dev/null
+++ b/subprojects/libhandy/src/themes/fallback.css
@@ -0,0 +1,74 @@
+/*************************** Check and Radio buttons * */
+row label.subtitle { font-size: smaller; opacity: 0.55; text-shadow: none; }
+
+row > box.header { margin-left: 12px; margin-right: 12px; min-height: 50px; }
+
+row > box.header > box.title { margin-top: 8px; margin-bottom: 8px; }
+
+row.expander { background-color: transparent; }
+
+row.expander list.nested > row { background-color: alpha(#f6f5f4, 0.5); border-color: alpha(#cdc7c2, 0.7); border-style: solid; border-width: 1px 0px 0px 0px; }
+
+row.expander image.expander-row-arrow { transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
+
+row.expander:checked image.expander-row-arrow { -gtk-icon-transform: rotate(0turn); }
+
+row.expander:not(:checked) image.expander-row-arrow { opacity: 0.55; text-shadow: none; }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(ltr) { -gtk-icon-transform: rotate(-0.25turn); }
+
+row.expander:not(:checked) image.expander-row-arrow:dir(rtl) { -gtk-icon-transform: rotate(0.25turn); }
+
+row.expander:checked image.expander-row-arrow:not(:disabled) { color: #3584e4; }
+
+row.expander image.expander-row-arrow:disabled { color: #929595; }
+
+deck > dimming, leaflet > dimming { background: rgba(0, 0, 0, 0.12); }
+
+deck > border, leaflet > border { min-width: 1px; min-height: 1px; background: rgba(0, 0, 0, 0.05); }
+
+deck > shadow, leaflet > shadow { min-width: 56px; min-height: 56px; }
+
+deck > shadow.left, leaflet > shadow.left { background-image: linear-gradient(to right, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to right, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.right, leaflet > shadow.right { background-image: linear-gradient(to left, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to left, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.up, leaflet > shadow.up { background-image: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to bottom, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > shadow.down, leaflet > shadow.down { background-image: linear-gradient(to top, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.01) 40px, rgba(0, 0, 0, 0) 56px), linear-gradient(to top, rgba(0, 0, 0, 0.03), rgba(0, 0, 0, 0.01) 7px, rgba(0, 0, 0, 0) 24px); }
+
+deck > outline, leaflet > outline { min-width: 1px; min-height: 1px; background: rgba(255, 255, 255, 0.2); }
+
+avatar { border-radius: 9999px; -gtk-outline-radius: 9999px; font-weight: bold; }
+
+avatar.color1 { background-image: linear-gradient(#83b6ec, #337fdc); color: #cfe1f5; }
+
+avatar.color2 { background-image: linear-gradient(#7ad9f1, #0f9ac8); color: #caeaf2; }
+
+avatar.color3 { background-image: linear-gradient(#8de6b1, #29ae74); color: #cef8d8; }
+
+avatar.color4 { background-image: linear-gradient(#b5e98a, #6ab85b); color: #e6f9d7; }
+
+avatar.color5 { background-image: linear-gradient(#f8e359, #d29d09); color: #f9f4e1; }
+
+avatar.color6 { background-image: linear-gradient(#ffcb62, #d68400); color: #ffead1; }
+
+avatar.color7 { background-image: linear-gradient(#ffa95a, #ed5b00); color: #ffe5c5; }
+
+avatar.color8 { background-image: linear-gradient(#f78773, #e62d42); color: #f8d2ce; }
+
+avatar.color9 { background-image: linear-gradient(#e973ab, #e33b6a); color: #fac7de; }
+
+avatar.color10 { background-image: linear-gradient(#cb78d4, #9945b5); color: #e7c2e8; }
+
+avatar.color11 { background-image: linear-gradient(#9e91e8, #7a59ca); color: #d5d2f5; }
+
+avatar.color12 { background-image: linear-gradient(#e3cf9c, #b08952); color: #f2eade; }
+
+avatar.color13 { background-image: linear-gradient(#be916d, #785336); color: #e5d6ca; }
+
+avatar.color14 { background-image: linear-gradient(#c0bfbc, #6e6d71); color: #d8d7d3; }
+
+avatar.contrasted { color: #fff; }
+
+viewswitchertitle viewswitcher { margin-left: 12px; margin-right: 12px; }
diff --git a/subprojects/libhandy/src/themes/fallback.scss b/subprojects/libhandy/src/themes/fallback.scss
new file mode 100644
index 0000000..d8a0985
--- /dev/null
+++ b/subprojects/libhandy/src/themes/fallback.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'fallback-base';
diff --git a/subprojects/libhandy/src/themes/parse-sass.sh b/subprojects/libhandy/src/themes/parse-sass.sh
new file mode 100755
index 0000000..4238e88
--- /dev/null
+++ b/subprojects/libhandy/src/themes/parse-sass.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+
+if [ ! "$(which sassc 2> /dev/null)" ]; then
+ echo sassc needs to be installed to generate the css.
+ exit 1
+fi
+
+if [ ! "$(which git 2> /dev/null)" ]; then
+ echo git needs to be installed to check GTK.
+ exit 1
+fi
+
+SASSC_OPT="-M -t compact"
+
+: ${GTK_SOURCE_PATH:="../../../gtk"}
+: ${GTK_TAG:="3.24.21"}
+
+if [ ! -d "${GTK_SOURCE_PATH}/gtk/theme/Adwaita" ]; then
+ echo GTK sources not found at ${GTK_SOURCE_PATH}.
+ exit 1
+fi
+
+# > /dev/null makes pushd and popd silent.
+pushd ${GTK_SOURCE_PATH} > /dev/null
+GTK_CURRENT_TAG=`git describe --tags`
+popd > /dev/null
+
+if [ "${GTK_CURRENT_TAG}" != "${GTK_TAG}" ]; then
+ echo GTK must be at tag ${GTK_TAG}.
+ exit 1
+fi
+
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ Adwaita.scss Adwaita.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ Adwaita-dark.scss Adwaita-dark.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ fallback.scss fallback.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \
+ HighContrast.scss HighContrast.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita -I${GTK_SOURCE_PATH}/gtk/theme/HighContrast \
+ HighContrastInverse.scss HighContrastInverse.css
+sassc $SASSC_OPT -I${GTK_SOURCE_PATH}/gtk/theme/Adwaita \
+ shared.scss shared.css
diff --git a/subprojects/libhandy/src/themes/shared.css b/subprojects/libhandy/src/themes/shared.css
new file mode 100644
index 0000000..6bfd522
--- /dev/null
+++ b/subprojects/libhandy/src/themes/shared.css
@@ -0,0 +1,6 @@
+/*************************** Check and Radio buttons * */
+popover.combo list { min-width: 200px; }
+
+window.csd.unified:not(.solid-csd), window.csd.unified:not(.solid-csd) headerbar { border-radius: 0; }
+
+.windowhandle, .windowhandle * { -GtkWidget-window-dragging: true; }
diff --git a/subprojects/libhandy/src/themes/shared.scss b/subprojects/libhandy/src/themes/shared.scss
new file mode 100644
index 0000000..86f64b0
--- /dev/null
+++ b/subprojects/libhandy/src/themes/shared.scss
@@ -0,0 +1,5 @@
+$variant: 'light';
+$high_contrast: false;
+
+@import 'colors';
+@import 'shared-base';
diff --git a/subprojects/libhandy/tests/meson.build b/subprojects/libhandy/tests/meson.build
new file mode 100644
index 0000000..c80e96e
--- /dev/null
+++ b/subprojects/libhandy/tests/meson.build
@@ -0,0 +1,59 @@
+if get_option('tests')
+
+test_env = [
+ 'G_TEST_SRCDIR=@0@'.format(meson.current_source_dir()),
+ 'G_TEST_BUILDDIR=@0@'.format(meson.current_build_dir()),
+ 'G_DEBUG=gc-friendly,fatal-warnings',
+ 'GSETTINGS_BACKEND=memory',
+ 'PYTHONDONTWRITEBYTECODE=yes',
+ 'MALLOC_CHECK_=2',
+]
+
+test_cflags = [
+ '-DHDY_LOG_DOMAIN="Handy"',
+ '-DTEST_DATA_DIR="@0@/data"'.format(meson.current_source_dir()),
+]
+
+test_link_args = [
+ '-fPIC',
+]
+
+test_names = [
+ 'test-action-row',
+ 'test-application-window',
+ 'test-avatar',
+ 'test-carousel',
+ 'test-carousel-indicator-dots',
+ 'test-carousel-indicator-lines',
+ 'test-combo-row',
+ 'test-deck',
+ 'test-expander-row',
+ 'test-header-bar',
+ 'test-header-group',
+ 'test-keypad',
+ 'test-leaflet',
+ 'test-preferences-group',
+ 'test-preferences-page',
+ 'test-preferences-row',
+ 'test-preferences-window',
+ 'test-search-bar',
+ 'test-squeezer',
+ 'test-swipe-group',
+ 'test-value-object',
+ 'test-view-switcher',
+ 'test-view-switcher-bar',
+ 'test-window',
+ 'test-window-handle',
+]
+
+foreach test_name : test_names
+ t = executable(test_name, [test_name + '.c'] + libhandy_generated_headers,
+ c_args: test_cflags,
+ link_args: test_link_args,
+ dependencies: libhandy_deps + [libhandy_dep],
+ pie: true,
+ )
+ test(test_name, t, env: test_env)
+endforeach
+
+endif
diff --git a/subprojects/libhandy/tests/test-action-row.c b/subprojects/libhandy/tests/test-action-row.c
new file mode 100644
index 0000000..b6ae3c6
--- /dev/null
+++ b/subprojects/libhandy/tests/test-action-row.c
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint activated;
+
+static void
+activated_cb (GtkWidget *widget, gpointer data)
+{
+ activated++;
+}
+
+
+static void
+test_hdy_action_row_add (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+ GtkWidget *sw;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ sw = gtk_switch_new ();
+ g_assert_nonnull (sw);
+
+ gtk_container_add (GTK_CONTAINER (row), sw);
+}
+
+
+static void
+test_hdy_action_row_add_prefix (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+ GtkWidget *radio;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ radio = gtk_radio_button_new (NULL);
+ g_assert_nonnull (radio);
+
+ hdy_action_row_add_prefix (row, radio);
+}
+
+
+static void
+test_hdy_action_row_subtitle (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_cmpstr (hdy_action_row_get_subtitle (row), ==, "");
+
+ hdy_action_row_set_subtitle (row, "Dummy subtitle");
+ g_assert_cmpstr (hdy_action_row_get_subtitle (row), ==, "Dummy subtitle");
+}
+
+
+static void
+test_hdy_action_row_icon_name (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_null (hdy_action_row_get_icon_name (row));
+
+ hdy_action_row_set_icon_name (row, "dummy-icon-name");
+ g_assert_cmpstr (hdy_action_row_get_icon_name (row), ==, "dummy-icon-name");
+}
+
+
+static void
+test_hdy_action_row_use_undeline (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_action_row_get_use_underline (row));
+
+ hdy_action_row_set_use_underline (row, TRUE);
+ g_assert_true (hdy_action_row_get_use_underline (row));
+
+ hdy_action_row_set_use_underline (row, FALSE);
+ g_assert_false (hdy_action_row_get_use_underline (row));
+}
+
+
+static void
+test_hdy_action_row_activate (void)
+{
+ g_autoptr (HdyActionRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_ACTION_ROW (hdy_action_row_new ()));
+ g_assert_nonnull (row);
+
+ activated = 0;
+ g_signal_connect (row, "activated", G_CALLBACK (activated_cb), NULL);
+
+ hdy_action_row_activate (row);
+ g_assert_cmpint (activated, ==, 1);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ActionRow/add", test_hdy_action_row_add);
+ g_test_add_func("/Handy/ActionRow/add_prefix", test_hdy_action_row_add_prefix);
+ g_test_add_func("/Handy/ActionRow/subtitle", test_hdy_action_row_subtitle);
+ g_test_add_func("/Handy/ActionRow/icon_name", test_hdy_action_row_icon_name);
+ g_test_add_func("/Handy/ActionRow/use_underline", test_hdy_action_row_use_undeline);
+ g_test_add_func("/Handy/ActionRow/activate", test_hdy_action_row_activate);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-application-window.c b/subprojects/libhandy/tests/test-application-window.c
new file mode 100644
index 0000000..a745de6
--- /dev/null
+++ b/subprojects/libhandy/tests/test-application-window.c
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_application_window_new (void)
+{
+ g_autoptr (GtkWidget) window = NULL;
+
+ window = g_object_ref_sink (hdy_application_window_new ());
+ g_assert_nonnull (window);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ApplicationWindow/new", test_hdy_application_window_new);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-avatar.c b/subprojects/libhandy/tests/test-avatar.c
new file mode 100644
index 0000000..ad330ae
--- /dev/null
+++ b/subprojects/libhandy/tests/test-avatar.c
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+#define TEST_ICON_NAME "avatar-default-symbolic"
+#define TEST_STRING "Mario Rossi"
+#define TEST_SIZE 128
+
+
+static gboolean
+is_surface_empty (cairo_surface_t *surface)
+{
+ unsigned char * data;
+ guint length;
+
+ cairo_surface_flush (surface);
+ data = cairo_image_surface_get_data (surface);
+ length = cairo_image_surface_get_width (surface) * cairo_image_surface_get_height (surface);
+
+ for (int i = 0; i < length; i++) {
+ if (data[i] != 0)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static GdkPixbuf *
+load_null_image_func (gint size,
+ gpointer data)
+{
+ return NULL;
+}
+
+static GdkPixbuf *
+load_image_func (gint size,
+ GdkRGBA *color)
+{
+ GdkPixbuf *pixbuf;
+ cairo_surface_t *surface;
+ cairo_t *cr;
+
+ surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size);
+ cr = cairo_create (surface);
+ if (color != NULL) {
+ gdk_cairo_set_source_rgba (cr, color);
+ cairo_paint (cr);
+ }
+ pixbuf = gdk_pixbuf_get_from_surface (surface, 0, 0, size, size);
+
+ cairo_surface_destroy (surface);
+ cairo_destroy (cr);
+ return pixbuf;
+}
+
+
+static void
+map_event_cb (GtkWidget *widget, GdkEvent *event, cairo_surface_t **surface)
+{
+ cairo_t *cr;
+
+ g_assert (surface != NULL);
+
+ *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, TEST_SIZE, TEST_SIZE);
+ cr = cairo_create (*surface);
+ gtk_widget_draw (widget, cr);
+ cairo_destroy (cr);
+ gtk_main_quit ();
+}
+
+
+static gboolean
+did_draw_something (GtkWidget *widget)
+{
+ GtkWidget *window;
+ gboolean empty;
+ cairo_surface_t *surface;
+
+ window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
+
+ gtk_widget_set_events (widget, GDK_STRUCTURE_MASK);
+ g_signal_connect (widget, "map-event", G_CALLBACK (map_event_cb), &surface);
+
+ gtk_window_resize (GTK_WINDOW (window), TEST_SIZE, TEST_SIZE);
+ gtk_container_add (GTK_CONTAINER (window), widget);
+
+ gtk_widget_show (widget);
+ gtk_widget_show (window);
+
+ gtk_main ();
+
+ g_assert (surface);
+ g_assert (cairo_surface_status (surface) == CAIRO_STATUS_SUCCESS);
+ empty = is_surface_empty (surface);
+
+ cairo_surface_destroy (surface);
+ gtk_widget_destroy (window);
+
+ return !empty;
+}
+
+
+static void
+test_hdy_avatar_generate (void)
+{
+ GtkWidget *avatar = hdy_avatar_new (TEST_SIZE, "", TRUE);
+ g_assert (HDY_IS_AVATAR (avatar));
+
+ g_assert_true (did_draw_something (GTK_WIDGET (avatar)));
+}
+
+
+static void
+test_hdy_avatar_icon_name (void)
+{
+ HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (128, NULL, TRUE));
+
+ g_assert_null (hdy_avatar_get_icon_name (avatar));
+ hdy_avatar_set_icon_name (avatar, TEST_ICON_NAME);
+ g_assert_cmpstr (hdy_avatar_get_icon_name (avatar), ==, TEST_ICON_NAME);
+
+ g_assert_true (did_draw_something (GTK_WIDGET (avatar)));
+}
+
+static void
+test_hdy_avatar_text (void)
+{
+ HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (128, NULL, TRUE));
+
+ g_assert_null (hdy_avatar_get_text (avatar));
+ hdy_avatar_set_text (avatar, TEST_STRING);
+ g_assert_cmpstr (hdy_avatar_get_text (avatar), ==, TEST_STRING);
+
+ g_assert_true (did_draw_something (GTK_WIDGET (avatar)));
+}
+
+static void
+test_hdy_avatar_size (void)
+{
+ HdyAvatar *avatar = HDY_AVATAR (hdy_avatar_new (TEST_SIZE, NULL, TRUE));
+
+ g_assert_cmpint (hdy_avatar_get_size (avatar), ==, TEST_SIZE);
+ hdy_avatar_set_size (avatar, TEST_SIZE / 2);
+ g_assert_cmpint (hdy_avatar_get_size (avatar), ==, TEST_SIZE / 2);
+
+ g_assert_true (did_draw_something (GTK_WIDGET (avatar)));
+}
+
+static void
+test_hdy_avatar_custom_image (void)
+{
+ GtkWidget *avatar;
+ GdkRGBA color;
+
+ avatar = hdy_avatar_new (TEST_SIZE, NULL, TRUE);
+
+ g_assert (HDY_IS_AVATAR (avatar));
+
+ hdy_avatar_set_image_load_func (HDY_AVATAR (avatar),
+ (HdyAvatarImageLoadFunc) load_image_func,
+ NULL,
+ NULL);
+
+ g_object_ref (avatar);
+ g_assert_false (did_draw_something (avatar));
+
+ hdy_avatar_set_image_load_func (HDY_AVATAR (avatar),
+ NULL,
+ NULL,
+ NULL);
+
+ g_assert_true (did_draw_something (avatar));
+
+ gdk_rgba_parse (&color, "#F00");
+ hdy_avatar_set_image_load_func (HDY_AVATAR (avatar),
+ (HdyAvatarImageLoadFunc) load_image_func,
+ &color,
+ NULL);
+
+ g_assert_true (did_draw_something (avatar));
+
+ hdy_avatar_set_image_load_func (HDY_AVATAR (avatar),
+ (HdyAvatarImageLoadFunc) load_null_image_func,
+ NULL,
+ NULL);
+
+ g_assert_true (did_draw_something (avatar));
+
+ g_object_unref (avatar);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func ("/Handy/Avatar/generate", test_hdy_avatar_generate);
+ g_test_add_func ("/Handy/Avatar/custom_image", test_hdy_avatar_custom_image);
+ g_test_add_func ("/Handy/Avatar/icon_name", test_hdy_avatar_icon_name);
+ g_test_add_func ("/Handy/Avatar/text", test_hdy_avatar_text);
+ g_test_add_func ("/Handy/Avatar/size", test_hdy_avatar_size);
+
+ return g_test_run ();
+}
diff --git a/subprojects/libhandy/tests/test-carousel-indicator-dots.c b/subprojects/libhandy/tests/test-carousel-indicator-dots.c
new file mode 100644
index 0000000..bd02d10
--- /dev/null
+++ b/subprojects/libhandy/tests/test-carousel-indicator-dots.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_hdy_carousel_indicator_dots_carousel (void)
+{
+ g_autoptr (HdyCarouselIndicatorDots) dots = NULL;
+ HdyCarousel *carousel;
+
+ dots = g_object_ref_sink (HDY_CAROUSEL_INDICATOR_DOTS (hdy_carousel_indicator_dots_new ()));
+ g_assert_nonnull (dots);
+
+ notified = 0;
+ g_signal_connect (dots, "notify::carousel", G_CALLBACK (notify_cb), NULL);
+
+ carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ g_assert_nonnull (carousel);
+
+ g_assert_null (hdy_carousel_indicator_dots_get_carousel (dots));
+ g_assert_cmpint (notified, ==, 0);
+
+ hdy_carousel_indicator_dots_set_carousel (dots, carousel);
+ g_assert (hdy_carousel_indicator_dots_get_carousel (dots) == carousel);
+ g_assert_cmpint (notified, ==, 1);
+
+ hdy_carousel_indicator_dots_set_carousel (dots, NULL);
+ g_assert_null (hdy_carousel_indicator_dots_get_carousel (dots));
+ g_assert_cmpint (notified, ==, 2);
+}
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/CarouselIndicatorDots/carousel", test_hdy_carousel_indicator_dots_carousel);
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-carousel-indicator-lines.c b/subprojects/libhandy/tests/test-carousel-indicator-lines.c
new file mode 100644
index 0000000..dfccdd2
--- /dev/null
+++ b/subprojects/libhandy/tests/test-carousel-indicator-lines.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_hdy_carousel_indicator_lines_carousel (void)
+{
+ g_autoptr (HdyCarouselIndicatorLines) lines = NULL;
+ HdyCarousel *carousel;
+
+ lines = g_object_ref_sink (HDY_CAROUSEL_INDICATOR_LINES (hdy_carousel_indicator_lines_new ()));
+ g_assert_nonnull (lines);
+
+ notified = 0;
+ g_signal_connect (lines, "notify::carousel", G_CALLBACK (notify_cb), NULL);
+
+ carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ g_assert_nonnull (carousel);
+
+ g_assert_null (hdy_carousel_indicator_lines_get_carousel (lines));
+ g_assert_cmpint (notified, ==, 0);
+
+ hdy_carousel_indicator_lines_set_carousel (lines, carousel);
+ g_assert (hdy_carousel_indicator_lines_get_carousel (lines) == carousel);
+ g_assert_cmpint (notified, ==, 1);
+
+ hdy_carousel_indicator_lines_set_carousel (lines, NULL);
+ g_assert_null (hdy_carousel_indicator_lines_get_carousel (lines));
+ g_assert_cmpint (notified, ==, 2);
+}
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/CarouselInidicatorLines/carousel", test_hdy_carousel_indicator_lines_carousel);
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-carousel.c b/subprojects/libhandy/tests/test-carousel.c
new file mode 100644
index 0000000..45b2fec
--- /dev/null
+++ b/subprojects/libhandy/tests/test-carousel.c
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_hdy_carousel_add_remove (void)
+{
+ HdyCarousel *carousel;
+ GtkWidget *child1, *child2, *child3;
+
+ carousel = HDY_CAROUSEL (hdy_carousel_new ());
+
+ child1 = gtk_label_new ("");
+ child2 = gtk_label_new ("");
+ child3 = gtk_label_new ("");
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::n-pages", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 0);
+
+ gtk_container_add (GTK_CONTAINER (carousel), child1);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 1);
+ g_assert_cmpint (notified, ==, 1);
+
+ hdy_carousel_prepend (carousel, child2);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 2);
+ g_assert_cmpint (notified, ==, 2);
+
+ hdy_carousel_insert (carousel, child3, 1);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 3);
+ g_assert_cmpint (notified, ==, 3);
+
+ hdy_carousel_reorder (carousel, child3, 0);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 3);
+ g_assert_cmpint (notified, ==, 3);
+
+ gtk_container_remove (GTK_CONTAINER (carousel), child2);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 2);
+ g_assert_cmpint (notified, ==, 4);
+
+ gtk_container_remove (GTK_CONTAINER (carousel), child1);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 1);
+ g_assert_cmpint (notified, ==, 5);
+
+ gtk_container_remove (GTK_CONTAINER (carousel), child3);
+ g_assert_cmpuint (hdy_carousel_get_n_pages (carousel), ==, 0);
+ g_assert_cmpint (notified, ==, 6);
+
+ g_object_unref (carousel);
+}
+
+static void
+test_hdy_carousel_scroll_to (void)
+{
+ HdyCarousel *carousel;
+ GtkWidget *child1, *child2, *child3;
+
+ carousel = HDY_CAROUSEL (hdy_carousel_new ());
+
+ child1 = gtk_label_new ("");
+ child2 = gtk_label_new ("");
+ child3 = gtk_label_new ("");
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::position", G_CALLBACK (notify_cb), NULL);
+
+ gtk_container_add (GTK_CONTAINER (carousel), child1);
+ gtk_container_add (GTK_CONTAINER (carousel), child2);
+ gtk_container_add (GTK_CONTAINER (carousel), child3);
+
+ /* Since tests are done synchronously, avoid animations */
+ hdy_carousel_set_animation_duration (carousel, 0);
+
+ g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 0);
+ g_assert_cmpint (notified, ==, 0);
+
+ hdy_carousel_scroll_to (carousel, child3);
+ g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 2);
+ g_assert_cmpint (notified, ==, 1);
+
+ hdy_carousel_scroll_to (carousel, child2);
+ g_assert_cmpfloat(hdy_carousel_get_position (carousel), ==, 1);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_object_unref (carousel);
+}
+
+static void
+test_hdy_carousel_interactive (void)
+{
+ HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ gboolean interactive;
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::interactive", G_CALLBACK (notify_cb), NULL);
+
+ /* Accessors */
+ g_assert_true (hdy_carousel_get_interactive (carousel));
+ hdy_carousel_set_interactive (carousel, FALSE);
+ g_assert_false (hdy_carousel_get_interactive (carousel));
+ g_assert_cmpint (notified, ==, 1);
+
+ /* Property */
+ g_object_set (carousel, "interactive", TRUE, NULL);
+ g_object_get (carousel, "interactive", &interactive, NULL);
+ g_assert_true (interactive);
+ g_assert_cmpint (notified, ==, 2);
+
+ /* Setting the same value should not notify */
+ hdy_carousel_set_interactive (carousel, TRUE);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_carousel_spacing (void)
+{
+ HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ guint spacing;
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::spacing", G_CALLBACK (notify_cb), NULL);
+
+ /* Accessors */
+ g_assert_cmpuint (hdy_carousel_get_spacing (carousel), ==, 0);
+ hdy_carousel_set_spacing (carousel, 12);
+ g_assert_cmpuint (hdy_carousel_get_spacing (carousel), ==, 12);
+ g_assert_cmpint (notified, ==, 1);
+
+ /* Property */
+ g_object_set (carousel, "spacing", 6, NULL);
+ g_object_get (carousel, "spacing", &spacing, NULL);
+ g_assert_cmpuint (spacing, ==, 6);
+ g_assert_cmpint (notified, ==, 2);
+
+ /* Setting the same value should not notify */
+ hdy_carousel_set_spacing (carousel, 6);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_carousel_animation_duration (void)
+{
+ HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ guint duration;
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::animation-duration", G_CALLBACK (notify_cb), NULL);
+
+ /* Accessors */
+ g_assert_cmpuint (hdy_carousel_get_animation_duration (carousel), ==, 250);
+ hdy_carousel_set_animation_duration (carousel, 200);
+ g_assert_cmpuint (hdy_carousel_get_animation_duration (carousel), ==, 200);
+ g_assert_cmpint (notified, ==, 1);
+
+ /* Property */
+ g_object_set (carousel, "animation-duration", 500, NULL);
+ g_object_get (carousel, "animation-duration", &duration, NULL);
+ g_assert_cmpuint (duration, ==, 500);
+ g_assert_cmpint (notified, ==, 2);
+
+ /* Setting the same value should not notify */
+ hdy_carousel_set_animation_duration (carousel, 500);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_carousel_allow_mouse_drag (void)
+{
+ HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ gboolean allow_mouse_drag;
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::allow-mouse-drag", G_CALLBACK (notify_cb), NULL);
+
+ /* Accessors */
+ g_assert_true (hdy_carousel_get_allow_mouse_drag (carousel));
+ hdy_carousel_set_allow_mouse_drag (carousel, FALSE);
+ g_assert_false (hdy_carousel_get_allow_mouse_drag (carousel));
+ g_assert_cmpint (notified, ==, 1);
+
+ /* Property */
+ g_object_set (carousel, "allow-mouse-drag", TRUE, NULL);
+ g_object_get (carousel, "allow-mouse-drag", &allow_mouse_drag, NULL);
+ g_assert_true (allow_mouse_drag);
+ g_assert_cmpint (notified, ==, 2);
+
+ /* Setting the same value should not notify */
+ hdy_carousel_set_allow_mouse_drag (carousel, TRUE);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_hdy_carousel_reveal_duration (void)
+{
+ HdyCarousel *carousel = HDY_CAROUSEL (hdy_carousel_new ());
+ guint duration;
+
+ notified = 0;
+ g_signal_connect (carousel, "notify::reveal-duration", G_CALLBACK (notify_cb), NULL);
+
+ /* Accessors */
+ g_assert_cmpuint (hdy_carousel_get_reveal_duration (carousel), ==, 0);
+ hdy_carousel_set_reveal_duration (carousel, 200);
+ g_assert_cmpuint (hdy_carousel_get_reveal_duration (carousel), ==, 200);
+ g_assert_cmpint (notified, ==, 1);
+
+ /* Property */
+ g_object_set (carousel, "reveal-duration", 500, NULL);
+ g_object_get (carousel, "reveal-duration", &duration, NULL);
+ g_assert_cmpuint (duration, ==, 500);
+ g_assert_cmpint (notified, ==, 2);
+
+ /* Setting the same value should not notify */
+ hdy_carousel_set_reveal_duration (carousel, 500);
+ g_assert_cmpint (notified, ==, 2);
+}
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/Carousel/add_remove", test_hdy_carousel_add_remove);
+ g_test_add_func("/Handy/Carousel/scroll_to", test_hdy_carousel_scroll_to);
+ g_test_add_func("/Handy/Carousel/interactive", test_hdy_carousel_interactive);
+ g_test_add_func("/Handy/Carousel/spacing", test_hdy_carousel_spacing);
+ g_test_add_func("/Handy/Carousel/animation_duration", test_hdy_carousel_animation_duration);
+ g_test_add_func("/Handy/Carousel/allow_mouse_drag", test_hdy_carousel_allow_mouse_drag);
+ g_test_add_func("/Handy/Carousel/reveal_duration", test_hdy_carousel_reveal_duration);
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-combo-row.c b/subprojects/libhandy/tests/test-combo-row.c
new file mode 100644
index 0000000..12feea6
--- /dev/null
+++ b/subprojects/libhandy/tests/test-combo-row.c
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_combo_row_set_for_enum (void)
+{
+ g_autoptr (HdyComboRow) row = NULL;
+ GListModel *model;
+ HdyEnumValueObject *value;
+
+ row = g_object_ref_sink (HDY_COMBO_ROW (hdy_combo_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_null (hdy_combo_row_get_model (row));
+
+ hdy_combo_row_set_for_enum (row, GTK_TYPE_ORIENTATION, hdy_enum_value_row_name, NULL, NULL);
+ model = hdy_combo_row_get_model (row);
+ g_assert_true (G_IS_LIST_MODEL (model));
+
+ g_assert_cmpuint (g_list_model_get_n_items (model), ==, 2);
+
+ value = g_list_model_get_item (model, 0);
+ g_assert_true (HDY_IS_ENUM_VALUE_OBJECT (value));
+ g_assert_cmpstr (hdy_enum_value_object_get_nick (value), ==, "horizontal");
+
+ value = g_list_model_get_item (model, 1);
+ g_assert_true (HDY_IS_ENUM_VALUE_OBJECT (value));
+ g_assert_cmpstr (hdy_enum_value_object_get_nick (value), ==, "vertical");
+}
+
+
+static void
+test_hdy_combo_row_use_subtitle (void)
+{
+ g_autoptr (HdyComboRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_COMBO_ROW (hdy_combo_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_combo_row_get_use_subtitle (row));
+
+ hdy_combo_row_set_use_subtitle (row, TRUE);
+ g_assert_true (hdy_combo_row_get_use_subtitle (row));
+
+ hdy_combo_row_set_use_subtitle (row, FALSE);
+ g_assert_false (hdy_combo_row_get_use_subtitle (row));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ComboRow/set_for_enum", test_hdy_combo_row_set_for_enum);
+ g_test_add_func("/Handy/ComboRow/use_subtitle", test_hdy_combo_row_use_subtitle);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-deck.c b/subprojects/libhandy/tests/test-deck.c
new file mode 100644
index 0000000..e2c5677
--- /dev/null
+++ b/subprojects/libhandy/tests/test-deck.c
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_deck_adjacent_child (void)
+{
+ g_autoptr (HdyDeck) deck = NULL;
+ GtkWidget *children[2];
+ gint i;
+ GtkWidget *result;
+
+ deck = HDY_DECK (hdy_deck_new ());
+ g_assert_nonnull (deck);
+
+ for (i = 0; i < 2; i++) {
+ children[i] = gtk_label_new ("");
+ g_assert_nonnull (children[i]);
+
+ gtk_container_add (GTK_CONTAINER (deck), children[i]);
+ }
+
+ hdy_deck_set_visible_child (deck, children[0]);
+
+ result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_null (result);
+
+ result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_true (result == children[1]);
+
+ hdy_deck_set_visible_child (deck, children[1]);
+
+ result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_true (result == children[0]);
+
+ result = hdy_deck_get_adjacent_child (deck, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_null (result);
+}
+
+
+static void
+test_hdy_deck_navigate (void)
+{
+ g_autoptr (HdyDeck) deck = NULL;
+ GtkWidget *children[2];
+ gint i;
+ gboolean result;
+
+ deck = HDY_DECK (hdy_deck_new ());
+ g_assert_nonnull (deck);
+
+ for (i = 0; i < 2; i++) {
+ children[i] = gtk_label_new ("");
+ g_assert_nonnull (children[i]);
+
+ gtk_container_add (GTK_CONTAINER (deck), children[i]);
+ }
+
+ hdy_deck_set_visible_child (deck, children[0]);
+
+ result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_false (result);
+
+ result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_true (result);
+ g_assert_true (hdy_deck_get_visible_child (deck) == children[1]);
+
+ result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_false (result);
+
+ result = hdy_deck_navigate (deck, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_true (result);
+ g_assert_true (hdy_deck_get_visible_child (deck) == children[0]);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func ("/Handy/Deck/adjacent_child", test_hdy_deck_adjacent_child);
+ g_test_add_func ("/Handy/Deck/navigate", test_hdy_deck_navigate);
+
+ return g_test_run ();
+}
diff --git a/subprojects/libhandy/tests/test-expander-row.c b/subprojects/libhandy/tests/test-expander-row.c
new file mode 100644
index 0000000..b0625d9
--- /dev/null
+++ b/subprojects/libhandy/tests/test-expander-row.c
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_expander_row_add (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+ GtkWidget *sw;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ sw = gtk_switch_new ();
+ g_assert_nonnull (sw);
+
+ gtk_container_add (GTK_CONTAINER (row), sw);
+}
+
+
+static void
+test_hdy_expander_row_subtitle (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_cmpstr (hdy_expander_row_get_subtitle (row), ==, "");
+
+ hdy_expander_row_set_subtitle (row, "Dummy subtitle");
+ g_assert_cmpstr (hdy_expander_row_get_subtitle (row), ==, "Dummy subtitle");
+}
+
+
+static void
+test_hdy_expander_row_icon_name (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_null (hdy_expander_row_get_icon_name (row));
+
+ hdy_expander_row_set_icon_name (row, "dummy-icon-name");
+ g_assert_cmpstr (hdy_expander_row_get_icon_name (row), ==, "dummy-icon-name");
+}
+
+
+static void
+test_hdy_expander_row_use_undeline (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_expander_row_get_use_underline (row));
+
+ hdy_expander_row_set_use_underline (row, TRUE);
+ g_assert_true (hdy_expander_row_get_use_underline (row));
+
+ hdy_expander_row_set_use_underline (row, FALSE);
+ g_assert_false (hdy_expander_row_get_use_underline (row));
+}
+
+
+static void
+test_hdy_expander_row_expanded (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_expanded (row, TRUE);
+ g_assert_true (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_expanded (row, FALSE);
+ g_assert_false (hdy_expander_row_get_expanded (row));
+}
+
+
+static void
+test_hdy_expander_row_enable_expansion (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_true (hdy_expander_row_get_enable_expansion (row));
+ g_assert_false (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_expanded (row, TRUE);
+ g_assert_true (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_enable_expansion (row, FALSE);
+ g_assert_false (hdy_expander_row_get_enable_expansion (row));
+ g_assert_false (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_expanded (row, TRUE);
+ g_assert_false (hdy_expander_row_get_expanded (row));
+
+ hdy_expander_row_set_enable_expansion (row, TRUE);
+ g_assert_true (hdy_expander_row_get_enable_expansion (row));
+ g_assert_true (hdy_expander_row_get_expanded (row));
+}
+
+
+static void
+test_hdy_expander_row_show_enable_switch (void)
+{
+ g_autoptr (HdyExpanderRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_EXPANDER_ROW (hdy_expander_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_expander_row_get_show_enable_switch (row));
+
+ hdy_expander_row_set_show_enable_switch (row, TRUE);
+ g_assert_true (hdy_expander_row_get_show_enable_switch (row));
+
+ hdy_expander_row_set_show_enable_switch (row, FALSE);
+ g_assert_false (hdy_expander_row_get_show_enable_switch (row));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ExpanderRow/add", test_hdy_expander_row_add);
+ g_test_add_func("/Handy/ExpanderRow/subtitle", test_hdy_expander_row_subtitle);
+ g_test_add_func("/Handy/ExpanderRow/icon_name", test_hdy_expander_row_icon_name);
+ g_test_add_func("/Handy/ExpanderRow/use_underline", test_hdy_expander_row_use_undeline);
+ g_test_add_func("/Handy/ExpanderRow/expanded", test_hdy_expander_row_expanded);
+ g_test_add_func("/Handy/ExpanderRow/enable_expansion", test_hdy_expander_row_enable_expansion);
+ g_test_add_func("/Handy/ExpanderRow/show_enable_switch", test_hdy_expander_row_show_enable_switch);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-header-bar.c b/subprojects/libhandy/tests/test-header-bar.c
new file mode 100644
index 0000000..15064bf
--- /dev/null
+++ b/subprojects/libhandy/tests/test-header-bar.c
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_header_bar_pack (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+ GtkWidget *widget;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+
+ hdy_header_bar_pack_start (bar, widget);
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+
+ hdy_header_bar_pack_end (bar, widget);
+}
+
+
+static void
+test_hdy_header_bar_title (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_null (hdy_header_bar_get_title (bar));
+
+ hdy_header_bar_set_title (bar, "Dummy title");
+ g_assert_cmpstr (hdy_header_bar_get_title (bar), ==, "Dummy title");
+
+ hdy_header_bar_set_title (bar, NULL);
+ g_assert_null (hdy_header_bar_get_title (bar));
+}
+
+
+static void
+test_hdy_header_bar_subtitle (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_null (hdy_header_bar_get_subtitle (bar));
+
+ hdy_header_bar_set_subtitle (bar, "Dummy subtitle");
+ g_assert_cmpstr (hdy_header_bar_get_subtitle (bar), ==, "Dummy subtitle");
+
+ hdy_header_bar_set_subtitle (bar, NULL);
+ g_assert_null (hdy_header_bar_get_subtitle (bar));
+}
+
+
+static void
+test_hdy_header_bar_custom_title (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+ GtkWidget *widget;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_null (hdy_header_bar_get_custom_title (bar));
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+ hdy_header_bar_set_custom_title (bar, widget);
+ g_assert (hdy_header_bar_get_custom_title (bar) == widget);
+
+ hdy_header_bar_set_custom_title (bar, NULL);
+ g_assert_null (hdy_header_bar_get_custom_title (bar));
+}
+
+
+static void
+test_hdy_header_bar_show_close_button (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_false (hdy_header_bar_get_show_close_button (bar));
+
+ hdy_header_bar_set_show_close_button (bar, TRUE);
+ g_assert_true (hdy_header_bar_get_show_close_button (bar));
+
+ hdy_header_bar_set_show_close_button (bar, FALSE);
+ g_assert_false (hdy_header_bar_get_show_close_button (bar));
+}
+
+
+static void
+test_hdy_header_bar_has_subtitle (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_true (hdy_header_bar_get_has_subtitle (bar));
+
+ hdy_header_bar_set_has_subtitle (bar, FALSE);
+ g_assert_false (hdy_header_bar_get_has_subtitle (bar));
+
+ hdy_header_bar_set_has_subtitle (bar, TRUE);
+ g_assert_true (hdy_header_bar_get_has_subtitle (bar));
+}
+
+
+static void
+test_hdy_header_bar_decoration_layout (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_null (hdy_header_bar_get_decoration_layout (bar));
+
+ hdy_header_bar_set_decoration_layout (bar, ":");
+ g_assert_cmpstr (hdy_header_bar_get_decoration_layout (bar), ==, ":");
+
+ hdy_header_bar_set_decoration_layout (bar, NULL);
+ g_assert_null (hdy_header_bar_get_decoration_layout (bar));
+}
+
+
+static void
+test_hdy_header_bar_centering_policy (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_LOOSE);
+
+ hdy_header_bar_set_centering_policy (bar, HDY_CENTERING_POLICY_STRICT);
+ g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_STRICT);
+
+ hdy_header_bar_set_centering_policy (bar, HDY_CENTERING_POLICY_LOOSE);
+ g_assert_cmpint (hdy_header_bar_get_centering_policy (bar), ==, HDY_CENTERING_POLICY_LOOSE);
+}
+
+
+static void
+test_hdy_header_bar_transition_duration (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 200);
+
+ hdy_header_bar_set_transition_duration (bar, 0);
+ g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 0);
+
+ hdy_header_bar_set_transition_duration (bar, 1000);
+ g_assert_cmpuint (hdy_header_bar_get_transition_duration (bar), ==, 1000);
+}
+
+
+static void
+test_hdy_header_bar_interpolate_size (void)
+{
+ g_autoptr (HdyHeaderBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_HEADER_BAR (hdy_header_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_false (hdy_header_bar_get_interpolate_size (bar));
+
+ hdy_header_bar_set_interpolate_size (bar, TRUE);
+ g_assert_true (hdy_header_bar_get_interpolate_size (bar));
+
+ hdy_header_bar_set_interpolate_size (bar, FALSE);
+ g_assert_false (hdy_header_bar_get_interpolate_size (bar));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/HeaderBar/pack", test_hdy_header_bar_pack);
+ g_test_add_func("/Handy/HeaderBar/title", test_hdy_header_bar_title);
+ g_test_add_func("/Handy/HeaderBar/subtitle", test_hdy_header_bar_subtitle);
+ g_test_add_func("/Handy/HeaderBar/custom_title", test_hdy_header_bar_custom_title);
+ g_test_add_func("/Handy/HeaderBar/show_close_button", test_hdy_header_bar_show_close_button);
+ g_test_add_func("/Handy/HeaderBar/has_subtitle", test_hdy_header_bar_has_subtitle);
+ g_test_add_func("/Handy/HeaderBar/decoration_layout", test_hdy_header_bar_decoration_layout);
+ g_test_add_func("/Handy/HeaderBar/centering_policy", test_hdy_header_bar_centering_policy);
+ g_test_add_func("/Handy/HeaderBar/transition_duration", test_hdy_header_bar_transition_duration);
+ g_test_add_func("/Handy/HeaderBar/interpolate_size", test_hdy_header_bar_interpolate_size);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-header-group.c b/subprojects/libhandy/tests/test-header-group.c
new file mode 100644
index 0000000..632c1ff
--- /dev/null
+++ b/subprojects/libhandy/tests/test-header-group.c
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_header_group_decorate_all (void)
+{
+ g_autoptr (HdyHeaderGroup) hg = HDY_HEADER_GROUP (hdy_header_group_new ());
+ gboolean decorate_all = FALSE;
+
+ g_assert_false (hdy_header_group_get_decorate_all (hg));
+ g_object_get (hg, "decorate-all", &decorate_all, NULL);
+ g_assert_false (decorate_all);
+
+ hdy_header_group_set_decorate_all (hg, TRUE);
+
+ g_assert_true (hdy_header_group_get_decorate_all (hg));
+ g_object_get (hg, "decorate-all", &decorate_all, NULL);
+ g_assert_true (decorate_all);
+
+ g_object_set (hg, "decorate-all", FALSE, NULL);
+
+ g_assert_false (hdy_header_group_get_decorate_all (hg));
+ g_object_get (hg, "decorate-all", &decorate_all, NULL);
+ g_assert_false (decorate_all);
+}
+
+
+static void
+test_hdy_header_group_add_remove (void)
+{
+ g_autoptr (HdyHeaderGroup) hg = HDY_HEADER_GROUP (hdy_header_group_new ());
+ g_autoptr (HdyHeaderBar) bar1 = HDY_HEADER_BAR (g_object_ref_sink (hdy_header_bar_new ()));
+ g_autoptr (GtkHeaderBar) bar2 = GTK_HEADER_BAR (g_object_ref_sink (gtk_header_bar_new ()));
+
+ g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 0);
+
+ hdy_header_group_add_header_bar (hg, bar1);
+ g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 1);
+
+ hdy_header_group_add_gtk_header_bar (hg, bar2);
+ g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 2);
+
+ hdy_header_group_remove_gtk_header_bar (hg, bar2);
+ g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 1);
+
+ hdy_header_group_remove_header_bar (hg, bar1);
+
+ g_assert_cmpint (g_slist_length (hdy_header_group_get_children (hg)), ==, 0);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/HeaderGroup/decorate_all", test_hdy_header_group_decorate_all);
+ g_test_add_func("/Handy/HeaderGroup/add_remove", test_hdy_header_group_add_remove);
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-keypad.c b/subprojects/libhandy/tests/test-keypad.c
new file mode 100644
index 0000000..f33037a
--- /dev/null
+++ b/subprojects/libhandy/tests/test-keypad.c
@@ -0,0 +1,247 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+gint notified;
+
+
+static void
+notify_cb (GtkWidget *widget,
+ gpointer data)
+{
+ notified++;
+}
+
+
+static void
+test_hdy_keypad_row_spacing (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ guint row_spacing = 0;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::row-spacing", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 6);
+ g_object_get (keypad, "row-spacing", &row_spacing, NULL);
+ g_assert_cmpuint (row_spacing, ==, 6);
+
+ hdy_keypad_set_row_spacing (keypad, 0);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 0);
+ g_object_get (keypad, "row-spacing", &row_spacing, NULL);
+ g_assert_cmpuint (row_spacing, ==, 0);
+
+ g_object_set (keypad, "row-spacing", 12, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_cmpuint (hdy_keypad_get_row_spacing (keypad), ==, 12);
+ g_object_get (keypad, "row-spacing", &row_spacing, NULL);
+ g_assert_cmpuint (row_spacing, ==, 12);
+
+ g_assert_cmpint (notified, ==, 2);
+}
+
+
+static void
+test_hdy_keypad_column_spacing (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ guint column_spacing = 0;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::column-spacing", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 6);
+ g_object_get (keypad, "column-spacing", &column_spacing, NULL);
+ g_assert_cmpuint (column_spacing, ==, 6);
+
+ hdy_keypad_set_column_spacing (keypad, 0);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 0);
+ g_object_get (keypad, "column-spacing", &column_spacing, NULL);
+ g_assert_cmpuint (column_spacing, ==, 0);
+
+ g_object_set (keypad, "column-spacing", 12, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_cmpuint (hdy_keypad_get_column_spacing (keypad), ==, 12);
+ g_object_get (keypad, "column-spacing", &column_spacing, NULL);
+ g_assert_cmpuint (column_spacing, ==, 12);
+
+ g_assert_cmpint (notified, ==, 2);
+}
+
+
+static void
+test_hdy_keypad_letters_visible (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ gboolean letters_visible = FALSE;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::letters-visible", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_true (hdy_keypad_get_letters_visible (keypad));
+ g_object_get (keypad, "letters-visible", &letters_visible, NULL);
+ g_assert_true (letters_visible);
+
+ hdy_keypad_set_letters_visible (keypad, FALSE);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_false (hdy_keypad_get_letters_visible (keypad));
+ g_object_get (keypad, "letters-visible", &letters_visible, NULL);
+ g_assert_false (letters_visible);
+
+ g_object_set (keypad, "letters-visible", TRUE, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_true (hdy_keypad_get_letters_visible (keypad));
+ g_object_get (keypad, "letters-visible", &letters_visible, NULL);
+ g_assert_true (letters_visible);
+
+ g_assert_cmpint (notified, ==, 2);
+}
+
+
+static void
+test_hdy_keypad_symbols_visible (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ gboolean symbols_visible = TRUE;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::symbols-visible", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_false (hdy_keypad_get_symbols_visible (keypad));
+ g_object_get (keypad, "symbols-visible", &symbols_visible, NULL);
+ g_assert_false (symbols_visible);
+
+ hdy_keypad_set_symbols_visible (keypad, TRUE);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_true (hdy_keypad_get_symbols_visible (keypad));
+ g_object_get (keypad, "symbols-visible", &symbols_visible, NULL);
+ g_assert_true (symbols_visible);
+
+ g_object_set (keypad, "symbols-visible", FALSE, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_false (hdy_keypad_get_symbols_visible (keypad));
+ g_object_get (keypad, "symbols-visible", &symbols_visible, NULL);
+ g_assert_false (symbols_visible);
+
+ g_assert_cmpint (notified, ==, 2);
+}
+
+
+static void
+test_hdy_keypad_entry (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ g_autoptr (GtkEntry) entry = NULL;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+ entry = g_object_ref_sink (GTK_ENTRY (gtk_entry_new ()));
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::entry", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_null (hdy_keypad_get_entry (keypad));
+
+ hdy_keypad_set_entry (keypad, entry);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_true (hdy_keypad_get_entry (keypad) == entry);
+
+ g_object_set (keypad, "entry", NULL, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_null (hdy_keypad_get_entry (keypad));
+}
+
+
+static void
+test_hdy_keypad_start_action (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ g_autoptr (GtkWidget) button = NULL;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+ button = g_object_ref_sink (gtk_button_new ());
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::start-action", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_nonnull (hdy_keypad_get_start_action (keypad));
+
+ hdy_keypad_set_start_action (keypad, button);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_true (hdy_keypad_get_start_action (keypad) == button);
+
+ g_object_set (keypad, "start-action", NULL, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_null (hdy_keypad_get_start_action (keypad));
+}
+
+
+static void
+test_hdy_keypad_end_action (void)
+{
+ g_autoptr (HdyKeypad) keypad = NULL;
+ g_autoptr (GtkWidget) button = NULL;
+
+ keypad = g_object_ref_sink (HDY_KEYPAD (hdy_keypad_new (FALSE, TRUE)));
+ button = g_object_ref_sink (gtk_button_new ());
+
+ notified = 0;
+ g_signal_connect (keypad, "notify::end-action", G_CALLBACK (notify_cb), NULL);
+
+ g_assert_nonnull (hdy_keypad_get_end_action (keypad));
+
+ hdy_keypad_set_end_action (keypad, button);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_assert_true (hdy_keypad_get_end_action (keypad) == button);
+
+ g_object_set (keypad, "end-action", NULL, NULL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_null (hdy_keypad_get_end_action (keypad));
+}
+
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func ("/Handy/Keypad/row_spacing", test_hdy_keypad_row_spacing);
+ g_test_add_func ("/Handy/Keypad/column_spacing", test_hdy_keypad_column_spacing);
+ g_test_add_func ("/Handy/Keypad/letters_visible", test_hdy_keypad_letters_visible);
+ g_test_add_func ("/Handy/Keypad/symbols_visible", test_hdy_keypad_symbols_visible);
+ g_test_add_func ("/Handy/Keypad/entry", test_hdy_keypad_entry);
+ g_test_add_func ("/Handy/Keypad/start_action", test_hdy_keypad_start_action);
+ g_test_add_func ("/Handy/Keypad/end_action", test_hdy_keypad_end_action);
+
+ return g_test_run ();
+}
diff --git a/subprojects/libhandy/tests/test-leaflet.c b/subprojects/libhandy/tests/test-leaflet.c
new file mode 100644
index 0000000..43afb3c
--- /dev/null
+++ b/subprojects/libhandy/tests/test-leaflet.c
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_leaflet_adjacent_child (void)
+{
+ g_autoptr (HdyLeaflet) leaflet = NULL;
+ GtkWidget *children[3];
+ gint i;
+ GtkWidget *result;
+
+ leaflet = HDY_LEAFLET (hdy_leaflet_new ());
+ g_assert_nonnull (leaflet);
+
+ for (i = 0; i < 3; i++) {
+ children[i] = gtk_label_new ("");
+ g_assert_nonnull (children[i]);
+
+ gtk_container_add (GTK_CONTAINER (leaflet), children[i]);
+ }
+
+ gtk_container_child_set (GTK_CONTAINER (leaflet), children[1],
+ "navigatable", FALSE,
+ NULL);
+
+ hdy_leaflet_set_visible_child (leaflet, children[0]);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_null (result);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_true (result == children[2]);
+
+ hdy_leaflet_set_visible_child (leaflet, children[1]);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_true (result == children[0]);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_true (result == children[2]);
+
+ hdy_leaflet_set_visible_child (leaflet, children[2]);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_true (result == children[0]);
+
+ result = hdy_leaflet_get_adjacent_child (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_null (result);
+}
+
+
+static void
+test_hdy_leaflet_navigate (void)
+{
+ g_autoptr (HdyLeaflet) leaflet = NULL;
+ GtkWidget *children[3];
+ gint i;
+ gboolean result;
+
+ leaflet = HDY_LEAFLET (hdy_leaflet_new ());
+ g_assert_nonnull (leaflet);
+
+ for (i = 0; i < 3; i++) {
+ children[i] = gtk_label_new ("");
+ g_assert_nonnull (children[i]);
+
+ gtk_container_add (GTK_CONTAINER (leaflet), children[i]);
+ }
+
+ gtk_container_child_set (GTK_CONTAINER (leaflet), children[1],
+ "navigatable", FALSE,
+ NULL);
+
+ hdy_leaflet_set_visible_child (leaflet, children[0]);
+
+ result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_false (result);
+
+ result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_true (result);
+ g_assert_true (hdy_leaflet_get_visible_child (leaflet) == children[2]);
+
+ result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_FORWARD);
+ g_assert_false (result);
+
+ result = hdy_leaflet_navigate (leaflet, HDY_NAVIGATION_DIRECTION_BACK);
+ g_assert_true (result);
+ g_assert_true (hdy_leaflet_get_visible_child (leaflet) == children[0]);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func ("/Handy/Leaflet/adjacent_child", test_hdy_leaflet_adjacent_child);
+ g_test_add_func ("/Handy/Leaflet/navigate", test_hdy_leaflet_navigate);
+
+ return g_test_run ();
+}
diff --git a/subprojects/libhandy/tests/test-preferences-group.c b/subprojects/libhandy/tests/test-preferences-group.c
new file mode 100644
index 0000000..d5c69ad
--- /dev/null
+++ b/subprojects/libhandy/tests/test-preferences-group.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_preferences_group_add (void)
+{
+ g_autoptr (HdyPreferencesGroup) group = NULL;
+ HdyPreferencesRow *row;
+ GtkWidget *widget;
+
+ group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ()));
+ g_assert_nonnull (group);
+
+ row = HDY_PREFERENCES_ROW (hdy_preferences_row_new ());
+ g_assert_nonnull (row);
+ gtk_container_add (GTK_CONTAINER (group), GTK_WIDGET (row));
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+ gtk_container_add (GTK_CONTAINER (group), widget);
+
+ g_assert (G_TYPE_CHECK_INSTANCE_TYPE (gtk_widget_get_parent (GTK_WIDGET (row)), GTK_TYPE_LIST_BOX));
+ g_assert (G_TYPE_CHECK_INSTANCE_TYPE (gtk_widget_get_parent (widget), GTK_TYPE_BOX));
+}
+
+
+static void
+test_hdy_preferences_group_title (void)
+{
+ g_autoptr (HdyPreferencesGroup) group = NULL;
+
+ group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ()));
+ g_assert_nonnull (group);
+
+ g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, "");
+
+ hdy_preferences_group_set_title (group, "Dummy title");
+ g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, "Dummy title");
+
+ hdy_preferences_group_set_title (group, NULL);
+ g_assert_cmpstr (hdy_preferences_group_get_title (group), ==, "");
+}
+
+
+static void
+test_hdy_preferences_group_description (void)
+{
+ g_autoptr (HdyPreferencesGroup) group = NULL;
+
+ group = g_object_ref_sink (HDY_PREFERENCES_GROUP (hdy_preferences_group_new ()));
+ g_assert_nonnull (group);
+
+ g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, "");
+
+ hdy_preferences_group_set_description (group, "Dummy description");
+ g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, "Dummy description");
+
+ hdy_preferences_group_set_description (group, NULL);
+ g_assert_cmpstr (hdy_preferences_group_get_description (group), ==, "");
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/PreferencesGroup/add", test_hdy_preferences_group_add);
+ g_test_add_func("/Handy/PreferencesGroup/title", test_hdy_preferences_group_title);
+ g_test_add_func("/Handy/PreferencesGroup/description", test_hdy_preferences_group_description);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-preferences-page.c b/subprojects/libhandy/tests/test-preferences-page.c
new file mode 100644
index 0000000..5f6d15c
--- /dev/null
+++ b/subprojects/libhandy/tests/test-preferences-page.c
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_preferences_page_add (void)
+{
+ g_autoptr (HdyPreferencesPage) page = NULL;
+ HdyPreferencesGroup *group;
+ GtkWidget *widget;
+
+ page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ()));
+ g_assert_nonnull (page);
+
+ group = HDY_PREFERENCES_GROUP (hdy_preferences_group_new ());
+ g_assert_nonnull (group);
+ gtk_container_add (GTK_CONTAINER (page), GTK_WIDGET (group));
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+ g_test_expect_message (HDY_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Can't add children of type GtkSwitch to HdyPreferencesPage");
+ gtk_container_add (GTK_CONTAINER (page), widget);
+ g_test_assert_expected_messages ();
+}
+
+
+static void
+test_hdy_preferences_page_title (void)
+{
+ g_autoptr (HdyPreferencesPage) page = NULL;
+
+ page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ()));
+ g_assert_nonnull (page);
+
+ g_assert_null (hdy_preferences_page_get_title (page));
+
+ hdy_preferences_page_set_title (page, "Dummy title");
+ g_assert_cmpstr (hdy_preferences_page_get_title (page), ==, "Dummy title");
+
+ hdy_preferences_page_set_title (page, NULL);
+ g_assert_null (hdy_preferences_page_get_title (page));
+}
+
+
+static void
+test_hdy_preferences_page_icon_name (void)
+{
+ g_autoptr (HdyPreferencesPage) page = NULL;
+
+ page = g_object_ref_sink (HDY_PREFERENCES_PAGE (hdy_preferences_page_new ()));
+ g_assert_nonnull (page);
+
+ g_assert_null (hdy_preferences_page_get_icon_name (page));
+
+ hdy_preferences_page_set_icon_name (page, "dummy-icon-name");
+ g_assert_cmpstr (hdy_preferences_page_get_icon_name (page), ==, "dummy-icon-name");
+
+ hdy_preferences_page_set_icon_name (page, NULL);
+ g_assert_null (hdy_preferences_page_get_icon_name (page));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/PreferencesPage/add", test_hdy_preferences_page_add);
+ g_test_add_func("/Handy/PreferencesPage/title", test_hdy_preferences_page_title);
+ g_test_add_func("/Handy/PreferencesPage/icon_name", test_hdy_preferences_page_icon_name);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-preferences-row.c b/subprojects/libhandy/tests/test-preferences-row.c
new file mode 100644
index 0000000..c4f1769
--- /dev/null
+++ b/subprojects/libhandy/tests/test-preferences-row.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_preferences_row_title (void)
+{
+ g_autoptr (HdyPreferencesRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_PREFERENCES_ROW (hdy_preferences_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_null (hdy_preferences_row_get_title (row));
+
+ hdy_preferences_row_set_title (row, "Dummy title");
+ g_assert_cmpstr (hdy_preferences_row_get_title (row), ==, "Dummy title");
+
+ hdy_preferences_row_set_title (row, NULL);
+ g_assert_null (hdy_preferences_row_get_title (row));
+}
+
+
+static void
+test_hdy_preferences_row_use_undeline (void)
+{
+ g_autoptr (HdyPreferencesRow) row = NULL;
+
+ row = g_object_ref_sink (HDY_PREFERENCES_ROW (hdy_preferences_row_new ()));
+ g_assert_nonnull (row);
+
+ g_assert_false (hdy_preferences_row_get_use_underline (row));
+
+ hdy_preferences_row_set_use_underline (row, TRUE);
+ g_assert_true (hdy_preferences_row_get_use_underline (row));
+
+ hdy_preferences_row_set_use_underline (row, FALSE);
+ g_assert_false (hdy_preferences_row_get_use_underline (row));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/PreferencesRow/title", test_hdy_preferences_row_title);
+ g_test_add_func("/Handy/PreferencesRow/use_underline", test_hdy_preferences_row_use_undeline);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-preferences-window.c b/subprojects/libhandy/tests/test-preferences-window.c
new file mode 100644
index 0000000..32a0f8d
--- /dev/null
+++ b/subprojects/libhandy/tests/test-preferences-window.c
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_preferences_window_add (void)
+{
+ g_autoptr (HdyPreferencesWindow) window = NULL;
+ HdyPreferencesPage *page;
+ GtkWidget *widget;
+
+ window = g_object_ref_sink (HDY_PREFERENCES_WINDOW (hdy_preferences_window_new ()));
+ g_assert_nonnull (window);
+
+ page = HDY_PREFERENCES_PAGE (hdy_preferences_page_new ());
+ g_assert_nonnull (page);
+ gtk_container_add (GTK_CONTAINER (window), GTK_WIDGET (page));
+
+ widget = gtk_switch_new ();
+ g_assert_nonnull (widget);
+ g_test_expect_message (HDY_LOG_DOMAIN, G_LOG_LEVEL_WARNING, "Can't add children of type GtkSwitch to HdyPreferencesWindow");
+ gtk_container_add (GTK_CONTAINER (window), widget);
+ g_test_assert_expected_messages ();
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/PreferencesWindow/add", test_hdy_preferences_window_add);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-search-bar.c b/subprojects/libhandy/tests/test-search-bar.c
new file mode 100644
index 0000000..9f90721
--- /dev/null
+++ b/subprojects/libhandy/tests/test-search-bar.c
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_search_bar_add (void)
+{
+ g_autoptr (HdySearchBar) bar = NULL;
+ GtkWidget *entry;
+
+ bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ()));
+ g_assert_nonnull (bar);
+
+ entry = gtk_entry_new ();
+ g_assert_nonnull (entry);
+
+ gtk_container_add (GTK_CONTAINER (bar), entry);
+}
+
+
+static void
+test_hdy_search_bar_connect_entry (void)
+{
+ g_autoptr (HdySearchBar) bar = NULL;
+ GtkWidget *box, *entry;
+
+ bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ()));
+ g_assert_nonnull (bar);
+
+ box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
+ g_assert_nonnull (box);
+
+ entry = gtk_entry_new ();
+ g_assert_nonnull (entry);
+
+ gtk_container_add (GTK_CONTAINER (box), entry);
+ gtk_container_add (GTK_CONTAINER (bar), box);
+ hdy_search_bar_connect_entry (bar, GTK_ENTRY (entry));
+}
+
+
+static void
+test_hdy_search_bar_search_mode (void)
+{
+ g_autoptr (HdySearchBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_false (hdy_search_bar_get_search_mode (bar));
+
+ hdy_search_bar_set_search_mode (bar, TRUE);
+ g_assert_true (hdy_search_bar_get_search_mode (bar));
+
+ hdy_search_bar_set_search_mode (bar, FALSE);
+ g_assert_false (hdy_search_bar_get_search_mode (bar));
+}
+
+
+static void
+test_hdy_search_bar_show_close_button (void)
+{
+ g_autoptr (HdySearchBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_SEARCH_BAR (hdy_search_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_false (hdy_search_bar_get_show_close_button (bar));
+
+ hdy_search_bar_set_show_close_button (bar, TRUE);
+ g_assert_true (hdy_search_bar_get_show_close_button (bar));
+
+ hdy_search_bar_set_show_close_button (bar, FALSE);
+ g_assert_false (hdy_search_bar_get_show_close_button (bar));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/SearchBar/add", test_hdy_search_bar_add);
+ g_test_add_func("/Handy/SearchBar/connect_entry", test_hdy_search_bar_connect_entry);
+ g_test_add_func("/Handy/SearchBar/search_mode", test_hdy_search_bar_search_mode);
+ g_test_add_func("/Handy/SearchBar/show_close_button", test_hdy_search_bar_show_close_button);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-squeezer.c b/subprojects/libhandy/tests/test-squeezer.c
new file mode 100644
index 0000000..90a12e7
--- /dev/null
+++ b/subprojects/libhandy/tests/test-squeezer.c
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_squeezer_homogeneous (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_true (hdy_squeezer_get_homogeneous (squeezer));
+
+ hdy_squeezer_set_homogeneous (squeezer, FALSE);
+ g_assert_false (hdy_squeezer_get_homogeneous (squeezer));
+
+ hdy_squeezer_set_homogeneous (squeezer, TRUE);
+ g_assert_true (hdy_squeezer_get_homogeneous (squeezer));
+}
+
+
+static void
+test_hdy_squeezer_transition_duration (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, 200);
+
+ hdy_squeezer_set_transition_duration (squeezer, 400);
+ g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, 400);
+
+ hdy_squeezer_set_transition_duration (squeezer, -1);
+ g_assert_cmpuint (hdy_squeezer_get_transition_duration (squeezer), ==, G_MAXUINT);
+}
+
+
+static void
+test_hdy_squeezer_transition_type (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_NONE);
+
+ hdy_squeezer_set_transition_type (squeezer, HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE);
+ g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_CROSSFADE);
+
+ hdy_squeezer_set_transition_type (squeezer, HDY_SQUEEZER_TRANSITION_TYPE_NONE);
+ g_assert_cmpuint (hdy_squeezer_get_transition_type (squeezer), ==, HDY_SQUEEZER_TRANSITION_TYPE_NONE);
+}
+
+
+static void
+test_hdy_squeezer_transition_running (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_false (hdy_squeezer_get_transition_running (squeezer));
+}
+
+
+static void
+test_hdy_squeezer_show_hide_child (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+ GtkWidget *child;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_null (hdy_squeezer_get_visible_child (squeezer));
+
+ child = gtk_label_new ("");
+ gtk_container_add (GTK_CONTAINER (squeezer), child);
+ g_assert_null (hdy_squeezer_get_visible_child (squeezer));
+
+ gtk_widget_show (child);
+ g_assert (hdy_squeezer_get_visible_child (squeezer) == child);
+
+ gtk_widget_hide (child);
+ g_assert_null (hdy_squeezer_get_visible_child (squeezer));
+
+ gtk_widget_show (child);
+ g_assert (hdy_squeezer_get_visible_child (squeezer) == child);
+
+ gtk_container_remove (GTK_CONTAINER (squeezer), child);
+ g_assert_null (hdy_squeezer_get_visible_child (squeezer));
+}
+
+
+static void
+test_hdy_squeezer_interpolate_size (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ g_assert_false (hdy_squeezer_get_interpolate_size (squeezer));
+
+ hdy_squeezer_set_interpolate_size (squeezer, TRUE);
+ g_assert_true (hdy_squeezer_get_interpolate_size (squeezer));
+
+ hdy_squeezer_set_interpolate_size (squeezer, FALSE);
+ g_assert_false (hdy_squeezer_get_interpolate_size (squeezer));
+}
+
+
+static void
+test_hdy_squeezer_child_enabled (void)
+{
+ g_autoptr (HdySqueezer) squeezer = NULL;
+ GtkWidget *child;
+
+ squeezer = g_object_ref_sink (HDY_SQUEEZER (hdy_squeezer_new ()));
+ g_assert_nonnull (squeezer);
+
+ child = gtk_label_new ("");
+ gtk_widget_show (child);
+ gtk_container_add (GTK_CONTAINER (squeezer), child);
+ g_assert_true (hdy_squeezer_get_child_enabled (squeezer, child));
+
+ hdy_squeezer_set_child_enabled (squeezer, child, FALSE);
+ g_assert_false (hdy_squeezer_get_child_enabled (squeezer, child));
+
+ hdy_squeezer_set_child_enabled (squeezer, child, TRUE);
+ g_assert_true (hdy_squeezer_get_child_enabled (squeezer, child));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ViewSwitcher/homogeneous", test_hdy_squeezer_homogeneous);
+ g_test_add_func("/Handy/ViewSwitcher/transition_duration", test_hdy_squeezer_transition_duration);
+ g_test_add_func("/Handy/ViewSwitcher/transition_type", test_hdy_squeezer_transition_type);
+ g_test_add_func("/Handy/ViewSwitcher/transition_running", test_hdy_squeezer_transition_running);
+ g_test_add_func("/Handy/ViewSwitcher/show_hide_child", test_hdy_squeezer_show_hide_child);
+ g_test_add_func("/Handy/ViewSwitcher/interpolate_size", test_hdy_squeezer_interpolate_size);
+ g_test_add_func("/Handy/ViewSwitcher/child_enabled", test_hdy_squeezer_child_enabled);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-swipe-group.c b/subprojects/libhandy/tests/test-swipe-group.c
new file mode 100644
index 0000000..41b174f
--- /dev/null
+++ b/subprojects/libhandy/tests/test-swipe-group.c
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+static void
+test_hdy_swipe_group_add_remove (void)
+{
+ g_autoptr (HdySwipeGroup) group = NULL;
+ g_autoptr (HdySwipeable) swipeable1 = NULL;
+ g_autoptr (HdySwipeable) swipeable2 = NULL;
+
+ group = hdy_swipe_group_new ();
+
+ swipeable1 = HDY_SWIPEABLE (hdy_carousel_new ());
+ swipeable2 = HDY_SWIPEABLE (hdy_carousel_new ());
+
+ g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 0);
+
+ hdy_swipe_group_add_swipeable (group, swipeable1);
+ g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 1);
+
+ hdy_swipe_group_add_swipeable (group, swipeable2);
+ g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 2);
+
+ hdy_swipe_group_remove_swipeable (group, swipeable2);
+ g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 1);
+
+ hdy_swipe_group_remove_swipeable (group, swipeable1);
+ g_assert_cmpint (g_slist_length (hdy_swipe_group_get_swipeables (group)), ==, 0);
+}
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/SwipeGroup/add_remove", test_hdy_swipe_group_add_remove);
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-value-object.c b/subprojects/libhandy/tests/test-value-object.c
new file mode 100644
index 0000000..3b9f03c
--- /dev/null
+++ b/subprojects/libhandy/tests/test-value-object.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 Red Hat Inc.
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_value_object_init (void)
+{
+ HdyValueObject *obj;
+ GValue value = G_VALUE_INIT;
+ gchar *str;
+
+ g_value_init (&value, G_TYPE_STRING);
+ g_value_set_string (&value, "asdfasdf");
+ obj = hdy_value_object_new (&value);
+ g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf");
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_string ("asdfasdf");
+ g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf");
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_take_string (g_strdup ("asdfasdf"));
+ g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf");
+ g_clear_object (&obj);
+
+ obj = hdy_value_object_new_collect (G_TYPE_STRING, "asdfasdf");
+ g_assert_cmpstr (hdy_value_object_get_string (obj), ==, "asdfasdf");
+
+ /* And check that _dup_string works too */
+ str = hdy_value_object_dup_string (obj);
+ g_assert_cmpstr (str, ==, "asdfasdf");
+ g_clear_pointer (&str, g_free);
+ g_clear_object (&obj);
+
+ g_value_unset (&value);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ValueObject/init", test_hdy_value_object_init);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-view-switcher-bar.c b/subprojects/libhandy/tests/test-view-switcher-bar.c
new file mode 100644
index 0000000..54538a6
--- /dev/null
+++ b/subprojects/libhandy/tests/test-view-switcher-bar.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_view_switcher_bar_policy (void)
+{
+ g_autoptr (HdyViewSwitcherBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_AUTO);
+ g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_AUTO);
+
+ hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_WIDE);
+ g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_WIDE);
+
+ hdy_view_switcher_bar_set_policy (bar, HDY_VIEW_SWITCHER_POLICY_NARROW);
+ g_assert_cmpint (hdy_view_switcher_bar_get_policy (bar), ==, HDY_VIEW_SWITCHER_POLICY_NARROW);
+}
+
+
+static void
+test_hdy_view_switcher_bar_stack (void)
+{
+ g_autoptr (HdyViewSwitcherBar) bar = NULL;
+ GtkStack *stack;
+
+ bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ()));
+ g_assert_nonnull (bar);
+
+ stack = GTK_STACK (gtk_stack_new ());
+ g_assert_nonnull (stack);
+
+ g_assert_null (hdy_view_switcher_bar_get_stack (bar));
+
+ hdy_view_switcher_bar_set_stack (bar, stack);
+ g_assert (hdy_view_switcher_bar_get_stack (bar) == stack);
+
+ hdy_view_switcher_bar_set_stack (bar, NULL);
+ g_assert_null (hdy_view_switcher_bar_get_stack (bar));
+}
+
+
+static void
+test_hdy_view_switcher_bar_reveal (void)
+{
+ g_autoptr (HdyViewSwitcherBar) bar = NULL;
+
+ bar = g_object_ref_sink (HDY_VIEW_SWITCHER_BAR (hdy_view_switcher_bar_new ()));
+ g_assert_nonnull (bar);
+
+ g_assert_false (hdy_view_switcher_bar_get_reveal (bar));
+
+ hdy_view_switcher_bar_set_reveal (bar, TRUE);
+ g_assert_true (hdy_view_switcher_bar_get_reveal (bar));
+
+ hdy_view_switcher_bar_set_reveal (bar, FALSE);
+ g_assert_false (hdy_view_switcher_bar_get_reveal (bar));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ViewSwitcherBar/policy", test_hdy_view_switcher_bar_policy);
+ g_test_add_func("/Handy/ViewSwitcherBar/stack", test_hdy_view_switcher_bar_stack);
+ g_test_add_func("/Handy/ViewSwitcherBar/reveal", test_hdy_view_switcher_bar_reveal);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-view-switcher.c b/subprojects/libhandy/tests/test-view-switcher.c
new file mode 100644
index 0000000..2f6c89d
--- /dev/null
+++ b/subprojects/libhandy/tests/test-view-switcher.c
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2019 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_view_switcher_policy (void)
+{
+ g_autoptr (HdyViewSwitcher) view_switcher = NULL;
+
+ view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ()));
+ g_assert_nonnull (view_switcher);
+
+ g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_AUTO);
+
+ hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_NARROW);
+ g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_NARROW);
+
+ hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_WIDE);
+ g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_WIDE);
+
+ hdy_view_switcher_set_policy (view_switcher, HDY_VIEW_SWITCHER_POLICY_AUTO);
+ g_assert_cmpint (hdy_view_switcher_get_policy (view_switcher), ==, HDY_VIEW_SWITCHER_POLICY_AUTO);
+}
+
+
+static void
+test_hdy_view_switcher_narrow_ellipsize (void)
+{
+ g_autoptr (HdyViewSwitcher) view_switcher = NULL;
+
+ view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ()));
+ g_assert_nonnull (view_switcher);
+
+ g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_NONE);
+
+ hdy_view_switcher_set_narrow_ellipsize (view_switcher, PANGO_ELLIPSIZE_END);
+ g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_END);
+
+ hdy_view_switcher_set_narrow_ellipsize (view_switcher, PANGO_ELLIPSIZE_NONE);
+ g_assert_cmpint (hdy_view_switcher_get_narrow_ellipsize (view_switcher), ==, PANGO_ELLIPSIZE_NONE);
+}
+
+
+static void
+test_hdy_view_switcher_stack (void)
+{
+ g_autoptr (HdyViewSwitcher) view_switcher = NULL;
+ GtkStack *stack;
+
+ view_switcher = g_object_ref_sink (HDY_VIEW_SWITCHER (hdy_view_switcher_new ()));
+ g_assert_nonnull (view_switcher);
+
+ stack = GTK_STACK (gtk_stack_new ());
+ g_assert_nonnull (stack);
+
+ g_assert_null (hdy_view_switcher_get_stack (view_switcher));
+
+ hdy_view_switcher_set_stack (view_switcher, stack);
+ g_assert (hdy_view_switcher_get_stack (view_switcher) == stack);
+
+ hdy_view_switcher_set_stack (view_switcher, NULL);
+ g_assert_null (hdy_view_switcher_get_stack (view_switcher));
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/ViewSwitcher/policy", test_hdy_view_switcher_policy);
+ g_test_add_func("/Handy/ViewSwitcher/narrow_ellipsize", test_hdy_view_switcher_narrow_ellipsize);
+ g_test_add_func("/Handy/ViewSwitcher/stack", test_hdy_view_switcher_stack);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-window-handle.c b/subprojects/libhandy/tests/test-window-handle.c
new file mode 100644
index 0000000..190c667
--- /dev/null
+++ b/subprojects/libhandy/tests/test-window-handle.c
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_window_handle_new (void)
+{
+ g_autoptr (GtkWidget) handle = NULL;
+
+ handle = g_object_ref_sink (hdy_window_handle_new ());
+ g_assert_nonnull (handle);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/WindowHandle/new", test_hdy_window_handle_new);
+
+ return g_test_run();
+}
diff --git a/subprojects/libhandy/tests/test-window.c b/subprojects/libhandy/tests/test-window.c
new file mode 100644
index 0000000..4b286de
--- /dev/null
+++ b/subprojects/libhandy/tests/test-window.c
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <alexm@gnome.org>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include <handy.h>
+
+
+static void
+test_hdy_window_new (void)
+{
+ g_autoptr (GtkWidget) window = NULL;
+
+ window = g_object_ref_sink (hdy_window_new ());
+ g_assert_nonnull (window);
+}
+
+
+gint
+main (gint argc,
+ gchar *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ hdy_init ();
+
+ g_test_add_func("/Handy/Window/new", test_hdy_window_new);
+
+ return g_test_run();
+}