Compare commits
365 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cabdf7089 | ||
|
|
6a12efec50 | ||
|
|
388a3500e2 | ||
|
|
24806ecf31 | ||
|
|
33a1e9e3bd | ||
|
|
4fd05d613b | ||
|
|
b80e864bbb | ||
|
|
33bc121bc8 | ||
|
|
106e7febe5 | ||
|
|
3c6e72350e | ||
|
|
43c16a7865 | ||
|
|
264a838e1f | ||
|
|
b927cb7f21 | ||
|
|
8e35de9e8b | ||
|
|
13984f8636 | ||
|
|
5f3ace0c47 | ||
|
|
651782a3a3 | ||
|
|
4e5a073854 | ||
|
|
1442016a18 | ||
|
|
2adc462d94 | ||
|
|
9aea35200d | ||
|
|
5ef06a2466 | ||
|
|
c3e9aa0f81 | ||
|
|
444599c6ce | ||
|
|
6127995b36 | ||
|
|
83aa20a9e4 | ||
|
|
02e4e25b75 | ||
|
|
b32ae6e2f1 | ||
|
|
b8d29f0ac0 | ||
|
|
2395fbcf69 | ||
|
|
d6a64b03e0 | ||
|
|
1951fd7c20 | ||
|
|
4b47063406 | ||
|
|
48609e33c9 | ||
|
|
52afad5824 | ||
|
|
636eca0d0e | ||
|
|
8fd7064bff | ||
|
|
fde1b438df | ||
|
|
1d96b73e1a | ||
|
|
1ce12067aa | ||
|
|
728ccc40a6 | ||
|
|
02db186232 | ||
|
|
44123d35a5 | ||
|
|
a31bfbe288 | ||
|
|
f3d6d8bc53 | ||
|
|
74d6a05e2f | ||
|
|
89995b805e | ||
|
|
f921be46c0 | ||
|
|
16ca12f29b | ||
|
|
ee873e2000 | ||
|
|
bed241eaa0 | ||
|
|
d007a12471 | ||
|
|
9b59b57352 | ||
|
|
29d2e66e28 | ||
|
|
a46027bfdc | ||
|
|
44bb989cea | ||
|
|
1dbe34c891 | ||
|
|
3b2a09f8e1 | ||
|
|
03b4407d11 | ||
|
|
ca0e001376 | ||
|
|
9a8aac1ba0 | ||
|
|
03cacfdbf5 | ||
|
|
3a57167d54 | ||
|
|
b48c971a8b | ||
|
|
12f27ac6a6 | ||
|
|
9ed4bc3426 | ||
|
|
c6339ac9c2 | ||
|
|
0341930394 | ||
|
|
3d673247a7 | ||
|
|
ed671e9d7c | ||
|
|
6a72717fe5 | ||
|
|
6ea867b9d2 | ||
|
|
756b993078 | ||
|
|
007e2546a9 | ||
|
|
ec98533f1b | ||
|
|
ebc460ecc8 | ||
|
|
540e2df322 | ||
|
|
d0c581684b | ||
|
|
d4dbb27213 | ||
|
|
bfc7df5c56 | ||
|
|
9c5688f61b | ||
|
|
9ccb4dd541 | ||
|
|
1e3e76fcee | ||
|
|
9c9df47d62 | ||
|
|
00ceaa989d | ||
|
|
23b1526092 | ||
|
|
9339d6760e | ||
|
|
8bf6e533c5 | ||
|
|
902fc7f6a9 | ||
|
|
794064a8b8 | ||
|
|
e44b2ec528 | ||
|
|
5f484bd82c | ||
|
|
0269387b9a | ||
|
|
5c4ebbab59 | ||
|
|
40b2af5668 | ||
|
|
aa717a95ec | ||
|
|
86424607b7 | ||
|
|
74bb6f0070 | ||
|
|
61bbaf3728 | ||
|
|
fed47000ce | ||
|
|
7f43adfbd5 | ||
|
|
1f6251baf3 | ||
|
|
d1220b013e | ||
|
|
93a55b6bdf | ||
|
|
974e760136 | ||
|
|
387141d36f | ||
|
|
3713d3d59e | ||
|
|
2bb6754523 | ||
|
|
df1610431d | ||
|
|
1ea9615584 | ||
|
|
45ae7c95cf | ||
|
|
f1b6df4d56 | ||
|
|
202c0b2415 | ||
|
|
fd5026489c | ||
|
|
8032cb2cf0 | ||
|
|
13562d2aa1 | ||
|
|
1971d4a288 | ||
|
|
c039b79174 | ||
|
|
245dcf5730 | ||
|
|
e68a342b81 | ||
|
|
11aa237821 | ||
|
|
d7be9b38b1 | ||
|
|
4717c64b03 | ||
|
|
2c2633ec58 | ||
|
|
71d28f8ba3 | ||
|
|
bb1e9c6616 | ||
|
|
e14bb0cbcf | ||
|
|
5a13bd2491 | ||
|
|
b875f96885 | ||
|
|
0d3d4229bf | ||
|
|
ed23f56a29 | ||
|
|
a9a1f9d01c | ||
|
|
2506750243 | ||
|
|
f017f04bdc | ||
|
|
d1f8db3760 | ||
|
|
0995e86e89 | ||
|
|
4992185323 | ||
|
|
70df557c2b | ||
|
|
be07070789 | ||
|
|
2bc2252d30 | ||
|
|
9af3c85161 | ||
|
|
d7f6d2cc0c | ||
|
|
0f5b225107 | ||
|
|
85e8b04ee2 | ||
|
|
a6b1111230 | ||
|
|
d70b36000f | ||
|
|
12c090c7d3 | ||
|
|
d9496e0a0a | ||
|
|
c4ff7fd6b8 | ||
|
|
5bd600fad6 | ||
|
|
5144994575 | ||
|
|
1c24616388 | ||
|
|
ecd9a1f13f | ||
|
|
4181d80405 | ||
|
|
085f4d8bad | ||
|
|
bb320e97ed | ||
|
|
ccf96030da | ||
|
|
ca4061f171 | ||
|
|
0b4af1e6bb | ||
|
|
9e03cd0354 | ||
|
|
3d4badf5cd | ||
|
|
071ecf46de | ||
|
|
5ee2b95384 | ||
|
|
d610a980f8 | ||
|
|
70780ae14e | ||
|
|
5f7cb94f4e | ||
|
|
748c51e2b6 | ||
|
|
3ba9ce771b | ||
|
|
c18b062180 | ||
|
|
705da21363 | ||
|
|
609a3e54fd | ||
|
|
4e62d12e8c | ||
|
|
b4e003c8f7 | ||
|
|
9efe9d3c91 | ||
|
|
ef4a0fe7cb | ||
|
|
dacf6126bf | ||
|
|
9bbec944de | ||
|
|
6a55338b12 | ||
|
|
2d3abace0e | ||
|
|
47c02fc6c8 | ||
|
|
5f8c366b43 | ||
|
|
f4ed622510 | ||
|
|
f1ee19d014 | ||
|
|
67a8040e57 | ||
|
|
ff00be30df | ||
|
|
cf282bc225 | ||
|
|
c05a8290b7 | ||
|
|
a9e118ea8f | ||
|
|
8ed1fe4799 | ||
|
|
c1d76b5169 | ||
|
|
b1e650c7ec | ||
|
|
2e0dc48f3e | ||
|
|
d64e698eb1 | ||
|
|
315fab99a8 | ||
|
|
8ffd8de74a | ||
|
|
ad94cff59e | ||
|
|
b64cb6a3fd | ||
|
|
182c96d8e9 | ||
|
|
9192b3eba1 | ||
|
|
dd7aae3191 | ||
|
|
3fee07ad4c | ||
|
|
2daa8ba4aa | ||
|
|
a78cefc65b | ||
|
|
a0d1de55d7 | ||
|
|
c0cd6337fc | ||
|
|
0b8a3815b4 | ||
|
|
fc82d73728 | ||
|
|
0dfcb004e4 | ||
|
|
644d3f36d1 | ||
|
|
6607aba30b | ||
|
|
aa62c1bb9a | ||
|
|
9eab194c5f | ||
|
|
abeaf5cb61 | ||
|
|
575592a12d | ||
|
|
d72ce588fb | ||
|
|
7d2f2e9b47 | ||
|
|
636150ef08 | ||
|
|
612fe6a9c2 | ||
|
|
57448f6579 | ||
|
|
4d7526d21e | ||
|
|
fded9b8d57 | ||
|
|
b80e3f8beb | ||
|
|
b807712d79 | ||
|
|
2df417f23f | ||
|
|
a82d1a2dfc | ||
|
|
043b6df255 | ||
|
|
831f583f89 | ||
|
|
d0f8b7061f | ||
|
|
3a4f03ce27 | ||
|
|
e8dc3859fe | ||
|
|
5fe5830056 | ||
|
|
9ac14c963e | ||
|
|
cae1c47643 | ||
|
|
de1ed58f8d | ||
|
|
ff564fcb52 | ||
|
|
aabe190bf1 | ||
|
|
320d368699 | ||
|
|
af4fc84ef7 | ||
|
|
c7bfaf90ec | ||
|
|
61f8c666fe | ||
|
|
28be9d1c6f | ||
|
|
305c9df7ac | ||
|
|
d08ea69277 | ||
|
|
180a3b73db | ||
|
|
ac1d57e8ba | ||
|
|
5a32c469d3 | ||
|
|
5a17aae0ab | ||
|
|
329ccdc970 | ||
|
|
b64b90d0b1 | ||
|
|
41412db704 | ||
|
|
736f2f3095 | ||
|
|
719236d4f4 | ||
|
|
19f3fe67bf | ||
|
|
405b2f2dfe | ||
|
|
28481db82c | ||
|
|
4d8328a8d5 | ||
|
|
6fe0cf09b4 | ||
|
|
0018788780 | ||
|
|
e3e6c3c3b9 | ||
|
|
38feee9f29 | ||
|
|
90a1272a65 | ||
|
|
9ada8caabc | ||
|
|
746ce51e65 | ||
|
|
e04cfb9ac4 | ||
|
|
4df36142e5 | ||
|
|
01ce934294 | ||
|
|
7be4e6b514 | ||
|
|
0a3acd4a63 | ||
|
|
c8b0c9a1b2 | ||
|
|
302cfb13b7 | ||
|
|
2e3adfc510 | ||
|
|
44f35f8f3b | ||
|
|
c26b54047f | ||
|
|
a958e16465 | ||
|
|
c8eadbc7c4 | ||
|
|
c7080e5d99 | ||
|
|
3060e3ee00 | ||
|
|
2b63fa048c | ||
|
|
7654639f6f | ||
|
|
726e0c7dce | ||
|
|
fcc3bf3d50 | ||
|
|
2ec59c6812 | ||
|
|
a0d8af9d37 | ||
|
|
71b6c395ca | ||
|
|
5ec357b10f | ||
|
|
dc70bd27f2 | ||
|
|
44e7f57d21 | ||
|
|
9b461edd0c | ||
|
|
a6bd165d97 | ||
|
|
f9e1e3ec26 | ||
|
|
d45897164a | ||
|
|
d8a0b49bc2 | ||
|
|
34b9aad24b | ||
|
|
63b2b6cbc3 | ||
|
|
6c7158c06d | ||
|
|
7d1f6f9a25 | ||
|
|
3c8dd9c4db | ||
|
|
c7fcf251e3 | ||
|
|
6d58b2495d | ||
|
|
347eced060 | ||
|
|
6449133c57 | ||
|
|
1168e68278 | ||
|
|
4836c661ce | ||
|
|
f0bbbbe4a9 | ||
|
|
d9a1e5c2eb | ||
|
|
b6c59e1049 | ||
|
|
af6984cd7e | ||
|
|
51a47193d7 | ||
|
|
189736c1a9 | ||
|
|
8003c209fe | ||
|
|
1734d48af6 | ||
|
|
fc2f6f4c50 | ||
|
|
f4e44cbef5 | ||
|
|
3d6354c642 | ||
|
|
efb5fc53c1 | ||
|
|
51367ac078 | ||
|
|
b0ab2099fd | ||
|
|
4a0612ae8f | ||
|
|
c650974a11 | ||
|
|
6fe9f1a8d5 | ||
|
|
8c148aceda | ||
|
|
b4c85910ce | ||
|
|
fd63ac3626 | ||
|
|
2a0782eb02 | ||
|
|
f505323d56 | ||
|
|
309cc3425b | ||
|
|
81cb8f539f | ||
|
|
5214fb1d7f | ||
|
|
9aebe81ec4 | ||
|
|
d73bd68a70 | ||
|
|
3cb156aecb | ||
|
|
dea4393588 | ||
|
|
269d55d7eb | ||
|
|
c04e6a87e6 | ||
|
|
d8acac6ba9 | ||
|
|
010d4dd5aa | ||
|
|
e1397c1c97 | ||
|
|
aed169aa40 | ||
|
|
c396a1f922 | ||
|
|
f036fcbc0f | ||
|
|
5b84d7421f | ||
|
|
1d9d4d6398 | ||
|
|
47ada4d798 | ||
|
|
92401d8bc8 | ||
|
|
0d6d7a7459 | ||
|
|
c05e10fe48 | ||
|
|
9f40aeddc5 | ||
|
|
62d5daad90 | ||
|
|
5be5b4c8eb | ||
|
|
e938241fe8 | ||
|
|
a9637f87e7 | ||
|
|
eb4ce76f01 | ||
|
|
d4d61b8c93 | ||
|
|
35a2fcc615 | ||
|
|
bb54b67956 | ||
|
|
35578e79ec | ||
|
|
ce6c924f58 | ||
|
|
77dd32a3ff | ||
|
|
b0def958c7 | ||
|
|
698538ac84 | ||
|
|
affa44e387 | ||
|
|
df2eec24a3 | ||
|
|
9a5c20836a | ||
|
|
ee123ceb0a | ||
|
|
f2544020b3 |
9
.gitignore
vendored
@@ -2,5 +2,10 @@
|
||||
sibs-build/
|
||||
compile_commands.json
|
||||
|
||||
gpu-screen-recorder-overlay-daemon/sibs-build/
|
||||
gpu-screen-recorder-overlay-daemon/compile_commands.json
|
||||
**/xdg-output-unstable-v1-client-protocol.h
|
||||
**/xdg-output-unstable-v1-protocol.c
|
||||
|
||||
depends/.wraplock
|
||||
|
||||
.cache
|
||||
build/
|
||||
86
README.md
@@ -2,20 +2,21 @@
|
||||
|
||||
# GPU Screen Recorder UI
|
||||
A fullscreen overlay UI for [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/about/) in the style of ShadowPlay.\
|
||||
The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.\
|
||||
Note: This software is still in early alpha. Expect bugs, and please report any if you experience them. Some are already known, but it doesn't hurt to report them anyways.\
|
||||
You can report an issue by emailing the issue to dec05eba@protonmail.com.
|
||||
|
||||
# Usage
|
||||
Run `gsr-ui` and press `Left Alt+Z` to show/hide the UI. You can start the overlay UI at system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
There is also an option in the settings to enable/disable starting the program on system startup. This option only works on systems that use systemd.
|
||||
You have to manually add `gsr-ui` to system startup on systems that uses another init system.\
|
||||
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
|
||||
The application is currently primarly designed for X11 but it can run on Wayland as well through XWayland, with some caveats because of Wayland limitations.
|
||||
|
||||
# Installation
|
||||
If you are using an Arch Linux based distro then you can find gpu screen recorder ui on aur under the name gpu-screen-recorder-ui (`yay -S gpu-screen-recorder-ui`).\
|
||||
If you are running an Arch Linux based distro then you can find gpu screen recorder ui in the official repositories under the name gpu-screen-recorder-ui (`sudo pacman -S gpu-screen-recorder-ui`).\
|
||||
If you are running another distro then you can run `sudo ./install.sh`, but you need to manually install the dependencies, as described below.\
|
||||
You can also install gpu screen recorder (the gtk gui version) from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder). This flatpak includes both this UI and gpu-screen-recorder so no need to install that first.
|
||||
You can also install gpu screen recorder from [flathub](https://flathub.org/apps/details/com.dec05eba.gpu_screen_recorder) which includes this UI.
|
||||
|
||||
# Usage
|
||||
Start the program by running `gsr-ui` or clicking on `GPU Screen Recorder` on your desktop or in your application launcher.
|
||||
Press `Left Alt+Z` to show/hide the UI. Go into the settings (the icon on the right) to view all of the different hotkeys configured.\
|
||||
If you want the program to start on system startup and have it running in the background where it can be controlled with the hotkeys at any time
|
||||
then open the UI and go into the settings (the icon on the right) and enable "Start program on system startup?".\
|
||||
The application will be added to `~/.config/autostart/gpu-screen-recorder-ui.desktop`. This will be launched automatically when you use a desktop environment,
|
||||
but if you use a minimal window manager then you need to use a XDG autostart program such as `dex`, or launch the program manually from your window manager startup script.\
|
||||
A program called `gsr-ui-cli` is also installed when installing this software. This can be used to remotely control the UI. Run `gsr-ui-cli --help` to list the available commands.
|
||||
|
||||
# Dependencies
|
||||
GPU Screen Recorder UI uses meson build system so you need to install `meson` to build GPU Screen Recorder UI.
|
||||
@@ -23,39 +24,66 @@ GPU Screen Recorder UI uses meson build system so you need to install `meson` to
|
||||
## Build dependencies
|
||||
These are the dependencies needed to build GPU Screen Recorder UI:
|
||||
|
||||
* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxi)
|
||||
* libxcursor
|
||||
* x11 (libx11, libxrandr, libxrender, libxcomposite, libxfixes, libxext, libxi, libxcursor)
|
||||
* libglvnd (which provides libgl, libglx and libegl)
|
||||
* linux-api-headers
|
||||
* libpulse (libpulse-simple)
|
||||
* libdrm
|
||||
* libdbus
|
||||
* wayland (wayland-client, wayland-egl, wayland-scanner)
|
||||
* setcap (libcap)
|
||||
|
||||
## Runtime dependencies
|
||||
There are also additional dependencies needed at runtime:
|
||||
|
||||
* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/)
|
||||
* [GPU Screen Recorder](https://git.dec05eba.com/gpu-screen-recorder/) (version 5.0.0 or later)
|
||||
* [GPU Screen Recorder Notification](https://git.dec05eba.com/gpu-screen-recorder-notification/)
|
||||
|
||||
## Translation guide
|
||||
GPU Screen Recorder UI uses its own translation system for it. See the `translations/template.txt` file for the translation template.
|
||||
|
||||
You can even translate the program without building it from source code by just creating a translation file in `/usr/share/gsr-ui/translations/` folder (if installed as a system package).
|
||||
|
||||
## Program behavior notes
|
||||
At the moment different keyboard layouts are not supported. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Left Alt+Y instead of Left Alt+Z to open/hide the UI.\
|
||||
If you experience this issue then please email dec05eba@protonmail.com to get it fixed.\
|
||||
This program has to grab all keyboards and create a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.
|
||||
This might cause issues for you if you use input remapping software. To workaround this you can go into settings and select "Only grab virtual devices"
|
||||
By default this program has to grab all keyboards and creates a virtual keyboard (`gsr-ui virtual keyboard`) to make global hotkeys work on all Wayland compositors.\
|
||||
This might cause issues for you if you use keyboard remapping software. To workaround this you can go into settings and select "Yes, but only grab virtual devices" or "Yes, but don't grab devices".\
|
||||
If you use keyboard remapping software such as keyd then make sure to make it ignore "gsr-ui virtual keyboard" (dec0:5eba device id), otherwise your keyboard can get locked
|
||||
as gpu screen recorder tries to grab keys and keyd grabs gpu screen recorder, leading to a lock.\
|
||||
If you are stuck in such a lock where you cant press and keyboard keys you can press (left) ctrl+shift+alt+esc to close gpu screen recorder and remove it from system startup.
|
||||
|
||||
# License
|
||||
This software is licensed under GPL3.0-only. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.
|
||||
This software is licensed under GPL-3.0-only, see the LICENSE file for more information. Files under `fonts/` directory belong to the Noto Sans Google fonts project and they are licensed under `SIL Open Font License`.\
|
||||
`images/default.cur` it part of the [Adwaita icon theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/tree/master) which is licensed under `CC BY-SA 3.0`.\
|
||||
The controller buttons under `images/` were created by [Julio Cacko](https://juliocacko.itch.io/free-input-prompts) and they are licensed under `CC0 1.0 Universal`.\
|
||||
The PlayStation logo under `images/` was created by [ArksDigital](https://arks.itch.io/ps4-buttons) and it's licensed under `CC BY 4.0`.
|
||||
|
||||
# Reporting bugs, contributing patches, questions or donation
|
||||
See [https://git.dec05eba.com/?p=about](https://git.dec05eba.com/?p=about).\
|
||||
I'm looking for somebody that can create sound effects for the notifications.
|
||||
|
||||
# Demo
|
||||
[](https://www.youtube.com/watch?v=SOqXusCTXXA)
|
||||
|
||||
# Screenshots
|
||||

|
||||

|
||||
|
||||
# Donations
|
||||
If you want to donate you can donate via bitcoin or monero.
|
||||
* Bitcoin: bc1qqvuqnwrdyppf707ge27fqz2n9y9gu7lf5ypyuf
|
||||
* Monero: 4An9kp2qW1C9Gah7ewv4JzcNFQ5TAX7ineGCqXWK6vQnhsGGcRpNgcn8r9EC3tMcgY7vqCKs3nSRXhejMHBaGvFdN2egYet
|
||||

|
||||

|
||||
|
||||
# Known issues
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland and Sway. This is an issue with Hyprland and Sway. It cant be fixed until the UI is redesigned to not be a fullscreen overlay.
|
||||
* Different keyboard layouts are not supported at the moment. The physical layout of keys are used for global hotkeys. If your Z and Y keys are swapped for example then you need to press Left Alt+Y instead of Left Alt+Z to open/hide the UI. If you experience this issue then please email dec05eba@protonmail.com to get it fixed.
|
||||
* The mouse position can sometimes shift when opening the UI.
|
||||
* When the UI is open the wallpaper is shown instead of the game on Hyprland. This is an issue with Hyprland. It cant be fixed until the UI is redesigned to not be a fullscreen overlay. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* Opening the UI when a game is fullscreen can mess up the game window a bit on Hyprland. This is an issue with Hyprland. Change your waybar dock mode to "dock" in its config to fix this.
|
||||
* The background of the UI is black when opening the UI while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
* Unable to close the region selection with escape key while a Wayland application is focused on COSMIC. This is an issue with COSMIC.
|
||||
|
||||
# FAQ
|
||||
## I get an error when trying to start the gpu-screen-recorder-ui.service systemd service
|
||||
If you have previously used the flatpak version of GPU Screen Recorder with the new UI then the non-flatpak version of the systemd service will conflict with that. Run `gsr-ui` to fix that.
|
||||
## I use a non-qwerty keyboard layout and I have an issue with incorrect keys registered in the software
|
||||
This is a KDE Plasma Wayland issue. Use `setxkbmap <language>` command, for example `setxkbmap se` to make sure X11 applications (such as this one) gets updated to use your languages keyboard layout.
|
||||
## "Save to clipboard" option doesn't work for screenshots
|
||||
Some Wayland compositors don't support copying images on the clipboard between X11 and Wayland applications. GPU Screen Recorder UI is an X11 application. It can't be done properly on Wayland
|
||||
since Wayland doesn't support a non-focused application from setting the clipboard, so it can't work with GPU Screen Recorder hotkey usage. Use X11 if you want a functioning desktop.
|
||||
## The controller hotkey and steam overlap (home button brings up steam overlay)
|
||||
You can either disable the steam overlay or in steam click Steam->Settings->Controller and then click "Begin Test" under "Test Device Inputs". Click on "Setup Device Inputs" and configure controller buttons there and when you get to the home button press X to unbind it from steam.
|
||||
## The UI looks messed up on my Wayland system
|
||||
Wayland doesn't support GPU Screen Recorder UI properly. Some Wayland environments can display GPU Screen Recorder UI pretty well (such as KDE Plasma and Gnome) while others cannot (such as Hyprland and Niri).
|
||||
This is an issue in Wayland and it may be the case that it will never be fixed. Use X11 if you experience issues.
|
||||
|
||||
205
TODO
@@ -12,16 +12,8 @@ Handle events in draw function because the render position of elements is availa
|
||||
|
||||
Add nvidia overclock option.
|
||||
|
||||
Add support for window selection in capture.
|
||||
|
||||
Add option to record the focused monitor. This works on wayland too when using kms capture since we can get cursor position without root and see which monitor (crtc) the cursor is on. Or use create_window_get_center_position.
|
||||
|
||||
Make hotkeys configurable.
|
||||
|
||||
Filechooser should have the option to select list view, search bar and common folders/mounted drives on the left side for quick navigation. Also a button to create a new directory.
|
||||
|
||||
Support wayland (excluding gnome, or force xwayland on gnome).
|
||||
|
||||
Restart replay on system start if monitor resolution changes.
|
||||
|
||||
Show warning when selecting hevc/av1 on amd because of amd driver/ffmpeg bug.
|
||||
@@ -33,8 +25,6 @@ Have different modes. Overlay, window and side menu. Overlay can be used on x11,
|
||||
|
||||
Show navigation breadcrumbs for settings and deeper navigation (such as selecting a directory to save videos).
|
||||
|
||||
Add option to hide stream key like a password input.
|
||||
|
||||
Add global setting. In that setting there should be an option to enable/disable gsr-ui from system startup (the systemd service).
|
||||
|
||||
Add profiles and hotkey to switch between profiles (show notification when switching profile).
|
||||
@@ -43,8 +33,6 @@ Fix first frame being black when running without a compositor.
|
||||
|
||||
Add support for systray.
|
||||
|
||||
Add option to take screenshot.
|
||||
|
||||
Move event callbacks to a global list instead of std::function object in each widget. This reduces the size of widgets,
|
||||
since most widgets wont have the event callback set.
|
||||
This event callback would pass the widget as an argument.
|
||||
@@ -74,18 +62,10 @@ Play camera shutter sound when saving recording. When another sound when startin
|
||||
|
||||
Some games such as "The Finals" crashes/freezes when they lose focus when running them on x11 on a laptop with prime setup and the monitor runs on the iGPU while the game runs on the dGPU.
|
||||
|
||||
Run `systemctl status --user gpu-screen-recorder` when starting recording and give a notification warning if it returns 0 (running). Or run pidof gpu-screen-recorder.
|
||||
|
||||
Add option to select which gpu to record with, or list all monitors and automatically use the gpu associated with the monitor. Do the same in gtk application.
|
||||
|
||||
Test global hotkeys with azerty instead of qwerty.
|
||||
|
||||
Dont allow autostart of replay if capture option is window recording (when window recording is added).
|
||||
|
||||
Use global shortcuts desktop portal protocol on wayland when available.
|
||||
|
||||
When support for window capture is enabled on x11 then make sure to not save the window except temporary while the program is open.
|
||||
|
||||
Support CJK.
|
||||
|
||||
Move ui hover code from ::draw to ::on_event, to properly handle widget event stack.
|
||||
@@ -100,15 +80,180 @@ All steam game names by ID are available at https://api.steampowered.com/ISteamA
|
||||
|
||||
Dont put widget position to int position when scrolling. This makes the UI jitter when it's coming to a halt.
|
||||
|
||||
Show warning if another instance of gpu screen recorder is already running when starting recording?
|
||||
|
||||
Keyboard leds get turned off when stopping gsr-global-hotkeys (for example numlock). The numlock key has to be pressed twice again to make it look correct to match its state.
|
||||
|
||||
Implement hotkey changing in global settings by getting mgl key events. During this time gsr-global-hotkey would either need to be paused or add code in the callback handler for the existing hotkeys since they are grabbing hotkeys.
|
||||
This can only be done after gsr-global-hotkeys properly handle different keyboard layouts to make sure mgl keys match gsr-global-hotkey keys.
|
||||
|
||||
Re-enable hotkey disable option for flatpak.
|
||||
|
||||
Make gsr-ui flatpak systemd work nicely with non-flatpak gsr-ui. Maybe change ExecStart to do flatpak run ... || gsr-ui, but make it run as a shell command first with /bin/sh -c "".
|
||||
|
||||
When enabling X11 global hotkey again only grab lalt, not ralt.
|
||||
When enabling X11 global hotkey again only grab lalt, not ralt.
|
||||
|
||||
Show an error that prime run will be disabled when using desktop portal capture option. This can cause issues as the user may have selected a video codec option that isn't available on their iGPU but is available on the prime-run dGPU.
|
||||
|
||||
For keyboards that report supporting mice the keyboard grab will be delayed until any key has been pressed (and then released), see: https://github.com/dec05eba/gpu-screen-recorder-issues/issues/97
|
||||
See if there is any way around this.
|
||||
|
||||
Check if "modprobe uinput" is needed on some systems (old fedora?).
|
||||
|
||||
Add recording timer to see duration of recording/streaming.
|
||||
|
||||
Make folder with window name work when using gamescope. Gamescope runs x11 itself so to get the window name inside that we have to connect to the gamescope X11 server (DISPLAY=:1 on x11 and DISPLAY=:2 on wayland, but not always).
|
||||
Detect if the window is gamescope automatically (WM_CLASS = "gamescope") and get the x11 display automatically and connect to it to get the application its running.
|
||||
This seems to only be an issue on wayland? the window title of the gamescope/steam bigpicture mode is the title of the game on x11, so it works automatically on x11.
|
||||
|
||||
When clicking on current directory in file manager show a dropdown menu where you can select common directories (HOME, Videos, Downloads and mounted drives) for quick navigation. Maybe even button to search.
|
||||
|
||||
Maybe change gsr-ui startup retry time in the systemd service, from 5 seconds to 2 seconds.
|
||||
|
||||
Make it possible to take a screenshot through a button in the ui instead of having to use hotkey.
|
||||
|
||||
Handle failing to save a replay. gsr should output "failed to save replay, or something like that" to make it possible to detect that.
|
||||
|
||||
Dont allow saving replay while a replay save is in progress.
|
||||
|
||||
Make input work with cjk input systems (such as fcitx).
|
||||
|
||||
System startup option should also support runit and some other init systems, not only soystemd.
|
||||
|
||||
Use x11 shm instead of XGetImage (https://stackoverflow.com/questions/43442675/how-to-use-xshmgetimage-and-xshmputimage).
|
||||
|
||||
Do xi grab for keys as well. Otherwise the ui cant be used for keyboard input if a program has grabbed the keyboard, and there could possibly be a game that grabs the keyboard as well.
|
||||
|
||||
Make inactive buttons gray (in dropdown boxes and in the front page with save, etc when replay is not running).
|
||||
|
||||
Add option to do screen-direct recording. But make it clear that it should not be used, except for gsync on x11 nvidia.
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Verify if cursor tracker monitor name is always correct. It uses the wayland monitor name for recording, but gpu screen recorder uses a custom name created from the drm connector name.
|
||||
|
||||
Notification with the focused monitor (with CursorTrackerWayland) assumes that the x11 monitor name is the same as the drm monitor name. Same for find_monitor_by_name.
|
||||
|
||||
If CursorTrackerWayland fails then fallback to getting focused monitor by window creation trick. Need to take into consideration prime laptop with dGPU that controls external monitors which cant be captured (different /dev/dri/card device).
|
||||
Maybe automatically switch to recording with the device that controls the monitor.
|
||||
In that case also add all monitors available to capture in the capture list and automatically choose the gpu that controls the monitor.
|
||||
|
||||
Support cjk font. Use fc-match to find the location of the font. This also works in flatpak, in which case the fonts are in /run/host/..., where it lists system fonts.
|
||||
|
||||
Keyboard layout is incorrect on wayland when using kde plasma keyboard settings to setup multiple keyboards, for example when changing to french.
|
||||
Text input is correct, but hotkey is incorrect.
|
||||
Need to use "setxkbmap fr" as well.
|
||||
This happens only when grabbing keyboard (gsr-global-hotkeys). Same thing is seen with xev.
|
||||
|
||||
Getting focused monitor on wayland doesn't work when vrr is enabled. This is because it uses software cursor instead (at least on kde plasma wayland).
|
||||
Right now it falls back to create window & getting window position trick if there is no cursor visible (or a software cursor) and one monitor has vrr enabled.
|
||||
Remove this when linux & wayland supports vrr with hardware cursor plane.
|
||||
Find out another way to get cursor position on wayland.
|
||||
This was fixed in linux 6.11 and in kde plasma in this commit: https://invent.kde.org/plasma/kwin/-/merge_requests/7582/diffs.
|
||||
|
||||
Add option to start recording/replay/stream after the notification has disappeared. Show "Starting recording on this monitor in 3 seconds".
|
||||
See if we can use hardware overlay plane instead somehow.
|
||||
|
||||
When using wayland for mgl try using wlr-layer-shell and set layer to overlay and keyboard interactivity to exclusive. Do something similar for notifications.
|
||||
|
||||
When starting gsr-ui remove any temporary replay disk data that has possibly remained from a crash, by looking for all folders that starts with gsr-replay and end with .gsr, in the replay directory.
|
||||
|
||||
Add restart program button, in global settings. It should do almost the same thing as exit program, execept execv gsr-ui.
|
||||
|
||||
When gpu screen recorder ui can run as a regular window (and supports tray icon and global shortcut portal) remove gpu screen recorder gtk. Then all error checking needs to be moved from that project to this project.
|
||||
May need support for multi windows, or create a small project to display dialogs.
|
||||
|
||||
Add a bug report page that automatically includes system info (make this clear to the user).
|
||||
Do this by sending the report to a custom server that stores that data.
|
||||
The server should limit reports per IP to prevent spam.
|
||||
|
||||
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
|
||||
|
||||
Show message that replay/streaming has to be restarted if recording settings are changed while replay/streaming is ongoing.
|
||||
|
||||
Support vector graphics. Maybe support svg, rendering it to a texture for better performance.
|
||||
|
||||
Support freetype for text rendering. Maybe load freetype as runtime (with dlopen) and use that when available and fallback to stb_freetype if not available.
|
||||
|
||||
Show .webm container option. It's currently chosen automatically if vp8/vp9 is chosen. The available containers should automatically switch depending on the video codec.
|
||||
|
||||
In settings show audio levels for each audio. Maybe show audio level image beside the audio name in the dropdown box and switch to a different image (have 3-4 different images for each level) depending on the volume.
|
||||
|
||||
Only use fake cursor on wayland if the focused x11 window is fullscreen.
|
||||
|
||||
Create window as a real overlay window, using layer shell protocol, when possible. This will however minimize windows on floating wms. Check if this can be fixed somehow, or only use layer shell in tiling wms.
|
||||
|
||||
Add timeout option for screenshots.
|
||||
|
||||
Add a window that shows a warning for wayland users, that wayland doesn't support this software and if they experience issues then they should use x11 instead.
|
||||
|
||||
Add a window that shows a warning if gpu video encoding isn't supported.
|
||||
|
||||
Disable system notifications when recording. Does the notification dbus interface support pausing notifications?
|
||||
|
||||
Disable hotkeys if virtual keyboard is found (either at startup or after running), if grab type if not virtual. Show a notification if that happens that hotkeys have been disabled.
|
||||
Detect if keyboard is locked by listening to gsr-ui virtual keyboard events and if no event is received after pressing a key (when writing to it after receiving input from another keyboard)
|
||||
then remove the keyboard grab and show a message or something.
|
||||
This can happen if the gsr-ui virtual keyboard is grabbed by some other software.
|
||||
Maybe this can be fixed automatically by grabbing gsr-ui virtual keyboard and releasing it just before we write to it and then release it again.
|
||||
But wont keyboard remapping software grab the keyboard first if they detect it quickly?
|
||||
If we fail to grab it because some other software did then dont grab any keyboards nor gsr-ui virtual keyboards, just listen to them.
|
||||
|
||||
Add option to not capture cursor in screenshot when doing region/window capture.
|
||||
|
||||
Window selection doesn't work when a window is fullscreen on x11.
|
||||
|
||||
Make it possible to change replay duration of the "save 1 min" and "save 10 min" by adding them to the replay settings as options.
|
||||
|
||||
If replay duration is set below the "save 1 min" or "save 10 min" then gray them out and when hovering over those buttons
|
||||
show a tooltip that says that those buttons cant be used because the replay duration in replay settings is set to a lower value than that (and display the replay duration there).
|
||||
|
||||
The UI is unusable on a vertical monitor.
|
||||
|
||||
Steam overlay interfers with controller input in gsr ui. Maybe move controller handling the gsr-global-hotkeys to do a grab on the controller, to only give the key input to gsr ui.
|
||||
|
||||
For joysticks (gamepads) create a virtual device for each one (/dev/uinput) that has the same vendor, product and name. This is to make sure that it behaves the same way in applications since applications
|
||||
access joysticks directly through /dev/input/eventN or /dev/input/jsN. It needs the same number of buttons and pretend to be a controller of the same time, for example a ps4 controller
|
||||
so that games automatically display ps4 buttons if supported.
|
||||
This also allows us to copy event bits and other data from the device instead of saying we support everything.
|
||||
This should fix the issue of not being able to write to /dev/uinput with ABS_X event bit set.
|
||||
Maybe do this for regular keyboard inputs as well?
|
||||
|
||||
Use generic icons for controller input in settings and allow configuring them.
|
||||
|
||||
Add option to include game name in file name (video and screenshot). Replace / with space.
|
||||
|
||||
Check if the focused window is on top on x11 when choosing to take screenshot or show the window as the background of the overlay.
|
||||
|
||||
Convert clipboard image to requested type (from jpg to png for example).
|
||||
|
||||
Save clipboard image with wayland on wayland. Some wayland compositors (such as hyprland, budgie and maybe more (wlroots based ones?)) don't support copying clipboard image data from x11 applications to wayland applications.
|
||||
This can be done because retarded wayland only supports setting clipboard when the application has focus. This doesn't work with hotkey screenshot use.
|
||||
This is specifically an issue when using wl_data_device_manager, which is a standard protocol. It can be done when using wlr specific protocol.
|
||||
|
||||
When gsr supports pausing recording done in replay/streaming session then add support for that in gsr-ui as well.
|
||||
|
||||
Add recording timer when recording to show for how long we have been recording for. Maybe the same for live streaming and replay.
|
||||
|
||||
Add option to trim video in the ui. Show a list of all videos recorded so you dont have to navigate to them (maybe add option to manually navigate to a video as well). Maybe use mpv to view it (embedded) in the ui and trim regions (multiple) and ffmpeg command to trim it.
|
||||
|
||||
Show the currently recorded capture in the ui, to preview if everything looks ok. This is also good for webcam overlay. Do this when gsr supports rpc, which would also include an option to get a fd to the capture texture.
|
||||
|
||||
Show a question mark beside options. When hovering the question mark show a tooltip that explains the options.
|
||||
|
||||
Remove all mgl::Clock usage in Overlay. We only need to get the time once per update in Overlay::handle_events. Also get time in other places outside handle_events.
|
||||
|
||||
Handle stopping replay/stream when recording is running (show notification that the video is saved and move the video to folder with game name).
|
||||
|
||||
Sometimes when opening gpu screen recorder ui gsr-global-hotkeys incorrectly detects that keyboard input is locked.
|
||||
|
||||
When running replay for a long time and then stopping it it takes a while. Improve this.
|
||||
|
||||
Make it possible to resize webcam box from top left, top right and bottom left as well.
|
||||
|
||||
The flatpak version can for some get stuck at shutdown when instant replay is running. It only happens in the flatpak version and only when instant replay is running and it happens always. Manual SIGINT on gsr-ui stops gsr-ui properly, so why does it fail when shutting down the computer when the systemd stop signal is SIGINT? Maybe its related to the flatpak version being launched through gsr-gtk. I cant personally reproduce it.
|
||||
|
||||
Redesign the UI to allow capturing multiple video sources. Move webcam to capture sources as well then. Maybe design the UI to work more like obs studio then, where you start recording and then add sources at capture time, with a preview.
|
||||
|
||||
Add option to choose video container (either flv or mpegts) for youtube livestreaming.
|
||||
|
||||
Get wayland cursor position for region selector, otherwise the start position before the cursor moves is off.
|
||||
|
||||
Add option to set preset on nvidia. Use -ffmpeg-video-opts for that.
|
||||
|
||||
Webcam resolution list is too long for some people. Fix it by separating resolution and framerate. Sort resolution and framerate from highest to lowest. Add scrollbar for dropdown list. POOP. Add --filesystem=xdg-run/hypr and run the gsr-hyprland-helper directly instead of flatpak spawn or use wlr foreign top level window protocol. Nvidia webcam yuyv capture doesn't seem to work on x11?
|
||||
|
||||
Allow settings page to select input capture option/audio, to make sure it doesn't blindly select the default option when the sources aren't temporary available when opening the settings.
|
||||
|
||||
Add simple video cutting in the ui.
|
||||
@@ -2,7 +2,7 @@
|
||||
Description=GPU Screen Recorder UI Service
|
||||
|
||||
[Service]
|
||||
ExecStart=gsr-ui
|
||||
ExecStart=gsr-ui launch-daemon
|
||||
KillSignal=SIGINT
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
13
gpu-screen-recorder.desktop
Normal file
@@ -0,0 +1,13 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GPU Screen Recorder
|
||||
GenericName=Screen recorder
|
||||
GenericName[hu]=Képernyőrögzítő
|
||||
Comment=A ShadowPlay-like screen recorder for Linux
|
||||
Comment[hu]=ShadowPlay-szerű képernyőrögzítő Linuxra
|
||||
Icon=gpu-screen-recorder
|
||||
Exec=gsr-ui launch-hide-announce
|
||||
Terminal=false
|
||||
Keywords=gpu-screen-recorder;gsr-ui;screen recorder;streaming;twitch;replay;shadowplay;
|
||||
Keywords[hu]=gpu-screen-recorder;gsr-ui;képernyőrögzítő;streamelés;közvetítés;twitch;visszajátszás;shadowplay;
|
||||
Categories=AudioVideo;Recorder;
|
||||
BIN
icons/hicolor/128x128/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
icons/hicolor/32x32/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
icons/hicolor/64x64/apps/gpu-screen-recorder.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
images/default.cur
Normal file
BIN
images/delete.png
Normal file
|
After Width: | Height: | Size: 511 B |
BIN
images/info.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/masked.png
Normal file
|
After Width: | Height: | Size: 930 B |
BIN
images/ps4_cross.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/ps4_dpad_down.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/ps4_dpad_left.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/ps4_dpad_right.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/ps4_dpad_up.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/ps4_home.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/ps4_options.png
Normal file
|
After Width: | Height: | Size: 801 B |
BIN
images/ps4_triangle.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/question_mark.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/screenshot.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
images/settings_extra_small.png
Normal file
|
After Width: | Height: | Size: 959 B |
BIN
images/trash.png
Normal file
|
After Width: | Height: | Size: 410 B |
BIN
images/unmasked.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/warning.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
22
include/AudioPlayer.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <thread>
|
||||
|
||||
namespace gsr {
|
||||
// Only plays raw stereo PCM audio in 48000hz in s16le format.
|
||||
// Use this command to convert an audio file (input.wav) to a format playable by this class (output.pcm):
|
||||
// ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ar 48000 output.pcm
|
||||
class AudioPlayer {
|
||||
public:
|
||||
AudioPlayer() = default;
|
||||
~AudioPlayer();
|
||||
AudioPlayer(const AudioPlayer&) = delete;
|
||||
AudioPlayer& operator=(const AudioPlayer&) = delete;
|
||||
|
||||
bool play(const char *filepath);
|
||||
private:
|
||||
std::thread thread;
|
||||
bool stop_playing_audio = false;
|
||||
int audio_file_fd = -1;
|
||||
};
|
||||
}
|
||||
60
include/ClipboardFile.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
namespace gsr {
|
||||
struct ClipboardCopy {
|
||||
Window requestor = None;
|
||||
uint64_t file_offset = 0;
|
||||
Atom property = None;
|
||||
Atom requestor_target = None;
|
||||
};
|
||||
|
||||
class ClipboardFile {
|
||||
public:
|
||||
enum class FileType {
|
||||
JPG,
|
||||
PNG
|
||||
};
|
||||
|
||||
ClipboardFile();
|
||||
~ClipboardFile();
|
||||
ClipboardFile(const ClipboardFile&) = delete;
|
||||
ClipboardFile& operator=(const ClipboardFile&) = delete;
|
||||
|
||||
// Set this to an empty string to unset clipboard
|
||||
void set_current_file(const std::string &filepath, FileType file_type);
|
||||
private:
|
||||
bool file_type_matches_request_atom(FileType file_type, Atom request_atom);
|
||||
const char* file_type_clipboard_get_name(Atom request_atom);
|
||||
const char* file_type_get_name(FileType file_type);
|
||||
void send_clipboard_start(XSelectionRequestEvent *xselectionrequest);
|
||||
void transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy);
|
||||
ClipboardCopy* get_clipboard_copy_by_requestor(Window requestor);
|
||||
void remove_clipboard_copy(Window requestor);
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Window clipboard_window = None;
|
||||
int file_fd = -1;
|
||||
uint64_t file_size = 0;
|
||||
FileType file_type = FileType::JPG;
|
||||
|
||||
Atom incr_atom = None;
|
||||
Atom targets_atom = None;
|
||||
Atom clipboard_atom = None;
|
||||
Atom image_jpg_atom = None;
|
||||
Atom image_jpeg_atom = None;
|
||||
Atom image_png_atom = None;
|
||||
|
||||
std::thread event_thread;
|
||||
std::mutex mutex;
|
||||
bool running = true;
|
||||
|
||||
std::vector<ClipboardCopy> clipboard_copies;
|
||||
bool should_clear_selection = false;
|
||||
};
|
||||
}
|
||||
@@ -6,15 +6,36 @@
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
|
||||
#define GSR_CONFIG_FILE_VERSION 2
|
||||
|
||||
namespace gsr {
|
||||
struct SupportedCaptureOptions;
|
||||
|
||||
enum class ReplayStartupMode {
|
||||
DONT_TURN_ON_AUTOMATICALLY,
|
||||
TURN_ON_AT_SYSTEM_STARTUP,
|
||||
TURN_ON_AT_FULLSCREEN,
|
||||
TURN_ON_AT_POWER_SUPPLY_CONNECTED
|
||||
};
|
||||
|
||||
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str);
|
||||
|
||||
struct ConfigHotkey {
|
||||
int64_t keysym = 0;
|
||||
uint32_t modifiers = 0;
|
||||
int64_t key = 0; // Mgl key
|
||||
uint32_t modifiers = 0; // HotkeyModifier
|
||||
|
||||
bool operator==(const ConfigHotkey &other) const;
|
||||
bool operator!=(const ConfigHotkey &other) const;
|
||||
|
||||
std::string to_string(bool spaces = true, bool modifier_side = true) const;
|
||||
};
|
||||
|
||||
struct AudioTrack {
|
||||
std::vector<std::string> audio_inputs; // ids
|
||||
bool application_audio_invert = false;
|
||||
|
||||
bool operator==(const AudioTrack &other) const;
|
||||
bool operator!=(const AudioTrack &other) const;
|
||||
};
|
||||
|
||||
struct RecordOptions {
|
||||
@@ -24,27 +45,48 @@ namespace gsr {
|
||||
int32_t video_width = 0;
|
||||
int32_t video_height = 0;
|
||||
int32_t fps = 60;
|
||||
int32_t video_bitrate = 15000;
|
||||
bool merge_audio_tracks = true; // Currently unused for streaming because all known streaming sites only support 1 audio track
|
||||
bool application_audio_invert = false;
|
||||
int32_t video_bitrate = 8000;
|
||||
bool merge_audio_tracks = true; // TODO: Remove in the future
|
||||
bool application_audio_invert = false; // TODO: Remove in the future
|
||||
bool change_video_resolution = false;
|
||||
std::vector<std::string> audio_tracks;
|
||||
std::vector<std::string> audio_tracks; // ids, TODO: Remove in the future
|
||||
std::vector<AudioTrack> audio_tracks_list;
|
||||
std::string color_range = "limited";
|
||||
std::string video_quality = "very_high";
|
||||
std::string video_codec = "auto";
|
||||
std::string audio_codec = "opus";
|
||||
std::string framerate_mode = "vfr";
|
||||
std::string framerate_mode = "auto";
|
||||
bool advanced_view = false;
|
||||
bool overclock = false;
|
||||
bool record_cursor = true;
|
||||
bool restore_portal_session = true;
|
||||
bool low_power_mode = false;
|
||||
|
||||
std::string webcam_source = "";
|
||||
bool webcam_flip_horizontally = false;
|
||||
std::string webcam_video_format = "auto";
|
||||
int32_t webcam_camera_width = 0;
|
||||
int32_t webcam_camera_height = 0;
|
||||
int32_t webcam_camera_fps = 0;
|
||||
int32_t webcam_x = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_y = 0; // A value between 0 and 100 (percentage)
|
||||
int32_t webcam_width = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
int32_t webcam_height = 30; // A value between 0 and 100 (percentage), 0 = Don't scale it
|
||||
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
};
|
||||
|
||||
struct MainConfig {
|
||||
int32_t config_file_version = 0;
|
||||
int32_t config_file_version = GSR_CONFIG_FILE_VERSION;
|
||||
bool software_encoding_warning_shown = false;
|
||||
bool wayland_warning_shown = false;
|
||||
std::string hotkeys_enable_option = "enable_hotkeys";
|
||||
std::string joystick_hotkeys_enable_option = "disable_hotkeys";
|
||||
std::string tint_color;
|
||||
std::string notification_speed = "normal";
|
||||
std::string language;
|
||||
ConfigHotkey show_hide_hotkey;
|
||||
};
|
||||
|
||||
struct YoutubeStreamConfig {
|
||||
@@ -55,45 +97,79 @@ namespace gsr {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct RumbleStreamConfig {
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct KickStreamConfig {
|
||||
std::string stream_url;
|
||||
std::string stream_key;
|
||||
};
|
||||
|
||||
struct CustomStreamConfig {
|
||||
std::string url;
|
||||
std::string key;
|
||||
std::string container = "flv";
|
||||
};
|
||||
|
||||
struct StreamingConfig {
|
||||
RecordOptions record_options;
|
||||
bool show_streaming_started_notifications = true;
|
||||
bool show_streaming_stopped_notifications = true;
|
||||
std::string streaming_service = "twitch";
|
||||
YoutubeStreamConfig youtube;
|
||||
TwitchStreamConfig twitch;
|
||||
RumbleStreamConfig rumble;
|
||||
KickStreamConfig kick;
|
||||
CustomStreamConfig custom;
|
||||
ConfigHotkey start_stop_recording_hotkey;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
};
|
||||
|
||||
struct RecordConfig {
|
||||
RecordOptions record_options;
|
||||
bool save_video_in_game_folder = false;
|
||||
bool show_recording_started_notifications = true;
|
||||
bool show_video_saved_notifications = true;
|
||||
std::string save_directory;
|
||||
std::string container = "mp4";
|
||||
ConfigHotkey start_stop_recording_hotkey;
|
||||
ConfigHotkey pause_unpause_recording_hotkey;
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
ConfigHotkey pause_unpause_hotkey;
|
||||
ConfigHotkey start_stop_region_hotkey;
|
||||
ConfigHotkey start_stop_window_hotkey;
|
||||
};
|
||||
|
||||
struct ReplayConfig {
|
||||
RecordOptions record_options;
|
||||
std::string turn_on_replay_automatically_mode = "dont_turn_on_automatically";
|
||||
bool save_video_in_game_folder = false;
|
||||
bool show_replay_started_notifications = true;
|
||||
bool show_replay_stopped_notifications = true;
|
||||
bool show_replay_saved_notifications = true;
|
||||
bool restart_replay_on_save = false;
|
||||
std::string save_directory;
|
||||
std::string container = "mp4";
|
||||
int32_t replay_time = 60;
|
||||
ConfigHotkey start_stop_recording_hotkey;
|
||||
ConfigHotkey save_recording_hotkey;
|
||||
std::string replay_storage = "ram";
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
ConfigHotkey save_hotkey;
|
||||
ConfigHotkey save_1_min_hotkey;
|
||||
ConfigHotkey save_10_min_hotkey;
|
||||
};
|
||||
|
||||
struct ScreenshotConfig {
|
||||
std::string record_area_option = "screen";
|
||||
int32_t image_width = 0;
|
||||
int32_t image_height = 0;
|
||||
bool change_image_resolution = false;
|
||||
std::string image_quality = "very_high";
|
||||
std::string image_format = "jpg";
|
||||
bool record_cursor = true;
|
||||
bool restore_portal_session = true;
|
||||
|
||||
bool save_screenshot_in_game_folder = false;
|
||||
bool save_screenshot_to_clipboard = false;
|
||||
bool save_screenshot_to_disk = true;
|
||||
bool show_notifications = true;
|
||||
bool use_led_indicator = false;
|
||||
std::string save_directory;
|
||||
ConfigHotkey take_screenshot_hotkey;
|
||||
ConfigHotkey take_screenshot_region_hotkey;
|
||||
ConfigHotkey take_screenshot_window_hotkey; // Or desktop portal, on wayland
|
||||
|
||||
std::string custom_script;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
@@ -101,12 +177,15 @@ namespace gsr {
|
||||
bool operator==(const Config &other);
|
||||
bool operator!=(const Config &other);
|
||||
|
||||
void set_hotkeys_to_default();
|
||||
|
||||
MainConfig main_config;
|
||||
StreamingConfig streaming_config;
|
||||
RecordConfig record_config;
|
||||
ReplayConfig replay_config;
|
||||
ScreenshotConfig screenshot_config;
|
||||
};
|
||||
|
||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);
|
||||
void save_config(Config &config);
|
||||
}
|
||||
}
|
||||
|
||||
23
include/CursorTracker/CursorTracker.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <mglpp/system/vec.hpp>
|
||||
|
||||
namespace gsr {
|
||||
struct CursorInfo {
|
||||
mgl::vec2i position;
|
||||
std::string monitor_name;
|
||||
};
|
||||
|
||||
class CursorTracker {
|
||||
public:
|
||||
CursorTracker() = default;
|
||||
CursorTracker(const CursorTracker&) = delete;
|
||||
CursorTracker& operator=(const CursorTracker&) = delete;
|
||||
virtual ~CursorTracker() = default;
|
||||
|
||||
virtual void update() = 0;
|
||||
virtual std::optional<CursorInfo> get_latest_cursor_info() = 0;
|
||||
};
|
||||
}
|
||||
23
include/CursorTracker/CursorTrackerWayland.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "CursorTracker.hpp"
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
class CursorTrackerWayland : public CursorTracker {
|
||||
public:
|
||||
CursorTrackerWayland(const char *card_path, struct wl_display *wayland_dpy);
|
||||
CursorTrackerWayland(const CursorTrackerWayland&) = delete;
|
||||
CursorTrackerWayland& operator=(const CursorTrackerWayland&) = delete;
|
||||
~CursorTrackerWayland();
|
||||
|
||||
void update() override;
|
||||
std::optional<CursorInfo> get_latest_cursor_info() override;
|
||||
private:
|
||||
int drm_fd = -1;
|
||||
mgl::vec2i latest_cursor_position; // Position of the cursor within the monitor
|
||||
int latest_crtc_id = -1;
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
};
|
||||
}
|
||||
20
include/CursorTracker/CursorTrackerX11.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "CursorTracker.hpp"
|
||||
|
||||
typedef struct _XDisplay Display;
|
||||
|
||||
namespace gsr {
|
||||
class CursorTrackerX11 : public CursorTracker {
|
||||
public:
|
||||
CursorTrackerX11(Display *dpy);
|
||||
CursorTrackerX11(const CursorTrackerX11&) = delete;
|
||||
CursorTrackerX11& operator=(const CursorTrackerX11&) = delete;
|
||||
~CursorTrackerX11() = default;
|
||||
|
||||
void update() override {}
|
||||
std::optional<CursorInfo> get_latest_cursor_info() override;
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -9,9 +9,20 @@ namespace mgl {
|
||||
}
|
||||
|
||||
namespace gsr {
|
||||
enum HotkeyModifier : uint32_t {
|
||||
HOTKEY_MOD_LSHIFT = 1 << 0,
|
||||
HOTKEY_MOD_RSHIFT = 1 << 1,
|
||||
HOTKEY_MOD_LCTRL = 1 << 2,
|
||||
HOTKEY_MOD_RCTRL = 1 << 3,
|
||||
HOTKEY_MOD_LALT = 1 << 4,
|
||||
HOTKEY_MOD_RALT = 1 << 5,
|
||||
HOTKEY_MOD_LSUPER = 1 << 6,
|
||||
HOTKEY_MOD_RSUPER = 1 << 7
|
||||
};
|
||||
|
||||
struct Hotkey {
|
||||
uint64_t key = 0;
|
||||
uint32_t modifiers = 0;
|
||||
uint32_t key = 0; // X11 keysym
|
||||
uint32_t modifiers = 0; // HotkeyModifier
|
||||
};
|
||||
|
||||
using GlobalHotkeyCallback = std::function<void(const std::string &id)>;
|
||||
79
include/GlobalHotkeys/GlobalHotkeysJoystick.hpp
Normal file
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include "GlobalHotkeys.hpp"
|
||||
#include "../Hotplug.hpp"
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <poll.h>
|
||||
#include <linux/input.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int max_js_poll_fd = 16;
|
||||
|
||||
class GlobalHotkeysJoystick : public GlobalHotkeys {
|
||||
class GlobalHotkeysJoystickHotplugDelegate;
|
||||
public:
|
||||
GlobalHotkeysJoystick() = default;
|
||||
GlobalHotkeysJoystick(const GlobalHotkeysJoystick&) = delete;
|
||||
GlobalHotkeysJoystick& operator=(const GlobalHotkeysJoystick&) = delete;
|
||||
~GlobalHotkeysJoystick() override;
|
||||
|
||||
bool start();
|
||||
// Currently valid ids:
|
||||
// save_replay
|
||||
// save_1_min_replay
|
||||
// save_10_min_replay
|
||||
// take_screenshot
|
||||
// toggle_record
|
||||
// toggle_replay
|
||||
// toggle_show
|
||||
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
void poll_events() override;
|
||||
private:
|
||||
void close_fds();
|
||||
void read_events();
|
||||
void process_input_event(int fd, input_event &event);
|
||||
void add_all_joystick_devices();
|
||||
bool add_device(const char *dev_input_filepath, bool print_error = true);
|
||||
bool remove_device(const char *dev_input_filepath);
|
||||
bool remove_poll_fd(int index);
|
||||
// Returns -1 if not found
|
||||
int get_poll_fd_index_by_dev_input_id(int dev_input_id) const;
|
||||
private:
|
||||
struct ExtraData {
|
||||
int dev_input_id = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
|
||||
std::thread read_thread;
|
||||
|
||||
std::thread close_fd_thread;
|
||||
std::vector<int> fds_to_close;
|
||||
std::mutex close_fd_mutex;
|
||||
std::condition_variable close_fd_cv;
|
||||
|
||||
pollfd poll_fd[max_js_poll_fd];
|
||||
ExtraData extra_data[max_js_poll_fd];
|
||||
int num_poll_fd = 0;
|
||||
int event_fd = -1;
|
||||
int event_index = -1;
|
||||
|
||||
bool playstation_button_pressed = false;
|
||||
bool up_pressed = false;
|
||||
bool down_pressed = false;
|
||||
bool left_pressed = false;
|
||||
bool right_pressed = false;
|
||||
|
||||
bool save_replay = false;
|
||||
bool save_1_min_replay = false;
|
||||
bool save_10_min_replay = false;
|
||||
bool take_screenshot = false;
|
||||
bool toggle_record = false;
|
||||
bool toggle_replay = false;
|
||||
bool toggle_show = false;
|
||||
int hotplug_poll_index = -1;
|
||||
Hotplug hotplug;
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,8 @@ namespace gsr {
|
||||
public:
|
||||
enum class GrabType {
|
||||
ALL,
|
||||
VIRTUAL
|
||||
VIRTUAL,
|
||||
NO_GRAB
|
||||
};
|
||||
|
||||
GlobalHotkeysLinux(GrabType grab_type);
|
||||
@@ -18,11 +19,17 @@ namespace gsr {
|
||||
~GlobalHotkeysLinux() override;
|
||||
|
||||
bool start();
|
||||
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
bool bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
void unbind_all_keys() override;
|
||||
void poll_events() override;
|
||||
|
||||
std::function<void()> on_gsr_ui_virtual_keyboard_grabbed;
|
||||
private:
|
||||
void close_fds();
|
||||
private:
|
||||
pid_t process_id = 0;
|
||||
int pipes[2];
|
||||
int read_pipes[2];
|
||||
int write_pipes[2];
|
||||
FILE *read_file = nullptr;
|
||||
std::unordered_map<std::string, GlobalHotkeyCallback> bound_actions_by_id;
|
||||
GrabType grab_type;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
|
||||
@@ -19,16 +20,55 @@ namespace gsr {
|
||||
bool vp9 = false;
|
||||
};
|
||||
|
||||
struct SupportedImageFormats {
|
||||
bool jpeg = false;
|
||||
bool png = false;
|
||||
};
|
||||
|
||||
enum GsrCameraPixelFormat {
|
||||
YUYV,
|
||||
MJPEG
|
||||
};
|
||||
|
||||
struct GsrMonitor {
|
||||
std::string name;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
struct GsrCameraSetup {
|
||||
mgl::vec2i resolution;
|
||||
int fps;
|
||||
//GsrCameraPixelFormat pixel_format;
|
||||
};
|
||||
|
||||
struct GsrCamera {
|
||||
std::string path;
|
||||
std::vector<GsrCameraSetup> yuyv_setups;
|
||||
std::vector<GsrCameraSetup> mjpeg_setups;
|
||||
};
|
||||
|
||||
struct GsrVersion {
|
||||
uint8_t major = 0;
|
||||
uint8_t minor = 0;
|
||||
uint8_t patch = 0;
|
||||
|
||||
bool operator>(const GsrVersion &other) const;
|
||||
bool operator>=(const GsrVersion &other) const;
|
||||
bool operator<(const GsrVersion &other) const;
|
||||
bool operator<=(const GsrVersion &other) const;
|
||||
bool operator==(const GsrVersion &other) const;
|
||||
bool operator!=(const GsrVersion &other) const;
|
||||
|
||||
std::string to_string() const;
|
||||
};
|
||||
|
||||
struct SupportedCaptureOptions {
|
||||
bool window = false;
|
||||
bool region = false;
|
||||
bool focused = false;
|
||||
bool portal = false;
|
||||
std::vector<GsrMonitor> monitors;
|
||||
std::vector<GsrCamera> cameras;
|
||||
};
|
||||
|
||||
enum class DisplayServer {
|
||||
@@ -40,13 +80,15 @@ namespace gsr {
|
||||
struct SystemInfo {
|
||||
DisplayServer display_server = DisplayServer::UNKNOWN;
|
||||
bool supports_app_audio = false;
|
||||
GsrVersion gsr_version;
|
||||
};
|
||||
|
||||
enum class GpuVendor {
|
||||
UNKNOWN,
|
||||
AMD,
|
||||
INTEL,
|
||||
NVIDIA
|
||||
NVIDIA,
|
||||
BROADCOM
|
||||
};
|
||||
|
||||
struct GpuInfo {
|
||||
@@ -58,6 +100,7 @@ namespace gsr {
|
||||
SystemInfo system_info;
|
||||
GpuInfo gpu_info;
|
||||
SupportedVideoCodecs supported_video_codecs;
|
||||
SupportedImageFormats supported_image_formats;
|
||||
};
|
||||
|
||||
enum class GsrInfoExitStatus {
|
||||
@@ -78,4 +121,5 @@ namespace gsr {
|
||||
std::vector<AudioDevice> get_audio_devices();
|
||||
std::vector<std::string> get_application_audio();
|
||||
SupportedCaptureOptions get_supported_capture_options(const GsrInfo &gsr_info);
|
||||
std::vector<GsrCamera> get_v4l2_devices();
|
||||
}
|
||||
33
include/Hotplug.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
|
||||
namespace gsr {
|
||||
enum class HotplugAction {
|
||||
ADD,
|
||||
REMOVE
|
||||
};
|
||||
|
||||
using HotplugEventCallback = std::function<void(HotplugAction hotplug_action, const char *devname)>;
|
||||
|
||||
class Hotplug {
|
||||
public:
|
||||
Hotplug() = default;
|
||||
Hotplug(const Hotplug&) = delete;
|
||||
Hotplug& operator=(const Hotplug&) = delete;
|
||||
~Hotplug();
|
||||
|
||||
bool start();
|
||||
int steal_fd();
|
||||
void process_event_data(int fd, const HotplugEventCallback &callback);
|
||||
private:
|
||||
void parse_netlink_data(const char *line, const HotplugEventCallback &callback);
|
||||
private:
|
||||
int fd = -1;
|
||||
bool started = false;
|
||||
bool event_is_add = false;
|
||||
bool event_is_remove = false;
|
||||
bool subsystem_is_input = false;
|
||||
char event_data[1024];
|
||||
};
|
||||
}
|
||||
13
include/HyprlandWorkaround.hpp
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gsr {
|
||||
struct ActiveHyprlandWindow {
|
||||
std::string window_id = "";
|
||||
std::string title = "Game";
|
||||
};
|
||||
|
||||
void start_hyprland_listener_thread();
|
||||
std::string get_current_hyprland_window_title();
|
||||
}
|
||||
16
include/KwinWorkaround.hpp
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace gsr {
|
||||
struct ActiveKwinWindow {
|
||||
std::string title = "Game";
|
||||
bool fullscreen = false;
|
||||
std::string monitorName = "";
|
||||
};
|
||||
|
||||
void start_kwin_helper_thread();
|
||||
std::string get_current_kwin_window_title();
|
||||
bool get_current_kwin_window_fullscreen();
|
||||
std::string get_current_kwin_window_monitor_name();
|
||||
}
|
||||
33
include/LedIndicator.hpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
#include <sys/types.h>
|
||||
#include <vector>
|
||||
#include <mglpp/system/Clock.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class LedIndicator {
|
||||
public:
|
||||
LedIndicator();
|
||||
LedIndicator(const LedIndicator&) = delete;
|
||||
LedIndicator& operator=(const LedIndicator&) = delete;
|
||||
~LedIndicator();
|
||||
|
||||
void set_led(bool enabled);
|
||||
void blink();
|
||||
void update();
|
||||
private:
|
||||
bool run_gsr_global_hotkeys_set_leds(bool enabled);
|
||||
void update_led(bool new_state);
|
||||
void update_led_with_active_status();
|
||||
void check_led_status_outdated();
|
||||
private:
|
||||
pid_t gsr_global_hotkeys_pid = -1;
|
||||
bool led_indicator_on = false;
|
||||
bool led_enabled = false;
|
||||
bool perform_blink = false;
|
||||
mgl::Clock blink_timer;
|
||||
|
||||
std::vector<int> led_brightness_files;
|
||||
mgl::Clock read_led_brightness_timer;
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,13 @@
|
||||
#include "GsrInfo.hpp"
|
||||
#include "Config.hpp"
|
||||
#include "window_texture.h"
|
||||
#include "WindowUtils.hpp"
|
||||
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "ClipboardFile.hpp"
|
||||
#include "LedIndicator.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
@@ -16,6 +23,8 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
class DropdownButton;
|
||||
class GlobalHotkeys;
|
||||
@@ -31,7 +40,31 @@ namespace gsr {
|
||||
NONE,
|
||||
RECORD,
|
||||
REPLAY,
|
||||
STREAM
|
||||
STREAM,
|
||||
SCREENSHOT,
|
||||
NOTICE
|
||||
};
|
||||
|
||||
enum class NotificationLevel {
|
||||
INFO,
|
||||
ERROR,
|
||||
};
|
||||
|
||||
enum class RecordForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class ScreenshotForceType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
enum class NotificationSpeed {
|
||||
NORMAL,
|
||||
FAST
|
||||
};
|
||||
|
||||
class Overlay {
|
||||
@@ -41,45 +74,72 @@ namespace gsr {
|
||||
Overlay& operator=(const Overlay&) = delete;
|
||||
~Overlay();
|
||||
|
||||
void handle_events(gsr::GlobalHotkeys *global_hotkeys);
|
||||
void on_event(mgl::Event &event);
|
||||
void handle_events();
|
||||
// Returns false if not visible
|
||||
bool draw();
|
||||
|
||||
void show();
|
||||
void hide();
|
||||
void hide_next_frame();
|
||||
void toggle_show();
|
||||
void toggle_record();
|
||||
void toggle_record(RecordForceType force_type);
|
||||
void toggle_pause();
|
||||
void toggle_stream();
|
||||
void toggle_replay();
|
||||
void save_replay();
|
||||
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type);
|
||||
void save_replay_1_min();
|
||||
void save_replay_10_min();
|
||||
void take_screenshot();
|
||||
void take_screenshot_region();
|
||||
void take_screenshot_window();
|
||||
void show_notification(const char *str, double timeout_seconds, mgl::Color icon_color, mgl::Color bg_color, NotificationType notification_type, const char *capture_target = nullptr, NotificationLevel notification_level = NotificationLevel::INFO);
|
||||
bool is_open() const;
|
||||
bool should_exit(std::string &reason) const;
|
||||
void exit();
|
||||
void go_back_to_old_ui();
|
||||
|
||||
const Config& get_config() const;
|
||||
|
||||
void unbind_all_keyboard_hotkeys();
|
||||
void rebind_all_keyboard_hotkeys();
|
||||
|
||||
void set_notification_speed(NotificationSpeed notification_speed);
|
||||
|
||||
bool global_hotkeys_ungrab_keyboard = false;
|
||||
private:
|
||||
const char* notification_type_to_string(NotificationType notification_type);
|
||||
void update_upause_status();
|
||||
|
||||
void hide();
|
||||
|
||||
void handle_keyboard_mapping_event();
|
||||
void on_event(mgl::Event &event);
|
||||
|
||||
void recreate_global_hotkeys(const char *hotkey_option);
|
||||
void update_led_indicator_after_settings_change();
|
||||
void recreate_frontpage_ui_components();
|
||||
void xi_setup();
|
||||
void handle_xi_events();
|
||||
void process_key_bindings(mgl::Event &event);
|
||||
void grab_mouse_and_keyboard();
|
||||
void xi_setup_fake_cursor();
|
||||
void xi_grab_all_devices();
|
||||
|
||||
void close_gpu_screen_recorder_output();
|
||||
|
||||
double get_time_passed_in_replay_buffer_seconds();
|
||||
void update_notification_process_status();
|
||||
void save_video_in_current_game_directory(const char *video_filepath, NotificationType notification_type);
|
||||
void update_gsr_replay_save();
|
||||
void save_video_in_current_game_directory(std::string &video_filepath, NotificationType notification_type);
|
||||
void on_replay_saved(const char *replay_saved_filepath);
|
||||
void process_gsr_output();
|
||||
void on_gsr_process_error(int exit_code, NotificationType notification_type);
|
||||
void update_gsr_process_status();
|
||||
void update_gsr_screenshot_process_status();
|
||||
|
||||
void replay_status_update_status();
|
||||
void update_focused_fullscreen_status();
|
||||
void update_power_supply_status();
|
||||
void update_system_startup_status();
|
||||
|
||||
void on_stop_recording(int exit_code);
|
||||
void on_stop_recording(int exit_code, std::string &video_filepath);
|
||||
|
||||
void update_ui_recording_paused();
|
||||
void update_ui_recording_unpaused();
|
||||
@@ -93,11 +153,20 @@ namespace gsr {
|
||||
void update_ui_replay_started();
|
||||
void update_ui_replay_stopped();
|
||||
|
||||
void prepare_gsr_output_for_reading();
|
||||
void on_press_save_replay();
|
||||
void on_press_start_replay(bool disable_notification);
|
||||
void on_press_start_record();
|
||||
void on_press_start_stream();
|
||||
bool update_compositor_texture(const mgl_monitor *monitor);
|
||||
void on_press_save_replay_1_min_replay();
|
||||
void on_press_save_replay_10_min_replay();
|
||||
bool on_press_start_replay(bool disable_notification, bool finished_selection, std::string monitor_to_capture = "");
|
||||
void on_press_start_record(bool finished_selection, RecordForceType force_type);
|
||||
void on_press_start_stream(bool finished_selection);
|
||||
void on_press_take_screenshot(bool finished_selection, ScreenshotForceType force_type);
|
||||
bool update_compositor_texture(const Monitor &monitor);
|
||||
|
||||
void add_region_command(std::vector<const char*> &args, char *region_str, int region_str_size);
|
||||
void add_common_gpu_screen_recorder_args(std::vector<const char*> &args, const RecordOptions &record_options, const std::vector<std::string> &audio_tracks, const std::string &video_bitrate, const char *region, char *region_str, int region_str_size, const std::string ®ion_area_option, RecordForceType force_type = RecordForceType::NONE);
|
||||
|
||||
std::string get_capture_target(const std::string &capture_target, const SupportedCaptureOptions &capture_options);
|
||||
|
||||
void force_window_on_top();
|
||||
private:
|
||||
@@ -113,6 +182,9 @@ namespace gsr {
|
||||
GsrInfo gsr_info;
|
||||
egl_functions egl_funcs;
|
||||
Config config;
|
||||
Config current_recording_config;
|
||||
|
||||
std::string gsr_icon_path;
|
||||
|
||||
bool visible = false;
|
||||
|
||||
@@ -139,6 +211,7 @@ namespace gsr {
|
||||
pid_t notification_process = -1;
|
||||
int gpu_screen_recorder_process_output_fd = -1;
|
||||
FILE *gpu_screen_recorder_process_output_file = nullptr;
|
||||
pid_t gpu_screen_recorder_screenshot_process = -1;
|
||||
|
||||
DropdownButton *replay_dropdown_button_ptr = nullptr;
|
||||
DropdownButton *record_dropdown_button_ptr = nullptr;
|
||||
@@ -148,6 +221,8 @@ namespace gsr {
|
||||
|
||||
RecordingStatus recording_status = RecordingStatus::NONE;
|
||||
bool paused = false;
|
||||
mgl::Clock paused_clock;
|
||||
double paused_total_time_seconds = 0.0;
|
||||
|
||||
mgl::Clock replay_status_update_clock;
|
||||
std::string power_supply_online_filepath;
|
||||
@@ -155,6 +230,7 @@ namespace gsr {
|
||||
bool focused_window_is_fullscreen = false;
|
||||
|
||||
std::string record_filepath;
|
||||
std::string screenshot_filepath;
|
||||
|
||||
Display *xi_display = nullptr;
|
||||
int xi_opcode = 0;
|
||||
@@ -169,5 +245,51 @@ namespace gsr {
|
||||
|
||||
mgl::vec2i window_size = { 1280, 720 };
|
||||
mgl::vec2i window_pos = { 0, 0 };
|
||||
|
||||
mgl::Clock show_overlay_clock;
|
||||
double show_overlay_timeout_seconds = 0.0;
|
||||
|
||||
std::unique_ptr<GlobalHotkeys> global_hotkeys = nullptr;
|
||||
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
|
||||
Display *x11_dpy = nullptr;
|
||||
XEvent x11_mapping_xev;
|
||||
|
||||
struct wl_display *wayland_dpy = nullptr;
|
||||
|
||||
mgl::Clock replay_save_clock;
|
||||
bool replay_save_show_notification = false;
|
||||
ReplayStartupMode replay_startup_mode = ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
||||
bool try_replay_startup = true;
|
||||
bool replay_recording = false;
|
||||
int replay_save_duration_min = 0;
|
||||
mgl::Clock replay_duration_clock;
|
||||
double replay_saved_duration_sec = 0.0;
|
||||
bool replay_restart_on_save = false;
|
||||
|
||||
mgl::Clock recording_duration_clock;
|
||||
|
||||
AudioPlayer audio_player;
|
||||
|
||||
RegionSelector region_selector;
|
||||
bool start_region_capture = false;
|
||||
std::function<void()> on_region_selected;
|
||||
|
||||
bool start_window_capture = false;
|
||||
|
||||
std::string recording_capture_target;
|
||||
std::string screenshot_capture_target;
|
||||
|
||||
std::unique_ptr<CursorTracker> cursor_tracker;
|
||||
mgl::Clock cursor_tracker_update_clock;
|
||||
|
||||
bool hide_ui = false;
|
||||
bool reload_ui = false;
|
||||
double notification_duration_multiplier = 1.0;
|
||||
ClipboardFile clipboard_file;
|
||||
|
||||
std::unique_ptr<LedIndicator> led_indicator = nullptr;
|
||||
|
||||
bool supports_window_title = false;
|
||||
bool supports_window_fullscreen_state = false;
|
||||
};
|
||||
}
|
||||
@@ -12,14 +12,18 @@ namespace gsr {
|
||||
};
|
||||
|
||||
// Arguments ending with NULL
|
||||
bool exec_program_daemonized(const char **args);
|
||||
bool exec_program_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host.
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug = true);
|
||||
// Arguments ending with NULL. |read_fd| can be NULL
|
||||
pid_t exec_program(const char **args, int *read_fd);
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error
|
||||
int exec_program_get_stdout(const char **args, std::string &result);
|
||||
int exec_program_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
// Arguments ending with NULL. Returns the exit status of the program or -1 on error.
|
||||
// This works the same as |exec_program_get_stdout|, except on flatpak where this runs the program on the
|
||||
// host machine with flatpak-spawn --host
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result);
|
||||
// host machine with flatpak-spawn --host.
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug = true);
|
||||
pid_t pidof(const char *process_name, pid_t ignore_pid);
|
||||
}
|
||||
75
include/RegionSelector.hpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#pragma once
|
||||
|
||||
#include "WindowUtils.hpp"
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#include <vector>
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
struct Region {
|
||||
mgl::vec2i pos;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
struct RegionWindow {
|
||||
Window window = None;
|
||||
mgl::vec2i pos;
|
||||
mgl::vec2i size;
|
||||
};
|
||||
|
||||
class RegionSelector {
|
||||
public:
|
||||
enum class SelectionType {
|
||||
NONE,
|
||||
REGION,
|
||||
WINDOW
|
||||
};
|
||||
|
||||
RegionSelector();
|
||||
RegionSelector(const RegionSelector&) = delete;
|
||||
RegionSelector& operator=(const RegionSelector&) = delete;
|
||||
~RegionSelector();
|
||||
|
||||
bool start(SelectionType selection_type, mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_region_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const;
|
||||
// Returns None (0) if none is selected
|
||||
Window get_window_selection() const;
|
||||
|
||||
SelectionType get_selection_type() const;
|
||||
private:
|
||||
void on_button_press(const void *de);
|
||||
void on_button_release(const void *de);
|
||||
void on_mouse_motion(const void *de);
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
unsigned long region_window = 0;
|
||||
unsigned long cursor_window = 0;
|
||||
unsigned long region_window_colormap = 0;
|
||||
int xi_opcode = 0;
|
||||
GC region_gc = nullptr;
|
||||
GC cursor_gc = nullptr;
|
||||
|
||||
Region region;
|
||||
bool selecting_region = false;
|
||||
bool selected = false;
|
||||
bool canceled = false;
|
||||
bool is_wayland = false;
|
||||
std::vector<Monitor> monitors;
|
||||
std::vector<RegionWindow> windows; // First window is the window that is on top
|
||||
std::optional<RegionWindow> focused_window;
|
||||
mgl::vec2i cursor_pos;
|
||||
|
||||
SelectionType selection_type = SelectionType::NONE;
|
||||
};
|
||||
}
|
||||
@@ -4,31 +4,47 @@
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <poll.h>
|
||||
|
||||
typedef struct _IO_FILE FILE;
|
||||
#define GSR_RPC_MAX_CONNECTIONS 8
|
||||
#define GSR_RPC_MAX_POLLS (1 + GSR_RPC_MAX_CONNECTIONS) /* +1 to include the socket_fd itself for accept */
|
||||
#define GSR_RPC_MAX_MESSAGE_SIZE 128
|
||||
|
||||
namespace gsr {
|
||||
using RpcCallback = std::function<void(const std::string &name)>;
|
||||
|
||||
enum class RpcOpenResult {
|
||||
OK,
|
||||
CONNECTION_REFUSED,
|
||||
ERROR
|
||||
};
|
||||
|
||||
class Rpc {
|
||||
public:
|
||||
Rpc() = default;
|
||||
struct PollData {
|
||||
char buffer[GSR_RPC_MAX_MESSAGE_SIZE];
|
||||
int buffer_size = 0;
|
||||
};
|
||||
|
||||
Rpc();
|
||||
Rpc(const Rpc&) = delete;
|
||||
Rpc& operator=(const Rpc&) = delete;
|
||||
~Rpc();
|
||||
|
||||
bool create(const char *name);
|
||||
bool open(const char *name);
|
||||
RpcOpenResult open(const char *name);
|
||||
bool write(const char *str, size_t size);
|
||||
void poll();
|
||||
|
||||
bool add_handler(const std::string &name, RpcCallback callback);
|
||||
private:
|
||||
bool open_filepath(const char *filepath);
|
||||
void handle_client_data(int client_fd, PollData &poll_data);
|
||||
private:
|
||||
int fd = 0;
|
||||
FILE *file = nullptr;
|
||||
std::string fifo_filepath;
|
||||
int socket_fd = 0;
|
||||
std::string socket_filepath;
|
||||
struct pollfd polls[GSR_RPC_MAX_POLLS];
|
||||
PollData polls_data[GSR_RPC_MAX_POLLS];
|
||||
int num_polls = 0;
|
||||
std::unordered_map<std::string, RpcCallback> handlers_by_name;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <assert.h>
|
||||
#include "gui/Widget.hpp"
|
||||
|
||||
template <typename T>
|
||||
struct SafeVectorItem {
|
||||
T item;
|
||||
bool alive = false;
|
||||
};
|
||||
|
||||
// A vector that can be modified while iterating
|
||||
template <typename T>
|
||||
@@ -10,64 +17,84 @@ class SafeVector {
|
||||
public:
|
||||
using PointerType = typename std::pointer_traits<T>::element_type*;
|
||||
|
||||
SafeVector() = default;
|
||||
SafeVector(const SafeVector&) = delete;
|
||||
SafeVector& operator=(const SafeVector&) = delete;
|
||||
~SafeVector() {
|
||||
clear();
|
||||
}
|
||||
|
||||
void push_back(T item) {
|
||||
data.push_back(std::move(item));
|
||||
data.push_back({std::move(item), true});
|
||||
++num_items_alive;
|
||||
}
|
||||
|
||||
// Safe to call when vector is empty
|
||||
// TODO: Make this iterator safe
|
||||
void pop_back() {
|
||||
if(!data.empty())
|
||||
if(!data.empty()) {
|
||||
gsr::add_widget_to_remove(std::move(data.back().item));
|
||||
data.pop_back();
|
||||
--num_items_alive;
|
||||
}
|
||||
}
|
||||
|
||||
// Might not remove the data immediately if inside for_each loop.
|
||||
// In that case the item is removed at the end of the loop.
|
||||
void remove(PointerType item_to_remove) {
|
||||
if(for_each_depth == 0)
|
||||
if(for_each_depth == 0) {
|
||||
remove_item(item_to_remove);
|
||||
else
|
||||
remove_queue.push_back(item_to_remove);
|
||||
return;
|
||||
}
|
||||
|
||||
SafeVectorItem<T> *item = get_item(item_to_remove);
|
||||
if(item && item->alive) {
|
||||
item->alive = false;
|
||||
--num_items_alive;
|
||||
has_items_to_remove = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Safe to call when vector is empty, in which case it returns nullptr
|
||||
T* back() {
|
||||
if(data.empty())
|
||||
return nullptr;
|
||||
else
|
||||
return &data.back();
|
||||
for(auto it = data.rbegin(), end = data.rend(); it != end; ++it) {
|
||||
if(it->alive)
|
||||
return &it->item;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// TODO: Make this iterator safe
|
||||
void clear() {
|
||||
for(auto &item : data) {
|
||||
gsr::add_widget_to_remove(std::move(item.item));
|
||||
}
|
||||
data.clear();
|
||||
remove_queue.clear();
|
||||
num_items_alive = 0;
|
||||
}
|
||||
|
||||
// Return true from |callback| to continue. This function returns false if |callback| returned false
|
||||
bool for_each(std::function<bool(T &t)> callback) {
|
||||
bool for_each(std::function<bool(T &t)> callback, bool include_dead = false) {
|
||||
bool result = true;
|
||||
++for_each_depth;
|
||||
|
||||
for(size_t i = 0; i < data.size(); ++i) {
|
||||
result = callback(data[i]);
|
||||
if(!result)
|
||||
break;
|
||||
if(data[i].alive || include_dead) {
|
||||
result = callback(data[i].item);
|
||||
if(!result)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
--for_each_depth;
|
||||
if(for_each_depth == 0) {
|
||||
for(PointerType item_to_remove : remove_queue) {
|
||||
remove_item(item_to_remove);
|
||||
}
|
||||
remove_queue.clear();
|
||||
}
|
||||
if(for_each_depth == 0)
|
||||
remove_dead_items();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return true from |callback| to continue. This function returns false if |callback| returned false
|
||||
bool for_each_reverse(std::function<bool(T &t)> callback) {
|
||||
bool for_each_reverse(std::function<bool(T &t)> callback, bool include_dead = false) {
|
||||
bool result = true;
|
||||
++for_each_depth;
|
||||
|
||||
@@ -80,50 +107,84 @@ public:
|
||||
if(i < 0)
|
||||
break;
|
||||
|
||||
result = callback(data[i]);
|
||||
if(!result)
|
||||
break;
|
||||
if(data[i].alive || include_dead) {
|
||||
result = callback(data[i].item);
|
||||
if(!result)
|
||||
break;
|
||||
}
|
||||
|
||||
--i;
|
||||
}
|
||||
|
||||
--for_each_depth;
|
||||
if(for_each_depth == 0) {
|
||||
for(PointerType item_to_remove : remove_queue) {
|
||||
remove_item(item_to_remove);
|
||||
}
|
||||
remove_queue.clear();
|
||||
}
|
||||
if(for_each_depth == 0)
|
||||
remove_dead_items();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
T& operator[](size_t index) {
|
||||
return data[index];
|
||||
assert(index < data.size());
|
||||
return data[index].item;
|
||||
}
|
||||
|
||||
const T& operator[](size_t index) const {
|
||||
return data[index];
|
||||
assert(index < data.size());
|
||||
return data[index].item;
|
||||
}
|
||||
|
||||
size_t size() const {
|
||||
return data.size();
|
||||
return (size_t)num_items_alive;
|
||||
}
|
||||
|
||||
bool empty() const {
|
||||
return data.empty();
|
||||
return num_items_alive == 0;
|
||||
}
|
||||
|
||||
void replace_item(PointerType item_to_replace, T new_item) {
|
||||
SafeVectorItem<T> *item = get_item(item_to_replace);
|
||||
if(item->alive) {
|
||||
gsr::add_widget_to_remove(std::move(item->item));
|
||||
item->item = std::move(new_item);
|
||||
}
|
||||
}
|
||||
private:
|
||||
void remove_item(PointerType item_to_remove) {
|
||||
for(auto it = data.begin(), end = data.end(); it != end; ++it) {
|
||||
if(&*(*it) == item_to_remove) {
|
||||
if(&*(it->item) == item_to_remove) {
|
||||
gsr::add_widget_to_remove(std::move(it->item));
|
||||
data.erase(it);
|
||||
--num_items_alive;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SafeVectorItem<T>* get_item(PointerType item_to_remove) {
|
||||
for(auto &item : data) {
|
||||
if(&*(item.item) == item_to_remove)
|
||||
return &item;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void remove_dead_items() {
|
||||
if(!has_items_to_remove)
|
||||
return;
|
||||
|
||||
for(auto it = data.begin(); it != data.end();) {
|
||||
if(it->alive) {
|
||||
++it;
|
||||
} else {
|
||||
gsr::add_widget_to_remove(std::move(it->item));
|
||||
it = data.erase(it);
|
||||
}
|
||||
}
|
||||
has_items_to_remove = false;
|
||||
}
|
||||
private:
|
||||
std::vector<T> data;
|
||||
std::vector<PointerType> remove_queue;
|
||||
std::vector<SafeVectorItem<T>> data;
|
||||
int for_each_depth = 0;
|
||||
int num_items_alive = 0;
|
||||
bool has_items_to_remove = false;
|
||||
};
|
||||
@@ -24,10 +24,12 @@ namespace gsr {
|
||||
mgl::Font body_font;
|
||||
mgl::Font title_font;
|
||||
mgl::Font top_bar_font;
|
||||
mgl::Font camera_setup_font;
|
||||
|
||||
mgl::Texture combobox_arrow_texture;
|
||||
mgl::Texture settings_texture;
|
||||
mgl::Texture settings_small_texture;
|
||||
mgl::Texture settings_extra_small_texture;
|
||||
mgl::Texture folder_texture;
|
||||
mgl::Texture up_arrow_texture;
|
||||
mgl::Texture replay_button_texture;
|
||||
@@ -41,6 +43,22 @@ namespace gsr {
|
||||
mgl::Texture stop_texture;
|
||||
mgl::Texture pause_texture;
|
||||
mgl::Texture save_texture;
|
||||
mgl::Texture screenshot_texture;
|
||||
mgl::Texture trash_texture;
|
||||
mgl::Texture masked_texture;
|
||||
mgl::Texture unmasked_texture;
|
||||
mgl::Texture warning_texture;
|
||||
mgl::Texture info_texture;
|
||||
mgl::Texture question_mark_texture;
|
||||
|
||||
mgl::Texture ps4_home_texture;
|
||||
mgl::Texture ps4_options_texture;
|
||||
mgl::Texture ps4_dpad_up_texture;
|
||||
mgl::Texture ps4_dpad_down_texture;
|
||||
mgl::Texture ps4_dpad_left_texture;
|
||||
mgl::Texture ps4_dpad_right_texture;
|
||||
mgl::Texture ps4_cross_texture;
|
||||
mgl::Texture ps4_triangle_texture;
|
||||
|
||||
double double_click_timeout_seconds = 0.4;
|
||||
|
||||
|
||||
43
include/Translation.hpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace gsr {
|
||||
class Translation {
|
||||
public:
|
||||
static Translation& instance();
|
||||
void init(const char* translations_directory, const char* initial_language = nullptr);
|
||||
bool load_language(const char* lang);
|
||||
bool is_language_supported(const char* lang);
|
||||
bool plural_numbers_are_complex();
|
||||
const char* translate(const char* key);
|
||||
|
||||
std::string get_complex_plural_number_key(const char* key, int number);
|
||||
|
||||
template<typename... Args>
|
||||
std::string format(const char* key, Args&&... args) {
|
||||
const char* fmt = translate(key);
|
||||
|
||||
// result buffer
|
||||
char buffer[4096];
|
||||
snprintf(buffer, sizeof(buffer), fmt, std::forward<Args>(args)...);
|
||||
return std::string(buffer);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string get_system_language();
|
||||
std::string trim(const std::string& str);
|
||||
void process_escapes(std::string& str);
|
||||
|
||||
private:
|
||||
std::string translations_directory;
|
||||
std::string current_language = "en";
|
||||
std::unordered_map<std::string, std::string> translations;
|
||||
};
|
||||
}
|
||||
|
||||
#define TR(s) gsr::Translation::instance().translate(s)
|
||||
#define TRF(s, ...) gsr::Translation::instance().format(s, __VA_ARGS__)
|
||||
#define TRC(s, n) gsr::Translation::instance().get_complex_plural_number_key(s, n)
|
||||
#define TRPF(s, n, ...) TRF(TRC(s, n).c_str(), __VA_ARGS__)
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <string_view>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <optional>
|
||||
|
||||
namespace gsr {
|
||||
struct KeyValue {
|
||||
@@ -15,6 +14,9 @@ namespace gsr {
|
||||
using StringSplitCallback = std::function<bool(std::string_view line)>;
|
||||
|
||||
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func);
|
||||
bool starts_with(std::string_view str, const char *substr);
|
||||
bool ends_with(std::string_view str, const char *substr);
|
||||
std::string strip(const std::string &str);
|
||||
|
||||
std::string get_home_dir();
|
||||
std::string get_config_dir();
|
||||
@@ -24,6 +26,8 @@ namespace gsr {
|
||||
std::map<std::string, std::string> get_xdg_variables();
|
||||
|
||||
std::string get_videos_dir();
|
||||
std::string get_pictures_dir();
|
||||
|
||||
// Returns 0 on success
|
||||
int create_directory_recursive(char *path);
|
||||
bool file_get_content(const char *filepath, std::string &file_content);
|
||||
@@ -32,4 +36,15 @@ namespace gsr {
|
||||
// Returns the path to the parent directory (ignoring trailing /)
|
||||
// of "." if there is no parent directory and the directory path is relative
|
||||
std::string get_parent_directory(std::string_view directory);
|
||||
|
||||
// XDG Autostart helpers — toggle ~/.config/autostart/gpu-screen-recorder-ui.desktop
|
||||
bool is_xdg_autostart_enabled();
|
||||
// Returns 0 on success
|
||||
int set_xdg_autostart(bool enable);
|
||||
void replace_xdg_autostart_with_current_gsr_type();
|
||||
|
||||
// Systemd user service helpers
|
||||
bool wait_until_systemd_user_service_available();
|
||||
bool is_systemd_service_enabled(const char *service_name);
|
||||
bool disable_systemd_service(const char *service_name);
|
||||
}
|
||||
@@ -1,14 +1,55 @@
|
||||
#pragma once
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <optional>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
enum class WindowCaptureType {
|
||||
FOCUSED,
|
||||
CURSOR
|
||||
};
|
||||
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
|
||||
struct Monitor {
|
||||
mgl::vec2i position; // Logical position on Wayland
|
||||
mgl::vec2i size; // Logical size on Wayland
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct DrawableGeometry {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
};
|
||||
|
||||
std::optional<std::string> get_window_title(Display *dpy, Window window);
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused = true);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused = true);
|
||||
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window);
|
||||
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window);
|
||||
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height);
|
||||
Window window_get_target_window_child(Display *display, Window window);
|
||||
unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size);
|
||||
mgl::vec2i get_cursor_position(Display *dpy, Window *window);
|
||||
mgl::vec2i create_window_get_center_position(Display *display);
|
||||
std::string get_window_manager_name(Display *display);
|
||||
bool is_compositor_running(Display *dpy, int screen);
|
||||
std::vector<Monitor> get_monitors(Display *dpy);
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy);
|
||||
void xi_grab_all_mouse_devices(Display *dpy);
|
||||
void xi_ungrab_all_mouse_devices(Display *dpy);
|
||||
void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position);
|
||||
void window_set_fullscreen(Display *dpy, Window window, bool fullscreen);
|
||||
bool window_is_fullscreen(Display *display, Window window);
|
||||
bool get_drawable_geometry(Display *display, Drawable drawable, DrawableGeometry *geometry);
|
||||
std::optional<Monitor> get_monitor_by_window_center(Display *display, Window window);
|
||||
bool set_window_wm_state(Display *dpy, Window window, Atom atom);
|
||||
void make_window_click_through(Display *display, Window window);
|
||||
bool make_window_sticky(Display *dpy, Window window);
|
||||
bool hide_window_from_taskbar(Display *dpy, Window window);
|
||||
}
|
||||
@@ -21,6 +21,7 @@ namespace gsr {
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
void set_border_scale(float scale);
|
||||
void set_icon_padding_scale(float scale);
|
||||
void set_bg_hover_color(mgl::Color color);
|
||||
void set_icon(mgl::Texture *texture);
|
||||
|
||||
@@ -30,6 +31,7 @@ namespace gsr {
|
||||
std::function<void()> on_click;
|
||||
private:
|
||||
void scale_sprite_to_button_size();
|
||||
float get_button_height();
|
||||
private:
|
||||
mgl::vec2f size;
|
||||
mgl::Color bg_color;
|
||||
@@ -37,5 +39,6 @@ namespace gsr {
|
||||
mgl::Text text;
|
||||
mgl::Sprite sprite;
|
||||
float border_scale = 0.0015f;
|
||||
float icon_padding_scale = 1.0f;
|
||||
};
|
||||
}
|
||||
@@ -18,8 +18,12 @@ namespace gsr {
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
void add_item(const std::string &text, const std::string &id);
|
||||
void add_item(const std::string &text, const std::string &id, bool allow_duplicate = true);
|
||||
void clear_items();
|
||||
|
||||
// The item can only be selected if it's enabled
|
||||
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
|
||||
void set_item_enabled(const std::string &id, bool enabled);
|
||||
const std::string& get_selected_id() const;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
@@ -36,6 +40,7 @@ namespace gsr {
|
||||
mgl::Text text;
|
||||
std::string id;
|
||||
mgl::vec2f position;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
mgl::vec2f max_size;
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace gsr {
|
||||
void add_item(const std::string &text, const std::string &id, const std::string &description = "");
|
||||
void set_item_label(const std::string &id, const std::string &new_label);
|
||||
void set_item_icon(const std::string &id, mgl::Texture *texture);
|
||||
void set_item_description(const std::string &id, const std::string &new_description);
|
||||
void set_item_enabled(const std::string &id, bool enabled);
|
||||
|
||||
void set_description(std::string description_text);
|
||||
void set_activated(bool activated);
|
||||
@@ -35,6 +37,7 @@ namespace gsr {
|
||||
mgl::Text description_text;
|
||||
mgl::Texture *icon_texture = nullptr;
|
||||
std::string id;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
std::vector<Item> items;
|
||||
|
||||
@@ -4,13 +4,31 @@
|
||||
#include <functional>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
#include <mglpp/graphics/Text32.hpp>
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
|
||||
namespace gsr {
|
||||
using EntryValidateHandler = std::function<bool(std::string &str)>;
|
||||
class Entry;
|
||||
|
||||
enum class EntryValidateHandlerResult {
|
||||
DENY,
|
||||
ALLOW,
|
||||
REPLACED
|
||||
};
|
||||
using EntryValidateHandler = std::function<EntryValidateHandlerResult(Entry &entry, const std::u32string &str)>;
|
||||
|
||||
struct CaretIndexPos {
|
||||
int index;
|
||||
mgl::vec2f pos;
|
||||
};
|
||||
|
||||
class Entry : public Widget {
|
||||
public:
|
||||
enum class Direction {
|
||||
LEFT,
|
||||
RIGHT
|
||||
};
|
||||
|
||||
Entry(mgl::Font *font, const char *text, float max_width);
|
||||
Entry(const Entry&) = delete;
|
||||
Entry& operator=(const Entry&) = delete;
|
||||
@@ -20,8 +38,11 @@ namespace gsr {
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
void set_text(std::string str);
|
||||
const std::string& get_text() const;
|
||||
EntryValidateHandlerResult set_text(const std::string &str);
|
||||
std::string get_text() const;
|
||||
|
||||
void set_masked(bool masked);
|
||||
bool is_masked() const;
|
||||
|
||||
// Return false to specify that the string should not be accepted. This reverts the string back to its previous value.
|
||||
// The input can be changed by changing the input parameter and returning true.
|
||||
@@ -29,10 +50,31 @@ namespace gsr {
|
||||
|
||||
std::function<void(const std::string &text)> on_changed;
|
||||
private:
|
||||
mgl::Text text;
|
||||
// Also updates the cursor position
|
||||
void replace_text(size_t index, size_t size, const std::u32string &replacement);
|
||||
void move_caret_word(Direction direction, size_t max_codepoints);
|
||||
EntryValidateHandlerResult set_text_internal(std::u32string str);
|
||||
void draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
|
||||
void draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size);
|
||||
CaretIndexPos find_closest_caret_index_by_position(mgl::vec2f position);
|
||||
private:
|
||||
struct Caret {
|
||||
float offset_x = 0.0f;
|
||||
int index = 0;
|
||||
};
|
||||
|
||||
mgl::Rectangle background;
|
||||
mgl::Text32 text;
|
||||
mgl::Text32 masked_text;
|
||||
float max_width;
|
||||
bool selected = false;
|
||||
float caret_offset_x = 0.0f;
|
||||
bool selecting_text = false;
|
||||
bool selecting_with_keyboard = false;
|
||||
bool show_selection = false;
|
||||
bool masked = false;
|
||||
Caret caret;
|
||||
Caret selection_start_caret;
|
||||
float text_overflow = 0.0f;
|
||||
};
|
||||
|
||||
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max);
|
||||
|
||||
@@ -5,18 +5,40 @@
|
||||
#include "../Config.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class Overlay;
|
||||
class GsrPage;
|
||||
class PageStack;
|
||||
class ScrollablePage;
|
||||
class Subsection;
|
||||
class RadioButton;
|
||||
class Button;
|
||||
class List;
|
||||
class ComboBox;
|
||||
class CustomRendererWidget;
|
||||
|
||||
enum ConfigureHotkeyType {
|
||||
NONE,
|
||||
REPLAY_START_STOP,
|
||||
REPLAY_SAVE,
|
||||
REPLAY_SAVE_1_MIN,
|
||||
REPLAY_SAVE_10_MIN,
|
||||
RECORD_START_STOP,
|
||||
RECORD_PAUSE_UNPAUSE,
|
||||
RECORD_START_STOP_REGION,
|
||||
RECORD_START_STOP_WINDOW,
|
||||
STREAM_START_STOP,
|
||||
TAKE_SCREENSHOT,
|
||||
TAKE_SCREENSHOT_REGION,
|
||||
TAKE_SCREENSHOT_WINDOW,
|
||||
SHOW_HIDE
|
||||
};
|
||||
|
||||
class GlobalSettingsPage : public StaticPage {
|
||||
public:
|
||||
GlobalSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
|
||||
GlobalSettingsPage(Overlay *overlay, const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
|
||||
GlobalSettingsPage(const GlobalSettingsPage&) = delete;
|
||||
GlobalSettingsPage& operator=(const GlobalSettingsPage&) = delete;
|
||||
|
||||
@@ -24,19 +46,50 @@ namespace gsr {
|
||||
void save();
|
||||
void on_navigate_away_from_page() override;
|
||||
|
||||
// Called with (enable, exit_status)
|
||||
std::function<void(bool, int)> on_startup_changed;
|
||||
// Called with (reason)
|
||||
std::function<void(const char*)> on_click_exit_program_button;
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
std::function<void(bool enable, int exit_status)> on_startup_changed;
|
||||
std::function<void(const char *reason)> on_click_exit_program_button;
|
||||
std::function<void(const char *hotkey_option)> on_keyboard_hotkey_changed;
|
||||
std::function<void(const char *hotkey_option)> on_joystick_hotkey_changed;
|
||||
std::function<void()> on_page_closed;
|
||||
private:
|
||||
void load_hotkeys();
|
||||
|
||||
std::unique_ptr<Subsection> create_appearance_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_startup_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_hotkey_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<RadioButton> create_enable_keyboard_hotkeys_button();
|
||||
std::unique_ptr<RadioButton> create_enable_joystick_hotkeys_button();
|
||||
std::unique_ptr<List> create_show_hide_hotkey_options();
|
||||
std::unique_ptr<List> create_replay_hotkey_options();
|
||||
std::unique_ptr<List> create_replay_partial_save_hotkey_options();
|
||||
std::unique_ptr<List> create_record_hotkey_options();
|
||||
std::unique_ptr<List> create_record_hotkey_window_region_options();
|
||||
std::unique_ptr<List> create_record_hotkey_window_options();
|
||||
std::unique_ptr<List> create_stream_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_region_hotkey_options();
|
||||
std::unique_ptr<List> create_screenshot_window_hotkey_options();
|
||||
std::unique_ptr<List> create_hotkey_control_buttons();
|
||||
std::unique_ptr<Subsection> create_keyboard_hotkey_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_controller_hotkey_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Button> create_exit_program_button();
|
||||
std::unique_ptr<Button> create_go_back_to_old_ui_button();
|
||||
std::unique_ptr<List> create_notification_speed();
|
||||
std::unique_ptr<List> create_language();
|
||||
std::unique_ptr<Subsection> create_application_options_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_application_info_subsection(ScrollablePage *parent_page);
|
||||
std::unique_ptr<Subsection> create_donate_subsection(ScrollablePage *parent_page);
|
||||
void add_widgets();
|
||||
|
||||
Button* configure_hotkey_get_button_by_active_type();
|
||||
ConfigHotkey* configure_hotkey_get_config_by_active_type();
|
||||
void for_each_config_hotkey(std::function<void(ConfigHotkey *config_hotkey)> callback);
|
||||
void configure_hotkey_start(ConfigureHotkeyType hotkey_type);
|
||||
void configure_hotkey_cancel();
|
||||
void configure_hotkey_stop_and_save();
|
||||
private:
|
||||
Overlay *overlay = nullptr;
|
||||
Config &config;
|
||||
const GsrInfo *gsr_info = nullptr;
|
||||
|
||||
@@ -44,6 +97,29 @@ namespace gsr {
|
||||
PageStack *page_stack = nullptr;
|
||||
RadioButton *tint_color_radio_button_ptr = nullptr;
|
||||
RadioButton *startup_radio_button_ptr = nullptr;
|
||||
RadioButton *enable_hotkeys_radio_button_ptr = nullptr;
|
||||
RadioButton *enable_keyboard_hotkeys_radio_button_ptr = nullptr;
|
||||
RadioButton *enable_joystick_hotkeys_radio_button_ptr = nullptr;
|
||||
|
||||
Button *turn_replay_on_off_button_ptr = nullptr;
|
||||
Button *save_replay_button_ptr = nullptr;
|
||||
Button *save_replay_1_min_button_ptr = nullptr;
|
||||
Button *save_replay_10_min_button_ptr = nullptr;
|
||||
Button *start_stop_recording_button_ptr = nullptr;
|
||||
Button *pause_unpause_recording_button_ptr = nullptr;
|
||||
Button *start_stop_recording_region_button_ptr = nullptr;
|
||||
Button *start_stop_recording_window_button_ptr = nullptr;
|
||||
Button *start_stop_streaming_button_ptr = nullptr;
|
||||
Button *take_screenshot_button_ptr = nullptr;
|
||||
Button *take_screenshot_region_button_ptr = nullptr;
|
||||
Button *take_screenshot_window_button_ptr = nullptr;
|
||||
Button *show_hide_button_ptr = nullptr;
|
||||
RadioButton *notification_speed_button_ptr = nullptr;
|
||||
ComboBox *language_combo_box_ptr = nullptr;
|
||||
|
||||
ConfigHotkey configure_config_hotkey;
|
||||
ConfigureHotkeyType configure_hotkey_type = ConfigureHotkeyType::NONE;
|
||||
|
||||
CustomRendererWidget *hotkey_overlay_ptr = nullptr;
|
||||
std::string hotkey_configure_action_name;
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
namespace gsr {
|
||||
class GsrPage : public Page {
|
||||
public:
|
||||
GsrPage();
|
||||
GsrPage(const char *top_text, const char *bottom_text);
|
||||
GsrPage(const GsrPage&) = delete;
|
||||
GsrPage& operator=(const GsrPage&) = delete;
|
||||
|
||||
@@ -42,7 +42,8 @@ namespace gsr {
|
||||
float margin_bottom_scale = 0.0f;
|
||||
float margin_left_scale = 0.0f;
|
||||
float margin_right_scale = 0.0f;
|
||||
mgl::Text label_text;
|
||||
mgl::Text top_text;
|
||||
mgl::Text bottom_text;
|
||||
std::vector<ButtonItem> buttons;
|
||||
};
|
||||
}
|
||||
32
include/gui/Image.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "Widget.hpp"
|
||||
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace gsr {
|
||||
class Image : public Widget {
|
||||
public:
|
||||
enum class ScaleBehavior {
|
||||
SCALE,
|
||||
CLAMP
|
||||
};
|
||||
|
||||
// Set size to {0.0f, 0.0f} for no limit. The image is scaled to the size while keeping its aspect ratio
|
||||
Image(mgl::Texture *texture, mgl::vec2f size, ScaleBehavior scale_behavior);
|
||||
Image(const Image&) = delete;
|
||||
Image& operator=(const Image&) = delete;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
std::function<void(bool inside)> on_mouse_move;
|
||||
private:
|
||||
mgl::Sprite sprite;
|
||||
mgl::vec2f size;
|
||||
ScaleBehavior scale_behavior;
|
||||
};
|
||||
}
|
||||
@@ -21,19 +21,20 @@ namespace gsr {
|
||||
List(Orientation orientation, Alignment content_alignment = Alignment::START);
|
||||
List(const List&) = delete;
|
||||
List& operator=(const List&) = delete;
|
||||
virtual ~List() override;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
//void remove_child_widget(Widget *widget) override;
|
||||
|
||||
void add_widget(std::unique_ptr<Widget> widget);
|
||||
void remove_widget(Widget *widget);
|
||||
void replace_widget(Widget *widget, std::unique_ptr<Widget> new_widget);
|
||||
void clear();
|
||||
// Return true from |callback| to continue
|
||||
void for_each_child_widget(std::function<bool(std::unique_ptr<Widget> &widget)> callback);
|
||||
// Returns nullptr if index is invalid
|
||||
Widget* get_child_widget_by_index(size_t index) const;
|
||||
size_t get_num_children() const;
|
||||
|
||||
void set_spacing(float spacing);
|
||||
|
||||
|
||||
@@ -10,13 +10,11 @@ namespace gsr {
|
||||
Page() = default;
|
||||
Page(const Page&) = delete;
|
||||
Page& operator=(const Page&) = delete;
|
||||
virtual ~Page() = default;
|
||||
virtual ~Page() override;
|
||||
|
||||
virtual void on_navigate_to_page() {}
|
||||
virtual void on_navigate_away_from_page() {}
|
||||
|
||||
//void remove_child_widget(Widget *widget) override;
|
||||
|
||||
virtual void add_widget(std::unique_ptr<Widget> widget);
|
||||
protected:
|
||||
SafeVector<std::unique_ptr<Widget>> widgets;
|
||||
|
||||
@@ -23,7 +23,8 @@ namespace gsr {
|
||||
|
||||
void add_item(const std::string &text, const std::string &id);
|
||||
void set_selected_item(const std::string &id, bool trigger_event = true, bool trigger_event_even_if_selection_not_changed = true);
|
||||
const std::string get_selected_id() const;
|
||||
const std::string& get_selected_id() const;
|
||||
const std::string& get_selected_text() const;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
|
||||
91
include/gui/ScreenshotSettingsPage.hpp
Normal file
@@ -0,0 +1,91 @@
|
||||
#pragma once
|
||||
|
||||
#include "StaticPage.hpp"
|
||||
#include "List.hpp"
|
||||
#include "ComboBox.hpp"
|
||||
#include "Entry.hpp"
|
||||
#include "CheckBox.hpp"
|
||||
#include "../GsrInfo.hpp"
|
||||
#include "../Config.hpp"
|
||||
|
||||
namespace gsr {
|
||||
class PageStack;
|
||||
class GsrPage;
|
||||
class ScrollablePage;
|
||||
class Button;
|
||||
|
||||
class ScreenshotSettingsPage : public StaticPage {
|
||||
public:
|
||||
ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title);
|
||||
ScreenshotSettingsPage(const ScreenshotSettingsPage&) = delete;
|
||||
ScreenshotSettingsPage& operator=(const ScreenshotSettingsPage&) = delete;
|
||||
|
||||
void load();
|
||||
void save();
|
||||
void on_navigate_away_from_page() override;
|
||||
|
||||
std::function<void()> on_config_changed;
|
||||
private:
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
std::unique_ptr<Entry> create_image_width_entry();
|
||||
std::unique_ptr<Entry> create_image_height_entry();
|
||||
std::unique_ptr<List> create_image_resolution();
|
||||
std::unique_ptr<List> create_image_resolution_section();
|
||||
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
|
||||
std::unique_ptr<List> create_restore_portal_session_section();
|
||||
std::unique_ptr<Widget> create_change_image_resolution_section();
|
||||
std::unique_ptr<Widget> create_capture_target_section();
|
||||
std::unique_ptr<List> create_image_quality_section();
|
||||
std::unique_ptr<Widget> create_record_cursor_section();
|
||||
std::unique_ptr<Widget> create_image_section();
|
||||
std::unique_ptr<List> create_save_directory(const char *label);
|
||||
std::unique_ptr<ComboBox> create_image_format_box();
|
||||
std::unique_ptr<List> create_image_format_section();
|
||||
std::unique_ptr<Widget> create_file_info_section();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_in_game_folder();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_to_clipboard();
|
||||
std::unique_ptr<CheckBox> create_save_screenshot_to_disk();
|
||||
std::unique_ptr<Widget> create_notifications();
|
||||
std::unique_ptr<Widget> create_led_indicator();
|
||||
std::unique_ptr<Widget> create_general_section();
|
||||
std::unique_ptr<Widget> create_screenshot_indicator_section();
|
||||
std::unique_ptr<Widget> create_custom_script_screenshot_section();
|
||||
std::unique_ptr<List> create_custom_script_screenshot_entry();
|
||||
std::unique_ptr<List> create_custom_script_screenshot();
|
||||
std::unique_ptr<Widget> create_settings();
|
||||
void add_widgets();
|
||||
|
||||
void save(RecordOptions &record_options);
|
||||
private:
|
||||
Config &config;
|
||||
const GsrInfo *gsr_info = nullptr;
|
||||
SupportedCaptureOptions capture_options;
|
||||
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *image_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
List *color_range_list_ptr = nullptr;
|
||||
Widget *image_format_ptr = nullptr;
|
||||
ComboBox *record_area_box_ptr = nullptr;
|
||||
Entry *image_width_entry_ptr = nullptr;
|
||||
Entry *image_height_entry_ptr = nullptr;
|
||||
CheckBox *record_cursor_checkbox_ptr = nullptr;
|
||||
CheckBox *restore_portal_session_checkbox_ptr = nullptr;
|
||||
CheckBox *change_image_resolution_checkbox_ptr = nullptr;
|
||||
ComboBox *image_quality_box_ptr = nullptr;
|
||||
ComboBox *image_format_box_ptr = nullptr;
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
|
||||
CheckBox *save_screenshot_to_clipboard_checkbox_ptr = nullptr;
|
||||
CheckBox *save_screenshot_to_disk_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
Entry *create_custom_script_screenshot_entry_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
|
||||
bool supports_window_title = false;
|
||||
};
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace gsr {
|
||||
ScrollablePage(mgl::vec2f size);
|
||||
ScrollablePage(const ScrollablePage&) = delete;
|
||||
ScrollablePage& operator=(const ScrollablePage&) = delete;
|
||||
virtual ~ScrollablePage() override;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
@@ -18,6 +18,20 @@ namespace gsr {
|
||||
class ScrollablePage;
|
||||
class Label;
|
||||
class LineSeparator;
|
||||
class Subsection;
|
||||
|
||||
enum class AudioDeviceType {
|
||||
OUTPUT,
|
||||
INPUT
|
||||
};
|
||||
|
||||
enum class WebcamBoxResizeCorner {
|
||||
NONE,
|
||||
//TOP_LEFT,
|
||||
//TOP_RIGHT,
|
||||
//BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT
|
||||
};
|
||||
|
||||
class SettingsPage : public StaticPage {
|
||||
public:
|
||||
@@ -27,7 +41,7 @@ namespace gsr {
|
||||
STREAM
|
||||
};
|
||||
|
||||
SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
|
||||
SettingsPage(Type type, const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title, bool supports_window_fullscreen_state);
|
||||
SettingsPage(const SettingsPage&) = delete;
|
||||
SettingsPage& operator=(const SettingsPage&) = delete;
|
||||
|
||||
@@ -40,7 +54,6 @@ namespace gsr {
|
||||
std::unique_ptr<RadioButton> create_view_radio_button();
|
||||
std::unique_ptr<ComboBox> create_record_area_box();
|
||||
std::unique_ptr<Widget> create_record_area();
|
||||
std::unique_ptr<List> create_select_window();
|
||||
std::unique_ptr<Entry> create_area_width_entry();
|
||||
std::unique_ptr<Entry> create_area_height_entry();
|
||||
std::unique_ptr<List> create_area_size();
|
||||
@@ -52,21 +65,33 @@ namespace gsr {
|
||||
std::unique_ptr<CheckBox> create_restore_portal_session_checkbox();
|
||||
std::unique_ptr<List> create_restore_portal_session_section();
|
||||
std::unique_ptr<Widget> create_change_video_resolution_section();
|
||||
std::unique_ptr<Widget> create_capture_target();
|
||||
std::unique_ptr<ComboBox> create_audio_device_selection_combobox();
|
||||
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_device_list_ptr);
|
||||
std::unique_ptr<List> create_audio_device();
|
||||
std::unique_ptr<Button> create_add_audio_device_button();
|
||||
std::unique_ptr<ComboBox> create_application_audio_selection_combobox();
|
||||
std::unique_ptr<List> create_application_audio();
|
||||
std::unique_ptr<List> create_custom_application_audio();
|
||||
std::unique_ptr<Button> create_add_application_audio_button();
|
||||
std::unique_ptr<Button> create_add_custom_application_audio_button();
|
||||
std::unique_ptr<List> create_add_audio_buttons();
|
||||
std::unique_ptr<List> create_audio_track_track_section();
|
||||
std::unique_ptr<CheckBox> create_merge_audio_tracks_checkbox();
|
||||
std::unique_ptr<Widget> create_capture_target_section();
|
||||
std::unique_ptr<List> create_webcam_sources();
|
||||
std::unique_ptr<List> create_webcam_video_setups();
|
||||
std::unique_ptr<List> create_webcam_video_format();
|
||||
std::unique_ptr<List> create_webcam_video_setup_list();
|
||||
std::unique_ptr<Widget> create_webcam_location_widget();
|
||||
std::unique_ptr<CheckBox> create_flip_camera_checkbox();
|
||||
std::unique_ptr<List> create_webcam_body();
|
||||
std::unique_ptr<Widget> create_webcam_section();
|
||||
std::unique_ptr<ComboBox> create_audio_device_selection_combobox(AudioDeviceType device_type);
|
||||
std::unique_ptr<Button> create_remove_audio_device_button(List *audio_input_list_ptr, List *audio_device_list_ptr);
|
||||
std::unique_ptr<List> create_audio_device(AudioDeviceType device_type, List *audio_input_list_ptr);
|
||||
std::unique_ptr<Button> create_add_audio_track_button();
|
||||
void update_application_audio_warning_visibility();
|
||||
std::unique_ptr<Button> create_add_audio_output_device_button(List *audio_input_list_ptr);
|
||||
std::unique_ptr<Button> create_add_audio_input_device_button(List *audio_input_list_ptr);
|
||||
std::unique_ptr<ComboBox> create_application_audio_selection_combobox(List *application_audio_row);
|
||||
std::unique_ptr<List> create_application_audio(List *audio_input_list_ptr);
|
||||
std::unique_ptr<List> create_custom_application_audio(List *audio_input_list_ptr);
|
||||
std::unique_ptr<Button> create_add_application_audio_button(List *audio_input_list_ptr);
|
||||
std::unique_ptr<List> create_add_audio_buttons(List *audio_input_list_ptr);
|
||||
std::unique_ptr<List> create_audio_input_section();
|
||||
std::unique_ptr<CheckBox> create_application_audio_invert_checkbox();
|
||||
std::unique_ptr<Widget> create_audio_track_section();
|
||||
std::unique_ptr<Widget> create_application_audio_warning();
|
||||
std::unique_ptr<List> create_audio_track_title_and_remove(Subsection *audio_track_subsection, const char *title);
|
||||
std::unique_ptr<Subsection> create_audio_track_section(Widget *parent_widget);
|
||||
std::unique_ptr<List> create_audio_track_section_list();
|
||||
std::unique_ptr<Widget> create_audio_section();
|
||||
std::unique_ptr<List> create_video_quality_box();
|
||||
std::unique_ptr<List> create_video_bitrate_entry();
|
||||
@@ -93,24 +118,33 @@ namespace gsr {
|
||||
std::unique_ptr<List> create_save_directory(const char *label);
|
||||
std::unique_ptr<ComboBox> create_container_box();
|
||||
std::unique_ptr<List> create_container_section();
|
||||
std::unique_ptr<Entry> create_replay_time_entry();
|
||||
std::unique_ptr<List> create_replay_time_entry();
|
||||
std::unique_ptr<List> create_replay_time();
|
||||
std::unique_ptr<List> create_replay_storage();
|
||||
std::unique_ptr<RadioButton> create_start_replay_automatically();
|
||||
std::unique_ptr<CheckBox> create_save_replay_in_game_folder();
|
||||
std::unique_ptr<CheckBox> create_restart_replay_on_save();
|
||||
std::unique_ptr<Label> create_estimated_replay_file_size();
|
||||
void update_estimated_replay_file_size();
|
||||
void update_estimated_replay_file_size(const std::string &replay_storage_type);
|
||||
void update_replay_time_text();
|
||||
std::unique_ptr<CheckBox> create_save_recording_in_game_folder();
|
||||
std::unique_ptr<Label> create_estimated_record_file_size();
|
||||
void update_estimated_record_file_size();
|
||||
std::unique_ptr<CheckBox> create_led_indicator(const char *type);
|
||||
std::unique_ptr<CheckBox> create_notifications(const char *type);
|
||||
std::unique_ptr<List> create_indicator(const char *type);
|
||||
std::unique_ptr<Widget> create_low_power_mode();
|
||||
void add_replay_widgets();
|
||||
void add_record_widgets();
|
||||
|
||||
std::unique_ptr<ComboBox> create_streaming_service_box();
|
||||
std::unique_ptr<List> create_streaming_service_section();
|
||||
std::unique_ptr<List> create_stream_key_section();
|
||||
std::unique_ptr<List> create_stream_url_section();
|
||||
std::unique_ptr<List> create_stream_custom_url();
|
||||
std::unique_ptr<List> create_stream_custom_key();
|
||||
std::unique_ptr<List> create_stream_custom_section();
|
||||
std::unique_ptr<ComboBox> create_stream_container_box();
|
||||
std::unique_ptr<List> create_stream_container_section();
|
||||
std::unique_ptr<List> create_stream_container();
|
||||
void add_stream_widgets();
|
||||
|
||||
void load_audio_tracks(const RecordOptions &record_options);
|
||||
@@ -123,6 +157,10 @@ namespace gsr {
|
||||
void save_replay();
|
||||
void save_record();
|
||||
void save_stream();
|
||||
|
||||
void view_changed(bool advanced_view);
|
||||
|
||||
RecordOptions& get_current_record_options();
|
||||
private:
|
||||
Type type;
|
||||
Config &config;
|
||||
@@ -134,7 +172,6 @@ namespace gsr {
|
||||
GsrPage *content_page_ptr = nullptr;
|
||||
ScrollablePage *settings_scrollable_page_ptr = nullptr;
|
||||
List *settings_list_ptr = nullptr;
|
||||
List *select_window_list_ptr = nullptr;
|
||||
List *area_size_list_ptr = nullptr;
|
||||
List *video_resolution_list_ptr = nullptr;
|
||||
List *restore_portal_session_list_ptr = nullptr;
|
||||
@@ -150,11 +187,6 @@ namespace gsr {
|
||||
Entry *framerate_entry_ptr = nullptr;
|
||||
Entry *video_bitrate_entry_ptr = nullptr;
|
||||
List *video_bitrate_list_ptr = nullptr;
|
||||
List *audio_track_list_ptr = nullptr;
|
||||
Button *add_application_audio_button_ptr = nullptr;
|
||||
Button *add_custom_application_audio_button_ptr = nullptr;
|
||||
CheckBox *merge_audio_tracks_checkbox_ptr = nullptr;
|
||||
CheckBox *application_audio_invert_checkbox_ptr = nullptr;
|
||||
CheckBox *change_video_resolution_checkbox_ptr = nullptr;
|
||||
ComboBox *color_range_box_ptr = nullptr;
|
||||
ComboBox *video_quality_box_ptr = nullptr;
|
||||
@@ -167,25 +199,56 @@ namespace gsr {
|
||||
ComboBox *container_box_ptr = nullptr;
|
||||
ComboBox *streaming_service_box_ptr = nullptr;
|
||||
List *stream_key_list_ptr = nullptr;
|
||||
List *stream_url_list_ptr = nullptr;
|
||||
List *container_list_ptr = nullptr;
|
||||
List *custom_stream_list_ptr = nullptr;
|
||||
CheckBox *save_replay_in_game_folder_ptr = nullptr;
|
||||
CheckBox *restart_replay_on_save = nullptr;
|
||||
Label *estimated_file_size_ptr = nullptr;
|
||||
CheckBox *show_replay_started_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_replay_stopped_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_replay_saved_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *save_recording_in_game_folder_ptr = nullptr;
|
||||
CheckBox *show_recording_started_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_video_saved_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_streaming_started_notification_checkbox_ptr = nullptr;
|
||||
CheckBox *show_streaming_stopped_notification_checkbox_ptr = nullptr;
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
Entry *twitch_stream_key_entry_ptr = nullptr;
|
||||
Entry *youtube_stream_key_entry_ptr = nullptr;
|
||||
Entry *rumble_stream_key_entry_ptr = nullptr;
|
||||
Entry *kick_stream_url_entry_ptr = nullptr;
|
||||
Entry *kick_stream_key_entry_ptr = nullptr;
|
||||
Entry *stream_url_entry_ptr = nullptr;
|
||||
Entry *stream_key_entry_ptr = nullptr;
|
||||
Entry *replay_time_entry_ptr = nullptr;
|
||||
RadioButton *replay_storage_button_ptr = nullptr;
|
||||
Label *replay_time_label_ptr = nullptr;
|
||||
RadioButton *turn_on_replay_automatically_mode_ptr = nullptr;
|
||||
Subsection *audio_section_ptr = nullptr;
|
||||
List *audio_track_section_list_ptr = nullptr;
|
||||
CheckBox *led_indicator_checkbox_ptr = nullptr;
|
||||
CheckBox *show_notification_checkbox_ptr = nullptr;
|
||||
ComboBox *webcam_sources_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_setup_box_ptr = nullptr;
|
||||
ComboBox *webcam_video_format_box_ptr = nullptr;
|
||||
List *webcam_body_list_ptr = nullptr;
|
||||
CheckBox *flip_camera_horizontally_checkbox_ptr = nullptr;
|
||||
CheckBox *low_power_mode_checkbox_ptr = nullptr;
|
||||
|
||||
PageStack *page_stack = nullptr;
|
||||
|
||||
mgl::vec2f webcam_box_pos;
|
||||
mgl::vec2f webcam_box_size;
|
||||
|
||||
mgl::vec2f webcam_box_drawn_pos;
|
||||
mgl::vec2f webcam_box_drawn_size;
|
||||
mgl::vec2f webcam_box_grab_offset;
|
||||
|
||||
mgl::vec2f camera_screen_size;
|
||||
mgl::vec2f screen_inner_size;
|
||||
bool moving_webcam_box = false;
|
||||
|
||||
WebcamBoxResizeCorner webcam_resize_corner = WebcamBoxResizeCorner::NONE;
|
||||
mgl::vec2f webcam_resize_start_pos;
|
||||
mgl::vec2f webcam_box_pos_resize_start;
|
||||
mgl::vec2f webcam_box_size_resize_start;
|
||||
|
||||
std::optional<GsrCamera> selected_camera;
|
||||
std::optional<GsrCameraSetup> selected_camera_setup;
|
||||
|
||||
bool supports_window_title = false;
|
||||
bool supports_window_fullscreen_state = false;
|
||||
};
|
||||
}
|
||||
@@ -11,15 +11,20 @@ namespace gsr {
|
||||
Subsection(const char *title, std::unique_ptr<Widget> inner_widget, mgl::vec2f size);
|
||||
Subsection(const Subsection&) = delete;
|
||||
Subsection& operator=(const Subsection&) = delete;
|
||||
virtual ~Subsection() override;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
mgl::vec2f get_inner_size() override;
|
||||
|
||||
Widget* get_inner_widget();
|
||||
void set_bg_color(mgl::Color color);
|
||||
private:
|
||||
Label label;
|
||||
std::unique_ptr<Widget> inner_widget;
|
||||
mgl::vec2f size;
|
||||
mgl::Color bg_color{25, 30, 34};
|
||||
};
|
||||
}
|
||||
22
include/gui/Tooltip.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "Widget.hpp"
|
||||
#include <mglpp/graphics/Text.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class Tooltip : public Widget {
|
||||
public:
|
||||
Tooltip(mgl::Font *font);
|
||||
Tooltip(const Tooltip&) = delete;
|
||||
Tooltip& operator=(const Tooltip&) = delete;
|
||||
|
||||
bool on_event(mgl::Event &event, mgl::Window &window, mgl::vec2f offset) override;
|
||||
void draw(mgl::Window &window, mgl::vec2f offset) override;
|
||||
|
||||
mgl::vec2f get_size() override;
|
||||
|
||||
void set_text(std::string text);
|
||||
private:
|
||||
mgl::Text label;
|
||||
};
|
||||
}
|
||||
@@ -2,18 +2,22 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
namespace mgl {
|
||||
class Window;
|
||||
}
|
||||
|
||||
namespace gsr {
|
||||
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b);
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max);
|
||||
|
||||
// Inner border
|
||||
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size);
|
||||
double get_frame_delta_seconds();
|
||||
void set_frame_delta_seconds(double frame_delta);
|
||||
mgl::vec2f scale_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
|
||||
mgl::vec2f clamp_keep_aspect_ratio(mgl::vec2f from, mgl::vec2f to);
|
||||
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace mgl {
|
||||
class Event;
|
||||
@@ -31,8 +33,6 @@ namespace gsr {
|
||||
virtual void draw(mgl::Window &window, mgl::vec2f offset) = 0;
|
||||
virtual void set_position(mgl::vec2f position);
|
||||
|
||||
//virtual void remove_child_widget(Widget *widget) { (void)widget; }
|
||||
|
||||
virtual mgl::vec2f get_position() const;
|
||||
virtual mgl::vec2f get_size() = 0;
|
||||
// This can be different from get_size, for example with ScrollablePage this excludes the margins
|
||||
@@ -45,6 +45,13 @@ namespace gsr {
|
||||
Alignment get_vertical_alignment() const;
|
||||
|
||||
void set_visible(bool visible);
|
||||
bool is_visible() const;
|
||||
|
||||
Widget* get_parent_widget();
|
||||
|
||||
void set_tooltip_text(std::string text);
|
||||
const std::string& get_tooltip_text() const;
|
||||
void handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size);
|
||||
|
||||
void *userdata = nullptr;
|
||||
protected:
|
||||
@@ -60,5 +67,13 @@ namespace gsr {
|
||||
Alignment vertical_aligment = Alignment::START;
|
||||
|
||||
bool visible = true;
|
||||
std::string tooltip_text;
|
||||
};
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget);
|
||||
void remove_widgets_to_be_removed();
|
||||
|
||||
void set_current_tooltip(Widget *widget);
|
||||
void remove_as_current_tooltip(Widget *widget);
|
||||
void draw_tooltip(mgl::Window &window);
|
||||
}
|
||||
73
meson.build
@@ -1,4 +1,6 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.0.5', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.10.9', default_options : ['warning_level=2', 'cpp_std=c++17'], subproject_dir : 'depends')
|
||||
|
||||
add_project_arguments('-D_FILE_OFFSET_BITS=64', language : ['c', 'cpp'])
|
||||
|
||||
if get_option('buildtype') == 'debug'
|
||||
add_project_arguments('-g3', language : ['c', 'cpp'])
|
||||
@@ -23,31 +25,54 @@ src = [
|
||||
'src/gui/Utils.cpp',
|
||||
'src/gui/DropdownButton.cpp',
|
||||
'src/gui/Label.cpp',
|
||||
'src/gui/Image.cpp',
|
||||
'src/gui/LineSeparator.cpp',
|
||||
'src/gui/CustomRendererWidget.cpp',
|
||||
'src/gui/FileChooser.cpp',
|
||||
'src/gui/SettingsPage.cpp',
|
||||
'src/gui/ScreenshotSettingsPage.cpp',
|
||||
'src/gui/GlobalSettingsPage.cpp',
|
||||
'src/gui/GsrPage.cpp',
|
||||
'src/gui/Subsection.cpp',
|
||||
'src/gui/Tooltip.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysX11.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysLinux.cpp',
|
||||
'src/GlobalHotkeys/GlobalHotkeysJoystick.cpp',
|
||||
'src/CursorTracker/CursorTrackerX11.cpp',
|
||||
'src/CursorTracker/CursorTrackerWayland.cpp',
|
||||
'src/Utils.cpp',
|
||||
'src/HyprlandWorkaround.cpp',
|
||||
'src/KwinWorkaround.cpp',
|
||||
'src/WindowUtils.cpp',
|
||||
'src/RegionSelector.cpp',
|
||||
'src/Config.cpp',
|
||||
'src/GsrInfo.cpp',
|
||||
'src/Process.cpp',
|
||||
'src/Overlay.cpp',
|
||||
'src/GlobalHotkeysX11.cpp',
|
||||
'src/GlobalHotkeysLinux.cpp',
|
||||
'src/AudioPlayer.cpp',
|
||||
'src/Hotplug.cpp',
|
||||
'src/ClipboardFile.cpp',
|
||||
'src/LedIndicator.cpp',
|
||||
'src/Rpc.cpp',
|
||||
'src/Translation.cpp',
|
||||
'src/main.cpp',
|
||||
]
|
||||
|
||||
subdir('protocol')
|
||||
src += protocol_src
|
||||
|
||||
mglpp_proj = subproject('mglpp')
|
||||
mglpp_dep = mglpp_proj.get_variable('mglpp_dep')
|
||||
|
||||
prefix = get_option('prefix')
|
||||
datadir = get_option('datadir')
|
||||
gsr_ui_resources_path = join_paths(prefix, datadir, 'gsr-ui')
|
||||
icons_path = join_paths(prefix, datadir, 'icons')
|
||||
|
||||
add_project_arguments('-DGSR_UI_VERSION="' + meson.project_version() + '"', language: ['c', 'cpp'])
|
||||
add_project_arguments('-DGSR_FLATPAK_VERSION="5.12.2"', language: ['c', 'cpp'])
|
||||
|
||||
add_project_arguments('-DKWIN_HELPER_SCRIPT_PATH="' + gsr_ui_resources_path + '/gsrkwinhelper.js"', language: ['c', 'cpp'])
|
||||
|
||||
executable(
|
||||
meson.project_name(),
|
||||
@@ -55,10 +80,16 @@ executable(
|
||||
install : true,
|
||||
dependencies : [
|
||||
mglpp_dep,
|
||||
dependency('threads'),
|
||||
dependency('xcomposite'),
|
||||
dependency('xfixes'),
|
||||
dependency('xext'),
|
||||
dependency('xi'),
|
||||
dependency('xcursor'),
|
||||
dependency('xrandr'),
|
||||
dependency('libpulse-simple'),
|
||||
dependency('libdrm'),
|
||||
dependency('wayland-client'),
|
||||
],
|
||||
cpp_args : '-DGSR_UI_RESOURCES_PATH="' + gsr_ui_resources_path + '"',
|
||||
)
|
||||
@@ -68,6 +99,8 @@ executable(
|
||||
[
|
||||
'tools/gsr-global-hotkeys/hotplug.c',
|
||||
'tools/gsr-global-hotkeys/keyboard_event.c',
|
||||
'tools/gsr-global-hotkeys/keys.c',
|
||||
'tools/gsr-global-hotkeys/leds.c',
|
||||
'tools/gsr-global-hotkeys/main.c'
|
||||
],
|
||||
c_args : '-fstack-protector-all',
|
||||
@@ -82,8 +115,42 @@ executable(
|
||||
install : true
|
||||
)
|
||||
|
||||
executable(
|
||||
'gsr-kwin-helper',
|
||||
[
|
||||
'tools/gsr-kwin-helper/main.cpp'
|
||||
],
|
||||
install : true,
|
||||
dependencies: [
|
||||
dependency('dbus-1'),
|
||||
]
|
||||
)
|
||||
|
||||
install_data(
|
||||
'tools/gsr-kwin-helper/gsrkwinhelper.js',
|
||||
install_dir: gsr_ui_resources_path,
|
||||
install_mode: 'rwxr-xr-x'
|
||||
)
|
||||
|
||||
executable(
|
||||
'gsr-hyprland-helper',
|
||||
[
|
||||
'tools/gsr-hyprland-helper/main.c'
|
||||
],
|
||||
install : true
|
||||
)
|
||||
|
||||
install_subdir('images', install_dir : gsr_ui_resources_path)
|
||||
install_subdir('fonts', install_dir : gsr_ui_resources_path)
|
||||
install_subdir('translations', install_dir : gsr_ui_resources_path)
|
||||
|
||||
if get_option('desktop-files') == true
|
||||
install_data(files('gpu-screen-recorder.desktop'), install_dir : join_paths(prefix, datadir, 'applications'))
|
||||
install_subdir('icons/hicolor', install_dir : icons_path)
|
||||
|
||||
gnome = import('gnome')
|
||||
gnome.post_install(update_desktop_database : true)
|
||||
endif
|
||||
|
||||
if get_option('systemd') == true
|
||||
install_data(files('extra/gpu-screen-recorder-ui.service'), install_dir : 'lib/systemd/user')
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
option('systemd', type : 'boolean', value : true, description : 'Install systemd service file')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('capabilities', type : 'boolean', value : true, description : 'Set binary setuid capability on gsr-global-hotkeys binary to allow global hotkeys')
|
||||
option('desktop-files', type : 'boolean', value : true, description : 'Install desktop files')
|
||||
10
project.conf
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.0.5"
|
||||
version = "1.10.9"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
@@ -10,8 +10,16 @@ version = "c++17"
|
||||
[config]
|
||||
ignore_dirs = ["build", "tools"]
|
||||
|
||||
[define]
|
||||
_FILE_OFFSET_BITS = "64"
|
||||
|
||||
[dependencies]
|
||||
xcomposite = ">=0"
|
||||
xfixes = ">=0"
|
||||
xext = ">=0"
|
||||
xi = ">=0"
|
||||
xcursor = ">=1"
|
||||
xrandr = ">=0.5"
|
||||
libpulse-simple = ">=0"
|
||||
libdrm = ">=2"
|
||||
wayland-client = ">=1"
|
||||
|
||||
25
protocol/meson.build
Normal file
@@ -0,0 +1,25 @@
|
||||
wayland_scanner = dependency('wayland-scanner', native: true)
|
||||
wayland_scanner_path = wayland_scanner.get_variable(pkgconfig: 'wayland_scanner')
|
||||
wayland_scanner_prog = find_program(wayland_scanner_path, native: true)
|
||||
|
||||
wayland_scanner_code = generator(
|
||||
wayland_scanner_prog,
|
||||
output: '@BASENAME@-protocol.c',
|
||||
arguments: ['private-code', '@INPUT@', '@OUTPUT@'],
|
||||
)
|
||||
|
||||
wayland_scanner_client = generator(
|
||||
wayland_scanner_prog,
|
||||
output: '@BASENAME@-client-protocol.h',
|
||||
arguments: ['client-header', '@INPUT@', '@OUTPUT@'],
|
||||
)
|
||||
|
||||
protocols = [
|
||||
'xdg-output-unstable-v1.xml',
|
||||
]
|
||||
|
||||
protocol_src = []
|
||||
foreach xml : protocols
|
||||
protocol_src += wayland_scanner_code.process(xml)
|
||||
protocol_src += wayland_scanner_client.process(xml)
|
||||
endforeach
|
||||
222
protocol/xdg-output-unstable-v1.xml
Normal file
@@ -0,0 +1,222 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="xdg_output_unstable_v1">
|
||||
|
||||
<copyright>
|
||||
Copyright © 2017 Red Hat Inc.
|
||||
|
||||
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 (including the next
|
||||
paragraph) 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 AUTHORS OR COPYRIGHT HOLDERS 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.
|
||||
</copyright>
|
||||
|
||||
<description summary="Protocol to describe output regions">
|
||||
This protocol aims at describing outputs in a way which is more in line
|
||||
with the concept of an output on desktop oriented systems.
|
||||
|
||||
Some information are more specific to the concept of an output for
|
||||
a desktop oriented system and may not make sense in other applications,
|
||||
such as IVI systems for example.
|
||||
|
||||
Typically, the global compositor space on a desktop system is made of
|
||||
a contiguous or overlapping set of rectangular regions.
|
||||
|
||||
The logical_position and logical_size events defined in this protocol
|
||||
might provide information identical to their counterparts already
|
||||
available from wl_output, in which case the information provided by this
|
||||
protocol should be preferred to their equivalent in wl_output. The goal is
|
||||
to move the desktop specific concepts (such as output location within the
|
||||
global compositor space, etc.) out of the core wl_output protocol.
|
||||
|
||||
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="zxdg_output_manager_v1" version="3">
|
||||
<description summary="manage xdg_output objects">
|
||||
A global factory interface for xdg_output objects.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the xdg_output_manager object">
|
||||
Using this request a client can tell the server that it is not
|
||||
going to use the xdg_output_manager object anymore.
|
||||
|
||||
Any objects already created through this instance are not affected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_xdg_output">
|
||||
<description summary="create an xdg output from a wl_output">
|
||||
This creates a new xdg_output object for the given wl_output.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="zxdg_output_v1"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="zxdg_output_v1" version="3">
|
||||
<description summary="compositor logical output region">
|
||||
An xdg_output describes part of the compositor geometry.
|
||||
|
||||
This typically corresponds to a monitor that displays part of the
|
||||
compositor space.
|
||||
|
||||
For objects version 3 onwards, after all xdg_output properties have been
|
||||
sent (when the object is created and when properties are updated), a
|
||||
wl_output.done event is sent. This allows changes to the output
|
||||
properties to be seen as atomic, even if they happen via multiple events.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the xdg_output object">
|
||||
Using this request a client can tell the server that it is not
|
||||
going to use the xdg_output object anymore.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="logical_position">
|
||||
<description summary="position of the output within the global compositor space">
|
||||
The position event describes the location of the wl_output within
|
||||
the global compositor space.
|
||||
|
||||
The logical_position event is sent after creating an xdg_output
|
||||
(see xdg_output_manager.get_xdg_output) and whenever the location
|
||||
of the output changes within the global compositor space.
|
||||
</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="logical_size">
|
||||
<description summary="size of the output in the global compositor space">
|
||||
The logical_size event describes the size of the output in the
|
||||
global compositor space.
|
||||
|
||||
Most regular Wayland clients should not pay attention to the
|
||||
logical size and would rather rely on xdg_shell interfaces.
|
||||
|
||||
Some clients such as Xwayland, however, need this to configure
|
||||
their surfaces in the global compositor space as the compositor
|
||||
may apply a different scale from what is advertised by the output
|
||||
scaling property (to achieve fractional scaling, for example).
|
||||
|
||||
For example, for a wl_output mode 3840×2160 and a scale factor 2:
|
||||
|
||||
- A compositor not scaling the monitor viewport in its compositing space
|
||||
will advertise a logical size of 3840×2160,
|
||||
|
||||
- A compositor scaling the monitor viewport with scale factor 2 will
|
||||
advertise a logical size of 1920×1080,
|
||||
|
||||
- A compositor scaling the monitor viewport using a fractional scale of
|
||||
1.5 will advertise a logical size of 2560×1440.
|
||||
|
||||
For example, for a wl_output mode 1920×1080 and a 90 degree rotation,
|
||||
the compositor will advertise a logical size of 1080x1920.
|
||||
|
||||
The logical_size event is sent after creating an xdg_output
|
||||
(see xdg_output_manager.get_xdg_output) and whenever the logical
|
||||
size of the output changes, either as a result of a change in the
|
||||
applied scale or because of a change in the corresponding output
|
||||
mode(see wl_output.mode) or transform (see wl_output.transform).
|
||||
</description>
|
||||
<arg name="width" type="int"
|
||||
summary="width in global compositor space"/>
|
||||
<arg name="height" type="int"
|
||||
summary="height in global compositor space"/>
|
||||
</event>
|
||||
|
||||
<event name="done" deprecated-since="3">
|
||||
<description summary="all information about the output have been sent">
|
||||
This event is sent after all other properties of an xdg_output
|
||||
have been sent.
|
||||
|
||||
This allows changes to the xdg_output properties to be seen as
|
||||
atomic, even if they happen via multiple events.
|
||||
|
||||
For objects version 3 onwards, this event is deprecated. Compositors
|
||||
are not required to send it anymore and must send wl_output.done
|
||||
instead.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
|
||||
<event name="name" since="2">
|
||||
<description summary="name of this output">
|
||||
Many compositors will assign names to their outputs, show them to the
|
||||
user, allow them to be configured by name, etc. The client may wish to
|
||||
know this name as well to offer the user similar behaviors.
|
||||
|
||||
The naming convention is compositor defined, but limited to
|
||||
alphanumeric characters and dashes (-). Each name is unique among all
|
||||
wl_output globals, but if a wl_output global 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.
|
||||
|
||||
The name event is sent after creating an xdg_output (see
|
||||
xdg_output_manager.get_xdg_output). This event is only sent once per
|
||||
xdg_output, and the name does not change over the lifetime of the
|
||||
wl_output global.
|
||||
|
||||
This event is deprecated, instead clients should use wl_output.name.
|
||||
Compositors must still support this event.
|
||||
</description>
|
||||
<arg name="name" type="string" summary="output name"/>
|
||||
</event>
|
||||
|
||||
<event name="description" since="2">
|
||||
<description summary="human-readable description of this output">
|
||||
Many compositors can produce human-readable descriptions of their
|
||||
outputs. The client may wish to know this description as well, to
|
||||
communicate the user for various purposes.
|
||||
|
||||
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'.
|
||||
|
||||
The description event is sent after creating an xdg_output (see
|
||||
xdg_output_manager.get_xdg_output) and whenever the description
|
||||
changes. The description is optional, and may not be sent at all.
|
||||
|
||||
For objects of version 2 and lower, this event is only sent once per
|
||||
xdg_output, and the description does not change over the lifetime of
|
||||
the wl_output global.
|
||||
|
||||
This event is deprecated, instead clients should use
|
||||
wl_output.description. Compositors must still support this event.
|
||||
</description>
|
||||
<arg name="description" type="string" summary="output description"/>
|
||||
</event>
|
||||
|
||||
</interface>
|
||||
</protocol>
|
||||
86
src/AudioPlayer.cpp
Normal file
@@ -0,0 +1,86 @@
|
||||
#include "../include/AudioPlayer.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <pulse/simple.h>
|
||||
#include <pulse/error.h>
|
||||
|
||||
#define BUFSIZE 4096
|
||||
|
||||
namespace gsr {
|
||||
AudioPlayer::~AudioPlayer() {
|
||||
if(thread.joinable()) {
|
||||
stop_playing_audio = true;
|
||||
thread.join();
|
||||
}
|
||||
|
||||
if(audio_file_fd > 0)
|
||||
close(audio_file_fd);
|
||||
}
|
||||
|
||||
bool AudioPlayer::play(const char *filepath) {
|
||||
if(thread.joinable()) {
|
||||
stop_playing_audio = true;
|
||||
thread.join();
|
||||
}
|
||||
|
||||
stop_playing_audio = false;
|
||||
audio_file_fd = open(filepath, O_RDONLY);
|
||||
if(audio_file_fd == -1)
|
||||
return false;
|
||||
|
||||
thread = std::thread([this]() {
|
||||
pa_sample_spec ss;
|
||||
ss.format = PA_SAMPLE_S16LE;
|
||||
ss.rate = 48000;
|
||||
ss.channels = 2;
|
||||
|
||||
pa_simple *s = NULL;
|
||||
int error;
|
||||
|
||||
/* Create a new playback stream */
|
||||
if(!(s = pa_simple_new(NULL, "gsr-ui-audio-playback", PA_STREAM_PLAYBACK, NULL, "playback", &ss, NULL, NULL, &error))) {
|
||||
fprintf(stderr, __FILE__": pa_simple_new() failed: %s\n", pa_strerror(error));
|
||||
goto finish;
|
||||
}
|
||||
|
||||
uint8_t buf[BUFSIZE];
|
||||
for(;;) {
|
||||
ssize_t r;
|
||||
|
||||
if(stop_playing_audio)
|
||||
goto finish;
|
||||
|
||||
if((r = read(audio_file_fd, buf, sizeof(buf))) <= 0) {
|
||||
if(r == 0) /* EOF */
|
||||
break;
|
||||
|
||||
fprintf(stderr, __FILE__": read() failed: %s\n", strerror(errno));
|
||||
goto finish;
|
||||
}
|
||||
|
||||
if(pa_simple_write(s, buf, (size_t) r, &error) < 0) {
|
||||
fprintf(stderr, __FILE__": pa_simple_write() failed: %s\n", pa_strerror(error));
|
||||
goto finish;
|
||||
}
|
||||
}
|
||||
|
||||
if(pa_simple_drain(s, &error) < 0) {
|
||||
fprintf(stderr, __FILE__": pa_simple_drain() failed: %s\n", pa_strerror(error));
|
||||
goto finish;
|
||||
}
|
||||
|
||||
finish:
|
||||
if(s)
|
||||
pa_simple_free(s);
|
||||
|
||||
close(audio_file_fd);
|
||||
audio_file_fd = -1;
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
316
src/ClipboardFile.cpp
Normal file
@@ -0,0 +1,316 @@
|
||||
#include "../include/ClipboardFile.hpp"
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
#include <poll.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <X11/Xatom.h>
|
||||
|
||||
#define FORMAT_I64 "%" PRIi64
|
||||
#define FORMAT_U64 "%" PRIu64
|
||||
|
||||
namespace gsr {
|
||||
ClipboardFile::ClipboardFile() {
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to connect to the X11 server\n");
|
||||
return;
|
||||
}
|
||||
|
||||
clipboard_window = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), 0, 0, 8, 8, 0, 0, 0);
|
||||
if(!clipboard_window) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile: failed to create clipboard window\n");
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
incr_atom = XInternAtom(dpy, "INCR", False);
|
||||
targets_atom = XInternAtom(dpy, "TARGETS", False);
|
||||
clipboard_atom = XInternAtom(dpy, "CLIPBOARD", False);
|
||||
image_jpg_atom = XInternAtom(dpy, "image/jpg", False);
|
||||
image_jpeg_atom = XInternAtom(dpy, "image/jpeg", False);
|
||||
image_png_atom = XInternAtom(dpy, "image/png", False);
|
||||
|
||||
event_thread = std::thread([&]() {
|
||||
pollfd poll_fds[1];
|
||||
poll_fds[0].fd = ConnectionNumber(dpy);
|
||||
poll_fds[0].events = POLLIN;
|
||||
poll_fds[0].revents = 0;
|
||||
|
||||
XEvent xev;
|
||||
while(running) {
|
||||
poll_fds[0].revents = 0;
|
||||
poll(poll_fds, 1, 100);
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
switch(xev.type) {
|
||||
case SelectionClear: {
|
||||
bool clear_current_file = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
should_clear_selection = true;
|
||||
if(clipboard_copies.empty()) {
|
||||
should_clear_selection = false;
|
||||
clear_current_file = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(clear_current_file)
|
||||
set_current_file("", file_type);
|
||||
break;
|
||||
}
|
||||
case SelectionRequest:
|
||||
send_clipboard_start(&xev.xselectionrequest);
|
||||
break;
|
||||
case PropertyNotify: {
|
||||
if(xev.xproperty.state == PropertyDelete) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xev.xproperty.window);
|
||||
if(!clipboard_copy || xev.xproperty.atom != clipboard_copy->property)
|
||||
return;
|
||||
|
||||
XSelectionRequestEvent xselectionrequest;
|
||||
xselectionrequest.display = xev.xproperty.display;;
|
||||
xselectionrequest.requestor = xev.xproperty.window;
|
||||
xselectionrequest.selection = clipboard_atom;
|
||||
xselectionrequest.target = clipboard_copy->requestor_target;
|
||||
xselectionrequest.property = clipboard_copy->property;
|
||||
xselectionrequest.time = xev.xproperty.time;
|
||||
transfer_clipboard_data(&xselectionrequest, clipboard_copy);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ClipboardFile::~ClipboardFile() {
|
||||
running = false;
|
||||
if(event_thread.joinable())
|
||||
event_thread.join();
|
||||
|
||||
if(file_fd > 0)
|
||||
close(file_fd);
|
||||
|
||||
if(dpy) {
|
||||
XDestroyWindow(dpy, clipboard_window);
|
||||
XCloseDisplay(dpy);
|
||||
}
|
||||
}
|
||||
|
||||
bool ClipboardFile::file_type_matches_request_atom(FileType file_type, Atom request_atom) {
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
return request_atom == image_jpg_atom || request_atom == image_jpeg_atom;
|
||||
case FileType::PNG:
|
||||
return request_atom == image_png_atom;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* ClipboardFile::file_type_clipboard_get_name(Atom request_atom) {
|
||||
if(request_atom == image_jpg_atom)
|
||||
return "image/jpg";
|
||||
else if(request_atom == image_jpeg_atom)
|
||||
return "image/jpeg";
|
||||
else if(request_atom == image_png_atom)
|
||||
return "image/png";
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
const char* ClipboardFile::file_type_get_name(FileType file_type) {
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
return "image/jpeg";
|
||||
case FileType::PNG:
|
||||
return "image/png";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
void ClipboardFile::send_clipboard_start(XSelectionRequestEvent *xselectionrequest) {
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
if(file_fd <= 0) {
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to get clipboard from us but we don't have any clipboard file open\n", (int64_t)xselectionrequest->requestor);
|
||||
return;
|
||||
}
|
||||
|
||||
if(xselectionrequest->selection != clipboard_atom) {
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to non-clipboard selection from us\n", (int64_t)xselectionrequest->requestor);
|
||||
return;
|
||||
}
|
||||
|
||||
XSelectionEvent selection_event;
|
||||
selection_event.type = SelectionNotify;
|
||||
selection_event.display = xselectionrequest->display;
|
||||
selection_event.requestor = xselectionrequest->requestor;
|
||||
selection_event.selection = xselectionrequest->selection;
|
||||
selection_event.property = xselectionrequest->property;
|
||||
selection_event.time = xselectionrequest->time;
|
||||
selection_event.target = xselectionrequest->target;
|
||||
|
||||
if(xselectionrequest->target == targets_atom) {
|
||||
int num_targets = 1;
|
||||
Atom targets[4];
|
||||
targets[0] = targets_atom;
|
||||
|
||||
switch(file_type) {
|
||||
case FileType::JPG:
|
||||
num_targets = 4;
|
||||
targets[1] = image_jpg_atom;
|
||||
targets[2] = image_jpeg_atom;
|
||||
targets[3] = image_png_atom;
|
||||
break;
|
||||
case FileType::PNG:
|
||||
num_targets = 2;
|
||||
targets[1] = image_png_atom;
|
||||
targets[2] = image_jpg_atom;
|
||||
targets[3] = image_jpeg_atom;
|
||||
break;
|
||||
}
|
||||
|
||||
XChangeProperty(dpy, selection_event.requestor, selection_event.property, XA_ATOM, 32, PropModeReplace, (unsigned char*)targets, num_targets);
|
||||
} else if(xselectionrequest->target == image_jpg_atom || xselectionrequest->target == image_jpeg_atom || xselectionrequest->target == image_png_atom) {
|
||||
// TODO: Convert image to requested image type. Right now sending a jpg file when a png file is requested works ok in browsers (discord and element)
|
||||
if(!file_type_matches_request_atom(file_type, xselectionrequest->target)) {
|
||||
const char *expected_file_type = file_type_get_name(file_type);
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, but %s was expected. Ignoring requestor and sending as %s\n", (int64_t)xselectionrequest->requestor, file_type_clipboard_get_name(xselectionrequest->target), expected_file_type, expected_file_type);
|
||||
//return;
|
||||
}
|
||||
|
||||
ClipboardCopy *clipboard_copy = get_clipboard_copy_by_requestor(xselectionrequest->requestor);
|
||||
if(!clipboard_copy) {
|
||||
clipboard_copies.push_back({ xselectionrequest->requestor, 0 });
|
||||
clipboard_copy = &clipboard_copies.back();
|
||||
}
|
||||
|
||||
*clipboard_copy = { xselectionrequest->requestor, 0 };
|
||||
clipboard_copy->property = selection_event.property;
|
||||
clipboard_copy->requestor_target = selection_event.target;
|
||||
XSelectInput(dpy, selection_event.requestor, PropertyChangeMask);
|
||||
|
||||
const long lower_bound = std::min((uint64_t)1<<16, file_size);
|
||||
XChangeProperty(dpy, selection_event.requestor, selection_event.property, incr_atom, 32, PropModeReplace, (const unsigned char*)&lower_bound, 1);
|
||||
} else {
|
||||
char *target_clipboard_name = XGetAtomName(dpy, xselectionrequest->target);
|
||||
fprintf(stderr, "gsr ui: warning: ClipboardFile::send_clipboard: requestor window " FORMAT_I64 " tried to request clipboard of type %s, expected TARGETS, image/jpg, image/jpeg or image/png\n", (int64_t)xselectionrequest->requestor, target_clipboard_name ? target_clipboard_name : "Unknown");
|
||||
if(target_clipboard_name)
|
||||
XFree(target_clipboard_name);
|
||||
selection_event.property = None;
|
||||
}
|
||||
|
||||
XSendEvent(dpy, selection_event.requestor, False, NoEventMask, (XEvent*)&selection_event);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
void ClipboardFile::transfer_clipboard_data(XSelectionRequestEvent *xselectionrequest, ClipboardCopy *clipboard_copy) {
|
||||
uint8_t file_buffer[1<<16];
|
||||
ssize_t file_bytes_read = 0;
|
||||
|
||||
if(file_fd <= 0)
|
||||
return;
|
||||
|
||||
if(lseek(file_fd, clipboard_copy->file_offset, SEEK_SET) == -1) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::send_clipboard: failed to seek in clipboard file to offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
|
||||
clipboard_copy->file_offset = 0;
|
||||
// TODO: Cancel transfer
|
||||
return;
|
||||
}
|
||||
|
||||
file_bytes_read = read(file_fd, file_buffer, sizeof(file_buffer));
|
||||
if(file_bytes_read < 0) {
|
||||
fprintf(stderr, "gsr ui: error: ClipbaordFile::send_clipboard: failed to read data from offset " FORMAT_U64 " for requestor window " FORMAT_I64 ", error: %s\n", (uint64_t)clipboard_copy->file_offset, (int64_t)xselectionrequest->requestor, strerror(errno));
|
||||
clipboard_copy->file_offset = 0;
|
||||
// TODO: Cancel transfer
|
||||
return;
|
||||
}
|
||||
|
||||
XChangeProperty(dpy, xselectionrequest->requestor, xselectionrequest->property, xselectionrequest->target, 8, PropModeReplace, (const unsigned char*)file_buffer, file_bytes_read);
|
||||
XSendEvent(dpy, xselectionrequest->requestor, False, NoEventMask, (XEvent*)xselectionrequest);
|
||||
XFlush(dpy);
|
||||
|
||||
clipboard_copy->file_offset += file_bytes_read;
|
||||
if(file_bytes_read == 0)
|
||||
remove_clipboard_copy(clipboard_copy->requestor);
|
||||
}
|
||||
|
||||
ClipboardCopy* ClipboardFile::get_clipboard_copy_by_requestor(Window requestor) {
|
||||
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
|
||||
if(clipboard_copy.requestor == requestor)
|
||||
return &clipboard_copy;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void ClipboardFile::remove_clipboard_copy(Window requestor) {
|
||||
for(auto it = clipboard_copies.begin(), end = clipboard_copies.end(); it != end; ++it) {
|
||||
if(it->requestor == requestor) {
|
||||
clipboard_copies.erase(it);
|
||||
XSelectInput(dpy, requestor, 0);
|
||||
|
||||
if(clipboard_copies.empty() && should_clear_selection) {
|
||||
should_clear_selection = false;
|
||||
set_current_file("", file_type);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ClipboardFile::set_current_file(const std::string &filepath, FileType file_type) {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
std::lock_guard<std::mutex> lock(mutex);
|
||||
for(ClipboardCopy &clipboard_copy : clipboard_copies) {
|
||||
XSelectInput(dpy, clipboard_copy.requestor, 0);
|
||||
}
|
||||
clipboard_copies.clear();
|
||||
|
||||
if(XGetSelectionOwner(dpy, clipboard_atom) == clipboard_window) {
|
||||
XSetSelectionOwner(dpy, clipboard_atom, None, CurrentTime);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
if(filepath.empty()) {
|
||||
// TODO: Cancel transfer
|
||||
if(file_fd > 0) {
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
}
|
||||
file_size = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(file_fd > 0) {
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
file_size = 0;
|
||||
}
|
||||
|
||||
file_fd = open(filepath.c_str(), O_RDONLY);
|
||||
if(file_fd <= 0) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to open file %s, error: %s\n", filepath.c_str(), strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
struct stat stat;
|
||||
if(fstat(file_fd, &stat) == -1) {
|
||||
fprintf(stderr, "gsr ui: error: ClipboardFile::set_current_file: failed to get file size for file %s, error: %s\n", filepath.c_str(), strerror(errno));
|
||||
close(file_fd);
|
||||
file_fd = -1;
|
||||
return;
|
||||
}
|
||||
file_size = stat.st_size;
|
||||
this->file_type = file_type;
|
||||
|
||||
XSetSelectionOwner(dpy, clipboard_atom, clipboard_window, CurrentTime);
|
||||
XFlush(dpy);
|
||||
}
|
||||
}
|
||||
319
src/Config.cpp
@@ -1,50 +1,165 @@
|
||||
#include "../include/Config.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
#include "../include/GsrInfo.hpp"
|
||||
#include "../include/GlobalHotkeys/GlobalHotkeys.hpp"
|
||||
#include <variant>
|
||||
#include <limits.h>
|
||||
#include <inttypes.h>
|
||||
#include <libgen.h>
|
||||
#include <iostream>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
#include <mglpp/window/Keyboard.hpp>
|
||||
|
||||
#define FORMAT_I32 "%" PRIi32
|
||||
#define FORMAT_I64 "%" PRIi64
|
||||
#define FORMAT_U32 "%" PRIu32
|
||||
|
||||
#define CONFIG_FILE_VERSION 1
|
||||
|
||||
namespace gsr {
|
||||
static const std::string_view add_audio_track_tag = "[add_audio_track]";
|
||||
|
||||
static std::vector<mgl::Keyboard::Key> hotkey_modifiers_to_mgl_keys(uint32_t modifiers) {
|
||||
std::vector<mgl::Keyboard::Key> result;
|
||||
if(modifiers & HOTKEY_MOD_LCTRL)
|
||||
result.push_back(mgl::Keyboard::LControl);
|
||||
if(modifiers & HOTKEY_MOD_LSHIFT)
|
||||
result.push_back(mgl::Keyboard::LShift);
|
||||
if(modifiers & HOTKEY_MOD_LALT)
|
||||
result.push_back(mgl::Keyboard::LAlt);
|
||||
if(modifiers & HOTKEY_MOD_LSUPER)
|
||||
result.push_back(mgl::Keyboard::LSystem);
|
||||
if(modifiers & HOTKEY_MOD_RCTRL)
|
||||
result.push_back(mgl::Keyboard::RControl);
|
||||
if(modifiers & HOTKEY_MOD_RSHIFT)
|
||||
result.push_back(mgl::Keyboard::RShift);
|
||||
if(modifiers & HOTKEY_MOD_RALT)
|
||||
result.push_back(mgl::Keyboard::RAlt);
|
||||
if(modifiers & HOTKEY_MOD_RSUPER)
|
||||
result.push_back(mgl::Keyboard::RSystem);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void string_remove_all(std::string &str, const std::string &substr) {
|
||||
size_t index = 0;
|
||||
while(true) {
|
||||
index = str.find(substr, index);
|
||||
if(index == std::string::npos)
|
||||
break;
|
||||
str.erase(index, substr.size());
|
||||
}
|
||||
}
|
||||
|
||||
ReplayStartupMode replay_startup_string_to_type(const char *startup_mode_str) {
|
||||
if(strcmp(startup_mode_str, "dont_turn_on_automatically") == 0)
|
||||
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
|
||||
else if(strcmp(startup_mode_str, "turn_on_at_system_startup") == 0)
|
||||
return ReplayStartupMode::TURN_ON_AT_SYSTEM_STARTUP;
|
||||
else if(strcmp(startup_mode_str, "turn_on_at_fullscreen") == 0)
|
||||
return ReplayStartupMode::TURN_ON_AT_FULLSCREEN;
|
||||
else if(strcmp(startup_mode_str, "turn_on_at_power_supply_connected") == 0)
|
||||
return ReplayStartupMode::TURN_ON_AT_POWER_SUPPLY_CONNECTED;
|
||||
else
|
||||
return ReplayStartupMode::DONT_TURN_ON_AUTOMATICALLY;
|
||||
}
|
||||
|
||||
bool ConfigHotkey::operator==(const ConfigHotkey &other) const {
|
||||
return keysym == other.keysym && modifiers == other.modifiers;
|
||||
return key == other.key && modifiers == other.modifiers;
|
||||
}
|
||||
|
||||
bool ConfigHotkey::operator!=(const ConfigHotkey &other) const {
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
std::string ConfigHotkey::to_string(bool spaces, bool modifier_side) const {
|
||||
std::string result;
|
||||
|
||||
const std::vector<mgl::Keyboard::Key> modifier_keys = hotkey_modifiers_to_mgl_keys(modifiers);
|
||||
std::string modifier_str;
|
||||
for(const mgl::Keyboard::Key modifier_key : modifier_keys) {
|
||||
if(!result.empty()) {
|
||||
if(spaces)
|
||||
result += " + ";
|
||||
else
|
||||
result += "+";
|
||||
}
|
||||
|
||||
modifier_str = mgl::Keyboard::key_to_string(modifier_key);
|
||||
if(!modifier_side) {
|
||||
string_remove_all(modifier_str, "Left ");
|
||||
string_remove_all(modifier_str, "Right ");
|
||||
}
|
||||
result += modifier_str;
|
||||
}
|
||||
|
||||
if(key != 0) {
|
||||
if(!result.empty()) {
|
||||
if(spaces)
|
||||
result += " + ";
|
||||
else
|
||||
result += "+";
|
||||
}
|
||||
result += mgl::Keyboard::key_to_string((mgl::Keyboard::Key)key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AudioTrack::operator==(const AudioTrack &other) const {
|
||||
return audio_inputs == other.audio_inputs && application_audio_invert == other.application_audio_invert;
|
||||
}
|
||||
|
||||
bool AudioTrack::operator!=(const AudioTrack &other) const {
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
Config::Config(const SupportedCaptureOptions &capture_options) {
|
||||
const std::string default_save_directory = get_videos_dir();
|
||||
const std::string default_videos_save_directory = get_videos_dir();
|
||||
const std::string default_pictures_save_directory = get_pictures_dir();
|
||||
|
||||
set_hotkeys_to_default();
|
||||
|
||||
streaming_config.record_options.video_quality = "custom";
|
||||
streaming_config.record_options.audio_tracks.push_back("default_output");
|
||||
streaming_config.record_options.video_bitrate = 15000;
|
||||
streaming_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
streaming_config.record_options.video_bitrate = 8000;
|
||||
|
||||
record_config.save_directory = default_save_directory;
|
||||
record_config.record_options.audio_tracks.push_back("default_output");
|
||||
record_config.record_options.video_bitrate = 45000;
|
||||
record_config.save_directory = default_videos_save_directory;
|
||||
record_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
record_config.record_options.video_bitrate = 40000;
|
||||
|
||||
replay_config.record_options.video_quality = "custom";
|
||||
replay_config.save_directory = default_save_directory;
|
||||
replay_config.record_options.audio_tracks.push_back("default_output");
|
||||
replay_config.record_options.video_bitrate = 45000;
|
||||
replay_config.save_directory = default_videos_save_directory;
|
||||
replay_config.record_options.audio_tracks_list.push_back({std::vector<std::string>{"default_output"}, false});
|
||||
replay_config.record_options.video_bitrate = 40000;
|
||||
|
||||
screenshot_config.save_directory = default_pictures_save_directory;
|
||||
|
||||
if(!capture_options.monitors.empty()) {
|
||||
streaming_config.record_options.record_area_option = capture_options.monitors.front().name;
|
||||
record_config.record_options.record_area_option = capture_options.monitors.front().name;
|
||||
replay_config.record_options.record_area_option = capture_options.monitors.front().name;
|
||||
streaming_config.record_options.record_area_option = "focused_monitor";
|
||||
record_config.record_options.record_area_option = "focused_monitor";
|
||||
replay_config.record_options.record_area_option = "focused_monitor";
|
||||
screenshot_config.record_area_option = "focused_monitor";
|
||||
}
|
||||
}
|
||||
|
||||
void Config::set_hotkeys_to_default() {
|
||||
streaming_config.start_stop_hotkey = {mgl::Keyboard::F8, HOTKEY_MOD_LALT};
|
||||
|
||||
record_config.start_stop_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LALT};
|
||||
record_config.pause_unpause_hotkey = {mgl::Keyboard::F7, HOTKEY_MOD_LALT};
|
||||
record_config.start_stop_region_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LCTRL};
|
||||
record_config.start_stop_window_hotkey = {mgl::Keyboard::F9, HOTKEY_MOD_LSHIFT};
|
||||
|
||||
replay_config.start_stop_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT | HOTKEY_MOD_LSHIFT};
|
||||
replay_config.save_hotkey = {mgl::Keyboard::F10, HOTKEY_MOD_LALT};
|
||||
replay_config.save_1_min_hotkey = {mgl::Keyboard::F11, HOTKEY_MOD_LALT};
|
||||
replay_config.save_10_min_hotkey = {mgl::Keyboard::F12, HOTKEY_MOD_LALT};
|
||||
|
||||
screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Printscreen, 0};
|
||||
screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LCTRL};
|
||||
screenshot_config.take_screenshot_window_hotkey = {mgl::Keyboard::Printscreen, HOTKEY_MOD_LSHIFT};
|
||||
|
||||
main_config.show_hide_hotkey = {mgl::Keyboard::Z, HOTKEY_MOD_LALT};
|
||||
}
|
||||
|
||||
static std::optional<KeyValue> parse_key_value(std::string_view line) {
|
||||
const size_t space_index = line.find(' ');
|
||||
if(space_index == std::string_view::npos)
|
||||
@@ -52,14 +167,19 @@ namespace gsr {
|
||||
return KeyValue{line.substr(0, space_index), line.substr(space_index + 1)};
|
||||
}
|
||||
|
||||
using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*>;
|
||||
using ConfigValue = std::variant<bool*, std::string*, int32_t*, ConfigHotkey*, std::vector<std::string>*, std::vector<AudioTrack>*>;
|
||||
|
||||
static std::map<std::string_view, ConfigValue> get_config_options(Config &config) {
|
||||
return {
|
||||
{"main.config_file_version", &config.main_config.config_file_version},
|
||||
{"main.software_encoding_warning_shown", &config.main_config.software_encoding_warning_shown},
|
||||
{"main.wayland_warning_shown", &config.main_config.wayland_warning_shown},
|
||||
{"main.hotkeys_enable_option", &config.main_config.hotkeys_enable_option},
|
||||
{"main.joystick_hotkeys_enable_option", &config.main_config.joystick_hotkeys_enable_option},
|
||||
{"main.tint_color", &config.main_config.tint_color},
|
||||
{"main.notification_speed", &config.main_config.notification_speed},
|
||||
{"main.language", &config.main_config.language},
|
||||
{"main.show_hide_hotkey", &config.main_config.show_hide_hotkey},
|
||||
|
||||
{"streaming.record_options.record_area_option", &config.streaming_config.record_options.record_area_option},
|
||||
{"streaming.record_options.record_area_width", &config.streaming_config.record_options.record_area_width},
|
||||
@@ -72,6 +192,7 @@ namespace gsr {
|
||||
{"streaming.record_options.application_audio_invert", &config.streaming_config.record_options.application_audio_invert},
|
||||
{"streaming.record_options.change_video_resolution", &config.streaming_config.record_options.change_video_resolution},
|
||||
{"streaming.record_options.audio_track", &config.streaming_config.record_options.audio_tracks},
|
||||
{"streaming.record_options.audio_track_item", &config.streaming_config.record_options.audio_tracks_list},
|
||||
{"streaming.record_options.color_range", &config.streaming_config.record_options.color_range},
|
||||
{"streaming.record_options.video_quality", &config.streaming_config.record_options.video_quality},
|
||||
{"streaming.record_options.codec", &config.streaming_config.record_options.video_codec},
|
||||
@@ -81,14 +202,29 @@ namespace gsr {
|
||||
{"streaming.record_options.overclock", &config.streaming_config.record_options.overclock},
|
||||
{"streaming.record_options.record_cursor", &config.streaming_config.record_options.record_cursor},
|
||||
{"streaming.record_options.restore_portal_session", &config.streaming_config.record_options.restore_portal_session},
|
||||
{"streaming.show_streaming_started_notifications", &config.streaming_config.show_streaming_started_notifications},
|
||||
{"streaming.show_streaming_stopped_notifications", &config.streaming_config.show_streaming_stopped_notifications},
|
||||
{"streaming.record_options.low_power_mode", &config.streaming_config.record_options.low_power_mode},
|
||||
{"streaming.record_options.webcam_source", &config.streaming_config.record_options.webcam_source},
|
||||
{"streaming.record_options.webcam_flip_horizontally", &config.streaming_config.record_options.webcam_flip_horizontally},
|
||||
{"streaming.record_options.webcam_video_format", &config.streaming_config.record_options.webcam_video_format},
|
||||
{"streaming.record_options.webcam_camera_width", &config.streaming_config.record_options.webcam_camera_width},
|
||||
{"streaming.record_options.webcam_camera_height", &config.streaming_config.record_options.webcam_camera_height},
|
||||
{"streaming.record_options.webcam_camera_fps", &config.streaming_config.record_options.webcam_camera_fps},
|
||||
{"streaming.record_options.webcam_x", &config.streaming_config.record_options.webcam_x},
|
||||
{"streaming.record_options.webcam_y", &config.streaming_config.record_options.webcam_y},
|
||||
{"streaming.record_options.webcam_width", &config.streaming_config.record_options.webcam_width},
|
||||
{"streaming.record_options.webcam_height", &config.streaming_config.record_options.webcam_height},
|
||||
{"streaming.record_options.show_notifications", &config.streaming_config.record_options.show_notifications},
|
||||
{"streaming.record_options.use_led_indicator", &config.streaming_config.record_options.use_led_indicator},
|
||||
{"streaming.service", &config.streaming_config.streaming_service},
|
||||
{"streaming.youtube.key", &config.streaming_config.youtube.stream_key},
|
||||
{"streaming.twitch.key", &config.streaming_config.twitch.stream_key},
|
||||
{"streaming.rumble.key", &config.streaming_config.rumble.stream_key},
|
||||
{"streaming.kick.url", &config.streaming_config.kick.stream_url},
|
||||
{"streaming.kick.key", &config.streaming_config.kick.stream_key},
|
||||
{"streaming.custom.url", &config.streaming_config.custom.url},
|
||||
{"streaming.custom.key", &config.streaming_config.custom.key},
|
||||
{"streaming.custom.container", &config.streaming_config.custom.container},
|
||||
{"streaming.start_stop_recording_hotkey", &config.streaming_config.start_stop_recording_hotkey},
|
||||
{"streaming.start_stop_hotkey", &config.streaming_config.start_stop_hotkey},
|
||||
|
||||
{"record.record_options.record_area_option", &config.record_config.record_options.record_area_option},
|
||||
{"record.record_options.record_area_width", &config.record_config.record_options.record_area_width},
|
||||
@@ -101,6 +237,7 @@ namespace gsr {
|
||||
{"record.record_options.application_audio_invert", &config.record_config.record_options.application_audio_invert},
|
||||
{"record.record_options.change_video_resolution", &config.record_config.record_options.change_video_resolution},
|
||||
{"record.record_options.audio_track", &config.record_config.record_options.audio_tracks},
|
||||
{"record.record_options.audio_track_item", &config.record_config.record_options.audio_tracks_list},
|
||||
{"record.record_options.color_range", &config.record_config.record_options.color_range},
|
||||
{"record.record_options.video_quality", &config.record_config.record_options.video_quality},
|
||||
{"record.record_options.codec", &config.record_config.record_options.video_codec},
|
||||
@@ -110,13 +247,26 @@ namespace gsr {
|
||||
{"record.record_options.overclock", &config.record_config.record_options.overclock},
|
||||
{"record.record_options.record_cursor", &config.record_config.record_options.record_cursor},
|
||||
{"record.record_options.restore_portal_session", &config.record_config.record_options.restore_portal_session},
|
||||
{"record.record_options.low_power_mode", &config.record_config.record_options.low_power_mode},
|
||||
{"record.record_options.webcam_source", &config.record_config.record_options.webcam_source},
|
||||
{"record.record_options.webcam_flip_horizontally", &config.record_config.record_options.webcam_flip_horizontally},
|
||||
{"record.record_options.webcam_video_format", &config.record_config.record_options.webcam_video_format},
|
||||
{"record.record_options.webcam_camera_width", &config.record_config.record_options.webcam_camera_width},
|
||||
{"record.record_options.webcam_camera_height", &config.record_config.record_options.webcam_camera_height},
|
||||
{"record.record_options.webcam_camera_fps", &config.record_config.record_options.webcam_camera_fps},
|
||||
{"record.record_options.webcam_x", &config.record_config.record_options.webcam_x},
|
||||
{"record.record_options.webcam_y", &config.record_config.record_options.webcam_y},
|
||||
{"record.record_options.webcam_width", &config.record_config.record_options.webcam_width},
|
||||
{"record.record_options.webcam_height", &config.record_config.record_options.webcam_height},
|
||||
{"record.record_options.show_notifications", &config.record_config.record_options.show_notifications},
|
||||
{"record.record_options.use_led_indicator", &config.record_config.record_options.use_led_indicator},
|
||||
{"record.save_video_in_game_folder", &config.record_config.save_video_in_game_folder},
|
||||
{"record.show_recording_started_notifications", &config.record_config.show_recording_started_notifications},
|
||||
{"record.show_video_saved_notifications", &config.record_config.show_video_saved_notifications},
|
||||
{"record.save_directory", &config.record_config.save_directory},
|
||||
{"record.container", &config.record_config.container},
|
||||
{"record.start_stop_recording_hotkey", &config.record_config.start_stop_recording_hotkey},
|
||||
{"record.pause_unpause_recording_hotkey", &config.record_config.pause_unpause_recording_hotkey},
|
||||
{"record.start_stop_hotkey", &config.record_config.start_stop_hotkey},
|
||||
{"record.pause_unpause_hotkey", &config.record_config.pause_unpause_hotkey},
|
||||
{"record.start_stop_region_hotkey", &config.record_config.start_stop_region_hotkey},
|
||||
{"record.start_stop_window_hotkey", &config.record_config.start_stop_window_hotkey},
|
||||
|
||||
{"replay.record_options.record_area_option", &config.replay_config.record_options.record_area_option},
|
||||
{"replay.record_options.record_area_width", &config.replay_config.record_options.record_area_width},
|
||||
@@ -129,6 +279,7 @@ namespace gsr {
|
||||
{"replay.record_options.application_audio_invert", &config.replay_config.record_options.application_audio_invert},
|
||||
{"replay.record_options.change_video_resolution", &config.replay_config.record_options.change_video_resolution},
|
||||
{"replay.record_options.audio_track", &config.replay_config.record_options.audio_tracks},
|
||||
{"replay.record_options.audio_track_item", &config.replay_config.record_options.audio_tracks_list},
|
||||
{"replay.record_options.color_range", &config.replay_config.record_options.color_range},
|
||||
{"replay.record_options.video_quality", &config.replay_config.record_options.video_quality},
|
||||
{"replay.record_options.codec", &config.replay_config.record_options.video_codec},
|
||||
@@ -138,16 +289,49 @@ namespace gsr {
|
||||
{"replay.record_options.overclock", &config.replay_config.record_options.overclock},
|
||||
{"replay.record_options.record_cursor", &config.replay_config.record_options.record_cursor},
|
||||
{"replay.record_options.restore_portal_session", &config.replay_config.record_options.restore_portal_session},
|
||||
{"replay.record_options.low_power_mode", &config.replay_config.record_options.low_power_mode},
|
||||
{"replay.record_options.webcam_source", &config.replay_config.record_options.webcam_source},
|
||||
{"replay.record_options.webcam_flip_horizontally", &config.replay_config.record_options.webcam_flip_horizontally},
|
||||
{"replay.record_options.webcam_video_format", &config.replay_config.record_options.webcam_video_format},
|
||||
{"replay.record_options.webcam_camera_width", &config.replay_config.record_options.webcam_camera_width},
|
||||
{"replay.record_options.webcam_camera_height", &config.replay_config.record_options.webcam_camera_height},
|
||||
{"replay.record_options.webcam_camera_fps", &config.replay_config.record_options.webcam_camera_fps},
|
||||
{"replay.record_options.webcam_x", &config.replay_config.record_options.webcam_x},
|
||||
{"replay.record_options.webcam_y", &config.replay_config.record_options.webcam_y},
|
||||
{"replay.record_options.webcam_width", &config.replay_config.record_options.webcam_width},
|
||||
{"replay.record_options.webcam_height", &config.replay_config.record_options.webcam_height},
|
||||
{"replay.record_options.show_notifications", &config.replay_config.record_options.show_notifications},
|
||||
{"replay.record_options.use_led_indicator", &config.replay_config.record_options.use_led_indicator},
|
||||
{"replay.turn_on_replay_automatically_mode", &config.replay_config.turn_on_replay_automatically_mode},
|
||||
{"replay.save_video_in_game_folder", &config.replay_config.save_video_in_game_folder},
|
||||
{"replay.show_replay_started_notifications", &config.replay_config.show_replay_started_notifications},
|
||||
{"replay.show_replay_stopped_notifications", &config.replay_config.show_replay_stopped_notifications},
|
||||
{"replay.show_replay_saved_notifications", &config.replay_config.show_replay_saved_notifications},
|
||||
{"replay.restart_replay_on_save", &config.replay_config.restart_replay_on_save},
|
||||
{"replay.save_directory", &config.replay_config.save_directory},
|
||||
{"replay.container", &config.replay_config.container},
|
||||
{"replay.time", &config.replay_config.replay_time},
|
||||
{"replay.start_stop_recording_hotkey", &config.replay_config.start_stop_recording_hotkey},
|
||||
{"replay.save_recording_hotkey", &config.replay_config.save_recording_hotkey}
|
||||
{"replay.replay_storage", &config.replay_config.replay_storage},
|
||||
{"replay.start_stop_hotkey", &config.replay_config.start_stop_hotkey},
|
||||
{"replay.save_hotkey", &config.replay_config.save_hotkey},
|
||||
{"replay.save_1_min_hotkey", &config.replay_config.save_1_min_hotkey},
|
||||
{"replay.save_10_min_hotkey", &config.replay_config.save_10_min_hotkey},
|
||||
|
||||
{"screenshot.record_area_option", &config.screenshot_config.record_area_option},
|
||||
{"screenshot.image_width", &config.screenshot_config.image_width},
|
||||
{"screenshot.image_height", &config.screenshot_config.image_height},
|
||||
{"screenshot.change_image_resolution", &config.screenshot_config.change_image_resolution},
|
||||
{"screenshot.image_quality", &config.screenshot_config.image_quality},
|
||||
{"screenshot.image_format", &config.screenshot_config.image_format},
|
||||
{"screenshot.record_cursor", &config.screenshot_config.record_cursor},
|
||||
{"screenshot.restore_portal_session", &config.screenshot_config.restore_portal_session},
|
||||
{"screenshot.save_screenshot_in_game_folder", &config.screenshot_config.save_screenshot_in_game_folder},
|
||||
{"screenshot.save_screenshot_to_clipboard", &config.screenshot_config.save_screenshot_to_clipboard},
|
||||
{"screenshot.save_screenshot_to_disk", &config.screenshot_config.save_screenshot_to_disk},
|
||||
{"screenshot.show_notifications", &config.screenshot_config.show_notifications},
|
||||
{"screenshot.use_led_indicator", &config.screenshot_config.use_led_indicator},
|
||||
{"screenshot.save_directory", &config.screenshot_config.save_directory},
|
||||
{"screenshot.take_screenshot_hotkey", &config.screenshot_config.take_screenshot_hotkey},
|
||||
{"screenshot.take_screenshot_region_hotkey", &config.screenshot_config.take_screenshot_region_hotkey},
|
||||
{"screenshot.take_screenshot_window_hotkey", &config.screenshot_config.take_screenshot_window_hotkey},
|
||||
{"screenshot.custom_script", &config.screenshot_config.custom_script},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -174,6 +358,11 @@ namespace gsr {
|
||||
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
|
||||
if(*std::get<std::vector<std::string>*>(it.second) != *std::get<std::vector<std::string>*>(it_other->second))
|
||||
return false;
|
||||
} else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) {
|
||||
if(*std::get<std::vector<AudioTrack>*>(it.second) != *std::get<std::vector<AudioTrack>*>(it_other->second))
|
||||
return false;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -183,6 +372,17 @@ namespace gsr {
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
static void populate_new_audio_track_from_old(RecordOptions &record_options) {
|
||||
if(record_options.merge_audio_tracks) {
|
||||
record_options.audio_tracks_list.push_back({std::move(record_options.audio_tracks), record_options.application_audio_invert});
|
||||
} else {
|
||||
for(const std::string &audio_input : record_options.audio_tracks) {
|
||||
record_options.audio_tracks_list.push_back({std::vector<std::string>{audio_input}, record_options.application_audio_invert});
|
||||
}
|
||||
}
|
||||
record_options.audio_tracks.clear();
|
||||
}
|
||||
|
||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options) {
|
||||
std::optional<Config> config;
|
||||
|
||||
@@ -194,10 +394,15 @@ namespace gsr {
|
||||
}
|
||||
|
||||
config = Config(capture_options);
|
||||
|
||||
config->streaming_config.record_options.audio_tracks.clear();
|
||||
config->record_config.record_options.audio_tracks.clear();
|
||||
config->replay_config.record_options.audio_tracks.clear();
|
||||
|
||||
config->streaming_config.record_options.audio_tracks_list.clear();
|
||||
config->record_config.record_options.audio_tracks_list.clear();
|
||||
config->replay_config.record_options.audio_tracks_list.clear();
|
||||
|
||||
auto config_options = get_config_options(config.value());
|
||||
|
||||
string_split_char(file_content, '\n', [&](std::string_view line) {
|
||||
@@ -228,29 +433,53 @@ namespace gsr {
|
||||
} else if(std::holds_alternative<ConfigHotkey*>(it->second)) {
|
||||
std::string value_str(key_value->value);
|
||||
ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it->second);
|
||||
if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->keysym, &config_hotkey->modifiers) != 2) {
|
||||
if(sscanf(value_str.c_str(), FORMAT_I64 " " FORMAT_U32, &config_hotkey->key, &config_hotkey->modifiers) != 2) {
|
||||
fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
|
||||
config_hotkey->keysym = 0;
|
||||
config_hotkey->key = 0;
|
||||
config_hotkey->modifiers = 0;
|
||||
}
|
||||
} else if(std::holds_alternative<std::vector<std::string>*>(it->second)) {
|
||||
std::string array_value(key_value->value);
|
||||
std::get<std::vector<std::string>*>(it->second)->push_back(std::move(array_value));
|
||||
} else if(std::holds_alternative<std::vector<AudioTrack>*>(it->second)) {
|
||||
const size_t space_index = key_value->value.find(' ');
|
||||
if(space_index == std::string_view::npos) {
|
||||
fprintf(stderr, "Warning: Invalid config option value for %.*s\n", (int)key_value->key.size(), key_value->key.data());
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool application_audio_invert = key_value->value.substr(0, space_index) == "true";
|
||||
const std::string_view audio_input = key_value->value.substr(space_index + 1);
|
||||
std::vector<AudioTrack> &audio_tracks = *std::get<std::vector<AudioTrack>*>(it->second);
|
||||
|
||||
if(audio_input == add_audio_track_tag) {
|
||||
audio_tracks.push_back({std::vector<std::string>{}, application_audio_invert});
|
||||
} else if(!audio_tracks.empty()) {
|
||||
audio_tracks.back().application_audio_invert = application_audio_invert;
|
||||
audio_tracks.back().audio_inputs.emplace_back(audio_input);
|
||||
}
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if(config->main_config.config_file_version != CONFIG_FILE_VERSION) {
|
||||
fprintf(stderr, "Info: the config file is outdated, resetting it\n");
|
||||
config = std::nullopt;
|
||||
if(config->main_config.config_file_version == 1) {
|
||||
populate_new_audio_track_from_old(config->streaming_config.record_options);
|
||||
populate_new_audio_track_from_old(config->record_config.record_options);
|
||||
populate_new_audio_track_from_old(config->replay_config.record_options);
|
||||
}
|
||||
|
||||
config->streaming_config.record_options.audio_tracks.clear();
|
||||
config->record_config.record_options.audio_tracks.clear();
|
||||
config->replay_config.record_options.audio_tracks.clear();
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
void save_config(Config &config) {
|
||||
config.main_config.config_file_version = CONFIG_FILE_VERSION;
|
||||
config.main_config.config_file_version = GSR_CONFIG_FILE_VERSION;
|
||||
|
||||
const std::string config_path = get_config_dir() + "/config_ui";
|
||||
|
||||
@@ -279,15 +508,25 @@ namespace gsr {
|
||||
fprintf(file, "%.*s " FORMAT_I32 "\n", (int)it.first.size(), it.first.data(), *std::get<int32_t*>(it.second));
|
||||
} else if(std::holds_alternative<ConfigHotkey*>(it.second)) {
|
||||
const ConfigHotkey *config_hotkey = std::get<ConfigHotkey*>(it.second);
|
||||
fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->keysym, config_hotkey->modifiers);
|
||||
fprintf(file, "%.*s " FORMAT_I64 " " FORMAT_U32 "\n", (int)it.first.size(), it.first.data(), config_hotkey->key, config_hotkey->modifiers);
|
||||
} else if(std::holds_alternative<std::vector<std::string>*>(it.second)) {
|
||||
std::vector<std::string> *array = std::get<std::vector<std::string>*>(it.second);
|
||||
for(const std::string &value : *array) {
|
||||
fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), value.c_str());
|
||||
std::vector<std::string> *audio_inputs = std::get<std::vector<std::string>*>(it.second);
|
||||
for(const std::string &audio_input : *audio_inputs) {
|
||||
fprintf(file, "%.*s %s\n", (int)it.first.size(), it.first.data(), audio_input.c_str());
|
||||
}
|
||||
} else if(std::holds_alternative<std::vector<AudioTrack>*>(it.second)) {
|
||||
std::vector<AudioTrack> *audio_tracks = std::get<std::vector<AudioTrack>*>(it.second);
|
||||
for(const AudioTrack &audio_track : *audio_tracks) {
|
||||
fprintf(file, "%.*s %s %.*s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", (int)add_audio_track_tag.size(), add_audio_track_tag.data());
|
||||
for(const std::string &audio_input : audio_track.audio_inputs) {
|
||||
fprintf(file, "%.*s %s %s\n", (int)it.first.size(), it.first.data(), audio_track.application_audio_invert ? "true" : "false", audio_input.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
344
src/CursorTracker/CursorTrackerWayland.cpp
Normal file
@@ -0,0 +1,344 @@
|
||||
#include "../../include/CursorTracker/CursorTrackerWayland.hpp"
|
||||
#include "../../include/WindowUtils.hpp"
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <xf86drm.h>
|
||||
#include <xf86drmMode.h>
|
||||
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const int MAX_CONNECTORS = 32;
|
||||
static const uint32_t plane_property_all = 0x3F;
|
||||
|
||||
typedef enum {
|
||||
PLANE_PROPERTY_CRTC_X = 1 << 0,
|
||||
PLANE_PROPERTY_CRTC_Y = 1 << 1,
|
||||
PLANE_PROPERTY_CRTC_W = 1 << 2,
|
||||
PLANE_PROPERTY_CRTC_H = 1 << 3,
|
||||
PLANE_PROPERTY_CRTC_ID = 1 << 4,
|
||||
PLANE_PROPERTY_TYPE_CURSOR = 1 << 5,
|
||||
} plane_property_mask;
|
||||
|
||||
typedef struct {
|
||||
uint64_t crtc_id;
|
||||
mgl::vec2i size;
|
||||
bool vrr_enabled;
|
||||
} drm_connector;
|
||||
|
||||
typedef struct {
|
||||
drm_connector connectors[MAX_CONNECTORS];
|
||||
int num_connectors;
|
||||
bool has_any_crtc_with_vrr_enabled;
|
||||
} drm_connectors;
|
||||
|
||||
static bool rectangles_intersect(mgl::IntRect rect1, mgl::IntRect rect2) {
|
||||
return rect1.position.x < rect2.position.x + rect2.size.x && rect1.position.x + rect1.size.x > rect2.position.x &&
|
||||
rect1.position.y < rect2.position.y + rect2.size.y && rect1.position.y + rect1.size.y > rect2.position.y;
|
||||
}
|
||||
|
||||
/* Returns plane_property_mask */
|
||||
static uint32_t plane_get_properties(int drm_fd, uint32_t plane_id, int *crtc_x, int *crtc_y, int *crtc_w, int *crtc_h, int *crtc_id) {
|
||||
*crtc_x = 0;
|
||||
*crtc_y = 0;
|
||||
*crtc_w = 0;
|
||||
*crtc_h = 0;
|
||||
*crtc_id = 0;
|
||||
|
||||
uint32_t property_mask = 0;
|
||||
|
||||
drmModeObjectPropertiesPtr props = drmModeObjectGetProperties(drm_fd, plane_id, DRM_MODE_OBJECT_PLANE);
|
||||
if(!props)
|
||||
return property_mask;
|
||||
|
||||
for(uint32_t i = 0; i < props->count_props; ++i) {
|
||||
drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
|
||||
if(!prop)
|
||||
continue;
|
||||
|
||||
// SRC_* values are fixed 16.16 points
|
||||
const uint32_t type = prop->flags & (DRM_MODE_PROP_LEGACY_TYPE | DRM_MODE_PROP_EXTENDED_TYPE);
|
||||
if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_X") == 0) {
|
||||
*crtc_x = (int)props->prop_values[i];
|
||||
property_mask |= PLANE_PROPERTY_CRTC_X;
|
||||
} else if((type & DRM_MODE_PROP_SIGNED_RANGE) && strcmp(prop->name, "CRTC_Y") == 0) {
|
||||
*crtc_y = (int)props->prop_values[i];
|
||||
property_mask |= PLANE_PROPERTY_CRTC_Y;
|
||||
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "CRTC_W") == 0) {
|
||||
*crtc_w = (int)props->prop_values[i];
|
||||
property_mask |= PLANE_PROPERTY_CRTC_W;
|
||||
} else if((type & DRM_MODE_PROP_RANGE) && strcmp(prop->name, "CRTC_H") == 0) {
|
||||
*crtc_h = (int)props->prop_values[i];
|
||||
property_mask |= PLANE_PROPERTY_CRTC_H;
|
||||
} else if((type & DRM_MODE_PROP_OBJECT) && strcmp(prop->name, "CRTC_ID") == 0) {
|
||||
*crtc_id = (int)props->prop_values[i];
|
||||
property_mask |= PLANE_PROPERTY_CRTC_ID;
|
||||
} else if((type & DRM_MODE_PROP_ENUM) && strcmp(prop->name, "type") == 0) {
|
||||
const uint64_t current_enum_value = props->prop_values[i];
|
||||
for(int j = 0; j < prop->count_enums; ++j) {
|
||||
if(prop->enums[j].value == current_enum_value && strcmp(prop->enums[j].name, "Cursor") == 0) {
|
||||
property_mask |= PLANE_PROPERTY_TYPE_CURSOR;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drmModeFreeProperty(prop);
|
||||
}
|
||||
|
||||
drmModeFreeObjectProperties(props);
|
||||
return property_mask;
|
||||
}
|
||||
|
||||
static bool get_drm_property_by_name(int drm_fd, drmModeObjectPropertiesPtr props, const char *name, uint64_t *result) {
|
||||
for(uint32_t i = 0; i < props->count_props; ++i) {
|
||||
drmModePropertyPtr prop = drmModeGetProperty(drm_fd, props->props[i]);
|
||||
if(!prop)
|
||||
continue;
|
||||
|
||||
if(strcmp(name, prop->name) == 0) {
|
||||
*result = props->prop_values[i];
|
||||
drmModeFreeProperty(prop);
|
||||
return true;
|
||||
}
|
||||
drmModeFreeProperty(prop);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool connector_get_property_by_name(int drm_fd, drmModeConnectorPtr props, const char *name, uint64_t *result) {
|
||||
drmModeObjectProperties properties;
|
||||
properties.count_props = (uint32_t)props->count_props;
|
||||
properties.props = props->props;
|
||||
properties.prop_values = props->prop_values;
|
||||
return get_drm_property_by_name(drm_fd, &properties, name, result);
|
||||
}
|
||||
|
||||
// Note: this monitor name logic is kept in sync with gpu screen recorder
|
||||
static std::string get_monitor_name_from_crtc_id(int drm_fd, uint32_t crtc_id) {
|
||||
std::string result;
|
||||
drmModeResPtr resources = drmModeGetResources(drm_fd);
|
||||
if(!resources)
|
||||
return result;
|
||||
|
||||
for(int i = 0; i < resources->count_connectors; ++i) {
|
||||
uint64_t connector_crtc_id = 0;
|
||||
drmModeConnectorPtr connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
|
||||
if(!connector)
|
||||
continue;
|
||||
|
||||
const char *connection_name = drmModeGetConnectorTypeName(connector->connector_type);
|
||||
if(!connection_name)
|
||||
goto next;
|
||||
|
||||
if(connector->connection != DRM_MODE_CONNECTED)
|
||||
goto next;
|
||||
|
||||
if(connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &connector_crtc_id) && connector_crtc_id == crtc_id) {
|
||||
result = connection_name;
|
||||
result += "-";
|
||||
result += std::to_string(connector->connector_type_id);
|
||||
drmModeFreeConnector(connector);
|
||||
break;
|
||||
}
|
||||
|
||||
next:
|
||||
drmModeFreeConnector(connector);
|
||||
}
|
||||
|
||||
drmModeFreeResources(resources);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Name is the crtc name. TODO: verify if this works on all wayland compositors
|
||||
static const Monitor* get_wayland_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) {
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(monitor.name == name)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Returns nullptr if not found */
|
||||
static drm_connector* get_drm_connector_by_crtc_id(drm_connectors *connectors, uint32_t crtc_id) {
|
||||
for(int i = 0; i < connectors->num_connectors; ++i) {
|
||||
if(connectors->connectors[i].crtc_id == crtc_id)
|
||||
return &connectors->connectors[i];
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void get_drm_connectors(int drm_fd, drm_connectors *drm_connectors) {
|
||||
drm_connectors->num_connectors = 0;
|
||||
drm_connectors->has_any_crtc_with_vrr_enabled = false;
|
||||
|
||||
drmModeResPtr resources = drmModeGetResources(drm_fd);
|
||||
if(!resources)
|
||||
return;
|
||||
|
||||
for(int i = 0; i < resources->count_connectors && drm_connectors->num_connectors < MAX_CONNECTORS; ++i) {
|
||||
drmModeConnectorPtr connector = nullptr;
|
||||
drmModeCrtcPtr crtc = nullptr;
|
||||
|
||||
connector = drmModeGetConnectorCurrent(drm_fd, resources->connectors[i]);
|
||||
if(!connector)
|
||||
continue;
|
||||
|
||||
uint64_t crtc_id = 0;
|
||||
connector_get_property_by_name(drm_fd, connector, "CRTC_ID", &crtc_id);
|
||||
if(crtc_id == 0)
|
||||
goto next_connector;
|
||||
|
||||
crtc = drmModeGetCrtc(drm_fd, crtc_id);
|
||||
if(!crtc)
|
||||
goto next_connector;
|
||||
|
||||
drm_connectors->connectors[drm_connectors->num_connectors].crtc_id = crtc_id;
|
||||
drm_connectors->connectors[drm_connectors->num_connectors].size = mgl::vec2i{(int)crtc->width, (int)crtc->height};
|
||||
drm_connectors->connectors[drm_connectors->num_connectors].vrr_enabled = false;
|
||||
++drm_connectors->num_connectors;
|
||||
|
||||
next_connector:
|
||||
if(crtc)
|
||||
drmModeFreeCrtc(crtc);
|
||||
|
||||
if(connector)
|
||||
drmModeFreeConnector(connector);
|
||||
}
|
||||
|
||||
for(int i = 0; i < resources->count_crtcs; ++i) {
|
||||
drmModeCrtcPtr crtc = nullptr;
|
||||
drmModeObjectPropertiesPtr properties = nullptr;
|
||||
uint64_t vrr_enabled = 0;
|
||||
drm_connector *connector = nullptr;
|
||||
|
||||
crtc = drmModeGetCrtc(drm_fd, resources->crtcs[i]);
|
||||
if(!crtc)
|
||||
continue;
|
||||
|
||||
properties = drmModeObjectGetProperties(drm_fd, crtc->crtc_id, DRM_MODE_OBJECT_CRTC);
|
||||
if(!properties)
|
||||
goto next_crtc;
|
||||
|
||||
get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled);
|
||||
|
||||
connector = get_drm_connector_by_crtc_id(drm_connectors, crtc->crtc_id);
|
||||
if(!connector)
|
||||
goto next_crtc;
|
||||
|
||||
if(vrr_enabled) {
|
||||
connector->vrr_enabled = true;
|
||||
drm_connectors->has_any_crtc_with_vrr_enabled = true;
|
||||
}
|
||||
|
||||
next_crtc:
|
||||
if(properties)
|
||||
drmModeFreeObjectProperties(properties);
|
||||
|
||||
if(crtc)
|
||||
drmModeFreeCrtc(crtc);
|
||||
}
|
||||
|
||||
drmModeFreeResources(resources);
|
||||
}
|
||||
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path, struct wl_display *wayland_dpy) : wayland_dpy(wayland_dpy) {
|
||||
drm_fd = open(card_path, O_RDONLY);
|
||||
if(drm_fd <= 0) {
|
||||
fprintf(stderr, "Error: CursorTrackerWayland: failed to open %s\n", card_path);
|
||||
return;
|
||||
}
|
||||
|
||||
drmSetClientCap(drm_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1);
|
||||
drmSetClientCap(drm_fd, DRM_CLIENT_CAP_ATOMIC, 1);
|
||||
}
|
||||
|
||||
CursorTrackerWayland::~CursorTrackerWayland() {
|
||||
if(drm_fd > 0)
|
||||
close(drm_fd);
|
||||
}
|
||||
|
||||
void CursorTrackerWayland::update() {
|
||||
if(drm_fd <= 0)
|
||||
return;
|
||||
|
||||
drm_connectors connectors;
|
||||
connectors.num_connectors = 0;
|
||||
connectors.has_any_crtc_with_vrr_enabled = false;
|
||||
get_drm_connectors(drm_fd, &connectors);
|
||||
|
||||
drmModePlaneResPtr planes = drmModeGetPlaneResources(drm_fd);
|
||||
if(!planes)
|
||||
return;
|
||||
|
||||
bool found_cursor = false;
|
||||
for(uint32_t i = 0; i < planes->count_planes; ++i) {
|
||||
drmModePlanePtr plane = nullptr;
|
||||
const drm_connector *connector = nullptr;
|
||||
|
||||
int crtc_x = 0;
|
||||
int crtc_y = 0;
|
||||
int crtc_w = 0;
|
||||
int crtc_h = 0;
|
||||
int crtc_id = 0;
|
||||
uint32_t property_mask = 0;
|
||||
|
||||
mgl::IntRect monitor_rect;
|
||||
mgl::IntRect cursor_rect;
|
||||
|
||||
plane = drmModeGetPlane(drm_fd, planes->planes[i]);
|
||||
if(!plane)
|
||||
goto next;
|
||||
|
||||
if(!plane->fb_id)
|
||||
goto next;
|
||||
|
||||
property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_w, &crtc_h, &crtc_id);
|
||||
if(property_mask != plane_property_all || crtc_id <= 0)
|
||||
goto next;
|
||||
|
||||
connector = get_drm_connector_by_crtc_id(&connectors, crtc_id);
|
||||
if(!connector)
|
||||
goto next;
|
||||
|
||||
monitor_rect = { mgl::vec2i(0, 0), connector->size };
|
||||
cursor_rect = { mgl::vec2i(crtc_x, crtc_y), mgl::vec2i(crtc_w, crtc_h) };
|
||||
|
||||
if(rectangles_intersect(cursor_rect, cursor_rect)) {
|
||||
latest_cursor_position.x = crtc_x;
|
||||
latest_cursor_position.y = crtc_y;
|
||||
latest_crtc_id = crtc_id;
|
||||
found_cursor = true;
|
||||
drmModeFreePlane(plane);
|
||||
break;
|
||||
}
|
||||
|
||||
next:
|
||||
drmModeFreePlane(plane);
|
||||
}
|
||||
|
||||
// On kde plasma wayland (and possibly other wayland compositors) it uses a software cursor only for the monitors with vrr enabled.
|
||||
// In that case we cant know the cursor location and we instead want to fallback to getting focused monitor by using the hack of creating a window and getting the position.
|
||||
if(!found_cursor && latest_crtc_id > 0 && connectors.has_any_crtc_with_vrr_enabled)
|
||||
latest_crtc_id = -1;
|
||||
|
||||
drmModeFreePlaneResources(planes);
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1 || !wayland_dpy)
|
||||
return std::nullopt;
|
||||
|
||||
std::string monitor_name = get_monitor_name_from_crtc_id(drm_fd, latest_crtc_id);
|
||||
if(monitor_name.empty())
|
||||
return std::nullopt;
|
||||
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, monitor_name);
|
||||
if(!wayland_monitor)
|
||||
return std::nullopt;
|
||||
|
||||
return CursorInfo{ wayland_monitor->position + latest_cursor_position, std::move(monitor_name) };
|
||||
}
|
||||
}
|
||||
29
src/CursorTracker/CursorTrackerX11.cpp
Normal file
@@ -0,0 +1,29 @@
|
||||
#include "../../include/CursorTracker/CursorTrackerX11.hpp"
|
||||
#include "../../include/WindowUtils.hpp"
|
||||
|
||||
namespace gsr {
|
||||
CursorTrackerX11::CursorTrackerX11(Display *dpy) : dpy(dpy) {
|
||||
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerX11::get_latest_cursor_info() {
|
||||
Window window = None;
|
||||
const auto cursor_pos = get_cursor_position(dpy, &window);
|
||||
const auto monitors = get_monitors(dpy);
|
||||
std::string monitor_name;
|
||||
|
||||
for(const auto &monitor : monitors) {
|
||||
if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x
|
||||
&& cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y)
|
||||
{
|
||||
monitor_name = monitor.name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(monitor_name.empty())
|
||||
return std::nullopt;
|
||||
|
||||
return CursorInfo{ cursor_pos, std::move(monitor_name) };
|
||||
}
|
||||
}
|
||||
406
src/GlobalHotkeys/GlobalHotkeysJoystick.cpp
Normal file
@@ -0,0 +1,406 @@
|
||||
#include "../../include/GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int button_pressed = 1;
|
||||
|
||||
// Returns -1 on error
|
||||
static int get_dev_input_event_id_from_filepath(const char *dev_input_filepath) {
|
||||
if(strncmp(dev_input_filepath, "/dev/input/event", 16) != 0)
|
||||
return -1;
|
||||
|
||||
int dev_input_id = -1;
|
||||
if(sscanf(dev_input_filepath + 16, "%d", &dev_input_id) == 1)
|
||||
return dev_input_id;
|
||||
return -1;
|
||||
}
|
||||
|
||||
static inline bool supports_key(unsigned char *key_bits, unsigned int key) {
|
||||
return key_bits[key/8] & (1 << (key % 8));
|
||||
}
|
||||
|
||||
static bool supports_joystick_keys(unsigned char *key_bits) {
|
||||
const int keys[7] = { BTN_A, BTN_B, BTN_X, BTN_Y, BTN_SELECT, BTN_START, BTN_SELECT };
|
||||
for(int i = 0; i < 7; ++i) {
|
||||
if(supports_key(key_bits, keys[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool is_input_device_joystick(int input_fd) {
|
||||
unsigned long evbit = 0;
|
||||
ioctl(input_fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
|
||||
if((evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY))) {
|
||||
unsigned char key_bits[KEY_MAX/8 + 1];
|
||||
memset(key_bits, 0, sizeof(key_bits));
|
||||
ioctl(input_fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
|
||||
return supports_joystick_keys(key_bits);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
GlobalHotkeysJoystick::~GlobalHotkeysJoystick() {
|
||||
if(event_fd > 0) {
|
||||
const uint64_t exit = 1;
|
||||
write(event_fd, &exit, sizeof(exit));
|
||||
}
|
||||
|
||||
if(read_thread.joinable())
|
||||
read_thread.join();
|
||||
|
||||
if(event_fd > 0) {
|
||||
close(event_fd);
|
||||
event_fd = 0;
|
||||
}
|
||||
|
||||
close_fd_cv.notify_one();
|
||||
if(close_fd_thread.joinable())
|
||||
close_fd_thread.join();
|
||||
|
||||
for(int fd : fds_to_close) {
|
||||
close(fd);
|
||||
}
|
||||
|
||||
for(int i = 0; i < num_poll_fd; ++i) {
|
||||
if(poll_fd[i].fd > 0)
|
||||
close(poll_fd[i].fd);
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::start() {
|
||||
if(num_poll_fd > 0)
|
||||
return false;
|
||||
|
||||
event_fd = eventfd(0, 0);
|
||||
if(event_fd <= 0)
|
||||
return false;
|
||||
|
||||
event_index = num_poll_fd;
|
||||
poll_fd[num_poll_fd] = {
|
||||
event_fd,
|
||||
POLLIN,
|
||||
0
|
||||
};
|
||||
extra_data[num_poll_fd] = {
|
||||
-1
|
||||
};
|
||||
++num_poll_fd;
|
||||
|
||||
if(!hotplug.start()) {
|
||||
fprintf(stderr, "Warning: failed to setup hotplugging\n");
|
||||
} else {
|
||||
hotplug_poll_index = num_poll_fd;
|
||||
poll_fd[num_poll_fd] = {
|
||||
hotplug.steal_fd(),
|
||||
POLLIN,
|
||||
0
|
||||
};
|
||||
extra_data[num_poll_fd] = {
|
||||
-1
|
||||
};
|
||||
++num_poll_fd;
|
||||
}
|
||||
|
||||
add_all_joystick_devices();
|
||||
|
||||
read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
|
||||
close_fd_thread = std::thread(&GlobalHotkeysJoystick::close_fds, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::bind_action(const std::string &id, GlobalHotkeyCallback callback) {
|
||||
if(num_poll_fd == 0)
|
||||
return false;
|
||||
return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second;
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::poll_events() {
|
||||
if(num_poll_fd == 0)
|
||||
return;
|
||||
|
||||
if(save_replay) {
|
||||
save_replay = false;
|
||||
auto it = bound_actions_by_id.find("save_replay");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("save_replay");
|
||||
}
|
||||
|
||||
if(save_1_min_replay) {
|
||||
save_1_min_replay = false;
|
||||
auto it = bound_actions_by_id.find("save_1_min_replay");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("save_1_min_replay");
|
||||
}
|
||||
|
||||
if(save_10_min_replay) {
|
||||
save_10_min_replay = false;
|
||||
auto it = bound_actions_by_id.find("save_10_min_replay");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("save_10_min_replay");
|
||||
}
|
||||
|
||||
if(take_screenshot) {
|
||||
take_screenshot = false;
|
||||
auto it = bound_actions_by_id.find("take_screenshot");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("take_screenshot");
|
||||
}
|
||||
|
||||
if(toggle_record) {
|
||||
toggle_record = false;
|
||||
auto it = bound_actions_by_id.find("toggle_record");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("toggle_record");
|
||||
}
|
||||
|
||||
if(toggle_replay) {
|
||||
toggle_replay = false;
|
||||
auto it = bound_actions_by_id.find("toggle_replay");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("toggle_replay");
|
||||
}
|
||||
|
||||
if(toggle_show) {
|
||||
toggle_show = false;
|
||||
auto it = bound_actions_by_id.find("toggle_show");
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second("toggle_show");
|
||||
}
|
||||
}
|
||||
|
||||
// Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only
|
||||
void GlobalHotkeysJoystick::close_fds() {
|
||||
std::vector<int> current_fds_to_close;
|
||||
while(event_fd > 0) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(close_fd_mutex);
|
||||
close_fd_cv.wait(lock, [this]{ return !fds_to_close.empty() || event_fd <= 0; });
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
current_fds_to_close = std::move(fds_to_close);
|
||||
fds_to_close.clear();
|
||||
}
|
||||
|
||||
for(int fd : current_fds_to_close) {
|
||||
close(fd);
|
||||
}
|
||||
current_fds_to_close.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::read_events() {
|
||||
input_event event;
|
||||
while(poll(poll_fd, num_poll_fd, -1) > 0) {
|
||||
for(int i = 0; i < num_poll_fd; ++i) {
|
||||
if(poll_fd[i].revents & (POLLHUP|POLLERR|POLLNVAL)) {
|
||||
if(i == event_index)
|
||||
goto done;
|
||||
|
||||
char dev_input_filepath[256];
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", extra_data[i].dev_input_id);
|
||||
fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
|
||||
if(remove_poll_fd(i))
|
||||
--i; // This item was removed so we want to repeat the same index to continue to the next item
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!(poll_fd[i].revents & POLLIN)) {
|
||||
poll_fd[i].revents = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(i == event_index) {
|
||||
goto done;
|
||||
} else if(i == hotplug_poll_index) {
|
||||
hotplug.process_event_data(poll_fd[i].fd, [&](HotplugAction hotplug_action, const char *devname) {
|
||||
switch(hotplug_action) {
|
||||
case HotplugAction::ADD: {
|
||||
add_device(devname, false);
|
||||
break;
|
||||
}
|
||||
case HotplugAction::REMOVE: {
|
||||
if(remove_device(devname))
|
||||
--i; // This item was removed so we want to repeat the same index to continue to the next item
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
process_input_event(poll_fd[i].fd, event);
|
||||
}
|
||||
|
||||
poll_fd[i].revents = 0;
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
;
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::process_input_event(int fd, input_event &event) {
|
||||
if(read(fd, &event, sizeof(event)) != sizeof(event))
|
||||
return;
|
||||
|
||||
if(event.type == EV_KEY) {
|
||||
switch(event.code) {
|
||||
case BTN_MODE: {
|
||||
playstation_button_pressed = (event.value == button_pressed);
|
||||
break;
|
||||
}
|
||||
case BTN_START: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
toggle_show = true;
|
||||
break;
|
||||
}
|
||||
case BTN_SOUTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_1_min_replay = true;
|
||||
break;
|
||||
}
|
||||
case BTN_NORTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_10_min_replay = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(event.type == EV_ABS && playstation_button_pressed) {
|
||||
const bool prev_up_pressed = up_pressed;
|
||||
const bool prev_down_pressed = down_pressed;
|
||||
const bool prev_left_pressed = left_pressed;
|
||||
const bool prev_right_pressed = right_pressed;
|
||||
|
||||
if(event.code == ABS_HAT0Y) {
|
||||
up_pressed = event.value == -1;
|
||||
down_pressed = event.value == 1;
|
||||
} else if(event.code == ABS_HAT0X) {
|
||||
left_pressed = event.value == -1;
|
||||
right_pressed = event.value == 1;
|
||||
}
|
||||
|
||||
if(up_pressed && !prev_up_pressed)
|
||||
take_screenshot = true;
|
||||
else if(down_pressed && !prev_down_pressed)
|
||||
save_replay = true;
|
||||
else if(left_pressed && !prev_left_pressed)
|
||||
toggle_record = true;
|
||||
else if(right_pressed && !prev_right_pressed)
|
||||
toggle_replay = true;
|
||||
}
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::add_all_joystick_devices() {
|
||||
DIR *dir = opendir("/dev/input");
|
||||
if(!dir) {
|
||||
fprintf(stderr, "Error: failed to open /dev/input, error: %s\n", strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
char dev_input_filepath[1024];
|
||||
for(;;) {
|
||||
struct dirent *entry = readdir(dir);
|
||||
if(!entry)
|
||||
break;
|
||||
|
||||
if(strncmp(entry->d_name, "event", 5) != 0)
|
||||
continue;
|
||||
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/%s", entry->d_name);
|
||||
add_device(dev_input_filepath, false);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::add_device(const char *dev_input_filepath, bool print_error) {
|
||||
if(num_poll_fd >= max_js_poll_fd) {
|
||||
fprintf(stderr, "Warning: failed to add joystick device %s, too many joysticks have been added\n", dev_input_filepath);
|
||||
return false;
|
||||
}
|
||||
|
||||
const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
|
||||
if(dev_input_id == -1)
|
||||
return false;
|
||||
|
||||
const int fd = open(dev_input_filepath, O_RDONLY);
|
||||
if(fd <= 0) {
|
||||
if(print_error)
|
||||
fprintf(stderr, "Error: failed to add joystick %s, error: %s\n", dev_input_filepath, strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!is_input_device_joystick(fd)) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
fds_to_close.push_back(fd);
|
||||
}
|
||||
close_fd_cv.notify_one();
|
||||
return false;
|
||||
}
|
||||
|
||||
poll_fd[num_poll_fd] = {
|
||||
fd,
|
||||
POLLIN,
|
||||
0
|
||||
};
|
||||
|
||||
extra_data[num_poll_fd] = {
|
||||
dev_input_id
|
||||
};
|
||||
|
||||
//const DeviceId device_id = joystick_get_device_id(dev_input_filepath);
|
||||
|
||||
++num_poll_fd;
|
||||
fprintf(stderr, "Info: added joystick: %s\n", dev_input_filepath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
|
||||
const int dev_input_id = get_dev_input_event_id_from_filepath(dev_input_filepath);
|
||||
if(dev_input_id == -1)
|
||||
return false;
|
||||
|
||||
const int poll_fd_index = get_poll_fd_index_by_dev_input_id(dev_input_id);
|
||||
if(poll_fd_index == -1)
|
||||
return false;
|
||||
|
||||
fprintf(stderr, "Info: removed joystick: %s\n", dev_input_filepath);
|
||||
return remove_poll_fd(poll_fd_index);
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::remove_poll_fd(int index) {
|
||||
if(index < 0 || index >= num_poll_fd)
|
||||
return false;
|
||||
|
||||
if(poll_fd[index].fd > 0) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(close_fd_mutex);
|
||||
fds_to_close.push_back(poll_fd[index].fd);
|
||||
}
|
||||
close_fd_cv.notify_one();
|
||||
}
|
||||
|
||||
for(int i = index + 1; i < num_poll_fd; ++i) {
|
||||
poll_fd[i - 1] = poll_fd[i];
|
||||
extra_data[i - 1] = extra_data[i];
|
||||
}
|
||||
--num_poll_fd;
|
||||
return true;
|
||||
}
|
||||
|
||||
int GlobalHotkeysJoystick::get_poll_fd_index_by_dev_input_id(int dev_input_id) const {
|
||||
for(int i = 0; i < num_poll_fd; ++i) {
|
||||
if(dev_input_id == extra_data[i].dev_input_id)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
278
src/GlobalHotkeys/GlobalHotkeysLinux.cpp
Normal file
@@ -0,0 +1,278 @@
|
||||
#include "../../include/GlobalHotkeys/GlobalHotkeysLinux.hpp"
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
extern "C" {
|
||||
#include <mgl/mgl.h>
|
||||
}
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
|
||||
#define PIPE_READ 0
|
||||
#define PIPE_WRITE 1
|
||||
|
||||
namespace gsr {
|
||||
static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) {
|
||||
switch(grab_type) {
|
||||
case GlobalHotkeysLinux::GrabType::ALL: return "--all";
|
||||
case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
|
||||
case GlobalHotkeysLinux::GrabType::NO_GRAB: return "--no-grab";
|
||||
}
|
||||
return "--all";
|
||||
}
|
||||
|
||||
static inline uint8_t x11_keycode_to_linux_keycode(uint8_t code) {
|
||||
return code - 8;
|
||||
}
|
||||
|
||||
static std::vector<uint8_t> modifiers_to_linux_keys(uint32_t modifiers) {
|
||||
std::vector<uint8_t> result;
|
||||
if(modifiers & HOTKEY_MOD_LSHIFT)
|
||||
result.push_back(KEY_LEFTSHIFT);
|
||||
if(modifiers & HOTKEY_MOD_RSHIFT)
|
||||
result.push_back(KEY_RIGHTSHIFT);
|
||||
if(modifiers & HOTKEY_MOD_LCTRL)
|
||||
result.push_back(KEY_LEFTCTRL);
|
||||
if(modifiers & HOTKEY_MOD_RCTRL)
|
||||
result.push_back(KEY_RIGHTCTRL);
|
||||
if(modifiers & HOTKEY_MOD_LALT)
|
||||
result.push_back(KEY_LEFTALT);
|
||||
if(modifiers & HOTKEY_MOD_RALT)
|
||||
result.push_back(KEY_RIGHTALT);
|
||||
if(modifiers & HOTKEY_MOD_LSUPER)
|
||||
result.push_back(KEY_LEFTMETA);
|
||||
if(modifiers & HOTKEY_MOD_RSUPER)
|
||||
result.push_back(KEY_RIGHTMETA);
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string linux_keys_to_command_string(const uint8_t *keys, size_t size) {
|
||||
std::string result;
|
||||
for(size_t i = 0; i < size; ++i) {
|
||||
if(!result.empty())
|
||||
result += "+";
|
||||
result += std::to_string(keys[i]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool x11_key_is_alpha_numerical(KeySym keysym) {
|
||||
return (keysym >= XK_A && keysym <= XK_Z) || (keysym >= XK_a && keysym <= XK_z) || (keysym >= XK_0 && keysym <= XK_9);
|
||||
}
|
||||
|
||||
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
read_pipes[i] = -1;
|
||||
write_pipes[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalHotkeysLinux::~GlobalHotkeysLinux() {
|
||||
if(write_pipes[PIPE_WRITE] > 0) {
|
||||
char command[32];
|
||||
const int command_size = snprintf(command, sizeof(command), "exit\n");
|
||||
if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
|
||||
fprintf(stderr, "Error: GlobalHotkeysLinux::~GlobalHotkeysLinux: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
|
||||
close_fds();
|
||||
}
|
||||
} else {
|
||||
close_fds();
|
||||
}
|
||||
|
||||
if(process_id > 0) {
|
||||
int status;
|
||||
waitpid(process_id, &status, 0);
|
||||
}
|
||||
|
||||
close_fds();
|
||||
}
|
||||
|
||||
void GlobalHotkeysLinux::close_fds() {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
if(read_pipes[i] > 0) {
|
||||
close(read_pipes[i]);
|
||||
read_pipes[i] = -1;
|
||||
}
|
||||
|
||||
if(write_pipes[i] > 0) {
|
||||
close(write_pipes[i]);
|
||||
write_pipes[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
if(read_file) {
|
||||
fclose(read_file);
|
||||
read_file = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalHotkeysLinux::start() {
|
||||
const char *grab_type_arg = grab_type_to_arg(grab_type);
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
const char *user_homepath = getenv("HOME");
|
||||
if(!user_homepath)
|
||||
user_homepath = "/tmp";
|
||||
|
||||
if(process_id > 0)
|
||||
return false;
|
||||
|
||||
if(pipe(read_pipes) == -1)
|
||||
return false;
|
||||
|
||||
if(pipe(write_pipes) == -1) {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(read_pipes[i]);
|
||||
read_pipes[i] = -1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const pid_t pid = vfork();
|
||||
if(pid == -1) {
|
||||
perror("Failed to vfork");
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(read_pipes[i]);
|
||||
close(write_pipes[i]);
|
||||
read_pipes[i] = -1;
|
||||
write_pipes[i] = -1;
|
||||
}
|
||||
return false;
|
||||
} else if(pid == 0) { /* child */
|
||||
dup2(read_pipes[PIPE_WRITE], STDOUT_FILENO);
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(read_pipes[i]);
|
||||
}
|
||||
|
||||
dup2(write_pipes[PIPE_READ], STDIN_FILENO);
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(write_pipes[i]);
|
||||
}
|
||||
|
||||
if(inside_flatpak) {
|
||||
const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
} else {
|
||||
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
}
|
||||
|
||||
perror("gsr-global-hotkeys");
|
||||
_exit(127);
|
||||
} else { /* parent */
|
||||
process_id = pid;
|
||||
|
||||
close(read_pipes[PIPE_WRITE]);
|
||||
read_pipes[PIPE_WRITE] = -1;
|
||||
|
||||
close(write_pipes[PIPE_READ]);
|
||||
write_pipes[PIPE_READ] = -1;
|
||||
|
||||
fcntl(read_pipes[PIPE_READ], F_SETFL, fcntl(read_pipes[PIPE_READ], F_GETFL) | O_NONBLOCK);
|
||||
read_file = fdopen(read_pipes[PIPE_READ], "r");
|
||||
if(read_file)
|
||||
read_pipes[PIPE_READ] = -1;
|
||||
else
|
||||
fprintf(stderr, "fdopen failed for read, error: %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalHotkeysLinux::bind_key_press(Hotkey hotkey, const std::string &id, GlobalHotkeyCallback callback) {
|
||||
if(process_id <= 0)
|
||||
return false;
|
||||
|
||||
if(bound_actions_by_id.find(id) != bound_actions_by_id.end())
|
||||
return false;
|
||||
|
||||
if(id.find(' ') != std::string::npos || id.find('\n') != std::string::npos) {
|
||||
fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: id \"%s\" contains either space or newline\n", id.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
if(hotkey.key == 0 || hotkey.key == XK_VoidSymbol) {
|
||||
//fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(hotkey.modifiers == 0 && x11_key_is_alpha_numerical(hotkey.key)) {
|
||||
//fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a modifier\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
mgl_context *context = mgl_get_context();
|
||||
Display *display = (Display*)context->connection;
|
||||
const uint8_t keycode = x11_keycode_to_linux_keycode(XKeysymToKeycode(display, hotkey.key));
|
||||
const std::vector<uint8_t> modifiers = modifiers_to_linux_keys(hotkey.modifiers);
|
||||
const std::string modifiers_command = linux_keys_to_command_string(modifiers.data(), modifiers.size());
|
||||
|
||||
char command[256];
|
||||
int command_size = 0;
|
||||
if(modifiers_command.empty())
|
||||
command_size = snprintf(command, sizeof(command), "bind %s %d\n", id.c_str(), (int)keycode);
|
||||
else
|
||||
command_size = snprintf(command, sizeof(command), "bind %s %d+%s\n", id.c_str(), (int)keycode, modifiers_command.c_str());
|
||||
|
||||
if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
|
||||
fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
bound_actions_by_id[id] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
void GlobalHotkeysLinux::unbind_all_keys() {
|
||||
if(process_id <= 0)
|
||||
return;
|
||||
|
||||
if(bound_actions_by_id.empty())
|
||||
return;
|
||||
|
||||
char command[32];
|
||||
const int command_size = snprintf(command, sizeof(command), "unbind_all\n");
|
||||
if(write(write_pipes[PIPE_WRITE], command, command_size) != command_size) {
|
||||
fprintf(stderr, "Error: GlobalHotkeysLinux::unbind_all_keys: failed to write command to gsr-global-hotkeys, error: %s\n", strerror(errno));
|
||||
}
|
||||
bound_actions_by_id.clear();
|
||||
}
|
||||
|
||||
void GlobalHotkeysLinux::poll_events() {
|
||||
if(process_id <= 0) {
|
||||
//fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!read_file) {
|
||||
//fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string action;
|
||||
char buffer[256];
|
||||
while(true) {
|
||||
char *line = fgets(buffer, sizeof(buffer), read_file);
|
||||
if(!line)
|
||||
break;
|
||||
|
||||
int line_len = strlen(line);
|
||||
if(line_len == 0)
|
||||
continue;
|
||||
|
||||
if(line[line_len - 1] == '\n') {
|
||||
line[line_len - 1] = '\0';
|
||||
--line_len;
|
||||
}
|
||||
|
||||
action = line;
|
||||
auto it = bound_actions_by_id.find(action);
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second(action);
|
||||
else if(on_gsr_ui_virtual_keyboard_grabbed && action == "gsr-ui-virtual-keyboard-grabbed")
|
||||
on_gsr_ui_virtual_keyboard_grabbed();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "../include/GlobalHotkeysX11.hpp"
|
||||
#include "../../include/GlobalHotkeys/GlobalHotkeysX11.hpp"
|
||||
#include <X11/keysym.h>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <assert.h>
|
||||
@@ -50,6 +50,27 @@ namespace gsr {
|
||||
return mask;
|
||||
}
|
||||
|
||||
static uint32_t modifiers_to_x11_modifiers(uint32_t modifiers) {
|
||||
uint32_t result = 0;
|
||||
if(modifiers & HOTKEY_MOD_LSHIFT)
|
||||
result |= ShiftMask;
|
||||
if(modifiers & HOTKEY_MOD_RSHIFT)
|
||||
result |= ShiftMask;
|
||||
if(modifiers & HOTKEY_MOD_LCTRL)
|
||||
result |= ControlMask;
|
||||
if(modifiers & HOTKEY_MOD_RCTRL)
|
||||
result |= ControlMask;
|
||||
if(modifiers & HOTKEY_MOD_LALT)
|
||||
result |= Mod1Mask;
|
||||
if(modifiers & HOTKEY_MOD_RALT)
|
||||
result |= Mod5Mask;
|
||||
if(modifiers & HOTKEY_MOD_LSUPER)
|
||||
result |= Mod4Mask;
|
||||
if(modifiers & HOTKEY_MOD_RSUPER)
|
||||
result |= Mod4Mask;
|
||||
return result;
|
||||
}
|
||||
|
||||
GlobalHotkeysX11::GlobalHotkeysX11() {
|
||||
dpy = XOpenDisplay(NULL);
|
||||
if(!dpy)
|
||||
@@ -74,16 +95,17 @@ namespace gsr {
|
||||
x_failed = false;
|
||||
XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error);
|
||||
|
||||
const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers);
|
||||
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
|
||||
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync);
|
||||
XGrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy), False, GrabModeAsync, GrabModeAsync);
|
||||
}
|
||||
XSync(dpy, False);
|
||||
|
||||
if(x_failed) {
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
|
||||
}
|
||||
XSync(dpy, False);
|
||||
XSetErrorHandler(prev_xerror);
|
||||
@@ -106,10 +128,11 @@ namespace gsr {
|
||||
x_failed = false;
|
||||
XErrorHandler prev_xerror = XSetErrorHandler(xerror_grab_error);
|
||||
|
||||
const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers);
|
||||
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
|
||||
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
|
||||
}
|
||||
XSync(dpy, False);
|
||||
|
||||
@@ -127,8 +150,9 @@ namespace gsr {
|
||||
unsigned int numlock_mask = x11_get_numlock_mask(dpy);
|
||||
unsigned int modifiers[] = { 0, LockMask, numlock_mask, numlock_mask|LockMask };
|
||||
for(auto it = bound_keys_by_id.begin(); it != bound_keys_by_id.end();) {
|
||||
const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(it->second.hotkey.modifiers);
|
||||
for(int i = 0; i < 4; ++i) {
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), it->second.hotkey.modifiers | modifiers[i], DefaultRootWindow(dpy));
|
||||
XUngrabKey(dpy, XKeysymToKeycode(dpy, it->second.hotkey.key), modifiers_x11 | modifiers[i], DefaultRootWindow(dpy));
|
||||
}
|
||||
}
|
||||
bound_keys_by_id.clear();
|
||||
@@ -138,11 +162,14 @@ namespace gsr {
|
||||
}
|
||||
|
||||
void GlobalHotkeysX11::poll_events() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
if(xev.type == KeyPress) {
|
||||
const KeySym key_sym = XLookupKeysym(&xev.xkey, 0);
|
||||
call_hotkey_callback({ key_sym, xev.xkey.state });
|
||||
call_hotkey_callback({ (uint32_t)key_sym, xev.xkey.state });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,7 +181,7 @@ namespace gsr {
|
||||
// Note: not all keys are mapped in mgl_key_to_key_sym. If more hotkeys are added or changed then add the key mapping there
|
||||
const KeySym key_sym = mgl_key_to_key_sym(event.key.code);
|
||||
const uint32_t modifiers = mgl_key_modifiers_to_x11_modifier_mask(event.key);
|
||||
return !call_hotkey_callback(Hotkey{key_sym, modifiers});
|
||||
return !call_hotkey_callback(Hotkey{(uint32_t)key_sym, modifiers});
|
||||
}
|
||||
|
||||
static unsigned int key_state_without_locks(unsigned int key_state) {
|
||||
@@ -162,8 +189,9 @@ namespace gsr {
|
||||
}
|
||||
|
||||
bool GlobalHotkeysX11::call_hotkey_callback(Hotkey hotkey) const {
|
||||
const uint32_t modifiers_x11 = modifiers_to_x11_modifiers(hotkey.modifiers);
|
||||
for(const auto &[key, val] : bound_keys_by_id) {
|
||||
if(val.hotkey.key == hotkey.key && key_state_without_locks(val.hotkey.modifiers) == key_state_without_locks(hotkey.modifiers)) {
|
||||
if(val.hotkey.key == hotkey.key && key_state_without_locks(modifiers_to_x11_modifiers(val.hotkey.modifiers)) == key_state_without_locks(modifiers_x11)) {
|
||||
val.callback(key);
|
||||
return true;
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
#include "../include/GlobalHotkeysLinux.hpp"
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#define PIPE_READ 0
|
||||
#define PIPE_WRITE 1
|
||||
|
||||
namespace gsr {
|
||||
static const char* grab_type_to_arg(GlobalHotkeysLinux::GrabType grab_type) {
|
||||
switch(grab_type) {
|
||||
case GlobalHotkeysLinux::GrabType::ALL: return "--all";
|
||||
case GlobalHotkeysLinux::GrabType::VIRTUAL: return "--virtual";
|
||||
}
|
||||
return "--all";
|
||||
}
|
||||
|
||||
GlobalHotkeysLinux::GlobalHotkeysLinux(GrabType grab_type) : grab_type(grab_type) {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
pipes[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
GlobalHotkeysLinux::~GlobalHotkeysLinux() {
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
if(pipes[i] > 0)
|
||||
close(pipes[i]);
|
||||
}
|
||||
|
||||
if(read_file)
|
||||
fclose(read_file);
|
||||
|
||||
if(process_id > 0) {
|
||||
kill(process_id, SIGKILL);
|
||||
int status;
|
||||
waitpid(process_id, &status, 0);
|
||||
}
|
||||
}
|
||||
|
||||
bool GlobalHotkeysLinux::start() {
|
||||
const char *grab_type_arg = grab_type_to_arg(grab_type);
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
const char *user_homepath = getenv("HOME");
|
||||
if(!user_homepath)
|
||||
user_homepath = "/tmp";
|
||||
|
||||
char gsr_global_hotkeys_flatpak[PATH_MAX];
|
||||
snprintf(gsr_global_hotkeys_flatpak, sizeof(gsr_global_hotkeys_flatpak), "%s/.local/share/gpu-screen-recorder/gsr-global-hotkeys", user_homepath);
|
||||
|
||||
if(process_id > 0)
|
||||
return false;
|
||||
|
||||
if(pipe(pipes) == -1)
|
||||
return false;
|
||||
|
||||
const pid_t pid = vfork();
|
||||
if(pid == -1) {
|
||||
perror("Failed to vfork");
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(pipes[i]);
|
||||
pipes[i] = -1;
|
||||
}
|
||||
return false;
|
||||
} else if(pid == 0) { /* child */
|
||||
dup2(pipes[PIPE_WRITE], STDOUT_FILENO);
|
||||
for(int i = 0; i < 2; ++i) {
|
||||
close(pipes[i]);
|
||||
}
|
||||
|
||||
if(inside_flatpak) {
|
||||
const char *args[] = { "flatpak-spawn", "--host", "--", gsr_global_hotkeys_flatpak, grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
} else {
|
||||
const char *args[] = { "gsr-global-hotkeys", grab_type_arg, nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
}
|
||||
|
||||
perror("gsr-global-hotkeys");
|
||||
_exit(127);
|
||||
} else { /* parent */
|
||||
process_id = pid;
|
||||
close(pipes[PIPE_WRITE]);
|
||||
pipes[PIPE_WRITE] = -1;
|
||||
|
||||
const int fdl = fcntl(pipes[PIPE_READ], F_GETFL);
|
||||
fcntl(pipes[PIPE_READ], F_SETFL, fdl | O_NONBLOCK);
|
||||
|
||||
read_file = fdopen(pipes[PIPE_READ], "r");
|
||||
if(read_file)
|
||||
pipes[PIPE_READ] = -1;
|
||||
else
|
||||
fprintf(stderr, "fdopen failed, error: %s\n", strerror(errno));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GlobalHotkeysLinux::bind_action(const std::string &id, GlobalHotkeyCallback callback) {
|
||||
return bound_actions_by_id.insert(std::make_pair(id, std::move(callback))).second;
|
||||
}
|
||||
|
||||
void GlobalHotkeysLinux::poll_events() {
|
||||
if(process_id <= 0) {
|
||||
fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, process has not been started yet. Use GlobalHotkeysLinux::start to start the process first\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(!read_file) {
|
||||
fprintf(stderr, "error: GlobalHotkeysLinux::poll_events failed, read file hasn't opened\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string action;
|
||||
char buffer[256];
|
||||
while(true) {
|
||||
char *line = fgets(buffer, sizeof(buffer), read_file);
|
||||
if(!line)
|
||||
break;
|
||||
|
||||
int line_len = strlen(line);
|
||||
if(line_len == 0)
|
||||
continue;
|
||||
|
||||
if(line[line_len - 1] == '\n') {
|
||||
line[line_len - 1] = '\0';
|
||||
--line_len;
|
||||
}
|
||||
|
||||
action = line;
|
||||
auto it = bound_actions_by_id.find(action);
|
||||
if(it != bound_actions_by_id.end())
|
||||
it->second(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
234
src/GsrInfo.cpp
@@ -6,6 +6,93 @@
|
||||
#include <string.h>
|
||||
|
||||
namespace gsr {
|
||||
bool GsrVersion::operator>(const GsrVersion &other) const {
|
||||
return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch > other.patch);
|
||||
}
|
||||
|
||||
bool GsrVersion::operator>=(const GsrVersion &other) const {
|
||||
return major > other.major || (major == other.major && minor > other.minor) || (major == other.major && minor == other.minor && patch >= other.patch);
|
||||
}
|
||||
|
||||
bool GsrVersion::operator<(const GsrVersion &other) const {
|
||||
return !operator>=(other);
|
||||
}
|
||||
|
||||
bool GsrVersion::operator<=(const GsrVersion &other) const {
|
||||
return !operator>(other);
|
||||
}
|
||||
|
||||
bool GsrVersion::operator==(const GsrVersion &other) const {
|
||||
return major == other.major && minor == other.minor && patch == other.patch;
|
||||
}
|
||||
|
||||
bool GsrVersion::operator!=(const GsrVersion &other) const {
|
||||
return !operator==(other);
|
||||
}
|
||||
|
||||
std::string GsrVersion::to_string() const {
|
||||
std::string result;
|
||||
if(major == 0 && minor == 0 && patch == 0)
|
||||
result = "Unknown";
|
||||
else
|
||||
result = std::to_string(major) + "." + std::to_string(minor) + "." + std::to_string(patch);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* Returns -1 on error */
|
||||
static int parse_u8(const char *str, int size) {
|
||||
if(size <= 0)
|
||||
return -1;
|
||||
|
||||
int result = 0;
|
||||
for(int i = 0; i < size; ++i) {
|
||||
char c = str[i];
|
||||
if(c >= '0' && c <= '9') {
|
||||
result = result * 10 + (c - '0');
|
||||
if(result > 255)
|
||||
return -1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static GsrVersion parse_gsr_version(const std::string_view str) {
|
||||
GsrVersion result;
|
||||
uint8_t numbers[3];
|
||||
int number_index = 0;
|
||||
|
||||
size_t index = 0;
|
||||
while(true) {
|
||||
size_t next_index = str.find('.', index);
|
||||
if(next_index == std::string::npos)
|
||||
next_index = str.size();
|
||||
|
||||
const int number = parse_u8(str.data() + index, next_index - index);
|
||||
if(number == -1) {
|
||||
fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data());
|
||||
return {0, 0, 0};
|
||||
}
|
||||
|
||||
if(number_index >= 3) {
|
||||
fprintf(stderr, "Error: gpu-screen-recorder --info contains invalid gsr version: %.*s\n", (int)str.size(), str.data());
|
||||
return {0, 0, 0};
|
||||
}
|
||||
|
||||
numbers[number_index] = number;
|
||||
++number_index;
|
||||
index = next_index + 1;
|
||||
if(next_index == str.size())
|
||||
break;
|
||||
}
|
||||
|
||||
result.major = numbers[0];
|
||||
result.minor = numbers[1];
|
||||
result.patch = numbers[2];
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::optional<KeyValue> parse_key_value(std::string_view line) {
|
||||
const size_t space_index = line.find('|');
|
||||
if(space_index == std::string_view::npos)
|
||||
@@ -25,6 +112,8 @@ namespace gsr {
|
||||
gsr_info->system_info.display_server = DisplayServer::WAYLAND;
|
||||
} else if(key_value->key == "supports_app_audio") {
|
||||
gsr_info->system_info.supports_app_audio = key_value->value == "yes";
|
||||
} else if(key_value->key == "gsr_version") {
|
||||
gsr_info->system_info.gsr_version = parse_gsr_version(key_value->value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +129,8 @@ namespace gsr {
|
||||
gsr_info->gpu_info.vendor = GpuVendor::INTEL;
|
||||
else if(key_value->value == "nvidia")
|
||||
gsr_info->gpu_info.vendor = GpuVendor::NVIDIA;
|
||||
else if(key_value->value == "broadcom")
|
||||
gsr_info->gpu_info.vendor = GpuVendor::BROADCOM;
|
||||
} else if(key_value->key == "card_path") {
|
||||
gsr_info->gpu_info.card_path = key_value->value;
|
||||
}
|
||||
@@ -68,19 +159,22 @@ namespace gsr {
|
||||
gsr_info->supported_video_codecs.vp9 = true;
|
||||
}
|
||||
|
||||
static void parse_image_formats_line(GsrInfo *gsr_info, std::string_view line) {
|
||||
if(line == "jpeg")
|
||||
gsr_info->supported_image_formats.jpeg = true;
|
||||
else if(line == "png")
|
||||
gsr_info->supported_image_formats.png = true;
|
||||
}
|
||||
|
||||
enum class GsrInfoSection {
|
||||
UNKNOWN,
|
||||
SYSTEM_INFO,
|
||||
GPU_INFO,
|
||||
VIDEO_CODECS,
|
||||
IMAGE_FORMATS,
|
||||
CAPTURE_OPTIONS
|
||||
};
|
||||
|
||||
static bool starts_with(std::string_view str, const char *substr) {
|
||||
size_t len = strlen(substr);
|
||||
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
|
||||
}
|
||||
|
||||
GsrInfoExitStatus get_gpu_screen_recorder_info(GsrInfo *gsr_info) {
|
||||
*gsr_info = GsrInfo{};
|
||||
|
||||
@@ -105,6 +199,8 @@ namespace gsr {
|
||||
section = GsrInfoSection::GPU_INFO;
|
||||
else if(section_name == "video_codecs")
|
||||
section = GsrInfoSection::VIDEO_CODECS;
|
||||
else if(section_name == "image_formats")
|
||||
section = GsrInfoSection::IMAGE_FORMATS;
|
||||
else if(section_name == "capture_options")
|
||||
section = GsrInfoSection::CAPTURE_OPTIONS;
|
||||
else
|
||||
@@ -128,6 +224,10 @@ namespace gsr {
|
||||
parse_video_codecs_line(gsr_info, line);
|
||||
break;
|
||||
}
|
||||
case GsrInfoSection::IMAGE_FORMATS: {
|
||||
parse_image_formats_line(gsr_info, line);
|
||||
break;
|
||||
}
|
||||
case GsrInfoSection::CAPTURE_OPTIONS: {
|
||||
// Intentionally ignore, get capture options with get_supported_capture_options instead
|
||||
break;
|
||||
@@ -155,7 +255,7 @@ namespace gsr {
|
||||
|
||||
std::string stdout_str;
|
||||
const char *args[] = { "gpu-screen-recorder", "--list-audio-devices", nullptr };
|
||||
if(exec_program_get_stdout(args, stdout_str) != 0) {
|
||||
if(exec_program_get_stdout(args, stdout_str, false) != 0) {
|
||||
fprintf(stderr, "error: 'gpu-screen-recorder --list-audio-devices' failed\n");
|
||||
return audio_devices;
|
||||
}
|
||||
@@ -188,6 +288,58 @@ namespace gsr {
|
||||
return application_audio;
|
||||
}
|
||||
|
||||
struct KeyValue3 {
|
||||
std::string_view value1;
|
||||
std::string_view value2;
|
||||
std::string_view value3;
|
||||
};
|
||||
|
||||
static std::optional<KeyValue3> parse_3(std::string_view line) {
|
||||
const size_t space_index1 = line.find('|');
|
||||
if(space_index1 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
const size_t space_index2 = line.find('|', space_index1 + 1);
|
||||
if(space_index2 == std::string_view::npos)
|
||||
return std::nullopt;
|
||||
|
||||
return KeyValue3{
|
||||
line.substr(0, space_index1),
|
||||
line.substr(space_index1 + 1, space_index2 - (space_index1 + 1)),
|
||||
line.substr(space_index2 + 1),
|
||||
};
|
||||
}
|
||||
|
||||
static bool parse_camera_pixel_format(std::string_view line, GsrCameraPixelFormat &pixel_format) {
|
||||
if(line == "yuyv") {
|
||||
pixel_format = YUYV;
|
||||
return true;
|
||||
} else if(line == "mjpeg") {
|
||||
pixel_format = MJPEG;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool capture_option_line_to_camera(std::string_view line, std::string &path, GsrCameraSetup &camera_setup, GsrCameraPixelFormat &pixel_format) {
|
||||
const std::optional<KeyValue3> key_value3 = parse_3(line);
|
||||
if(!key_value3)
|
||||
return false;
|
||||
|
||||
path = key_value3->value1;
|
||||
|
||||
char value_buffer[256];
|
||||
snprintf(value_buffer, sizeof(value_buffer), "%.*s", (int)key_value3->value2.size(), key_value3->value2.data());
|
||||
if(sscanf(value_buffer, "%dx%d@%dhz", &camera_setup.resolution.x, &camera_setup.resolution.y, &camera_setup.fps) != 3)
|
||||
return false;
|
||||
|
||||
if(!parse_camera_pixel_format(key_value3->value3, pixel_format))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static std::optional<GsrMonitor> capture_option_line_to_monitor(std::string_view line) {
|
||||
std::optional<GsrMonitor> monitor;
|
||||
const std::optional<KeyValue> key_value = parse_key_value(line);
|
||||
@@ -204,14 +356,49 @@ namespace gsr {
|
||||
return monitor;
|
||||
}
|
||||
|
||||
static GsrCamera* get_gsr_camera_by_path(std::vector<GsrCamera> &cameras, const std::string &path) {
|
||||
for(GsrCamera &camera : cameras) {
|
||||
if(camera.path == path)
|
||||
return &camera;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void parse_camera_line(std::string_view line, std::vector<GsrCamera> &cameras) {
|
||||
std::string camera_path;
|
||||
GsrCameraSetup camera_setup;
|
||||
GsrCameraPixelFormat pixel_format;
|
||||
if(!capture_option_line_to_camera(line, camera_path, camera_setup, pixel_format))
|
||||
return;
|
||||
|
||||
GsrCamera *existing_camera = get_gsr_camera_by_path(cameras, camera_path);
|
||||
if(!existing_camera) {
|
||||
cameras.push_back(GsrCamera{camera_path, std::vector<GsrCameraSetup>{}, std::vector<GsrCameraSetup>{}});
|
||||
existing_camera = &cameras.back();
|
||||
}
|
||||
|
||||
switch(pixel_format) {
|
||||
case YUYV:
|
||||
existing_camera->yuyv_setups.push_back(camera_setup);
|
||||
break;
|
||||
case MJPEG:
|
||||
existing_camera->mjpeg_setups.push_back(camera_setup);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void parse_capture_options_line(SupportedCaptureOptions &capture_options, std::string_view line) {
|
||||
if(line == "window")
|
||||
if(line == "window") {
|
||||
capture_options.window = true;
|
||||
else if(line == "focused")
|
||||
} else if(line == "region") {
|
||||
capture_options.region = true;
|
||||
} else if(line == "focused") {
|
||||
capture_options.focused = true;
|
||||
else if(line == "portal")
|
||||
} else if(line == "portal") {
|
||||
capture_options.portal = true;
|
||||
else {
|
||||
} else if(!line.empty() && line[0] == '/') {
|
||||
parse_camera_line(line, capture_options.cameras);
|
||||
} else {
|
||||
std::optional<GsrMonitor> monitor = capture_option_line_to_monitor(line);
|
||||
if(monitor)
|
||||
capture_options.monitors.push_back(std::move(monitor.value()));
|
||||
@@ -220,10 +407,11 @@ namespace gsr {
|
||||
|
||||
static const char* gpu_vendor_to_string(GpuVendor vendor) {
|
||||
switch(vendor) {
|
||||
case GpuVendor::UNKNOWN: return "unknown";
|
||||
case GpuVendor::AMD: return "amd";
|
||||
case GpuVendor::INTEL: return "intel";
|
||||
case GpuVendor::NVIDIA: return "nvidia";
|
||||
case GpuVendor::UNKNOWN: return "unknown";
|
||||
case GpuVendor::AMD: return "amd";
|
||||
case GpuVendor::INTEL: return "intel";
|
||||
case GpuVendor::NVIDIA: return "nvidia";
|
||||
case GpuVendor::BROADCOM: return "broadcom";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
@@ -245,4 +433,22 @@ namespace gsr {
|
||||
|
||||
return capture_options;
|
||||
}
|
||||
|
||||
std::vector<GsrCamera> get_v4l2_devices() {
|
||||
std::vector<GsrCamera> cameras;
|
||||
|
||||
std::string stdout_str;
|
||||
const char *args[] = { "gpu-screen-recorder", "--list-v4l2-devices", nullptr };
|
||||
if(exec_program_get_stdout(args, stdout_str) != 0) {
|
||||
fprintf(stderr, "error: 'gpu-screen-recorder --list-v4l2-devices' failed\n");
|
||||
return cameras;
|
||||
}
|
||||
|
||||
string_split_char(stdout_str, '\n', [&](std::string_view line) {
|
||||
parse_camera_line(line, cameras);
|
||||
return true;
|
||||
});
|
||||
|
||||
return cameras;
|
||||
}
|
||||
}
|
||||
|
||||
81
src/Hotplug.cpp
Normal file
@@ -0,0 +1,81 @@
|
||||
#include "../include/Hotplug.hpp"
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <linux/types.h>
|
||||
#include <linux/netlink.h>
|
||||
|
||||
namespace gsr {
|
||||
Hotplug::~Hotplug() {
|
||||
if(fd > 0)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
bool Hotplug::start() {
|
||||
if(started)
|
||||
return false;
|
||||
|
||||
struct sockaddr_nl nls = {
|
||||
AF_NETLINK,
|
||||
0,
|
||||
(unsigned int)getpid(),
|
||||
(unsigned int)-1
|
||||
};
|
||||
|
||||
fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
|
||||
if(fd == -1)
|
||||
return false; /* Not root user */
|
||||
|
||||
if(bind(fd, (const struct sockaddr*)&nls, sizeof(struct sockaddr_nl))) {
|
||||
close(fd);
|
||||
fd = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
started = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
int Hotplug::steal_fd() {
|
||||
const int val = fd;
|
||||
fd = -1;
|
||||
return val;
|
||||
}
|
||||
|
||||
void Hotplug::process_event_data(int fd, const HotplugEventCallback &callback) {
|
||||
const int bytes_read = read(fd, event_data, sizeof(event_data) - 1);
|
||||
if(bytes_read <= 0)
|
||||
return;
|
||||
event_data[bytes_read] = '\0';
|
||||
|
||||
/* Hotplug data ends with a newline and a null terminator */
|
||||
int data_index = 0;
|
||||
while(data_index < bytes_read) {
|
||||
parse_netlink_data(event_data + data_index, callback);
|
||||
data_index += strlen(event_data + data_index) + 1; /* Skip null terminator as well */
|
||||
}
|
||||
}
|
||||
|
||||
/* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */
|
||||
void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) {
|
||||
if(strncmp(line, "ACTION=", 7) == 0) {
|
||||
event_is_add = strncmp(line+7, "add", 3) == 0;
|
||||
event_is_remove = strncmp(line+7, "remove", 6) == 0;
|
||||
subsystem_is_input = false;
|
||||
} else if(event_is_add || event_is_remove) {
|
||||
if(strcmp(line, "SUBSYSTEM=input") == 0)
|
||||
subsystem_is_input = true;
|
||||
|
||||
if(subsystem_is_input && strncmp(line, "DEVNAME=", 8) == 0) {
|
||||
if(event_is_add)
|
||||
callback(HotplugAction::ADD, line+8);
|
||||
else if(event_is_remove)
|
||||
callback(HotplugAction::REMOVE, line+8);
|
||||
|
||||
event_is_add = false;
|
||||
event_is_remove = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
126
src/HyprlandWorkaround.cpp
Normal file
@@ -0,0 +1,126 @@
|
||||
#include "../include/HyprlandWorkaround.hpp"
|
||||
#include "../include/Process.hpp"
|
||||
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
namespace gsr {
|
||||
static ActiveHyprlandWindow active_hyprland_window;
|
||||
static bool hyprland_listener_thread_started = false;
|
||||
static std::mutex active_window_mutex;
|
||||
|
||||
static bool get_hyprland_socket_path(char *path, int path_len) {
|
||||
const char* xdg_runtime_dir = getenv("XDG_RUNTIME_DIR");
|
||||
const char* instance_sig = getenv("HYPRLAND_INSTANCE_SIGNATURE");
|
||||
if (!xdg_runtime_dir || !instance_sig) {
|
||||
fprintf(stderr, "Error: HyprlandWorkaround: environment variables not set\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (snprintf(path, path_len, "%s/hypr/%s/.socket2.sock", xdg_runtime_dir, instance_sig) >= path_len) {
|
||||
fprintf(stderr, "Error: HyprlandWorkaround: path to hyprland socket (%s/hypr/%s/.socket2.sock) is more than %d characters long\n", xdg_runtime_dir, instance_sig, path_len);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void hyprland_listener_thread() {
|
||||
char hyprland_socket_path[256];
|
||||
char buffer[4096];
|
||||
const std::string prefix = "Window title changed: ";
|
||||
std::string line;
|
||||
FILE *stdout_file = nullptr;
|
||||
|
||||
// Get path inside the flatpak before flatpak-spawn is called because of a bug in flatpak:
|
||||
// https://github.com/flatpak/flatpak/issues/6486
|
||||
// where environment variables are missing in flatpak-spawn --host
|
||||
if(!get_hyprland_socket_path(hyprland_socket_path, sizeof(hyprland_socket_path))) {
|
||||
fprintf(stderr, "Error: HyprlandWorkaround: failed to get hyprland socket path\n");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
|
||||
|
||||
size_t arg_index = 0;
|
||||
const char *args[6];
|
||||
|
||||
if(inside_flatpak) {
|
||||
args[arg_index++] = "flatpak-spawn";
|
||||
args[arg_index++] = "--host";
|
||||
args[arg_index++] = "--";
|
||||
args[arg_index++] = "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/gsr-hyprland-helper";
|
||||
} else {
|
||||
args[arg_index++] = "gsr-hyprland-helper";
|
||||
}
|
||||
|
||||
args[arg_index++] = hyprland_socket_path;
|
||||
args[arg_index++] = nullptr;
|
||||
|
||||
int read_fd = -1;
|
||||
const pid_t process_id = exec_program(args, &read_fd, false);
|
||||
if(process_id == -1) {
|
||||
fprintf(stderr, "Error: HyprlandWorkaround: failed to execute gsr-hyprland-helper\n");
|
||||
return;
|
||||
}
|
||||
|
||||
stdout_file = fdopen(read_fd, "r");
|
||||
if (!stdout_file) {
|
||||
perror("Error: HyprlandWorkaround: fdopen");
|
||||
goto done;
|
||||
return;
|
||||
}
|
||||
read_fd = -1;
|
||||
|
||||
fprintf(stderr, "Info: HyprlandWorkaround: started Hyprland helper thread\n");
|
||||
|
||||
while (fgets(buffer, sizeof(buffer), stdout_file) != nullptr) {
|
||||
line = buffer;
|
||||
|
||||
if (!line.empty() && line.back() == '\n') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
size_t pos = line.find(prefix);
|
||||
if (pos != std::string::npos) {
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
active_hyprland_window.title = line.substr(pos + prefix.length());
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
if(stdout_file)
|
||||
fclose(stdout_file);
|
||||
|
||||
if(read_fd > 0)
|
||||
close(read_fd);
|
||||
|
||||
if(process_id > 0) {
|
||||
kill(process_id, SIGKILL);
|
||||
int status;
|
||||
if(waitpid(process_id, &status, 0) == -1) {
|
||||
perror("waitpid failed");
|
||||
/* Ignore... */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_current_hyprland_window_title() {
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
return active_hyprland_window.title;
|
||||
}
|
||||
|
||||
void start_hyprland_listener_thread() {
|
||||
if (hyprland_listener_thread_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
hyprland_listener_thread_started = true;
|
||||
|
||||
std::thread([&] {
|
||||
hyprland_listener_thread();
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
80
src/KwinWorkaround.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "../include/KwinWorkaround.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <sys/types.h>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
|
||||
namespace gsr {
|
||||
static ActiveKwinWindow active_kwin_window;
|
||||
static bool kwin_helper_thread_started = false;
|
||||
static std::mutex active_window_mutex;
|
||||
|
||||
void kwin_script_thread() {
|
||||
FILE* pipe = popen("gsr-kwin-helper", "r");
|
||||
if (!pipe) {
|
||||
std::cerr << "Failed to start gsr-kwin-helper process\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::cerr << "Started a KWin helper thread\n";
|
||||
|
||||
char buffer[4096];
|
||||
const std::string prefix_title = "Active window title set to: ";
|
||||
const std::string prefix_fullscreen = "Active window fullscreen state set to: ";
|
||||
const std::string prefix_monitor = "Active window monitor name set to: ";
|
||||
|
||||
std::string line;
|
||||
while (fgets(buffer, sizeof(buffer), pipe) != nullptr) {
|
||||
line = buffer;
|
||||
|
||||
if (!line.empty() && line.back() == '\n') {
|
||||
line.pop_back();
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
size_t pos = std::string::npos;
|
||||
if ((pos = line.find(prefix_title)) != std::string::npos) {
|
||||
std::string title = line.substr(pos + prefix_title.length());
|
||||
active_kwin_window.title = std::move(title);
|
||||
} else if ((pos = line.find(prefix_fullscreen)) != std::string::npos) {
|
||||
std::string fullscreen = line.substr(pos + prefix_fullscreen.length());
|
||||
active_kwin_window.fullscreen = fullscreen == "1";
|
||||
} else if ((pos = line.find(prefix_monitor)) != std::string::npos) {
|
||||
std::string monitorName = line.substr(pos + prefix_monitor.length());
|
||||
active_kwin_window.monitorName = std::move(monitorName);
|
||||
}
|
||||
}
|
||||
|
||||
pclose(pipe);
|
||||
}
|
||||
|
||||
std::string get_current_kwin_window_title() {
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
return active_kwin_window.title;
|
||||
}
|
||||
|
||||
bool get_current_kwin_window_fullscreen() {
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
return active_kwin_window.fullscreen;
|
||||
}
|
||||
|
||||
std::string get_current_kwin_window_monitor_name() {
|
||||
std::lock_guard<std::mutex> lock(active_window_mutex);
|
||||
return active_kwin_window.monitorName;
|
||||
}
|
||||
|
||||
void start_kwin_helper_thread() {
|
||||
if (kwin_helper_thread_started) {
|
||||
return;
|
||||
}
|
||||
|
||||
kwin_helper_thread_started = true;
|
||||
|
||||
std::thread([&] {
|
||||
kwin_script_thread();
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
175
src/LedIndicator.cpp
Normal file
@@ -0,0 +1,175 @@
|
||||
#include "../include/LedIndicator.hpp"
|
||||
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/wait.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
// TODO: Support hotplug for led indicator check (led_brightness_files)
|
||||
|
||||
namespace gsr {
|
||||
static bool string_starts_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static bool string_ends_with(const char *str, const char *sub) {
|
||||
const int str_len = strlen(str);
|
||||
const int sub_len = strlen(sub);
|
||||
return str_len >= sub_len && memcmp(str + str_len - sub_len, sub, sub_len) == 0;
|
||||
}
|
||||
|
||||
static std::vector<int> open_device_leds_brightness_files(const char *led_name_path) {
|
||||
std::vector<int> files;
|
||||
|
||||
DIR *dir = opendir("/sys/class/leds");
|
||||
if(!dir)
|
||||
return files;
|
||||
|
||||
char brightness_filepath[1024];
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
if(entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
if(!string_starts_with(entry->d_name, "input") || !string_ends_with(entry->d_name, led_name_path))
|
||||
continue;
|
||||
|
||||
snprintf(brightness_filepath, sizeof(brightness_filepath), "/sys/class/leds/%s/brightness", entry->d_name);
|
||||
const int led_brightness_file_fd = open(brightness_filepath, O_RDONLY | O_NONBLOCK);
|
||||
if(led_brightness_file_fd > 0)
|
||||
files.push_back(led_brightness_file_fd);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return files;
|
||||
}
|
||||
|
||||
LedIndicator::LedIndicator() {
|
||||
led_brightness_files = open_device_leds_brightness_files("scrolllock");
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
}
|
||||
|
||||
LedIndicator::~LedIndicator() {
|
||||
for(int led_brightness_file_fd : led_brightness_files) {
|
||||
close(led_brightness_file_fd);
|
||||
}
|
||||
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
if(gsr_global_hotkeys_pid > 0) {
|
||||
int status;
|
||||
waitpid(gsr_global_hotkeys_pid, &status, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::set_led(bool enabled) {
|
||||
led_enabled = enabled;
|
||||
perform_blink = false;
|
||||
}
|
||||
|
||||
void LedIndicator::blink() {
|
||||
perform_blink = true;
|
||||
blink_timer.restart();
|
||||
}
|
||||
|
||||
bool LedIndicator::run_gsr_global_hotkeys_set_leds(bool enabled) {
|
||||
if(gsr_global_hotkeys_pid > 0) {
|
||||
int status;
|
||||
if(waitpid(gsr_global_hotkeys_pid, &status, WNOHANG) == 0) {
|
||||
// Still running
|
||||
return false;
|
||||
}
|
||||
gsr_global_hotkeys_pid = -1;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
const char *user_homepath = getenv("HOME");
|
||||
if(!user_homepath)
|
||||
user_homepath = "/tmp";
|
||||
|
||||
gsr_global_hotkeys_pid = vfork();
|
||||
if(gsr_global_hotkeys_pid == -1) {
|
||||
fprintf(stderr, "Error: LedIndicator::run_gsr_global_hotkeys_set_leds: failed to fork\n");
|
||||
return false;
|
||||
} else if(gsr_global_hotkeys_pid == 0) { // Child
|
||||
if(inside_flatpak) {
|
||||
const char *args[] = { "flatpak-spawn", "--host", "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/bin/kms-server-proxy", "launch-gsr-global-hotkeys", user_homepath, "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
} else {
|
||||
const char *args[] = { "gsr-global-hotkeys", "--set-led", "Scroll Lock", enabled ? "on" : "off", nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
}
|
||||
|
||||
perror("gsr-global-hotkeys");
|
||||
_exit(127);
|
||||
return false;
|
||||
} else { // Parent
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::update_led(bool new_state) {
|
||||
if(new_state == led_indicator_on)
|
||||
return;
|
||||
|
||||
if(run_gsr_global_hotkeys_set_leds(new_state))
|
||||
led_indicator_on = new_state;
|
||||
}
|
||||
|
||||
void LedIndicator::update() {
|
||||
update_led_with_active_status();
|
||||
check_led_status_outdated();
|
||||
}
|
||||
|
||||
void LedIndicator::update_led_with_active_status() {
|
||||
if(perform_blink) {
|
||||
const double blink_elapsed_sec = blink_timer.get_elapsed_time_seconds();
|
||||
if(blink_elapsed_sec < 0.2) {
|
||||
update_led(false);
|
||||
} else if(blink_elapsed_sec < 0.4) {
|
||||
update_led(true);
|
||||
} else if(blink_elapsed_sec < 0.6) {
|
||||
update_led(false);
|
||||
} else if(blink_elapsed_sec < 0.8) {
|
||||
update_led(true);
|
||||
} else {
|
||||
perform_blink = false;
|
||||
}
|
||||
} else {
|
||||
update_led(led_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void LedIndicator::check_led_status_outdated() {
|
||||
// The display server will unset our scroll lock led when pressing capslock/numlock as it updates
|
||||
// all leds at the same time (not just the button pressed) (or at least xorg server does that).
|
||||
// When that is done we want to set the scroll lock led on again if it should be on.
|
||||
// TODO: Improve this. Dont do this with a timer.. but inotify doesn't work sysfs. netlink should work (man 7 netlink).
|
||||
if(read_led_brightness_timer.get_elapsed_time_seconds() > 0.2) {
|
||||
read_led_brightness_timer.restart();
|
||||
|
||||
bool any_keyboard_with_led_enabled = false;
|
||||
bool any_keyboard_with_led_disabled = false;
|
||||
char buffer[32];
|
||||
for(int led_brightness_file_fd : led_brightness_files) {
|
||||
const ssize_t bytes_read = read(led_brightness_file_fd, buffer, sizeof(buffer));
|
||||
if(bytes_read > 0) {
|
||||
if(buffer[0] == '0')
|
||||
any_keyboard_with_led_disabled = true;
|
||||
else
|
||||
any_keyboard_with_led_enabled = true;
|
||||
lseek(led_brightness_file_fd, 0, SEEK_SET);
|
||||
}
|
||||
}
|
||||
|
||||
if(led_enabled && any_keyboard_with_led_disabled)
|
||||
run_gsr_global_hotkeys_set_leds(true);
|
||||
else if(!led_enabled && any_keyboard_with_led_enabled)
|
||||
run_gsr_global_hotkeys_set_leds(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
2818
src/Overlay.cpp
@@ -40,12 +40,13 @@ namespace gsr {
|
||||
return num_args;
|
||||
}
|
||||
|
||||
bool exec_program_daemonized(const char **args) {
|
||||
bool exec_program_daemonized(const char **args, bool debug) {
|
||||
/* 1 argument */
|
||||
if(args[0] == nullptr)
|
||||
return false;
|
||||
|
||||
debug_print_args(args);
|
||||
if(debug)
|
||||
debug_print_args(args);
|
||||
|
||||
const pid_t pid = vfork();
|
||||
if(pid == -1) {
|
||||
@@ -72,7 +73,29 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
pid_t exec_program(const char **args, int *read_fd) {
|
||||
bool exec_program_on_host_daemonized(const char **args, bool debug) {
|
||||
if(count_num_args(args) > 64 - 3) {
|
||||
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
if(inside_flatpak) {
|
||||
// Assumes programs wont need more than 64 - 3 args
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_daemonized(modified_args, debug);
|
||||
} else {
|
||||
return exec_program_daemonized(args, debug);
|
||||
}
|
||||
}
|
||||
|
||||
pid_t exec_program(const char **args, int *read_fd, bool debug) {
|
||||
if(read_fd)
|
||||
*read_fd = -1;
|
||||
|
||||
@@ -84,7 +107,8 @@ namespace gsr {
|
||||
if(pipe(fds) == -1)
|
||||
return -1;
|
||||
|
||||
debug_print_args(args);
|
||||
if(debug)
|
||||
debug_print_args(args);
|
||||
|
||||
const pid_t pid = vfork();
|
||||
if(pid == -1) {
|
||||
@@ -110,10 +134,10 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
int exec_program_get_stdout(const char **args, std::string &result) {
|
||||
int exec_program_get_stdout(const char **args, std::string &result, bool debug) {
|
||||
result.clear();
|
||||
int read_fd = -1;
|
||||
const pid_t process_id = exec_program(args, &read_fd);
|
||||
const pid_t process_id = exec_program(args, &read_fd, debug);
|
||||
if(process_id == -1)
|
||||
return -1;
|
||||
|
||||
@@ -128,8 +152,6 @@ namespace gsr {
|
||||
exit_status = -1;
|
||||
break;
|
||||
}
|
||||
|
||||
buffer[bytes_read] = '\0';
|
||||
result.append(buffer, bytes_read);
|
||||
}
|
||||
|
||||
@@ -152,7 +174,7 @@ namespace gsr {
|
||||
return exit_status;
|
||||
}
|
||||
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result) {
|
||||
int exec_program_on_host_get_stdout(const char **args, std::string &result, bool debug) {
|
||||
if(count_num_args(args) > 64 - 3) {
|
||||
fprintf(stderr, "Error: too many arguments when trying to launch \"%s\"\n", args[0]);
|
||||
return -1;
|
||||
@@ -164,23 +186,31 @@ namespace gsr {
|
||||
const char *modified_args[64] = { "flatpak-spawn", "--host", "--" };
|
||||
for(int i = 3; i < 64; ++i) {
|
||||
const char *arg = args[i - 3];
|
||||
if(!arg) {
|
||||
modified_args[i] = nullptr;
|
||||
break;
|
||||
}
|
||||
modified_args[i] = arg;
|
||||
if(!arg)
|
||||
break;
|
||||
}
|
||||
return exec_program_get_stdout(modified_args, result);
|
||||
return exec_program_get_stdout(modified_args, result, debug);
|
||||
} else {
|
||||
return exec_program_get_stdout(args, result);
|
||||
return exec_program_get_stdout(args, result, debug);
|
||||
}
|
||||
}
|
||||
|
||||
static const char *get_basename(const char *path, int size) {
|
||||
for(int i = size - 1; i >= 0; --i) {
|
||||
if(path[i] == '/')
|
||||
return path + i + 1;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// |output_buffer| should be at least PATH_MAX in size
|
||||
bool read_cmdline_arg0(const char *filepath, char *output_buffer, int output_buffer_size) {
|
||||
output_buffer[0] = '\0';
|
||||
|
||||
const char *arg0_start = NULL;
|
||||
const char *arg0_end = NULL;
|
||||
int arg0_size = 0;
|
||||
int fd = open(filepath, O_RDONLY);
|
||||
if(fd == -1)
|
||||
return false;
|
||||
@@ -190,13 +220,16 @@ namespace gsr {
|
||||
if(bytes_read == -1)
|
||||
goto err;
|
||||
|
||||
arg0_end = (const char*)memchr(buffer, '\0', bytes_read);
|
||||
arg0_start = buffer;
|
||||
arg0_end = (const char*)memchr(arg0_start, '\0', bytes_read);
|
||||
if(!arg0_end)
|
||||
goto err;
|
||||
|
||||
if((arg0_end - buffer) + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, buffer, arg0_end - buffer);
|
||||
output_buffer[arg0_end - buffer] = '\0';
|
||||
arg0_start = get_basename(arg0_start, arg0_end - arg0_start);
|
||||
arg0_size = arg0_end - arg0_start;
|
||||
if(arg0_size + 1 <= output_buffer_size) {
|
||||
memcpy(output_buffer, arg0_start, arg0_size);
|
||||
output_buffer[arg0_size] = '\0';
|
||||
close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
615
src/RegionSelector.cpp
Normal file
@@ -0,0 +1,615 @@
|
||||
#include "../include/RegionSelector.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const int cursor_window_size = 32;
|
||||
static const int cursor_thickness = 5;
|
||||
static const int region_border_size = 2;
|
||||
|
||||
static bool xinput_is_supported(Display *dpy, int *xi_opcode) {
|
||||
*xi_opcode = 0;
|
||||
int query_event = 0;
|
||||
int query_error = 0;
|
||||
if(!XQueryExtension(dpy, "XInputExtension", xi_opcode, &query_event, &query_error)) {
|
||||
fprintf(stderr, "error: RegionSelector: X Input extension not available\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
int major = 2;
|
||||
int minor = 1;
|
||||
int retval = XIQueryVersion(dpy, &major, &minor);
|
||||
if(retval != Success) {
|
||||
fprintf(stderr, "error: RegionSelector: XInput 2.1 is not supported\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int max_int(int a, int b) {
|
||||
return a >= b ? a : b;
|
||||
}
|
||||
|
||||
static void set_region_rectangle(Display *dpy, Window window, int x, int y, int width, int height, int border_size) {
|
||||
if(width < 0) {
|
||||
x += width;
|
||||
width = abs(width);
|
||||
}
|
||||
|
||||
if(height < 0) {
|
||||
y += height;
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
XRectangle rectangles[] = {
|
||||
{
|
||||
(short)max_int(0, x), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Left
|
||||
{
|
||||
(short)max_int(0, x + width - border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, border_size), (unsigned short)max_int(0, height)
|
||||
}, // Right
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Top
|
||||
{
|
||||
(short)max_int(0, x + border_size), (short)max_int(0, y + height - border_size),
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Bottom
|
||||
};
|
||||
|
||||
if(width == 0 && height == 0)
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 0, ShapeSet, Unsorted);
|
||||
else
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static void set_window_shape_cross(Display *dpy, Window window, int window_width, int window_height, int thickness) {
|
||||
XRectangle rectangles[] = {
|
||||
{
|
||||
(short)(window_width / 2 - thickness / 2), (short)0,
|
||||
(unsigned short)thickness, (unsigned short)window_height
|
||||
}, // Vertical
|
||||
{
|
||||
(short)(0), (short)(window_height / 2 - thickness / 2),
|
||||
(unsigned short)window_width, (unsigned short)thickness
|
||||
}, // Horizontal
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 2, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static void draw_rectangle(Display *dpy, Window window, GC gc, int x, int y, int width, int height) {
|
||||
if(width < 0) {
|
||||
x += width;
|
||||
width = abs(width);
|
||||
}
|
||||
|
||||
if(height < 0) {
|
||||
y += height;
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
if(width != 0 && height != 0)
|
||||
XDrawRectangle(dpy, window, gc, x, y, width, height);
|
||||
}
|
||||
|
||||
static Window create_cursor_window(Display *dpy, int width, int height, XVisualInfo *vinfo, unsigned long background_pixel) {
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = background_pixel;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask;
|
||||
window_attr.colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo->visual, AllocNone);
|
||||
const Window window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, width, height, 0, vinfo->depth, InputOutput, vinfo->visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
||||
if(window) {
|
||||
set_window_size_not_resizable(dpy, window, width, height);
|
||||
set_window_shape_cross(dpy, window, width, height, cursor_thickness);
|
||||
make_window_click_through(dpy, window);
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
static void draw_rectangle_or_region(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, mgl::vec2i pos, mgl::vec2i size) {
|
||||
if(is_wayland)
|
||||
draw_rectangle(dpy, window, region_gc, pos.x, pos.y, size.x, size.y);
|
||||
else
|
||||
set_region_rectangle(dpy, window, pos.x, pos.y, size.x, size.y, region_border_size);
|
||||
}
|
||||
|
||||
static void draw_rectangle_around_selected_monitor(Display *dpy, Window window, GC region_gc, int region_border_size, bool is_wayland, const std::vector<Monitor> &monitors, mgl::vec2i cursor_pos) {
|
||||
const Monitor *focused_monitor = nullptr;
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(cursor_pos.x >= monitor.position.x && cursor_pos.x <= monitor.position.x + monitor.size.x
|
||||
&& cursor_pos.y >= monitor.position.y && cursor_pos.y <= monitor.position.y + monitor.size.y)
|
||||
{
|
||||
focused_monitor = &monitor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
if(focused_monitor) {
|
||||
x = focused_monitor->position.x;
|
||||
y = focused_monitor->position.y;
|
||||
width = focused_monitor->size.x;
|
||||
height = focused_monitor->size.y;
|
||||
}
|
||||
|
||||
draw_rectangle_or_region(dpy, window, region_gc, region_border_size, is_wayland, mgl::vec2i(x, y), mgl::vec2i(width, height));
|
||||
}
|
||||
|
||||
static void update_cursor_window(Display *dpy, Window window, Window cursor_window, bool is_wayland, int cursor_x, int cursor_y, int cursor_window_size, int thickness, GC cursor_gc) {
|
||||
if(is_wayland) {
|
||||
const int x = cursor_x - cursor_window_size / 2;
|
||||
const int y = cursor_y - cursor_window_size / 2;
|
||||
XFillRectangle(dpy, window, cursor_gc, x + cursor_window_size / 2 - thickness / 2 , y, thickness, cursor_window_size);
|
||||
XFillRectangle(dpy, window, cursor_gc, x, y + cursor_window_size / 2 - thickness / 2, cursor_window_size, thickness);
|
||||
} else if(cursor_window) {
|
||||
XMoveWindow(dpy, cursor_window, cursor_x - cursor_window_size / 2, cursor_y - cursor_window_size / 2);
|
||||
}
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
static bool is_xwayland(Display *dpy) {
|
||||
int opcode, event, error;
|
||||
return XQueryExtension(dpy, "XWAYLAND", &opcode, &event, &error);
|
||||
}
|
||||
|
||||
static unsigned long mgl_color_to_x11_color(mgl::Color color) {
|
||||
if(color.a == 0)
|
||||
return 0;
|
||||
return ((uint32_t)color.a << 24) | (((uint32_t)color.r * color.a / 0xFF) << 16) | (((uint32_t)color.g * color.a / 0xFF) << 8) | ((uint32_t)color.b * color.a / 0xFF);
|
||||
}
|
||||
|
||||
static const Monitor* get_monitor_by_region_center(const std::vector<Monitor> &monitors, Region region) {
|
||||
const mgl::vec2i center = {region.pos.x + region.size.x / 2, region.pos.y + region.size.y / 2};
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(center.x >= monitor.position.x && center.x <= monitor.position.x + monitor.size.x
|
||||
&& center.y >= monitor.position.y && center.y <= monitor.position.y + monitor.size.y)
|
||||
{
|
||||
return &monitor;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Name is the x11 name. TODO: verify if this works on all wayland compositors
|
||||
static const Monitor* get_wayland_monitor_by_name(const std::vector<Monitor> &monitors, const std::string &name) {
|
||||
for(const Monitor &monitor : monitors) {
|
||||
if(monitor.name == name)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static mgl::vec2d to_vec2d(mgl::vec2i v) {
|
||||
return { (double)v.x, (double)v.y };
|
||||
}
|
||||
|
||||
static Region x11_region_to_wayland_region(Display *dpy, struct wl_display *wayland_dpy, Region x11_region) {
|
||||
const std::vector<Monitor> x11_monitors = get_monitors(dpy);
|
||||
const Monitor *x11_selected_monitor = get_monitor_by_region_center(x11_monitors, x11_region);
|
||||
if(!x11_selected_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get x11 monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const std::vector<Monitor> wayland_monitors = get_monitors_wayland(wayland_dpy);
|
||||
const Monitor *wayland_monitor = get_wayland_monitor_by_name(wayland_monitors, x11_selected_monitor->name);
|
||||
if(!wayland_monitor) {
|
||||
fprintf(stderr, "Warning: RegionSelector: failed to get wayland monitor\n");
|
||||
return x11_region;
|
||||
}
|
||||
|
||||
const mgl::vec2d region_relative_pos = {
|
||||
(double)(x11_region.pos.x - x11_selected_monitor->position.x) / (double)x11_selected_monitor->size.x,
|
||||
(double)(x11_region.pos.y - x11_selected_monitor->position.y) / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
const mgl::vec2d region_relative_size = {
|
||||
(double)x11_region.size.x / (double)x11_selected_monitor->size.x,
|
||||
(double)x11_region.size.y / (double)x11_selected_monitor->size.y,
|
||||
};
|
||||
|
||||
return Region {
|
||||
wayland_monitor->position + (region_relative_pos * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
(region_relative_size * to_vec2d(wayland_monitor->size)).to_vec2i(),
|
||||
};
|
||||
}
|
||||
|
||||
static std::vector<RegionWindow> query_windows(Display *dpy) {
|
||||
std::vector<RegionWindow> windows;
|
||||
|
||||
Window root_return = None;
|
||||
Window parent_return = None;
|
||||
Window *children_return = nullptr;
|
||||
unsigned int num_children_return = 0;
|
||||
if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root_return, &parent_return, &children_return, &num_children_return) || !children_return)
|
||||
return windows;
|
||||
|
||||
for(int i = (int)num_children_return - 1; i >= 0; --i) {
|
||||
const Window child_window = children_return[i];
|
||||
XWindowAttributes win_attr;
|
||||
if(XGetWindowAttributes(dpy, child_window, &win_attr) && !win_attr.override_redirect && win_attr.c_class == InputOutput && win_attr.map_state == IsViewable) {
|
||||
windows.push_back(
|
||||
RegionWindow{
|
||||
child_window,
|
||||
mgl::vec2i(win_attr.x, win_attr.y),
|
||||
mgl::vec2i(win_attr.width, win_attr.height)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
XFree(children_return);
|
||||
return windows;
|
||||
}
|
||||
|
||||
static std::optional<RegionWindow> get_window_by_position(const std::vector<RegionWindow> &windows, mgl::vec2i pos) {
|
||||
for(const RegionWindow &window : windows) {
|
||||
if(mgl::IntRect(window.pos, window.size).contains(pos))
|
||||
return window;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
RegionSelector::RegionSelector() {
|
||||
|
||||
}
|
||||
|
||||
RegionSelector::~RegionSelector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool RegionSelector::start(SelectionType selection_type, mgl::Color border_color) {
|
||||
if(dpy)
|
||||
return false;
|
||||
|
||||
const unsigned long border_color_x11 = mgl_color_to_x11_color(border_color);
|
||||
dpy = XOpenDisplay(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: RegionSelector::start: failed to connect to the X11 server\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
xi_opcode = 0;
|
||||
if(!xinput_is_supported(dpy, &xi_opcode)) {
|
||||
fprintf(stderr, "Error: RegionSelector::start: xinput not supported on your system\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
is_wayland = is_xwayland(dpy);
|
||||
monitors = get_monitors(dpy);
|
||||
|
||||
Window x11_cursor_window = None;
|
||||
cursor_pos = get_cursor_position(dpy, &x11_cursor_window);
|
||||
region.pos = {0, 0};
|
||||
region.size = {0, 0};
|
||||
|
||||
XVisualInfo vinfo;
|
||||
memset(&vinfo, 0, sizeof(vinfo));
|
||||
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
||||
region_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
||||
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = is_wayland ? 0 : border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = region_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
region_window = XCreateWindow(dpy, DefaultRootWindow(dpy), 0, 0, XWidthOfScreen(screen), XHeightOfScreen(screen), 0,
|
||||
vinfo.depth, InputOutput, vinfo.visual, CWBackPixel | CWBorderPixel | CWOverrideRedirect | CWEventMask | CWColormap, &window_attr);
|
||||
if(!region_window) {
|
||||
fprintf(stderr, "Error: RegionSelector::start: failed to create region window\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
set_window_size_not_resizable(dpy, region_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
|
||||
unsigned char data = 2; // Prefer being composed to allow transparency. Do this to prevent the compositor from getting turned on/off when taking a screenshot
|
||||
XChangeProperty(dpy, region_window, XInternAtom(dpy, "_NET_WM_BYPASS_COMPOSITOR", False), XA_CARDINAL, 32, PropModeReplace, &data, 1);
|
||||
|
||||
if(!is_wayland) {
|
||||
cursor_window = create_cursor_window(dpy, cursor_window_size, cursor_window_size, &vinfo, border_color_x11);
|
||||
if(!cursor_window)
|
||||
fprintf(stderr, "Warning: RegionSelector::start: failed to create cursor window\n");
|
||||
set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
XGCValues region_gc_values;
|
||||
memset(®ion_gc_values, 0, sizeof(region_gc_values));
|
||||
region_gc_values.foreground = border_color_x11;
|
||||
region_gc_values.line_width = region_border_size;
|
||||
region_gc_values.line_style = LineSolid;
|
||||
region_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, ®ion_gc_values);
|
||||
|
||||
XGCValues cursor_gc_values;
|
||||
memset(&cursor_gc_values, 0, sizeof(cursor_gc_values));
|
||||
cursor_gc_values.foreground = border_color_x11;
|
||||
cursor_gc_values.line_width = cursor_thickness;
|
||||
cursor_gc_values.line_style = LineSolid;
|
||||
cursor_gc = XCreateGC(dpy, region_window, GCForeground | GCLineWidth | GCLineStyle, &cursor_gc_values);
|
||||
|
||||
if(!region_gc || !cursor_gc) {
|
||||
fprintf(stderr, "Error: RegionSelector::start: failed to create gc\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
XMapWindow(dpy, region_window);
|
||||
make_window_sticky(dpy, region_window);
|
||||
hide_window_from_taskbar(dpy, region_window);
|
||||
XFixesHideCursor(dpy, region_window);
|
||||
XGrabPointer(dpy, DefaultRootWindow(dpy), True, ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, None, CurrentTime);
|
||||
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
xi_grab_all_mouse_devices(dpy);
|
||||
XFlush(dpy);
|
||||
|
||||
window_set_fullscreen(dpy, region_window, true);
|
||||
|
||||
if(!is_wayland || x11_cursor_window)
|
||||
update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
|
||||
|
||||
if(cursor_window) {
|
||||
XMapWindow(dpy, cursor_window);
|
||||
make_window_sticky(dpy, cursor_window);
|
||||
hide_window_from_taskbar(dpy, cursor_window);
|
||||
}
|
||||
|
||||
windows = query_windows(dpy);
|
||||
|
||||
if(selection_type == SelectionType::WINDOW) {
|
||||
focused_window = get_window_by_position(windows, cursor_pos);
|
||||
if(focused_window)
|
||||
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, focused_window->pos, focused_window->size);
|
||||
} else if(selection_type == SelectionType::REGION) {
|
||||
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
selected = false;
|
||||
canceled = false;
|
||||
this->selection_type = selection_type;
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegionSelector::stop() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
XWarpPointer(dpy, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, cursor_pos.x, cursor_pos.y);
|
||||
xi_warp_all_mouse_devices(dpy, cursor_pos);
|
||||
XFixesShowCursor(dpy, region_window);
|
||||
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
xi_ungrab_all_mouse_devices(dpy);
|
||||
XFlush(dpy);
|
||||
|
||||
if(region_gc) {
|
||||
XFreeGC(dpy, region_gc);
|
||||
region_gc = nullptr;
|
||||
}
|
||||
|
||||
if(cursor_gc) {
|
||||
XFreeGC(dpy, cursor_gc);
|
||||
cursor_gc = nullptr;
|
||||
}
|
||||
|
||||
if(region_window_colormap) {
|
||||
XFreeColormap(dpy, region_window_colormap);
|
||||
region_window_colormap = 0;
|
||||
}
|
||||
|
||||
if(region_window) {
|
||||
XDestroyWindow(dpy, region_window);
|
||||
region_window = 0;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
selecting_region = false;
|
||||
monitors.clear();
|
||||
windows.clear();
|
||||
}
|
||||
|
||||
bool RegionSelector::is_started() const {
|
||||
return dpy != nullptr;
|
||||
}
|
||||
|
||||
bool RegionSelector::failed() const {
|
||||
return !dpy;
|
||||
}
|
||||
|
||||
bool RegionSelector::poll_events() {
|
||||
if(!dpy || selected)
|
||||
return false;
|
||||
|
||||
XEvent xev;
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
|
||||
if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
|
||||
canceled = true;
|
||||
selected = false;
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
|
||||
XGenericEventCookie *cookie = &xev.xcookie;
|
||||
if(cookie->type != GenericEvent || cookie->extension != xi_opcode || !XGetEventData(dpy, cookie))
|
||||
continue;
|
||||
|
||||
const XIDeviceEvent *de = (XIDeviceEvent*)cookie->data;
|
||||
switch(cookie->evtype) {
|
||||
case XI_ButtonPress: {
|
||||
on_button_press(de);
|
||||
break;
|
||||
}
|
||||
case XI_ButtonRelease: {
|
||||
on_button_release(de);
|
||||
break;
|
||||
}
|
||||
case XI_Motion: {
|
||||
on_mouse_motion(de);
|
||||
break;
|
||||
}
|
||||
}
|
||||
XFreeEventData(dpy, cookie);
|
||||
|
||||
if(selected) {
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RegionSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RegionSelector::take_canceled() {
|
||||
const bool result = canceled;
|
||||
canceled = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Region RegionSelector::get_region_selection(Display *x11_dpy, struct wl_display *wayland_dpy) const {
|
||||
assert(selection_type == SelectionType::REGION);
|
||||
Region returned_region = region;
|
||||
if(is_wayland && x11_dpy && wayland_dpy)
|
||||
returned_region = x11_region_to_wayland_region(x11_dpy, wayland_dpy, returned_region);
|
||||
return returned_region;
|
||||
}
|
||||
|
||||
Window RegionSelector::get_window_selection() const {
|
||||
assert(selection_type == SelectionType::WINDOW);
|
||||
if(focused_window)
|
||||
return focused_window->window;
|
||||
else
|
||||
return None;
|
||||
}
|
||||
|
||||
RegionSelector::SelectionType RegionSelector::get_selection_type() const {
|
||||
return selection_type;
|
||||
}
|
||||
|
||||
void RegionSelector::on_button_press(const void *de) {
|
||||
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
||||
if(device_event->detail != Button1)
|
||||
return;
|
||||
|
||||
if(selection_type == SelectionType::REGION) {
|
||||
region.pos = { (int)device_event->root_x, (int)device_event->root_y };
|
||||
selecting_region = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RegionSelector::on_button_release(const void *de) {
|
||||
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
||||
if(device_event->detail != Button1)
|
||||
return;
|
||||
|
||||
if(selection_type == SelectionType::WINDOW) {
|
||||
focused_window = get_window_by_position(windows, mgl::vec2i(device_event->root_x, device_event->root_y));
|
||||
if(focused_window) {
|
||||
const Window real_window = window_get_target_window_child(dpy, focused_window->window);
|
||||
XWindowAttributes win_attr;
|
||||
if(XGetWindowAttributes(dpy, real_window, &win_attr)) {
|
||||
focused_window = RegionWindow{
|
||||
real_window,
|
||||
mgl::vec2i(win_attr.x, win_attr.y),
|
||||
mgl::vec2i(win_attr.width, win_attr.height)
|
||||
};
|
||||
}
|
||||
}
|
||||
} else if(selection_type == SelectionType::REGION) {
|
||||
if(!selecting_region)
|
||||
return;
|
||||
}
|
||||
|
||||
if(is_wayland) {
|
||||
XClearWindow(dpy, region_window);
|
||||
XFlush(dpy);
|
||||
} else {
|
||||
set_region_rectangle(dpy, region_window, 0, 0, 0, 0, 0);
|
||||
}
|
||||
selecting_region = false;
|
||||
|
||||
if(selection_type == SelectionType::WINDOW) {
|
||||
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
|
||||
} else if(selection_type == SelectionType::REGION) {
|
||||
cursor_pos = region.pos + region.size;
|
||||
}
|
||||
|
||||
if(region.size.x < 0) {
|
||||
region.pos.x += region.size.x;
|
||||
region.size.x = abs(region.size.x);
|
||||
}
|
||||
|
||||
if(region.size.y < 0) {
|
||||
region.pos.y += region.size.y;
|
||||
region.size.y = abs(region.size.y);
|
||||
}
|
||||
|
||||
if(region.size.x > 0)
|
||||
region.size.x += 1;
|
||||
|
||||
if(region.size.y > 0)
|
||||
region.size.y += 1;
|
||||
|
||||
selected = true;
|
||||
}
|
||||
|
||||
void RegionSelector::on_mouse_motion(const void *de) {
|
||||
const XIDeviceEvent *device_event = (XIDeviceEvent*)de;
|
||||
XClearWindow(dpy, region_window);
|
||||
|
||||
if(selecting_region) {
|
||||
region.size.x = device_event->root_x - region.pos.x;
|
||||
region.size.y = device_event->root_y - region.pos.y;
|
||||
cursor_pos = region.pos + region.size;
|
||||
|
||||
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, region.pos, region.size);
|
||||
} else if(selection_type == SelectionType::WINDOW) {
|
||||
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
|
||||
|
||||
focused_window = get_window_by_position(windows, cursor_pos);
|
||||
if(focused_window)
|
||||
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, focused_window->pos, focused_window->size);
|
||||
else
|
||||
draw_rectangle_or_region(dpy, region_window, region_gc, region_border_size, is_wayland, mgl::vec2i(0, 0), mgl::vec2i(0, 0));
|
||||
} else if(selection_type == SelectionType::REGION) {
|
||||
cursor_pos = { (int)device_event->root_x, (int)device_event->root_y };
|
||||
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
|
||||
}
|
||||
|
||||
update_cursor_window(dpy, region_window, cursor_window, is_wayland, cursor_pos.x, cursor_pos.y, cursor_window_size, cursor_thickness, cursor_gc);
|
||||
XFlush(dpy);
|
||||
}
|
||||
}
|
||||
205
src/Rpc.cpp
@@ -5,11 +5,12 @@
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
namespace gsr {
|
||||
static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
static void get_socket_filepath(char *buffer, size_t buffer_size, const char *filename) {
|
||||
char dir[PATH_MAX];
|
||||
|
||||
const char *runtime_dir = getenv("XDG_RUNTIME_DIR");
|
||||
@@ -24,78 +25,117 @@ namespace gsr {
|
||||
snprintf(buffer, buffer_size, "%s/%s", dir, filename);
|
||||
}
|
||||
|
||||
static int create_socket(const char *name, struct sockaddr_un *addr, std::string &socket_filepath) {
|
||||
char socket_filepath_tmp[PATH_MAX];
|
||||
get_socket_filepath(socket_filepath_tmp, sizeof(socket_filepath_tmp), name);
|
||||
socket_filepath = socket_filepath_tmp;
|
||||
|
||||
memset(addr, 0, sizeof(*addr));
|
||||
if(strlen(name) > sizeof(addr->sun_path))
|
||||
return false;
|
||||
|
||||
addr->sun_family = AF_UNIX;
|
||||
snprintf(addr->sun_path, sizeof(addr->sun_path), "%s", socket_filepath.c_str());
|
||||
|
||||
return socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
}
|
||||
|
||||
Rpc::Rpc() {
|
||||
num_polls = 0;
|
||||
}
|
||||
|
||||
Rpc::~Rpc() {
|
||||
if(fd > 0)
|
||||
close(fd);
|
||||
if(socket_fd > 0)
|
||||
close(socket_fd);
|
||||
|
||||
if(file)
|
||||
fclose(file);
|
||||
|
||||
if(!fifo_filepath.empty())
|
||||
remove(fifo_filepath.c_str());
|
||||
if(!socket_filepath.empty())
|
||||
unlink(socket_filepath.c_str());
|
||||
}
|
||||
|
||||
bool Rpc::create(const char *name) {
|
||||
if(file) {
|
||||
if(socket_fd > 0) {
|
||||
fprintf(stderr, "Error: Rpc::create: already created/opened\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
char fifo_filepath_tmp[PATH_MAX];
|
||||
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
|
||||
fifo_filepath = fifo_filepath_tmp;
|
||||
remove(fifo_filepath.c_str());
|
||||
|
||||
if(mkfifo(fifo_filepath.c_str(), 0600) != 0) {
|
||||
fprintf(stderr, "Error: mkfifo failed, error: %s, %s\n", strerror(errno), fifo_filepath.c_str());
|
||||
struct sockaddr_un addr;
|
||||
socket_fd = create_socket(name, &addr, socket_filepath);
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::create: failed to create socket, error: %s\n", strerror(errno));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!open_filepath(fifo_filepath.c_str())) {
|
||||
remove(fifo_filepath.c_str());
|
||||
fifo_filepath.clear();
|
||||
unlink(socket_filepath.c_str());
|
||||
|
||||
if(bind(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
|
||||
fprintf(stderr, "Error: Rpc::create: failed to bind, error: %s\n", strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(listen(socket_fd, GSR_RPC_MAX_CONNECTIONS) == -1) {
|
||||
const int err = errno;
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
|
||||
fprintf(stderr, "Error: Rpc::create: failed to listen, error: %s\n", strerror(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
polls[0].fd = socket_fd;
|
||||
polls[0].events = POLLIN;
|
||||
polls[0].revents = 0;
|
||||
++num_polls;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Rpc::open(const char *name) {
|
||||
if(file) {
|
||||
RpcOpenResult Rpc::open(const char *name) {
|
||||
if(socket_fd > 0) {
|
||||
fprintf(stderr, "Error: Rpc::open: already created/opened\n");
|
||||
return false;
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
|
||||
char fifo_filepath_tmp[PATH_MAX];
|
||||
get_runtime_filepath(fifo_filepath_tmp, sizeof(fifo_filepath_tmp), name);
|
||||
return open_filepath(fifo_filepath_tmp);
|
||||
}
|
||||
|
||||
bool Rpc::open_filepath(const char *filepath) {
|
||||
fd = ::open(filepath, O_RDWR | O_NONBLOCK);
|
||||
if(fd <= 0)
|
||||
return false;
|
||||
|
||||
file = fdopen(fd, "r+");
|
||||
if(!file) {
|
||||
close(fd);
|
||||
fd = 0;
|
||||
return false;
|
||||
struct sockaddr_un addr;
|
||||
socket_fd = create_socket(name, &addr, socket_filepath);
|
||||
socket_filepath.clear(); /* We dont want to delete the socket on exit as the client */
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::open: failed to create socket, error: %s\n", strerror(errno));
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
fd = 0;
|
||||
return true;
|
||||
|
||||
while(true) {
|
||||
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
if(err == EWOULDBLOCK) {
|
||||
usleep(10 * 1000);
|
||||
} else {
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
if(err != ENOENT && err != ECONNREFUSED)
|
||||
fprintf(stderr, "Error: Rpc::create: failed to connect, error: %s\n", strerror(err));
|
||||
return RpcOpenResult::ERROR;
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return RpcOpenResult::OK;
|
||||
}
|
||||
|
||||
bool Rpc::write(const char *str, size_t size) {
|
||||
if(!file) {
|
||||
fprintf(stderr, "Error: Rpc::write: fifo not created/opened yet\n");
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: Rpc::write: unix domain socket not created/opened yet\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
ssize_t offset = 0;
|
||||
while(offset < (ssize_t)size) {
|
||||
const ssize_t bytes_written = fwrite(str + offset, 1, size - offset, file);
|
||||
fflush(file);
|
||||
const ssize_t bytes_written = ::write(socket_fd, str + offset, size - offset);
|
||||
if(bytes_written > 0)
|
||||
offset += bytes_written;
|
||||
}
|
||||
@@ -103,30 +143,75 @@ namespace gsr {
|
||||
}
|
||||
|
||||
void Rpc::poll() {
|
||||
if(!file) {
|
||||
//fprintf(stderr, "Error: Rpc::poll: fifo not created/opened yet\n");
|
||||
if(socket_fd <= 0) {
|
||||
//fprintf(stderr, "Error: Rpc::poll: unix domain socket not created/opened yet\n");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
char line[1024];
|
||||
while(fgets(line, sizeof(line), file)) {
|
||||
int line_len = strlen(line);
|
||||
if(line_len == 0)
|
||||
continue;
|
||||
while(::poll(polls, num_polls, 0) > 0) {
|
||||
for(int i = 0; i < num_polls; ++i) {
|
||||
if(polls[i].fd == socket_fd) {
|
||||
if(polls[i].revents & (POLLERR|POLLHUP)) {
|
||||
close(socket_fd);
|
||||
socket_fd = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if(line[line_len - 1] == '\n') {
|
||||
line[line_len - 1] = '\0';
|
||||
--line_len;
|
||||
const int client_fd = accept(socket_fd, NULL, NULL);
|
||||
if(num_polls >= GSR_RPC_MAX_POLLS) {
|
||||
if(errno != EWOULDBLOCK)
|
||||
fprintf(stderr, "Error: Rpc::poll: unable to accept more clients, error: %s\n", strerror(errno));
|
||||
} else {
|
||||
polls[num_polls].fd = client_fd;
|
||||
polls[num_polls].events = POLLIN;
|
||||
polls[num_polls].revents = 0;
|
||||
++num_polls;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if(polls[i].revents & POLLIN)
|
||||
handle_client_data(polls[i].fd, polls_data[i]);
|
||||
|
||||
if(polls[i].revents & (POLLERR|POLLHUP)) {
|
||||
close(polls[i].fd);
|
||||
polls[i] = polls[num_polls - 1];
|
||||
|
||||
memcpy(polls_data[i].buffer, polls_data[num_polls - 1].buffer, polls_data[num_polls - 1].buffer_size);
|
||||
polls_data[i].buffer_size = polls_data[num_polls - 1].buffer_size;
|
||||
|
||||
--num_polls;
|
||||
--i;
|
||||
}
|
||||
|
||||
polls[i].revents = 0;
|
||||
}
|
||||
|
||||
name = line;
|
||||
auto it = handlers_by_name.find(name);
|
||||
if(it != handlers_by_name.end())
|
||||
it->second(name);
|
||||
}
|
||||
}
|
||||
|
||||
void Rpc::handle_client_data(int client_fd, PollData &poll_data) {
|
||||
char *write_buffer = poll_data.buffer + poll_data.buffer_size;
|
||||
const ssize_t num_bytes_read = read(client_fd, write_buffer, sizeof(poll_data.buffer) - poll_data.buffer_size);
|
||||
if(num_bytes_read <= 0)
|
||||
return;
|
||||
|
||||
poll_data.buffer_size += num_bytes_read;
|
||||
const char *newline_p = (const char*)memchr(write_buffer, '\n', num_bytes_read);
|
||||
if(!newline_p)
|
||||
return;
|
||||
|
||||
const size_t command_size = newline_p - poll_data.buffer;
|
||||
std::string name;
|
||||
name.assign(poll_data.buffer, command_size);
|
||||
memmove(poll_data.buffer, newline_p + 1, poll_data.buffer_size - (command_size + 1));
|
||||
poll_data.buffer_size -= (command_size + 1);
|
||||
|
||||
auto it = handlers_by_name.find(name);
|
||||
if(it != handlers_by_name.end())
|
||||
it->second(name);
|
||||
}
|
||||
|
||||
bool Rpc::add_handler(const std::string &name, RpcCallback callback) {
|
||||
return handlers_by_name.insert(std::make_pair(name, std::move(callback))).second;
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ namespace gsr {
|
||||
|
||||
static mgl::Color gpu_vendor_to_color(GpuVendor vendor) {
|
||||
switch(vendor) {
|
||||
case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49);
|
||||
case GpuVendor::AMD: return mgl::Color(221, 0, 49);
|
||||
case GpuVendor::INTEL: return mgl::Color(8, 109, 183);
|
||||
case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0);
|
||||
case GpuVendor::UNKNOWN: return mgl::Color(221, 0, 49);
|
||||
case GpuVendor::AMD: return mgl::Color(221, 0, 49);
|
||||
case GpuVendor::INTEL: return mgl::Color(8, 109, 183);
|
||||
case GpuVendor::NVIDIA: return mgl::Color(118, 185, 0);
|
||||
case GpuVendor::BROADCOM: return mgl::Color(221, 0, 49);
|
||||
}
|
||||
return mgl::Color(221, 0, 49);
|
||||
}
|
||||
@@ -26,6 +27,8 @@ namespace gsr {
|
||||
vendor = GpuVendor::INTEL;
|
||||
else if(color_name == "nvidia")
|
||||
vendor = GpuVendor::NVIDIA;
|
||||
else if(color_name == "broadcom")
|
||||
vendor = GpuVendor::BROADCOM;
|
||||
return gpu_vendor_to_color(vendor);
|
||||
}
|
||||
|
||||
@@ -45,6 +48,9 @@ namespace gsr {
|
||||
if(!theme->body_font.load_from_file(theme->body_font_file, std::max(13.0f, window_size.y * 0.015f)))
|
||||
return false;
|
||||
|
||||
if(!theme->camera_setup_font.load_from_file(theme->body_font_file, 24))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -60,52 +66,100 @@ namespace gsr {
|
||||
if(!theme->title_font_file.load((resources_path + "fonts/NotoSans-Bold.ttf").c_str(), mgl::MemoryMappedFile::LoadOptions{true, false}))
|
||||
goto error;
|
||||
|
||||
if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str()))
|
||||
if(!theme->combobox_arrow_texture.load_from_file((resources_path + "images/combobox_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->settings_texture.load_from_file((resources_path + "images/settings.png").c_str()))
|
||||
goto error;
|
||||
|
||||
if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str()))
|
||||
if(!theme->settings_small_texture.load_from_file((resources_path + "images/settings_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str()))
|
||||
if(!theme->settings_extra_small_texture.load_from_file((resources_path + "images/settings_extra_small.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str()))
|
||||
if(!theme->folder_texture.load_from_file((resources_path + "images/folder.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str()))
|
||||
if(!theme->up_arrow_texture.load_from_file((resources_path + "images/up_arrow.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str()))
|
||||
if(!theme->replay_button_texture.load_from_file((resources_path + "images/replay.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str()))
|
||||
if(!theme->record_button_texture.load_from_file((resources_path + "images/record.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str()))
|
||||
if(!theme->stream_button_texture.load_from_file((resources_path + "images/stream.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->close_texture.load_from_file((resources_path + "images/cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->logo_texture.load_from_file((resources_path + "images/gpu_screen_recorder_logo.png").c_str()))
|
||||
goto error;
|
||||
|
||||
if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str()))
|
||||
if(!theme->checkbox_circle_texture.load_from_file((resources_path + "images/checkbox_circle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str()))
|
||||
if(!theme->checkbox_background_texture.load_from_file((resources_path + "images/checkbox_background.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str()))
|
||||
if(!theme->play_texture.load_from_file((resources_path + "images/play.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str()))
|
||||
if(!theme->stop_texture.load_from_file((resources_path + "images/stop.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str()))
|
||||
if(!theme->pause_texture.load_from_file((resources_path + "images/pause.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str()))
|
||||
if(!theme->save_texture.load_from_file((resources_path + "images/save.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->screenshot_texture.load_from_file((resources_path + "images/screenshot.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->trash_texture.load_from_file((resources_path + "images/trash.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->masked_texture.load_from_file((resources_path + "images/masked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->unmasked_texture.load_from_file((resources_path + "images/unmasked.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->warning_texture.load_from_file((resources_path + "images/warning.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->question_mark_texture.load_from_file((resources_path + "images/question_mark.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->info_texture.load_from_file((resources_path + "images/info.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_home_texture.load_from_file((resources_path + "images/ps4_home.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_options_texture.load_from_file((resources_path + "images/ps4_options.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_dpad_up_texture.load_from_file((resources_path + "images/ps4_dpad_up.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_dpad_down_texture.load_from_file((resources_path + "images/ps4_dpad_down.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_dpad_left_texture.load_from_file((resources_path + "images/ps4_dpad_left.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_dpad_right_texture.load_from_file((resources_path + "images/ps4_dpad_right.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_cross_texture.load_from_file((resources_path + "images/ps4_cross.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
if(!theme->ps4_triangle_texture.load_from_file((resources_path + "images/ps4_triangle.png").c_str(), mgl::Texture::LoadOptions{false, false, MGL_TEXTURE_SCALE_LINEAR_MIPMAP}))
|
||||
goto error;
|
||||
|
||||
return true;
|
||||
|
||||
187
src/Translation.cpp
Normal file
@@ -0,0 +1,187 @@
|
||||
#include "../include/Translation.hpp"
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <fstream>
|
||||
|
||||
|
||||
namespace gsr {
|
||||
std::string Translation::get_system_language() {
|
||||
const char* lang = getenv("LANGUAGE");
|
||||
if (!lang || !lang[0]) lang = getenv("LC_ALL");
|
||||
if (!lang || !lang[0]) lang = getenv("LC_MESSAGES");
|
||||
if (!lang || !lang[0]) lang = getenv("LANG");
|
||||
|
||||
if (lang && lang[0]) {
|
||||
std::string lang_str(lang);
|
||||
|
||||
// we usually need only two symbols
|
||||
size_t underscore = lang_str.find('_');
|
||||
if (underscore != std::string::npos) {
|
||||
return lang_str.substr(0, underscore);
|
||||
}
|
||||
size_t dot = lang_str.find('.');
|
||||
if (dot != std::string::npos) {
|
||||
return lang_str.substr(0, dot);
|
||||
}
|
||||
return lang_str;
|
||||
}
|
||||
|
||||
return "en";
|
||||
}
|
||||
|
||||
void Translation::process_escapes(std::string& str) {
|
||||
size_t pos = 0;
|
||||
while ((pos = str.find("\\n", pos)) != std::string::npos) {
|
||||
str.replace(pos, 2, "\n");
|
||||
pos += 1;
|
||||
}
|
||||
}
|
||||
|
||||
bool Translation::is_language_supported(const char* lang) {
|
||||
if(strcmp(lang, "en") == 0)
|
||||
return true;
|
||||
|
||||
std::string paths[] = {
|
||||
std::string("translations/") + lang + ".txt",
|
||||
std::string(this->translations_directory) + lang + ".txt"
|
||||
};
|
||||
|
||||
for (const auto& path : paths) {
|
||||
std::ifstream file(path);
|
||||
if (file.is_open()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Translation::load_language(const char* lang) {
|
||||
translations.clear();
|
||||
|
||||
const std::string system_language = get_system_language();
|
||||
if(!lang || lang[0] == '\0')
|
||||
lang = system_language.c_str();
|
||||
|
||||
if (!is_language_supported(lang)) {
|
||||
fprintf(stderr, "Warning: language '%s' is not supported\n", lang);
|
||||
return false;
|
||||
}
|
||||
|
||||
current_language = lang;
|
||||
|
||||
if (strcmp(lang, "en") == 0) {
|
||||
return true; // english is the base
|
||||
}
|
||||
|
||||
std::string paths[] = {
|
||||
std::string("translations/") + lang + ".txt",
|
||||
std::string(this->translations_directory) + lang + ".txt"
|
||||
};
|
||||
|
||||
for (const auto& path : paths) {
|
||||
std::ifstream file(path);
|
||||
if (!file.is_open()) continue;
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
if (line.empty() || line[0] == '#') continue;
|
||||
|
||||
size_t eq_pos = line.find('=');
|
||||
if (eq_pos == std::string::npos) continue;
|
||||
|
||||
std::string key = line.substr(0, eq_pos);
|
||||
std::string value = line.substr(eq_pos + 1);
|
||||
|
||||
// Process escape sequences in both key and value
|
||||
process_escapes(key);
|
||||
process_escapes(value);
|
||||
|
||||
translations[key] = value;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Info: Loaded translation file for '%s' from %s\n", lang, path.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Warning: translation file for '%s' not found\n", lang);
|
||||
return false;
|
||||
}
|
||||
|
||||
Translation& Translation::instance() {
|
||||
static Translation tr;
|
||||
return tr;
|
||||
}
|
||||
|
||||
void Translation::init(const char* translations_directory, const char* initial_language) {
|
||||
if(initial_language && initial_language[0] == '\0')
|
||||
initial_language = nullptr;
|
||||
|
||||
this->translations_directory = translations_directory;
|
||||
|
||||
load_language(initial_language == nullptr ? "" : initial_language);
|
||||
}
|
||||
|
||||
bool Translation::plural_numbers_are_complex() {
|
||||
if (
|
||||
current_language == "ru" ||
|
||||
current_language == "uk" ||
|
||||
current_language == "pl" ||
|
||||
current_language == "cs" ||
|
||||
current_language == "sk" ||
|
||||
current_language == "hr" ||
|
||||
current_language == "sl"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return current_language != "en";
|
||||
}
|
||||
|
||||
std::string Translation::get_complex_plural_number_key(const char* key, int number) {
|
||||
std::string s_key = key;
|
||||
if (current_language == "ru" || current_language == "uk") {
|
||||
int n = number % 100;
|
||||
if (n >= 11 && n <= 14) {
|
||||
return s_key + "_many";
|
||||
}
|
||||
n = number % 10;
|
||||
if (n == 1) {
|
||||
return s_key + "_one";
|
||||
}
|
||||
if (n >= 2 && n <= 4) {
|
||||
return s_key + "_few";
|
||||
}
|
||||
return s_key + "_many";
|
||||
} else if (current_language == "pl") {
|
||||
int n = number % 100;
|
||||
if (n >= 12 && n <= 14) {
|
||||
return s_key + "_many";
|
||||
}
|
||||
n = number % 10;
|
||||
if (n == 1) {
|
||||
return s_key + "_one";
|
||||
}
|
||||
if (n >= 2 && n <= 4) {
|
||||
return s_key + "_few";
|
||||
}
|
||||
return s_key + "_many";
|
||||
}
|
||||
// Add more languages as needed
|
||||
|
||||
return key; // default fallback
|
||||
}
|
||||
|
||||
const char* Translation::translate(const char* key) {
|
||||
auto it = translations.find(key);
|
||||
if (it != translations.end()) {
|
||||
return it->second.c_str();
|
||||
}
|
||||
#ifndef DNDEBUG
|
||||
if(current_language != "en")
|
||||
fprintf(stderr, "Warning: translation key '%s' not found\n", key);
|
||||
#endif
|
||||
return key; // falling back if nothing found
|
||||
}
|
||||
}
|
||||
174
src/Utils.cpp
@@ -1,13 +1,74 @@
|
||||
#include "../include/Utils.hpp"
|
||||
#include "../include/Process.hpp"
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <optional>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
extern "C" {
|
||||
#include <mgl/system/clock.h>
|
||||
}
|
||||
|
||||
namespace gsr {
|
||||
static std::optional<std::string> get_xdg_autostart_content() {
|
||||
const char *args[] = {
|
||||
"/bin/sh", "-c",
|
||||
"cat \"${XDG_CONFIG_HOME:-$HOME/.config}/autostart/gpu-screen-recorder-ui.desktop\"",
|
||||
nullptr
|
||||
};
|
||||
std::string output;
|
||||
if(exec_program_on_host_get_stdout(args, output, false) != 0)
|
||||
return std::nullopt;
|
||||
return output;
|
||||
}
|
||||
|
||||
// Returns the exit status or -1 on timeout
|
||||
static int run_command_timeout(const char **args, double sleep_time_sec, double timeout_sec) {
|
||||
mgl_clock clock;
|
||||
mgl_clock_init(&clock);
|
||||
|
||||
do {
|
||||
int read_fd = 0;
|
||||
const pid_t process_id = exec_program(args, &read_fd, false);
|
||||
if(process_id <= 0)
|
||||
continue;
|
||||
|
||||
const double time_elapsed_sleep_start = mgl_clock_get_elapsed_time_seconds(&clock);
|
||||
pid_t waitpid_result = 0;
|
||||
do {
|
||||
int status = 0;
|
||||
waitpid_result = waitpid(process_id, &status, WNOHANG);
|
||||
if(waitpid_result > 0)
|
||||
break;
|
||||
|
||||
usleep(30 * 1000); // 30ms
|
||||
} while(mgl_clock_get_elapsed_time_seconds(&clock) - time_elapsed_sleep_start < sleep_time_sec);
|
||||
|
||||
int status = 0;
|
||||
if(waitpid_result > 0) {
|
||||
int exit_status = -0;
|
||||
if(WIFEXITED(status))
|
||||
exit_status = -1;
|
||||
|
||||
if(exit_status == 0)
|
||||
exit_status = WEXITSTATUS(status);
|
||||
|
||||
close(read_fd);
|
||||
return exit_status;
|
||||
} else {
|
||||
kill(process_id, SIGKILL);
|
||||
waitpid(process_id, &status, 0);
|
||||
close(read_fd);
|
||||
}
|
||||
} while(mgl_clock_get_elapsed_time_seconds(&clock) < timeout_sec);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
void string_split_char(std::string_view str, char delimiter, StringSplitCallback callback_func) {
|
||||
size_t index = 0;
|
||||
while(index < str.size()) {
|
||||
@@ -22,6 +83,38 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
bool starts_with(std::string_view str, const char *substr) {
|
||||
size_t len = strlen(substr);
|
||||
return str.size() >= len && memcmp(str.data(), substr, len) == 0;
|
||||
}
|
||||
|
||||
bool ends_with(std::string_view str, const char *substr) {
|
||||
size_t len = strlen(substr);
|
||||
return str.size() >= len && memcmp(str.data() + str.size() - len, substr, len) == 0;
|
||||
}
|
||||
|
||||
std::string strip(const std::string &str) {
|
||||
int start_index = 0;
|
||||
int str_len = str.size();
|
||||
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
start_index += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return str.substr(start_index, str_len);
|
||||
}
|
||||
|
||||
std::string get_home_dir() {
|
||||
const char *home_dir = getenv("HOME");
|
||||
if(!home_dir) {
|
||||
@@ -114,6 +207,14 @@ namespace gsr {
|
||||
return xdg_videos_dir;
|
||||
}
|
||||
|
||||
std::string get_pictures_dir() {
|
||||
auto xdg_vars = get_xdg_variables();
|
||||
std::string xdg_videos_dir = xdg_vars["XDG_PICTURES_DIR"];
|
||||
if(xdg_videos_dir.empty())
|
||||
xdg_videos_dir = get_home_dir() + "/Pictures";
|
||||
return xdg_videos_dir;
|
||||
}
|
||||
|
||||
int create_directory_recursive(char *path) {
|
||||
int path_len = strlen(path);
|
||||
char *p = path;
|
||||
@@ -198,4 +299,77 @@ namespace gsr {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool is_xdg_autostart_enabled() {
|
||||
const std::optional<std::string> output = get_xdg_autostart_content();
|
||||
return output.has_value() && output.value().find("Hidden=true") == std::string::npos;
|
||||
}
|
||||
|
||||
int set_xdg_autostart(bool enable) {
|
||||
const char *xdg_current_desktop = getenv("XDG_CURRENT_DESKTOP");
|
||||
if(!xdg_current_desktop || strlen(xdg_current_desktop) == 0) {
|
||||
std::string output;
|
||||
const char *check_dex_args[] = { "/bin/sh", "-c", "command -v dex", nullptr };
|
||||
if(exec_program_on_host_get_stdout(check_dex_args, output, true) != 0)
|
||||
return 67;
|
||||
}
|
||||
|
||||
const bool is_flatpak = getenv("FLATPAK_ID") != nullptr;
|
||||
const char *exec_line = is_flatpak
|
||||
? "Exec=flatpak run com.dec05eba.gpu_screen_recorder gsr-ui"
|
||||
: "Exec=gsr-ui launch-daemon";
|
||||
|
||||
std::string content =
|
||||
"[Desktop Entry]\n"
|
||||
"Type=Application\n"
|
||||
"Name=GPU Screen Recorder\n"
|
||||
"GenericName=Screen recorder\n"
|
||||
"Comment=A ShadowPlay-like screen recorder for Linux\n"
|
||||
"Icon=gpu-screen-recorder\n" +
|
||||
std::string(exec_line) + "\n" +
|
||||
"Terminal=false\n" +
|
||||
"Hidden=" + (enable ? "false" : "true") + "\n";
|
||||
|
||||
std::string shell_cmd =
|
||||
"p=\"${XDG_CONFIG_HOME:-$HOME/.config}/autostart/gpu-screen-recorder-ui.desktop\" && "
|
||||
"mkdir -p \"$(dirname \"$p\")\" && "
|
||||
"printf '" + content + "' > \"$p\"";
|
||||
|
||||
const char *args[] = { "/bin/sh", "-c", shell_cmd.c_str(), nullptr };
|
||||
std::string dummy;
|
||||
return exec_program_on_host_get_stdout(args, dummy, true);
|
||||
}
|
||||
|
||||
void replace_xdg_autostart_with_current_gsr_type() {
|
||||
const std::optional<std::string> output = get_xdg_autostart_content();
|
||||
if(!output.has_value())
|
||||
return;
|
||||
|
||||
const bool is_flatpak = getenv("FLATPAK_ID") != nullptr;
|
||||
const bool is_exec_flatpak = output.value().find("flatpak run") != std::string::npos;
|
||||
if(is_flatpak != is_exec_flatpak) {
|
||||
const bool is_autostart_enabled = output.value().find("Hidden=true") == std::string::npos;
|
||||
set_xdg_autostart(is_autostart_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
bool wait_until_systemd_user_service_available() {
|
||||
const char *args[] = { "systemctl", "--user", "-q", "is-enabled", "gpu-screen-recorder-ui.service", nullptr };
|
||||
const char *flatpak_args[] = { "flatpak-spawn", "--host", "--", "systemctl", "--user", "-q", "is-enabled", "gpu-screen-recorder-ui.service", nullptr };
|
||||
const bool is_flatpak = getenv("FLATPAK_ID") != nullptr;
|
||||
return run_command_timeout(is_flatpak ? flatpak_args : args, 1.0, 5.0) >= 0;
|
||||
}
|
||||
|
||||
bool is_systemd_service_enabled(const char *service_name) {
|
||||
const char *args[] = { "systemctl", "--user", "is-enabled", service_name, nullptr };
|
||||
std::string output;
|
||||
return exec_program_on_host_get_stdout(args, output, false) == 0;
|
||||
}
|
||||
|
||||
bool disable_systemd_service(const char *service_name) {
|
||||
const char *args[] = { "systemctl", "--user", "disable", service_name, nullptr };
|
||||
std::string output;
|
||||
return exec_program_on_host_get_stdout(args, output, false) == 0;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,255 @@
|
||||
#include "../include/WindowUtils.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/shapeconst.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
|
||||
#include <wayland-client.h>
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
|
||||
extern "C" {
|
||||
#include <mgl/window/window.h>
|
||||
}
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <poll.h>
|
||||
|
||||
#define MAX_PROPERTY_VALUE_LEN 4096
|
||||
|
||||
namespace gsr {
|
||||
struct WaylandOutput {
|
||||
uint32_t wl_name;
|
||||
struct wl_output *output;
|
||||
struct zxdg_output_v1 *xdg_output;
|
||||
mgl::vec2i pos;
|
||||
mgl::vec2i size;
|
||||
int32_t transform;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct Wayland {
|
||||
std::vector<WaylandOutput> outputs;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
};
|
||||
|
||||
static WaylandOutput* get_wayland_monitor_by_output(Wayland &wayland, struct wl_output *output) {
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
if(monitor.output == output)
|
||||
return &monitor;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static void output_handle_geometry(void *data, struct wl_output *wl_output,
|
||||
int32_t x, int32_t y, int32_t phys_width, int32_t phys_height,
|
||||
int32_t subpixel, const char *make, const char *model,
|
||||
int32_t transform) {
|
||||
(void)wl_output;
|
||||
(void)phys_width;
|
||||
(void)phys_height;
|
||||
(void)subpixel;
|
||||
(void)make;
|
||||
(void)model;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
monitor->transform = transform;
|
||||
}
|
||||
|
||||
static void output_handle_mode(void *data, struct wl_output *wl_output, uint32_t flags, int32_t width, int32_t height, int32_t refresh) {
|
||||
(void)wl_output;
|
||||
(void)flags;
|
||||
(void)refresh;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = height;
|
||||
}
|
||||
|
||||
static void output_handle_done(void *data, struct wl_output *wl_output) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
}
|
||||
|
||||
static void output_handle_scale(void* data, struct wl_output *wl_output, int32_t factor) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)factor;
|
||||
}
|
||||
|
||||
static void output_handle_name(void *data, struct wl_output *wl_output, const char *name) {
|
||||
(void)wl_output;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*wayland, wl_output);
|
||||
if(!monitor)
|
||||
return;
|
||||
|
||||
monitor->name = name;
|
||||
}
|
||||
|
||||
static void output_handle_description(void *data, struct wl_output *wl_output, const char *description) {
|
||||
(void)data;
|
||||
(void)wl_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct wl_output_listener output_listener = {
|
||||
output_handle_geometry,
|
||||
output_handle_mode,
|
||||
output_handle_done,
|
||||
output_handle_scale,
|
||||
output_handle_name,
|
||||
output_handle_description,
|
||||
};
|
||||
|
||||
static void registry_add_object(void *data, struct wl_registry *registry, uint32_t name, const char *interface, uint32_t version) {
|
||||
(void)version;
|
||||
Wayland *wayland = (Wayland*)data;
|
||||
if(strcmp(interface, wl_output_interface.name) == 0) {
|
||||
if(version < 4) {
|
||||
fprintf(stderr, "Warning: wl output interface version is < 4, expected >= 4\n");
|
||||
return;
|
||||
}
|
||||
|
||||
struct wl_output *output = (struct wl_output*)wl_registry_bind(registry, name, &wl_output_interface, 4);
|
||||
wayland->outputs.push_back(
|
||||
WaylandOutput{
|
||||
name,
|
||||
output,
|
||||
nullptr,
|
||||
mgl::vec2i{0, 0},
|
||||
mgl::vec2i{0, 0},
|
||||
0,
|
||||
""
|
||||
});
|
||||
wl_output_add_listener(output, &output_listener, wayland);
|
||||
} else if(strcmp(interface, zxdg_output_manager_v1_interface.name) == 0) {
|
||||
if(version < 1) {
|
||||
fprintf(stderr, "Warning: xdg output interface version is < 1, expected >= 1\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(wayland->xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland->xdg_output_manager);
|
||||
wayland->xdg_output_manager = NULL;
|
||||
}
|
||||
wayland->xdg_output_manager = (struct zxdg_output_manager_v1*)wl_registry_bind(registry, name, &zxdg_output_manager_v1_interface, 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void registry_remove_object(void *data, struct wl_registry *registry, uint32_t name) {
|
||||
(void)data;
|
||||
(void)registry;
|
||||
(void)name;
|
||||
// TODO: Remove output
|
||||
}
|
||||
|
||||
static struct wl_registry_listener registry_listener = {
|
||||
registry_add_object,
|
||||
registry_remove_object,
|
||||
};
|
||||
|
||||
static void xdg_output_logical_position(void *data, struct zxdg_output_v1 *zxdg_output_v1, int32_t x, int32_t y) {
|
||||
(void)zxdg_output_v1;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->pos.x = x;
|
||||
monitor->pos.y = y;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_logical_size(void *data, struct zxdg_output_v1 *xdg_output, int32_t width, int32_t height) {
|
||||
(void)xdg_output;
|
||||
WaylandOutput *monitor = (WaylandOutput*)data;
|
||||
monitor->size.x = width;
|
||||
monitor->size.y = height;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_done(void *data, struct zxdg_output_v1 *xdg_output) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_name(void *data, struct zxdg_output_v1 *xdg_output, const char *name) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)name;
|
||||
}
|
||||
|
||||
static void xdg_output_handle_description(void *data, struct zxdg_output_v1 *xdg_output, const char *description) {
|
||||
(void)data;
|
||||
(void)xdg_output;
|
||||
(void)description;
|
||||
}
|
||||
|
||||
static const struct zxdg_output_v1_listener xdg_output_listener = {
|
||||
xdg_output_logical_position,
|
||||
xdg_output_handle_logical_size,
|
||||
xdg_output_handle_done,
|
||||
xdg_output_handle_name,
|
||||
xdg_output_handle_description,
|
||||
};
|
||||
|
||||
static const int transform_90 = 1;
|
||||
static const int transform_270 = 3;
|
||||
|
||||
static void transform_monitors(Wayland &wayland) {
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
if(output.transform == transform_90 || output.transform == transform_270)
|
||||
std::swap(output.size.x, output.size.y);
|
||||
}
|
||||
}
|
||||
|
||||
static void set_monitor_outputs_from_xdg_output(Wayland &wayland, struct wl_display *dpy) {
|
||||
if(!wayland.xdg_output_manager) {
|
||||
fprintf(stderr, "Warning: WindowUtils::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for(WaylandOutput &monitor : wayland.outputs) {
|
||||
monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(wayland.xdg_output_manager, monitor.output);
|
||||
zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
|
||||
}
|
||||
|
||||
// Fetch xdg_output
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
|
||||
Atom ret_property_type = None;
|
||||
int ret_format = 0;
|
||||
unsigned long num_items = 0;
|
||||
unsigned long num_remaining_bytes = 0;
|
||||
unsigned char *data = nullptr;
|
||||
const Atom atom = XInternAtom(dpy, property_name, False);
|
||||
if(XGetWindowProperty(dpy, window, atom, 0, MAX_PROPERTY_VALUE_LEN / 4, False, property_type, &ret_property_type, &ret_format, &num_items, &num_remaining_bytes, &data) != Success || !data) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if(ret_property_type != property_type) {
|
||||
XFree(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
*property_size = (ret_format / (32 / sizeof(long))) * num_items;
|
||||
return data;
|
||||
}
|
||||
|
||||
static bool window_has_atom(Display *dpy, Window window, Atom atom) {
|
||||
Atom type;
|
||||
unsigned long len, bytes_left;
|
||||
@@ -29,7 +270,31 @@ namespace gsr {
|
||||
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
|
||||
}
|
||||
|
||||
static Window window_get_target_window_child(Display *display, Window window) {
|
||||
static Window get_window_graphics_parent(Display *dpy, Window window) {
|
||||
if(window == DefaultRootWindow(dpy) || window == None)
|
||||
return window;
|
||||
|
||||
XWindowAttributes attr;
|
||||
memset(&attr, 0, sizeof(attr));
|
||||
XGetWindowAttributes(dpy, window, &attr);
|
||||
if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable || !window_is_user_program(dpy, window)) {
|
||||
Window root;
|
||||
Window parent;
|
||||
Window *children = nullptr;
|
||||
unsigned int num_children = 0;
|
||||
if(!XQueryTree(dpy, window, &root, &parent, &children, &num_children))
|
||||
return None;
|
||||
|
||||
if(children)
|
||||
XFree(children);
|
||||
|
||||
if(parent)
|
||||
return get_window_graphics_parent(dpy, parent);
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
Window window_get_target_window_child(Display *display, Window window) {
|
||||
if(window == None)
|
||||
return None;
|
||||
|
||||
@@ -44,14 +309,14 @@ namespace gsr {
|
||||
return None;
|
||||
|
||||
Window found_window = None;
|
||||
for(int i = num_children - 1; i >= 0; --i) {
|
||||
for(int i = (int)num_children - 1; i >= 0; --i) {
|
||||
if(children[i] && window_is_user_program(display, children[i])) {
|
||||
found_window = children[i];
|
||||
goto finished;
|
||||
}
|
||||
}
|
||||
|
||||
for(int i = num_children - 1; i >= 0; --i) {
|
||||
for(int i = (int)num_children - 1; i >= 0; --i) {
|
||||
if(children[i]) {
|
||||
Window win = window_get_target_window_child(display, children[i]);
|
||||
if(win) {
|
||||
@@ -66,18 +331,28 @@ namespace gsr {
|
||||
return found_window;
|
||||
}
|
||||
|
||||
static Window get_window_at_cursor_position(Display *dpy) {
|
||||
mgl::vec2i get_cursor_position(Display *dpy, Window *window) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
*window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &dummy_i, &dummy_i, &dummy_i, &dummy_i, &dummy_u);
|
||||
if(window)
|
||||
window = window_get_target_window_child(dpy, window);
|
||||
return window;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
|
||||
const Window direct_window = *window;
|
||||
*window = window_get_target_window_child(dpy, *window);
|
||||
// HACK: Count some other x11 windows as having an x11 window focused. Some games seem to create an Input window and that gets focused.
|
||||
if(!*window) {
|
||||
XWindowAttributes attr;
|
||||
memset(&attr, 0, sizeof(attr));
|
||||
XGetWindowAttributes(dpy, direct_window, &attr);
|
||||
if(attr.c_class == InputOnly && !get_window_title(dpy, direct_window))
|
||||
*window = direct_window;
|
||||
}
|
||||
return root_pos;
|
||||
}
|
||||
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type) {
|
||||
Window get_focused_window(Display *dpy, WindowCaptureType cap_type, bool fallback_cursor_focused) {
|
||||
//const Atom net_active_window_atom = XInternAtom(dpy, "_NET_ACTIVE_WINDOW", False);
|
||||
Window focused_window = None;
|
||||
|
||||
@@ -100,18 +375,43 @@ namespace gsr {
|
||||
|
||||
int revert_to = 0;
|
||||
XGetInputFocus(dpy, &focused_window, &revert_to);
|
||||
if(focused_window && focused_window != DefaultRootWindow(dpy) && window_is_user_program(dpy, focused_window))
|
||||
focused_window = get_window_graphics_parent(dpy, focused_window);
|
||||
|
||||
if(focused_window && focused_window != DefaultRootWindow(dpy))
|
||||
return focused_window;
|
||||
|
||||
if(!fallback_cursor_focused)
|
||||
return None;
|
||||
}
|
||||
|
||||
focused_window = get_window_at_cursor_position(dpy);
|
||||
get_cursor_position(dpy, &focused_window);
|
||||
if(focused_window && focused_window != DefaultRootWindow(dpy))
|
||||
return focused_window;
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
static char* get_window_title(Display *dpy, Window window) {
|
||||
static std::string utf8_sanitize(const uint8_t *str, int size) {
|
||||
const uint32_t zero_width_space_codepoint = 0x200b; // Some games such as the finals has zero-width space characters
|
||||
std::string result;
|
||||
for(int i = 0; i < size;) {
|
||||
// Some games such as the finals has utf8-bom between each character, wtf?
|
||||
if(i + 3 <= size && memcmp(str + i, "\xEF\xBB\xBF", 3) == 0) {
|
||||
i += 3;
|
||||
continue;
|
||||
}
|
||||
|
||||
uint32_t codepoint = 0;
|
||||
size_t codepoint_length = 1;
|
||||
if(mgl::utf8_decode(str + i, size - i, &codepoint, &codepoint_length) && codepoint != zero_width_space_codepoint)
|
||||
result.append((const char*)str + i, codepoint_length);
|
||||
i += codepoint_length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_window_title(Display *dpy, Window window) {
|
||||
std::optional<std::string> result;
|
||||
const Atom net_wm_name_atom = XInternAtom(dpy, "_NET_WM_NAME", False);
|
||||
const Atom wm_name_atom = XInternAtom(dpy, "WM_NAME", False);
|
||||
const Atom utf8_string_atom = XInternAtom(dpy, "UTF8_STRING", False);
|
||||
@@ -123,8 +423,13 @@ namespace gsr {
|
||||
unsigned char *data = NULL;
|
||||
XGetWindowProperty(dpy, window, net_wm_name_atom, 0, 1024, False, utf8_string_atom, &type, &format, &num_items, &bytes_left, &data);
|
||||
|
||||
if(type == utf8_string_atom && format == 8 && data)
|
||||
return (char*)data;
|
||||
if(type == utf8_string_atom && format == 8 && data) {
|
||||
result = utf8_sanitize(data, num_items);
|
||||
goto done;
|
||||
}
|
||||
|
||||
if(data)
|
||||
XFree(data);
|
||||
|
||||
type = None;
|
||||
format = 0;
|
||||
@@ -133,60 +438,556 @@ namespace gsr {
|
||||
data = NULL;
|
||||
XGetWindowProperty(dpy, window, wm_name_atom, 0, 1024, False, 0, &type, &format, &num_items, &bytes_left, &data);
|
||||
|
||||
if((type == XA_STRING || type == utf8_string_atom) && data)
|
||||
return (char*)data;
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static const char* strip(const char *str, int *len) {
|
||||
int str_len = strlen(str);
|
||||
for(int i = 0; i < str_len; ++i) {
|
||||
if(str[i] != ' ') {
|
||||
str += i;
|
||||
str_len -= i;
|
||||
break;
|
||||
}
|
||||
if((type == XA_STRING || type == utf8_string_atom) && data) {
|
||||
result = utf8_sanitize(data, num_items);
|
||||
goto done;
|
||||
}
|
||||
|
||||
for(int i = str_len - 1; i >= 0; --i) {
|
||||
if(str[i] != ' ') {
|
||||
str_len = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
*len = str_len;
|
||||
return str;
|
||||
done:
|
||||
if(data)
|
||||
XFree(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string string_string(const char *str) {
|
||||
int len = 0;
|
||||
str = strip(str, &len);
|
||||
return std::string(str, len);
|
||||
}
|
||||
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type) {
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type, bool fallback_cursor_focused) {
|
||||
std::string result;
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type);
|
||||
const Window focused_window = get_focused_window(dpy, window_capture_type, fallback_cursor_focused);
|
||||
if(focused_window == None)
|
||||
return result;
|
||||
|
||||
// Window title is not always ideal (for example for a browser), but for games its pretty much required
|
||||
char *window_title = get_window_title(dpy, focused_window);
|
||||
const std::optional<std::string> window_title = get_window_title(dpy, focused_window);
|
||||
if(window_title) {
|
||||
result = string_string(window_title);
|
||||
XFree(window_title);
|
||||
result = strip(window_title.value());
|
||||
return result;
|
||||
}
|
||||
|
||||
XClassHint class_hint = {nullptr, nullptr};
|
||||
XGetClassHint(dpy, focused_window, &class_hint);
|
||||
if(class_hint.res_class) {
|
||||
result = string_string(class_hint.res_class);
|
||||
return result;
|
||||
}
|
||||
if(class_hint.res_class)
|
||||
result = strip(class_hint.res_class);
|
||||
|
||||
if(class_hint.res_name)
|
||||
XFree(class_hint.res_name);
|
||||
|
||||
if(class_hint.res_class)
|
||||
XFree(class_hint.res_class);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_window_name_at_position(Display *dpy, mgl::vec2i position, Window ignore_window) {
|
||||
std::string result;
|
||||
|
||||
Window root;
|
||||
Window parent;
|
||||
Window *children = nullptr;
|
||||
unsigned int num_children = 0;
|
||||
if(!XQueryTree(dpy, DefaultRootWindow(dpy), &root, &parent, &children, &num_children) || !children)
|
||||
return result;
|
||||
|
||||
for(int i = (int)num_children - 1; i >= 0; --i) {
|
||||
if(children[i] == ignore_window)
|
||||
continue;
|
||||
|
||||
XWindowAttributes attr;
|
||||
memset(&attr, 0, sizeof(attr));
|
||||
XGetWindowAttributes(dpy, children[i], &attr);
|
||||
if(attr.override_redirect || attr.c_class != InputOutput || attr.map_state != IsViewable)
|
||||
continue;
|
||||
|
||||
if(position.x >= attr.x && position.x <= attr.x + attr.width && position.y >= attr.y && position.y <= attr.y + attr.height) {
|
||||
const Window real_window = window_get_target_window_child(dpy, children[i]);
|
||||
if(!real_window || real_window == ignore_window)
|
||||
continue;
|
||||
|
||||
const std::optional<std::string> window_title = get_window_title(dpy, real_window);
|
||||
if(window_title)
|
||||
result = strip(window_title.value());
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XFree(children);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_window_name_at_cursor_position(Display *dpy, Window ignore_window) {
|
||||
Window cursor_window;
|
||||
const mgl::vec2i cursor_position = get_cursor_position(dpy, &cursor_window);
|
||||
return get_window_name_at_position(dpy, cursor_position, ignore_window);
|
||||
}
|
||||
|
||||
void set_window_size_not_resizable(Display *dpy, Window window, int width, int height) {
|
||||
XSizeHints *size_hints = XAllocSizeHints();
|
||||
if(size_hints) {
|
||||
size_hints->width = width;
|
||||
size_hints->height = height;
|
||||
size_hints->min_width = width;
|
||||
size_hints->min_height = height;
|
||||
size_hints->max_width = width;
|
||||
size_hints->max_height = height;
|
||||
size_hints->flags = PSize | PMinSize | PMaxSize;
|
||||
XSetWMNormalHints(dpy, window, size_hints);
|
||||
XFree(size_hints);
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
unsigned long flags;
|
||||
unsigned long functions;
|
||||
unsigned long decorations;
|
||||
long input_mode;
|
||||
unsigned long status;
|
||||
} MotifHints;
|
||||
|
||||
#define MWM_HINTS_DECORATIONS 2
|
||||
|
||||
#define MWM_DECOR_NONE 0
|
||||
#define MWM_DECOR_ALL 1
|
||||
|
||||
static void window_set_decorations_visible(Display *display, Window window, bool visible) {
|
||||
const Atom motif_wm_hints_atom = XInternAtom(display, "_MOTIF_WM_HINTS", False);
|
||||
MotifHints motif_hints;
|
||||
memset(&motif_hints, 0, sizeof(motif_hints));
|
||||
motif_hints.flags = MWM_HINTS_DECORATIONS;
|
||||
motif_hints.decorations = visible ? MWM_DECOR_ALL : MWM_DECOR_NONE;
|
||||
XChangeProperty(display, window, motif_wm_hints_atom, motif_wm_hints_atom, 32, PropModeReplace, (unsigned char*)&motif_hints, sizeof(motif_hints) / sizeof(long));
|
||||
}
|
||||
|
||||
static bool create_window_get_center_position_kde(Display *display, mgl::vec2i &position) {
|
||||
const int size = 1;
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.event_mask = StructureNotifyMask;
|
||||
window_attr.background_pixel = 0;
|
||||
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
|
||||
if(!window)
|
||||
return false;
|
||||
|
||||
const Atom net_wm_window_type_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE", False);
|
||||
const Atom net_wm_window_type_notification_atom = XInternAtom(display, "_NET_WM_WINDOW_TYPE_NOTIFICATION", False);
|
||||
const Atom net_wm_window_type_utility = XInternAtom(display, "_NET_WM_WINDOW_TYPE_UTILITY", False);
|
||||
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
|
||||
|
||||
const Atom window_type_atoms[2] = {
|
||||
net_wm_window_type_notification_atom,
|
||||
net_wm_window_type_utility
|
||||
};
|
||||
XChangeProperty(display, window, net_wm_window_type_atom, XA_ATOM, 32, PropModeReplace, (const unsigned char*)window_type_atoms, 2L);
|
||||
|
||||
const double alpha = 0.0;
|
||||
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
|
||||
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
|
||||
|
||||
window_set_decorations_visible(display, window, false);
|
||||
set_window_size_not_resizable(display, window, size, size);
|
||||
|
||||
XMapWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
bool got_data = false;
|
||||
const int x_fd = XConnectionNumber(display);
|
||||
XEvent xev;
|
||||
while(true) {
|
||||
struct pollfd poll_fd;
|
||||
poll_fd.fd = x_fd;
|
||||
poll_fd.events = POLLIN;
|
||||
poll_fd.revents = 0;
|
||||
const int fds_ready = poll(&poll_fd, 1, 200);
|
||||
if(fds_ready == 0) {
|
||||
fprintf(stderr, "Error: timed out waiting for ConfigureNotify after XCreateWindow\n");
|
||||
break;
|
||||
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while(XPending(display)) {
|
||||
XNextEvent(display, &xev);
|
||||
if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
|
||||
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
|
||||
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
|
||||
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
XDestroyWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
return got_data;
|
||||
}
|
||||
|
||||
static bool create_window_get_center_position_gnome(Display *display, mgl::vec2i &position) {
|
||||
const int size = 32;
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.event_mask = StructureNotifyMask | ExposureMask;
|
||||
window_attr.background_pixel = 0;
|
||||
const Window window = XCreateWindow(display, DefaultRootWindow(display), 0, 0, size, size, 0, CopyFromParent, InputOutput, CopyFromParent, CWBackPixel | CWEventMask, &window_attr);
|
||||
if(!window)
|
||||
return false;
|
||||
|
||||
const Atom net_wm_window_opacity = XInternAtom(display, "_NET_WM_WINDOW_OPACITY", False);
|
||||
const double alpha = 0.0;
|
||||
const unsigned long opacity = (unsigned long)(0xFFFFFFFFul * alpha);
|
||||
XChangeProperty(display, window, net_wm_window_opacity, XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&opacity, 1L);
|
||||
|
||||
window_set_decorations_visible(display, window, false);
|
||||
set_window_size_not_resizable(display, window, size, size);
|
||||
|
||||
XMapWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
bool got_data = false;
|
||||
const int x_fd = XConnectionNumber(display);
|
||||
XEvent xev;
|
||||
while(true) {
|
||||
struct pollfd poll_fd;
|
||||
poll_fd.fd = x_fd;
|
||||
poll_fd.events = POLLIN;
|
||||
poll_fd.revents = 0;
|
||||
const int fds_ready = poll(&poll_fd, 1, 200);
|
||||
if(fds_ready == 0) {
|
||||
fprintf(stderr, "Error: timed out waiting for MapNotify/ConfigureNotify after XCreateWindow\n");
|
||||
break;
|
||||
} else if(fds_ready == -1 || !(poll_fd.revents & POLLIN)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
while(XPending(display)) {
|
||||
XNextEvent(display, &xev);
|
||||
if(xev.type == MapNotify && xev.xmap.window == window) {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
Window w = None;
|
||||
XTranslateCoordinates(display, window, DefaultRootWindow(display), 0, 0, &x, &y, &w);
|
||||
|
||||
got_data = x > 0 && y > 0;
|
||||
position.x = x + size / 2;
|
||||
position.y = y + size / 2;
|
||||
if(got_data)
|
||||
goto done;
|
||||
} else if(xev.type == ConfigureNotify && xev.xconfigure.window == window) {
|
||||
got_data = xev.xconfigure.x > 0 && xev.xconfigure.y > 0;
|
||||
position.x = xev.xconfigure.x + xev.xconfigure.width / 2;
|
||||
position.y = xev.xconfigure.y + xev.xconfigure.height / 2;
|
||||
if(got_data)
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
done:
|
||||
XDestroyWindow(display, window);
|
||||
XFlush(display);
|
||||
|
||||
return got_data;
|
||||
}
|
||||
|
||||
mgl::vec2i create_window_get_center_position(Display *display) {
|
||||
mgl::vec2i pos;
|
||||
if(!create_window_get_center_position_kde(display, pos)) {
|
||||
pos.x = 0;
|
||||
pos.y = 0;
|
||||
create_window_get_center_position_gnome(display, pos);
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
std::string get_window_manager_name(Display *display) {
|
||||
std::string wm_name;
|
||||
unsigned int property_size = 0;
|
||||
Window window = None;
|
||||
|
||||
unsigned char *net_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_NET_SUPPORTING_WM_CHECK", &property_size);
|
||||
if(net_supporting_wm_check) {
|
||||
if(property_size == 8)
|
||||
window = *(Window*)net_supporting_wm_check;
|
||||
XFree(net_supporting_wm_check);
|
||||
}
|
||||
|
||||
if(!window) {
|
||||
unsigned char *win_supporting_wm_check = window_get_property(display, DefaultRootWindow(display), XA_WINDOW, "_WIN_SUPPORTING_WM_CHECK", &property_size);
|
||||
if(win_supporting_wm_check) {
|
||||
if(property_size == 8)
|
||||
window = *(Window*)win_supporting_wm_check;
|
||||
XFree(win_supporting_wm_check);
|
||||
}
|
||||
}
|
||||
|
||||
if(!window)
|
||||
return wm_name;
|
||||
|
||||
const std::optional<std::string> window_title = get_window_title(display, window);
|
||||
if(window_title)
|
||||
wm_name = strip(window_title.value());
|
||||
|
||||
return wm_name;
|
||||
}
|
||||
|
||||
bool is_compositor_running(Display *dpy, int screen) {
|
||||
char prop_name[20];
|
||||
snprintf(prop_name, sizeof(prop_name), "_NET_WM_CM_S%d", screen);
|
||||
const Atom prop_atom = XInternAtom(dpy, prop_name, False);
|
||||
return XGetSelectionOwner(dpy, prop_atom) != None;
|
||||
}
|
||||
|
||||
std::vector<Monitor> get_monitors(Display *dpy) {
|
||||
std::vector<Monitor> monitors;
|
||||
int nmonitors = 0;
|
||||
XRRMonitorInfo *monitor_info = XRRGetMonitors(dpy, DefaultRootWindow(dpy), True, &nmonitors);
|
||||
if(monitor_info) {
|
||||
for(int i = 0; i < nmonitors; ++i) {
|
||||
char *monitor_name = XGetAtomName(dpy, monitor_info[i].name);
|
||||
if(!monitor_name)
|
||||
continue;
|
||||
|
||||
monitors.push_back({mgl::vec2i(monitor_info[i].x, monitor_info[i].y), mgl::vec2i(monitor_info[i].width, monitor_info[i].height), std::string(monitor_name)});
|
||||
XFree(monitor_name);
|
||||
}
|
||||
XRRFreeMonitors(monitor_info);
|
||||
}
|
||||
return monitors;
|
||||
}
|
||||
|
||||
std::vector<Monitor> get_monitors_wayland(struct wl_display *dpy) {
|
||||
Wayland wayland;
|
||||
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, &wayland);
|
||||
|
||||
// Fetch globals
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
// Fetch wl_output
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
transform_monitors(wayland);
|
||||
set_monitor_outputs_from_xdg_output(wayland, dpy);
|
||||
|
||||
std::vector<Monitor> monitors;
|
||||
for(WaylandOutput &output : wayland.outputs) {
|
||||
monitors.push_back(Monitor{output.pos, output.size, std::move(output.name)});
|
||||
|
||||
if(output.output) {
|
||||
wl_output_destroy(output.output);
|
||||
output.output = nullptr;
|
||||
}
|
||||
|
||||
if(output.xdg_output) {
|
||||
zxdg_output_v1_destroy(output.xdg_output);
|
||||
output.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
wayland.outputs.clear();
|
||||
|
||||
if(wayland.xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(wayland.xdg_output_manager);
|
||||
wayland.xdg_output_manager = nullptr;
|
||||
}
|
||||
|
||||
wl_registry_destroy(registry);
|
||||
|
||||
return monitors;
|
||||
}
|
||||
|
||||
static bool device_is_mouse(const XIDeviceInfo *dev) {
|
||||
for(int i = 0; i < dev->num_classes; ++i) {
|
||||
if(dev->classes[i]->type == XIMasterPointer || dev->classes[i]->type == XISlavePointer)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void xi_grab_all_mouse_devices(Display *dpy, bool grab) {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
int num_devices = 0;
|
||||
XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &num_devices);
|
||||
if(!info)
|
||||
return;
|
||||
|
||||
unsigned char mask[XIMaskLen(XI_LASTEVENT)];
|
||||
memset(mask, 0, sizeof(mask));
|
||||
XISetMask(mask, XI_Motion);
|
||||
//XISetMask(mask, XI_RawMotion);
|
||||
XISetMask(mask, XI_ButtonPress);
|
||||
XISetMask(mask, XI_ButtonRelease);
|
||||
XISetMask(mask, XI_KeyPress);
|
||||
XISetMask(mask, XI_KeyRelease);
|
||||
|
||||
for (int i = 0; i < num_devices; ++i) {
|
||||
const XIDeviceInfo *dev = &info[i];
|
||||
if(!device_is_mouse(dev))
|
||||
continue;
|
||||
|
||||
XIEventMask xi_masks;
|
||||
xi_masks.deviceid = dev->deviceid;
|
||||
xi_masks.mask_len = sizeof(mask);
|
||||
xi_masks.mask = mask;
|
||||
if(grab)
|
||||
XIGrabDevice(dpy, dev->deviceid, DefaultRootWindow(dpy), CurrentTime, None, XIGrabModeAsync, XIGrabModeAsync, XIOwnerEvents, &xi_masks);
|
||||
else
|
||||
XIUngrabDevice(dpy, dev->deviceid, CurrentTime);
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XIFreeDeviceInfo(info);
|
||||
}
|
||||
|
||||
void xi_grab_all_mouse_devices(Display *dpy) {
|
||||
xi_grab_all_mouse_devices(dpy, true);
|
||||
}
|
||||
|
||||
void xi_ungrab_all_mouse_devices(Display *dpy) {
|
||||
xi_grab_all_mouse_devices(dpy, false);
|
||||
}
|
||||
|
||||
void xi_warp_all_mouse_devices(Display *dpy, mgl::vec2i position) {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
int num_devices = 0;
|
||||
XIDeviceInfo *info = XIQueryDevice(dpy, XIAllDevices, &num_devices);
|
||||
if(!info)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < num_devices; ++i) {
|
||||
const XIDeviceInfo *dev = &info[i];
|
||||
if(!device_is_mouse(dev))
|
||||
continue;
|
||||
|
||||
XIWarpPointer(dpy, dev->deviceid, DefaultRootWindow(dpy), DefaultRootWindow(dpy), 0, 0, 0, 0, position.x, position.y);
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XIFreeDeviceInfo(info);
|
||||
}
|
||||
|
||||
void window_set_fullscreen(Display *dpy, Window window, bool fullscreen) {
|
||||
const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
|
||||
const Atom net_wm_state_fullscreen_atom = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False);
|
||||
|
||||
XEvent xev;
|
||||
xev.type = ClientMessage;
|
||||
xev.xclient.window = window;
|
||||
xev.xclient.message_type = net_wm_state_atom;
|
||||
xev.xclient.format = 32;
|
||||
xev.xclient.data.l[0] = fullscreen ? 1 : 0;
|
||||
xev.xclient.data.l[1] = net_wm_state_fullscreen_atom;
|
||||
xev.xclient.data.l[2] = 0;
|
||||
xev.xclient.data.l[3] = 1;
|
||||
xev.xclient.data.l[4] = 0;
|
||||
|
||||
if(!XSendEvent(dpy, DefaultRootWindow(dpy), 0, SubstructureRedirectMask | SubstructureNotifyMask, &xev)) {
|
||||
fprintf(stderr, "mgl warning: failed to change window fullscreen state\n");
|
||||
return;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
bool window_is_fullscreen(Display *display, Window window) {
|
||||
const Atom wm_state_atom = XInternAtom(display, "_NET_WM_STATE", False);
|
||||
const Atom wm_state_fullscreen_atom = XInternAtom(display, "_NET_WM_STATE_FULLSCREEN", False);
|
||||
|
||||
Atom type = None;
|
||||
int format = 0;
|
||||
unsigned long num_items = 0;
|
||||
unsigned long bytes_after = 0;
|
||||
unsigned char *properties = nullptr;
|
||||
if(XGetWindowProperty(display, window, wm_state_atom, 0, 1024, False, XA_ATOM, &type, &format, &num_items, &bytes_after, &properties) < Success) {
|
||||
fprintf(stderr, "Failed to get window wm state property\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!properties)
|
||||
return false;
|
||||
|
||||
bool is_fullscreen = false;
|
||||
Atom *atoms = (Atom*)properties;
|
||||
for(unsigned long i = 0; i < num_items; ++i) {
|
||||
if(atoms[i] == wm_state_fullscreen_atom) {
|
||||
is_fullscreen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
XFree(properties);
|
||||
return is_fullscreen;
|
||||
}
|
||||
|
||||
bool get_drawable_geometry(Display *display, Drawable drawable, DrawableGeometry *geometry) {
|
||||
geometry->x = 0;
|
||||
geometry->y = 0;
|
||||
geometry->width = 0;
|
||||
geometry->height = 0;
|
||||
|
||||
Window root_window;
|
||||
unsigned int w, h;
|
||||
unsigned int dummy_border, dummy_depth;
|
||||
Status s = XGetGeometry(display, drawable, &root_window, &geometry->x, &geometry->y, &w, &h, &dummy_border, &dummy_depth);
|
||||
|
||||
geometry->width = w;
|
||||
geometry->height = h;
|
||||
return s == True;
|
||||
}
|
||||
|
||||
std::optional<Monitor> get_monitor_by_window_center(Display *display, Window window) {
|
||||
DrawableGeometry geometry;
|
||||
if(!get_drawable_geometry(display, window, &geometry))
|
||||
return std::nullopt;
|
||||
|
||||
const mgl::vec2i window_center = mgl::vec2i(geometry.x, geometry.y) + mgl::vec2i(geometry.width, geometry.height) / 2;
|
||||
auto monitors = get_monitors(display);
|
||||
for(auto &monitor : monitors) {
|
||||
if(mgl::IntRect(monitor.position, monitor.size).contains(window_center))
|
||||
return std::move(monitor);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
#define _NET_WM_STATE_REMOVE 0
|
||||
#define _NET_WM_STATE_ADD 1
|
||||
#define _NET_WM_STATE_TOGGLE 2
|
||||
|
||||
bool set_window_wm_state(Display *dpy, Window window, Atom atom) {
|
||||
const Atom net_wm_state_atom = XInternAtom(dpy, "_NET_WM_STATE", False);
|
||||
|
||||
XClientMessageEvent xclient;
|
||||
memset(&xclient, 0, sizeof(xclient));
|
||||
|
||||
xclient.type = ClientMessage;
|
||||
xclient.window = window;
|
||||
xclient.message_type = net_wm_state_atom;
|
||||
xclient.format = 32;
|
||||
xclient.data.l[0] = _NET_WM_STATE_ADD;
|
||||
xclient.data.l[1] = atom;
|
||||
xclient.data.l[2] = 0;
|
||||
xclient.data.l[3] = 0;
|
||||
xclient.data.l[4] = 0;
|
||||
|
||||
XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, (XEvent*)&xclient);
|
||||
XFlush(dpy);
|
||||
return true;
|
||||
}
|
||||
|
||||
void make_window_click_through(Display *display, Window window) {
|
||||
XRectangle rect;
|
||||
memset(&rect, 0, sizeof(rect));
|
||||
XserverRegion region = XFixesCreateRegion(display, &rect, 1);
|
||||
XFixesSetWindowShapeRegion(display, window, ShapeInput, 0, 0, region);
|
||||
XFixesDestroyRegion(display, region);
|
||||
}
|
||||
|
||||
bool make_window_sticky(Display *dpy, Window window) {
|
||||
return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_STICKY", False));
|
||||
}
|
||||
|
||||
bool hide_window_from_taskbar(Display *dpy, Window window) {
|
||||
return set_window_wm_state(dpy, window, XInternAtom(dpy, "_NET_WM_STATE_SKIP_TASKBAR", False));
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,8 @@ namespace gsr {
|
||||
// These are relative to the button size
|
||||
static const float padding_top_icon_scale = 0.25f;
|
||||
static const float padding_bottom_icon_scale = 0.25f;
|
||||
static const float padding_left_icon_scale = 0.25f;
|
||||
static const float padding_right_icon_scale = 0.25f;
|
||||
//static const float padding_left_icon_scale = 0.25f;
|
||||
static const float padding_right_icon_scale = 0.15f;
|
||||
|
||||
Button::Button(mgl::Font *font, const char *text, mgl::vec2f size, mgl::Color bg_color) :
|
||||
size(size), bg_color(bg_color), bg_hover_color(bg_color), text(text, *font)
|
||||
@@ -53,13 +53,21 @@ namespace gsr {
|
||||
background.set_color(mouse_inside ? bg_hover_color : bg_color);
|
||||
window.draw(background);
|
||||
|
||||
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(text);
|
||||
|
||||
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
|
||||
scale_sprite_to_button_size();
|
||||
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
if(text.get_string().empty()) // Center
|
||||
sprite.set_position((background.get_position() + background.get_size() * 0.5f - sprite.get_size() * 0.5f).floor());
|
||||
else // Left
|
||||
sprite.set_position((draw_pos + mgl::vec2f(padding_left, background.get_size().y * 0.5f - sprite.get_size().y * 0.5f)).floor());
|
||||
window.draw(sprite);
|
||||
|
||||
const int padding_icon_right = padding_right_icon_scale * get_button_height();
|
||||
text.set_position((sprite.get_position() + mgl::vec2f(sprite.get_size().x + padding_icon_right, sprite.get_size().y * 0.5f - text.get_bounds().size.y * 0.52f)).floor());
|
||||
window.draw(text);
|
||||
} else {
|
||||
text.set_position((draw_pos + item_size * 0.5f - text.get_bounds().size * 0.5f).floor());
|
||||
window.draw(text);
|
||||
}
|
||||
|
||||
if(mouse_inside) {
|
||||
@@ -72,24 +80,35 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return {0.0f, 0.0f};
|
||||
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int padding_right = padding_right_scale * get_theme().window_height;
|
||||
|
||||
const mgl::vec2f text_bounds = text.get_bounds().size;
|
||||
mgl::vec2f s = size;
|
||||
if(s.x < 0.0001f)
|
||||
s.x = padding_left + text_bounds.x + padding_right;
|
||||
if(s.y < 0.0001f)
|
||||
s.y = padding_top + text_bounds.y + padding_bottom;
|
||||
return s;
|
||||
mgl::vec2f widget_size = size;
|
||||
|
||||
if(widget_size.y < 0.0001f)
|
||||
widget_size.y = get_button_height();
|
||||
|
||||
if(widget_size.x < 0.0001f) {
|
||||
widget_size.x = padding_left + text_bounds.x + padding_right;
|
||||
if(sprite.get_texture() && sprite.get_texture()->is_valid()) {
|
||||
scale_sprite_to_button_size();
|
||||
const int padding_icon_right = text_bounds.x > 0.001f ? padding_right_icon_scale * widget_size.y : 0.0f;
|
||||
widget_size.x += sprite.get_size().x + padding_icon_right;
|
||||
}
|
||||
}
|
||||
|
||||
return widget_size;
|
||||
}
|
||||
|
||||
void Button::set_border_scale(float scale) {
|
||||
border_scale = scale;
|
||||
}
|
||||
|
||||
void Button::set_icon_padding_scale(float scale) {
|
||||
icon_padding_scale = scale;
|
||||
}
|
||||
|
||||
void Button::set_bg_hover_color(mgl::Color color) {
|
||||
bg_hover_color = color;
|
||||
}
|
||||
@@ -110,13 +129,23 @@ namespace gsr {
|
||||
if(!sprite.get_texture() || !sprite.get_texture()->is_valid())
|
||||
return;
|
||||
|
||||
const mgl::vec2f button_size = get_size();
|
||||
const int padding_icon_top = padding_top_icon_scale * button_size.y;
|
||||
const int padding_icon_bottom = padding_bottom_icon_scale * button_size.y;
|
||||
const int padding_icon_left = padding_left_icon_scale * button_size.y;
|
||||
const int padding_icon_right = padding_right_icon_scale * button_size.y;
|
||||
const float widget_height = get_button_height();
|
||||
|
||||
const mgl::vec2f desired_size = button_size - mgl::vec2f(padding_icon_left + padding_icon_right, padding_icon_top + padding_icon_bottom);
|
||||
sprite.set_size(scale_keep_aspect_ratio(sprite.get_texture()->get_size().to_vec2f(), desired_size).floor());
|
||||
const int padding_icon_top = padding_top_icon_scale * icon_padding_scale * widget_height;
|
||||
const int padding_icon_bottom = padding_bottom_icon_scale * icon_padding_scale * widget_height;
|
||||
|
||||
const float desired_height = widget_height - (padding_icon_top + padding_icon_bottom);
|
||||
sprite.set_height((int)desired_height);
|
||||
}
|
||||
|
||||
float Button::get_button_height() {
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
|
||||
|
||||
float widget_height = size.y;
|
||||
if(widget_height < 0.0001f)
|
||||
widget_height = padding_top + text.get_bounds().size.y + padding_bottom;
|
||||
|
||||
return widget_height;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,8 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return true;
|
||||
|
||||
handle_tooltip_event(event, position + offset, get_size());
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
const bool clicked_inside = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
|
||||
if(clicked_inside) {
|
||||
|
||||
@@ -26,16 +26,21 @@ namespace gsr {
|
||||
return true;
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
|
||||
|
||||
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
|
||||
const mgl::vec2f item_size = get_size();
|
||||
mgl::vec2f item_size = get_size();
|
||||
|
||||
if(show_dropdown) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
Item &item = items[i];
|
||||
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
|
||||
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
|
||||
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos) && item.enabled) {
|
||||
const size_t prev_selected_item = selected_item;
|
||||
selected_item = i;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
remove_widget_as_selected_in_parent();
|
||||
|
||||
if(selected_item != prev_selected_item && on_selection_changed)
|
||||
@@ -47,6 +52,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
item_size = get_size();
|
||||
if(mgl::FloatRect(draw_pos, item_size).contains(mouse_pos)) {
|
||||
show_dropdown = !show_dropdown;
|
||||
if(show_dropdown)
|
||||
@@ -66,9 +72,10 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
//const mgl::Scissor scissor = window.get_scissor();
|
||||
update_if_dirty();
|
||||
|
||||
const mgl::vec2f draw_pos = (position + offset).floor();
|
||||
//max_size.x = std::min((scissor.position.x + scissor.size.x) - draw_pos.x, max_size.x);
|
||||
|
||||
if(show_dropdown)
|
||||
draw_selected(window, draw_pos);
|
||||
@@ -76,17 +83,34 @@ namespace gsr {
|
||||
draw_unselected(window, draw_pos);
|
||||
}
|
||||
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id) {
|
||||
void ComboBox::add_item(const std::string &text, const std::string &id, bool allow_duplicate) {
|
||||
if(!allow_duplicate) {
|
||||
for(const auto &item : items) {
|
||||
if(item.id == id)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
items.push_back({mgl::Text(text, *font), id, {0.0f, 0.0f}});
|
||||
items.back().text.set_max_width(font->get_character_size() * 20); // TODO: Make a proper solution
|
||||
//items.back().text.set_max_rows(1);
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void ComboBox::clear_items() {
|
||||
items.clear();
|
||||
selected_item = 0;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void ComboBox::set_selected_item(const std::string &id, bool trigger_event, bool trigger_event_even_if_selection_not_changed) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
if(item.id == id) {
|
||||
if(item.id == id && item.enabled) {
|
||||
const size_t prev_selected_item = selected_item;
|
||||
selected_item = i;
|
||||
dirty = true;
|
||||
|
||||
if(trigger_event && (trigger_event_even_if_selection_not_changed || selected_item != prev_selected_item) && on_selection_changed)
|
||||
on_selection_changed(item.text.get_string(), item.id);
|
||||
@@ -96,6 +120,22 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
void ComboBox::set_item_enabled(const std::string &id, bool enabled) {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
auto &item = items[i];
|
||||
if(item.id == id) {
|
||||
item.enabled = enabled;
|
||||
item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
|
||||
if(selected_item == i) {
|
||||
selected_item = 0;
|
||||
show_dropdown = false;
|
||||
dirty = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& ComboBox::get_selected_id() const {
|
||||
if(items.empty()) {
|
||||
static std::string dummy;
|
||||
@@ -107,13 +147,13 @@ namespace gsr {
|
||||
|
||||
void ComboBox::draw_selected(mgl::Window &window, mgl::vec2f draw_pos) {
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
|
||||
mgl_scissor scissor;
|
||||
mgl_window_get_scissor(window.internal_window(), &scissor);
|
||||
const mgl::Scissor scissor = window.get_scissor();
|
||||
const bool bottom_is_outside_scissor = draw_pos.y + max_size.y > scissor.position.y + scissor.size.y;
|
||||
|
||||
const mgl::vec2f item_size = get_size();
|
||||
mgl::vec2f item_size = get_size();
|
||||
mgl::vec2f items_draw_pos = draw_pos + mgl::vec2f(0.0f, item_size.y);
|
||||
|
||||
mgl::Rectangle background(draw_pos, item_size.floor());
|
||||
@@ -137,7 +177,10 @@ namespace gsr {
|
||||
const mgl::vec2f mouse_pos = window.get_mouse_position().to_vec2f();
|
||||
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
if(!cursor_inside) {
|
||||
Item &item = items[i];
|
||||
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
|
||||
|
||||
if(!cursor_inside && item.enabled) {
|
||||
cursor_inside = mgl::FloatRect(items_draw_pos, item_size).contains(mouse_pos);
|
||||
if(cursor_inside) {
|
||||
mgl::Rectangle item_background(items_draw_pos.floor(), item_size.floor());
|
||||
@@ -146,7 +189,6 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
Item &item = items[i];
|
||||
item.text.set_position((items_draw_pos + mgl::vec2f(padding_left, padding_top)).floor());
|
||||
window.draw(item.text);
|
||||
|
||||
@@ -160,7 +202,7 @@ namespace gsr {
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int padding_right = padding_right_scale * get_theme().window_height;
|
||||
|
||||
const mgl::vec2f item_size = get_size();
|
||||
mgl::vec2f item_size = get_size();
|
||||
mgl::Rectangle background(draw_pos.floor(), item_size.floor());
|
||||
background.set_color(mgl::Color(0, 0, 0, 120));
|
||||
window.draw(background);
|
||||
@@ -197,11 +239,12 @@ namespace gsr {
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int padding_right = padding_right_scale * get_theme().window_height;
|
||||
|
||||
max_size = { 0.0f, font->get_character_size() + (float)padding_top + (float)padding_bottom };
|
||||
Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
|
||||
max_size = { 0.0f, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : font->get_character_size()) };
|
||||
for(Item &item : items) {
|
||||
const mgl::vec2f bounds = item.text.get_bounds().size;
|
||||
max_size.x = std::max(max_size.x, bounds.x + padding_left + padding_right);
|
||||
max_size.y += bounds.y + padding_top + padding_bottom;
|
||||
max_size.y += padding_top + bounds.y + padding_bottom;
|
||||
}
|
||||
|
||||
if(max_size.x <= 0.001f)
|
||||
@@ -219,7 +262,8 @@ namespace gsr {
|
||||
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_bottom = padding_bottom_scale * get_theme().window_height;
|
||||
return { max_size.x, font->get_character_size() + (float)padding_top + (float)padding_bottom };
|
||||
Item *selected_item_ptr = (selected_item < items.size()) ? &items[selected_item] : nullptr;
|
||||
return { max_size.x, padding_top + padding_bottom + (selected_item_ptr ? selected_item_ptr->text.get_bounds().size.y : font->get_character_size()) };
|
||||
}
|
||||
|
||||
float ComboBox::get_dropdown_arrow_height() const {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
@@ -17,19 +18,14 @@ namespace gsr {
|
||||
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
mgl_scissor prev_scissor;
|
||||
mgl_window_get_scissor(window.internal_window(), &prev_scissor);
|
||||
|
||||
const mgl_scissor new_scissor = {
|
||||
mgl_vec2i{(int)draw_pos.x, (int)draw_pos.y},
|
||||
mgl_vec2i{(int)size.x, (int)size.y}
|
||||
};
|
||||
mgl_window_set_scissor(window.internal_window(), &new_scissor);
|
||||
const mgl::Scissor parent_scissor = window.get_scissor();
|
||||
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor, {draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
if(draw_handler)
|
||||
draw_handler(window, draw_pos, size);
|
||||
|
||||
mgl_window_set_scissor(window.internal_window(), &prev_scissor);
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
mgl::vec2f CustomRendererWidget::get_size() {
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace gsr {
|
||||
{
|
||||
if(icon_texture && icon_texture->is_valid()) {
|
||||
icon_sprite.set_texture(icon_texture);
|
||||
icon_sprite.set_height((int)(size.y * 0.5f));
|
||||
icon_sprite.set_height((int)(size.y * 0.45f));
|
||||
}
|
||||
this->description.set_color(mgl::Color(150, 150, 150));
|
||||
}
|
||||
@@ -110,6 +110,14 @@ namespace gsr {
|
||||
window.draw(rect);
|
||||
}
|
||||
|
||||
if(activated) {
|
||||
description.set_color(get_color_theme().tint_color);
|
||||
icon_sprite.set_color(get_color_theme().tint_color);
|
||||
} else {
|
||||
description.set_color(mgl::Color(150, 150, 150));
|
||||
icon_sprite.set_color(mgl::Color(255, 255, 255));
|
||||
}
|
||||
|
||||
const int text_margin = size.y * 0.085;
|
||||
|
||||
const auto title_bounds = title.get_bounds();
|
||||
@@ -148,7 +156,7 @@ namespace gsr {
|
||||
window.draw(separator);
|
||||
}
|
||||
|
||||
if(mouse_inside_item == -1) {
|
||||
if(mouse_inside_item == -1 && item.enabled) {
|
||||
const bool inside = mgl::FloatRect(item_position, item_size).contains({ (float)mouse_pos.x, (float)mouse_pos.y });
|
||||
if(inside) {
|
||||
draw_rectangle_outline(window, item_position, item_size, get_color_theme().tint_color, border_size);
|
||||
@@ -161,16 +169,18 @@ namespace gsr {
|
||||
mgl::Sprite icon(item.icon_texture);
|
||||
icon.set_height((int)(item_size.y * 0.4f));
|
||||
icon.set_position((item_position + mgl::vec2f(padding_left, item_size.y * 0.5f - icon.get_size().y * 0.5f)).floor());
|
||||
icon.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
|
||||
window.draw(icon);
|
||||
icon_offset = icon.get_size().x + icon_spacing;
|
||||
}
|
||||
|
||||
item.text.set_position((item_position + mgl::vec2f(padding_left + icon_offset, item_size.y * 0.5f - text_bounds.size.y * 0.5f)).floor());
|
||||
item.text.set_color(item.enabled ? mgl::Color(255, 255, 255, 255) : mgl::Color(255, 255, 255, 80));
|
||||
window.draw(item.text);
|
||||
|
||||
const auto description_bounds = item.description_text.get_bounds();
|
||||
item.description_text.set_position((item_position + mgl::vec2f(item_size.x - description_bounds.size.x - padding_right, item_size.y * 0.5f - description_bounds.size.y * 0.5f)).floor());
|
||||
item.description_text.set_color(mgl::Color(255, 255, 255, 120));
|
||||
item.description_text.set_color(item.enabled ? mgl::Color(255, 255, 255, 120) : mgl::Color(255, 255, 255, 40));
|
||||
window.draw(item.description_text);
|
||||
|
||||
item_position.y += item_size.y;
|
||||
@@ -179,6 +189,10 @@ namespace gsr {
|
||||
}
|
||||
|
||||
void DropdownButton::add_item(const std::string &text, const std::string &id, const std::string &description) {
|
||||
for(auto &item : items) {
|
||||
if(item.id == id)
|
||||
return;
|
||||
}
|
||||
items.push_back({mgl::Text(text, *title_font), mgl::Text(description, *description_font), nullptr, id});
|
||||
dirty = true;
|
||||
}
|
||||
@@ -201,6 +215,24 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownButton::set_item_description(const std::string &id, const std::string &new_description) {
|
||||
for(auto &item : items) {
|
||||
if(item.id == id) {
|
||||
item.description_text.set_string(new_description);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownButton::set_item_enabled(const std::string &id, bool enabled) {
|
||||
for(auto &item : items) {
|
||||
if(item.id == id) {
|
||||
item.enabled = enabled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownButton::set_description(std::string description_text) {
|
||||
description.set_string(std::move(description_text));
|
||||
}
|
||||
@@ -210,14 +242,6 @@ namespace gsr {
|
||||
return;
|
||||
|
||||
this->activated = activated;
|
||||
|
||||
if(activated) {
|
||||
description.set_color(get_color_theme().tint_color);
|
||||
icon_sprite.set_color(get_color_theme().tint_color);
|
||||
} else {
|
||||
description.set_color(mgl::Color(150, 150, 150));
|
||||
icon_sprite.set_color(mgl::Color(255, 255, 255));
|
||||
}
|
||||
}
|
||||
|
||||
void DropdownButton::update_if_dirty() {
|
||||
@@ -242,4 +266,4 @@ namespace gsr {
|
||||
update_if_dirty();
|
||||
return size;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||