4 Commits
1.0 ... master

Author SHA1 Message Date
dd7e1e22ee Update README 2020-05-09 15:42:15 -04:00
0ed52013dd Create WdHeadForm class 2020-05-09 13:37:18 -04:00
f3f2b9e9e2 Backport GTK4 changes
Includes refactoring to use GMenuModel and GtkEventController
2020-05-08 20:26:33 -04:00
71ce9741d1 Add package links 2020-05-08 20:21:59 -04:00
13 changed files with 842 additions and 813 deletions

View File

@ -8,7 +8,7 @@ Copyright: 2020 Jason Francis <jason@cycles.network>
License: CC0-1.0 License: CC0-1.0
Files: resources/style.css resources/wdisplays.desktop.in resources/*.ui Files: resources/style.css resources/wdisplays.desktop.in resources/*.ui
resources/resources.xml resources/resources.xml.in
Copyright: 2020 Jason Francis <jason@cycles.network> Copyright: 2020 Jason Francis <jason@cycles.network>
License: GPL-3.0-or-later License: GPL-3.0-or-later

View File

@ -4,13 +4,27 @@
wdisplays is a graphical application for configuring displays in Wayland wdisplays is a graphical application for configuring displays in Wayland
compositors. It borrows some code from [kanshi]. It should work in any compositors. It borrows some code from [kanshi]. It should work in any
compositor that implements the wlr-output-management-unstable-v1 protocol, compositor that implements the wlr-output-management-unstable-v1 protocol.
including [sway]. The goal of this project is to allow precise adjustment of Compositors that are known to support the protocol are [Sway] and [Wayfire].
display settings in kiosks, digital signage, and other elaborate multi-monitor The goal of this project is to allow precise adjustment of display settings in
setups. kiosks, digital signage, and other elaborate multi-monitor setups.
![Screenshot](wdisplays.png) ![Screenshot](wdisplays.png)
# Installation
[![Repology][repology-img]][repology-pkg]
Check your distro for a `wdisplays` package. Known distro packages:
- [Alpine](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/wdisplays)
- [Arch](https://aur.archlinux.org/packages/wdisplays-git/)
- [Debian](https://packages.debian.org/sid/wdisplays)
- [Fedora](https://copr.fedorainfracloud.org/coprs/wef/wdisplays/)
- [FreeBSD](https://svnweb.freebsd.org/ports/head/x11/wdisplays/)
- [Nix](https://github.com/NixOS/nixpkgs/tree/master/pkgs/tools/graphics/wdisplays)
- [OpenSUSE](https://build.opensuse.org/package/show/home%3AMWh3/wdisplays)
# Building # Building
Build requirements are: Build requirements are:
@ -26,8 +40,6 @@ ninja -C build
sudo ninja -C build install sudo ninja -C build install
``` ```
Binaries are not available. Only building from source is supported.
# Usage # Usage
Displays can be moved around the virtual screen space by clicking and dragging Displays can be moved around the virtual screen space by clicking and dragging
@ -60,10 +72,19 @@ file. See man `sway-output`. If you want to have multiple configurations
depending on the monitors connected, you'll need to use an external program depending on the monitors connected, you'll need to use an external program
like [kanshi]. Integration with that and other external daemons is planned. like [kanshi]. Integration with that and other external daemons is planned.
### How do I add support to my compositor?
A minimal amount of code (approximately 150-200 LOC) is currently required to
get support for this in wlroots compositors. See the diff here for a sample
implementation on top of tinywl: [tinywl-output-management].
[kanshi]: https://github.com/emersion/kanshi [kanshi]: https://github.com/emersion/kanshi
[sway]: https://github.com/swaywm/sway [Sway]: https://swaywm.org
[Wayfire]: https://wayfire.org
[ARandR]: https://christian.amsuess.com/tools/arandr/ [ARandR]: https://christian.amsuess.com/tools/arandr/
[tinywl-output-management]: https://git.sr.ht/~jf/tinywl-output-management/commit/87a45d89ae0e7975e2a59f84e960380dd2f5ac08
[license-img]: https://img.shields.io/badge/License-GPL%203.0%20or%20later-blue.svg?logo=gnu [license-img]: https://img.shields.io/badge/License-GPL%203.0%20or%20later-blue.svg?logo=gnu
[license-spdx]: https://spdx.org/licenses/GPL-3.0-or-later.html [license-spdx]: https://spdx.org/licenses/GPL-3.0-or-later.html
[repology-img]: https://repology.org/badge/tiny-repos/wdisplays.svg
[repology-pkg]: https://repology.org/project/wdisplays/versions

View File

@ -3,9 +3,11 @@
project('network.cycles.wdisplays', 'c', license: 'MIT', version: '1.0') project('network.cycles.wdisplays', 'c', license: 'MIT', version: '1.0')
conf = configuration_data() conf = configuration_data({
conf.set('app_id', meson.project_name()) 'app_id': meson.project_name(),
conf.set('version', meson.project_version()) 'version': meson.project_version(),
'resource_prefix': '/' / '/'.join(meson.project_name().split('.')),
})
subdir('protocol') subdir('protocol')
subdir('resources') subdir('resources')

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 --> <!-- Generated with glade 3.22.2 -->
<interface> <interface>
<requires lib="gtk+" version="3.22"/> <requires lib="gtk+" version="3.22"/>
<object class="GtkAdjustment" id="height_adjustment"> <object class="GtkAdjustment" id="height_adjustment">
@ -7,23 +7,6 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">10</property> <property name="page_increment">10</property>
</object> </object>
<object class="GtkPopover" id="modes">
<property name="can_focus">False</property>
<child>
<object class="GtkBox" id="mode_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
<object class="GtkAdjustment" id="pos_x_adjustment"> <object class="GtkAdjustment" id="pos_x_adjustment">
<property name="upper">16383</property> <property name="upper">16383</property>
<property name="step_increment">1</property> <property name="step_increment">1</property>
@ -50,7 +33,7 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">10</property> <property name="page_increment">10</property>
</object> </object>
<object class="GtkGrid" id="form"> <template class="WdHeadForm" parent="GtkGrid">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="margin_start">8</property> <property name="margin_start">8</property>
@ -69,7 +52,7 @@
<property name="halign">start</property> <property name="halign">start</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="enabled" swapped="no"/> <signal name="toggled" handler="enabled_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -99,7 +82,7 @@
<property name="adjustment">scale_adjustment</property> <property name="adjustment">scale_adjustment</property>
<property name="digits">2</property> <property name="digits">2</property>
<property name="value">1</property> <property name="value">1</property>
<signal name="change-value" handler="scale" swapped="no"/> <signal name="value-changed" handler="position_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -194,6 +177,7 @@
<property name="spacing">8</property> <property name="spacing">8</property>
<child> <child>
<object class="GtkSpinButton" id="refresh"> <object class="GtkSpinButton" id="refresh">
<property name="name">refresh</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">9</property> <property name="width_chars">9</property>
@ -202,6 +186,7 @@
<property name="digits">3</property> <property name="digits">3</property>
<property name="numeric">True</property> <property name="numeric">True</property>
<property name="update_policy">if-valid</property> <property name="update_policy">if-valid</property>
<signal name="value-changed" handler="mode_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>
@ -245,7 +230,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="popover">transforms</property>
<child> <child>
<placeholder/> <placeholder/>
</child> </child>
@ -277,7 +261,7 @@
<property name="halign">start</property> <property name="halign">start</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<property name="draw_indicator">True</property> <property name="draw_indicator">True</property>
<signal name="toggled" handler="flipped" swapped="no"/> <signal name="toggled" handler="flipped_toggled" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">1</property> <property name="left_attach">1</property>
@ -299,6 +283,7 @@
<property name="adjustment">pos_x_adjustment</property> <property name="adjustment">pos_x_adjustment</property>
<property name="numeric">True</property> <property name="numeric">True</property>
<property name="update_policy">if-valid</property> <property name="update_policy">if-valid</property>
<signal name="value-changed" handler="position_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
@ -315,6 +300,7 @@
<property name="adjustment">pos_y_adjustment</property> <property name="adjustment">pos_y_adjustment</property>
<property name="numeric">True</property> <property name="numeric">True</property>
<property name="update_policy">if-valid</property> <property name="update_policy">if-valid</property>
<signal name="value-changed" handler="position_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">2</property> <property name="left_attach">2</property>
@ -323,6 +309,7 @@
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="width"> <object class="GtkSpinButton" id="width">
<property name="name">width</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">4</property> <property name="width_chars">4</property>
@ -331,6 +318,7 @@
<property name="adjustment">width_adjustment</property> <property name="adjustment">width_adjustment</property>
<property name="numeric">True</property> <property name="numeric">True</property>
<property name="update_policy">if-valid</property> <property name="update_policy">if-valid</property>
<signal name="value-changed" handler="mode_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">0</property> <property name="left_attach">0</property>
@ -351,6 +339,7 @@
</child> </child>
<child> <child>
<object class="GtkSpinButton" id="height"> <object class="GtkSpinButton" id="height">
<property name="name">height</property>
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="width_chars">4</property> <property name="width_chars">4</property>
@ -359,6 +348,7 @@
<property name="adjustment">height_adjustment</property> <property name="adjustment">height_adjustment</property>
<property name="numeric">True</property> <property name="numeric">True</property>
<property name="update_policy">if-valid</property> <property name="update_policy">if-valid</property>
<signal name="value-changed" handler="mode_spin_changed" swapped="no"/>
</object> </object>
<packing> <packing>
<property name="left_attach">2</property> <property name="left_attach">2</property>
@ -371,9 +361,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Select Mode Preset</property> <property name="tooltip_text" translatable="yes">Select Mode Preset</property>
<property name="margin_left">8</property>
<property name="margin_start">8</property> <property name="margin_start">8</property>
<property name="popover">modes</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
@ -406,76 +394,5 @@
<child> <child>
<placeholder/> <placeholder/>
</child> </child>
</object> </template>
<object class="GtkPopover" id="transforms">
<property name="can_focus">False</property>
<property name="relative_to">rotate_button</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton" id="rotate_0">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">transform.rotate_0</property>
<property name="text" translatable="yes">Don't Rotate</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="rotate_90">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">transform.rotate_90</property>
<property name="text" translatable="yes">Rotate 90°</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="rotate_180">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">transform.rotate_180</property>
<property name="text" translatable="yes">Rotate 180°</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkModelButton" id="rotate_270">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">transform.rotate_270</property>
<property name="text" translatable="yes">Rotate 270°</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
</child>
</object>
</interface> </interface>

View File

@ -1,11 +1,17 @@
# SPDX-FileCopyrightText: 2020 Jason Francis <jason@cycles.network> # SPDX-FileCopyrightText: 2020 Jason Francis <jason@cycles.network>
# SPDX-License-Identifier: CC0-1.0 # SPDX-License-Identifier: CC0-1.0
resources_xml = configure_file(
input: 'resources.xml.in',
output: 'resources.xml',
configuration: conf
)
gnome = import('gnome') gnome = import('gnome')
resources = gnome.compile_resources( resources = gnome.compile_resources(
'waydisplay-resources', 'resources.xml', 'wdisplays-resources', resources_xml,
source_dir : '.', source_dir : '.',
c_name : 'waydisplay_resources') c_name : 'wdisplays_resources')
scour = find_program('scour', required: false) scour = find_program('scour', required: false)

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/"> <gresource prefix="@resource_prefix@">
<file compressed="true" preprocess="xml-stripblanks">wdisplays.ui</file> <file compressed="true" preprocess="xml-stripblanks">wdisplays.ui</file>
<file compressed="true" preprocess="xml-stripblanks">head.ui</file> <file compressed="true" preprocess="xml-stripblanks">head.ui</file>
<file compressed="true">style.css</file> <file compressed="true">style.css</file>

View File

@ -11,62 +11,6 @@
<property name="step_increment">1</property> <property name="step_increment">1</property>
<property name="page_increment">10</property> <property name="page_increment">10</property>
</object> </object>
<object class="GtkPopover" id="main_menu">
<property name="can_focus">False</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">10</property>
<property name="margin_end">10</property>
<property name="margin_top">10</property>
<property name="margin_bottom">10</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.auto-apply</property>
<property name="text" translatable="yes">_Automatically Apply Changes</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.capture-screens</property>
<property name="text" translatable="yes">Show Screen Contents</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkModelButton">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="action_name">app.show-overlay</property>
<property name="text" translatable="yes">Overlay Screen Names</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
</child>
</object>
<object class="GtkWindow" id="heads_window"> <object class="GtkWindow" id="heads_window">
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="title" translatable="yes">wdisplays</property> <property name="title" translatable="yes">wdisplays</property>
@ -171,8 +115,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="halign">center</property> <property name="halign">center</property>
<property name="margin_left">8</property>
<property name="margin_right">8</property>
<property name="margin_start">8</property> <property name="margin_start">8</property>
<property name="margin_end">8</property> <property name="margin_end">8</property>
<property name="margin_top">8</property> <property name="margin_top">8</property>
@ -248,14 +190,14 @@
<object class="GtkButtonBox" id="zoom_box"> <object class="GtkButtonBox" id="zoom_box">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">False</property> <property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child> <child>
<object class="GtkButton" id="zoom_out"> <object class="GtkButton" id="zoom_out">
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom Out</property> <property name="tooltip_text" translatable="yes">Zoom Out</property>
<signal name="clicked" handler="zoom_out" swapped="yes"/> <property name="action_name">app.zoom-out</property>
<accelerator key="minus" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
@ -263,7 +205,6 @@
<property name="icon_name">zoom-out-symbolic</property> <property name="icon_name">zoom-out-symbolic</property>
</object> </object>
</child> </child>
<accelerator key="minus" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -278,7 +219,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom Reset</property> <property name="tooltip_text" translatable="yes">Zoom Reset</property>
<signal name="clicked" handler="zoom_reset" swapped="yes"/> <property name="action_name">app.zoom-reset</property>
<accelerator key="0" signal="clicked" modifiers="GDK_CONTROL_MASK"/> <accelerator key="0" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object> </object>
<packing> <packing>
@ -294,7 +235,8 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom In</property> <property name="tooltip_text" translatable="yes">Zoom In</property>
<signal name="clicked" handler="zoom_in" swapped="yes"/> <property name="action_name">app.zoom-in</property>
<accelerator key="equal" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
@ -302,7 +244,6 @@
<property name="icon_name">zoom-in-symbolic</property> <property name="icon_name">zoom-in-symbolic</property>
</object> </object>
</child> </child>
<accelerator key="equal" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object> </object>
<packing> <packing>
<property name="expand">True</property> <property name="expand">True</property>
@ -311,6 +252,9 @@
<property name="non_homogeneous">True</property> <property name="non_homogeneous">True</property>
</packing> </packing>
</child> </child>
<style>
<class name="linked"/>
</style>
</object> </object>
</child> </child>
<child> <child>
@ -318,7 +262,6 @@
<property name="visible">True</property> <property name="visible">True</property>
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="popover">main_menu</property>
<child> <child>
<object class="GtkImage"> <object class="GtkImage">
<property name="visible">True</property> <property name="visible">True</property>
@ -355,7 +298,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="clicked" handler="apply_changes" swapped="no"/> <property name="action_name">app.apply-changes</property>
<style> <style>
<class name="suggested-action"/> <class name="suggested-action"/>
</style> </style>
@ -371,7 +314,7 @@
<property name="can_focus">True</property> <property name="can_focus">True</property>
<property name="receives_default">True</property> <property name="receives_default">True</property>
<property name="use_underline">True</property> <property name="use_underline">True</property>
<signal name="clicked" handler="cancel_changes" swapped="no"/> <property name="action_name">app.cancel-changes</property>
</object> </object>
<packing> <packing>
<property name="position">1</property> <property name="position">1</property>

View File

@ -6,5 +6,6 @@
#define WDISPLAYS_APP_ID "@app_id@" #define WDISPLAYS_APP_ID "@app_id@"
#define WDISPLAYS_VERSION "@version@" #define WDISPLAYS_VERSION "@version@"
#define WDISPLAYS_RESOURCE_PREFIX "@resource_prefix@"
#endif #endif

393
src/headform.c Normal file
View File

@ -0,0 +1,393 @@
/* SPDX-FileCopyrightText: 2020 Jason Francis <jason@cycles.network>
* SPDX-License-Identifier: GPL-3.0-or-later */
#include "headform.h"
#include "wdisplays.h"
typedef struct _WdHeadFormPrivate {
GtkWidget *enabled;
GtkWidget *description;
GtkWidget *physical_size;
GtkWidget *scale;
GtkWidget *pos_x;
GtkWidget *pos_y;
GtkWidget *width;
GtkWidget *height;
GtkWidget *refresh;
GtkWidget *mode_button;
GtkWidget *rotate_button;
GtkWidget *flipped;
GAction *mode_action;
GAction *rotate_action;
} WdHeadFormPrivate;
enum {
CHANGED,
LAST_SIGNAL
};
static guint signals[LAST_SIGNAL];
G_DEFINE_TYPE_WITH_CODE(WdHeadForm, wd_head_form, GTK_TYPE_GRID,
G_ADD_PRIVATE(WdHeadForm))
static const char *HEAD_PREFIX = "head";
static const char *MODE_PREFIX = "mode";
static const char *ROTATE_PREFIX = "rotate";
static void head_form_update_sensitivity(WdHeadForm *form) {
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
bool enabled_toggled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled));
g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(form));
for (GList *child = children; child != NULL; child = child->next) {
GtkWidget *widget = GTK_WIDGET(child->data);
if (widget != priv->enabled) {
gtk_widget_set_sensitive(widget, enabled_toggled);
}
}
}
static GVariant *create_mode_variant(int32_t w, int32_t h, int32_t r) {
GVariant * const children[] = {
g_variant_new_int32(w),
g_variant_new_int32(h),
g_variant_new_int32(r),
};
return g_variant_new_tuple(children, G_N_ELEMENTS(children));
}
struct vid_mode {
int32_t width;
int32_t height;
int32_t refresh;
};
static void unpack_mode_variant(GVariant *value, struct vid_mode *mode) {
g_autoptr(GVariant) width = g_variant_get_child_value(value, 0);
mode->width = g_variant_get_int32(width);
g_autoptr(GVariant) height = g_variant_get_child_value(value, 1);
mode->height = g_variant_get_int32(height);
g_autoptr(GVariant) refresh = g_variant_get_child_value(value, 2);
mode->refresh = g_variant_get_int32(refresh);
}
static void enabled_toggled(GtkToggleButton *toggle, gpointer data) {
WdHeadForm *form = WD_HEAD_FORM(data);
head_form_update_sensitivity(form);
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_ENABLED);
}
static void mode_spin_changed(GtkSpinButton *spin_button, gpointer data) {
WdHeadForm *form = WD_HEAD_FORM(data);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
struct vid_mode mode;
GVariant *value = g_action_get_state(priv->mode_action);
unpack_mode_variant(value, &mode);
if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "width") == 0) {
mode.width = gtk_spin_button_get_value(spin_button);
} else if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "height") == 0) {
mode.height = gtk_spin_button_get_value(spin_button);
} else if (strcmp(gtk_widget_get_name(GTK_WIDGET(spin_button)), "refresh") == 0) {
mode.refresh = gtk_spin_button_get_value(spin_button) * 1000.;
}
g_action_activate(priv->mode_action, create_mode_variant(mode.width, mode.height, mode.refresh));
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_MODE);
}
static void position_spin_changed(GtkSpinButton *spin_button, gpointer data) {
WdHeadForm *form = WD_HEAD_FORM(data);
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_POSITION);
}
static void flipped_toggled(GtkToggleButton *toggle, gpointer data) {
WdHeadForm *form = WD_HEAD_FORM(data);
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_TRANSFORM);
}
static void wd_head_form_class_init(WdHeadFormClass *class) {
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
signals[CHANGED] = g_signal_new("changed",
G_OBJECT_CLASS_TYPE(class),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET(WdHeadFormClass, changed),
NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_INT);
gtk_widget_class_set_template_from_resource(widget_class,
WDISPLAYS_RESOURCE_PREFIX "/head.ui");
gtk_widget_class_bind_template_callback(widget_class, enabled_toggled);
gtk_widget_class_bind_template_callback(widget_class, mode_spin_changed);
gtk_widget_class_bind_template_callback(widget_class, position_spin_changed);
gtk_widget_class_bind_template_callback(widget_class, flipped_toggled);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, enabled);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, description);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, physical_size);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, scale);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, pos_x);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, pos_y);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, width);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, height);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, refresh);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, mode_button);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, rotate_button);
gtk_widget_class_bind_template_child_private(widget_class, WdHeadForm, flipped);
gtk_widget_class_set_css_name(widget_class, "wd-head-form");
}
static int32_t get_rotate_value(enum wl_output_transform transform) {
if (transform == WL_OUTPUT_TRANSFORM_90 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_90) {
return 90;
} else if (transform == WL_OUTPUT_TRANSFORM_180 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_180) {
return 180;
} else if (transform == WL_OUTPUT_TRANSFORM_270 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_270) {
return 270;
}
return 0;
}
static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) {
WdHeadForm *form = data;
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
GMenuModel *menu = gtk_menu_button_get_menu_model(GTK_MENU_BUTTON(priv->rotate_button));
int items = g_menu_model_get_n_items(menu);
for (int i = 0; i < items; i++) {
g_autoptr(GVariant) target = g_menu_model_get_item_attribute_value(menu, i, G_MENU_ATTRIBUTE_TARGET, NULL);
g_autoptr(GVariant) label = g_menu_model_get_item_attribute_value(menu, i, G_MENU_ATTRIBUTE_LABEL, NULL);
if (g_variant_get_int32(target) == g_variant_get_int32(param)) {
gtk_button_set_label(GTK_BUTTON(priv->rotate_button), g_variant_get_string(label, NULL));
break;
}
}
g_simple_action_set_state(action, param);
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_TRANSFORM);
}
static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) {
WdHeadForm *form = data;
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
struct vid_mode mode;
unpack_mode_variant(param, &mode);
g_simple_action_set_state(action, param);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->width), mode.width);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->height), mode.height);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->refresh), mode.refresh / 1000.);
g_signal_emit(form, signals[CHANGED], 0, WD_FIELD_MODE);
}
static void wd_head_form_init(WdHeadForm *form) {
gtk_widget_init_template(GTK_WIDGET(form));
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
GSimpleActionGroup *head_actions = g_simple_action_group_new();
gtk_widget_insert_action_group(priv->mode_button, HEAD_PREFIX, G_ACTION_GROUP(head_actions));
gtk_widget_insert_action_group(priv->rotate_button, HEAD_PREFIX, G_ACTION_GROUP(head_actions));
GMenu *rotate_menu = g_menu_new();
g_menu_append(rotate_menu, "Don't Rotate", "head.rotate(0)");
g_menu_append(rotate_menu, "Rotate 90°", "head.rotate(90)");
g_menu_append(rotate_menu, "Rotate 180°", "head.rotate(180)");
g_menu_append(rotate_menu, "Rotate 270°", "head.rotate(270)");
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(priv->rotate_button), G_MENU_MODEL(rotate_menu));
static const GVariantType * const mode_types[] = {
G_VARIANT_TYPE_INT32,
G_VARIANT_TYPE_INT32,
G_VARIANT_TYPE_INT32
};
GSimpleAction *action = g_simple_action_new_stateful("mode",
g_variant_type_new_tuple(mode_types, G_N_ELEMENTS(mode_types)),
create_mode_variant(0, 0, 0));
g_action_map_add_action(G_ACTION_MAP(head_actions), G_ACTION(action));
g_signal_connect(action, "change-state", G_CALLBACK(mode_selected), form);
g_object_unref(action);
priv->mode_action = G_ACTION(action);
action = g_simple_action_new_stateful(ROTATE_PREFIX, G_VARIANT_TYPE_INT32,
g_variant_new_int32(0));
g_action_map_add_action(G_ACTION_MAP(head_actions), G_ACTION(action));
g_signal_connect(action, "change-state", G_CALLBACK(rotate_selected), form);
g_object_unref(action);
priv->rotate_action = G_ACTION(action);
g_object_unref(head_actions);
}
void wd_head_form_update(WdHeadForm *form, const struct wd_head *head,
enum wd_head_fields fields) {
g_return_if_fail(form);
g_return_if_fail(head);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
if (!fields)
return;
if (fields & WD_FIELD_DESCRIPTION)
gtk_label_set_text(GTK_LABEL(priv->description), head->description);
if (fields & WD_FIELD_PHYSICAL_SIZE) {
g_autofree gchar *physical_str = g_strdup_printf("%dmm × %dmm", head->phys_width, head->phys_height);
gtk_label_set_text(GTK_LABEL(priv->physical_size), physical_str);
}
if (fields & WD_FIELD_ENABLED)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->enabled), head->enabled);
if (fields & WD_FIELD_SCALE)
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->scale), head->scale);
if (fields & WD_FIELD_POSITION) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_x), head->x);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_y), head->y);
}
if (fields & WD_FIELD_MODE) {
GMenu *mode_menu = g_menu_new();
struct wd_mode *mode;
g_autofree gchar *action = g_strdup_printf("%s.%s", HEAD_PREFIX, MODE_PREFIX);
wl_list_for_each(mode, &head->modes, link) {
g_autofree gchar *name = g_strdup_printf("%d×%d@%0.3fHz", mode->width, mode->height, mode->refresh / 1000.);
GMenuItem *item = g_menu_item_new(name, action);
g_menu_item_set_attribute_value(item, G_MENU_ATTRIBUTE_TARGET,
create_mode_variant(mode->width, mode->height, mode->refresh));
g_menu_append_item(mode_menu, item);
}
gtk_menu_button_set_menu_model(GTK_MENU_BUTTON(priv->mode_button), G_MENU_MODEL(mode_menu));
// Mode entries
int w = head->custom_mode.width;
int h = head->custom_mode.height;
int r = head->custom_mode.refresh;
if (head->enabled && head->mode != NULL) {
w = head->mode->width;
h = head->mode->height;
r = head->mode->refresh;
} else if (!head->enabled && w == 0 && h == 0) {
struct wd_mode *mode;
wl_list_for_each(mode, &head->modes, link) {
if (mode->preferred) {
w = mode->width;
h = mode->height;
r = mode->refresh;
break;
}
}
}
g_action_change_state(priv->mode_action, create_mode_variant(w, h, r));
}
if (fields & WD_FIELD_TRANSFORM) {
int active_rotate = get_rotate_value(head->transform);
g_action_change_state(priv->rotate_action, g_variant_new_int32(active_rotate));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(priv->flipped),
head->transform == WL_OUTPUT_TRANSFORM_FLIPPED
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270);
}
// Sync state
if (fields & WD_FIELD_ENABLED) {
head_form_update_sensitivity(form);
}
g_signal_emit(form, signals[CHANGED], 0);
}
GtkWidget *wd_head_form_new(void) {
return gtk_widget_new(WD_TYPE_HEAD_FORM, NULL);
}
gboolean wd_head_form_get_enabled(WdHeadForm *form) {
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
return gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled));
}
gboolean wd_head_form_has_changes(WdHeadForm *form, const struct wd_head *head) {
g_return_val_if_fail(form, FALSE);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
if (head->enabled != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled))) {
return TRUE;
}
double old_scale = round(head->scale * 100.) / 100.;
double new_scale = round(gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale)) * 100.) / 100.;
if (old_scale != new_scale) {
return TRUE;
}
if (head->x != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x))) {
return TRUE;
}
if (head->y != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y))) {
return TRUE;
}
int w = head->mode != NULL ? head->mode->width : head->custom_mode.width;
if (w != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width))) {
return TRUE;
}
int h = head->mode != NULL ? head->mode->height : head->custom_mode.height;
if (h != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height))) {
return TRUE;
}
int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh;
if (r / 1000. != gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->refresh))) {
return TRUE;
}
if (g_variant_get_int32(g_action_get_state(priv->rotate_action)) != get_rotate_value(head->transform)) {
return TRUE;
}
bool flipped = head->transform == WL_OUTPUT_TRANSFORM_FLIPPED
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_90
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_180
|| head->transform == WL_OUTPUT_TRANSFORM_FLIPPED_270;
if (flipped != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped))) {
return TRUE;
}
return FALSE;
}
void wd_head_form_fill_config(WdHeadForm *form, struct wd_head_config *output) {
g_return_if_fail(form);
g_return_if_fail(output);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
output->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->enabled));
output->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale));
output->x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x));
output->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y));
output->width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width));
output->height = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height));
output->refresh = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->refresh)) * 1000.;
int32_t rotate = g_variant_get_int32(g_action_get_state(priv->rotate_action));
gboolean flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped));
switch (rotate) {
case 0: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED : WL_OUTPUT_TRANSFORM_NORMAL; break;
case 90: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_90 : WL_OUTPUT_TRANSFORM_90; break;
case 180: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_180 : WL_OUTPUT_TRANSFORM_180; break;
case 270: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_270 : WL_OUTPUT_TRANSFORM_270; break;
}
}
void wd_head_form_get_dimensions(WdHeadForm *form, WdHeadDimensions *dimensions) {
g_return_if_fail(form);
g_return_if_fail(dimensions);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
dimensions->x = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_x));
dimensions->y = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->pos_y));
dimensions->w = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->width));
dimensions->h = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->height));
dimensions->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(priv->scale));
dimensions->rotation_id = g_variant_get_int32(g_action_get_state(priv->rotate_action)) / 90;
dimensions->flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(priv->flipped));
}
void wd_head_form_set_position(WdHeadForm *form, double x, double y) {
g_return_if_fail(form);
WdHeadFormPrivate *priv = wd_head_form_get_instance_private(form);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_x), x);
gtk_spin_button_set_value(GTK_SPIN_BUTTON(priv->pos_y), y);
}

59
src/headform.h Normal file
View File

@ -0,0 +1,59 @@
/* SPDX-FileCopyrightText: 2020 Jason Francis <jason@cycles.network>
* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef WDISPLAY_HEADFORM_H
#define WDISPLAY_HEADFORM_H
#include <gtk/gtk.h>
G_BEGIN_DECLS
enum wd_head_fields {
WD_FIELD_NAME = 1 << 0,
WD_FIELD_ENABLED = 1 << 1,
WD_FIELD_DESCRIPTION = 1 << 2,
WD_FIELD_PHYSICAL_SIZE = 1 << 3,
WD_FIELD_SCALE = 1 << 4,
WD_FIELD_POSITION = 1 << 5,
WD_FIELD_MODE = 1 << 6,
WD_FIELD_TRANSFORM = 1 << 7,
WD_FIELDS_ALL = (1 << 8) - 1
};
#define WD_TYPE_HEAD_FORM (wd_head_form_get_type())
G_DECLARE_DERIVABLE_TYPE(
WdHeadForm, wd_head_form, WD, HEAD_FORM, GtkGrid)
struct _WdHeadFormClass {
GtkGridClass parent_class;
void (*changed)(WdHeadForm *form, enum wd_head_fields fields);
};
struct wd_head;
struct wd_head_config;
typedef struct _WdHeadDimensions {
gdouble x;
gdouble y;
gdouble w;
gdouble h;
gdouble scale;
int rotation_id;
gboolean flipped;
} WdHeadDimensions;
GtkWidget *wd_head_form_new(void);
gboolean wd_head_form_get_enabled(WdHeadForm *form);
gboolean wd_head_form_has_changes(WdHeadForm *form, const struct wd_head *head);
void wd_head_form_update(WdHeadForm *form, const struct wd_head *head,
enum wd_head_fields fields);
void wd_head_form_fill_config(WdHeadForm *form, struct wd_head_config *output);
void wd_head_form_get_dimensions(WdHeadForm *form, WdHeadDimensions *dimensions);
void wd_head_form_set_position(WdHeadForm *form, double x, double y);
G_END_DECLS
#endif

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,8 @@
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
m_dep = cc.find_library('m', required : false) m_dep = cc.find_library('m', required : false)
rt_dep = cc.find_library('rt', required : false) rt_dep = cc.find_library('rt', required : false)
gdk = dependency('gdk-3.0') gdk = dependency('gdk-3.0', version: '>= 3.24')
gtk = dependency('gtk+-3.0') gtk = dependency('gtk+-3.0', version: '>= 3.24')
assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present') assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present')
epoxy = dependency('epoxy') epoxy = dependency('epoxy')
@ -15,10 +15,11 @@ executable(
'wdisplays', 'wdisplays',
[ [
'main.c', 'main.c',
'outputs.c',
'render.c',
'glviewport.c', 'glviewport.c',
'headform.c',
'outputs.c',
'overlay.c', 'overlay.c',
'render.c',
resources, resources,
], ],
dependencies : [ dependencies : [

View File

@ -20,6 +20,8 @@
#include <stdbool.h> #include <stdbool.h>
#include <wayland-client.h> #include <wayland-client.h>
#include "headform.h"
struct zxdg_output_v1; struct zxdg_output_v1;
struct zxdg_output_manager_v1; struct zxdg_output_manager_v1;
struct zwlr_output_mode_v1; struct zwlr_output_mode_v1;
@ -39,18 +41,6 @@ typedef struct _GdkCursor GdkCursor;
struct _cairo_surface; struct _cairo_surface;
typedef struct _cairo_surface cairo_surface_t; typedef struct _cairo_surface cairo_surface_t;
enum wd_head_fields {
WD_FIELD_NAME = 1 << 0,
WD_FIELD_ENABLED = 1 << 1,
WD_FIELD_DESCRIPTION = 1 << 2,
WD_FIELD_PHYSICAL_SIZE = 1 << 3,
WD_FIELD_SCALE = 1 << 4,
WD_FIELD_POSITION = 1 << 5,
WD_FIELD_MODE = 1 << 6,
WD_FIELD_TRANSFORM = 1 << 7,
WD_FIELDS_ALL = (1 << 8) - 1
};
struct wd_output { struct wd_output {
struct wd_state *state; struct wd_state *state;
struct zxdg_output_v1 *xdg_output; struct zxdg_output_v1 *xdg_output;
@ -205,10 +195,10 @@ struct wd_state {
unsigned int reset_idle; unsigned int reset_idle;
struct wd_render_head_data *clicked; struct wd_render_head_data *clicked;
/* top left, bottom right */ struct wd_point drag_start;
struct wd_point click_offset; struct wd_point head_drag_start; /* 0-1 range in head rect */
bool panning; bool panning;
struct wd_point pan_last; struct wd_point pan_start;
GtkWidget *main_box; GtkWidget *main_box;
GtkWidget *header_stack; GtkWidget *header_stack;
@ -301,7 +291,7 @@ void wd_ui_reset_heads(struct wd_state *state);
* Updates the UI form for a single head. Useful for when the compositor * Updates the UI form for a single head. Useful for when the compositor
* notifies us of updated configuration caused by another program. * notifies us of updated configuration caused by another program.
*/ */
void wd_ui_reset_head(const struct wd_head *head, unsigned int fields); void wd_ui_reset_head(const struct wd_head *head, enum wd_head_fields fields);
/* /*
* Updates the stack and all forms to the last known server state. * Updates the stack and all forms to the last known server state.