initialize repository
This commit is contained in:
commit
b278730ddd
20
LICENSE
Normal file
20
LICENSE
Normal 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
10
README.md
Normal 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
5
meson.build
Normal file
@ -0,0 +1,5 @@
|
||||
project('wdisplay', 'c')
|
||||
|
||||
subdir('protocol')
|
||||
subdir('resources')
|
||||
subdir('src')
|
38
protocol/meson.build
Normal file
38
protocol/meson.build
Normal 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,
|
||||
)
|
483
protocol/wlr-output-management-unstable-v1.xml
Normal file
483
protocol/wlr-output-management-unstable-v1.xml
Normal 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
464
resources/head.ui
Normal 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
7
resources/meson.build
Normal 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
8
resources/resources.xml
Normal 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
9
resources/style.css
Normal 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
287
resources/waydisplay.ui
Normal 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
635
src/main.c
Normal 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
21
src/meson.build
Normal 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
349
src/outputs.c
Normal 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, ®istry_listener, state);
|
||||
|
||||
wl_display_dispatch(display);
|
||||
wl_display_roundtrip(display);
|
||||
}
|
166
src/wdisplay.h
Normal file
166
src/wdisplay.h
Normal 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
|
Loading…
Reference in New Issue
Block a user