1
0
mirror of https://github.com/artizirk/wdisplays.git synced 2024-11-01 00:30:59 +02:00

initialize repository

This commit is contained in:
Jason Francis 2019-07-05 22:51:52 -04:00
commit b278730ddd
14 changed files with 2502 additions and 0 deletions

20
LICENSE Normal file
View File

@ -0,0 +1,20 @@
Copyright (C) 2019 cyclopsian
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# wdisplay
wdisplay is a graphical application for configuring displays in wlroots
compositors. It borrows some code from [kanshi].
## License
MIT
[kanshi]: https://github.com/emersion/kanshi

5
meson.build Normal file
View File

@ -0,0 +1,5 @@
project('wdisplay', 'c')
subdir('protocol')
subdir('resources')
subdir('src')

38
protocol/meson.build Normal file
View File

@ -0,0 +1,38 @@
wayland_scanner = find_program('wayland-scanner')
wayland_client = dependency('wayland-client')
wayland_scanner_code = generator(
wayland_scanner,
output: '@BASENAME@-protocol.c',
arguments: ['private-code', '@INPUT@', '@OUTPUT@'],
)
wayland_scanner_client = generator(
wayland_scanner,
output: '@BASENAME@-client-protocol.h',
arguments: ['client-header', '@INPUT@', '@OUTPUT@'],
)
client_protocols = [
['wlr-output-management-unstable-v1.xml'],
]
client_protos_src = []
client_protos_headers = []
foreach p : client_protocols
xml = join_paths(p)
client_protos_src += wayland_scanner_code.process(xml)
client_protos_headers += wayland_scanner_client.process(xml)
endforeach
lib_client_protos = static_library(
'client_protos',
client_protos_src + client_protos_headers,
dependencies: [wayland_client]
)
client_protos = declare_dependency(
link_with: lib_client_protos,
sources: client_protos_headers,
)

View File

@ -0,0 +1,483 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_output_management_unstable_v1">
<copyright>
Copyright © 2019 Purism SPC
Permission to use, copy, modify, distribute, and sell this
software and its documentation for any purpose is hereby granted
without fee, provided that the above copyright notice appear in
all copies and that both that copyright notice and this permission
notice appear in supporting documentation, and that the name of
the copyright holders not be used in advertising or publicity
pertaining to distribution of the software without specific,
written prior permission. The copyright holders make no
representations about the suitability of this software for any
purpose. It is provided "as is" without express or implied
warranty.
THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.
</copyright>
<description summary="protocol to configure output devices">
This protocol exposes interfaces to obtain and modify output device
configuration.
Warning! The protocol described in this file is experimental and
backward incompatible changes may be made. Backward compatible changes
may be added together with the corresponding interface version bump.
Backward incompatible changes are done by bumping the version number in
the protocol and interface names and resetting the interface version.
Once the protocol is to be declared stable, the 'z' prefix and the
version number in the protocol and interface names are removed and the
interface version number is reset.
</description>
<interface name="zwlr_output_manager_v1" version="1">
<description summary="output device configuration manager">
This interface is a manager that allows reading and writing the current
output device configuration.
Output devices that display pixels (e.g. a physical monitor or a virtual
output in a window) are represented as heads. Heads cannot be created nor
destroyed by the client, but they can be enabled or disabled and their
properties can be changed. Each head may have one or more available modes.
Whenever a head appears (e.g. a monitor is plugged in), it will be
advertised via the head event. Immediately after the output manager is
bound, all current heads are advertised.
Whenever a head's properties change, the relevant wlr_output_head events
will be sent. Not all head properties will be sent: only properties that
have changed need to.
Whenever a head disappears (e.g. a monitor is unplugged), a
wlr_output_head.finished event will be sent.
After one or more heads appear, change or disappear, the done event will
be sent. It carries a serial which can be used in a create_configuration
request to update heads properties.
The information obtained from this protocol should only be used for output
configuration purposes. This protocol is not designed to be a generic
output property advertisement protocol for regular clients. Instead,
protocols such as xdg-output should be used.
</description>
<event name="head">
<description summary="introduce a new head">
This event introduces a new head. This happens whenever a new head
appears (e.g. a monitor is plugged in) or after the output manager is
bound.
</description>
<arg name="head" type="new_id" interface="zwlr_output_head_v1"/>
</event>
<event name="done">
<description summary="sent all information about current configuration">
This event is sent after all information has been sent after binding to
the output manager object and after any subsequent changes. This applies
to child head and mode objects as well. In other words, this event is
sent whenever a head or mode is created or destroyed and whenever one of
their properties has been changed. Not all state is re-sent each time
the current configuration changes: only the actual changes are sent.
This allows changes to the output configuration to be seen as atomic,
even if they happen via multiple events.
A serial is sent to be used in a future create_configuration request.
</description>
<arg name="serial" type="uint" summary="current configuration serial"/>
</event>
<request name="create_configuration">
<description summary="create a new output configuration object">
Create a new output configuration object. This allows to update head
properties.
</description>
<arg name="id" type="new_id" interface="zwlr_output_configuration_v1"/>
<arg name="serial" type="uint"/>
</request>
<request name="stop">
<description summary="stop sending events">
Indicates the client no longer wishes to receive events for output
configuration changes. However the compositor may emit further events,
until the finished event is emitted.
The client must not send any more requests after this one.
</description>
</request>
<event name="finished">
<description summary="the compositor has finished with the manager">
This event indicates that the compositor is done sending manager events.
The compositor will destroy the object immediately after sending this
event, so it will become invalid and the client should release any
resources associated with it.
</description>
</event>
</interface>
<interface name="zwlr_output_head_v1" version="1">
<description summary="output device">
A head is an output device. The difference between a wl_output object and
a head is that heads are advertised even if they are turned off. A head
object only advertises properties and cannot be used directly to change
them.
A head has some read-only properties: modes, name, description and
physical_size. These cannot be changed by clients.
Other properties can be updated via a wlr_output_configuration object.
Properties sent via this interface are applied atomically via the
wlr_output_manager.done event. No guarantees are made regarding the order
in which properties are sent.
</description>
<event name="name">
<description summary="head name">
This event describes the head name.
The naming convention is compositor defined, but limited to alphanumeric
characters and dashes (-). Each name is unique among all wlr_output_head
objects, but if a wlr_output_head object is destroyed the same name may
be reused later. The names will also remain consistent across sessions
with the same hardware and software configuration.
Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do
not assume that the name is a reflection of an underlying DRM
connector, X11 connection, etc.
If the compositor implements the xdg-output protocol and this head is
enabled, the xdg_output.name event must report the same name.
The name event is sent after a wlr_output_head object is created. This
event is only sent once per object, and the name does not change over
the lifetime of the wlr_output_head object.
</description>
<arg name="name" type="string"/>
</event>
<event name="description">
<description summary="head description">
This event describes a human-readable description of the head.
The description is a UTF-8 string with no convention defined for its
contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11
output via :1'. However, do not assume that the name is a reflection of
the make, model, serial of the underlying DRM connector or the display
name of the underlying X11 connection, etc.
If the compositor implements xdg-output and this head is enabled,
the xdg_output.description must report the same description.
The description event is sent after a wlr_output_head object is created.
This event is only sent once per object, and the description does not
change over the lifetime of the wlr_output_head object.
</description>
<arg name="description" type="string"/>
</event>
<event name="physical_size">
<description summary="head physical size">
This event describes the physical size of the head. This event is only
sent if the head has a physical size (e.g. is not a projector or a
virtual device).
</description>
<arg name="width" type="int" summary="width in millimeters of the output"/>
<arg name="height" type="int" summary="height in millimeters of the output"/>
</event>
<event name="mode">
<description summary="introduce a mode">
This event introduces a mode for this head. It is sent once per
supported mode.
</description>
<arg name="mode" type="new_id" interface="zwlr_output_mode_v1"/>
</event>
<event name="enabled">
<description summary="head is enabled or disabled">
This event describes whether the head is enabled. A disabled head is not
mapped to a region of the global compositor space.
When a head is disabled, some properties (current_mode, position,
transform and scale) are irrelevant.
</description>
<arg name="enabled" type="int" summary="zero if disabled, non-zero if enabled"/>
</event>
<event name="current_mode">
<description summary="current mode">
This event describes the mode currently in use for this head. It is only
sent if the output is enabled.
</description>
<arg name="mode" type="object" interface="zwlr_output_mode_v1"/>
</event>
<event name="position">
<description summary="current position">
This events describes the position of the head in the global compositor
space. It is only sent if the output is enabled.
</description>
<arg name="x" type="int"
summary="x position within the global compositor space"/>
<arg name="y" type="int"
summary="y position within the global compositor space"/>
</event>
<event name="transform">
<description summary="current transformation">
This event describes the transformation currently applied to the head.
It is only sent if the output is enabled.
</description>
<arg name="transform" type="int" enum="wl_output.transform"/>
</event>
<event name="scale">
<description summary="current scale">
This events describes the scale of the head in the global compositor
space. It is only sent if the output is enabled.
</description>
<arg name="scale" type="fixed"/>
</event>
<event name="finished">
<description summary="the head has been destroyed">
The compositor will destroy the object immediately after sending this
event, so it will become invalid and the client should release any
resources associated with it.
</description>
</event>
</interface>
<interface name="zwlr_output_mode_v1" version="1">
<description summary="output mode">
This object describes an output mode.
Some heads don't support output modes, in which case modes won't be
advertised.
Properties sent via this interface are applied atomically via the
wlr_output_manager.done event. No guarantees are made regarding the order
in which properties are sent.
</description>
<event name="size">
<description summary="mode size">
This event describes the mode size. The size is given in physical
hardware units of the output device. This is not necessarily the same as
the output size in the global compositor space. For instance, the output
may be scaled or transformed.
</description>
<arg name="width" type="int" summary="width of the mode in hardware units"/>
<arg name="height" type="int" summary="height of the mode in hardware units"/>
</event>
<event name="refresh">
<description summary="mode refresh rate">
This event describes the mode's fixed vertical refresh rate. It is only
sent if the mode has a fixed refresh rate.
</description>
<arg name="refresh" type="int" summary="vertical refresh rate in mHz"/>
</event>
<event name="preferred">
<description summary="mode is preferred">
This event advertises this mode as preferred.
</description>
</event>
<event name="finished">
<description summary="the mode has been destroyed">
The compositor will destroy the object immediately after sending this
event, so it will become invalid and the client should release any
resources associated with it.
</description>
</event>
</interface>
<interface name="zwlr_output_configuration_v1" version="1">
<description summary="output configuration">
This object is used by the client to describe a full output configuration.
First, the client needs to setup the output configuration. Each head can
be either enabled (and configured) or disabled. It is a protocol error to
send two enable_head or disable_head requests with the same head. It is a
protocol error to omit a head in a configuration.
Then, the client can apply or test the configuration. The compositor will
then reply with a succeeded, failed or cancelled event. Finally the client
should destroy the configuration object.
</description>
<enum name="error">
<entry name="already_configured_head" value="1"
summary="head has been configured twice"/>
<entry name="unconfigured_head" value="2"
summary="head has not been configured"/>
<entry name="already_used" value="3"
summary="request sent after configuration has been applied or tested"/>
</enum>
<request name="enable_head">
<description summary="enable and configure a head">
Enable a head. This request creates a head configuration object that can
be used to change the head's properties.
</description>
<arg name="id" type="new_id" interface="zwlr_output_configuration_head_v1"
summary="a new object to configure the head"/>
<arg name="head" type="object" interface="zwlr_output_head_v1"
summary="the head to be enabled"/>
</request>
<request name="disable_head">
<description summary="disable a head">
Disable a head.
</description>
<arg name="head" type="object" interface="zwlr_output_head_v1"
summary="the head to be disabled"/>
</request>
<request name="apply">
<description summary="apply the configuration">
Apply the new output configuration.
In case the configuration is successfully applied, there is no guarantee
that the new output state matches completely the requested
configuration. For instance, a compositor might round the scale if it
doesn't support fractional scaling.
After this request has been sent, the compositor must respond with an
succeeded, failed or cancelled event. Sending a request that isn't the
destructor is a protocol error.
</description>
</request>
<request name="test">
<description summary="test the configuration">
Test the new output configuration. The configuration won't be applied,
but will only be validated.
Even if the compositor succeeds to test a configuration, applying it may
fail.
After this request has been sent, the compositor must respond with an
succeeded, failed or cancelled event. Sending a request that isn't the
destructor is a protocol error.
</description>
</request>
<event name="succeeded">
<description summary="configuration changes succeeded">
Sent after the compositor has successfully applied the changes or
tested them.
Upon receiving this event, the client should destroy this object.
If the current configuration has changed, events to describe the changes
will be sent followed by a wlr_output_manager.done event.
</description>
</event>
<event name="failed">
<description summary="configuration changes failed">
Sent if the compositor rejects the changes or failed to apply them. The
compositor should revert any changes made by the apply request that
triggered this event.
Upon receiving this event, the client should destroy this object.
</description>
</event>
<event name="cancelled">
<description summary="configuration has been cancelled">
Sent if the compositor cancels the configuration because the state of an
output changed and the client has outdated information (e.g. after an
output has been hotplugged).
The client can create a new configuration with a newer serial and try
again.
Upon receiving this event, the client should destroy this object.
</description>
</event>
<request name="destroy" type="destructor">
<description summary="destroy the output configuration">
Using this request a client can tell the compositor that it is not going
to use the configuration object anymore. Any changes to the outputs
that have not been applied will be discarded.
This request also destroys wlr_output_configuration_head objects created
via this object.
</description>
</request>
</interface>
<interface name="zwlr_output_configuration_head_v1" version="1">
<description summary="head configuration">
This object is used by the client to update a single head's configuration.
It is a protocol error to set the same property twice.
</description>
<enum name="error">
<entry name="already_set" value="1" summary="property has already been set"/>
<entry name="invalid_mode" value="2" summary="mode doesn't belong to head"/>
<entry name="invalid_custom_mode" value="3" summary="mode is invalid"/>
<entry name="invalid_transform" value="4" summary="transform value outside enum"/>
<entry name="invalid_scale" value="5" summary="scale negative or zero"/>
</enum>
<request name="set_mode">
<description summary="set the mode">
This request sets the head's mode.
</description>
<arg name="mode" type="object" interface="zwlr_output_mode_v1"/>
</request>
<request name="set_custom_mode">
<description summary="set a custom mode">
This request assigns a custom mode to the head. The size is given in
physical hardware units of the output device. If set to zero, the
refresh rate is unspecified.
It is a protocol error to set both a mode and a custom mode.
</description>
<arg name="width" type="int" summary="width of the mode in hardware units"/>
<arg name="height" type="int" summary="height of the mode in hardware units"/>
<arg name="refresh" type="int" summary="vertical refresh rate in mHz or zero"/>
</request>
<request name="set_position">
<description summary="set the position">
This request sets the head's position in the global compositor space.
</description>
<arg name="x" type="int" summary="x position in the global compositor space"/>
<arg name="y" type="int" summary="y position in the global compositor space"/>
</request>
<request name="set_transform">
<description summary="set the transform">
This request sets the head's transform.
</description>
<arg name="transform" type="int" enum="wl_output.transform"/>
</request>
<request name="set_scale">
<description summary="set the scale">
This request sets the head's scale.
</description>
<arg name="scale" type="fixed"/>
</request>
</interface>
</protocol>

464
resources/head.ui Normal file
View File

@ -0,0 +1,464 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkAdjustment" id="scal">
<property name="upper">99999999999999</property>
<property name="step_increment">0.1</property>
<property name="page_increment">0.5</property>
</object>
<object class="GtkGrid" id="form">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_start">8</property>
<property name="margin_end">8</property>
<property name="margin_top">8</property>
<property name="margin_bottom">8</property>
<property name="row_spacing">8</property>
<property name="column_spacing">16</property>
<property name="row_homogeneous">True</property>
<child>
<object class="GtkCheckButton" id="enabled">
<property name="label" translatable="yes">_Enabled</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="enabled" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="description">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkSpinButton" id="scale">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="halign">start</property>
<property name="width_chars">6</property>
<property name="adjustment">scal</property>
<property name="digits">2</property>
<property name="value">1</property>
<signal name="change-value" handler="scale" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Scale</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">scale</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">3</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Si_ze</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">width</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Position</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">pos_x</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Refresh</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">refresh</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="spacing">8</property>
<child>
<object class="GtkEntry" id="refresh">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">10</property>
<property name="input_purpose">number</property>
<signal name="changed" handler="refresh" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</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="label" translatable="yes">Hz</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<child>
<object class="GtkEntry" id="pos_x">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">6</property>
<property name="input_purpose">number</property>
<signal name="changed" handler="pos_x" swapped="no"/>
</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="width_request">20</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">,</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="pos_y">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">6</property>
<property name="input_purpose">number</property>
<signal name="changed" handler="pos_y" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">4</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<child>
<object class="GtkEntry" id="width">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">6</property>
<property name="input_purpose">number</property>
<signal name="changed" handler="width" swapped="no"/>
</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="width_request">20</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">×</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="height">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="width_chars">6</property>
<property name="input_purpose">number</property>
<signal name="changed" handler="height" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="mode_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Select Mode Preset</property>
<property name="margin_start">8</property>
<property name="popover">modes</property>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">view-more-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">3</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">5</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Description</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="physical_size">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="wrap">True</property>
<property name="wrap_mode">word-char</property>
<property name="ellipsize">end</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Physical Size</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">_Transform</property>
<property name="use_underline">True</property>
<property name="xalign">1</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkMenuButton" id="rotate_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="popover">transforms</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">7</property>
</packing>
</child>
<child>
<object class="GtkCheckButton" id="flipped">
<property name="label" translatable="yes">_Flipped</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="halign">start</property>
<property name="use_underline">True</property>
<property name="draw_indicator">True</property>
<signal name="toggled" handler="flipped" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">8</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
</object>
<object class="GtkPopover" id="modes">
<property name="can_focus">False</property>
<property name="relative_to">mode_button</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="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>

7
resources/meson.build Normal file
View File

@ -0,0 +1,7 @@
gnome = import('gnome')
resources = gnome.compile_resources(
'waydisplay-resources', 'resources.xml',
source_dir : '.',
c_name : 'waydisplay_resources')

8
resources/resources.xml Normal file
View File

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

9
resources/style.css Normal file
View File

@ -0,0 +1,9 @@
spinner {
opacity: 0;
transition: opacity 200ms ease-in-out;
background-color: rgba(64, 64, 64, 0.5);
}
spinner.visible {
opacity: 1;
}

287
resources/waydisplay.ui Normal file
View File

@ -0,0 +1,287 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.0 -->
<interface>
<requires lib="gtk+" version="3.22"/>
<object class="GtkWindow" id="heads_window">
<property name="can_focus">False</property>
<property name="title" translatable="yes">Waydisplay</property>
<child>
<object class="GtkOverlay" id="overlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkPaned">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="position">400</property>
<property name="position_set">True</property>
<child>
<object class="GtkScrolledWindow" id="heads_scroll">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<object class="GtkLayout" id="heads_layout">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="width">400</property>
<signal name="draw" handler="heads_draw" swapped="no"/>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">False</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkStackSwitcher" id="heads_stack_switcher">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">center</property>
<property name="margin_start">8</property>
<property name="margin_end">8</property>
<property name="margin_top">8</property>
<property name="hexpand">True</property>
<property name="stack">heads_stack</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkStack" id="heads_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="resize">False</property>
<property name="shrink">False</property>
</packing>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
<child type="overlay">
<object class="GtkSpinner" id="spinner">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="active">True</property>
</object>
<packing>
<property name="pass_through">True</property>
</packing>
</child>
<child type="overlay">
<object class="GtkInfoBar" id="heads_info">
<property name="can_focus">False</property>
<property name="no_show_all">True</property>
<property name="valign">start</property>
<property name="message_type">error</property>
<property name="show_close_button">True</property>
<property name="revealed">False</property>
<signal name="response" handler="info_response" swapped="no"/>
<child internal-child="action_area">
<object class="GtkButtonBox">
<property name="can_focus">False</property>
<property name="spacing">6</property>
<property name="layout_style">end</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child internal-child="content_area">
<object class="GtkBox">
<property name="can_focus">False</property>
<property name="spacing">16</property>
<child>
<object class="GtkLabel" id="heads_info_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="wrap">True</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="index">1</property>
</packing>
</child>
</object>
</child>
<child type="titlebar">
<object class="GtkStack" id="header_stack">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="transition_type">crossfade</property>
<child>
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="title" translatable="yes">Waydisplay</property>
<property name="has_subtitle">False</property>
<property name="show_close_button">True</property>
<child>
<object class="GtkButtonBox" id="zoom_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="layout_style">expand</property>
<child>
<object class="GtkButton" id="zoom_out">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom Out</property>
<signal name="clicked" handler="zoom_out" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">zoom-out-symbolic</property>
</object>
</child>
<accelerator key="minus" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="zoom_reset">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom Reset</property>
<signal name="clicked" handler="zoom_reset" swapped="no"/>
<accelerator key="0" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="zoom_in">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="tooltip_text" translatable="yes">Zoom In</property>
<signal name="clicked" handler="zoom_in" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">zoom-in-symbolic</property>
</object>
</child>
<accelerator key="equal" signal="clicked" modifiers="GDK_CONTROL_MASK"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">2</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
</object>
</child>
</object>
<packing>
<property name="name">title</property>
</packing>
</child>
<child>
<object class="GtkHeaderBar">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child type="title">
<object class="GtkLabel">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Apply Changes?</property>
</object>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">_Apply</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="apply_changes" swapped="no"/>
<style>
<class name="suggested-action"/>
</style>
</object>
<packing>
<property name="pack_type">end</property>
</packing>
</child>
<child>
<object class="GtkButton">
<property name="label" translatable="yes">_Cancel</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_underline">True</property>
<signal name="clicked" handler="cancel_changes" swapped="no"/>
</object>
<packing>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="name">apply</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

635
src/main.c Normal file
View File

@ -0,0 +1,635 @@
/*
* Copyright (C) 2019 cyclopsian
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include <gtk/gtk.h>
#include <gdk/gdkwayland.h>
#include "wdisplay.h"
__attribute__((noreturn)) void wd_fatal_error(int status, const char *message) {
GtkWindow *parent = gtk_application_get_active_window(GTK_APPLICATION(g_application_get_default()));
GtkWidget *dialog = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "%s", message);
gtk_dialog_run(GTK_DIALOG(dialog));
gtk_widget_destroy(dialog);
exit(status);
}
#define DEFAULT_ZOOM 0.1
#define MIN_ZOOM (1./1000.)
#define MAX_ZOOM 1000.
#define CANVAS_MARGIN 100
static const char *MODE_PREFIX = "mode";
static const char *TRANSFORM_PREFIX = "transform";
#define NUM_ROTATIONS 4
static const char *ROTATE_IDS[NUM_ROTATIONS] = {
"rotate_0", "rotate_90", "rotate_180", "rotate_270"
};
static int get_rotate_index(enum wl_output_transform transform) {
if (transform == WL_OUTPUT_TRANSFORM_90 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_90) {
return 1;
} else if (transform == WL_OUTPUT_TRANSFORM_180 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_180) {
return 2;
} else if (transform == WL_OUTPUT_TRANSFORM_270 || transform == WL_OUTPUT_TRANSFORM_FLIPPED_270) {
return 3;
}
return 0;
}
static bool has_changes(const struct wd_state *state) {
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack));
for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder"));
const struct wd_head *head = g_object_get_data(G_OBJECT(form_iter->data), "head");
if (head->enabled != gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled")))) {
return TRUE;
}
if (head->scale != gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale")))) {
return TRUE;
}
if (head->x != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x"))))) {
return TRUE;
}
if (head->y != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y"))))) {
return TRUE;
}
int w = head->mode != NULL ? head->mode->width : head->custom_mode.width;
if (w != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width"))))) {
return TRUE;
}
int h = head->mode != NULL ? head->mode->height : head->custom_mode.height;
if (h != atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height"))))) {
return TRUE;
}
int r = head->mode != NULL ? head->mode->refresh : head->custom_mode.refresh;
if (r / 1000. != atof(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "refresh"))))) {
return TRUE;
}
for (int i = 0; i < NUM_ROTATIONS; i++) {
GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i]));
gboolean selected;
g_object_get(rotate, "active", &selected, NULL);
if (selected) {
if (i != get_rotate_index(head->transform)) {
return TRUE;
}
break;
}
}
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(gtk_builder_get_object(builder, "flipped")))) {
return TRUE;
}
}
return FALSE;
}
// BEGIN FORM CALLBACKS
static void show_apply(struct wd_state *state) {
bool changed = has_changes(state);
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), changed ? "apply" : "title");
gtk_widget_queue_draw(state->canvas);
}
static void update_sensitivity(GtkWidget *form) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled"));
bool enabled_toggled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(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 != enabled) {
gtk_widget_set_sensitive(widget, enabled_toggled);
}
}
}
static void select_rotate_option(GtkWidget *form, GtkWidget *model_button) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button"));
for (int i = 0; i < NUM_ROTATIONS; i++) {
GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i]));
gboolean selected = model_button == rotate;
g_object_set(rotate, "active", selected, NULL);
if (selected) {
g_autofree gchar *rotate_text = NULL;
g_object_get(rotate, "text", &rotate_text, NULL);
gtk_button_set_label(GTK_BUTTON(rotate_button), rotate_text);
}
}
}
static void rotate_selected(GSimpleAction *action, GVariant *param, gpointer data) {
select_rotate_option(GTK_WIDGET(data), g_object_get_data(G_OBJECT(action), "widget"));
const struct wd_head *head = g_object_get_data(G_OBJECT(data), "head");
show_apply(head->state);
}
static void select_mode_option(GtkWidget *form, int32_t w, int32_t h, int32_t r) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box"));
g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(mode_box));
for (GList *child = children; child != NULL; child = child->next) {
const struct wd_mode *mode = g_object_get_data(G_OBJECT(child->data), "mode");
g_object_set(child->data, "active", w == mode->width && h == mode->height && r == mode->refresh, NULL);
}
}
static void update_mode_entries(GtkWidget *form, int32_t w, int32_t h, int32_t r) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
GtkWidget *width = GTK_WIDGET(gtk_builder_get_object(builder, "width"));
GtkWidget *height = GTK_WIDGET(gtk_builder_get_object(builder, "height"));
GtkWidget *refresh = GTK_WIDGET(gtk_builder_get_object(builder, "refresh"));
g_autofree gchar *widthstr = g_strdup_printf("%d", w);
gtk_entry_set_text(GTK_ENTRY(width), widthstr);
g_autofree gchar *heightstr = g_strdup_printf("%d", h);
gtk_entry_set_text(GTK_ENTRY(height), heightstr);
g_autofree gchar *refreshstr = g_strdup_printf("%0.3f", r / 1000.0);
gtk_entry_set_text(GTK_ENTRY(refresh), refreshstr);
}
static void mode_selected(GSimpleAction *action, GVariant *param, gpointer data) {
GtkWidget *form = data;
const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head");
const struct wd_mode *mode = g_object_get_data(G_OBJECT(action), "mode");
update_mode_entries(form, mode->width, mode->height, mode->refresh);
select_mode_option(form, mode->width, mode->height, mode->refresh);
show_apply(head->state);
}
// END FORM CALLBACKS
/*
* Recalculates the desired canvas size, accounting for zoom + margins.
*/
static void update_canvas_size(struct wd_state *state) {
int xmin = 0;
int xmax = 0;
int ymin = 0;
int ymax = 0;
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
int w = head->custom_mode.width;
int h = head->custom_mode.height;
if (head->enabled && head->mode != NULL) {
w = head->mode->width;
h = head->mode->height;
}
int x2 = head->x + w;
int y2 = head->y + h;
xmin = MIN(xmin, head->x);
xmax = MAX(xmax, x2);
ymin = MIN(ymin, head->y);
ymax = MAX(ymax, y2);
}
// update canvas sizings
state->xorigin = floor(xmin * state->zoom) - CANVAS_MARGIN;
state->yorigin = floor(ymin * state->zoom) - CANVAS_MARGIN;
int heads_width = ceil((xmax - xmin) * state->zoom) + CANVAS_MARGIN * 2;
int heads_height = ceil((ymax - ymin) * state->zoom) + CANVAS_MARGIN * 2;
gtk_layout_set_size(GTK_LAYOUT(state->canvas), heads_width, heads_height);
}
static void clear_menu(GtkWidget *box, GActionMap *action_map) {
g_autoptr(GList) children = gtk_container_get_children(GTK_CONTAINER(box));
for (GList *child = children; child != NULL; child = child->next) {
g_action_map_remove_action(action_map, strchr(gtk_actionable_get_action_name(GTK_ACTIONABLE(child->data)), '.') + 1);
gtk_container_remove(GTK_CONTAINER(box), GTK_WIDGET(child->data));
}
}
static void update_head_form(GtkWidget *form, unsigned int fields) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
GtkWidget *description = GTK_WIDGET(gtk_builder_get_object(builder, "description"));
GtkWidget *physical_size = GTK_WIDGET(gtk_builder_get_object(builder, "physical_size"));
GtkWidget *enabled = GTK_WIDGET(gtk_builder_get_object(builder, "enabled"));
GtkWidget *scale = GTK_WIDGET(gtk_builder_get_object(builder, "scale"));
GtkWidget *pos_x = GTK_WIDGET(gtk_builder_get_object(builder, "pos_x"));
GtkWidget *pos_y = GTK_WIDGET(gtk_builder_get_object(builder, "pos_y"));
GtkWidget *mode_box = GTK_WIDGET(gtk_builder_get_object(builder, "mode_box"));
GtkWidget *flipped = GTK_WIDGET(gtk_builder_get_object(builder, "flipped"));
const struct wd_head *head = g_object_get_data(G_OBJECT(form), "head");
if (fields & WD_FIELD_NAME) {
gtk_container_child_set(GTK_CONTAINER(head->state->stack), form, "name", head->name, "title", head->name, NULL);
}
if (fields & WD_FIELD_DESCRIPTION) {
gtk_label_set_text(GTK_LABEL(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(physical_size), physical_str);
}
if (fields & WD_FIELD_ENABLED) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(enabled), head->enabled);
}
if (fields & WD_FIELD_SCALE) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(scale), head->scale);
}
if (fields & WD_FIELD_POSITION) {
g_autofree gchar *xstr = g_strdup_printf("%d", head->x);
gtk_entry_set_text(GTK_ENTRY(pos_x), xstr);
g_autofree gchar *ystr = g_strdup_printf("%d", head->y);
gtk_entry_set_text(GTK_ENTRY(pos_y), ystr);
}
if (fields & WD_FIELD_MODE) {
GActionMap *mode_actions = G_ACTION_MAP(g_object_get_data(G_OBJECT(form), "mode-group"));
clear_menu(mode_box, mode_actions);
struct wd_mode *mode;
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.);
GSimpleAction *action = g_simple_action_new(name, NULL);
g_action_map_add_action(G_ACTION_MAP(mode_actions), G_ACTION(action));
g_signal_connect(action, "activate", G_CALLBACK(mode_selected), form);
g_object_set_data(G_OBJECT(action), "mode", mode);
g_object_unref(action);
GtkWidget *button = gtk_model_button_new();
g_autoptr(GString) prefixed_name = g_string_new(MODE_PREFIX);
g_string_append(prefixed_name, ".");
g_string_append(prefixed_name, name);
gtk_actionable_set_action_name(GTK_ACTIONABLE(button), prefixed_name->str);
g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, "text", name, NULL);
gtk_box_pack_start(GTK_BOX(mode_box), button, FALSE, FALSE, 0);
g_object_set_data(G_OBJECT(button), "mode", mode);
gtk_widget_show_all(button);
}
// 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;
}
update_mode_entries(form, w, h, r);
select_mode_option(form, w, h, r);
gtk_widget_show_all(mode_box);
}
if (fields & WD_FIELD_TRANSFORM) {
int active_rotate = get_rotate_index(head->transform);
select_rotate_option(form, GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[active_rotate])));
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(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) {
update_sensitivity(form);
}
show_apply(head->state);
gtk_widget_queue_draw(head->state->canvas);
}
void wd_ui_reset_heads(struct wd_state *state) {
if (state->stack == NULL) {
return;
}
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack));
GList *form_iter = forms;
struct wd_head *head;
wl_list_for_each(head, &state->heads, link) {
GtkBuilder *builder;
GtkWidget *form;
if (form_iter == NULL) {
builder = gtk_builder_new_from_resource("/head.ui");
form = GTK_WIDGET(gtk_builder_get_object(builder, "form"));
g_object_set_data(G_OBJECT(form), "builder", builder);
g_object_set_data(G_OBJECT(form), "head", head);
gtk_stack_add_titled(GTK_STACK(state->stack), form, head->name, head->name);
GtkWidget *mode_button = GTK_WIDGET(gtk_builder_get_object(builder, "mode_button"));
GtkWidget *rotate_button = GTK_WIDGET(gtk_builder_get_object(builder, "rotate_button"));
GSimpleActionGroup *mode_actions = g_simple_action_group_new();
gtk_widget_insert_action_group(mode_button, MODE_PREFIX, G_ACTION_GROUP(mode_actions));
g_object_set_data(G_OBJECT(form), "mode-group", mode_actions);
g_object_unref(mode_actions);
GSimpleActionGroup *transform_actions = g_simple_action_group_new();
gtk_widget_insert_action_group(rotate_button, TRANSFORM_PREFIX, G_ACTION_GROUP(transform_actions));
g_object_unref(transform_actions);
for (int i = 0; i < NUM_ROTATIONS; i++) {
GtkWidget *button = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i]));
g_object_set(button, "role", GTK_BUTTON_ROLE_RADIO, NULL);
GSimpleAction *action = g_simple_action_new(ROTATE_IDS[i], NULL);
g_action_map_add_action(G_ACTION_MAP(transform_actions), G_ACTION(action));
g_signal_connect(action, "activate", G_CALLBACK(rotate_selected), form);
g_object_set_data(G_OBJECT(action), "widget", button);
g_object_unref(action);
}
update_head_form(form, WD_FIELDS_ALL);
gtk_widget_show_all(form);
g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(update_sensitivity), form);
g_signal_connect_swapped(gtk_builder_get_object(builder, "enabled"), "toggled", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "scale"), "value-changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_x"), "changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "pos_y"), "changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "width"), "changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "height"), "changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "refresh"), "changed", G_CALLBACK(show_apply), state);
g_signal_connect_swapped(gtk_builder_get_object(builder, "flipped"), "toggled", G_CALLBACK(show_apply), state);
} else {
form = form_iter->data;
g_object_set_data(G_OBJECT(form), "head", head);
form_iter = form_iter->next;
}
}
// remove everything else
for (; form_iter != NULL; form_iter = form_iter->next) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder"));
g_object_unref(builder);
gtk_container_remove(GTK_CONTAINER(state->stack), GTK_WIDGET(form_iter->data));
}
gtk_widget_queue_draw(state->canvas);
}
/*
* Updates the UI form for a single head. Useful for when the compositor notifies us of
* updated configuration caused by another program.
*/
void wd_ui_reset_head(const struct wd_head *head, unsigned int fields) {
if (head->state->stack == NULL) {
return;
}
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(head->state->stack));
for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) {
const struct wd_head *other = g_object_get_data(G_OBJECT(form_iter->data), "head");
if (head == other) {
update_head_form(GTK_WIDGET(form_iter->data), fields);
}
}
}
void wd_ui_reset_all(struct wd_state *state) {
wd_ui_reset_heads(state);
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack));
for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) {
update_head_form(GTK_WIDGET(form_iter->data), WD_FIELDS_ALL);
}
}
void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs) {
gtk_style_context_remove_class(gtk_widget_get_style_context(state->spinner), "visible");
gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, TRUE);
gtk_widget_set_sensitive(state->stack_switcher, TRUE);
gtk_widget_set_sensitive(state->stack, TRUE);
gtk_widget_set_sensitive(state->zoom_in, TRUE);
gtk_widget_set_sensitive(state->zoom_reset, TRUE);
gtk_widget_set_sensitive(state->zoom_out, TRUE);
show_apply(state);
}
void wd_ui_show_error(struct wd_state *state, const char *message) {
gtk_label_set_text(GTK_LABEL(state->info_label), message);
gtk_widget_show(state->info_bar);
gtk_info_bar_set_revealed(GTK_INFO_BAR(state->info_bar), TRUE);
}
void fill_output_from_form(struct wd_head_config *output, GtkWidget *form) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form), "builder"));
output->head = g_object_get_data(G_OBJECT(form), "head");
output->enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled")));
output->scale = gtk_spin_button_get_value(GTK_SPIN_BUTTON(gtk_builder_get_object(builder, "scale")));
output->x = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x"))));
output->y = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y"))));
output->width = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width"))));
output->height = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height"))));
output->refresh = atof(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "refresh")))) * 1000.;
gboolean flipped = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "flipped")));
for (int i = 0; i < NUM_ROTATIONS; i++) {
GtkWidget *rotate = GTK_WIDGET(gtk_builder_get_object(builder, ROTATE_IDS[i]));
gboolean selected;
g_object_get(rotate, "active", &selected, NULL);
if (selected) {
switch (i) {
case 0: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED : WL_OUTPUT_TRANSFORM_NORMAL; break;
case 1: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_90 : WL_OUTPUT_TRANSFORM_90; break;
case 2: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_180 : WL_OUTPUT_TRANSFORM_180; break;
case 3: output->transform = flipped ? WL_OUTPUT_TRANSFORM_FLIPPED_270 : WL_OUTPUT_TRANSFORM_270; break;
}
break;
}
}
}
// BEGIN GLOBAL CALLBACKS
static void cleanup(GtkWidget *window, gpointer state) {
g_free(state);
}
gboolean draw(GtkWidget *widget, cairo_t *cr, gpointer data) {
struct wd_state *state = data;
update_canvas_size(state);
GtkStyleContext *style_ctx = gtk_widget_get_style_context(widget);
GtkAdjustment *scroll_x_adj = gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(state->scroller));
GtkAdjustment *scroll_y_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(state->scroller));
double scroll_x = gtk_adjustment_get_value(scroll_x_adj);
double scroll_y = gtk_adjustment_get_value(scroll_y_adj);
int width = gtk_widget_get_allocated_width(widget);
int height = gtk_widget_get_allocated_height(widget);
GdkRGBA border;
gtk_style_context_lookup_color(style_ctx, "borders", &border);
gdk_cairo_set_source_rgba(cr, &border);
cairo_set_line_width(cr, .5);
gtk_render_background(style_ctx, cr, 0, 0, width, height);
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack));
for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) {
GtkBuilder *builder = GTK_BUILDER(g_object_get_data(G_OBJECT(form_iter->data), "builder"));
gboolean enabled = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "enabled")));
if (enabled) {
int x = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_x"))));
int y = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "pos_y"))));
int w = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "width"))));
int h = atoi(gtk_entry_get_text(GTK_ENTRY(gtk_builder_get_object(builder, "height"))));
cairo_rectangle(cr,
x * state->zoom + .5 - scroll_x - state->xorigin,
y * state->zoom + .5 - scroll_y - state->yorigin,
w * state->zoom,
h * state->zoom);
cairo_stroke(cr);
}
}
return TRUE;
}
static void cancel_changes(GtkButton *button, gpointer data) {
struct wd_state *state = data;
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title");
wd_ui_reset_all(state);
}
static void apply_changes(GtkButton *button, gpointer data) {
struct wd_state *state = data;
gtk_stack_set_visible_child_name(GTK_STACK(state->header_stack), "title");
gtk_style_context_add_class(gtk_widget_get_style_context(state->spinner), "visible");
gtk_overlay_set_overlay_pass_through(GTK_OVERLAY(state->overlay), state->spinner, FALSE);
gtk_widget_set_sensitive(state->stack_switcher, FALSE);
gtk_widget_set_sensitive(state->stack, FALSE);
gtk_widget_set_sensitive(state->zoom_in, FALSE);
gtk_widget_set_sensitive(state->zoom_reset, FALSE);
gtk_widget_set_sensitive(state->zoom_out, FALSE);
struct wl_list *outputs = calloc(1, sizeof(*outputs));
wl_list_init(outputs);
g_autoptr(GList) forms = gtk_container_get_children(GTK_CONTAINER(state->stack));
for (GList *form_iter = forms; form_iter != NULL; form_iter = form_iter->next) {
struct wd_head_config *output = calloc(1, sizeof(*output));
wl_list_insert(outputs, &output->link);
fill_output_from_form(output, GTK_WIDGET(form_iter->data));
}
wd_apply_state(state, outputs);
}
static void update_zoom(struct wd_state *state) {
g_autofree gchar *zoom_percent = g_strdup_printf("%.f%%", state->zoom * 100.);
gtk_button_set_label(GTK_BUTTON(state->zoom_reset), zoom_percent);
gtk_widget_set_sensitive(state->zoom_in, state->zoom < MAX_ZOOM);
gtk_widget_set_sensitive(state->zoom_out, state->zoom > MIN_ZOOM);
gtk_widget_queue_draw(state->canvas);
}
static void zoom_out(GtkButton *button, gpointer data) {
struct wd_state *state = data;
state->zoom *= 0.75;
state->zoom = MAX(state->zoom, MIN_ZOOM);
update_zoom(state);
}
static void zoom_reset(GtkButton *button, gpointer data) {
struct wd_state *state = data;
state->zoom = DEFAULT_ZOOM;
update_zoom(state);
}
static void zoom_in(GtkButton *button, gpointer data) {
struct wd_state *state = data;
state->zoom /= 0.75;
state->zoom = MIN(state->zoom, MAX_ZOOM);
update_zoom(state);
}
static void info_response(GtkInfoBar *info_bar, gint response_id, gpointer data) {
gtk_info_bar_set_revealed(info_bar, FALSE);
}
static void info_bar_animation_done(GObject *object, GParamSpec *pspec, gpointer data) {
gboolean done = gtk_revealer_get_child_revealed(GTK_REVEALER(object));
if (!done) {
struct wd_state *state = data;
gtk_widget_set_visible(state->info_bar, gtk_revealer_get_reveal_child(GTK_REVEALER(object)));
}
}
static void activate(GtkApplication* app, gpointer user_data) {
GdkDisplay *gdk_display = gdk_display_get_default();
if (!GDK_IS_WAYLAND_DISPLAY(gdk_display)) {
wd_fatal_error(1, "This program is only usable on Wayland sessions.");
}
struct wd_state *state = g_new0(struct wd_state, 1);
state->zoom = DEFAULT_ZOOM;
wl_list_init(&state->heads);
GtkCssProvider *css_provider = gtk_css_provider_new();
gtk_css_provider_load_from_resource(css_provider, "/style.css");
gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
GtkBuilder *builder = gtk_builder_new_from_resource("/wd.ui");
GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "heads_window"));
state->header_stack = GTK_WIDGET(gtk_builder_get_object(builder, "header_stack"));
state->stack_switcher = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack_switcher"));
state->stack = GTK_WIDGET(gtk_builder_get_object(builder, "heads_stack"));
state->scroller = GTK_WIDGET(gtk_builder_get_object(builder, "heads_scroll"));
state->canvas = GTK_WIDGET(gtk_builder_get_object(builder, "heads_layout"));
state->spinner = GTK_WIDGET(gtk_builder_get_object(builder, "spinner"));
state->zoom_out = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_out"));
state->zoom_reset = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_reset"));
state->zoom_in = GTK_WIDGET(gtk_builder_get_object(builder, "zoom_in"));
state->overlay = GTK_WIDGET(gtk_builder_get_object(builder, "overlay"));
state->info_bar = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info"));
state->info_label = GTK_WIDGET(gtk_builder_get_object(builder, "heads_info_label"));
gtk_builder_add_callback_symbol(builder, "heads_draw", G_CALLBACK(draw));
gtk_builder_add_callback_symbol(builder, "apply_changes", G_CALLBACK(apply_changes));
gtk_builder_add_callback_symbol(builder, "cancel_changes", G_CALLBACK(cancel_changes));
gtk_builder_add_callback_symbol(builder, "zoom_out", G_CALLBACK(zoom_out));
gtk_builder_add_callback_symbol(builder, "zoom_reset", G_CALLBACK(zoom_reset));
gtk_builder_add_callback_symbol(builder, "zoom_in", G_CALLBACK(zoom_in));
gtk_builder_add_callback_symbol(builder, "info_response", G_CALLBACK(info_response));
gtk_builder_connect_signals(builder, state);
gtk_box_set_homogeneous(GTK_BOX(gtk_builder_get_object(builder, "zoom_box")), FALSE);
update_zoom(state);
/* first child of GtkInfoBar is always GtkRevealer */
g_autoptr(GList) info_children = gtk_container_get_children(GTK_CONTAINER(state->info_bar));
g_signal_connect(info_children->data, "notify::child-revealed", G_CALLBACK(info_bar_animation_done), state);
struct wl_display *display = gdk_wayland_display_get_wl_display(gdk_display);
wd_add_output_management_listener(state, display);
if (state->output_manager == NULL) {
wd_fatal_error(1, "Compositor doesn't support wlr-output-management-unstable-v1");
}
gtk_application_add_window(app, GTK_WINDOW(window));
gtk_widget_show_all(window);
g_signal_connect(window, "destroy", G_CALLBACK(cleanup), state);
g_object_unref(builder);
}
// END GLOBAL CALLBACKS
int main(int argc, char *argv[]) {
GtkApplication *app = gtk_application_new("org.swaywm.sway-outputs", G_APPLICATION_FLAGS_NONE);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
int status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}

21
src/meson.build Normal file
View File

@ -0,0 +1,21 @@
cc = meson.get_compiler('c')
m_dep = cc.find_library('m', required : false)
gdk = dependency('gdk-3.0')
gtk = dependency('gtk+-3.0')
assert(gdk.get_pkgconfig_variable('targets').split().contains('wayland'), 'Wayland GDK backend not present')
executable(
'wdisplay',
[
'main.c',
'outputs.c',
resources,
],
dependencies : [
m_dep,
wayland_client,
client_protos,
gtk
]
)

349
src/outputs.c Normal file
View File

@ -0,0 +1,349 @@
/*
* Copyright (C) 2019 cyclopsian
* Copyright (C) 2017-2019 emersion
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* Parts of this file are taken from emersion/kanshi:
* https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/main.c
*/
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include "wdisplay.h"
#include "wlr-output-management-unstable-v1-client-protocol.h"
#define HEADS_MAX 64
struct wd_pending_config {
struct wd_state *state;
struct wl_list *outputs;
};
static void destroy_pending(struct wd_pending_config *pending) {
struct wd_head_config *output, *tmp;
wl_list_for_each_safe(output, tmp, pending->outputs, link) {
wl_list_remove(&output->link);
free(output);
}
free(pending->outputs);
free(pending);
}
static void config_handle_succeeded(void *data,
struct zwlr_output_configuration_v1 *config) {
struct wd_pending_config *pending = data;
zwlr_output_configuration_v1_destroy(config);
wd_ui_apply_done(pending->state, pending->outputs);
destroy_pending(pending);
}
static void config_handle_failed(void *data,
struct zwlr_output_configuration_v1 *config) {
struct wd_pending_config *pending = data;
zwlr_output_configuration_v1_destroy(config);
wd_ui_reset_all(pending->state);
wd_ui_apply_done(pending->state, NULL);
wd_ui_show_error(pending->state,
"The display server was not able to process your changes.");
destroy_pending(pending);
}
static void config_handle_cancelled(void *data,
struct zwlr_output_configuration_v1 *config) {
struct wd_pending_config *pending = data;
zwlr_output_configuration_v1_destroy(config);
wd_ui_reset_all(pending->state);
wd_ui_apply_done(pending->state, NULL);
wd_ui_show_error(pending->state,
"The display configuration was modified by the server before updates were processed. "
"Please check the configuration and apply the changes again.");
destroy_pending(pending);
}
static const struct zwlr_output_configuration_v1_listener config_listener = {
.succeeded = config_handle_succeeded,
.failed = config_handle_failed,
.cancelled = config_handle_cancelled,
};
void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs) {
struct zwlr_output_configuration_v1 *config =
zwlr_output_manager_v1_create_configuration(state->output_manager, state->serial);
struct wd_pending_config *pending = calloc(1, sizeof(*pending));
pending->state = state;
pending->outputs = new_outputs;
zwlr_output_configuration_v1_add_listener(config, &config_listener, pending);
ssize_t i = -1;
struct wd_head_config *output;
wl_list_for_each(output, new_outputs, link) {
i++;
struct wd_head *head = output->head;
if (!output->enabled && output->enabled != head->enabled) {
zwlr_output_configuration_v1_disable_head(config, head->wlr_head);
continue;
}
struct zwlr_output_configuration_head_v1 *config_head = zwlr_output_configuration_v1_enable_head(config, head->wlr_head);
const struct wd_mode *selected_mode = NULL;
const struct wd_mode *mode;
wl_list_for_each(mode, &head->modes, link) {
if (mode->width == output->width && mode->height == output->height && mode->refresh == output->refresh) {
selected_mode = mode;
break;
}
}
if (selected_mode != NULL) {
if (selected_mode != head->mode) {
zwlr_output_configuration_head_v1_set_mode(config_head, selected_mode->wlr_mode);
}
} else if (output->width != head->custom_mode.width
|| output->height != head->custom_mode.height
|| output->refresh != head->custom_mode.refresh) {
zwlr_output_configuration_head_v1_set_custom_mode(config_head,
output->width, output->height, output->refresh);
}
if (output->x != head->x || output->y != head->y) {
zwlr_output_configuration_head_v1_set_position(config_head, output->x, output->y);
}
if (output->scale != head->scale) {
zwlr_output_configuration_head_v1_set_scale(config_head, wl_fixed_from_double(output->scale));
}
if (output->transform != head->transform) {
zwlr_output_configuration_head_v1_set_transform(config_head, output->transform);
}
}
zwlr_output_configuration_v1_apply(config);
}
static void mode_handle_size(void *data, struct zwlr_output_mode_v1 *wlr_mode,
int32_t width, int32_t height) {
struct wd_mode *mode = data;
mode->width = width;
mode->height = height;
}
static void mode_handle_refresh(void *data,
struct zwlr_output_mode_v1 *wlr_mode, int32_t refresh) {
struct wd_mode *mode = data;
mode->refresh = refresh;
}
static void mode_handle_preferred(void *data,
struct zwlr_output_mode_v1 *wlr_mode) {
struct wd_mode *mode = data;
mode->preferred = true;
}
static void mode_handle_finished(void *data,
struct zwlr_output_mode_v1 *wlr_mode) {
struct wd_mode *mode = data;
wl_list_remove(&mode->link);
zwlr_output_mode_v1_destroy(mode->wlr_mode);
free(mode);
}
static const struct zwlr_output_mode_v1_listener mode_listener = {
.size = mode_handle_size,
.refresh = mode_handle_refresh,
.preferred = mode_handle_preferred,
.finished = mode_handle_finished,
};
static void head_handle_name(void *data,
struct zwlr_output_head_v1 *wlr_head, const char *name) {
struct wd_head *head = data;
head->name = strdup(name);
wd_ui_reset_head(head, WD_FIELD_NAME);
}
static void head_handle_description(void *data,
struct zwlr_output_head_v1 *wlr_head, const char *description) {
struct wd_head *head = data;
head->description = strdup(description);
wd_ui_reset_head(head, WD_FIELD_DESCRIPTION);
}
static void head_handle_physical_size(void *data,
struct zwlr_output_head_v1 *wlr_head, int32_t width, int32_t height) {
struct wd_head *head = data;
head->phys_width = width;
head->phys_height = height;
wd_ui_reset_head(head, WD_FIELD_PHYSICAL_SIZE);
}
static void head_handle_mode(void *data,
struct zwlr_output_head_v1 *wlr_head,
struct zwlr_output_mode_v1 *wlr_mode) {
struct wd_head *head = data;
struct wd_mode *mode = calloc(1, sizeof(*mode));
mode->head = head;
mode->wlr_mode = wlr_mode;
wl_list_insert(head->modes.prev, &mode->link);
zwlr_output_mode_v1_add_listener(wlr_mode, &mode_listener, mode);
}
static void head_handle_enabled(void *data,
struct zwlr_output_head_v1 *wlr_head, int32_t enabled) {
struct wd_head *head = data;
head->enabled = !!enabled;
if (!enabled) {
head->mode = NULL;
}
wd_ui_reset_head(head, WD_FIELD_ENABLED);
}
static void head_handle_current_mode(void *data,
struct zwlr_output_head_v1 *wlr_head,
struct zwlr_output_mode_v1 *wlr_mode) {
struct wd_head *head = data;
struct wd_mode *mode;
wl_list_for_each(mode, &head->modes, link) {
if (mode->wlr_mode == wlr_mode) {
head->mode = mode;
wd_ui_reset_head(head, WD_FIELD_MODE);
return;
}
}
fprintf(stderr, "received unknown current_mode\n");
head->mode = NULL;
}
static void head_handle_position(void *data,
struct zwlr_output_head_v1 *wlr_head, int32_t x, int32_t y) {
struct wd_head *head = data;
head->x = x;
head->y = y;
wd_ui_reset_head(head, WD_FIELD_POSITION);
}
static void head_handle_transform(void *data,
struct zwlr_output_head_v1 *wlr_head, int32_t transform) {
struct wd_head *head = data;
head->transform = transform;
wd_ui_reset_head(head, WD_FIELD_TRANSFORM);
}
static void head_handle_scale(void *data,
struct zwlr_output_head_v1 *wlr_head, wl_fixed_t scale) {
struct wd_head *head = data;
head->scale = wl_fixed_to_double(scale);
wd_ui_reset_head(head, WD_FIELD_SCALE);
}
static void head_handle_finished(void *data,
struct zwlr_output_head_v1 *wlr_head) {
struct wd_head *head = data;
wl_list_remove(&head->link);
zwlr_output_head_v1_destroy(head->wlr_head);
free(head->name);
free(head->description);
free(head);
}
static const struct zwlr_output_head_v1_listener head_listener = {
.name = head_handle_name,
.description = head_handle_description,
.physical_size = head_handle_physical_size,
.mode = head_handle_mode,
.enabled = head_handle_enabled,
.current_mode = head_handle_current_mode,
.position = head_handle_position,
.transform = head_handle_transform,
.scale = head_handle_scale,
.finished = head_handle_finished,
};
static void output_manager_handle_head(void *data,
struct zwlr_output_manager_v1 *manager,
struct zwlr_output_head_v1 *wlr_head) {
struct wd_state *state = data;
struct wd_head *head = calloc(1, sizeof(*head));
head->state = state;
head->wlr_head = wlr_head;
head->scale = 1.0;
wl_list_init(&head->modes);
wl_list_insert(&state->heads, &head->link);
zwlr_output_head_v1_add_listener(wlr_head, &head_listener, head);
}
static void output_manager_handle_done(void *data,
struct zwlr_output_manager_v1 *manager, uint32_t serial) {
struct wd_state *state = data;
state->serial = serial;
assert(wl_list_length(&state->heads) <= HEADS_MAX);
wd_ui_reset_heads(state);
}
static void output_manager_handle_finished(void *data,
struct zwlr_output_manager_v1 *manager) {
// This space is intentionally left blank
}
static const struct zwlr_output_manager_v1_listener output_manager_listener = {
.head = output_manager_handle_head,
.done = output_manager_handle_done,
.finished = output_manager_handle_finished,
};
static void registry_handle_global(void *data, struct wl_registry *registry,
uint32_t name, const char *interface, uint32_t version) {
struct wd_state *state = data;
if (strcmp(interface, zwlr_output_manager_v1_interface.name) == 0) {
state->output_manager = wl_registry_bind(registry, name, &zwlr_output_manager_v1_interface, 1);
zwlr_output_manager_v1_add_listener(state->output_manager, &output_manager_listener, state);
}
}
static void registry_handle_global_remove(void *data,
struct wl_registry *registry, uint32_t name) {
// This space is intentionally left blank
}
static const struct wl_registry_listener registry_listener = {
.global = registry_handle_global,
.global_remove = registry_handle_global_remove,
};
void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display) {
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, state);
wl_display_dispatch(display);
wl_display_roundtrip(display);
}

166
src/wdisplay.h Normal file
View File

@ -0,0 +1,166 @@
/*
* Copyright (C) 2019 cyclopsian
* Copyright (C) 2017-2019 emersion
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE X CONSORTIUM BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* Parts of this file are taken from emersion/kanshi:
* https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/kanshi.h
* https://github.com/emersion/kanshi/blob/38d27474b686fcc8324cc5e454741a49577c0988/include/config.h
*/
#ifndef WDISPLAY_WDISPLAY_H
#define WDISPLAY_WDISPLAY_H
#include <stdbool.h>
#include <wayland-client.h>
struct zwlr_output_mode_v1;
struct zwlr_output_head_v1;
struct zwlr_output_manager_v1;
struct _GtkWidget;
typedef struct _GtkWidget GtkWidget;
struct _GtkBuilder;
typedef struct _GtkBuilder GtkBuilder;
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_head_config {
struct wl_list link;
struct wd_head *head;
bool enabled;
int32_t width;
int32_t height;
int32_t refresh; // mHz
int32_t x;
int32_t y;
double scale;
enum wl_output_transform transform;
};
struct wd_mode {
struct wd_head *head;
struct zwlr_output_mode_v1 *wlr_mode;
struct wl_list link;
int32_t width, height;
int32_t refresh; // mHz
bool preferred;
};
struct wd_head {
struct wd_state *state;
struct zwlr_output_head_v1 *wlr_head;
struct wl_list link;
char *name, *description;
int32_t phys_width, phys_height; // mm
struct wl_list modes;
bool enabled;
struct wd_mode *mode;
struct {
int32_t width, height;
int32_t refresh;
} custom_mode;
int32_t x, y;
enum wl_output_transform transform;
double scale;
};
struct wd_state {
struct zwlr_output_manager_v1 *output_manager;
struct wl_list heads;
uint32_t serial;
double zoom;
int xorigin;
int yorigin;
GtkWidget *header_stack;
GtkWidget *stack_switcher;
GtkWidget *stack;
GtkWidget *scroller;
GtkWidget *canvas;
GtkWidget *spinner;
GtkWidget *zoom_out;
GtkWidget *zoom_reset;
GtkWidget *zoom_in;
GtkWidget *overlay;
GtkWidget *info_bar;
GtkWidget *info_label;
};
/*
* Displays an error message and then exits the program.
*/
void wd_fatal_error(int status, const char *message);
/*
* Starts listening for output management events from the compositor.
*/
void wd_add_output_management_listener(struct wd_state *state, struct wl_display *display);
/*
* Sends updated display configuration back to the compositor.
*/
void wd_apply_state(struct wd_state *state, struct wl_list *new_outputs);
/*
* Updates the UI stack of all heads. Does not update individual head forms.
* Useful for when a display is plugged/unplugged and we want to add/remove
* a page, but we don't want to wipe out user's changes on the other pages.
*/
void wd_ui_reset_heads(struct wd_state *state);
/*
* Updates a form with head configuration from the server. Only updates specified fields.
*/
void wd_ui_reset_head(const struct wd_head *head, unsigned int fields);
/*
* Updates the stack and all forms to the last known server state.
*/
void wd_ui_reset_all(struct wd_state *state);
/*
* Reactivates the GUI after the display configuration updates.
*/
void wd_ui_apply_done(struct wd_state *state, struct wl_list *outputs);
/*
* Reactivates the GUI after the display configuration updates.
*/
void wd_ui_show_error(struct wd_state *state, const char *message);
#endif