Compare commits
198 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 |
5
.gitignore
vendored
@@ -4,3 +4,8 @@ compile_commands.json
|
||||
|
||||
**/xdg-output-unstable-v1-client-protocol.h
|
||||
**/xdg-output-unstable-v1-protocol.c
|
||||
|
||||
depends/.wraplock
|
||||
|
||||
.cache
|
||||
build/
|
||||
38
README.md
@@ -4,18 +4,20 @@
|
||||
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.
|
||||
|
||||
# Usage
|
||||
You can start the overlay UI and make it start automatically on system startup by running `systemctl enable --now --user gpu-screen-recorder-ui`.
|
||||
Alternatively you can run `gsr-ui` and go into settings and enable start on system startup setting.\
|
||||
Press `Left Alt+Z` to show/hide the UI. Go into settings to view all of the different hotkeys configured.\
|
||||
If you use a non-systemd distro and want to start the UI on system startup then you have to manually add `gsr-ui` to your system 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.
|
||||
|
||||
# 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 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.
|
||||
|
||||
@@ -27,6 +29,7 @@ These are the dependencies needed to build GPU Screen Recorder UI:
|
||||
* linux-api-headers
|
||||
* libpulse (libpulse-simple)
|
||||
* libdrm
|
||||
* libdbus
|
||||
* wayland (wayland-client, wayland-egl, wayland-scanner)
|
||||
* setcap (libcap)
|
||||
|
||||
@@ -36,15 +39,20 @@ There are also additional dependencies needed at runtime:
|
||||
* [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
|
||||
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 keyboard 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`.
|
||||
@@ -71,3 +79,11 @@ I'm looking for somebody that can create sound effects for the notifications.
|
||||
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.
|
||||
|
||||
84
TODO
@@ -25,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).
|
||||
@@ -64,8 +62,6 @@ 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.
|
||||
|
||||
Use global shortcuts desktop portal protocol on wayland when available.
|
||||
@@ -84,10 +80,6 @@ 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.
|
||||
|
||||
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.
|
||||
@@ -121,8 +113,6 @@ System startup option should also support runit and some other init systems, not
|
||||
|
||||
Use x11 shm instead of XGetImage (https://stackoverflow.com/questions/43442675/how-to-use-xshmgetimage-and-xshmputimage).
|
||||
|
||||
Add a hotkey to record/stream/replay region.
|
||||
|
||||
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).
|
||||
@@ -131,8 +121,6 @@ Add option to do screen-direct recording. But make it clear that it should not b
|
||||
|
||||
Add systray for recording status.
|
||||
|
||||
Add a desktop icon when gsr-ui has a window mode option (which should be the default launch option).
|
||||
|
||||
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.
|
||||
@@ -172,8 +160,6 @@ Add a bug report page that automatically includes system info (make this clear t
|
||||
|
||||
Make it possible to change controller hotkeys. Also read from /dev/input/eventN instead of /dev/input/jsN. This is readable for controllers.
|
||||
|
||||
Add option to copy screenshot to clipboard. Does it work properly on Wayland compositors? Maybe need to wait until the application becomes Wayland native instead of XWayland.
|
||||
|
||||
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.
|
||||
@@ -196,8 +182,6 @@ 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?
|
||||
|
||||
Automatically mark window region in window capture for screenshot on x11.
|
||||
|
||||
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.
|
||||
@@ -205,3 +189,71 @@ Disable hotkeys if virtual keyboard is found (either at startup or after running
|
||||
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.
|
||||
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/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/question_mark.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
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 |
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;
|
||||
};
|
||||
}
|
||||
@@ -60,14 +60,32 @@ namespace gsr {
|
||||
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 = 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;
|
||||
};
|
||||
|
||||
@@ -83,19 +101,24 @@ namespace gsr {
|
||||
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_hotkey;
|
||||
};
|
||||
@@ -103,13 +126,12 @@ namespace gsr {
|
||||
struct RecordConfig {
|
||||
RecordOptions record_options;
|
||||
bool save_video_in_game_folder = false;
|
||||
bool show_recording_started_notifications = true;
|
||||
bool show_video_saved_notifications = true;
|
||||
bool show_video_paused_notifications = true;
|
||||
std::string save_directory;
|
||||
std::string container = "mp4";
|
||||
ConfigHotkey start_stop_hotkey;
|
||||
ConfigHotkey pause_unpause_hotkey;
|
||||
ConfigHotkey start_stop_region_hotkey;
|
||||
ConfigHotkey start_stop_window_hotkey;
|
||||
};
|
||||
|
||||
struct ReplayConfig {
|
||||
@@ -117,9 +139,6 @@ namespace gsr {
|
||||
std::string turn_on_replay_automatically_mode = "dont_turn_on_automatically";
|
||||
bool save_video_in_game_folder = false;
|
||||
bool restart_replay_on_save = false;
|
||||
bool show_replay_started_notifications = true;
|
||||
bool show_replay_stopped_notifications = true;
|
||||
bool show_replay_saved_notifications = true;
|
||||
std::string save_directory;
|
||||
std::string container = "mp4";
|
||||
int32_t replay_time = 60;
|
||||
@@ -141,10 +160,16 @@ namespace gsr {
|
||||
bool restore_portal_session = true;
|
||||
|
||||
bool save_screenshot_in_game_folder = false;
|
||||
bool show_screenshot_saved_notifications = true;
|
||||
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 {
|
||||
@@ -163,4 +188,4 @@ namespace gsr {
|
||||
|
||||
std::optional<Config> read_config(const SupportedCaptureOptions &capture_options);
|
||||
void save_config(Config &config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "CursorTracker.hpp"
|
||||
#include <stdint.h>
|
||||
#include <vector>
|
||||
|
||||
struct wl_display;
|
||||
struct wl_registry;
|
||||
struct wl_output;
|
||||
struct zxdg_output_manager_v1;
|
||||
struct zxdg_output_v1;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
class CursorTrackerWayland : public CursorTracker {
|
||||
public:
|
||||
CursorTrackerWayland(const char *card_path);
|
||||
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;
|
||||
|
||||
std::vector<WaylandOutput> monitors;
|
||||
struct zxdg_output_manager_v1 *xdg_output_manager = nullptr;
|
||||
private:
|
||||
void set_monitor_outputs_from_xdg_output(struct wl_display *dpy);
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -4,8 +4,10 @@
|
||||
#include "../Hotplug.hpp"
|
||||
#include <unordered_map>
|
||||
#include <thread>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <poll.h>
|
||||
#include <linux/joystick.h>
|
||||
#include <linux/input.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int max_js_poll_fd = 16;
|
||||
@@ -30,8 +32,10 @@ namespace gsr {
|
||||
bool bind_action(const std::string &id, GlobalHotkeyCallback callback) override;
|
||||
void poll_events() override;
|
||||
private:
|
||||
void close_fds();
|
||||
void read_events();
|
||||
void process_js_event(int fd, js_event &event);
|
||||
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);
|
||||
@@ -45,6 +49,11 @@ namespace gsr {
|
||||
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;
|
||||
@@ -56,8 +65,6 @@ namespace gsr {
|
||||
bool down_pressed = false;
|
||||
bool left_pressed = false;
|
||||
bool right_pressed = false;
|
||||
bool l3_button_pressed = false;
|
||||
bool r3_button_pressed = false;
|
||||
|
||||
bool save_replay = false;
|
||||
bool save_1_min_replay = false;
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace gsr {
|
||||
public:
|
||||
enum class GrabType {
|
||||
ALL,
|
||||
VIRTUAL
|
||||
VIRTUAL,
|
||||
NO_GRAB
|
||||
};
|
||||
|
||||
GlobalHotkeysLinux(GrabType grab_type);
|
||||
@@ -21,6 +22,8 @@ namespace gsr {
|
||||
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:
|
||||
|
||||
@@ -25,11 +25,28 @@ namespace gsr {
|
||||
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;
|
||||
@@ -51,6 +68,7 @@ namespace gsr {
|
||||
bool focused = false;
|
||||
bool portal = false;
|
||||
std::vector<GsrMonitor> monitors;
|
||||
std::vector<GsrCamera> cameras;
|
||||
};
|
||||
|
||||
enum class DisplayServer {
|
||||
@@ -103,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();
|
||||
}
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,8 @@
|
||||
#include "GlobalHotkeys/GlobalHotkeysJoystick.hpp"
|
||||
#include "AudioPlayer.hpp"
|
||||
#include "RegionSelector.hpp"
|
||||
#include "WindowSelector.hpp"
|
||||
#include "ClipboardFile.hpp"
|
||||
#include "LedIndicator.hpp"
|
||||
#include "CursorTracker/CursorTracker.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
@@ -22,6 +23,8 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
class DropdownButton;
|
||||
class GlobalHotkeys;
|
||||
@@ -38,7 +41,30 @@ namespace gsr {
|
||||
RECORD,
|
||||
REPLAY,
|
||||
STREAM,
|
||||
SCREENSHOT
|
||||
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 {
|
||||
@@ -53,9 +79,9 @@ namespace gsr {
|
||||
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();
|
||||
@@ -64,7 +90,8 @@ namespace gsr {
|
||||
void save_replay_10_min();
|
||||
void take_screenshot();
|
||||
void take_screenshot_region();
|
||||
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);
|
||||
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();
|
||||
@@ -74,11 +101,22 @@ namespace gsr {
|
||||
|
||||
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 create_frontpage_ui_components();
|
||||
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);
|
||||
@@ -87,8 +125,9 @@ namespace gsr {
|
||||
|
||||
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 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);
|
||||
@@ -100,7 +139,7 @@ namespace gsr {
|
||||
void update_power_supply_status();
|
||||
void update_system_startup_status();
|
||||
|
||||
void on_stop_recording(int exit_code, const std::string &video_filepath);
|
||||
void on_stop_recording(int exit_code, std::string &video_filepath);
|
||||
|
||||
void update_ui_recording_paused();
|
||||
void update_ui_recording_unpaused();
|
||||
@@ -118,12 +157,15 @@ namespace gsr {
|
||||
void on_press_save_replay();
|
||||
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);
|
||||
void on_press_start_record(bool finished_selection);
|
||||
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, bool force_region_capture);
|
||||
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();
|
||||
@@ -140,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;
|
||||
|
||||
@@ -176,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;
|
||||
@@ -204,15 +251,22 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<GlobalHotkeys> global_hotkeys = nullptr;
|
||||
std::unique_ptr<GlobalHotkeysJoystick> global_hotkeys_js = nullptr;
|
||||
Display *x11_mapping_display = 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;
|
||||
|
||||
@@ -220,14 +274,22 @@ namespace gsr {
|
||||
bool start_region_capture = false;
|
||||
std::function<void()> on_region_selected;
|
||||
|
||||
WindowSelector window_selector;
|
||||
bool start_window_capture = false;
|
||||
std::function<void()> on_window_selected;
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -13,13 +13,17 @@ namespace gsr {
|
||||
|
||||
// Arguments ending with NULL
|
||||
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, 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, 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
|
||||
// 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);
|
||||
}
|
||||
@@ -7,20 +7,34 @@
|
||||
|
||||
#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(mgl::Color border_color);
|
||||
bool start(SelectionType selection_type, mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
@@ -28,7 +42,11 @@ namespace gsr {
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Region get_selection() const;
|
||||
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);
|
||||
@@ -48,6 +66,10 @@ namespace gsr {
|
||||
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;
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ 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;
|
||||
@@ -44,6 +45,11 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
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__)
|
||||
@@ -36,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,33 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
|
||||
namespace gsr {
|
||||
class WindowSelector {
|
||||
public:
|
||||
WindowSelector();
|
||||
WindowSelector(const WindowSelector&) = delete;
|
||||
WindowSelector& operator=(const WindowSelector&) = delete;
|
||||
~WindowSelector();
|
||||
|
||||
bool start(mgl::Color border_color);
|
||||
void stop();
|
||||
bool is_started() const;
|
||||
|
||||
bool failed() const;
|
||||
bool poll_events();
|
||||
bool take_selection();
|
||||
bool take_canceled();
|
||||
Window get_selection() const;
|
||||
private:
|
||||
Display *dpy = nullptr;
|
||||
Cursor crosshair_cursor = None;
|
||||
Colormap border_window_colormap = None;
|
||||
Window border_window = None;
|
||||
Window selected_window = None;
|
||||
bool selected = false;
|
||||
bool canceled = false;
|
||||
};
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
#include <optional>
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
struct wl_display;
|
||||
|
||||
namespace gsr {
|
||||
enum class WindowCaptureType {
|
||||
FOCUSED,
|
||||
@@ -13,28 +15,39 @@ namespace gsr {
|
||||
};
|
||||
|
||||
struct Monitor {
|
||||
mgl::vec2i position;
|
||||
mgl::vec2i size;
|
||||
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);
|
||||
std::string get_focused_window_name(Display *dpy, WindowCaptureType window_capture_type);
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace gsr {
|
||||
class RadioButton;
|
||||
class Button;
|
||||
class List;
|
||||
class ComboBox;
|
||||
class CustomRendererWidget;
|
||||
|
||||
enum ConfigureHotkeyType {
|
||||
@@ -26,9 +27,12 @@ namespace gsr {
|
||||
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
|
||||
};
|
||||
|
||||
@@ -60,16 +64,22 @@ namespace gsr {
|
||||
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();
|
||||
@@ -96,10 +106,15 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "Widget.hpp"
|
||||
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <functional>
|
||||
|
||||
namespace gsr {
|
||||
class Image : public Widget {
|
||||
@@ -21,6 +22,8 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
@@ -16,13 +16,15 @@ namespace gsr {
|
||||
|
||||
class ScreenshotSettingsPage : public StaticPage {
|
||||
public:
|
||||
ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack);
|
||||
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();
|
||||
@@ -42,8 +44,15 @@ namespace gsr {
|
||||
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_notifications_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();
|
||||
|
||||
@@ -69,8 +78,14 @@ namespace gsr {
|
||||
ComboBox *image_format_box_ptr = nullptr;
|
||||
Button *save_directory_button_ptr = nullptr;
|
||||
CheckBox *save_screenshot_in_game_folder_checkbox_ptr = nullptr;
|
||||
CheckBox *show_screenshot_saved_notification_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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,14 @@ namespace gsr {
|
||||
INPUT
|
||||
};
|
||||
|
||||
enum class WebcamBoxResizeCorner {
|
||||
NONE,
|
||||
//TOP_LEFT,
|
||||
//TOP_RIGHT,
|
||||
//BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT
|
||||
};
|
||||
|
||||
class SettingsPage : public StaticPage {
|
||||
public:
|
||||
enum class Type {
|
||||
@@ -33,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;
|
||||
|
||||
@@ -58,10 +66,19 @@ namespace gsr {
|
||||
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_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);
|
||||
@@ -71,6 +88,7 @@ namespace gsr {
|
||||
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_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();
|
||||
@@ -112,15 +130,21 @@ namespace gsr {
|
||||
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);
|
||||
@@ -134,7 +158,9 @@ namespace gsr {
|
||||
void save_record();
|
||||
void save_stream();
|
||||
|
||||
void view_changed(bool advanced_view, Subsection *notifications_subsection_ptr);
|
||||
void view_changed(bool advanced_view);
|
||||
|
||||
RecordOptions& get_current_record_options();
|
||||
private:
|
||||
Type type;
|
||||
Config &config;
|
||||
@@ -173,32 +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_video_paused_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;
|
||||
};
|
||||
}
|
||||
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,16 +2,22 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <mglpp/graphics/Color.hpp>
|
||||
#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);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <mglpp/system/vec.hpp>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace mgl {
|
||||
class Event;
|
||||
@@ -44,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:
|
||||
@@ -59,8 +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);
|
||||
}
|
||||
51
meson.build
@@ -1,4 +1,6 @@
|
||||
project('gsr-ui', ['c', 'cpp'], version : '1.6.9', 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'])
|
||||
@@ -32,22 +34,27 @@ src = [
|
||||
'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/WindowSelector.cpp',
|
||||
'src/Config.cpp',
|
||||
'src/GsrInfo.cpp',
|
||||
'src/Process.cpp',
|
||||
'src/Overlay.cpp',
|
||||
'src/AudioPlayer.cpp',
|
||||
'src/Hotplug.cpp',
|
||||
'src/ClipboardFile.cpp',
|
||||
'src/LedIndicator.cpp',
|
||||
'src/Rpc.cpp',
|
||||
'src/Translation.cpp',
|
||||
'src/main.cpp',
|
||||
]
|
||||
|
||||
@@ -60,9 +67,12 @@ 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.7.3"', 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(),
|
||||
@@ -90,6 +100,7 @@ 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',
|
||||
@@ -104,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')
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "gsr-ui"
|
||||
type = "executable"
|
||||
version = "1.6.9"
|
||||
version = "1.10.9"
|
||||
platforms = ["posix"]
|
||||
|
||||
[lang.cpp]
|
||||
@@ -10,6 +10,9 @@ version = "c++17"
|
||||
[config]
|
||||
ignore_dirs = ["build", "tools"]
|
||||
|
||||
[define]
|
||||
_FILE_OFFSET_BITS = "64"
|
||||
|
||||
[dependencies]
|
||||
xcomposite = ">=0"
|
||||
xfixes = ">=0"
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -145,6 +145,8 @@ namespace gsr {
|
||||
|
||||
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};
|
||||
@@ -153,6 +155,7 @@ namespace gsr {
|
||||
|
||||
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};
|
||||
}
|
||||
@@ -170,9 +173,12 @@ namespace gsr {
|
||||
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},
|
||||
@@ -196,13 +202,27 @@ 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_hotkey", &config.streaming_config.start_stop_hotkey},
|
||||
|
||||
@@ -227,14 +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.show_video_paused_notifications", &config.record_config.show_video_paused_notifications},
|
||||
{"record.save_directory", &config.record_config.save_directory},
|
||||
{"record.container", &config.record_config.container},
|
||||
{"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},
|
||||
@@ -257,12 +289,22 @@ 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.restart_replay_on_save", &config.replay_config.restart_replay_on_save},
|
||||
{"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.save_directory", &config.replay_config.save_directory},
|
||||
{"replay.container", &config.replay_config.container},
|
||||
{"replay.time", &config.replay_config.replay_time},
|
||||
@@ -281,10 +323,15 @@ namespace gsr {
|
||||
{"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.show_screenshot_saved_notifications", &config.screenshot_config.show_screenshot_saved_notifications},
|
||||
{"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_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},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -482,4 +529,4 @@ namespace gsr {
|
||||
|
||||
fclose(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
#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 <wayland-client.h>
|
||||
#include "xdg-output-unstable-v1-client-protocol.h"
|
||||
|
||||
#include <mglpp/system/Rect.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const int MAX_CONNECTORS = 32;
|
||||
static const uint32_t plane_property_all = 0xF;
|
||||
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_ID = 1 << 2,
|
||||
PLANE_PROPERTY_TYPE_CURSOR = 1 << 3,
|
||||
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 {
|
||||
@@ -30,10 +33,17 @@ namespace gsr {
|
||||
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_id) {
|
||||
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;
|
||||
@@ -55,6 +65,12 @@ namespace gsr {
|
||||
} 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;
|
||||
@@ -136,177 +152,14 @@ namespace gsr {
|
||||
}
|
||||
|
||||
// Name is the crtc name. TODO: verify if this works on all wayland compositors
|
||||
static const WaylandOutput* get_wayland_monitor_by_name(const std::vector<WaylandOutput> &monitors, const std::string &name) {
|
||||
for(const WaylandOutput &monitor : monitors) {
|
||||
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 WaylandOutput* get_wayland_monitor_by_output(CursorTrackerWayland &cursor_tracker_wayland, struct wl_output *output) {
|
||||
for(WaylandOutput &monitor : cursor_tracker_wayland.monitors) {
|
||||
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;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_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;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_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;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)data;
|
||||
WaylandOutput *monitor = get_wayland_monitor_by_output(*cursor_tracker_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;
|
||||
CursorTrackerWayland *cursor_tracker_wayland = (CursorTrackerWayland*)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);
|
||||
cursor_tracker_wayland->monitors.push_back(
|
||||
WaylandOutput{
|
||||
name,
|
||||
output,
|
||||
nullptr,
|
||||
mgl::vec2i{0, 0},
|
||||
mgl::vec2i{0, 0},
|
||||
0,
|
||||
""
|
||||
});
|
||||
wl_output_add_listener(output, &output_listener, cursor_tracker_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(cursor_tracker_wayland->xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(cursor_tracker_wayland->xdg_output_manager);
|
||||
cursor_tracker_wayland->xdg_output_manager = NULL;
|
||||
}
|
||||
cursor_tracker_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)data;
|
||||
(void)xdg_output;
|
||||
(void)width;
|
||||
(void)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,
|
||||
};
|
||||
|
||||
/* 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) {
|
||||
@@ -368,8 +221,7 @@ namespace gsr {
|
||||
if(!properties)
|
||||
goto next_crtc;
|
||||
|
||||
if(!get_drm_property_by_name(drm_fd, properties, "VRR_ENABLED", &vrr_enabled))
|
||||
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)
|
||||
@@ -391,7 +243,7 @@ namespace gsr {
|
||||
drmModeFreeResources(resources);
|
||||
}
|
||||
|
||||
CursorTrackerWayland::CursorTrackerWayland(const char *card_path) {
|
||||
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);
|
||||
@@ -424,11 +276,17 @@ namespace gsr {
|
||||
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;
|
||||
@@ -436,7 +294,7 @@ namespace gsr {
|
||||
if(!plane->fb_id)
|
||||
goto next;
|
||||
|
||||
property_mask = plane_get_properties(drm_fd, planes->planes[i], &crtc_x, &crtc_y, &crtc_id);
|
||||
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;
|
||||
|
||||
@@ -444,7 +302,10 @@ namespace gsr {
|
||||
if(!connector)
|
||||
goto next;
|
||||
|
||||
if(crtc_x >= 0 && crtc_x <= connector->size.x && crtc_y >= 0 && crtc_y <= connector->size.y) {
|
||||
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;
|
||||
@@ -465,74 +326,19 @@ namespace gsr {
|
||||
drmModeFreePlaneResources(planes);
|
||||
}
|
||||
|
||||
void CursorTrackerWayland::set_monitor_outputs_from_xdg_output(struct wl_display *dpy) {
|
||||
if(!xdg_output_manager) {
|
||||
fprintf(stderr, "Warning: CursorTrackerWayland::set_monitor_outputs_from_xdg_output: zxdg_output_manager not found. Registered monitor positions might be incorrect\n");
|
||||
return;
|
||||
}
|
||||
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
monitor.xdg_output = zxdg_output_manager_v1_get_xdg_output(xdg_output_manager, monitor.output);
|
||||
zxdg_output_v1_add_listener(monitor.xdg_output, &xdg_output_listener, &monitor);
|
||||
}
|
||||
|
||||
// Fetch xdg_output
|
||||
wl_display_roundtrip(dpy);
|
||||
}
|
||||
|
||||
std::optional<CursorInfo> CursorTrackerWayland::get_latest_cursor_info() {
|
||||
if(drm_fd <= 0 || latest_crtc_id == -1)
|
||||
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;
|
||||
|
||||
struct wl_display *dpy = wl_display_connect(nullptr);
|
||||
if(!dpy) {
|
||||
fprintf(stderr, "Error: CursorTrackerWayland::get_latest_cursor_info: failed to connect to the wayland server\n");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
monitors.clear();
|
||||
struct wl_registry *registry = wl_display_get_registry(dpy);
|
||||
wl_registry_add_listener(registry, ®istry_listener, this);
|
||||
|
||||
// Fetch globals
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
// Fetch wl_output
|
||||
wl_display_roundtrip(dpy);
|
||||
|
||||
set_monitor_outputs_from_xdg_output(dpy);
|
||||
|
||||
mgl::vec2i cursor_position = latest_cursor_position;
|
||||
const WaylandOutput *wayland_monitor = get_wayland_monitor_by_name(monitors, monitor_name);
|
||||
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;
|
||||
|
||||
cursor_position = wayland_monitor->pos + latest_cursor_position;
|
||||
for(WaylandOutput &monitor : monitors) {
|
||||
if(monitor.output) {
|
||||
wl_output_destroy(monitor.output);
|
||||
monitor.output = nullptr;
|
||||
}
|
||||
|
||||
if(monitor.xdg_output) {
|
||||
zxdg_output_v1_destroy(monitor.xdg_output);
|
||||
monitor.xdg_output = nullptr;
|
||||
}
|
||||
}
|
||||
monitors.clear();
|
||||
|
||||
if(xdg_output_manager) {
|
||||
zxdg_output_manager_v1_destroy(xdg_output_manager);
|
||||
xdg_output_manager = nullptr;
|
||||
}
|
||||
|
||||
wl_registry_destroy(registry);
|
||||
wl_display_disconnect(dpy);
|
||||
|
||||
return CursorInfo{ cursor_position, std::move(monitor_name) };
|
||||
return CursorInfo{ wayland_monitor->position + latest_cursor_position, std::move(monitor_name) };
|
||||
}
|
||||
}
|
||||
@@ -3,92 +3,48 @@
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/eventfd.h>
|
||||
|
||||
namespace gsr {
|
||||
static constexpr int button_pressed = 1;
|
||||
static constexpr int cross_button = 0;
|
||||
static constexpr int triangle_button = 2;
|
||||
static constexpr int options_button = 9;
|
||||
static constexpr int playstation_button = 10;
|
||||
static constexpr int l3_button = 11;
|
||||
static constexpr int r3_button = 12;
|
||||
static constexpr int axis_up_down = 7;
|
||||
static constexpr int axis_left_right = 6;
|
||||
|
||||
struct DeviceId {
|
||||
uint16_t vendor;
|
||||
uint16_t product;
|
||||
};
|
||||
|
||||
static bool read_file_hex_number(const char *path, unsigned int *value) {
|
||||
*value = 0;
|
||||
FILE *f = fopen(path, "rb");
|
||||
if(!f)
|
||||
return false;
|
||||
|
||||
fscanf(f, "%x", value);
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
static DeviceId joystick_get_device_id(const char *path) {
|
||||
DeviceId device_id;
|
||||
device_id.vendor = 0;
|
||||
device_id.product = 0;
|
||||
|
||||
const char *js_path_id = nullptr;
|
||||
const int len = strlen(path);
|
||||
for(int i = len - 1; i >= 0; --i) {
|
||||
if(path[i] == '/') {
|
||||
js_path_id = path + i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(!js_path_id)
|
||||
return device_id;
|
||||
|
||||
unsigned int vendor = 0;
|
||||
unsigned int product = 0;
|
||||
char path_buf[1024];
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/vendor", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &vendor))
|
||||
return device_id;
|
||||
|
||||
snprintf(path_buf, sizeof(path_buf), "/sys/class/input/%s/device/id/product", js_path_id);
|
||||
if(!read_file_hex_number(path_buf, &product))
|
||||
return device_id;
|
||||
|
||||
device_id.vendor = vendor;
|
||||
device_id.product = product;
|
||||
return device_id;
|
||||
}
|
||||
|
||||
static bool is_ps4_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x09CC || device_id.product == 0x0BA0 || device_id.product == 0x05C4);
|
||||
}
|
||||
|
||||
static bool is_ps5_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x054C && (device_id.product == 0x0DF2 || device_id.product == 0x0CE6);
|
||||
}
|
||||
|
||||
static bool is_stadia_controller(DeviceId device_id) {
|
||||
return device_id.vendor == 0x18D1 && (device_id.product == 0x9400);
|
||||
}
|
||||
|
||||
// Returns -1 on error
|
||||
static int get_js_dev_input_id_from_filepath(const char *dev_input_filepath) {
|
||||
if(strncmp(dev_input_filepath, "/dev/input/js", 13) != 0)
|
||||
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 + 13, "%d", &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;
|
||||
@@ -98,11 +54,22 @@ namespace gsr {
|
||||
if(read_thread.joinable())
|
||||
read_thread.join();
|
||||
|
||||
if(event_fd > 0)
|
||||
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) {
|
||||
close(poll_fd[i].fd);
|
||||
if(poll_fd[i].fd > 0)
|
||||
close(poll_fd[i].fd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,16 +107,10 @@ namespace gsr {
|
||||
++num_poll_fd;
|
||||
}
|
||||
|
||||
char dev_input_path[128];
|
||||
for(int i = 0; i < 8; ++i) {
|
||||
snprintf(dev_input_path, sizeof(dev_input_path), "/dev/input/js%d", i);
|
||||
add_device(dev_input_path, false);
|
||||
}
|
||||
|
||||
if(num_poll_fd == 0)
|
||||
fprintf(stderr, "Info: no joysticks found, assuming they might be connected later\n");
|
||||
add_all_joystick_devices();
|
||||
|
||||
read_thread = std::thread(&GlobalHotkeysJoystick::read_events, this);
|
||||
close_fd_thread = std::thread(&GlobalHotkeysJoystick::close_fds, this);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -213,47 +174,71 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
// 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() {
|
||||
js_event event;
|
||||
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))
|
||||
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) {
|
||||
char dev_input_filepath[1024];
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/%s", devname);
|
||||
switch(hotplug_action) {
|
||||
case HotplugAction::ADD: {
|
||||
// Cant open the /dev/input device immediately or it fails.
|
||||
// TODO: Remove this hack when a better solution is found.
|
||||
usleep(50 * 1000);
|
||||
add_device(dev_input_filepath);
|
||||
add_device(devname, false);
|
||||
break;
|
||||
}
|
||||
case HotplugAction::REMOVE: {
|
||||
if(remove_device(dev_input_filepath))
|
||||
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_js_event(poll_fd[i].fd, event);
|
||||
process_input_event(poll_fd[i].fd, event);
|
||||
}
|
||||
|
||||
poll_fd[i].revents = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,54 +246,44 @@ namespace gsr {
|
||||
;
|
||||
}
|
||||
|
||||
void GlobalHotkeysJoystick::process_js_event(int fd, js_event &event) {
|
||||
void GlobalHotkeysJoystick::process_input_event(int fd, input_event &event) {
|
||||
if(read(fd, &event, sizeof(event)) != sizeof(event))
|
||||
return;
|
||||
|
||||
if((event.type & JS_EVENT_BUTTON) == JS_EVENT_BUTTON) {
|
||||
switch(event.number) {
|
||||
case playstation_button: {
|
||||
// Workaround weird steam input (in-game) behavior where steam triggers playstation button + options when pressing both l3 and r3 at the same time
|
||||
playstation_button_pressed = (event.value == button_pressed) && !l3_button_pressed && !r3_button_pressed;
|
||||
if(event.type == EV_KEY) {
|
||||
switch(event.code) {
|
||||
case BTN_MODE: {
|
||||
playstation_button_pressed = (event.value == button_pressed);
|
||||
break;
|
||||
}
|
||||
case options_button: {
|
||||
case BTN_START: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
toggle_show = true;
|
||||
break;
|
||||
}
|
||||
case cross_button: {
|
||||
case BTN_SOUTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_1_min_replay = true;
|
||||
break;
|
||||
}
|
||||
case triangle_button: {
|
||||
case BTN_NORTH: {
|
||||
if(playstation_button_pressed && event.value == button_pressed)
|
||||
save_10_min_replay = true;
|
||||
break;
|
||||
}
|
||||
case l3_button: {
|
||||
l3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
case r3_button: {
|
||||
r3_button_pressed = event.value == button_pressed;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if((event.type & JS_EVENT_AXIS) == JS_EVENT_AXIS && playstation_button_pressed) {
|
||||
const int trigger_threshold = 16383;
|
||||
} 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.number == axis_up_down) {
|
||||
up_pressed = event.value <= -trigger_threshold;
|
||||
down_pressed = event.value >= trigger_threshold;
|
||||
} else if(event.number == axis_left_right) {
|
||||
left_pressed = event.value <= -trigger_threshold;
|
||||
right_pressed = event.value >= trigger_threshold;
|
||||
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)
|
||||
@@ -322,13 +297,36 @@ namespace gsr {
|
||||
}
|
||||
}
|
||||
|
||||
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_js_dev_input_id_from_filepath(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;
|
||||
|
||||
@@ -339,6 +337,15 @@ namespace gsr {
|
||||
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,
|
||||
@@ -357,7 +364,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
bool GlobalHotkeysJoystick::remove_device(const char *dev_input_filepath) {
|
||||
const int dev_input_id = get_js_dev_input_id_from_filepath(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;
|
||||
|
||||
@@ -373,7 +380,14 @@ namespace gsr {
|
||||
if(index < 0 || index >= num_poll_fd)
|
||||
return false;
|
||||
|
||||
close(poll_fd[index].fd);
|
||||
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];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unistd.h>
|
||||
extern "C" {
|
||||
#include <mgl/mgl.h>
|
||||
}
|
||||
@@ -19,6 +19,7 @@ namespace gsr {
|
||||
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";
|
||||
}
|
||||
@@ -192,7 +193,7 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(hotkey.key == 0) {
|
||||
if(hotkey.key == 0 || hotkey.key == XK_VoidSymbol) {
|
||||
//fprintf(stderr, "Error: GlobalHotkeysLinux::bind_key_press: hotkey requires a key\n");
|
||||
return false;
|
||||
}
|
||||
@@ -270,6 +271,8 @@ namespace gsr {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
src/GsrInfo.cpp
@@ -288,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);
|
||||
@@ -304,16 +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 == "region")
|
||||
} else if(line == "region") {
|
||||
capture_options.region = true;
|
||||
else if(line == "focused")
|
||||
} 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()));
|
||||
@@ -348,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +59,9 @@ namespace gsr {
|
||||
|
||||
/* TODO: This assumes SUBSYSTEM= is output before DEVNAME=, is that always true? */
|
||||
void Hotplug::parse_netlink_data(const char *line, const HotplugEventCallback &callback) {
|
||||
const char *at_symbol = strchr(line, '@');
|
||||
if(at_symbol) {
|
||||
event_is_add = strncmp(line, "add@", 4) == 0;
|
||||
event_is_remove = strncmp(line, "remove@", 7) == 0;
|
||||
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)
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
1275
src/Overlay.cpp
@@ -73,6 +73,28 @@ namespace gsr {
|
||||
return true;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -164,11 +186,9 @@ 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, debug);
|
||||
} else {
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
|
||||
#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;
|
||||
@@ -65,7 +68,11 @@ namespace gsr {
|
||||
(unsigned short)max_int(0, width - border_size*2), (unsigned short)max_int(0, border_size)
|
||||
}, // Bottom
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -95,7 +102,8 @@ namespace gsr {
|
||||
height = abs(height);
|
||||
}
|
||||
|
||||
XDrawRectangle(dpy, window, gc, x, y, width, 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) {
|
||||
@@ -114,6 +122,13 @@ namespace gsr {
|
||||
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) {
|
||||
@@ -136,10 +151,7 @@ namespace gsr {
|
||||
height = focused_monitor->size.y;
|
||||
}
|
||||
|
||||
if(is_wayland)
|
||||
draw_rectangle(dpy, window, region_gc, x, y, width, height);
|
||||
else
|
||||
set_region_rectangle(dpy, window, x, y, width, height, region_border_size);
|
||||
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) {
|
||||
@@ -165,6 +177,98 @@ namespace gsr {
|
||||
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() {
|
||||
|
||||
}
|
||||
@@ -173,7 +277,7 @@ namespace gsr {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool RegionSelector::start(mgl::Color border_color) {
|
||||
bool RegionSelector::start(SelectionType selection_type, mgl::Color border_color) {
|
||||
if(dpy)
|
||||
return false;
|
||||
|
||||
@@ -221,6 +325,9 @@ namespace gsr {
|
||||
}
|
||||
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)
|
||||
@@ -268,11 +375,20 @@ namespace gsr {
|
||||
hide_window_from_taskbar(dpy, cursor_window);
|
||||
}
|
||||
|
||||
draw_rectangle_around_selected_monitor(dpy, region_window, region_gc, region_border_size, is_wayland, monitors, cursor_pos);
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -309,9 +425,14 @@ namespace gsr {
|
||||
region_window = 0;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XSync(dpy, False);
|
||||
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
selecting_region = false;
|
||||
monitors.clear();
|
||||
windows.clear();
|
||||
}
|
||||
|
||||
bool RegionSelector::is_started() const {
|
||||
@@ -378,8 +499,24 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
Region RegionSelector::get_selection() const {
|
||||
return region;
|
||||
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) {
|
||||
@@ -387,8 +524,10 @@ namespace gsr {
|
||||
if(device_event->detail != Button1)
|
||||
return;
|
||||
|
||||
region.pos = { (int)device_event->root_x, (int)device_event->root_y };
|
||||
selecting_region = true;
|
||||
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) {
|
||||
@@ -396,8 +535,23 @@ namespace gsr {
|
||||
if(device_event->detail != Button1)
|
||||
return;
|
||||
|
||||
if(!selecting_region)
|
||||
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);
|
||||
@@ -407,7 +561,11 @@ namespace gsr {
|
||||
}
|
||||
selecting_region = false;
|
||||
|
||||
cursor_pos = region.pos + region.size;
|
||||
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;
|
||||
@@ -431,19 +589,26 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
if(is_wayland)
|
||||
draw_rectangle(dpy, region_window, region_gc, region.pos.x, region.pos.y, region.size.x, region.size.y);
|
||||
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
|
||||
set_region_rectangle(dpy, region_window, region.pos.x, region.pos.y, region.size.x, region.size.y, region_border_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);
|
||||
}
|
||||
|
||||
206
src/Rpc.cpp
@@ -5,11 +5,12 @@
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <sys/stat.h>
|
||||
#include <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,79 +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())
|
||||
unlink(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;
|
||||
unlink(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());
|
||||
fifo_filepath.clear();
|
||||
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())) {
|
||||
unlink(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;
|
||||
}
|
||||
@@ -104,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;
|
||||
}
|
||||
|
||||
@@ -48,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;
|
||||
}
|
||||
|
||||
@@ -120,6 +123,21 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
134
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()) {
|
||||
@@ -238,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,229 +0,0 @@
|
||||
#include "../include/WindowSelector.hpp"
|
||||
#include "../include/WindowUtils.hpp"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <X11/extensions/shape.h>
|
||||
#include <X11/cursorfont.h>
|
||||
#include <X11/keysym.h>
|
||||
|
||||
namespace gsr {
|
||||
static const int rectangle_border_size = 2;
|
||||
|
||||
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
|
||||
};
|
||||
XShapeCombineRectangles(dpy, window, ShapeBounding, 0, 0, rectangles, 4, ShapeSet, Unsorted);
|
||||
XFlush(dpy);
|
||||
}
|
||||
|
||||
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 Window get_cursor_window(Display *dpy) {
|
||||
Window root_window = None;
|
||||
Window window = None;
|
||||
int dummy_i;
|
||||
unsigned int dummy_u;
|
||||
mgl::vec2i root_pos;
|
||||
XQueryPointer(dpy, DefaultRootWindow(dpy), &root_window, &window, &root_pos.x, &root_pos.y, &dummy_i, &dummy_i, &dummy_u);
|
||||
return window;
|
||||
}
|
||||
|
||||
static void get_window_geometry(Display *dpy, Window window, mgl::vec2i &pos, mgl::vec2i &size) {
|
||||
Window root_window;
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
unsigned int w = 0;
|
||||
unsigned int h = 0;
|
||||
unsigned int dummy_border, dummy_depth;
|
||||
XGetGeometry(dpy, window, &root_window, &x, &y, &w, &h, &dummy_border, &dummy_depth);
|
||||
pos.x = x;
|
||||
pos.y = y;
|
||||
size.x = w;
|
||||
size.y = h;
|
||||
}
|
||||
|
||||
WindowSelector::WindowSelector() {
|
||||
|
||||
}
|
||||
|
||||
WindowSelector::~WindowSelector() {
|
||||
stop();
|
||||
}
|
||||
|
||||
bool WindowSelector::start(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: WindowSelector::start: failed to connect to the X11 server\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
const Window cursor_window = get_cursor_window(dpy);
|
||||
mgl::vec2i cursor_window_pos, cursor_window_size;
|
||||
get_window_geometry(dpy, cursor_window, cursor_window_pos, cursor_window_size);
|
||||
|
||||
XVisualInfo vinfo;
|
||||
memset(&vinfo, 0, sizeof(vinfo));
|
||||
XMatchVisualInfo(dpy, DefaultScreen(dpy), 32, TrueColor, &vinfo);
|
||||
border_window_colormap = XCreateColormap(dpy, DefaultRootWindow(dpy), vinfo.visual, AllocNone);
|
||||
|
||||
XSetWindowAttributes window_attr;
|
||||
window_attr.background_pixel = border_color_x11;
|
||||
window_attr.border_pixel = 0;
|
||||
window_attr.override_redirect = true;
|
||||
window_attr.event_mask = StructureNotifyMask | PointerMotionMask | ButtonPressMask | ButtonReleaseMask;
|
||||
window_attr.colormap = border_window_colormap;
|
||||
|
||||
Screen *screen = XDefaultScreenOfDisplay(dpy);
|
||||
border_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(!border_window) {
|
||||
fprintf(stderr, "Error: WindowSelector::start: failed to create region window\n");
|
||||
stop();
|
||||
return false;
|
||||
}
|
||||
set_window_size_not_resizable(dpy, border_window, XWidthOfScreen(screen), XHeightOfScreen(screen));
|
||||
if(cursor_window && cursor_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, cursor_window_pos.x, cursor_window_pos.y, cursor_window_size.x, cursor_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
make_window_click_through(dpy, border_window);
|
||||
XMapWindow(dpy, border_window);
|
||||
|
||||
crosshair_cursor = XCreateFontCursor(dpy, XC_crosshair);
|
||||
XGrabPointer(dpy, DefaultRootWindow(dpy), True, PointerMotionMask | ButtonPressMask | ButtonReleaseMask | ButtonMotionMask, GrabModeAsync, GrabModeAsync, None, crosshair_cursor, CurrentTime);
|
||||
XGrabKeyboard(dpy, DefaultRootWindow(dpy), True, GrabModeAsync, GrabModeAsync, CurrentTime);
|
||||
XFlush(dpy);
|
||||
|
||||
selected = false;
|
||||
canceled = false;
|
||||
selected_window = None;
|
||||
return true;
|
||||
}
|
||||
|
||||
void WindowSelector::stop() {
|
||||
if(!dpy)
|
||||
return;
|
||||
|
||||
XUngrabPointer(dpy, CurrentTime);
|
||||
XUngrabKeyboard(dpy, CurrentTime);
|
||||
|
||||
if(border_window_colormap) {
|
||||
XFreeColormap(dpy, border_window_colormap);
|
||||
border_window_colormap = 0;
|
||||
}
|
||||
|
||||
if(border_window) {
|
||||
XDestroyWindow(dpy, border_window);
|
||||
border_window = 0;
|
||||
}
|
||||
|
||||
if(crosshair_cursor) {
|
||||
XFreeCursor(dpy, crosshair_cursor);
|
||||
crosshair_cursor = None;
|
||||
}
|
||||
|
||||
XFlush(dpy);
|
||||
XCloseDisplay(dpy);
|
||||
dpy = nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::is_started() const {
|
||||
return dpy != nullptr;
|
||||
}
|
||||
|
||||
bool WindowSelector::failed() const {
|
||||
return !dpy;
|
||||
}
|
||||
|
||||
bool WindowSelector::poll_events() {
|
||||
if(!dpy || selected)
|
||||
return false;
|
||||
|
||||
XEvent xev;
|
||||
while(XPending(dpy)) {
|
||||
XNextEvent(dpy, &xev);
|
||||
|
||||
if(xev.type == MotionNotify) {
|
||||
const Window motion_window = xev.xmotion.subwindow;
|
||||
mgl::vec2i motion_window_pos, motion_window_size;
|
||||
get_window_geometry(dpy, motion_window, motion_window_pos, motion_window_size);
|
||||
if(motion_window && motion_window != DefaultRootWindow(dpy))
|
||||
set_region_rectangle(dpy, border_window, motion_window_pos.x, motion_window_pos.y, motion_window_size.x, motion_window_size.y, rectangle_border_size);
|
||||
else
|
||||
set_region_rectangle(dpy, border_window, 0, 0, 0, 0, 0);
|
||||
XFlush(dpy);
|
||||
} else if(xev.type == ButtonRelease && xev.xbutton.button == Button1) {
|
||||
selected_window = xev.xbutton.subwindow;
|
||||
const Window clicked_window_real = window_get_target_window_child(dpy, selected_window);
|
||||
if(clicked_window_real)
|
||||
selected_window = clicked_window_real;
|
||||
selected = true;
|
||||
|
||||
stop();
|
||||
break;
|
||||
} else if(xev.type == KeyRelease && XKeycodeToKeysym(dpy, xev.xkey.keycode, 0) == XK_Escape) {
|
||||
canceled = true;
|
||||
selected = false;
|
||||
stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_selection() {
|
||||
const bool result = selected;
|
||||
selected = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
bool WindowSelector::take_canceled() {
|
||||
const bool result = canceled;
|
||||
canceled = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
Window WindowSelector::get_selection() const {
|
||||
return selected_window;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,10 @@
|
||||
#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" {
|
||||
@@ -23,7 +27,210 @@ extern "C" {
|
||||
#define MAX_PROPERTY_VALUE_LEN 4096
|
||||
|
||||
namespace gsr {
|
||||
static unsigned char* window_get_property(Display *dpy, Window window, Atom property_type, const char *property_name, unsigned int *property_size) {
|
||||
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;
|
||||
@@ -63,6 +270,30 @@ namespace gsr {
|
||||
return window_has_atom(dpy, window, net_wm_state_atom) || window_has_atom(dpy, window, wm_state_atom);
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -78,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) {
|
||||
@@ -121,7 +352,7 @@ namespace gsr {
|
||||
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;
|
||||
|
||||
@@ -144,8 +375,13 @@ 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;
|
||||
}
|
||||
|
||||
get_cursor_position(dpy, &focused_window);
|
||||
@@ -213,9 +449,9 @@ namespace gsr {
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -516,6 +752,47 @@ namespace gsr {
|
||||
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)
|
||||
@@ -643,6 +920,36 @@ namespace gsr {
|
||||
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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace gsr {
|
||||
for(size_t i = 0; i < items.size(); ++i) {
|
||||
Item &item = items[i];
|
||||
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
|
||||
if(mgl::FloatRect(item.position, item_size).contains(mouse_pos)) {
|
||||
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;
|
||||
@@ -83,17 +83,31 @@ 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;
|
||||
@@ -106,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;
|
||||
@@ -150,7 +180,7 @@ namespace gsr {
|
||||
Item &item = items[i];
|
||||
item_size.y = padding_top + item.text.get_bounds().size.y + padding_bottom;
|
||||
|
||||
if(!cursor_inside) {
|
||||
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());
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
@@ -17,11 +18,14 @@ namespace gsr {
|
||||
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
const mgl::Scissor prev_scissor = window.get_scissor();
|
||||
window.set_scissor({draw_pos.to_vec2i(), size.to_vec2i()});
|
||||
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);
|
||||
window.set_scissor(prev_scissor);
|
||||
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
mgl::vec2f CustomRendererWidget::get_size() {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
#include "../../include/gui/Entry.hpp"
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
#include <mglpp/system/Utf8.hpp>
|
||||
#include <optional>
|
||||
#include <string.h>
|
||||
|
||||
namespace gsr {
|
||||
static const float padding_top_scale = 0.004629f;
|
||||
@@ -16,8 +16,20 @@ namespace gsr {
|
||||
static const float border_scale = 0.0015f;
|
||||
static const float caret_width_scale = 0.001f;
|
||||
|
||||
Entry::Entry(mgl::Font *font, const char *text, float max_width) : text("", *font), max_width(max_width) {
|
||||
static void string_replace_all(std::string &str, char old_char, char new_char) {
|
||||
for(char &c : str) {
|
||||
if(c == old_char)
|
||||
c = new_char;
|
||||
}
|
||||
}
|
||||
|
||||
Entry::Entry(mgl::Font *font, const char *text, float max_width) :
|
||||
text(std::u32string(), *font),
|
||||
masked_text(std::u32string(), *font),
|
||||
max_width(max_width)
|
||||
{
|
||||
this->text.set_color(get_color_theme().text_color);
|
||||
this->masked_text.set_color(get_color_theme().text_color);
|
||||
set_text(text);
|
||||
}
|
||||
|
||||
@@ -25,25 +37,133 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return true;
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
selected = mgl::FloatRect(position + offset, get_size()).contains({ (float)event.mouse_button.x, (float)event.mouse_button.y });
|
||||
} else if(event.type == mgl::Event::KeyPressed && selected) {
|
||||
if(event.key.code == mgl::Keyboard::Backspace && !text.get_string().empty()) {
|
||||
std::string str = text.get_string();
|
||||
const size_t prev_index = mgl::utf8_get_start_of_codepoint((const unsigned char*)str.c_str(), str.size(), str.size());
|
||||
str.erase(prev_index, std::string::npos);
|
||||
set_text(std::move(str));
|
||||
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
|
||||
std::string clipboard_text = window.get_clipboard_string();
|
||||
std::string str = text.get_string();
|
||||
str += clipboard_text;
|
||||
set_text(std::move(str));
|
||||
const mgl::vec2f mouse_pos = { (float)event.mouse_button.x, (float)event.mouse_button.y };
|
||||
selected = mgl::FloatRect(position + offset, get_size()).contains(mouse_pos);
|
||||
if(selected) {
|
||||
selecting_text = true;
|
||||
|
||||
const auto caret_index_mouse = find_closest_caret_index_by_position(mouse_pos);
|
||||
caret.index = caret_index_mouse.index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
show_selection = true;
|
||||
} else {
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32) {
|
||||
std::string str = text.get_string();
|
||||
str.append(event.text.str, event.text.size);
|
||||
set_text(std::move(str));
|
||||
} else if(event.type == mgl::Event::MouseButtonReleased && event.mouse_button.button == mgl::Mouse::Left) {
|
||||
selecting_text = false;
|
||||
if(caret.index == selection_start_caret.index)
|
||||
show_selection = false;
|
||||
} else if(event.type == mgl::Event::MouseMoved && selected) {
|
||||
if(selecting_text) {
|
||||
const auto caret_index_mouse = find_closest_caret_index_by_position(mgl::vec2f(event.mouse_move.x, event.mouse_move.y));
|
||||
caret.index = caret_index_mouse.index;
|
||||
caret.offset_x = caret_index_mouse.pos.x - active_text.get_position().x;
|
||||
return false;
|
||||
}
|
||||
} else if(event.type == mgl::Event::KeyPressed && selected) {
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.index, selection_start_caret.index);
|
||||
selection_end_byte = std::max(caret.index, selection_start_caret.index);
|
||||
}
|
||||
|
||||
if(event.key.code == mgl::Keyboard::Backspace) {
|
||||
if(selection_start_byte == selection_end_byte && caret.index > 0)
|
||||
selection_start_byte -= 1;
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
|
||||
} else if(event.key.code == mgl::Keyboard::Delete) {
|
||||
if(selection_start_byte == selection_end_byte && caret.index < (int)active_text.get_string().size())
|
||||
selection_end_byte += 1;
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, std::u32string());
|
||||
} else if(event.key.code == mgl::Keyboard::C && event.key.control) {
|
||||
const size_t selection_num_bytes = selection_end_byte - selection_start_byte;
|
||||
if(selection_num_bytes > 0)
|
||||
window.set_clipboard(mgl::utf32_to_utf8(text.get_string().substr(selection_start_byte, selection_num_bytes)));
|
||||
} else if(event.key.code == mgl::Keyboard::V && event.key.control) {
|
||||
std::string clipboard_string = window.get_clipboard_string();
|
||||
string_replace_all(clipboard_string, '\n', ' ');
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32(clipboard_string));
|
||||
} else if(event.key.code == mgl::Keyboard::A && event.key.control) {
|
||||
selection_start_caret.index = 0;
|
||||
selection_start_caret.offset_x = 0.0f;
|
||||
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
|
||||
show_selection = true;
|
||||
} else if(event.key.code == mgl::Keyboard::Left) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
else
|
||||
move_caret_word(Direction::LEFT, event.key.control ? 999999 : 1);
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::Right) {
|
||||
if(!selecting_with_keyboard && show_selection)
|
||||
show_selection = false;
|
||||
else
|
||||
move_caret_word(Direction::RIGHT, event.key.control ? 999999 : 1);
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::Home) {
|
||||
caret.index = 0;
|
||||
caret.offset_x = 0.0f;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::End) {
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
|
||||
if(!selecting_with_keyboard) {
|
||||
selection_start_caret = caret;
|
||||
show_selection = false;
|
||||
}
|
||||
} else if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
|
||||
if(!show_selection)
|
||||
selection_start_caret = caret;
|
||||
selecting_with_keyboard = true;
|
||||
show_selection = true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if(event.type == mgl::Event::KeyReleased && selected) {
|
||||
if(event.key.code == mgl::Keyboard::LShift || event.key.code == mgl::Keyboard::RShift) {
|
||||
selecting_with_keyboard = false;
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if(event.type == mgl::Event::TextEntered && selected && event.text.codepoint >= 32 && event.text.codepoint != 127) {
|
||||
int selection_start_byte = caret.index;
|
||||
int selection_end_byte = caret.index;
|
||||
if(show_selection) {
|
||||
selection_start_byte = std::min(caret.index, selection_start_caret.index);
|
||||
selection_end_byte = std::max(caret.index, selection_start_caret.index);
|
||||
}
|
||||
|
||||
replace_text(selection_start_byte, selection_end_byte - selection_start_byte, mgl::utf8_to_utf32((const unsigned char*)event.text.str, event.text.size));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -54,26 +174,91 @@ namespace gsr {
|
||||
const mgl::vec2f draw_pos = position + offset;
|
||||
|
||||
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;
|
||||
|
||||
mgl::Rectangle background(get_size());
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
|
||||
background.set_size(get_size());
|
||||
background.set_position(draw_pos.floor());
|
||||
background.set_color(selected ? mgl::Color(0, 0, 0, 255) : mgl::Color(0, 0, 0, 120));
|
||||
window.draw(background);
|
||||
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
const mgl::vec2f caret_size = mgl::vec2f(caret_width, active_text.get_bounds().size.y).floor();
|
||||
|
||||
const float overflow_left = (caret.offset_x + padding_left) - (padding_left + text_overflow);
|
||||
if(overflow_left < 0.0f)
|
||||
text_overflow += overflow_left;
|
||||
|
||||
const float overflow_right = (caret.offset_x + padding_left) - (background.get_size().x - padding_right);
|
||||
if(overflow_right - text_overflow > 0.0f)
|
||||
text_overflow = overflow_right;
|
||||
|
||||
active_text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - active_text.get_bounds().size.y * 0.5f) - mgl::vec2f(text_overflow, 0.0f)).floor());
|
||||
|
||||
const auto text_bounds = active_text.get_bounds();
|
||||
const bool text_larger_than_background = text_bounds.size.x > (background.get_size().x - padding_left - padding_right);
|
||||
const float text_overflow_right = (text_bounds.position.x + text_bounds.size.x) - (background.get_position().x + background.get_size().x - padding_right);
|
||||
if(text_larger_than_background) {
|
||||
if(text_overflow_right < 0.0f) {
|
||||
text_overflow += text_overflow_right;
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow_right, 0.0f));
|
||||
}
|
||||
} else {
|
||||
active_text.set_position(active_text.get_position() + mgl::vec2f(-text_overflow, 0.0f));
|
||||
text_overflow = 0.0f;
|
||||
}
|
||||
|
||||
if(selected) {
|
||||
const int border_size = std::max(1.0f, border_scale * get_theme().window_height);
|
||||
draw_rectangle_outline(window, draw_pos.floor(), get_size().floor(), get_color_theme().tint_color, border_size);
|
||||
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
mgl::Rectangle caret({(float)caret_width, text.get_bounds().size.y});
|
||||
caret.set_position((draw_pos + mgl::vec2f(padding_left + caret_offset_x, padding_top)).floor());
|
||||
caret.set_color(mgl::Color(255, 255, 255));
|
||||
window.draw(caret);
|
||||
draw_caret(window, draw_pos, caret_size);
|
||||
}
|
||||
|
||||
text.set_position((draw_pos + mgl::vec2f(padding_left, get_size().y * 0.5f - text.get_bounds().size.y * 0.5f)).floor());
|
||||
window.draw(text);
|
||||
const mgl::Scissor parent_scissor = window.get_scissor();
|
||||
const mgl::Scissor scissor = scissor_get_sub_area(parent_scissor,
|
||||
mgl::Scissor{
|
||||
(background.get_position() + mgl::vec2f(padding_left, padding_top)).to_vec2i(),
|
||||
(background.get_size() - mgl::vec2f(padding_left + padding_right, padding_top + padding_bottom)).to_vec2i()
|
||||
});
|
||||
window.set_scissor(scissor);
|
||||
|
||||
window.draw(active_text);
|
||||
|
||||
if(show_selection)
|
||||
draw_caret_selection(window, draw_pos, caret_size);
|
||||
|
||||
window.set_scissor(parent_scissor);
|
||||
}
|
||||
|
||||
void Entry::draw_caret(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
|
||||
mgl::Rectangle caret_rect(caret_size);
|
||||
mgl::vec2f caret_draw_pos = draw_pos + mgl::vec2f(padding_left + caret.offset_x - text_overflow, padding_top);
|
||||
caret_rect.set_position(caret_draw_pos.floor());
|
||||
caret_rect.set_color(mgl::Color(255, 255, 255));
|
||||
window.draw(caret_rect);
|
||||
}
|
||||
|
||||
void Entry::draw_caret_selection(mgl::Window &window, mgl::vec2f draw_pos, mgl::vec2f caret_size) {
|
||||
if(selection_start_caret.index == caret.index)
|
||||
return;
|
||||
|
||||
const int padding_top = padding_top_scale * get_theme().window_height;
|
||||
const int padding_left = padding_left_scale * get_theme().window_height;
|
||||
const int caret_width = std::max(1.0f, caret_width_scale * get_theme().window_height);
|
||||
const int offset = caret.index < selection_start_caret.index ? caret_width : 0;
|
||||
|
||||
mgl::Rectangle caret_selection_rect(mgl::vec2f(std::abs(selection_start_caret.offset_x - caret.offset_x) - offset, caret_size.y).floor());
|
||||
caret_selection_rect.set_position((draw_pos + mgl::vec2f(padding_left + std::min(caret.offset_x, selection_start_caret.offset_x) - text_overflow + offset, padding_top)).floor());
|
||||
mgl::Color caret_select_color = get_color_theme().tint_color;
|
||||
caret_select_color.a = 100;
|
||||
caret_selection_rect.set_color(caret_select_color);
|
||||
window.draw(caret_selection_rect);
|
||||
}
|
||||
|
||||
mgl::vec2f Entry::get_size() {
|
||||
@@ -85,24 +270,157 @@ namespace gsr {
|
||||
return { max_width, text.get_bounds().size.y + padding_top + padding_bottom };
|
||||
}
|
||||
|
||||
void Entry::set_text(std::string str) {
|
||||
if(!validate_handler || validate_handler(str)) {
|
||||
text.set_string(std::move(str));
|
||||
caret_offset_x = text.find_character_pos(99999).x - this->text.get_position().x;
|
||||
if(on_changed)
|
||||
on_changed(text.get_string());
|
||||
void Entry::move_caret_word(Direction direction, size_t max_codepoints) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
const int dir_step = direction == Direction::LEFT ? -1 : 1;
|
||||
const int num_delimiter_chars = 15;
|
||||
const char delimiter_chars[num_delimiter_chars + 1] = " \t\n/.,:;\\[](){}";
|
||||
const char32_t *text_str = active_text.get_string().data();
|
||||
|
||||
int num_non_delimiter_chars_found = 0;
|
||||
|
||||
for(size_t i = 0; i < max_codepoints; ++i) {
|
||||
const uint32_t codepoint = text_str[caret.index];
|
||||
|
||||
const bool is_delimiter_char = codepoint < 127 && !!memchr(delimiter_chars, codepoint, num_delimiter_chars);
|
||||
if(is_delimiter_char) {
|
||||
if(num_non_delimiter_chars_found > 0)
|
||||
break;
|
||||
} else {
|
||||
++num_non_delimiter_chars_found;
|
||||
}
|
||||
|
||||
if(caret.index + dir_step < 0 || caret.index + dir_step > (int)active_text.get_string().size())
|
||||
break;
|
||||
|
||||
caret.index += dir_step;
|
||||
}
|
||||
|
||||
// TODO: Move right by some characters instead of calculating every character to caret index
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
}
|
||||
|
||||
const std::string& Entry::get_text() const {
|
||||
return text.get_string();
|
||||
EntryValidateHandlerResult Entry::set_text(const std::string &str) {
|
||||
EntryValidateHandlerResult validate_result = set_text_internal(mgl::utf8_to_utf32(str));
|
||||
if(validate_result == EntryValidateHandlerResult::ALLOW) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
caret.index = active_text.get_string().size();
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
EntryValidateHandlerResult Entry::set_text_internal(std::u32string str) {
|
||||
EntryValidateHandlerResult validate_result = EntryValidateHandlerResult::ALLOW;
|
||||
if(validate_handler)
|
||||
validate_result = validate_handler(*this, str);
|
||||
|
||||
if(validate_result == EntryValidateHandlerResult::ALLOW) {
|
||||
text.set_string(std::move(str));
|
||||
if(masked)
|
||||
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
|
||||
// TODO: Call callback with utf32 instead?
|
||||
if(on_changed)
|
||||
on_changed(mgl::utf32_to_utf8(text.get_string()));
|
||||
}
|
||||
|
||||
return validate_result;
|
||||
}
|
||||
|
||||
std::string Entry::get_text() const {
|
||||
return mgl::utf32_to_utf8(text.get_string());
|
||||
}
|
||||
|
||||
void Entry::set_masked(bool masked) {
|
||||
if(masked == this->masked)
|
||||
return;
|
||||
|
||||
this->masked = masked;
|
||||
|
||||
if(masked)
|
||||
masked_text.set_string(std::u32string(text.get_string().size(), '*'));
|
||||
else
|
||||
masked_text.set_string(std::u32string());
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret.offset_x = active_text.find_character_pos(selection_start_caret.index).x - active_text.get_position().x;
|
||||
}
|
||||
|
||||
bool Entry::is_masked() const {
|
||||
return masked;
|
||||
}
|
||||
|
||||
void Entry::replace_text(size_t index, size_t size, const std::u32string &replacement) {
|
||||
if(index + size > text.get_string().size())
|
||||
return;
|
||||
|
||||
const auto prev_caret = caret;
|
||||
|
||||
if((int)index >= caret.index)
|
||||
caret.index += replacement.size();
|
||||
else
|
||||
caret.index = caret.index - size + replacement.size();
|
||||
|
||||
std::u32string str = text.get_string();
|
||||
str.replace(index, size, replacement);
|
||||
const EntryValidateHandlerResult validate_result = set_text_internal(std::move(str));
|
||||
if(validate_result == EntryValidateHandlerResult::DENY) {
|
||||
caret = prev_caret;
|
||||
return;
|
||||
} else if(validate_result == EntryValidateHandlerResult::REPLACED) {
|
||||
return;
|
||||
}
|
||||
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
// TODO: Optimize
|
||||
caret.offset_x = active_text.find_character_pos(caret.index).x - active_text.get_position().x;
|
||||
selection_start_caret = caret;
|
||||
|
||||
selecting_text = false;
|
||||
selecting_with_keyboard = false;
|
||||
show_selection = false;
|
||||
}
|
||||
|
||||
CaretIndexPos Entry::find_closest_caret_index_by_position(mgl::vec2f position) {
|
||||
mgl::Text32 &active_text = masked ? masked_text : text;
|
||||
const std::u32string &str = active_text.get_string();
|
||||
mgl::Font *font = active_text.get_font();
|
||||
|
||||
CaretIndexPos result = {0, {active_text.get_position().x, active_text.get_position().y}};
|
||||
for(result.index = 0; result.index < (int)str.size(); ++result.index) {
|
||||
const uint32_t codepoint = str[result.index];
|
||||
|
||||
float glyph_width = 0.0f;
|
||||
if(codepoint == '\t') {
|
||||
const auto glyph = font->get_glyph(' ');
|
||||
const int tab_width = 4;
|
||||
glyph_width = glyph.advance * tab_width;
|
||||
} else {
|
||||
const auto glyph = font->get_glyph(codepoint);
|
||||
glyph_width = glyph.advance;
|
||||
}
|
||||
|
||||
if(result.pos.x + glyph_width * 0.5f >= position.x)
|
||||
break;
|
||||
|
||||
result.pos.x += glyph_width;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool is_number(uint8_t c) {
|
||||
return c >= '0' && c <= '9';
|
||||
}
|
||||
|
||||
static std::optional<int> to_integer(const std::string &str) {
|
||||
static std::optional<int> to_integer(const std::u32string &str) {
|
||||
if(str.empty())
|
||||
return std::nullopt;
|
||||
|
||||
@@ -114,7 +432,7 @@ namespace gsr {
|
||||
int number = 0;
|
||||
for(; i < str.size(); ++i) {
|
||||
if(!is_number(str[i]))
|
||||
return false;
|
||||
return std::nullopt;
|
||||
|
||||
const int new_number = number * 10 + (str[i] - '0');
|
||||
if(new_number < number)
|
||||
@@ -129,19 +447,23 @@ namespace gsr {
|
||||
}
|
||||
|
||||
EntryValidateHandler create_entry_validator_integer_in_range(int min, int max) {
|
||||
return [min, max](std::string &str) {
|
||||
return [min, max](Entry &entry, const std::u32string &str) {
|
||||
if(str.empty())
|
||||
return true;
|
||||
return EntryValidateHandlerResult::ALLOW;
|
||||
|
||||
std::optional<int> number = to_integer(str);
|
||||
const std::optional<int> number = to_integer(str);
|
||||
if(!number)
|
||||
return false;
|
||||
return EntryValidateHandlerResult::DENY;
|
||||
|
||||
if(number.value() < min)
|
||||
str = std::to_string(min);
|
||||
else if(number.value() > max)
|
||||
str = std::to_string(max);
|
||||
return true;
|
||||
if(number.value() < min) {
|
||||
entry.set_text(std::to_string(min));
|
||||
return EntryValidateHandlerResult::REPLACED;
|
||||
} else if(number.value() > max) {
|
||||
entry.set_text(std::to_string(max));
|
||||
return EntryValidateHandlerResult::REPLACED;
|
||||
}
|
||||
|
||||
return EntryValidateHandlerResult::ALLOW;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
|
||||
#include <limits.h>
|
||||
#include <dirent.h>
|
||||
#include <sys/stat.h>
|
||||
#include <errno.h>
|
||||
@@ -296,4 +297,4 @@ namespace gsr {
|
||||
const std::string& FileChooser::get_current_directory() const {
|
||||
return current_directory_text.get_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "../../include/Overlay.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include "../../include/Process.hpp"
|
||||
#include "../../include/Utils.hpp"
|
||||
#include "../../include/Translation.hpp"
|
||||
#include "../../include/gui/GsrPage.hpp"
|
||||
#include "../../include/gui/PageStack.hpp"
|
||||
#include "../../include/gui/ScrollablePage.hpp"
|
||||
@@ -11,6 +13,7 @@
|
||||
#include "../../include/gui/Label.hpp"
|
||||
#include "../../include/gui/Image.hpp"
|
||||
#include "../../include/gui/RadioButton.hpp"
|
||||
#include "../../include/gui/ComboBox.hpp"
|
||||
#include "../../include/gui/LineSeparator.hpp"
|
||||
#include "../../include/gui/CustomRendererWidget.hpp"
|
||||
|
||||
@@ -80,8 +83,8 @@ namespace gsr {
|
||||
gsr_info(gsr_info),
|
||||
page_stack(page_stack)
|
||||
{
|
||||
auto content_page = std::make_unique<GsrPage>("Global", "Settings");
|
||||
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
|
||||
auto content_page = std::make_unique<GsrPage>(TR("Global"), TR("Settings"));
|
||||
content_page->add_button(TR("Back"), "back", get_color_theme().page_bg_color);
|
||||
content_page->on_click = [page_stack](const std::string &id) {
|
||||
if(id == "back")
|
||||
page_stack->pop();
|
||||
@@ -98,9 +101,9 @@ namespace gsr {
|
||||
if(!configure_hotkey_button)
|
||||
return;
|
||||
|
||||
mgl::Text title_text("Press a key combination to use for the hotkey \"" + hotkey_configure_action_name + "\":", get_theme().title_font);
|
||||
mgl::Text title_text(TRF("Press a key combination to use for the hotkey: \"%s\"", hotkey_configure_action_name.c_str()), get_theme().title_font);
|
||||
mgl::Text hotkey_text(configure_hotkey_button->get_text(), get_theme().top_bar_font);
|
||||
mgl::Text description_text("Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.", get_theme().body_font);
|
||||
mgl::Text description_text(TR("Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey."), get_theme().body_font);
|
||||
const float text_max_width = std::max(title_text.get_bounds().size.x, std::max(hotkey_text.get_bounds().size.x, description_text.get_bounds().size.x));
|
||||
|
||||
const float padding_horizontal = int(get_theme().window_height * 0.01f);
|
||||
@@ -143,12 +146,12 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_appearance_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Accent color", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Accent color"), get_color_theme().text_color));
|
||||
auto tint_color_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
tint_color_radio_button_ptr = tint_color_radio_button.get();
|
||||
tint_color_radio_button->add_item("Red", "amd");
|
||||
tint_color_radio_button->add_item("Green", "nvidia");
|
||||
tint_color_radio_button->add_item("Blue", "intel");
|
||||
tint_color_radio_button->add_item(TR("Red"), "amd");
|
||||
tint_color_radio_button->add_item(TR("Green"), "nvidia");
|
||||
tint_color_radio_button->add_item(TR("Blue"), "intel");
|
||||
tint_color_radio_button->on_selection_changed = [](const std::string&, const std::string &id) {
|
||||
if(id == "amd")
|
||||
get_color_theme().tint_color = mgl::Color(221, 0, 49);
|
||||
@@ -159,16 +162,16 @@ namespace gsr {
|
||||
return true;
|
||||
};
|
||||
list->add_widget(std::move(tint_color_radio_button));
|
||||
return std::make_unique<Subsection>("Appearance", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("Appearance"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_startup_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start program on system startup?", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Start program on system startup?"), get_color_theme().text_color));
|
||||
auto startup_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
startup_radio_button_ptr = startup_radio_button.get();
|
||||
startup_radio_button->add_item("Yes", "start_on_system_startup");
|
||||
startup_radio_button->add_item("No", "dont_start_on_system_startup");
|
||||
startup_radio_button->add_item(TR("Yes"), "start_on_system_startup");
|
||||
startup_radio_button->add_item(TR("No"), "dont_start_on_system_startup");
|
||||
startup_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
|
||||
bool enable = false;
|
||||
if(id == "dont_start_on_system_startup")
|
||||
@@ -178,23 +181,22 @@ namespace gsr {
|
||||
else
|
||||
return false;
|
||||
|
||||
const char *args[] = { "systemctl", enable ? "enable" : "disable", "--user", "gpu-screen-recorder-ui", nullptr };
|
||||
std::string stdout_str;
|
||||
const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
|
||||
const int exit_status = set_xdg_autostart(enable);
|
||||
if(on_startup_changed)
|
||||
on_startup_changed(enable, exit_status);
|
||||
return exit_status == 0;
|
||||
};
|
||||
list->add_widget(std::move(startup_radio_button));
|
||||
return std::make_unique<Subsection>("Startup", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("Startup"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_keyboard_hotkeys_button() {
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::VERTICAL);
|
||||
enable_keyboard_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
|
||||
enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("No", "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("Only grab virtual devices (supports input remapping software)", "enable_hotkeys_virtual_devices");
|
||||
enable_hotkeys_radio_button->add_item(TR("Yes"), "enable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item(TR("Yes, but only grab virtual devices (supports some input remapping software)"), "enable_hotkeys_virtual_devices");
|
||||
enable_hotkeys_radio_button->add_item(TR("Yes, but don't grab devices (supports all input remapping software)"), "enable_hotkeys_no_grab");
|
||||
enable_hotkeys_radio_button->add_item(TR("No"), "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
|
||||
if(on_keyboard_hotkey_changed)
|
||||
on_keyboard_hotkey_changed(id.c_str());
|
||||
@@ -206,8 +208,8 @@ namespace gsr {
|
||||
std::unique_ptr<RadioButton> GlobalSettingsPage::create_enable_joystick_hotkeys_button() {
|
||||
auto enable_hotkeys_radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
enable_joystick_hotkeys_radio_button_ptr = enable_hotkeys_radio_button.get();
|
||||
enable_hotkeys_radio_button->add_item("Yes", "enable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item("No", "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item(TR("Yes"), "enable_hotkeys");
|
||||
enable_hotkeys_radio_button->add_item(TR("No"), "disable_hotkeys");
|
||||
enable_hotkeys_radio_button->on_selection_changed = [&](const std::string&, const std::string &id) {
|
||||
if(on_joystick_hotkey_changed)
|
||||
on_joystick_hotkey_changed(id.c_str());
|
||||
@@ -219,7 +221,7 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_show_hide_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Show/hide UI:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Show/hide UI:"), get_color_theme().text_color));
|
||||
auto show_hide_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
show_hide_button_ptr = show_hide_button.get();
|
||||
list->add_widget(std::move(show_hide_button));
|
||||
@@ -234,12 +236,12 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_replay_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Turn replay on/off:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Turn replay on/off:"), get_color_theme().text_color));
|
||||
auto turn_replay_on_off_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
turn_replay_on_off_button_ptr = turn_replay_on_off_button.get();
|
||||
list->add_widget(std::move(turn_replay_on_off_button));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save replay:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Save replay:"), get_color_theme().text_color));
|
||||
auto save_replay_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
save_replay_button_ptr = save_replay_button.get();
|
||||
list->add_widget(std::move(save_replay_button));
|
||||
@@ -258,12 +260,12 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_replay_partial_save_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 1 minute replay:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Save 1 minute replay:"), get_color_theme().text_color));
|
||||
auto save_replay_1_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
save_replay_1_min_button_ptr = save_replay_1_min_button.get();
|
||||
list->add_widget(std::move(save_replay_1_min_button));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Save 10 minute replay:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Save 10 minute replay:"), get_color_theme().text_color));
|
||||
auto save_replay_10_min_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
save_replay_10_min_button_ptr = save_replay_10_min_button.get();
|
||||
list->add_widget(std::move(save_replay_10_min_button));
|
||||
@@ -282,12 +284,12 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop recording:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Start/stop recording:"), get_color_theme().text_color));
|
||||
auto start_stop_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_button_ptr = start_stop_recording_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_button));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Pause/unpause recording:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Pause/unpause recording:"), get_color_theme().text_color));
|
||||
auto pause_unpause_recording_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
pause_unpause_recording_button_ptr = pause_unpause_recording_button.get();
|
||||
list->add_widget(std::move(pause_unpause_recording_button));
|
||||
@@ -303,10 +305,46 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_window_region_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Start/stop recording a region:"), get_color_theme().text_color));
|
||||
auto start_stop_recording_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_region_button_ptr = start_stop_recording_region_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_region_button));
|
||||
|
||||
start_stop_recording_region_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_REGION);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_record_hotkey_window_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
char str[128];
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
snprintf(str, sizeof(str), "%s", TR("Start/stop recording a window:"));
|
||||
else
|
||||
snprintf(str, sizeof(str), "%s", TR("Start/stop recording with desktop portal:"));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
auto start_stop_recording_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_recording_window_button_ptr = start_stop_recording_window_button.get();
|
||||
list->add_widget(std::move(start_stop_recording_window_button));
|
||||
|
||||
start_stop_recording_window_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::RECORD_START_STOP_WINDOW);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_stream_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Start/stop streaming:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Start/stop streaming:"), get_color_theme().text_color));
|
||||
auto start_stop_streaming_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
start_stop_streaming_button_ptr = start_stop_streaming_button.get();
|
||||
list->add_widget(std::move(start_stop_streaming_button));
|
||||
@@ -321,7 +359,7 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Take a screenshot:"), get_color_theme().text_color));
|
||||
auto take_screenshot_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
take_screenshot_button_ptr = take_screenshot_button.get();
|
||||
list->add_widget(std::move(take_screenshot_button));
|
||||
@@ -336,7 +374,7 @@ namespace gsr {
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_region_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Take a screenshot of a region:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Take a screenshot of a region:"), get_color_theme().text_color));
|
||||
auto take_screenshot_region_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
take_screenshot_region_button_ptr = take_screenshot_region_button.get();
|
||||
list->add_widget(std::move(take_screenshot_region_button));
|
||||
@@ -348,27 +386,41 @@ namespace gsr {
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_screenshot_window_hotkey_options() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
char str[128];
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
snprintf(str, sizeof(str), "%s", TR("Take a screenshot of a window:"));
|
||||
else
|
||||
snprintf(str, sizeof(str), "%s", TR("Take a screenshot with desktop portal:"));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
auto take_screenshot_window_button = std::make_unique<Button>(&get_theme().body_font, "", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
take_screenshot_window_button_ptr = take_screenshot_window_button.get();
|
||||
list->add_widget(std::move(take_screenshot_window_button));
|
||||
|
||||
take_screenshot_window_button_ptr->on_click = [this] {
|
||||
configure_hotkey_start(ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW);
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_hotkey_control_buttons() {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
|
||||
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Clear hotkeys", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
auto clear_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, TR("Clear hotkeys"), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
clear_hotkeys_button->on_click = [this] {
|
||||
config.streaming_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.record_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.record_config.pause_unpause_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.start_stop_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_1_min_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.replay_config.save_10_min_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.screenshot_config.take_screenshot_region_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
config.main_config.show_hide_hotkey = {mgl::Keyboard::Unknown, 0};
|
||||
for_each_config_hotkey([&](ConfigHotkey *config_hotkey_item) {
|
||||
*config_hotkey_item = {mgl::Keyboard::Unknown, 0};
|
||||
});
|
||||
load_hotkeys();
|
||||
overlay->rebind_all_keyboard_hotkeys();
|
||||
};
|
||||
list->add_widget(std::move(clear_hotkeys_button));
|
||||
|
||||
auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, "Reset hotkeys to default", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
auto reset_hotkeys_button = std::make_unique<Button>(&get_theme().body_font, TR("Reset hotkeys to default"), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
reset_hotkeys_button->on_click = [this] {
|
||||
config.set_hotkeys_to_default();
|
||||
load_hotkeys();
|
||||
@@ -381,9 +433,9 @@ namespace gsr {
|
||||
|
||||
static std::unique_ptr<List> create_joystick_hotkey_text(mgl::Texture *image1, mgl::Texture *image2, float max_height, const char *suffix) {
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL, List::Alignment::CENTER);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Press", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Press"), get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Image>(image1, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "and", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("and"), get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Image>(image2, mgl::vec2f{max_height, 1000.0f}, Image::ScaleBehavior::SCALE));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, suffix, get_color_theme().text_color));
|
||||
return list;
|
||||
@@ -392,18 +444,21 @@ namespace gsr {
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_keyboard_hotkey_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
List *list_ptr = list.get();
|
||||
auto subsection = std::make_unique<Subsection>("Keyboard hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
auto subsection = std::make_unique<Subsection>(TR("Keyboard hotkeys"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
|
||||
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable keyboard hotkeys?", get_color_theme().text_color));
|
||||
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Enable keyboard hotkeys?"), get_color_theme().text_color));
|
||||
list_ptr->add_widget(create_enable_keyboard_hotkeys_button());
|
||||
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
|
||||
list_ptr->add_widget(create_show_hide_hotkey_options());
|
||||
list_ptr->add_widget(create_replay_hotkey_options());
|
||||
list_ptr->add_widget(create_replay_partial_save_hotkey_options());
|
||||
list_ptr->add_widget(create_record_hotkey_options());
|
||||
list_ptr->add_widget(create_record_hotkey_window_region_options());
|
||||
list_ptr->add_widget(create_record_hotkey_window_options());
|
||||
list_ptr->add_widget(create_stream_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_region_hotkey_options());
|
||||
list_ptr->add_widget(create_screenshot_window_hotkey_options());
|
||||
list_ptr->add_widget(create_hotkey_control_buttons());
|
||||
return subsection;
|
||||
}
|
||||
@@ -411,23 +466,23 @@ namespace gsr {
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_controller_hotkey_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
List *list_ptr = list.get();
|
||||
auto subsection = std::make_unique<Subsection>("Controller hotkeys", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
auto subsection = std::make_unique<Subsection>(TR("Controller hotkeys"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
|
||||
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, "Enable controller hotkeys?", get_color_theme().text_color));
|
||||
list_ptr->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Enable controller hotkeys?"), get_color_theme().text_color));
|
||||
list_ptr->add_widget(create_enable_joystick_hotkeys_button());
|
||||
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_options_texture, get_theme().body_font.get_character_size(), "to show/hide the UI"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_up_texture, get_theme().body_font.get_character_size(), "to take a screenshot"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), "to save a replay"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), "to start/stop recording"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), "to turn replay on/off"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), "to save a 1 minute replay"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), "to save a 10 minute replay"));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_options_texture, get_theme().body_font.get_character_size(), TR("to show/hide the UI")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_up_texture, get_theme().body_font.get_character_size(), TR("to take a screenshot")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_down_texture, get_theme().body_font.get_character_size(), TR("to save a replay")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_left_texture, get_theme().body_font.get_character_size(), TR("to start/stop recording")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_dpad_right_texture, get_theme().body_font.get_character_size(), TR("to turn replay on/off")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_cross_texture, get_theme().body_font.get_character_size(), TR("to save a 1 minute replay")));
|
||||
list_ptr->add_widget(create_joystick_hotkey_text(&get_theme().ps4_home_texture, &get_theme().ps4_triangle_texture, get_theme().body_font.get_character_size(), TR("to save a 10 minute replay")));
|
||||
return subsection;
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> GlobalSettingsPage::create_exit_program_button() {
|
||||
auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Exit program", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, TR("Exit program"), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
exit_program_button->on_click = [&]() {
|
||||
if(on_click_exit_program_button)
|
||||
on_click_exit_program_button("exit");
|
||||
@@ -436,7 +491,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::unique_ptr<Button> GlobalSettingsPage::create_go_back_to_old_ui_button() {
|
||||
auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, "Go back to the old UI", mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
auto exit_program_button = std::make_unique<Button>(&get_theme().body_font, TR("Go back to the old UI"), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
exit_program_button->on_click = [&]() {
|
||||
if(on_click_exit_program_button)
|
||||
on_click_exit_program_button("back-to-old-ui");
|
||||
@@ -444,13 +499,70 @@ namespace gsr {
|
||||
return exit_program_button;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_notification_speed() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Notification speed"), get_color_theme().text_color));
|
||||
|
||||
auto radio_button = std::make_unique<RadioButton>(&get_theme().body_font, RadioButton::Orientation::HORIZONTAL);
|
||||
notification_speed_button_ptr = radio_button.get();
|
||||
radio_button->add_item(TR("Normal"), "normal");
|
||||
radio_button->add_item(TR("Fast"), "fast");
|
||||
radio_button->on_selection_changed = [this](const std::string&, const std::string &id) {
|
||||
if(id == "normal")
|
||||
overlay->set_notification_speed(NotificationSpeed::NORMAL);
|
||||
else if(id == "fast")
|
||||
overlay->set_notification_speed(NotificationSpeed::FAST);
|
||||
return true;
|
||||
};
|
||||
list->add_widget(std::move(radio_button));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> GlobalSettingsPage::create_language() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Language"), get_color_theme().text_color));
|
||||
|
||||
auto combo_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
language_combo_box_ptr = combo_box.get();
|
||||
combo_box->add_item(TR("System language"), "");
|
||||
combo_box->add_item("English", "en");
|
||||
combo_box->add_item("Español", "es");
|
||||
combo_box->add_item("Français", "fr");
|
||||
combo_box->add_item("Magyar", "hu");
|
||||
combo_box->add_item("Русский", "ru");
|
||||
combo_box->add_item("Українська", "uk");
|
||||
combo_box->on_selection_changed = [](const std::string&, const std::string &id) {
|
||||
Translation::instance().load_language(id.c_str());
|
||||
return true;
|
||||
};
|
||||
list->add_widget(std::move(combo_box));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_options_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
List *list_ptr = list.get();
|
||||
auto subsection = std::make_unique<Subsection>(TR("Application options"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
|
||||
{
|
||||
auto horizontal_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
horizontal_list->set_spacing(0.02f);
|
||||
horizontal_list->add_widget(create_notification_speed());
|
||||
horizontal_list->add_widget(create_language());
|
||||
list_ptr->add_widget(std::move(horizontal_list));
|
||||
}
|
||||
list_ptr->add_widget(std::make_unique<LineSeparator>(LineSeparator::Orientation::HORIZONTAL, subsection->get_inner_size().x));
|
||||
|
||||
const bool inside_flatpak = getenv("FLATPAK_ID") != NULL;
|
||||
auto list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
list->add_widget(create_exit_program_button());
|
||||
auto navigate_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
navigate_list->add_widget(create_exit_program_button());
|
||||
if(inside_flatpak)
|
||||
list->add_widget(create_go_back_to_old_ui_button());
|
||||
return std::make_unique<Subsection>("Application options", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
navigate_list->add_widget(create_go_back_to_old_ui_button());
|
||||
list_ptr->add_widget(std::move(navigate_list));
|
||||
|
||||
return subsection;
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_application_info_subsection(ScrollablePage *parent_page) {
|
||||
@@ -459,21 +571,37 @@ namespace gsr {
|
||||
|
||||
char str[128];
|
||||
const std::string gsr_version = gsr_info->system_info.gsr_version.to_string();
|
||||
snprintf(str, sizeof(str), "GSR version: %s", gsr_version.c_str());
|
||||
snprintf(str, sizeof(str), TR("GSR version: %s"), gsr_version.c_str());
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
|
||||
snprintf(str, sizeof(str), "GSR-UI version: %s", GSR_UI_VERSION);
|
||||
snprintf(str, sizeof(str), TR("GSR-UI version: %s"), GSR_UI_VERSION);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
|
||||
if(inside_flatpak) {
|
||||
snprintf(str, sizeof(str), "Flatpak version: %s", GSR_FLATPAK_VERSION);
|
||||
snprintf(str, sizeof(str), TR("Flatpak version: %s"), GSR_FLATPAK_VERSION);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
}
|
||||
|
||||
snprintf(str, sizeof(str), "GPU vendor: %s", gpu_vendor_to_string(gsr_info->gpu_info.vendor));
|
||||
snprintf(str, sizeof(str), TR("GPU vendor: %s"), gpu_vendor_to_string(gsr_info->gpu_info.vendor));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, str, get_color_theme().text_color));
|
||||
|
||||
return std::make_unique<Subsection>("Application info", std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("Application info"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Subsection> GlobalSettingsPage::create_donate_subsection(ScrollablePage *parent_page) {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:"), get_color_theme().text_color));
|
||||
|
||||
auto donate_button = std::make_unique<Button>(&get_theme().body_font, TR("Donate"), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
donate_button->on_click = [this] {
|
||||
const char *args[] = { "xdg-open", "https://buymeacoffee.com/dec05eba", nullptr };
|
||||
exec_program_daemonized(args);
|
||||
overlay->hide_next_frame();
|
||||
};
|
||||
list->add_widget(std::move(donate_button));
|
||||
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software."), get_color_theme().text_color));
|
||||
return std::make_unique<Subsection>(TR("Donate"), std::move(list), mgl::vec2f(parent_page->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
void GlobalSettingsPage::add_widgets() {
|
||||
@@ -487,6 +615,7 @@ namespace gsr {
|
||||
settings_list->add_widget(create_controller_hotkey_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_application_options_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_application_info_subsection(scrollable_page.get()));
|
||||
settings_list->add_widget(create_donate_subsection(scrollable_page.get()));
|
||||
scrollable_page->add_widget(std::move(settings_list));
|
||||
|
||||
content_page_ptr->add_widget(std::move(scrollable_page));
|
||||
@@ -504,14 +633,14 @@ namespace gsr {
|
||||
else
|
||||
tint_color_radio_button_ptr->set_selected_item(config.main_config.tint_color);
|
||||
|
||||
const char *args[] = { "systemctl", "is-enabled", "--quiet", "--user", "gpu-screen-recorder-ui", nullptr };
|
||||
std::string stdout_str;
|
||||
const int exit_status = exec_program_on_host_get_stdout(args, stdout_str);
|
||||
startup_radio_button_ptr->set_selected_item(exit_status == 0 ? "start_on_system_startup" : "dont_start_on_system_startup", false, false);
|
||||
startup_radio_button_ptr->set_selected_item(is_xdg_autostart_enabled() ? "start_on_system_startup" : "dont_start_on_system_startup", false, false);
|
||||
|
||||
enable_keyboard_hotkeys_radio_button_ptr->set_selected_item(config.main_config.hotkeys_enable_option, false, false);
|
||||
enable_joystick_hotkeys_radio_button_ptr->set_selected_item(config.main_config.joystick_hotkeys_enable_option, false, false);
|
||||
|
||||
notification_speed_button_ptr->set_selected_item(config.main_config.notification_speed);
|
||||
language_combo_box_ptr->set_selected_item(config.main_config.language);
|
||||
|
||||
load_hotkeys();
|
||||
}
|
||||
|
||||
@@ -523,11 +652,14 @@ namespace gsr {
|
||||
|
||||
start_stop_recording_button_ptr->set_text(config.record_config.start_stop_hotkey.to_string());
|
||||
pause_unpause_recording_button_ptr->set_text(config.record_config.pause_unpause_hotkey.to_string());
|
||||
start_stop_recording_region_button_ptr->set_text(config.record_config.start_stop_region_hotkey.to_string());
|
||||
start_stop_recording_window_button_ptr->set_text(config.record_config.start_stop_window_hotkey.to_string());
|
||||
|
||||
start_stop_streaming_button_ptr->set_text(config.streaming_config.start_stop_hotkey.to_string());
|
||||
|
||||
take_screenshot_button_ptr->set_text(config.screenshot_config.take_screenshot_hotkey.to_string());
|
||||
take_screenshot_region_button_ptr->set_text(config.screenshot_config.take_screenshot_region_hotkey.to_string());
|
||||
take_screenshot_window_button_ptr->set_text(config.screenshot_config.take_screenshot_window_hotkey.to_string());
|
||||
|
||||
show_hide_button_ptr->set_text(config.main_config.show_hide_hotkey.to_string());
|
||||
}
|
||||
@@ -537,6 +669,8 @@ namespace gsr {
|
||||
config.main_config.tint_color = tint_color_radio_button_ptr->get_selected_id();
|
||||
config.main_config.hotkeys_enable_option = enable_keyboard_hotkeys_radio_button_ptr->get_selected_id();
|
||||
config.main_config.joystick_hotkeys_enable_option = enable_joystick_hotkeys_radio_button_ptr->get_selected_id();
|
||||
config.main_config.notification_speed = notification_speed_button_ptr->get_selected_id();
|
||||
config.main_config.language = language_combo_box_ptr->get_selected_id();
|
||||
save_config(config);
|
||||
}
|
||||
|
||||
@@ -605,12 +739,18 @@ namespace gsr {
|
||||
return start_stop_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return pause_unpause_recording_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return start_stop_recording_region_button_ptr;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return start_stop_recording_window_button_ptr;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return start_stop_streaming_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
return take_screenshot_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
return take_screenshot_region_button_ptr;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
|
||||
return take_screenshot_window_button_ptr;
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
return show_hide_button_ptr;
|
||||
}
|
||||
@@ -633,12 +773,18 @@ namespace gsr {
|
||||
return &config.record_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
return &config.record_config.pause_unpause_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
return &config.record_config.start_stop_region_hotkey;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
return &config.record_config.start_stop_window_hotkey;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
return &config.streaming_config.start_stop_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
return &config.screenshot_config.take_screenshot_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
return &config.screenshot_config.take_screenshot_region_hotkey;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW:
|
||||
return &config.screenshot_config.take_screenshot_window_hotkey;
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
return &config.main_config.show_hide_hotkey;
|
||||
}
|
||||
@@ -649,11 +795,16 @@ namespace gsr {
|
||||
ConfigHotkey *config_hotkeys[] = {
|
||||
&config.replay_config.start_stop_hotkey,
|
||||
&config.replay_config.save_hotkey,
|
||||
&config.replay_config.save_1_min_hotkey,
|
||||
&config.replay_config.save_10_min_hotkey,
|
||||
&config.record_config.start_stop_hotkey,
|
||||
&config.record_config.pause_unpause_hotkey,
|
||||
&config.record_config.start_stop_region_hotkey,
|
||||
&config.record_config.start_stop_window_hotkey,
|
||||
&config.streaming_config.start_stop_hotkey,
|
||||
&config.screenshot_config.take_screenshot_hotkey,
|
||||
&config.screenshot_config.take_screenshot_region_hotkey,
|
||||
&config.screenshot_config.take_screenshot_window_hotkey,
|
||||
&config.main_config.show_hide_hotkey
|
||||
};
|
||||
for(ConfigHotkey *config_hotkey : config_hotkeys) {
|
||||
@@ -676,34 +827,50 @@ namespace gsr {
|
||||
hotkey_configure_action_name = "";
|
||||
break;
|
||||
case ConfigureHotkeyType::REPLAY_START_STOP:
|
||||
hotkey_configure_action_name = "Turn replay on/off";
|
||||
hotkey_configure_action_name = TR("Turn replay on/off");
|
||||
break;
|
||||
case ConfigureHotkeyType::REPLAY_SAVE:
|
||||
hotkey_configure_action_name = "Save replay";
|
||||
hotkey_configure_action_name = TR("Save replay");
|
||||
break;
|
||||
case ConfigureHotkeyType::REPLAY_SAVE_1_MIN:
|
||||
hotkey_configure_action_name = "Save 1 minute replay";
|
||||
hotkey_configure_action_name = TR("Save 1 minute replay");
|
||||
break;
|
||||
case ConfigureHotkeyType::REPLAY_SAVE_10_MIN:
|
||||
hotkey_configure_action_name = "Save 10 minute replay";
|
||||
hotkey_configure_action_name = TR("Save 10 minute replay");
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP:
|
||||
hotkey_configure_action_name = "Start/stop recording";
|
||||
hotkey_configure_action_name = TR("Start/stop recording");
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_PAUSE_UNPAUSE:
|
||||
hotkey_configure_action_name = "Pause/unpause recording";
|
||||
hotkey_configure_action_name = TR("Pause/unpause recording");
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_REGION:
|
||||
hotkey_configure_action_name = TR("Start/stop recording a region");
|
||||
break;
|
||||
case ConfigureHotkeyType::RECORD_START_STOP_WINDOW:
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
hotkey_configure_action_name = TR("Start/stop recording a window");
|
||||
else
|
||||
hotkey_configure_action_name = TR("Start/stop recording with desktop portal");
|
||||
break;
|
||||
case ConfigureHotkeyType::STREAM_START_STOP:
|
||||
hotkey_configure_action_name = "Start/stop streaming";
|
||||
hotkey_configure_action_name = TR("Start/stop streaming");
|
||||
break;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT:
|
||||
hotkey_configure_action_name = "Take a screenshot";
|
||||
hotkey_configure_action_name = TR("Take a screenshot");
|
||||
break;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_REGION:
|
||||
hotkey_configure_action_name = "Take a screenshot of a region";
|
||||
hotkey_configure_action_name = TR("Take a screenshot of a region");
|
||||
break;
|
||||
case ConfigureHotkeyType::TAKE_SCREENSHOT_WINDOW: {
|
||||
if(gsr_info->system_info.display_server == DisplayServer::X11)
|
||||
hotkey_configure_action_name = TR("Take a screenshot of a window");
|
||||
else
|
||||
hotkey_configure_action_name = TR("Take a screenshot with desktop portal");
|
||||
break;
|
||||
}
|
||||
case ConfigureHotkeyType::SHOW_HIDE:
|
||||
hotkey_configure_action_name = "Show/hide UI";
|
||||
hotkey_configure_action_name = TR("Show/hide UI");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -734,7 +901,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
if(hotkey_used_by_another_action) {
|
||||
const std::string error_msg = "The hotkey \"" + configure_config_hotkey.to_string() + " is already used for something else";
|
||||
const std::string error_msg = TR("The hotkey \"") + configure_config_hotkey.to_string() + TR("\" is already used for something else");
|
||||
overlay->show_notification(error_msg.c_str(), 3.0, mgl::Color(255, 0, 0, 255), mgl::Color(255, 0, 0, 255), NotificationType::NONE);
|
||||
config_hotkey_button->set_text(config_hotkey->to_string());
|
||||
configure_config_hotkey = {0, 0};
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "../../include/gui/Utils.hpp"
|
||||
|
||||
#include <mglpp/window/Window.hpp>
|
||||
#include <mglpp/window/Event.hpp>
|
||||
#include <mglpp/system/FloatRect.hpp>
|
||||
#include <mglpp/graphics/Texture.hpp>
|
||||
|
||||
namespace gsr {
|
||||
@@ -19,8 +21,15 @@ namespace gsr {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (position + offset).floor();
|
||||
|
||||
if(on_mouse_move) {
|
||||
const bool mouse_inside = mgl::FloatRect(draw_pos, get_size()).contains(window.get_mouse_position().to_vec2f());
|
||||
on_mouse_move(mouse_inside);
|
||||
}
|
||||
|
||||
sprite.set_size(get_size());
|
||||
sprite.set_position((position + offset).floor());
|
||||
sprite.set_position(draw_pos);
|
||||
window.draw(sprite);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "../../include/Theme.hpp"
|
||||
#include "../../include/GsrInfo.hpp"
|
||||
#include "../../include/Utils.hpp"
|
||||
#include "../../include/Translation.hpp"
|
||||
#include "../../include/gui/List.hpp"
|
||||
#include "../../include/gui/ScrollablePage.hpp"
|
||||
#include "../../include/gui/Label.hpp"
|
||||
@@ -11,16 +12,17 @@
|
||||
#include "../../include/gui/FileChooser.hpp"
|
||||
|
||||
namespace gsr {
|
||||
ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack) :
|
||||
ScreenshotSettingsPage::ScreenshotSettingsPage(const GsrInfo *gsr_info, Config &config, PageStack *page_stack, bool supports_window_title) :
|
||||
StaticPage(mgl::vec2f(get_theme().window_width, get_theme().window_height).floor()),
|
||||
config(config),
|
||||
gsr_info(gsr_info),
|
||||
page_stack(page_stack)
|
||||
page_stack(page_stack),
|
||||
supports_window_title(supports_window_title)
|
||||
{
|
||||
capture_options = get_supported_capture_options(*gsr_info);
|
||||
|
||||
auto content_page = std::make_unique<GsrPage>("Screenshot", "Settings");
|
||||
content_page->add_button("Back", "back", get_color_theme().page_bg_color);
|
||||
auto content_page = std::make_unique<GsrPage>(TR("Screenshot"), TR("Settings"));
|
||||
content_page->add_button(TR("Back"), "back", get_color_theme().page_bg_color);
|
||||
content_page->on_click = [page_stack](const std::string &id) {
|
||||
if(id == "back")
|
||||
page_stack->pop();
|
||||
@@ -36,25 +38,25 @@ namespace gsr {
|
||||
auto record_area_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
// TODO: Show options not supported but disable them
|
||||
if(capture_options.window)
|
||||
record_area_box->add_item("Window", "window");
|
||||
record_area_box->add_item(TR("Window"), "window");
|
||||
if(capture_options.region)
|
||||
record_area_box->add_item("Region", "region");
|
||||
record_area_box->add_item(TR("Region"), "region");
|
||||
if(!capture_options.monitors.empty())
|
||||
record_area_box->add_item(gsr_info->system_info.display_server == DisplayServer::WAYLAND ? "Focused monitor (Experimental on Wayland)" : "Focused monitor", "focused_monitor");
|
||||
record_area_box->add_item(TR("Focused monitor"), "focused_monitor");
|
||||
for(const auto &monitor : capture_options.monitors) {
|
||||
char name[256];
|
||||
snprintf(name, sizeof(name), "Monitor %s (%dx%d)", monitor.name.c_str(), monitor.size.x, monitor.size.y);
|
||||
snprintf(name, sizeof(name), TR("Monitor %s (%dx%d)"), monitor.name.c_str(), monitor.size.x, monitor.size.y);
|
||||
record_area_box->add_item(name, monitor.name);
|
||||
}
|
||||
if(capture_options.portal)
|
||||
record_area_box->add_item("Desktop portal", "portal");
|
||||
record_area_box->add_item(TR("Desktop portal"), "portal");
|
||||
record_area_box_ptr = record_area_box.get();
|
||||
return record_area_box;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_area() {
|
||||
auto record_area_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Capture target:", get_color_theme().text_color));
|
||||
record_area_list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Capture source:"), get_color_theme().text_color));
|
||||
record_area_list->add_widget(create_record_area_box());
|
||||
return record_area_list;
|
||||
}
|
||||
@@ -83,14 +85,14 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_image_resolution_section() {
|
||||
auto image_resolution_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image resolution limit:", get_color_theme().text_color));
|
||||
image_resolution_list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Image resolution limit:"), get_color_theme().text_color));
|
||||
image_resolution_list->add_widget(create_image_resolution());
|
||||
image_resolution_list_ptr = image_resolution_list.get();
|
||||
return image_resolution_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_restore_portal_session_checkbox() {
|
||||
auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Restore portal session");
|
||||
auto restore_portal_session_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Restore portal session"));
|
||||
restore_portal_session_checkbox->set_checked(true);
|
||||
restore_portal_session_checkbox_ptr = restore_portal_session_checkbox.get();
|
||||
return restore_portal_session_checkbox;
|
||||
@@ -105,7 +107,7 @@ namespace gsr {
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_change_image_resolution_section() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Change image resolution");
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Change image resolution"));
|
||||
change_image_resolution_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
@@ -120,18 +122,18 @@ namespace gsr {
|
||||
|
||||
ll->add_widget(std::move(capture_target_list));
|
||||
ll->add_widget(create_change_image_resolution_section());
|
||||
return std::make_unique<Subsection>("Record area", std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("Capture"), std::move(ll), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_image_quality_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image quality:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Image quality:"), get_color_theme().text_color));
|
||||
|
||||
auto image_quality_box = std::make_unique<ComboBox>(&get_theme().body_font);
|
||||
image_quality_box->add_item("Medium", "medium");
|
||||
image_quality_box->add_item("High", "high");
|
||||
image_quality_box->add_item("Very high (Recommended)", "very_high");
|
||||
image_quality_box->add_item("Ultra", "ultra");
|
||||
image_quality_box->add_item(TR("Medium"), "medium");
|
||||
image_quality_box->add_item(TR("High"), "high");
|
||||
image_quality_box->add_item(TR("Very high (Recommended)"), "very_high");
|
||||
image_quality_box->add_item(TR("Ultra"), "ultra");
|
||||
image_quality_box->set_selected_item("very_high");
|
||||
|
||||
image_quality_box_ptr = image_quality_box.get();
|
||||
@@ -139,9 +141,9 @@ namespace gsr {
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_record_cursor_section() {
|
||||
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Record cursor");
|
||||
auto record_cursor_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Record cursor"));
|
||||
record_cursor_checkbox->set_checked(true);
|
||||
record_cursor_checkbox_ptr = record_cursor_checkbox.get();
|
||||
return record_cursor_checkbox;
|
||||
@@ -151,7 +153,7 @@ namespace gsr {
|
||||
auto image_section_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
image_section_list->add_widget(create_image_quality_section());
|
||||
image_section_list->add_widget(create_record_cursor_section());
|
||||
return std::make_unique<Subsection>("Image", std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("Image"), std::move(image_section_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_save_directory(const char *label) {
|
||||
@@ -160,10 +162,10 @@ namespace gsr {
|
||||
auto save_directory_button = std::make_unique<Button>(&get_theme().body_font, get_pictures_dir().c_str(), mgl::vec2f(0.0f, 0.0f), mgl::Color(0, 0, 0, 120));
|
||||
save_directory_button_ptr = save_directory_button.get();
|
||||
save_directory_button->on_click = [this]() {
|
||||
auto select_directory_page = std::make_unique<GsrPage>("File", "Settings");
|
||||
select_directory_page->add_button("Save", "save", get_color_theme().tint_color);
|
||||
select_directory_page->add_button("Cancel", "cancel", get_color_theme().page_bg_color);
|
||||
|
||||
auto select_directory_page = std::make_unique<GsrPage>(TR("File"), TR("Settings"));
|
||||
select_directory_page->add_button(TR("Save"), "save", get_color_theme().tint_color);
|
||||
select_directory_page->add_button(TR("Cancel"), "cancel", get_color_theme().page_bg_color);
|
||||
|
||||
auto file_chooser = std::make_unique<FileChooser>(save_directory_button_ptr->get_text().c_str(), select_directory_page->get_inner_size());
|
||||
FileChooser *file_chooser_ptr = file_chooser.get();
|
||||
select_directory_page->add_widget(std::move(file_chooser));
|
||||
@@ -195,41 +197,93 @@ namespace gsr {
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_image_format_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, "Image format:", get_color_theme().text_color));
|
||||
list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Image format:"), get_color_theme().text_color));
|
||||
list->add_widget(create_image_format_box());
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_file_info_section() {
|
||||
auto file_info_data_list = std::make_unique<List>(List::Orientation::HORIZONTAL);
|
||||
file_info_data_list->add_widget(create_save_directory("Directory to save the screenshot:"));
|
||||
file_info_data_list->add_widget(create_save_directory(TR("Directory to save screenshots:")));
|
||||
file_info_data_list->add_widget(create_image_format_section());
|
||||
return std::make_unique<Subsection>("File info", std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
return std::make_unique<Subsection>(TR("File info"), std::move(file_info_data_list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_in_game_folder() {
|
||||
char text[256];
|
||||
snprintf(text, sizeof(text), "Save screenshot in a folder with the name of the game%s", gsr_info->system_info.display_server == DisplayServer::X11 ? "" : " (X11 applications only)");
|
||||
snprintf(text, sizeof(text), "%s%s", TR("Save screenshot in a folder based on the games name"), supports_window_title ? "" : " (X11 applications only)");
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, text);
|
||||
save_screenshot_in_game_folder_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
|
||||
return std::make_unique<Subsection>("General", create_save_screenshot_in_game_folder(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_clipboard() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, gsr_info->system_info.display_server == DisplayServer::X11 ? TR("Save screenshot to clipboard") : TR("Save screenshot to clipboard (Not supported properly by Wayland)"));
|
||||
save_screenshot_to_clipboard_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications_section() {
|
||||
auto show_screenshot_saved_notification_checkbox = std::make_unique<CheckBox>(&get_theme().body_font, "Show screenshot saved notification");
|
||||
show_screenshot_saved_notification_checkbox->set_checked(true);
|
||||
show_screenshot_saved_notification_checkbox_ptr = show_screenshot_saved_notification_checkbox.get();
|
||||
return std::make_unique<Subsection>("Notifications", std::move(show_screenshot_saved_notification_checkbox), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
std::unique_ptr<CheckBox> ScreenshotSettingsPage::create_save_screenshot_to_disk() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Save screenshot to disk"));
|
||||
save_screenshot_to_disk_checkbox_ptr = checkbox.get();
|
||||
checkbox->set_checked(true);
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_notifications() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Show screenshot notifications"));
|
||||
checkbox->set_checked(true);
|
||||
show_notification_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_led_indicator() {
|
||||
auto checkbox = std::make_unique<CheckBox>(&get_theme().body_font, TR("Blink scroll lock led when taking a screenshot"));
|
||||
checkbox->set_checked(true);
|
||||
led_indicator_checkbox_ptr = checkbox.get();
|
||||
return checkbox;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_general_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_save_screenshot_in_game_folder());
|
||||
list->add_widget(create_save_screenshot_to_clipboard());
|
||||
list->add_widget(create_save_screenshot_to_disk());
|
||||
return std::make_unique<Subsection>(TR("General"), std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_screenshot_indicator_section() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
list->add_widget(create_notifications());
|
||||
list->add_widget(create_led_indicator());
|
||||
return std::make_unique<Subsection>(TR("Screenshot indicator"), std::move(list), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot_entry() {
|
||||
auto list = std::make_unique<List>(List::Orientation::VERTICAL, List::Alignment::CENTER);
|
||||
|
||||
auto create_custom_script_screenshot_entry = std::make_unique<Entry>(&get_theme().body_font, "", get_theme().body_font.get_character_size() * 20);
|
||||
create_custom_script_screenshot_entry_ptr = create_custom_script_screenshot_entry.get();
|
||||
list->add_widget(std::move(create_custom_script_screenshot_entry));
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::unique_ptr<List> ScreenshotSettingsPage::create_custom_script_screenshot() {
|
||||
auto custom_script_screenshot_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
custom_script_screenshot_list->add_widget(std::make_unique<Label>(&get_theme().body_font, TR("Command to open the screenshot with:"), get_color_theme().text_color));
|
||||
custom_script_screenshot_list->add_widget(create_custom_script_screenshot_entry());
|
||||
return custom_script_screenshot_list;
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_custom_script_screenshot_section() {
|
||||
return std::make_unique<Subsection>(TR("Script"), create_custom_script_screenshot(), mgl::vec2f(settings_scrollable_page_ptr->get_inner_size().x, 0.0f));
|
||||
}
|
||||
|
||||
std::unique_ptr<Widget> ScreenshotSettingsPage::create_settings() {
|
||||
auto page_list = std::make_unique<List>(List::Orientation::VERTICAL);
|
||||
page_list->set_spacing(0.018f);
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y + 0.018f * get_theme().window_height));
|
||||
auto scrollable_page = std::make_unique<ScrollablePage>(content_page_ptr->get_inner_size() - mgl::vec2f(0.0f, page_list->get_size().y));
|
||||
settings_scrollable_page_ptr = scrollable_page.get();
|
||||
page_list->add_widget(std::move(scrollable_page));
|
||||
|
||||
@@ -239,7 +293,8 @@ namespace gsr {
|
||||
settings_list->add_widget(create_image_section());
|
||||
settings_list->add_widget(create_file_info_section());
|
||||
settings_list->add_widget(create_general_section());
|
||||
settings_list->add_widget(create_notifications_section());
|
||||
settings_list->add_widget(create_screenshot_indicator_section());
|
||||
settings_list->add_widget(create_custom_script_screenshot_section());
|
||||
settings_scrollable_page_ptr->add_widget(std::move(settings_list));
|
||||
return page_list;
|
||||
}
|
||||
@@ -281,7 +336,10 @@ namespace gsr {
|
||||
restore_portal_session_checkbox_ptr->set_checked(config.screenshot_config.restore_portal_session);
|
||||
save_directory_button_ptr->set_text(config.screenshot_config.save_directory);
|
||||
save_screenshot_in_game_folder_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_in_game_folder);
|
||||
show_screenshot_saved_notification_checkbox_ptr->set_checked(config.screenshot_config.show_screenshot_saved_notifications);
|
||||
save_screenshot_to_clipboard_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_clipboard);
|
||||
save_screenshot_to_disk_checkbox_ptr->set_checked(config.screenshot_config.save_screenshot_to_disk);
|
||||
show_notification_checkbox_ptr->set_checked(config.screenshot_config.show_notifications);
|
||||
led_indicator_checkbox_ptr->set_checked(config.screenshot_config.use_led_indicator);
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
@@ -296,9 +354,13 @@ namespace gsr {
|
||||
if(config.screenshot_config.image_height < 32)
|
||||
config.screenshot_config.image_height = 32;
|
||||
image_height_entry_ptr->set_text(std::to_string(config.screenshot_config.image_height));
|
||||
|
||||
create_custom_script_screenshot_entry_ptr->set_text(config.screenshot_config.custom_script);
|
||||
}
|
||||
|
||||
void ScreenshotSettingsPage::save() {
|
||||
Config prev_config = config;
|
||||
|
||||
config.screenshot_config.record_area_option = record_area_box_ptr->get_selected_id();
|
||||
config.screenshot_config.image_width = atoi(image_width_entry_ptr->get_text().c_str());
|
||||
config.screenshot_config.image_height = atoi(image_height_entry_ptr->get_text().c_str());
|
||||
@@ -309,7 +371,11 @@ namespace gsr {
|
||||
config.screenshot_config.restore_portal_session = restore_portal_session_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.save_directory = save_directory_button_ptr->get_text();
|
||||
config.screenshot_config.save_screenshot_in_game_folder = save_screenshot_in_game_folder_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.show_screenshot_saved_notifications = show_screenshot_saved_notification_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.save_screenshot_to_clipboard = save_screenshot_to_clipboard_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.save_screenshot_to_disk = save_screenshot_to_disk_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.show_notifications = show_notification_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.use_led_indicator = led_indicator_checkbox_ptr->is_checked();
|
||||
config.screenshot_config.custom_script = create_custom_script_screenshot_entry_ptr->get_text();
|
||||
|
||||
if(config.screenshot_config.image_width == 0)
|
||||
config.screenshot_config.image_width = 1920;
|
||||
@@ -328,5 +394,8 @@ namespace gsr {
|
||||
}
|
||||
|
||||
save_config(config);
|
||||
|
||||
if(on_config_changed && config != prev_config)
|
||||
on_config_changed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,9 @@ namespace gsr {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(event.type == mgl::Event::MouseButtonPressed || event.type == mgl::Event::MouseButtonReleased) {
|
||||
// Pass release to children even if outside area, because we want to be able to release mouse when moved outside,
|
||||
// for example in Entry when selecting text
|
||||
if(event.type == mgl::Event::MouseButtonPressed/* || event.type == mgl::Event::MouseButtonReleased*/) {
|
||||
if(!mgl::IntRect(scissor_pos, scissor_size).contains({event.mouse_button.x, event.mouse_button.y}))
|
||||
return true;
|
||||
} else if(event.type == mgl::Event::MouseMoved) {
|
||||
|
||||
67
src/gui/Tooltip.cpp
Normal file
@@ -0,0 +1,67 @@
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
|
||||
#include <mglpp/graphics/Rectangle.hpp>
|
||||
#include <mglpp/graphics/Sprite.hpp>
|
||||
#include <mglpp/window/Window.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static const float padding_top_scale = 0.008f;
|
||||
static const float padding_bottom_scale = 0.008f;
|
||||
static const float padding_left_scale = 0.008f;
|
||||
static const float padding_right_scale = 0.008f;
|
||||
static const float accent_scale = 0.0025f;
|
||||
|
||||
Tooltip::Tooltip(mgl::Font *font) : label("", *font) {}
|
||||
|
||||
bool Tooltip::on_event(mgl::Event&, mgl::Window&, mgl::vec2f) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void Tooltip::draw(mgl::Window &window, mgl::vec2f offset) {
|
||||
if(!visible)
|
||||
return;
|
||||
|
||||
const mgl::vec2f draw_pos = (window.get_mouse_position().to_vec2f() + offset).floor();
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
mgl::Rectangle background(get_size());
|
||||
background.set_position(draw_pos - mgl::vec2f(0.0f, background.get_size().y));
|
||||
background.set_color(mgl::Color(0, 0, 0));
|
||||
window.draw(background);
|
||||
|
||||
mgl::Rectangle accent(background.get_position(), mgl::vec2f(background.get_size().x, accent_height).floor());
|
||||
accent.set_color(get_color_theme().tint_color);
|
||||
window.draw(accent);
|
||||
|
||||
mgl::Sprite icon_sprite(&get_theme().info_texture, background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top).floor());
|
||||
icon_sprite.set_height(icon_height);
|
||||
window.draw(icon_sprite);
|
||||
|
||||
label.set_position(background.get_position() + mgl::vec2f(padding_left, accent_height + padding_top + icon_sprite.get_size().y).floor());
|
||||
window.draw(label);
|
||||
}
|
||||
|
||||
mgl::vec2f Tooltip::get_size() {
|
||||
if(!visible)
|
||||
return {0.0f, 0.0f};
|
||||
|
||||
const int padding_top = get_theme().window_height * padding_top_scale;
|
||||
const int padding_bottom = get_theme().window_height * padding_bottom_scale;
|
||||
const int padding_left = get_theme().window_height * padding_left_scale;
|
||||
const int padding_right = get_theme().window_height * padding_right_scale;
|
||||
const int accent_height = get_theme().window_height * accent_scale;
|
||||
const mgl::vec2f text_size = label.get_bounds().size.floor();
|
||||
const int icon_height = label.get_font()->get_character_size();
|
||||
|
||||
return mgl::vec2f(padding_left + text_size.x + padding_right, accent_height + padding_top + icon_height + text_size.y + padding_bottom).floor();
|
||||
}
|
||||
|
||||
void Tooltip::set_text(std::string text) {
|
||||
label.set_string(std::move(text));
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,18 @@
|
||||
namespace gsr {
|
||||
static double frame_delta_seconds = 1.0;
|
||||
|
||||
mgl::vec2i min_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::min(a.x, b.x), std::min(a.y, b.y) };
|
||||
}
|
||||
|
||||
mgl::vec2i max_vec2i(mgl::vec2i a, mgl::vec2i b) {
|
||||
return { std::max(a.x, b.x), std::max(a.y, b.y) };
|
||||
}
|
||||
|
||||
mgl::vec2i clamp_vec2i(mgl::vec2i value, mgl::vec2i min, mgl::vec2i max) {
|
||||
return min_vec2i(max, max_vec2i(value, min));
|
||||
}
|
||||
|
||||
// TODO: Use vertices to make it one draw call
|
||||
void draw_rectangle_outline(mgl::Window &window, mgl::vec2f pos, mgl::vec2f size, mgl::Color color, float border_size) {
|
||||
pos = pos.floor();
|
||||
@@ -74,4 +86,12 @@ namespace gsr {
|
||||
else
|
||||
return from;
|
||||
}
|
||||
|
||||
mgl::Scissor scissor_get_sub_area(mgl::Scissor parent, mgl::Scissor child) {
|
||||
const mgl::vec2i pos = clamp_vec2i(child.position, parent.position, parent.position + parent.size);
|
||||
return mgl::Scissor{
|
||||
pos,
|
||||
max_vec2i(mgl::vec2i(0, 0), min_vec2i(child.position + child.size - pos, parent.position + parent.size - pos))
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
#include "../../include/gui/Widget.hpp"
|
||||
#include "../../include/gui/Tooltip.hpp"
|
||||
#include "../../include/Theme.hpp"
|
||||
#include <vector>
|
||||
|
||||
#include <mglpp/window/Event.hpp>
|
||||
|
||||
namespace gsr {
|
||||
static std::vector<std::unique_ptr<Widget>> widgets_to_remove;
|
||||
static Widget *current_tooltip_widget = nullptr;
|
||||
static std::unique_ptr<Tooltip> tooltip;
|
||||
|
||||
static void set_current_tooltip_text(Widget *widget);
|
||||
|
||||
Widget::Widget() {
|
||||
|
||||
@@ -10,6 +18,7 @@ namespace gsr {
|
||||
|
||||
Widget::~Widget() {
|
||||
remove_widget_as_selected_in_parent();
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
|
||||
void Widget::set_position(mgl::vec2f position) {
|
||||
@@ -64,6 +73,34 @@ namespace gsr {
|
||||
this->visible = visible;
|
||||
}
|
||||
|
||||
bool Widget::is_visible() const {
|
||||
return visible;
|
||||
}
|
||||
|
||||
Widget* Widget::get_parent_widget() {
|
||||
return parent_widget;
|
||||
}
|
||||
|
||||
void Widget::set_tooltip_text(std::string text) {
|
||||
tooltip_text = std::move(text);
|
||||
if(current_tooltip_widget == this)
|
||||
set_current_tooltip_text(current_tooltip_widget);
|
||||
}
|
||||
|
||||
const std::string& Widget::get_tooltip_text() const {
|
||||
return tooltip_text;
|
||||
}
|
||||
|
||||
void Widget::handle_tooltip_event(mgl::Event &event, mgl::vec2f position, mgl::vec2f size) {
|
||||
if(event.type == mgl::Event::MouseMoved) {
|
||||
if(mgl::FloatRect(position, size).contains(mgl::vec2f(event.mouse_move.x, event.mouse_move.y))) {
|
||||
set_current_tooltip(this);
|
||||
} else {
|
||||
remove_as_current_tooltip(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void add_widget_to_remove(std::unique_ptr<Widget> widget) {
|
||||
widgets_to_remove.push_back(std::move(widget));
|
||||
}
|
||||
@@ -74,4 +111,40 @@ namespace gsr {
|
||||
}
|
||||
widgets_to_remove.clear();
|
||||
}
|
||||
|
||||
void set_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
return;
|
||||
|
||||
set_current_tooltip_text(widget);
|
||||
}
|
||||
|
||||
void remove_as_current_tooltip(Widget *widget) {
|
||||
if(current_tooltip_widget == widget)
|
||||
set_current_tooltip_text(nullptr);
|
||||
}
|
||||
|
||||
void set_current_tooltip_text(Widget *widget) {
|
||||
if(widget && !widget->get_tooltip_text().empty()) {
|
||||
current_tooltip_widget = widget;
|
||||
if(!tooltip)
|
||||
tooltip = std::make_unique<Tooltip>(&get_theme().body_font);
|
||||
tooltip->set_text(current_tooltip_widget->get_tooltip_text());
|
||||
} else {
|
||||
current_tooltip_widget = nullptr;
|
||||
tooltip.reset();
|
||||
}
|
||||
}
|
||||
|
||||
void draw_tooltip(mgl::Window &window) {
|
||||
if(!tooltip)
|
||||
return;
|
||||
|
||||
if(!current_tooltip_widget->is_visible()) {
|
||||
set_current_tooltip(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
tooltip->draw(window, mgl::vec2f(0.0f, 0.0f));
|
||||
}
|
||||
}
|
||||
196
src/main.cpp
@@ -1,13 +1,17 @@
|
||||
#include "../include/GsrInfo.hpp"
|
||||
#include "../include/Overlay.hpp"
|
||||
#include "../include/Utils.hpp"
|
||||
#include "../include/gui/Utils.hpp"
|
||||
#include "../include/Process.hpp"
|
||||
#include "../include/Rpc.hpp"
|
||||
#include "../include/Theme.hpp"
|
||||
#include "../include/Translation.hpp"
|
||||
|
||||
#include <signal.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
#include <malloc.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <mglpp/mglpp.hpp>
|
||||
#include <mglpp/system/Clock.hpp>
|
||||
@@ -23,8 +27,10 @@ extern "C" {
|
||||
}
|
||||
|
||||
static sig_atomic_t running = 1;
|
||||
static sig_atomic_t killed = 0;
|
||||
static void sigint_handler(int signal) {
|
||||
(void)signal;
|
||||
killed = 1;
|
||||
running = 0;
|
||||
}
|
||||
|
||||
@@ -53,7 +59,7 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
|
||||
rpc->add_handler("toggle-record", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record();
|
||||
overlay->toggle_record(gsr::RecordForceType::NONE);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-pause", [overlay](const std::string &name) {
|
||||
@@ -61,6 +67,16 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
overlay->toggle_pause();
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-region", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::REGION);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-record-window", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_record(gsr::RecordForceType::WINDOW);
|
||||
});
|
||||
|
||||
rpc->add_handler("toggle-stream", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->toggle_stream();
|
||||
@@ -95,67 +111,13 @@ static void rpc_add_commands(gsr::Rpc *rpc, gsr::Overlay *overlay) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->take_screenshot_region();
|
||||
});
|
||||
|
||||
rpc->add_handler("take-screenshot-window", [overlay](const std::string &name) {
|
||||
fprintf(stderr, "rpc command executed: %s\n", name.c_str());
|
||||
overlay->take_screenshot_window();
|
||||
});
|
||||
}
|
||||
|
||||
static bool is_gsr_ui_virtual_keyboard_running() {
|
||||
FILE *f = fopen("/proc/bus/input/devices", "rb");
|
||||
if(!f)
|
||||
return false;
|
||||
|
||||
bool virtual_keyboard_running = false;
|
||||
char line[1024];
|
||||
while(fgets(line, sizeof(line), f)) {
|
||||
if(strstr(line, "gsr-ui virtual keyboard")) {
|
||||
virtual_keyboard_running = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return virtual_keyboard_running;
|
||||
}
|
||||
|
||||
static void install_flatpak_systemd_service() {
|
||||
const bool systemd_service_exists = system(
|
||||
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
|
||||
"flatpak-spawn --host -- ls \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0;
|
||||
if(systemd_service_exists)
|
||||
return;
|
||||
|
||||
bool service_install_successful = (system(
|
||||
"data_home=$(flatpak-spawn --host -- /bin/sh -c 'echo \"${XDG_DATA_HOME:-$HOME/.local/share}\"') && "
|
||||
"flatpak-spawn --host -- install -Dm644 /var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gpu-screen-recorder/gpu-screen-recorder-ui.service \"$data_home/systemd/user/gpu-screen-recorder-ui.service\"") == 0);
|
||||
service_install_successful &= (system("flatpak-spawn --host -- systemctl --user daemon-reload") == 0);
|
||||
if(service_install_successful)
|
||||
fprintf(stderr, "Info: the systemd service file was missing. It has now been installed\n");
|
||||
else
|
||||
fprintf(stderr, "Error: the systemd service file is missing and failed to install it again\n");
|
||||
}
|
||||
|
||||
static void remove_flatpak_systemd_service() {
|
||||
char systemd_service_path[PATH_MAX];
|
||||
const char *xdg_data_home = getenv("XDG_DATA_HOME");
|
||||
const char *home = getenv("HOME");
|
||||
if(xdg_data_home) {
|
||||
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/systemd/user/gpu-screen-recorder-ui.service", xdg_data_home);
|
||||
} else if(home) {
|
||||
snprintf(systemd_service_path, sizeof(systemd_service_path), "%s/.local/share/systemd/user/gpu-screen-recorder-ui.service", home);
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to get user home directory\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if(access(systemd_service_path, F_OK) != 0)
|
||||
return;
|
||||
|
||||
remove(systemd_service_path);
|
||||
system("systemctl --user daemon-reload");
|
||||
fprintf(stderr, "Info: conflicting flatpak version of the systemd service for gsr-ui was found at \"%s\", it has now been removed\n", systemd_service_path);
|
||||
}
|
||||
|
||||
static bool is_flatpak() {
|
||||
return getenv("FLATPAK_ID") != nullptr;
|
||||
}
|
||||
|
||||
static void set_display_server_environment_variables() {
|
||||
// Some users dont have properly setup environments (no display manager that does systemctl --user import-environment DISPLAY WAYLAND_DISPLAY)
|
||||
@@ -167,7 +129,7 @@ static void set_display_server_environment_variables() {
|
||||
|
||||
const char *wayland_display = getenv("WAYLAND_DISPLAY");
|
||||
if(!wayland_display) {
|
||||
wayland_display = "wayland-1";
|
||||
wayland_display = "wayland-0";
|
||||
setenv("WAYLAND_DISPLAY", wayland_display, true);
|
||||
}
|
||||
}
|
||||
@@ -175,9 +137,10 @@ static void set_display_server_environment_variables() {
|
||||
static void usage() {
|
||||
printf("usage: gsr-ui [action]\n");
|
||||
printf("OPTIONS:\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
|
||||
printf(" action The launch action. Should be either \"launch-show\", \"launch-hide\", \"launch-hide-announce\" or \"launch-daemon\". Optional, defaults to \"launch-hide\".\n");
|
||||
printf(" If \"launch-show\" is used then the program starts and the UI is immediately opened and can be shown/hidden with Alt+Z.\n");
|
||||
printf(" If \"launch-hide\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will be opened if the program is already running in another process.\n");
|
||||
printf(" If \"launch-hide-announce\" is used then the program starts but the UI is not opened until Alt+Z is pressed and a notification tells the user to press Alt+Z. The UI will be opened if the program is already running in another process.\n");
|
||||
printf(" If \"launch-daemon\" is used then the program starts but the UI is not opened until Alt+Z is pressed. The UI will not be opened if the program is already running in another process.\n");
|
||||
exit(1);
|
||||
}
|
||||
@@ -185,12 +148,30 @@ static void usage() {
|
||||
enum class LaunchAction {
|
||||
LAUNCH_SHOW,
|
||||
LAUNCH_HIDE,
|
||||
LAUNCH_DAEMON
|
||||
LAUNCH_HIDE_ANNOUNCE,
|
||||
LAUNCH_DAEMON,
|
||||
INSTALL_STARTUP
|
||||
};
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
setlocale(LC_ALL, "C"); // Sigh... stupid C
|
||||
#ifdef __GLIBC__
|
||||
mallopt(M_MMAP_THRESHOLD, 65536);
|
||||
#endif
|
||||
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
std::optional<gsr::Config> config = read_config(gsr::SupportedCaptureOptions{});
|
||||
gsr::Translation::instance().init((resources_path + "translations/").c_str(), config.has_value() ? config.value().main_config.language.c_str() : nullptr);
|
||||
|
||||
if(geteuid() == 0) {
|
||||
fprintf(stderr, "Error: don't run gsr-ui as the root user\n");
|
||||
@@ -206,40 +187,55 @@ int main(int argc, char **argv) {
|
||||
launch_action = LaunchAction::LAUNCH_SHOW;
|
||||
} else if(strcmp(launch_action_opt, "launch-hide") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_HIDE;
|
||||
} else if(strcmp(launch_action_opt, "launch-hide-announce") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_HIDE_ANNOUNCE;
|
||||
} else if(strcmp(launch_action_opt, "launch-daemon") == 0) {
|
||||
launch_action = LaunchAction::LAUNCH_DAEMON;
|
||||
} else if(strcmp(launch_action_opt, "install-startup") == 0) {
|
||||
launch_action = LaunchAction::INSTALL_STARTUP;
|
||||
} else {
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
printf("error: invalid action \"%s\", expected \"launch-show\", \"launch-hide\", \"launch-hide-announce\" or \"launch-daemon\".\n", launch_action_opt);
|
||||
usage();
|
||||
}
|
||||
} else {
|
||||
usage();
|
||||
}
|
||||
|
||||
if(launch_action == LaunchAction::INSTALL_STARTUP)
|
||||
return gsr::set_xdg_autostart(true);
|
||||
|
||||
set_display_server_environment_variables();
|
||||
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
const bool rpc_created = rpc->create("gsr-ui");
|
||||
if(!rpc_created)
|
||||
fprintf(stderr, "Error: Failed to create rpc\n");
|
||||
const std::string gsr_icon_path = resources_path + "images/gpu_screen_recorder_logo.png";
|
||||
|
||||
if(is_gsr_ui_virtual_keyboard_running() || !rpc_created) {
|
||||
auto rpc = std::make_unique<gsr::Rpc>();
|
||||
const gsr::RpcOpenResult rpc_open_result = rpc->open("gsr-ui");
|
||||
|
||||
if(rpc_open_result == gsr::RpcOpenResult::OK) {
|
||||
if(launch_action == LaunchAction::LAUNCH_DAEMON)
|
||||
return 1;
|
||||
|
||||
rpc = std::make_unique<gsr::Rpc>();
|
||||
if(rpc->open("gsr-ui") && rpc->write("show_ui\n", 8)) {
|
||||
if(rpc->write("show_ui\n", 8)) {
|
||||
fprintf(stderr, "Error: another instance of gsr-ui is already running, opening that one instead\n");
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to send command to running gsr-ui instance, user will have to open the UI manually with Alt+Z\n");
|
||||
const char *args[] = { "gsr-notify", "--text", "Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
const char *args[] = {
|
||||
"gsr-notify", "--text", TR("Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI."), "--timeout", "5.0",
|
||||
"--icon-color", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(!rpc->create("gsr-ui"))
|
||||
fprintf(stderr, "Error: Failed to create rpc\n");
|
||||
|
||||
if(gsr::pidof("gpu-screen-recorder", -1) != -1) {
|
||||
const char *args[] = { "gsr-notify", "--text", "GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.", "--timeout", "5.0", "--icon-color", "ff0000", "--bg-color", "ff0000", nullptr };
|
||||
const char *args[] = {
|
||||
"gsr-notify", "--text", TR("GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI."),
|
||||
"--timeout", "5.0", "--icon-color", "ffffff", "--icon", gsr_icon_path.c_str(), "--bg-color", "ff0000", nullptr
|
||||
};
|
||||
gsr::exec_program_daemonized(args);
|
||||
}
|
||||
|
||||
@@ -248,11 +244,6 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(is_flatpak())
|
||||
install_flatpak_systemd_service();
|
||||
else
|
||||
remove_flatpak_systemd_service();
|
||||
|
||||
// Stop nvidia driver from buffering frames
|
||||
setenv("__GL_MaxFramesAllowed", "1", true);
|
||||
// If this is set to 1 then cuGraphicsGLRegisterImage will fail for egl context with error: invalid OpenGL or DirectX context,
|
||||
@@ -293,17 +284,6 @@ int main(int argc, char **argv) {
|
||||
|
||||
gsr::SupportedCaptureOptions capture_options = gsr::get_supported_capture_options(gsr_info);
|
||||
|
||||
std::string resources_path;
|
||||
if(access("sibs-build/linux_x86_64/debug/gsr-ui", F_OK) == 0) {
|
||||
resources_path = "./";
|
||||
} else {
|
||||
#ifdef GSR_UI_RESOURCES_PATH
|
||||
resources_path = GSR_UI_RESOURCES_PATH "/";
|
||||
#else
|
||||
resources_path = "/usr/share/gsr-ui/";
|
||||
#endif
|
||||
}
|
||||
|
||||
mgl_context *context = mgl_get_context();
|
||||
|
||||
egl_functions egl_funcs;
|
||||
@@ -322,9 +302,36 @@ int main(int argc, char **argv) {
|
||||
auto overlay = std::make_unique<gsr::Overlay>(resources_path, std::move(gsr_info), std::move(capture_options), egl_funcs);
|
||||
if(launch_action == LaunchAction::LAUNCH_SHOW)
|
||||
overlay->show();
|
||||
else if(launch_action == LaunchAction::LAUNCH_HIDE_ANNOUNCE)
|
||||
overlay->show_notification("Press Alt+Z to open the GPU Screen Recorder UI", 5.0, mgl::Color(255, 255, 255), gsr::get_color_theme().tint_color, gsr::NotificationType::NOTICE, nullptr, gsr::NotificationLevel::ERROR);
|
||||
|
||||
rpc_add_commands(rpc.get(), overlay.get());
|
||||
|
||||
// Evacuating from lennart's botnet to XDG (a.k.a. 'the spec that nobody actually follows').
|
||||
// TODO: Remove all this garbage related to systemd in 1-2 months and remove the systemd service file as well.
|
||||
// This garbage shit is needed because the systemd user daemon isn't always available at xdg autostart o algo
|
||||
const bool systemd_service_ready = gsr::wait_until_systemd_user_service_available();
|
||||
constexpr const char *deprecated_systemd_service_name = "gpu-screen-recorder-ui.service";
|
||||
if(systemd_service_ready && gsr::is_systemd_service_enabled(deprecated_systemd_service_name)) {
|
||||
const int autostart_result = gsr::set_xdg_autostart(true);
|
||||
if(autostart_result == 67) {
|
||||
const bool is_flatpak = getenv("FLATPAK_ID") != nullptr;
|
||||
const char *startup_command = is_flatpak ? "flatpak run com.dec05eba.gpu_screen_recorder gsr-ui" : "gsr-ui launch-daemon";
|
||||
overlay->show_notification(
|
||||
TRF("GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.", startup_command).c_str(),
|
||||
10.0, mgl::Color(255, 255, 255), mgl::Color(255, 0, 0),
|
||||
gsr::NotificationType::NOTICE, nullptr, gsr::NotificationLevel::ERROR);
|
||||
} else {
|
||||
gsr::disable_systemd_service(deprecated_systemd_service_name);
|
||||
overlay->show_notification(
|
||||
TR("GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart."),
|
||||
8.0, mgl::Color(255, 255, 255), gsr::get_color_theme().tint_color,
|
||||
gsr::NotificationType::NOTICE, nullptr, gsr::NotificationLevel::INFO);
|
||||
}
|
||||
}
|
||||
|
||||
gsr::replace_xdg_autostart_with_current_gsr_type();
|
||||
|
||||
// TODO: Add hotkeys in Overlay when using x11 global hotkeys. The hotkeys in Overlay should duplicate each key that is used for x11 global hotkeys.
|
||||
|
||||
std::string exit_reason;
|
||||
@@ -342,12 +349,15 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
}
|
||||
|
||||
const bool connected_to_display_server = mgl_is_connected_to_display_server();
|
||||
|
||||
fprintf(stderr, "Info: shutting down!\n");
|
||||
rpc.reset();
|
||||
overlay.reset();
|
||||
mgl_deinit();
|
||||
|
||||
if(exit_reason == "back-to-old-ui") {
|
||||
gsr::set_xdg_autostart(false);
|
||||
const char *args[] = { "gpu-screen-recorder-gtk", "use-old-ui", nullptr };
|
||||
execvp(args[0], (char* const*)args);
|
||||
return 0;
|
||||
@@ -355,5 +365,11 @@ int main(int argc, char **argv) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return mgl_is_connected_to_display_server() ? 0 : 1;
|
||||
if(killed)
|
||||
return 0;
|
||||
|
||||
if(connected_to_display_server)
|
||||
return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -24,4 +24,9 @@ To close gsr-global-hotkeys send `exit<newline>` to the programs stdin, for exam
|
||||
```
|
||||
exit
|
||||
|
||||
```
|
||||
```
|
||||
# Conflict with other keyboard software
|
||||
Some keyboard remapping software such as keyd may conflict with gsr-global-hotkeys if configured incorrect (if it's configured to grab all devices, including gsr-ui virtual keyboard).
|
||||
If that happens it may grab gsr-ui-virtual keyboard while gsr-global-hotkeys will grab the keyboard remapping software virtual device, leading to a circular lock, making it not possible
|
||||
to use your keyboard. gsr-global-hotkeys detects this and outputs `gsr-ui-virtual-keyboard-grabbed` to stdout. You can listen to this and stop gsr-global-hotkeys or restart it with `--no-grab`
|
||||
option to only listen to devices, not grabbing them.
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "keyboard_event.h"
|
||||
#include "keys.h"
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdio.h>
|
||||
@@ -7,6 +8,7 @@
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <time.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <fcntl.h>
|
||||
@@ -17,6 +19,7 @@
|
||||
/* LINUX */
|
||||
#include <linux/input.h>
|
||||
#include <linux/uinput.h>
|
||||
#include <sys/timerfd.h>
|
||||
|
||||
#define GSR_UI_VIRTUAL_KEYBOARD_NAME "gsr-ui virtual keyboard"
|
||||
|
||||
@@ -26,6 +29,14 @@
|
||||
|
||||
#define KEY_STATES_SIZE (KEY_MAX/8 + 1)
|
||||
|
||||
static double clock_get_monotonic_seconds(void) {
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = 0;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (double)ts.tv_sec + (double)ts.tv_nsec * 0.000000001;
|
||||
}
|
||||
|
||||
static inline int count_num_bits_set(unsigned char c) {
|
||||
int n = 0;
|
||||
n += (c & 1);
|
||||
@@ -183,6 +194,15 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
return;
|
||||
}
|
||||
|
||||
if(extra_data->gsr_ui_virtual_keyboard) {
|
||||
if(event.type == EV_KEY || event.type == EV_MSC)
|
||||
self->check_grab_lock = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if(extra_data->is_non_keyboard_device)
|
||||
return;
|
||||
|
||||
if(event.type == EV_SYN && event.code == SYN_DROPPED) {
|
||||
/* TODO: Don't do this on every SYN_DROPPED to prevent spamming this, instead wait until the next event or wait for timeout */
|
||||
keyboard_event_fetch_update_key_states(self, extra_data, fd);
|
||||
@@ -193,6 +213,8 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
//fprintf(stderr, "fd: %d, type: %d, pressed %d, value: %d\n", fd, event.type, event.code, event.value);
|
||||
//}
|
||||
|
||||
const bool prev_grabbed = extra_data->grabbed;
|
||||
|
||||
const bool keyboard_key = is_keyboard_key(event.code);
|
||||
if(event.type == EV_KEY && keyboard_key) {
|
||||
keyboard_event_process_key_state_change(self, &event, extra_data, fd);
|
||||
@@ -211,6 +233,11 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
}
|
||||
|
||||
if(extra_data->grabbed) {
|
||||
if(prev_grabbed && !self->check_grab_lock && (event.type == EV_KEY || event.type == EV_MSC)) {
|
||||
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
|
||||
self->check_grab_lock = true;
|
||||
}
|
||||
|
||||
/* TODO: What to do on error? */
|
||||
if(write(self->uinput_fd, &event, sizeof(event)) != sizeof(event))
|
||||
fprintf(stderr, "Error: failed to write event data to virtual keyboard for exclusively grabbed device\n");
|
||||
@@ -234,14 +261,23 @@ static void keyboard_event_process_input_event_data(keyboard_event *self, event_
|
||||
/* Retarded linux takes very long time to close /dev/input/eventN files, even though they are virtual and opened read-only */
|
||||
static void* keyboard_event_close_fds_callback(void *userdata) {
|
||||
keyboard_event *self = userdata;
|
||||
int fds_to_close_now[MAX_CLOSE_FDS];
|
||||
int num_fds_to_close_now = 0;
|
||||
|
||||
while(self->running) {
|
||||
pthread_mutex_lock(&self->close_dev_input_mutex);
|
||||
for(int i = 0; i < self->num_close_fds; ++i) {
|
||||
close(self->close_fds[i]);
|
||||
fds_to_close_now[i] = self->close_fds[i];
|
||||
}
|
||||
num_fds_to_close_now = self->num_close_fds;
|
||||
self->num_close_fds = 0;
|
||||
pthread_mutex_unlock(&self->close_dev_input_mutex);
|
||||
|
||||
for(int i = 0; i < num_fds_to_close_now; ++i) {
|
||||
close(fds_to_close_now[i]);
|
||||
}
|
||||
num_fds_to_close_now = 0;
|
||||
|
||||
usleep(100 * 1000); /* 100 milliseconds */
|
||||
}
|
||||
return NULL;
|
||||
@@ -359,7 +395,7 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
|
||||
if(keyboard_event_has_event_with_dev_input_fd(self, dev_input_id))
|
||||
return false;
|
||||
|
||||
const int fd = open(dev_input_filepath, O_RDONLY);
|
||||
const int fd = open(dev_input_filepath, O_RDWR);
|
||||
if(fd == -1)
|
||||
return false;
|
||||
|
||||
@@ -371,7 +407,29 @@ static bool keyboard_event_try_add_device_if_keyboard(keyboard_event *self, cons
|
||||
ioctl(fd, EVIOCGBIT(0, sizeof(evbit)), &evbit);
|
||||
const bool is_keyboard = (evbit & (1 << EV_SYN)) && (evbit & (1 << EV_KEY));
|
||||
|
||||
if(is_keyboard && strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) != 0) {
|
||||
if(strcmp(device_name, GSR_UI_VIRTUAL_KEYBOARD_NAME) == 0) {
|
||||
if(self->num_event_polls < MAX_EVENT_POLLS) {
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = fd,
|
||||
.events = POLLIN,
|
||||
.revents = 0
|
||||
};
|
||||
|
||||
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
|
||||
.dev_input_id = dev_input_id,
|
||||
.grabbed = false,
|
||||
.key_states = NULL,
|
||||
.key_presses_grabbed = NULL,
|
||||
.num_keys_pressed = 0,
|
||||
.gsr_ui_virtual_keyboard = true
|
||||
};
|
||||
|
||||
++self->num_event_polls;
|
||||
return true;
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to listen to gsr-ui virtual keyboard\n");
|
||||
}
|
||||
} else if(is_keyboard) {
|
||||
unsigned char key_bits[KEY_MAX/8 + 1] = {0};
|
||||
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(key_bits)), &key_bits);
|
||||
|
||||
@@ -456,9 +514,13 @@ static void keyboard_event_remove_event(keyboard_event *self, int index) {
|
||||
if(index < 0 || index >= self->num_event_polls)
|
||||
return;
|
||||
|
||||
if(self->event_polls[index].fd > 0) {
|
||||
ioctl(self->event_polls[index].fd, EVIOCGRAB, 0);
|
||||
close(self->event_polls[index].fd);
|
||||
const int poll_fd = self->event_polls[index].fd;
|
||||
if(poll_fd > 0) {
|
||||
ioctl(poll_fd, EVIOCGRAB, 0);
|
||||
if(!keyboard_event_try_add_close_fd(self, poll_fd)) {
|
||||
fprintf(stderr, "Error: failed to add immediately, closing now\n");
|
||||
close(poll_fd);
|
||||
}
|
||||
}
|
||||
free(self->event_extra_data[index].key_states);
|
||||
free(self->event_extra_data[index].key_presses_grabbed);
|
||||
@@ -497,10 +559,12 @@ static int setup_virtual_keyboard_input(const char *name) {
|
||||
if(is_keyboard_key(i) || is_mouse_button(i))
|
||||
success &= (ioctl(fd, UI_SET_KEYBIT, i) != -1);
|
||||
}
|
||||
|
||||
for(int i = 0; i < REL_MAX; ++i) {
|
||||
success &= (ioctl(fd, UI_SET_RELBIT, i) != -1);
|
||||
}
|
||||
// for(int i = 0; i < LED_MAX; ++i) {
|
||||
|
||||
// for(int i = 0; i <= LED_CHARGING; ++i) {
|
||||
// success &= (ioctl(fd, UI_SET_LEDBIT, i) != -1);
|
||||
// }
|
||||
|
||||
@@ -539,7 +603,6 @@ static int setup_virtual_keyboard_input(const char *name) {
|
||||
|
||||
bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_grab_type grab_type) {
|
||||
memset(self, 0, sizeof(*self));
|
||||
self->stdin_event_index = -1;
|
||||
self->hotplug_event_index = -1;
|
||||
self->grab_type = grab_type;
|
||||
self->running = true;
|
||||
@@ -571,9 +634,51 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
|
||||
.num_keys_pressed = 0
|
||||
};
|
||||
|
||||
self->stdin_event_index = self->num_event_polls;
|
||||
++self->num_event_polls;
|
||||
|
||||
self->timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);
|
||||
if(self->timer_fd <= 0) {
|
||||
fprintf(stderr, "Error: timerfd_create failed\n");
|
||||
keyboard_event_deinit(self);
|
||||
return false;
|
||||
}
|
||||
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = self->timer_fd,
|
||||
.events = POLLIN,
|
||||
.revents = 0
|
||||
};
|
||||
|
||||
self->event_extra_data[self->num_event_polls] = (event_extra_data) {
|
||||
.dev_input_id = -1,
|
||||
.grabbed = false,
|
||||
.key_states = NULL,
|
||||
.key_presses_grabbed = NULL,
|
||||
.num_keys_pressed = 0
|
||||
};
|
||||
|
||||
++self->num_event_polls;
|
||||
|
||||
/* 0.5 seconds */
|
||||
const struct itimerspec timer_value = {
|
||||
.it_value = (struct timespec) {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = 500000000ULL,
|
||||
},
|
||||
.it_interval = (struct timespec) {
|
||||
.tv_sec = 0,
|
||||
.tv_nsec = 500000000ULL
|
||||
}
|
||||
};
|
||||
|
||||
if(timerfd_settime(self->timer_fd, 0, &timer_value, NULL) < 0) {
|
||||
fprintf(stderr, "Error: timerfd_settime failed\n");
|
||||
keyboard_event_deinit(self);
|
||||
return false;
|
||||
}
|
||||
|
||||
self->uinput_written_time_seconds = clock_get_monotonic_seconds();
|
||||
|
||||
if(hotplug_event_init(&self->hotplug_ev)) {
|
||||
self->event_polls[self->num_event_polls] = (struct pollfd) {
|
||||
.fd = hotplug_event_steal_fd(&self->hotplug_ev),
|
||||
@@ -606,6 +711,41 @@ bool keyboard_event_init(keyboard_event *self, bool exclusive_grab, keyboard_gra
|
||||
return true;
|
||||
}
|
||||
|
||||
static void write_led_data_to_device(int fd, uint16_t led, int value) {
|
||||
struct input_event led_data = {
|
||||
.type = EV_LED,
|
||||
.code = led,
|
||||
.value = value
|
||||
};
|
||||
write(fd, &led_data, sizeof(led_data));
|
||||
|
||||
struct input_event syn_data = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(fd, &syn_data, sizeof(syn_data));
|
||||
}
|
||||
|
||||
/* When the device is ungrabbed the leds are unset for some reason. Set them back to their previous brightness */
|
||||
static void keyboard_event_device_deinit(int fd, event_extra_data *extra_data) {
|
||||
ggh_leds leds;
|
||||
const bool got_leds = get_leds(extra_data->dev_input_id, &leds);
|
||||
|
||||
ioctl(fd, EVIOCGRAB, 0);
|
||||
if(got_leds) {
|
||||
if(leds.scroll_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_SCROLLL, leds.scroll_lock_brightness);
|
||||
|
||||
if(leds.num_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_NUML, leds.num_lock_brightness);
|
||||
|
||||
if(leds.caps_lock_brightness >= 0)
|
||||
write_led_data_to_device(fd, LED_CAPSL, leds.caps_lock_brightness);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void keyboard_event_deinit(keyboard_event *self) {
|
||||
self->running = false;
|
||||
|
||||
@@ -622,8 +762,10 @@ void keyboard_event_deinit(keyboard_event *self) {
|
||||
|
||||
for(int i = 0; i < self->num_event_polls; ++i) {
|
||||
if(self->event_polls[i].fd > 0) {
|
||||
ioctl(self->event_polls[i].fd, EVIOCGRAB, 0);
|
||||
close(self->event_polls[i].fd);
|
||||
if(self->event_extra_data[i].dev_input_id > 0 && !self->event_extra_data[i].gsr_ui_virtual_keyboard)
|
||||
keyboard_event_device_deinit(self->event_polls[i].fd, &self->event_extra_data[i]);
|
||||
else
|
||||
close(self->event_polls[i].fd);
|
||||
}
|
||||
free(self->event_extra_data[i].key_states);
|
||||
free(self->event_extra_data[i].key_presses_grabbed);
|
||||
@@ -785,7 +927,7 @@ static void keyboard_event_process_stdin_command_data(keyboard_event *self, int
|
||||
return;
|
||||
|
||||
const char *command_start = self->stdin_command_data;
|
||||
const char *search = self->stdin_command_data + self->stdin_command_data_size;
|
||||
char *search = self->stdin_command_data + self->stdin_command_data_size;
|
||||
const char *end = search + bytes_read;
|
||||
self->stdin_command_data_size += bytes_read;
|
||||
|
||||
@@ -817,7 +959,7 @@ void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds)
|
||||
return;
|
||||
|
||||
for(int i = 0; i < self->num_event_polls; ++i) {
|
||||
if(i == self->stdin_event_index && (self->event_polls[i].revents & (POLLHUP|POLLERR)))
|
||||
if(self->event_polls[i].fd == STDIN_FILENO && (self->event_polls[i].revents & (POLLHUP|POLLERR)))
|
||||
self->stdin_failed = true;
|
||||
|
||||
if(self->event_polls[i].revents & POLLHUP) { /* TODO: What if this is the hotplug fd? */
|
||||
@@ -826,17 +968,30 @@ void keyboard_event_poll_events(keyboard_event *self, int timeout_milliseconds)
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!(self->event_polls[i].revents & POLLIN))
|
||||
if(!(self->event_polls[i].revents & POLLIN)) {
|
||||
self->event_polls[i].revents = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if(i == self->hotplug_event_index) {
|
||||
/* Device is added to end of |event_polls| so it's ok to add while iterating it via index */
|
||||
hotplug_event_process_event_data(&self->hotplug_ev, self->event_polls[i].fd, on_device_added_callback, self);
|
||||
} else if(i == self->stdin_event_index) {
|
||||
} else if(self->event_polls[i].fd == STDIN_FILENO) {
|
||||
keyboard_event_process_stdin_command_data(self, self->event_polls[i].fd);
|
||||
} else if(self->event_polls[i].fd == self->timer_fd) {
|
||||
uint64_t timers_elapsed = 0;
|
||||
read(self->timer_fd, &timers_elapsed, sizeof(timers_elapsed));
|
||||
|
||||
if(self->grab_type != KEYBOARD_GRAB_TYPE_NO_GRAB && self->check_grab_lock && clock_get_monotonic_seconds() - self->uinput_written_time_seconds >= 1.5) {
|
||||
self->check_grab_lock = false;
|
||||
puts("gsr-ui-virtual-keyboard-grabbed");
|
||||
fflush(stdout);
|
||||
}
|
||||
} else {
|
||||
keyboard_event_process_input_event_data(self, &self->event_extra_data[i], self->event_polls[i].fd);
|
||||
}
|
||||
|
||||
self->event_polls[i].revents = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
#define MAX_EVENT_POLLS 32
|
||||
#define MAX_CLOSE_FDS 256
|
||||
#define MAX_GLOBAL_HOTKEYS 32
|
||||
#define MAX_GLOBAL_HOTKEYS 40
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_MODKEY_LALT = 1 << 0,
|
||||
@@ -41,6 +41,7 @@ typedef struct {
|
||||
bool grabbed;
|
||||
bool is_non_keyboard_device;
|
||||
bool is_possibly_non_keyboard_device;
|
||||
bool gsr_ui_virtual_keyboard;
|
||||
unsigned char *key_states;
|
||||
unsigned char *key_presses_grabbed;
|
||||
int num_keys_pressed;
|
||||
@@ -48,7 +49,8 @@ typedef struct {
|
||||
|
||||
typedef enum {
|
||||
KEYBOARD_GRAB_TYPE_ALL,
|
||||
KEYBOARD_GRAB_TYPE_VIRTUAL
|
||||
KEYBOARD_GRAB_TYPE_VIRTUAL,
|
||||
KEYBOARD_GRAB_TYPE_NO_GRAB
|
||||
} keyboard_grab_type;
|
||||
|
||||
typedef struct {
|
||||
@@ -62,10 +64,12 @@ typedef struct {
|
||||
event_extra_data event_extra_data[MAX_EVENT_POLLS]; /* Current size is |num_event_polls| */
|
||||
int num_event_polls;
|
||||
|
||||
int stdin_event_index;
|
||||
int hotplug_event_index;
|
||||
int uinput_fd;
|
||||
int timer_fd;
|
||||
bool stdin_failed;
|
||||
bool check_grab_lock;
|
||||
double uinput_written_time_seconds;
|
||||
keyboard_grab_type grab_type;
|
||||
|
||||
pthread_t close_dev_input_fds_thread;
|
||||
|
||||
181
tools/gsr-global-hotkeys/leds.c
Normal file
@@ -0,0 +1,181 @@
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <limits.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
|
||||
/* LINUX */
|
||||
#include <linux/input.h>
|
||||
|
||||
/* Returns -1 on error */
|
||||
static int read_int_from_file(const char *filepath) {
|
||||
const int fd = open(filepath, O_RDONLY);
|
||||
if(fd == -1) {
|
||||
fprintf(stderr, "Warning: get_max_brightness open error: %s\n", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
int value = 0;
|
||||
char buffer[32];
|
||||
const ssize_t num_bytes_read = read(fd, buffer, sizeof(buffer));
|
||||
if(num_bytes_read > 0) {
|
||||
buffer[num_bytes_read] = '\0';
|
||||
success = sscanf(buffer, "%d", &value) == 1;
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return success ? value : -1;
|
||||
}
|
||||
|
||||
static int get_max_brightness(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/max_brightness", sys_class_path, filename);
|
||||
return read_int_from_file(path);
|
||||
}
|
||||
|
||||
static int get_brightness(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/brightness", sys_class_path, filename);
|
||||
return read_int_from_file(path);
|
||||
}
|
||||
|
||||
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 int sys_class_led_path_get_event_number(const char *sys_class_path, const char *filename) {
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "%s/%s/device", sys_class_path, filename);
|
||||
|
||||
DIR *dir = opendir(path);
|
||||
if(!dir)
|
||||
return -1;
|
||||
|
||||
int event_number = -1;
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
int v = -1;
|
||||
if(sscanf(entry->d_name, "event%d", &v) == 1 && v >= 0) {
|
||||
event_number = v;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return event_number;
|
||||
}
|
||||
|
||||
/*
|
||||
We have to do this retardation instead of setting /sys/class/leds brightness since it doesn't work with /dev/uinput
|
||||
and we cant loop all /dev/input devices and open and write to them either since closing a /dev/input is very slow on linux.
|
||||
So we instead check which devices have the led before opening it.
|
||||
*/
|
||||
static bool set_device_leds(const char *led_name_path, bool enabled) {
|
||||
DIR *dir = opendir("/sys/class/leds");
|
||||
if(!dir)
|
||||
return false;
|
||||
|
||||
char dev_input_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;
|
||||
|
||||
const int event_number = sys_class_led_path_get_event_number("/sys/class/leds", entry->d_name);
|
||||
if(event_number == -1)
|
||||
continue;
|
||||
|
||||
snprintf(dev_input_filepath, sizeof(dev_input_filepath), "/dev/input/event%d", event_number);
|
||||
|
||||
const int device_fd = open(dev_input_filepath, O_WRONLY);
|
||||
if(device_fd == -1)
|
||||
continue;
|
||||
|
||||
int brightness = 0;
|
||||
if(enabled) {
|
||||
brightness = get_max_brightness("/sys/class/leds", entry->d_name);
|
||||
if(brightness < 0)
|
||||
brightness = 1;
|
||||
}
|
||||
|
||||
struct input_event led_data = {
|
||||
.type = EV_LED,
|
||||
.code = LED_SCROLLL,
|
||||
.value = brightness
|
||||
};
|
||||
write(device_fd, &led_data, sizeof(led_data));
|
||||
|
||||
struct input_event syn_data = {
|
||||
.type = EV_SYN,
|
||||
.code = 0,
|
||||
.value = 0
|
||||
};
|
||||
write(device_fd, &syn_data, sizeof(syn_data));
|
||||
|
||||
close(device_fd);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool set_leds(const char *led_name, bool enabled) {
|
||||
if(strcmp(led_name, "Scroll Lock") == 0) {
|
||||
return set_device_leds("::scrolllock", enabled);
|
||||
} else {
|
||||
fprintf(stderr, "Error: invalid led: \"%s\", expected \"Scroll Lock\"\n", led_name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool get_leds(int event_number, ggh_leds *leds) {
|
||||
leds->scroll_lock_brightness = -1;
|
||||
leds->num_lock_brightness = -1;
|
||||
leds->caps_lock_brightness = -1;
|
||||
|
||||
char path[PATH_MAX];
|
||||
snprintf(path, sizeof(path), "/sys/class/input/event%d/device", event_number);
|
||||
|
||||
DIR *dir = opendir(path);
|
||||
if(!dir)
|
||||
return false;
|
||||
|
||||
struct dirent *entry;
|
||||
while((entry = readdir(dir)) != NULL) {
|
||||
if(entry->d_name[0] == '.')
|
||||
continue;
|
||||
|
||||
if(!string_starts_with(entry->d_name, "input"))
|
||||
continue;
|
||||
|
||||
if(string_ends_with(entry->d_name, "::scrolllock"))
|
||||
leds->scroll_lock_brightness = get_brightness(path, entry->d_name);
|
||||
else if(string_ends_with(entry->d_name, "::numlock"))
|
||||
leds->num_lock_brightness = get_brightness(path, entry->d_name);
|
||||
else if(string_ends_with(entry->d_name, "::capslock"))
|
||||
leds->caps_lock_brightness = get_brightness(path, entry->d_name);
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return true;
|
||||
}
|
||||
17
tools/gsr-global-hotkeys/leds.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#ifndef LEDS_H
|
||||
#define LEDS_H
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct {
|
||||
/* These are set to -1 if not supported */
|
||||
int scroll_lock_brightness;
|
||||
int num_lock_brightness;
|
||||
int caps_lock_brightness;
|
||||
} ggh_leds;
|
||||
|
||||
bool set_leds(const char *led_name, bool enabled);
|
||||
bool get_leds(int event_number, ggh_leds *leds);
|
||||
|
||||
#endif /* LEDS_H */
|
||||
@@ -1,18 +1,26 @@
|
||||
#include "keyboard_event.h"
|
||||
#include "leds.h"
|
||||
|
||||
/* C stdlib */
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <locale.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* POSIX */
|
||||
#include <unistd.h>
|
||||
|
||||
static void usage(void) {
|
||||
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual]\n");
|
||||
fprintf(stderr, "usage: gsr-global-hotkeys [--all|--virtual|--no-grab|--set-led] [Scroll Lock] [on|off]\n");
|
||||
fprintf(stderr, "OPTIONS:\n");
|
||||
fprintf(stderr, " --all Grab all devices.\n");
|
||||
fprintf(stderr, " --virtual Grab all virtual devices only.\n");
|
||||
fprintf(stderr, " --no-grab Don't grab devices, only listen to them.\n");
|
||||
fprintf(stderr, " --set-led Turn device led on/off.\n");
|
||||
fprintf(stderr, "EXAMPLES:\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --all\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" on\n");
|
||||
fprintf(stderr, " gsr-global-hotkeys --set-led \"Scroll Lock\" off\n");
|
||||
}
|
||||
|
||||
static bool is_gsr_global_hotkeys_already_running(void) {
|
||||
@@ -37,17 +45,52 @@ int main(int argc, char **argv) {
|
||||
setlocale(LC_ALL, "C"); /* Sigh... stupid C */
|
||||
|
||||
keyboard_grab_type grab_type = KEYBOARD_GRAB_TYPE_ALL;
|
||||
const uid_t user_id = getuid();
|
||||
|
||||
if(argc == 2) {
|
||||
const char *grab_type_arg = argv[1];
|
||||
if(strcmp(grab_type_arg, "--all") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_ALL;
|
||||
} else if(strcmp(grab_type_arg, "--virtual") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_VIRTUAL;
|
||||
} else if(strcmp(grab_type_arg, "--no-grab") == 0) {
|
||||
grab_type = KEYBOARD_GRAB_TYPE_NO_GRAB;
|
||||
} else if(strcmp(grab_type_arg, "--set-led") == 0) {
|
||||
fprintf(stderr, "Error: missing led name and on/off argument to --set-led\n");
|
||||
usage();
|
||||
return 1;
|
||||
} else {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected --all or --virtual, got %s\n", grab_type_arg);
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected --all, --virtual, --no-grab or --set-led, got %s\n", grab_type_arg);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
} else if(argc == 4) {
|
||||
/* It's not ideal to use gsr-global-hotkeys for leds, but we do that for now because it's a mess to create another binary for flatpak and distros */
|
||||
const char *led_name = argv[2];
|
||||
const char *led_enabled_str = argv[3];
|
||||
bool led_enabled = false;
|
||||
|
||||
if(strcmp(led_enabled_str, "on") == 0) {
|
||||
led_enabled = true;
|
||||
} else if(strcmp(led_enabled_str, "off") == 0) {
|
||||
led_enabled = false;
|
||||
} else {
|
||||
fprintf(stderr, "Error: expected \"on\" or \"off\" for --set-led option, got: \"%s\"", led_enabled_str);
|
||||
usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if(geteuid() != 0) {
|
||||
if(setuid(0) == -1) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
const bool success = set_leds(led_name, led_enabled);
|
||||
setuid(user_id);
|
||||
|
||||
return success ? 0 : 1;
|
||||
} else if(argc != 1) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: expected 0 or 1 arguments, got %d argument(s)\n", argc);
|
||||
usage();
|
||||
@@ -59,7 +102,6 @@ int main(int argc, char **argv) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const uid_t user_id = getuid();
|
||||
if(geteuid() != 0) {
|
||||
if(setuid(0) == -1) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to change user to root, global hotkeys will not work. Make sure to set the correct capability on gsr-global-hotkeys\n");
|
||||
@@ -68,7 +110,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
keyboard_event keyboard_ev;
|
||||
if(!keyboard_event_init(&keyboard_ev, true, grab_type)) {
|
||||
if(!keyboard_event_init(&keyboard_ev, grab_type != KEYBOARD_GRAB_TYPE_NO_GRAB, grab_type)) {
|
||||
fprintf(stderr, "gsr-global-hotkeys error: failed to setup hotplugging and no keyboard input devices were found\n");
|
||||
setuid(user_id);
|
||||
return 1;
|
||||
|
||||
83
tools/gsr-hyprland-helper/main.c
Normal file
@@ -0,0 +1,83 @@
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdbool.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
static void print_window_title(const char *window_title) {
|
||||
if (window_title[0] == 0) {
|
||||
printf("Window title changed: %s\n", "Desktop");
|
||||
fflush(stdout);
|
||||
return;
|
||||
}
|
||||
printf("Window title changed: %s\n", window_title);
|
||||
fflush(stdout);
|
||||
}
|
||||
|
||||
static int handle_ipc(const char *hyprland_socket_path) {
|
||||
const int sock_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (sock_fd == -1) {
|
||||
perror("Error: gsr-hyprland-helper: socket");
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
|
||||
if (snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", hyprland_socket_path) >= (int)sizeof(addr.sun_path)) {
|
||||
fprintf(stderr, "Error: gsr-hypland-helper: path to hyprland socket (%s) is more than %d characters long\n", hyprland_socket_path, (int)sizeof(addr.sun_path));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connect(sock_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
perror("Error: gsr-hyprland-helper: connect");
|
||||
close(sock_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Info: gsr-hyprland-helper: connected to Hyprland socket: %s\n", addr.sun_path);
|
||||
|
||||
FILE *sock_file = fdopen(sock_fd, "r");
|
||||
if (!sock_file) {
|
||||
perror("Error: gsr-hyprland-helper: fdopen");
|
||||
close(sock_fd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char buffer[1024];
|
||||
while (fgets(buffer, sizeof(buffer), sock_file)) {
|
||||
int line_len = strlen(buffer);
|
||||
if(line_len > 0 && buffer[line_len - 1] == '\n') {
|
||||
buffer[line_len - 1] = '\0';
|
||||
line_len -= 1;
|
||||
}
|
||||
|
||||
if(line_len >= 14 && memcmp(buffer, "activewindow>>", 14) == 0) {
|
||||
char *window_id = buffer + 14;
|
||||
char *window_title = strchr(buffer + 14, ',');
|
||||
if(!window_title)
|
||||
continue;
|
||||
|
||||
window_title[0] = '\0';
|
||||
window_title += 1;
|
||||
if(strcmp(window_id, "gsr-ui") == 0 || strcmp(window_id, "gsr-notify") == 0)
|
||||
continue;
|
||||
|
||||
print_window_title(window_title);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(sock_file);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if(argc != 2) {
|
||||
fprintf(stderr, "usage: gsr-hyprland-helper <hyprland-socket-path>\n");
|
||||
return 1;
|
||||
}
|
||||
return handle_ipc(argv[1]);
|
||||
}
|
||||
86
tools/gsr-kwin-helper/gsrkwinhelper.js
Normal file
@@ -0,0 +1,86 @@
|
||||
const DAEMON_DBUS_NAME = "com.dec05eba.gpu_screen_recorder";
|
||||
|
||||
function dbusSendUpdateActiveWindow(title, isFullscreen, monitorName) {
|
||||
callDBus(
|
||||
DAEMON_DBUS_NAME, "/", DAEMON_DBUS_NAME,
|
||||
"updateActiveWindow",
|
||||
title, isFullscreen, monitorName,
|
||||
);
|
||||
}
|
||||
|
||||
let prevWindow = null;
|
||||
let prevEmitActiveWindowUpdate = null;
|
||||
|
||||
let prevCaption = null;
|
||||
let prevFullScreen = null;
|
||||
let prevMonitorName = null;
|
||||
|
||||
function dbusSafeDisconnect(window, signalName, callback) {
|
||||
try {
|
||||
const signal = window?.[signalName];
|
||||
if (signal && callback) {
|
||||
signal.disconnect(callback);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore stale/missing signal disconnects
|
||||
}
|
||||
}
|
||||
|
||||
function dbusSafeConnect(window, signalName, callback) {
|
||||
try {
|
||||
const signal = window?.[signalName];
|
||||
if (signal && callback) {
|
||||
signal.connect(callback);
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore missing signals
|
||||
}
|
||||
}
|
||||
|
||||
function emitActiveWindowUpdate(window) {
|
||||
if (workspace.activeWindow === window) {
|
||||
let caption = window.caption || "";
|
||||
let fullScreen = window.fullScreen || false;
|
||||
let monitorName = window.output?.name || "";
|
||||
if (caption !== prevCaption || fullScreen !== prevFullScreen || monitorName !== prevMonitorName) {
|
||||
dbusSendUpdateActiveWindow(caption, fullScreen, monitorName);
|
||||
prevCaption = caption;
|
||||
prevFullScreen = fullScreen;
|
||||
prevMonitorName = monitorName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function subscribeToWindow(window) {
|
||||
if (!window) return;
|
||||
if (prevWindow !== window) {
|
||||
if (prevWindow !== null) {
|
||||
dbusSafeDisconnect(prevWindow, "captionChanged", prevEmitActiveWindowUpdate);
|
||||
dbusSafeDisconnect(prevWindow, "fullScreenChanged", prevEmitActiveWindowUpdate);
|
||||
dbusSafeDisconnect(prevWindow, "outputChanged", prevEmitActiveWindowUpdate);
|
||||
}
|
||||
let emitActiveWindowUpdateBound = emitActiveWindowUpdate.bind(null, window);
|
||||
dbusSafeConnect(window, "captionChanged", emitActiveWindowUpdateBound);
|
||||
dbusSafeConnect(window, "fullScreenChanged", emitActiveWindowUpdateBound);
|
||||
dbusSafeConnect(window, "outputChanged", emitActiveWindowUpdateBound);
|
||||
prevWindow = window;
|
||||
prevEmitActiveWindowUpdate = emitActiveWindowUpdateBound;
|
||||
}
|
||||
}
|
||||
|
||||
function updateActiveWindow(window) {
|
||||
if (!window) return;
|
||||
if (!window.normalWindow) return;
|
||||
if (window.resourceName === "gsr-ui" || window.resourceName === "gsr-notify") return; // ignore the overlay and notification
|
||||
if (window.resourceClass === "org.kde.spectacle") return;
|
||||
emitActiveWindowUpdate(window);
|
||||
subscribeToWindow(window);
|
||||
}
|
||||
|
||||
// handle window focus changes
|
||||
workspace.windowActivated.connect(updateActiveWindow);
|
||||
|
||||
// handle initial state
|
||||
if (workspace.activeWindow) {
|
||||
updateActiveWindow(workspace.activeWindow);
|
||||
}
|
||||
284
tools/gsr-kwin-helper/main.cpp
Normal file
@@ -0,0 +1,284 @@
|
||||
#include <dbus/dbus.h>
|
||||
#include <unistd.h>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static const char* INTROSPECTION_XML =
|
||||
"<!DOCTYPE node PUBLIC \"-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\"\n"
|
||||
"\"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\">\n"
|
||||
"<node>\n"
|
||||
" <interface name='com.dec05eba.gpu_screen_recorder'>\n"
|
||||
" <method name='updateActiveWindow'>\n"
|
||||
" <arg type='s' name='title' direction='in'/>\n"
|
||||
" <arg type='b' name='fullscreen' direction='in'/>\n"
|
||||
" <arg type='s' name='monitorName' direction='in'/>\n"
|
||||
" </method>\n"
|
||||
" </interface>\n"
|
||||
" <interface name='org.freedesktop.DBus.Introspectable'>\n"
|
||||
" <method name='Introspect'>\n"
|
||||
" <arg type='s' name='data' direction='out'/>\n"
|
||||
" </method>\n"
|
||||
" </interface>\n"
|
||||
"</node>\n";
|
||||
|
||||
|
||||
class GsrKwinHelper {
|
||||
public:
|
||||
std::string active_window_title;
|
||||
bool active_window_fullscreen;
|
||||
std::string active_window_monitor_name;
|
||||
DBusConnection* connection = nullptr;
|
||||
DBusError err;
|
||||
|
||||
bool init() {
|
||||
dbus_error_init(&err);
|
||||
|
||||
connection = dbus_bus_get(DBUS_BUS_SESSION, &err);
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to connect to session bus: " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!connection) {
|
||||
std::cerr << "Error: gsr-kwin-helper: connection is null\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = dbus_bus_request_name(connection, "com.dec05eba.gpu_screen_recorder",
|
||||
DBUS_NAME_FLAG_REPLACE_EXISTING, &err);
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to request name: " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ret != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
|
||||
std::cerr << "Error: gsr-kwin-helper: not primary owner of the name\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: DBus server initialized on com.dec05eba.gpu_screen_recorder\n";
|
||||
|
||||
const bool inside_flatpak = access("/app/manifest.json", F_OK) == 0;
|
||||
|
||||
const char *helper_path =
|
||||
!inside_flatpak
|
||||
? KWIN_HELPER_SCRIPT_PATH
|
||||
: "/var/lib/flatpak/app/com.dec05eba.gpu_screen_recorder/current/active/files/share/gsr-ui/gsrkwinhelper.js";
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: KWin script path: " << helper_path << std::endl;
|
||||
|
||||
if (!load_kwin_script(connection, helper_path)) {
|
||||
std::cerr << "Warning: gsr-kwin-helper: failed to load KWin script\n";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void run() {
|
||||
while (true) {
|
||||
dbus_connection_read_write(connection, 100);
|
||||
DBusMessage* msg = dbus_connection_pop_message(connection);
|
||||
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (dbus_message_is_method_call(msg, "org.freedesktop.DBus.Introspectable", "Introspect")) {
|
||||
handle_introspect(msg);
|
||||
} else if (dbus_message_is_method_call(msg, "com.dec05eba.gpu_screen_recorder", "updateActiveWindow")) {
|
||||
handle_update_active_window(msg);
|
||||
}
|
||||
|
||||
dbus_message_unref(msg);
|
||||
}
|
||||
}
|
||||
|
||||
void handle_introspect(DBusMessage* msg) {
|
||||
DBusMessage* reply = dbus_message_new_method_return(msg);
|
||||
if (!reply) return;
|
||||
|
||||
DBusMessageIter args;
|
||||
dbus_message_iter_init_append(reply, &args);
|
||||
|
||||
if (!dbus_message_iter_append_basic(&args, DBUS_TYPE_STRING, &INTROSPECTION_XML)) {
|
||||
dbus_message_unref(reply);
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
|
||||
void handle_update_active_window(DBusMessage* msg) {
|
||||
DBusMessageIter args;
|
||||
DBusBasicValue title;
|
||||
DBusBasicValue fullscreen;
|
||||
DBusBasicValue monitorName;
|
||||
|
||||
if (!dbus_message_iter_init(msg, &args)) {
|
||||
send_error_reply(msg, "No arguments provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_STRING) {
|
||||
send_error_reply(msg, "Expected string argument");
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_message_iter_get_basic(&args, &title);
|
||||
|
||||
if (!dbus_message_iter_next(&args)) {
|
||||
send_error_reply(msg, "Not enough arguments provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_BOOLEAN) {
|
||||
send_error_reply(msg, "Expected boolean argument");
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_message_iter_get_basic(&args, &fullscreen);
|
||||
|
||||
if (!dbus_message_iter_next(&args)) {
|
||||
send_error_reply(msg, "Not enough arguments provided");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbus_message_iter_get_arg_type(&args) != DBUS_TYPE_STRING) {
|
||||
send_error_reply(msg, "Expected string argument");
|
||||
return;
|
||||
}
|
||||
|
||||
dbus_message_iter_get_basic(&args, &monitorName);
|
||||
|
||||
if (title.str) {
|
||||
if (active_window_title != title.str) {
|
||||
active_window_title = title.str;
|
||||
std::cout << "Active window title set to: " << active_window_title << "\n";
|
||||
std::cout.flush();
|
||||
}
|
||||
} else {
|
||||
send_error_reply(msg, "Failed to read string");
|
||||
return;
|
||||
}
|
||||
|
||||
if (active_window_fullscreen != fullscreen.bool_val) {
|
||||
active_window_fullscreen = fullscreen.bool_val;
|
||||
std::cout << "Active window fullscreen state set to: " << active_window_fullscreen << "\n";
|
||||
std::cout.flush();
|
||||
}
|
||||
|
||||
if (monitorName.str) {
|
||||
if (active_window_monitor_name != monitorName.str) {
|
||||
active_window_monitor_name = monitorName.str;
|
||||
std::cout << "Active window monitor name set to: " << active_window_monitor_name << "\n";
|
||||
std::cout.flush();
|
||||
}
|
||||
} else {
|
||||
send_error_reply(msg, "Failed to read string");
|
||||
return;
|
||||
}
|
||||
|
||||
send_success_reply(msg);
|
||||
}
|
||||
|
||||
void send_success_reply(DBusMessage* msg) {
|
||||
DBusMessage* reply = dbus_message_new_method_return(msg);
|
||||
if (reply) {
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
void send_error_reply(DBusMessage* msg, const char* error_msg) {
|
||||
DBusMessage* reply = dbus_message_new_error(msg, "com.dec05eba.gpu_screen_recorder.Error", error_msg);
|
||||
if (reply) {
|
||||
dbus_connection_send(connection, reply, nullptr);
|
||||
dbus_connection_flush(connection);
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
}
|
||||
|
||||
bool call_kwin_method(DBusConnection* conn, const char* method,
|
||||
const char* arg1 = nullptr, const char* arg2 = nullptr) {
|
||||
DBusMessage* msg = dbus_message_new_method_call(
|
||||
"org.kde.KWin",
|
||||
"/Scripting",
|
||||
"org.kde.kwin.Scripting",
|
||||
method
|
||||
);
|
||||
|
||||
if (!msg) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to create message for " << method << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg1) {
|
||||
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg1, DBUS_TYPE_INVALID);
|
||||
if (arg2) {
|
||||
dbus_message_append_args(msg, DBUS_TYPE_STRING, &arg2, DBUS_TYPE_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
DBusError err;
|
||||
dbus_error_init(&err);
|
||||
|
||||
// Send message and wait for reply (with 1 second timeout)
|
||||
DBusMessage* reply = dbus_connection_send_with_reply_and_block(conn, msg, 1000, &err);
|
||||
|
||||
dbus_message_unref(msg);
|
||||
|
||||
if (dbus_error_is_set(&err)) {
|
||||
std::cerr << "Error: gsr-kwin-helper: error calling " << method << ": " << err.message << "\n";
|
||||
dbus_error_free(&err);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
dbus_message_unref(reply);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load_kwin_script(DBusConnection* conn, const char* script_path) {
|
||||
// Unload existing script
|
||||
call_kwin_method(conn, "unloadScript", "gsrkwinhelper");
|
||||
|
||||
if (!call_kwin_method(conn, "loadScript", script_path, "gsrkwinhelper")) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to load KWin script\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!call_kwin_method(conn, "start")) {
|
||||
std::cerr << "Error: gsr-kwin-helper: failed to start KWin script\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::cerr << "Info: gsr-kwin-helper: KWin script loaded and started successfully\n";
|
||||
return true;
|
||||
}
|
||||
|
||||
~GsrKwinHelper() {
|
||||
if (connection) {
|
||||
dbus_bus_release_name(connection, "com.dec05eba.gpu_screen_recorder", nullptr);
|
||||
dbus_connection_unref(connection);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
int main() {
|
||||
GsrKwinHelper helper;
|
||||
|
||||
if (!helper.init()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
helper.run();
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -5,9 +5,11 @@
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
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");
|
||||
@@ -23,7 +25,7 @@ static void get_runtime_filepath(char *buffer, size_t buffer_size, const char *f
|
||||
}
|
||||
|
||||
/* Assumes |str| size is less than 256 */
|
||||
static void fifo_write_all(int file_fd, const char *str) {
|
||||
static void file_write_all(int file_fd, const char *str) {
|
||||
char command[256];
|
||||
const ssize_t command_size = snprintf(command, sizeof(command), "%s\n", str);
|
||||
if(command_size >= (ssize_t)sizeof(command)) {
|
||||
@@ -33,7 +35,7 @@ static void fifo_write_all(int file_fd, const char *str) {
|
||||
|
||||
ssize_t offset = 0;
|
||||
while(offset < (ssize_t)command_size) {
|
||||
const ssize_t bytes_written = write(file_fd, str + offset, command_size - offset);
|
||||
const ssize_t bytes_written = write(file_fd, command + offset, command_size - offset);
|
||||
if(bytes_written > 0)
|
||||
offset += bytes_written;
|
||||
}
|
||||
@@ -50,6 +52,10 @@ static void usage(void) {
|
||||
printf(" Start/stop recording.\n");
|
||||
printf(" toggle-pause\n");
|
||||
printf(" Pause/unpause recording. Only applies to regular recording.\n");
|
||||
printf(" toggle-record-region\n");
|
||||
printf(" Start/stop recording a region.\n");
|
||||
printf(" toggle-record-window\n");
|
||||
printf(" Start/stop recording a window (or desktop portal on Wayland).\n");
|
||||
printf(" toggle-stream\n");
|
||||
printf(" Start/stop streaming.\n");
|
||||
printf(" toggle-replay\n");
|
||||
@@ -64,6 +70,8 @@ static void usage(void) {
|
||||
printf(" Take a screenshot.\n");
|
||||
printf(" take-screenshot-region\n");
|
||||
printf(" Take a screenshot of a region.\n");
|
||||
printf(" take-screenshot-window\n");
|
||||
printf(" Take a screenshot of a window (or desktop portal on Wayland).\n");
|
||||
printf("\n");
|
||||
printf("EXAMPLES:\n");
|
||||
printf(" gsr-ui-cli toggle-show\n");
|
||||
@@ -76,6 +84,8 @@ static bool is_valid_command(const char *command) {
|
||||
"toggle-show",
|
||||
"toggle-record",
|
||||
"toggle-pause",
|
||||
"toggle-record-region",
|
||||
"toggle-record-window",
|
||||
"toggle-stream",
|
||||
"toggle-replay",
|
||||
"replay-save",
|
||||
@@ -83,6 +93,7 @@ static bool is_valid_command(const char *command) {
|
||||
"replay-save-10-min",
|
||||
"take-screenshot",
|
||||
"take-screenshot-region",
|
||||
"take-screenshot-window",
|
||||
NULL
|
||||
};
|
||||
|
||||
@@ -109,15 +120,34 @@ int main(int argc, char **argv) {
|
||||
usage();
|
||||
}
|
||||
|
||||
char fifo_filepath[PATH_MAX];
|
||||
get_runtime_filepath(fifo_filepath, sizeof(fifo_filepath), "gsr-ui");
|
||||
const int fifo_fd = open(fifo_filepath, O_RDWR | O_NONBLOCK);
|
||||
if(fifo_fd <= 0) {
|
||||
fprintf(stderr, "Error: failed to open fifo file %s. Maybe gsr-ui is not running?\n", fifo_filepath);
|
||||
char socket_filepath[PATH_MAX];
|
||||
get_socket_filepath(socket_filepath, sizeof(socket_filepath), "gsr-ui");
|
||||
|
||||
const int socket_fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK, 0);
|
||||
if(socket_fd <= 0) {
|
||||
fprintf(stderr, "Error: failed to create socket\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
fifo_write_all(fifo_fd, command);
|
||||
close(fifo_fd);
|
||||
struct sockaddr_un addr = {0};
|
||||
addr.sun_family = AF_UNIX;
|
||||
snprintf(addr.sun_path, sizeof(addr.sun_path), "%s", socket_filepath);
|
||||
|
||||
for(;;) {
|
||||
if(connect(socket_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
const int err = errno;
|
||||
if(err == EWOULDBLOCK) {
|
||||
usleep(10 * 1000);
|
||||
} else {
|
||||
fprintf(stderr, "Error: failed to connect, error: %s. Maybe gsr-ui is not running?\n", strerror(err));
|
||||
exit(2);
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
file_write_all(socket_fd, command);
|
||||
close(socket_fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
414
translations/es.txt
Normal file
@@ -0,0 +1,414 @@
|
||||
# GPU Screen Recorder UI - Spanish translation
|
||||
|
||||
# General UI
|
||||
Record=Grabar
|
||||
Instant Replay=Repetición Instantánea
|
||||
Livestream=Transmisión
|
||||
Settings=Ajustes
|
||||
|
||||
# Status messages
|
||||
Off=Desactivado
|
||||
On=Activado
|
||||
Not recording=No grabando
|
||||
Recording=Grabando
|
||||
Not streaming=No transmitiendo
|
||||
Streaming=Transmitiendo
|
||||
Paused=Pausado
|
||||
|
||||
# Button labels
|
||||
Start=Iniciar
|
||||
Stop=Detener
|
||||
Stop and save=Detener y guardar
|
||||
Pause=Pausar
|
||||
Unpause=Reanudar
|
||||
Save=Guardar
|
||||
Save 1 min=Guardar 1 min
|
||||
Save 10 min=Guardar 10 min
|
||||
Turn on=Activar
|
||||
Turn off=Desactivar
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=La grabación se ha pausado
|
||||
Recording has been unpaused=La grabación se ha reanudado
|
||||
Started recording %s=Iniciando la grabación %s
|
||||
Saved a %s recording of %s\nto "%s"=Se guardó una grabación de %s de %s\nen "%s"
|
||||
Saved a %s recording of %s=Se guardó una grabación de %s de %s
|
||||
Failed to start/save recording=Fallo al iniciar/guardar grabación
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=Repetición detenida
|
||||
Started replaying %s=Iniciando la repetición %s
|
||||
Saving replay, this might take some time=Guardando repetición, esto podría tardar un rato
|
||||
Saved a %s replay of %s\nto "%s"=Se guardó una repetición de %s de %s\nen "%s"
|
||||
Saved a %s replay of %s=Se guardó una repetición de %s de %s
|
||||
Replay stopped because of an error=Repetición detenida debido a un error
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=Los ajustes de repetición han sido modificados.\nUn reinicio puede ser necesario para aplicar los cambios.
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=La transmisión se ha detenido
|
||||
Started streaming %s=Iniciando la transmisión %s
|
||||
Streaming stopped because of an error=Transmisión detenida debido a un error
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=Los ajustes de transmisión han sido modificados.\nUn reinicio puede ser necesario para aplicar los cambios.
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=Captura de pantalla de %s\nguardada en "%s"
|
||||
Saved a screenshot of %s=Captura de pantalla de %s guardada
|
||||
Failed to take a screenshot=Fallo al tomar la captura de pantalla
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=Ya hay otra instancia de GPU Screen Recorder UI en ejecución.\nPulsa Alt-Z para abrir la interfaz.
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=GPU Screen Recorder ya está en ejecución en otro proceso.\nPor favor, ciérralo antes de usar GPU Screen Recorder UI.
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=Fallo al iniciar la repetición, el objetivo de captura "%s" es inválido.\nPor favor, cambia el objetivo de captura en los ajustes
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=Fallo al iniciar la grabación, el objetivo de captura "%s" es inválido.\nPor favor, cambia el objetivo de captura en los ajustes
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=Fallo al iniciar la transmisión, el objetivo de captura "%s" es inválido.\nPor favor, cambia el objetivo de captura en los ajustes
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=Fallo al tomar la captura de pantalla, el objetivo de captura "%s" es inválido.\nPor favor, cambia el objetivo de captura en los ajustes
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=No se puede grabar cuando la repetición está activada.\nApaga la repetición antes de empezar a grabar.
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=No se puede transmitir cuando la repetición está activada.\nApaga la repetición antes de empezar a transmitir.
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=No se puede transmitir mientras se está grabando. Detén la grabación antes de empezar a transmitir.
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=No se puede grabar mientras se está transmitiendo. Detén la transmisión antes de empezar a grabar.
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=No se puede iniciar la repetición mientras se está grabando. Detén la grabación antes de iniciar la repetición.
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=No se puede iniciar la repetición mientras se está transmitiendo. Detén la transmisión antes de iniciar la repetición.
|
||||
|
||||
Started recording in the replay session=Grabación iniciada en la sesión de repetición
|
||||
Started recording in the streaming session=Grabación iniciada en la sesión de transmisión
|
||||
|
||||
Failed to start region capture=Fallo al iniciar la captura de región
|
||||
Failed to start window capture=Fallo al iniciar la captura de ventana
|
||||
No window selected=Ventana no seleccionada
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=Transmisión detenida debido a un error. Comprueba que los ajustes sean correctos
|
||||
%s. Verify if settings are correct=%s. Comprueba que los ajustes sean correctos
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=Falló la captura del portal de escritorio.\nO cancelaste el portal de escritorio, o tu compositor de Wayland no admite la captura del portal de escritorio\no está configurado incorrectamente en tu sistema.
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=Falló la captura del monitor.\nEl monitor que intentas capturar no es válido.\nPor favor, valida la configuración de captura.
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=Falló la captura. Ni los códecs de video H264, HEVC ni AV1 son compatibles\ncon tu sistema o estás intentando capturar a una resolución superior a la que tu\nsistema admite para cada códec de video.
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=Falló la captura. Tu sistema no admite la resolución a la que intentas\ngrabar con el códec de video que has elegido.\nCambia la resolución de captura o el códec de video e inténtalo de nuevo.\nNota: AV1 admite la resolución más alta, luego HEVC y después H264.
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=Falló la captura. Tu sistema no es compatible con el códec de video que has elegido.\nCambia el códec de video e inténtalo de nuevo
|
||||
Stopped capture because the user canceled the desktop portal=Se detuvo la captura porque el usuario canceló el portal de escritorio
|
||||
Failed to take a screenshot. Verify if settings are correct=Fallo al tomar una captura de pantalla. Verifica si la configuración es correcta
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=Fallo al lanzar gpu-screen-recorder para iniciar la repetición
|
||||
Failed to launch gpu-screen-recorder to start recording=Fallo al lanzar gpu-screen-recorder para iniciar la grabación
|
||||
Failed to launch gpu-screen-recorder to start streaming=Fallo al lanzar gpu-screen-recorder para iniciar la transmisión
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=Fallo al lanzar gpu-screen-recorder para tomar una captura de pantalla
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=Fallo al añadir GPU Screen Recorder al inicio del sistema
|
||||
Failed to remove GPU Screen Recorder from system startup=Fallo al eliminar GPU Screen Recorder del inicio del sistema
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=Para activar el inicio automático: instala y configura 'dex' (recomendado) o añade manualmente '%s' a las entradas de inicio automático del escritorio.
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=El inicio de GPU Screen Recorder UI ha cambiado del servicio systemd al inicio automático XDG.
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=El inicio automático de GPU Screen Recorder UI mediante systemd está obsoleto.\nPara migrar: instala y configura 'dex' (recomendado)\no añade manualmente '%s' a las entradas de inicio automático del escritorio.
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=Wayland no ofrece soporte adecuado para GPU Screen Recorder UI;\nes posible que el funcionamiento no sea el esperado. Si experimentas problemas, utiliza X11.
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=Algún programa de reasignación de teclas entra en conflicto con GPU Screen Recorder en tu sistema.\nSe ha liberado la captura del teclado, las aplicaciones ahora recibirán las teclas de acceso rápido que presiones.
|
||||
|
||||
# Capture targets
|
||||
this monitor=este monitor
|
||||
window=ventana
|
||||
window "%s"=ventana "%s"
|
||||
window %s=ventana %s
|
||||
focused=activa
|
||||
region=región
|
||||
portal=portal
|
||||
|
||||
# Time durations (used in recording/replay saved notifications)
|
||||
%d second=%d segundo
|
||||
%d minute=%d minuto
|
||||
%d hour=%d hora
|
||||
%d seconds=%d segundos
|
||||
%d minutes=%d minutos
|
||||
%d hours=%d horas
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=Color de acento
|
||||
Red=Rojo
|
||||
Green=Verde
|
||||
Blue=Azul
|
||||
|
||||
Start program on system startup?=¿Iniciar programa al iniciar el sistema?
|
||||
Yes=Sí
|
||||
No=No
|
||||
|
||||
Enable keyboard hotkeys?=¿Habilitar las teclas de acceso rápido?
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=Sí, pero capturar solo dispositivos virtuales (ofrece soporte para algunos prog. de reasig. de teclas)
|
||||
Yes, but don't grab devices (supports all input remapping software)=Sí, pero sin capturar dispositivos (ofrece soporte para todos los programas de reasignación de teclas)
|
||||
|
||||
Show/hide UI:=Mostrar/ocultar interfaz
|
||||
Turn replay on/off:=Activar/desactivar repetición
|
||||
Save replay:=Guardar repetición
|
||||
Save 1 minute replay:=Guardar repetición de 1 minuto
|
||||
Save 10 minute replay:=Guardar repetición de 10 minutos
|
||||
Start/stop recording:=Iniciar/detener grabación
|
||||
Pause/unpause recording:=Pausar/reanudar grabación
|
||||
Start/stop recording a region:=Iniciar/detener grabación de una región
|
||||
Start/stop streaming:=Iniciar/detener transmisión
|
||||
Take a screenshot:=Tomar una captura de pantalla
|
||||
Take a screenshot of a region:=Tomar una captura de pantalla de una región
|
||||
Start/stop recording with desktop portal:=Iniciar/detener grabación con portal de escritorio
|
||||
Take a screenshot with desktop portal:=Tomar una captura de pantalla con portal de escritorio
|
||||
Start/stop recording a window:=Iniciar/detener grabación de una ventana
|
||||
Take a screenshot of a window:=Tomar una captura de pantalla de una ventana
|
||||
|
||||
Clear hotkeys=Borrar teclas de acceso rápido
|
||||
Reset hotkeys to default=Reestablecer teclas de acceso rápido a los valores predeterminados
|
||||
|
||||
Enable controller hotkeys?=¿Habilitar las teclas de acceso rápido para mando?
|
||||
Press=Pulsa
|
||||
and=y
|
||||
|
||||
Notification speed=Velocidad de notificación
|
||||
Normal=Normal
|
||||
Fast=Rápida
|
||||
|
||||
Language=Idioma
|
||||
System language=Idioma del sistema
|
||||
|
||||
Exit program=Salir del programa
|
||||
Go back to the old UI=Volver a la interfaz antigua
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=Si deseas hacer una donación, puedes hacerlo en https://buymeacoffee.com/dec05eba:
|
||||
Donate=Donar
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=Todas las donaciones se destinan al desarrollo de software (incluido GPU Screen Recorder)\ny a la compra de hardware para probar el software.
|
||||
|
||||
# Subsection headers
|
||||
Global=Global
|
||||
Back=Atrás
|
||||
|
||||
Appearance=Apariencia
|
||||
Startup=Inicio
|
||||
Keyboard hotkeys=Teclas de acceso rápido
|
||||
Controller hotkeys=Teclas de acceso rápido para mando
|
||||
Application options=Opciones de la aplicación
|
||||
Application info=Información de la aplicación
|
||||
Donate=Donar
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=GSR versión: %s
|
||||
GSR-UI version: %s=GSR-UI versión: %s
|
||||
Flatpak version: %s=Flatpak versión: %s
|
||||
GPU vendor: %s=Proveedor de GPU: %s
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=Pulsa una combinación de teclas para el acceso rápido: "%s"
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=Las teclas alfanuméricas no se pueden usar solas en los atajos de teclado, deben usarse con una o más de estas teclas: Alt, Ctrl, Mayús y Super.\nPulsa Esc para cancelar o Retroceso para eliminar el atajo de teclado.
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=Mostrar/ocultar interfaz
|
||||
Turn replay on/off=Activar/desactivar repetición
|
||||
Save replay=Guardar repetición
|
||||
Save 1 minute replay=Guardar repetición de 1 minuto
|
||||
Save 10 minute replay=Guardar repetición de 10 minutos
|
||||
Start/stop recording=Iniciar/detener grabación
|
||||
Pause/unpause recording=Pausar/reanudar grabación
|
||||
Start/stop recording a region=Iniciar/detener grabación de una región
|
||||
Start/stop recording a window=Iniciar/detener grabación de una ventana
|
||||
Start/stop recording with desktop portal=Iniciar/detener grabación con un portal de escritorio
|
||||
Start/stop streaming=Iniciar/detener transmisión
|
||||
Take a screenshot=Tomar una captura de pantalla
|
||||
Take a screenshot of a region=Tomar una captura de pantalla de una región
|
||||
Take a screenshot of a window=Tomar una captura de pantalla de una ventana
|
||||
Take a screenshot with desktop portal=Tomar una captura de pantalla con un portal de escritorio
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=para mostrar/ocultar la interfaz
|
||||
to take a screenshot=para tomar una captura de pantalla
|
||||
to save a replay=para guardar una repetición
|
||||
to start/stop recording=para iniciar/detener una grabación
|
||||
to turn replay on/off=para activar/desactivar la repetición
|
||||
to save a 1 minute replay=para guardar una repetición de 1 minuto
|
||||
to save a 10 minute replay=para guardar una repetición de 10 minutos
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=La combinación de teclas %s ya está en uso para otra cosa
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=Captura de pantalla
|
||||
Capture=Captura
|
||||
Image=Imagen
|
||||
File info=Información del archivo
|
||||
General=General
|
||||
Screenshot indicator=Indicador de captura de pantalla
|
||||
Script=Script
|
||||
File=Archivo
|
||||
|
||||
Back=Atrás
|
||||
Save=Guardar
|
||||
Cancel=Cancelar
|
||||
|
||||
Capture source:=Fuente de captura:
|
||||
Window=Ventana
|
||||
Region=Región
|
||||
Desktop portal=Portal de escritorio
|
||||
Monitor %s (%dx%d)=Monitor %s (%dx%d)
|
||||
Screen=Pantalla
|
||||
|
||||
Image resolution limit:=Límite de resolución de imagen:
|
||||
Change image resolution=Cambiar la resolución de imagen
|
||||
Restore portal session=Restaurar sesión de portal
|
||||
|
||||
Image quality:=Calidad de imagen
|
||||
Medium=Media
|
||||
High=Alta
|
||||
Very high (Recommended)=Muy alta (recomendado)
|
||||
Ultra=Ultra
|
||||
|
||||
Record cursor=Grabar cursor
|
||||
|
||||
Directory to save screenshots:=Directorio para guardar capturas de pantalla:
|
||||
Image format:=Formato de imagen:
|
||||
|
||||
Save screenshot in a folder based on the games name=Guardar captura de pantalla en una carpeta con el mismo nombre que el juego
|
||||
|
||||
Save screenshot to clipboard=Guardar captura de pantalla en el portapapeles
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=Guardar captura de pantalla en el portapapeles (no admitido por Wayland)
|
||||
Save screenshot to disk=Guardar la captura de pantalla en el disco
|
||||
|
||||
Show screenshot notifications=Mostrar notificaciones de captura de pantalla
|
||||
Blink scroll lock led when taking a screenshot=Hacer parpadear el LED de Bloq Despl al tomar una captura de pantalla
|
||||
|
||||
Command to open the screenshot with:=Abrir la captura de pantalla con el comando:
|
||||
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=Vista simple
|
||||
Advanced view=Vista avanzada
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=Seguir la ventana activa
|
||||
Focused monitor=Monitor activo
|
||||
|
||||
Area size:=Tamaño del área:
|
||||
Video resolution limit:=Límite de resolución de vídeo:
|
||||
Change video resolution=Cambiar la resolución de vídeo
|
||||
Restore portal session=Restaurar sesión de portal
|
||||
|
||||
# Webcam settings
|
||||
Webcam=Webcam
|
||||
Webcam source:=Fuente de la webcam:
|
||||
None=Ninguna
|
||||
Video format:=Formato de vídeo:
|
||||
Auto (recommended)=Auto (recomendado)
|
||||
YUYV=YUYV
|
||||
Motion-JPEG=Motion-JPEG
|
||||
Video setup:=Configuración de vídeo:
|
||||
* Right click in the bottom right corner to resize the webcam=* Clic derecho en la esquina inferior derecha para cambiar el tamaño de la webcam
|
||||
Flip camera horizontally=Voltear la cámara horizontalmente
|
||||
|
||||
# Audio settings
|
||||
Audio=Sonido
|
||||
Audio codec:=Códec de sonido
|
||||
Opus (Recommended)=Opus (recomendado)
|
||||
AAC=AAC
|
||||
|
||||
Directory to save videos:=Directorio donde guardar los vídeos:
|
||||
Output device:=Dispositivo de salida:
|
||||
Input device: =Dispositivo de entrada:
|
||||
# yes, these spaces are intentional
|
||||
Application: =Aplicación:
|
||||
Custom...=Personalizado...
|
||||
|
||||
Save video in a folder based on the games name%s=Guardar el vídeo en una carpeta con el mismo nombre que el juego%s
|
||||
(X11 applications only)= (solo para aplicaciones X11)
|
||||
|
||||
Add audio track=Añadir pista de sonido
|
||||
Add input device=Añadir dispositivo de entrada
|
||||
Add output device=Añadir dispositivo de salida
|
||||
Add application audio=Añadir sonido de aplicación
|
||||
Record audio from all applications except the selected ones=Grabar el sonido de todas las aplicaciones salvo las seleccionadas
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=Grabar los dispositivos de salida y el audio de las aplicaciones puede que grabe todo el audio de salida, lo cual probablemente\nno es lo que quieres hacer. Elimina los dispositivos de salida.
|
||||
|
||||
Video=Vídeo
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=Códec de vídeo:
|
||||
H264=H264
|
||||
HEVC=HEVC
|
||||
HEVC (10 bit, reduces banding)=HEVC (10 bits, reduce el banding)
|
||||
HEVC (HDR)=HEVC (HDR)
|
||||
AV1=AV1
|
||||
AV1 (10 bit, reduces banding)=AV1 (10 bits, reduce el banding)
|
||||
AV1 (HDR)=AV1 (HDR)
|
||||
VP8=VP8
|
||||
VP9=VP9
|
||||
H264 Software Encoder (Slow, not recommended)=Codificador por software H264 (lento, no recomendado)
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=Calidad de vídeo:
|
||||
Very high=Muy alta
|
||||
Video bitrate (Kbps):=Tasa de bits del vídeo (Kbps)
|
||||
Constant bitrate=Tasa de bits constante
|
||||
Constant bitrate (Recommended)=Tasa de bits constante (recomendado)
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=Tasa de fotogramas:
|
||||
Frame rate mode:=Modo de tasa de fotogramas:
|
||||
Auto (Recommended)=Automático (ecomendado)
|
||||
Constant=Constante
|
||||
Variable=Variable
|
||||
Sync to content=Sincronizar con el contenido
|
||||
Sync to content (Only X11 or desktop portal capture)=Sincronizar con el contenido (solo para X11 o captura de portal de escritorio)
|
||||
|
||||
# Color range
|
||||
Color range:=Rango de color
|
||||
Limited=Limitado
|
||||
Full=Completo
|
||||
|
||||
# Container format
|
||||
Container:=Contenedor:
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=Grabar en modo de bajo consumo
|
||||
Record cursor=Grabar cursor
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=No forzar a la GPU a entrar en modo de alto rendimiento al grabar.\nPuede afectar al rendimiento de la grabación, especialmente al reproducir un vídeo al mismo tiempo.\nSi se activa, se recomienda usar el modo de sincronización con la velocidad de fotogramas del contenido para reducir el consumo de energía cuando está inactiva.
|
||||
|
||||
Show %s notifications=Mostrar notificación de %s
|
||||
Show %s status with scroll lock LED=Mostrar estado de %s con el LED de Bloq Despl
|
||||
Recording indicator=Indicador de grabación
|
||||
recording=grabando
|
||||
|
||||
Simple=Simple
|
||||
Audio track #%d=Pista de sonido #%d
|
||||
Output device=Dispositivo de salida
|
||||
Input device: =Dispositivo de entrada:
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=Tamaño estimado del archivo de vídeo por minuto (sin sonido): %.2fMB
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=Directorio para guardar las repeticiones:
|
||||
Replay indicator=Indicador de repetición
|
||||
replay=repetición
|
||||
Turn on replay when starting a fullscreen application%s=Activar la repetición al iniciar una aplicación a pantalla completa%s
|
||||
Autostart=Inicio automático
|
||||
in RAM=en RAM
|
||||
Replay duration in seconds:=Duración de la repetición en segundos:
|
||||
Where should temporary replay data be stored?=¿Dónde se deben almacenar los archivos temporales de la repetición?
|
||||
RAM=RAM
|
||||
Disk (Not recommended on SSDs)=Disco (no recomendado en SSDs)
|
||||
Turn on replay when this program starts=Activar la repetición cuando se inicie este programa
|
||||
Turn on replay when power supply is connected=Activar la repetición cuando se conecte la fuente de alimentación
|
||||
Don't turn on replay automatically=No activar la repetición automáticamente
|
||||
Restart replay on save=Reiniciar la repetición al guardar
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=Tamaño máximo estimado del archivo de vídeo %s: %.2fMB.\nCambia la tasa de bits del vídeo o la duración de la repetición para cambiar el tamaño del archivo.
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=Servicio de transmisión
|
||||
Twitch=Twitch
|
||||
YouTube=YouTube
|
||||
Kick=Kick
|
||||
Rumble=Rumble
|
||||
Custom=Personalizado
|
||||
Stream URL:=URL de la transmisión
|
||||
Stream key:=Clave de transmisión
|
||||
Streaming info=Información de transmisión
|
||||
Streaming indicator=Indicador de transmisión
|
||||
streaming=transmisión
|
||||
427
translations/fr.txt
Normal file
@@ -0,0 +1,427 @@
|
||||
# GPU Screen Recorder UI - translation template
|
||||
|
||||
# Important warning: we f'ed up a little bit and used %s for both strings and numbers in some places, such as time durations (they're fixed by the moment).
|
||||
# When translating, be careful to use the %d format specifier for numbers in those places.
|
||||
|
||||
# General UI
|
||||
Record=Enregistrer
|
||||
Instant Replay=Replay instantané
|
||||
Livestream=Diffusion en direct
|
||||
Settings=Paramètres
|
||||
|
||||
# Status messages
|
||||
Off=Éteint
|
||||
On=Allumé
|
||||
Not recording=Non enregistré
|
||||
Recording=Enregistrement
|
||||
Not streaming=Non diffusé
|
||||
Streaming=Diffusion
|
||||
Paused=En pause
|
||||
|
||||
# Button labels
|
||||
Start=Lancer
|
||||
Stop=Arrêter
|
||||
Stop and save=Arrêter et sauvegarder
|
||||
Pause=Pause
|
||||
Unpause=Reprendre
|
||||
Save=Sauvegarder
|
||||
Save 1 min=Sauvegarder 1 min
|
||||
Save 10 min=Sauvegarder 10 min
|
||||
Turn on=Lancer
|
||||
Turn off=Arrêter
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=L’enregistrement a été mis en pause
|
||||
Recording has been unpaused=L’enregistrement a repris
|
||||
Started recording %s=Enregistrement démarré %s
|
||||
Saved a %s recording of %s\nto "%s"=Enregistrement %s de %s sauvegardé dans "%s"
|
||||
Saved a %s recording of %s=Enregistrement %s de %s sauvegardé
|
||||
Failed to start/save recording=Échec du démarrage/sauvegarde de l’enregistrement
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=Replay arrêté
|
||||
Started replaying %s=Replay démarré %s
|
||||
Saving replay, this might take some time=Sauvegarde du replay, cela peut prendre un certain temps
|
||||
Saved a %s replay of %s\nto "%s"=Replay %s de %s sauvegardé dans "%s"
|
||||
Saved a %s replay of %s=Replay %s de %s sauvegardé
|
||||
Replay stopped because of an error=Replay arrêté en raison d’une erreur
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=Les paramètres de replays ont été modifiés.\nVous devez peut-être redémarrer le replay pour appliquer les changements.
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=Diffusion arrêtée
|
||||
Started streaming %s=Diffusion démarrée %s
|
||||
Streaming stopped because of an error=Diffusion arrêtée en raison d’une erreur
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=Les paramètres de diffusion ont été modifiés.\nVous devez peut-être redémarrer la diffusion pour appliquer les changements.
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=Capture d’écran de %s sauvegardée dans "%s"
|
||||
Saved a screenshot of %s=Capture d’écran de %s sauvegardée
|
||||
Failed to take a screenshot=Échec de la capture d’écran
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=Une autre instance de GPU Screen Recorder UI est déjà en cours.\nAppuyez sur Alt+Z pour ouvrir l’UI.
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=GPU Screen Recorder est déjà en cours dans un autre processus.\nVeuillez le fermer avant d’utiliser l’UI.
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=Échec du démarrage du replay, la cible "%s" est invalide.\nVeuillez modifier la cible dans les paramètres.
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=Échec du démarrage de l’enregistrement, la cible "%s" est invalide.\nVeuillez modifier la cible dans les paramètres.
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=Échec du démarrage de la diffusion, la cible "%s" est invalide.\nVeuillez modifier la cible dans les paramètres.
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=Échec de la capture d’écran, la cible "%s" est invalide.\nVeuillez modifier la cible dans les paramètres.
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=Impossible de démarrer l’enregistrement lorsque le replay est activé.\nDésactivez le replay avant de commencer l’enregistrement.
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=Impossible de démarrer la diffusion lorsque le replay est activé.\nDésactivez le replay avant de commencer la diffusion.
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=Impossible de démarrer la diffusion pendant l’enregistrement.\nArrêtez l’enregistrement avant de commencer la diffusion.
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=Impossible de démarrer l’enregistrement pendant la diffusion.\nArrêtez la diffusion avant de commencer l’enregistrement.
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=Impossible de démarrer le replay pendant l’enregistrement.\nArrêtez l’enregistrement avant de commencer le replay.
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=Impossible de démarrer le replay pendant la diffusion.\nArrêtez la diffusion avant de commencer le replay.
|
||||
|
||||
Started recording in the replay session=Enregistrement démarré pendant la session de replay
|
||||
Started recording in the streaming session=Enregistrement démarré pendant la session de diffusion
|
||||
|
||||
Failed to start region capture=Échec du démarrage de la capture de région
|
||||
Failed to start window capture=Échec du démarrage de la capture de fenêtre
|
||||
No window selected=Aucune fenêtre sélectionnée
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=Diffusion arrêtée en raison d’une erreur. Vérifiez les paramètres
|
||||
%s. Verify if settings are correct=%s. Vérifiez les paramètres
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=Échec de la capture via le portail du bureau.\nVous avez annulé le portail ou votre compositeur Wayland ne supporte pas la capture via le portail ou il est mal configuré.
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=Échec de la capture du moniteur.\nLe moniteur que vous essayez de capturer est invalide.\nVeuillez vérifier vos paramètres de capture.
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=Échec de la capture. Aucun codec vidéo H264, HEVC ou AV1 n’est supporté sur votre système ou vous essayez de capturer à une résolution plus élevée que celle supportée par votre système pour chaque codec.
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=Échec de la capture. Votre système ne supporte pas la résolution choisie pour le codec sélectionné.\nModifiez la résolution ou le codec et réessayez.\nNote : AV1 supporte la résolution la plus élevée, puis HEVC et H264.
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=Échec de la capture. Votre système ne supporte pas le codec vidéo sélectionné.\nChangez de codec et réessayez.
|
||||
Stopped capture because the user canceled the desktop portal=Capture arrêtée car l’utilisateur a annulé le portail du bureau
|
||||
Failed to take a screenshot. Verify if settings are correct=Échec de la capture d’écran. Vérifiez les paramètres.
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=Échec du lancement de gpu-screen-recorder pour démarrer le replay
|
||||
Failed to launch gpu-screen-recorder to start recording=Échec du lancement de gpu-screen-recorder pour démarrer l’enregistrement
|
||||
Failed to launch gpu-screen-recorder to start streaming=Échec du lancement de gpu-screen-recorder pour démarrer la diffusion
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=Échec du lancement de gpu-screen-recorder pour capturer l’écran
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=Échec de l’ajout de GPU Screen Recorder au démarrage du système
|
||||
Failed to remove GPU Screen Recorder from system startup=Échec de la suppression de GPU Screen Recorder du démarrage du système
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=Pour activer le démarrage automatique : installez et configurez 'dex' (recommandé) ou ajoutez manuellement '%s' aux entrées de démarrage automatique du bureau.
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=Le démarrage de GPU Screen Recorder UI a été basculé du service systemd vers le démarrage automatique XDG.
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=Le démarrage automatique de GPU Screen Recorder UI via systemd est obsolète.\nPour migrer : installez et configurez 'dex' (recommandé)\nou ajoutez manuellement '%s' aux entrées de démarrage automatique du bureau.
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=Wayland ne supporte pas correctement l’UI de GPU Screen Recorder,\ncertains éléments peuvent mal fonctionner. Utilisez X11 si vous rencontrez des problèmes.
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=Certaines applications de remappage clavier entrent en conflit avec GPU Screen Recorder.\nLes claviers ont été libérés, les applications recevront désormais vos touches rapides.
|
||||
|
||||
# Capture targets
|
||||
this monitor=Ce moniteur
|
||||
window=Fenêtre
|
||||
window "%s"=Fenêtre "%s"
|
||||
window %s=Fenêtre %s
|
||||
focused=Fenêtre active
|
||||
region=Région
|
||||
portal=Portail
|
||||
|
||||
# Time durations (used in recording/replay saved notifications, remember to use %d for numbers)
|
||||
# if you have complex plural forms in your language, please use the following format:
|
||||
%d second_one=%d seconde
|
||||
%d second_few=%d secondes
|
||||
%d second_many=%d secondes
|
||||
%d minute_one=%d minute
|
||||
%d minute_few=%d minutes
|
||||
%d minute_many=%d minutes
|
||||
%d hour_one=%d heure
|
||||
%d hour_few=%d heures
|
||||
%d hour_many=%d heures
|
||||
%d second=%d seconde
|
||||
%d minute=%d minute
|
||||
%d hour=%d heure
|
||||
%d seconds=%d secondes
|
||||
%d minutes=%d minutes
|
||||
%d hours=%d heures
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=Couleur d’accent
|
||||
Red=Rouge
|
||||
Green=Vert
|
||||
Blue=Bleu
|
||||
|
||||
Start program on system startup?=Lancer le programme au démarrage du système ?
|
||||
Yes=Oui
|
||||
No=Non
|
||||
|
||||
Enable keyboard hotkeys?=Activer les raccourcis clavier ?
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=Oui, mais capturer que les appareils virtuels (supporte certains logiciels de remappage)
|
||||
Yes, but don't grab devices (supports all input remapping software)=Oui, sans capturer les appareils (supporte tous les logiciels de remappage)
|
||||
|
||||
Show/hide UI:=Afficher/masquer l’UI
|
||||
Turn replay on/off:=Activer/désactiver le replay
|
||||
Save replay:=Sauvegarder le replay
|
||||
Save 1 minute replay:=Sauvegarder 1min de replay
|
||||
Save 10 minute replay:=Sauvegarder 10min de replay
|
||||
Start/stop recording:=Lancer/arrêter l’enregistrement
|
||||
Pause/unpause recording:=Pause/reprendre l’enregistrement
|
||||
Start/stop recording a region:=Lancer/arrêter l’enregistrement d’une région
|
||||
Start/stop streaming:=Lancer/arrêter la diffusion
|
||||
Take a screenshot:=Prendre une capture d’écran
|
||||
Take a screenshot of a region:=Prendre une capture d’écran d’une région
|
||||
Start/stop recording with desktop portal:=Lancer/arrêter l’enregistrement via le portail du bureau
|
||||
Take a screenshot with desktop portal:=Prendre une capture via le portail du bureau
|
||||
Start/stop recording a window:=Lancer/arrêter l’enregistrement d’une fenêtre
|
||||
Take a screenshot of a window:=Prendre une capture d’écran d’une fenêtre
|
||||
|
||||
Clear hotkeys=Effacer les raccourcis
|
||||
Reset hotkeys to default=Réinitialiser les raccourcis par défaut
|
||||
|
||||
Enable controller hotkeys?=Activer les raccourcis manette ?
|
||||
Press=Appuyer sur
|
||||
and=et
|
||||
|
||||
Notification speed=Vitesse des notifications
|
||||
Normal=Normale
|
||||
Fast=Rapide
|
||||
|
||||
Language=Langue
|
||||
System language=Langue du système
|
||||
|
||||
Exit program=Quitter le programme
|
||||
Go back to the old UI=Revenir à l’ancienne UI
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=Si vous souhaitez faire un don, vous pouvez le faire sur https://buymeacoffee.com/dec05eba:
|
||||
Donate=Faire un don
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=Tous les dons servent au développement du logiciel (y compris GPU Screen Recorder) \net à l’achat de matériel pour les tests.
|
||||
|
||||
# Subsection headers
|
||||
Global=Général
|
||||
Back=Retour
|
||||
|
||||
Appearance=Apparence
|
||||
Startup=Démarrage
|
||||
Keyboard hotkeys=Raccourcis clavier
|
||||
Controller hotkeys=Raccourcis manette
|
||||
Application options=Options de l’application
|
||||
Application info=Informations sur l’application
|
||||
Donate=Faire un don
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=Version GSR : %s
|
||||
GSR-UI version: %s=Version GSR-UI : %s
|
||||
Flatpak version: %s=Version Flatpak : %s
|
||||
GPU vendor: %s=Constructeur GPU : %s
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=Appuyez sur une combinaison de touches pour le raccourci : "%s"
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=Les touches alphanumériques ne peuvent pas être utilisées seules pour les raccourcis, elles doivent être combinées avec une ou plusieurs de ces touches : Alt, Ctrl, Shift, Super.\nAppuyez sur Échap pour annuler ou Retour arrière pour supprimer le raccourci.
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=Afficher/masquer l’UI
|
||||
Turn replay on/off=Activer/désactiver le replay
|
||||
Save replay=Sauvegarder le replay
|
||||
Save 1 minute replay=Sauvegarder 1min de replay
|
||||
Save 10 minute replay=Sauvegarder 10min de replay
|
||||
Start/stop recording=Lancer/arrêter l’enregistrement
|
||||
Pause/unpause recording=Pause/reprendre l’enregistrement
|
||||
Start/stop recording a region=Lancer/arrêter l’enregistrement d’une région
|
||||
Start/stop recording a window=Lancer/arrêter l’enregistrement d’une fenêtre
|
||||
Start/stop recording with desktop portal=Lancer/arrêter l’enregistrement via le portail du bureau
|
||||
Start/stop streaming=Lancer/arrêter la diffusion
|
||||
Take a screenshot=Prendre une capture d’écran
|
||||
Take a screenshot of a region=Prendre une capture d’écran d’une région
|
||||
Take a screenshot of a window=Prendre une capture d’écran d’une fenêtre
|
||||
Take a screenshot with desktop portal=Prendre une capture via le portail du bureau
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=pour afficher/masquer l’UI
|
||||
to take a screenshot=pour prendre une capture d’écran
|
||||
to save a replay=pour sauvegarder le replay
|
||||
to start/stop recording=pour démarrer/arrêter l’enregistrement
|
||||
to turn replay on/off=pour activer/désactiver le replay
|
||||
to save a 1 minute replay=pour sauvegarder 1min de replay
|
||||
to save a 10 minute replay=pour sauvegarder 10min de replay
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=Le raccourci %s est déjà utilisé pour une autre action
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=Capture d’écran
|
||||
Capture=Capture
|
||||
Image=Image
|
||||
File info=Infos fichier
|
||||
General=Général
|
||||
Screenshot indicator=Indicateur de capture
|
||||
Script=Script
|
||||
File=Fichier
|
||||
|
||||
Back=Retour
|
||||
Save=Enregistrer
|
||||
Cancel=Annuler
|
||||
|
||||
Capture source:=Source de capture
|
||||
Window=Fenêtre
|
||||
Region=Région
|
||||
Desktop portal=Portail du bureau
|
||||
Monitor %s (%dx%d)=Moniteur %s (%dx%d)
|
||||
Screen=Écran
|
||||
|
||||
Image resolution limit:=Limite de résolution de l’image
|
||||
Change image resolution=Modifier la résolution de l’image
|
||||
Restore portal session=Restaurer la session du portail
|
||||
|
||||
Image quality:=Qualité de l’image
|
||||
Medium=Moyenne
|
||||
High=Haute
|
||||
Very high (Recommended)=Très haute (Recommandé)
|
||||
Ultra=Ultra
|
||||
|
||||
Record cursor=Enregistrer le curseur
|
||||
|
||||
Directory to save screenshots:=Répertoire de sauvegarde des captures
|
||||
Image format:=Format de l’image
|
||||
|
||||
Save screenshot in a folder based on the games name=Enregistrer la capture dans un dossier nommé selon le jeu
|
||||
|
||||
Save screenshot to clipboard=Copier la capture dans le presse-papiers
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=Copier la capture dans le presse-papiers (non pleinement supporté par Wayland)
|
||||
Save screenshot to disk=Enregistrer la capture sur le disque
|
||||
|
||||
Show screenshot notifications=Afficher les notifications de capture
|
||||
Blink scroll lock led when taking a screenshot=Faire clignoter la LED d'Arrêt Défil. lors de la capture d'écran
|
||||
|
||||
Command to open the screenshot with:=Commande pour ouvrir la capture
|
||||
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=Vue simple
|
||||
Advanced view=Vue avancée
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=Suivre la fenêtre active
|
||||
Focused monitor=Moniteur actif
|
||||
|
||||
Area size:=
|
||||
Area size:=Taille de la zone
|
||||
Video resolution limit:=Limite de résolution vidéo
|
||||
Change video resolution=Modifier la résolution vidéo
|
||||
Restore portal session=Restaurer la session du portail
|
||||
|
||||
# Webcam settings
|
||||
Webcam=Webcam
|
||||
Webcam source=Source webcam
|
||||
None=Aucune
|
||||
Video format=Format vidéo
|
||||
Auto (recommended)=Automatique (recommandé)
|
||||
YUYV=YUYV
|
||||
Motion-JPEG=Motion-JPEG
|
||||
Video setup=Configuration vidéo
|
||||
* Right click in the bottom right corner to resize the webcam=* Clic droit en bas à droite pour redimensionner la webcam
|
||||
Flip camera horizontally=Inverser la caméra horizontalement
|
||||
|
||||
# Audio settings
|
||||
Audio=Audio
|
||||
Audio codec=Codec audio
|
||||
Opus (Recommended)=Opus (Recommandé)
|
||||
AAC=AAC
|
||||
|
||||
Directory to save videos:=Répertoire pour sauvegarder les vidéos
|
||||
Output device:=Périphérique de sortie
|
||||
Input device: =Périphérique d’entrée
|
||||
Application: =Application :
|
||||
Custom...=Personnalisé...
|
||||
|
||||
Save video in a folder based on the games name%s=Enregistrer la vidéo dans un dossier nommé selon le jeu %s
|
||||
(X11 applications only)=(Applications X11 uniquement)
|
||||
|
||||
Add audio track=Ajouter une piste audio
|
||||
Add input device=Ajouter appareil d’entrée
|
||||
Add output device=Ajouter appareil de sortie
|
||||
Add application audio=Ajouter l’audio d’application
|
||||
Record audio from all applications except the selected ones=Enregistrer l’audio de toutes les applications sauf celles sélectionnées
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=L’enregistrement des périphériques de sortie et de l’audio d’application peut enregistrer tout l’audio de sortie, ce qui n’est probablement pas souhaité.\nSupprimez les périphériques de sortie.
|
||||
|
||||
Video=Vidéo
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=Codec vidéo
|
||||
H264=H264
|
||||
HEVC=HEVC
|
||||
HEVC (10 bit, reduces banding)=HEVC (10 bits, réduit le banding)
|
||||
HEVC (HDR)=HEVC (HDR)
|
||||
AV1=AV1
|
||||
AV1 (10 bit, reduces banding)=AV1 (10 bits, réduit le banding)
|
||||
AV1 (HDR)=AV1 (HDR)
|
||||
VP8=VP8
|
||||
VP9=VP9
|
||||
H264 Software Encoder (Slow, not recommended)=Encodeur logiciel H264 (lent, non recommandé)
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=Qualité vidéo
|
||||
Very high=Très élevée
|
||||
Video bitrate (Kbps):=Débit vidéo (Kbps)
|
||||
Constant bitrate=Débit constant
|
||||
Constant bitrate (Recommended)=Débit constant (recommandé)
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=Fréquence d’images
|
||||
Frame rate mode:=Mode de fréquence d’images
|
||||
Auto (Recommended)=Automatique (recommandé)
|
||||
Constant=Constant
|
||||
Variable=Variable
|
||||
Sync to content=Synchroniser au contenu
|
||||
Sync to content (Only X11 or desktop portal capture)=Synchroniser au contenu (Uniquement capture X11 ou portail du bureau)
|
||||
|
||||
# Color range
|
||||
Color range:=Plage de couleurs
|
||||
Limited=Limitée
|
||||
Full=Complète
|
||||
|
||||
# Container format
|
||||
Container:=Format
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=Enregistrer en mode basse consommation
|
||||
Record cursor=Enregistrer le curseur
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=Ne forcez pas le GPU en mode haute performance lors de l’enregistrement.\nPeut affecter les performances, surtout en lisant une vidéo en parallèle.\nSi activé, il est recommandé d’utiliser le mode “Synchroniser au contenu” pour réduire la consommation d’énergie au repos.
|
||||
|
||||
Show %s notifications=Afficher les notifications %s
|
||||
Show %s status with scroll lock LED=Afficher le statut %s via la LED Arrêt Défil
|
||||
Recording indicator=Indicateur d’enregistrement
|
||||
recording=Enregistrement
|
||||
|
||||
Simple=Simple
|
||||
Audio track #%d=Piste audio #%d
|
||||
Output device=Périphérique de sortie
|
||||
Input device: =Périphérique d’entrée
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=Taille estimée du fichier vidéo par minute (hors audio) : %.2f Mo
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=Répertoire pour sauvegarder les replays
|
||||
Replay indicator=Indicateur de replay
|
||||
replay=replay
|
||||
Turn on replay when starting a fullscreen application%s=Activer le replay lors du lancement d’une application plein écran %s
|
||||
Autostart=Démarrage automatique
|
||||
in RAM=En RAM
|
||||
Replay duration in seconds:=Durée du replay (secondes)
|
||||
Where should temporary replay data be stored?=Où stocker les données temporaires des replays ?
|
||||
RAM=RAM
|
||||
Disk (Not recommended on SSDs)=Disque (Non recommandé sur SSD)
|
||||
Turn on replay when this program starts=Activer le replay au démarrage du programme
|
||||
Turn on replay when power supply is connected=Activer le replay lorsque l’alimentation est branchée
|
||||
Don't turn on replay automatically=Ne pas activer automatiquement le replay
|
||||
Restart replay on save=Redémarrer le replay après sauvegarde
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=Taille maximale estimée du fichier vidéo %s : %.2f Mo.\nModifiez le débit ou la durée de replay pour changer la taille du fichier.
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=Service de diffusion
|
||||
Twitch=Twitch
|
||||
YouTube=YouTube
|
||||
Kick=Kick
|
||||
Rumble=Rumble
|
||||
Custom=Personnalisé
|
||||
Stream URL:=URL du flux
|
||||
Stream key:=Clé de diffusion
|
||||
Streaming info=Infos diffusion
|
||||
Streaming indicator=Indicateur de diffusion
|
||||
streaming=Diffusion
|
||||
419
translations/hu.txt
Normal file
@@ -0,0 +1,419 @@
|
||||
# GPU Screen Recorder UI - Hungarian translation
|
||||
|
||||
# Important warning: we f'ed up a little bit and used %s for both strings and numbers in some places, such as time durations (they're fixed by the moment).
|
||||
# When translating, be careful to use the %d format specifier for numbers in those places.
|
||||
# Note that all translation strings should be on one line in these translation files. Some translations need to be on multiple lines and the newline character
|
||||
# should be replaced with \n.
|
||||
|
||||
# General UI
|
||||
Record=Felvétel
|
||||
Instant Replay=Azonnali visszajátszás
|
||||
Livestream=Élő közvetítés
|
||||
Settings=Beállítások
|
||||
|
||||
# Status messages
|
||||
Off=Kikapcsolva
|
||||
On=Bekapcsolva
|
||||
Not recording=Nincs felvétel
|
||||
Recording=Felvétel folyamatban
|
||||
Not streaming=Nincs közvetítés
|
||||
Streaming=Közvetítés folyamatban
|
||||
Paused=Szüneteltetve
|
||||
|
||||
# Button labels
|
||||
Start=Indítás
|
||||
Stop=Leállítás
|
||||
Stop and save=Leállítás és mentés
|
||||
Pause=Szüneteltetés
|
||||
Unpause=Folytatás
|
||||
Save=Mentés
|
||||
Save 1 min=1 perc mentése
|
||||
Save 10 min=10 perc mentése
|
||||
Turn on=Bekapcsolás
|
||||
Turn off=Kikapcsolás
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=A felvétel szüneteltetve lett
|
||||
Recording has been unpaused=A felvétel folytatva lett
|
||||
Started recording %s=Felvétel elindítva: %s
|
||||
Saved a %s recording of %s\nto "%s"=%s hosszú felvétel mentve erről: %s\nide: "%s"
|
||||
Saved a %s recording of %s=%s hosszú felvétel mentve erről: %s
|
||||
Failed to start/save recording=Nem sikerült elindítani vagy menteni a felvételt
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=A visszajátszás leállt
|
||||
Started replaying %s=Visszajátszás elindítva: %s
|
||||
Saving replay, this might take some time=Visszajátszás mentése, ez eltarthat egy ideig
|
||||
Saved a %s replay of %s\nto "%s"=%s hosszúságú visszajátszás mentve erről: %s\nide: "%s"
|
||||
Saved a %s replay of %s=%s hosszúságú visszajátszás mentve erről: %s
|
||||
Replay stopped because of an error=A visszajátszás hiba miatt leállt
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=A visszajátszás beállításai módosultak.\nA változtatások alkalmazásához újra kell indítani a visszajátszást
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=A közvetítés leállt
|
||||
Started streaming %s=Közvetítés elindítva: %s
|
||||
Streaming stopped because of an error=A közvetítés hiba miatt leállt
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=A közvetítés beállításai módosultak.\nA változtatások alkalmazásához újra kell indítani a közvetítést
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=Képernyőkép mentve (%s)\nide: "%s"
|
||||
Saved a screenshot of %s=Képernyőkép mentve (%s)
|
||||
Failed to take a screenshot=Nem sikerült a képernyőkép elkészítése
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=A GPU Screen Recorder UI már fut.\nNyomd meg az Alt+Z-t a felület megnyitásához.
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=A GPU Screen Recorder már fut egy másik folyamatban.\nZárd be, mielőtt használod a felületet.
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=Nem sikerült elindítani a visszajátszást, a rögzítési cél "%s" érvénytelen.\nMódosítsd a rögzítési célt a beállításokban.
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=Nem sikerült elindítani a felvételt, a rögzítési cél "%s" érvénytelen.\nMódosítsd a rögzítési célt a beállításokban.
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=Nem sikerült elindítani a közvetítést, a rögzítési cél "%s" érvénytelen.\nMódosítsd a rögzítési célt a beállításokban.
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=Nem sikerült a képernyőkép elkészítése, a rögzítési cél "%s" érvénytelen.\nMódosítsd a rögzítési célt a beállításokban.
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=Nem lehet felvételt indítani, ha a visszajátszás be van kapcsolva.\nA felvétel előtt kapcsold ki a visszajátszást.
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=Nem lehet közvetítést indítani, ha a visszajátszás be van kapcsolva.\nA közvetítés előtt kapcsold ki a visszajátszást.
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=Nem lehet közvetítést indítani felvétel közben.\nÁllítsd le a felvételt a közvetítéshez.
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=Nem lehet felvételt indítani közvetítés közben.\nÁllítsd le a közvetítést a felvételhez.
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=Nem lehet visszajátszást indítani felvétel közben.\nÁllítsd le a felvételt a visszajátszáshoz.
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=Nem lehet visszajátszást indítani közvetítés közben.\nÁllítsd le a közvetítést a visszajátszáshoz.
|
||||
|
||||
Started recording in the replay session=Felvétel indítva visszajátszás közben
|
||||
Started recording in the streaming session=Felvétel indítva közvetítés közben
|
||||
|
||||
Failed to start region capture=Nem sikerült a terület rögzítése
|
||||
Failed to start window capture=Nem sikerült az ablak rögzítése
|
||||
No window selected=Nincs kiválasztott ablak
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=A közvetítés hiba miatt leállt. Ellenőrizd a beállításokat
|
||||
%s. Verify if settings are correct=%s. Ellenőrizd a beállításokat
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=Az asztali portál rögzítése sikertelen.\nLehet, hogy megszakítottad, vagy a Wayland nem támogatja,\nvagy nincs megfelelően beállítva.
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=A monitor rögzítése sikertelen.\nA kiválasztott monitor érvénytelen.\nEllenőrizd a beállításokat.
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=A rögzítés sikertelen. A H264, HEVC és AV1 kodekek nem támogatottak\nvagy túl nagy felbontást választottál.
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=A rögzítés sikertelen. A rendszer nem támogatja a választott felbontást.\nVálts felbontást vagy kodeket.\nMegjegyzés: A nagyobb felbontásokat leginkább az AV1 támogatja, ezt követi a HEVC, majd a H264.
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=A rögzítés sikertelen. A választott kodek nem támogatott.\nVálassz másikat.
|
||||
Stopped capture because the user canceled the desktop portal=A rögzítés leállt, mert a felhasználó megszakította a portált
|
||||
Failed to take a screenshot. Verify if settings are correct=Nem sikerült a képernyőkép elkészítése. Ellenőrizd a beállításokat
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=Nem sikerült elindítani a gpu-screen-recorder-t a visszajátszáshoz
|
||||
Failed to launch gpu-screen-recorder to start recording=Nem sikerült elindítani a gpu-screen-recorder-t a felvételhez
|
||||
Failed to launch gpu-screen-recorder to start streaming=Nem sikerült elindítani a gpu-screen-recorder-t a közvetítéshez
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=Nem sikerült elindítani a gpu-screen-recorder-t képernyőképhez
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=Nem sikerült hozzáadni a GPU Screen Recordert az automatikus indításhoz
|
||||
Failed to remove GPU Screen Recorder from system startup=Nem sikerült eltávolítani a GPU Screen Recorder-t az automatikus indításból
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=Automatikus indításhoz: telepítsd és konfiguráld a 'dex'-et (ajánlott), vagy add hozzá manuálisan: '%s'
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=Az automatikus indítás systemd-ről XDG autostartra váltott át
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=A GPU Screen Recorder systemd-alapú automatikus indítása elavult.\nAz átköltöztetéshez telepítsd és konfiguráld a 'dex'-et, vagy add hozzá manuálisan: '%s'
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=A Wayland nem támogatja megfelelően a GPU Screen Recordert.\nHasználj X11-et, ha problémát észlelsz.
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=Egyes billentyűzet-átkötő programok ütköznek a GPU Screen Recorderrel.\nA billentyűzet lefoglalása megszűnt, a gyorsbillentyűket mostantól az alkalmazások fogják megkapni.
|
||||
|
||||
# Capture targets
|
||||
this monitor=Jelenlegi monitor
|
||||
window=Ablak
|
||||
window "%s"="%s" ablak
|
||||
window %s=%s ablak
|
||||
focused=Fókuszált ablak
|
||||
region=Kijelölt terület
|
||||
portal=Asztali portál
|
||||
|
||||
# if your language has simple plural forms, you can just use:
|
||||
%d second=%d másodperc
|
||||
%d minute=%d perc
|
||||
%d hour=%d óra
|
||||
%d seconds=%d másodperc
|
||||
%d minutes=%d perc
|
||||
%d hours=%d óra
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=Kiemelőszín
|
||||
Red=Piros
|
||||
Green=Zöld
|
||||
Blue=Kék
|
||||
|
||||
Start program on system startup?=Program indítása rendszerindításkor?
|
||||
Yes=Igen
|
||||
No=Nem
|
||||
|
||||
Enable keyboard hotkeys?=Gyorsbillentyűk engedélyezése?
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=Igen, de csak virtuális eszközök kezelése (egyes bemenet-átirányító szoftvereket támogat)
|
||||
Yes, but don't grab devices (supports all input remapping software)=Igen, eszközök lefoglalása nélkül (minden bemenet-átirányító szoftvert támogat)
|
||||
|
||||
Show/hide UI:=Felület megjelenítése/elrejtése:
|
||||
Turn replay on/off:=Visszajátszás be/ki:
|
||||
Save replay:=Visszajátszás mentése:
|
||||
Save 1 minute replay:=1 perces visszajátszás mentése:
|
||||
Save 10 minute replay:=10 perces visszajátszás mentése:
|
||||
Start/stop recording:=Felvétel indítása/leállítása:
|
||||
Pause/unpause recording:=Felvétel szüneteltetése/folytatása:
|
||||
Start/stop recording a region:=Kijelölt terület felvételének indítása/leállítása:
|
||||
Start/stop streaming:=Közvetítés indítása/leállítása:
|
||||
Take a screenshot:=Képernyőkép készítése:
|
||||
Take a screenshot of a region:=Képernyőkép készítése kijelölt területről:
|
||||
Start/stop recording with desktop portal:=Felvétel indítása/leállítása asztali portállal:
|
||||
Take a screenshot with desktop portal:=Képernyőkép készítése asztali portállal:
|
||||
Start/stop recording a window:=Ablak felvételének indítása/leállítása:
|
||||
Take a screenshot of a window:=Képernyőkép készítése ablakról:
|
||||
|
||||
Clear hotkeys=Gyorsbillentyűk törlése
|
||||
Reset hotkeys to default=Gyorsbillentyűk visszaállítása alapértelmezettre
|
||||
|
||||
Enable controller hotkeys?=Kontrolleres gyorsbillentyűk engedélyezése?
|
||||
Press=Nyomd meg
|
||||
and=és
|
||||
|
||||
Notification speed=Értesítések sebessége
|
||||
Normal=Normál
|
||||
Fast=Gyors
|
||||
|
||||
Language=Nyelv
|
||||
System language=Rendszer nyelve
|
||||
|
||||
Exit program=Kilépés
|
||||
Go back to the old UI=Visszatérés a régi felülethez
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=Ha szeretnél támogatni, megteheted itt: https://buymeacoffee.com/dec05eba
|
||||
Donate=Adományozás
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=Minden adomány a szoftverek fejlesztésére (beleértve a GPU Screen Recordert)\nés a teszteléshez szükséges hardverek beszerzésére fordítódik.
|
||||
|
||||
# Subsection headers
|
||||
Global=Általános
|
||||
Back=Vissza
|
||||
|
||||
Appearance=Megjelenés
|
||||
Startup=Indítás
|
||||
Keyboard hotkeys=Gyorsbillentyűk
|
||||
Controller hotkeys=Kontrolleres gyorsbillentyűk
|
||||
Application options=Alkalmazás beállításai
|
||||
Application info=Alkalmazásinformációk
|
||||
Donate=Adományozás
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=GSR verzió: %s
|
||||
GSR-UI version: %s=GSR-UI verzió: %s
|
||||
Flatpak version: %s=Flatpak verzió: %s
|
||||
GPU vendor: %s=GPU-gyártó: %s
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=Nyomj meg egy billentyűkombinációt a következőhöz: „%s”
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=Az alfanumerikus billentyűk önmagukban nem használhatók gyorsbillentyűként, csak az alábbiakkal együtt: Alt, Ctrl, Shift vagy Super.\nNyomd meg az Esc-et a megszakításhoz vagy a Backspace-t a törléshez.
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=Felület megjelenítése/elrejtése
|
||||
Turn replay on/off=Visszajátszás be/ki
|
||||
Save replay=Visszajátszás mentése
|
||||
Save 1 minute replay=1 perces visszajátszás mentése
|
||||
Save 10 minute replay=10 perces visszajátszás mentése
|
||||
Start/stop recording=Felvétel indítása/leállítása
|
||||
Pause/unpause recording=Felvétel szüneteltetése/folytatása
|
||||
Start/stop recording a region=Kijelölt terület felvételének indítása/leállítása
|
||||
Start/stop recording a window=Ablak felvételének indítása/leállítása
|
||||
Start/stop recording with desktop portal=Felvétel indítása/leállítása asztali portállal
|
||||
Start/stop streaming=Közvetítés indítása/leállítása
|
||||
Take a screenshot=Képernyőkép készítése
|
||||
Take a screenshot of a region=Képernyőkép készítése kijelölt területről
|
||||
Take a screenshot of a window=Képernyőkép készítése ablakról
|
||||
Take a screenshot with desktop portal=Képernyőkép készítése asztali portállal
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=a felület megjelenítéséhez/elrejtéséhez
|
||||
to take a screenshot=képernyőkép készítéséhez
|
||||
to save a replay=visszajátszás mentéséhez
|
||||
to start/stop recording=felvétel indításához/leállításához
|
||||
to turn replay on/off=visszajátszás be-/kikapcsolásához
|
||||
to save a 1 minute replay=1 perces visszajátszás mentéséhez
|
||||
to save a 10 minute replay=10 perces visszajátszás mentéséhez
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=A(z) %s gyorsbillentyű már használatban van más művelethez
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=Képernyőkép
|
||||
Capture=Rögzítés
|
||||
Image=Kép
|
||||
File info=Fájlinformációk
|
||||
General=Általános
|
||||
Screenshot indicator=Képernyőkép-jelző
|
||||
Script=Parancsfájl
|
||||
File=Fájl
|
||||
|
||||
Back=Vissza
|
||||
Save=Mentés
|
||||
Cancel=Mégse
|
||||
|
||||
Capture source:=Rögzítési forrás:
|
||||
Window=Ablak
|
||||
Region=Kijelölt terület
|
||||
Desktop portal=Asztali portál
|
||||
Monitor %s (%dx%d)=%s monitor (%dx%d)
|
||||
Screen=Képernyő
|
||||
|
||||
Image resolution limit:=Képfelbontás korlát:
|
||||
Change image resolution=Képfelbontás módosítása
|
||||
Restore portal session=Portál munkamenet visszaállítása
|
||||
|
||||
Image quality:=Képminőség:
|
||||
Medium=Közepes
|
||||
High=Magas
|
||||
Very high (Recommended)=Nagyon magas (ajánlott)
|
||||
Ultra=Ultra
|
||||
|
||||
Record cursor=Egérmutató felvétele
|
||||
|
||||
Directory to save screenshots:=Képernyőképek mentési mappája:
|
||||
Image format:=Képformátum:
|
||||
|
||||
Save screenshot in a folder based on the games name=Képernyőkép mentése a játék neve szerinti mappába
|
||||
|
||||
Save screenshot to clipboard=Képernyőkép mentése vágólapra
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=Képernyőkép mentése vágólapra (Wayland alatt nem teljesen támogatott)
|
||||
Save screenshot to disk=Képernyőkép mentése lemezre
|
||||
|
||||
Show screenshot notifications=Képernyőkép értesítések megjelenítése
|
||||
Blink scroll lock led when taking a screenshot=Scroll Lock LED villogtatása képernyőkép készítésekor
|
||||
|
||||
Command to open the screenshot with:=Parancs a képernyőkép megnyitásához:
|
||||
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=Egyszerű nézet
|
||||
Advanced view=Speciális nézet
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=Fókuszált ablak követése
|
||||
Focused monitor=Fókuszált monitor
|
||||
|
||||
Area size:=Terület mérete:
|
||||
Video resolution limit:=Videófelbontás korlát:
|
||||
Change video resolution=Videófelbontás módosítása
|
||||
Restore portal session=Portál munkamenet visszaállítása
|
||||
|
||||
# Webcam settings
|
||||
Webcam=Webkamera
|
||||
Webcam source:=Webkamera forrása:
|
||||
None=Nincs
|
||||
Video format:=Videóformátum:
|
||||
Auto (recommended)=Automatikus (ajánlott)
|
||||
YUYV=YUYV
|
||||
Motion-JPEG=Motion-JPEG
|
||||
Video setup:=Videóbeállítás
|
||||
* Right click in the bottom right corner to resize the webcam=* Jobb kattintás a jobb alsó sarokban a webkamera átméretezéséhez
|
||||
Flip camera horizontally=Kamera tükrözése vízszintesen
|
||||
|
||||
# Audio settings
|
||||
Audio=Hang
|
||||
Audio codec:=Hangkodek:
|
||||
Opus (Recommended)=Opus (ajánlott)
|
||||
AAC=AAC
|
||||
|
||||
Directory to save videos:=Videók mentési mappája:
|
||||
Output device:=Kimeneti eszköz:
|
||||
Input device: =Bemeneti eszköz:
|
||||
# yes, these spaces are intentional
|
||||
Application: =Alkalmazás:
|
||||
Custom...=Egyéni...
|
||||
|
||||
Save video in a folder based on the games name%s=Videó mentése a játék neve szerinti mappába%s
|
||||
(X11 applications only)= (csak X11 alkalmazásoknál)
|
||||
|
||||
Add audio track=Hangsáv hozzáadása
|
||||
Add input device=Bemeneti eszköz hozzáadása
|
||||
Add output device=Kimeneti eszköz hozzáadása
|
||||
Add application audio=Alkalmazáshang hozzáadása
|
||||
Record audio from all applications except the selected ones=Hang rögzítése minden alkalmazásból a kiválasztottak kivételével
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=A kimeneti eszközök és az alkalmazáshang rögzítése minden kimeneti hangot felvehet, ami valószínűleg\nnem kívánt. Távolítsd el a kimeneti eszközöket.
|
||||
|
||||
Video=Videó
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=Videókodek:
|
||||
H264=H264
|
||||
HEVC=HEVC
|
||||
HEVC (10 bit, reduces banding)=HEVC (10 bit, csökkenti a sávosodást)
|
||||
HEVC (HDR)=HEVC (HDR)
|
||||
AV1=AV1
|
||||
AV1 (10 bit, reduces banding)=AV1 (10 bit, csökkenti a sávosodást)
|
||||
AV1 (HDR)=AV1 (HDR)
|
||||
VP8=VP8
|
||||
VP9=VP9
|
||||
H264 Software Encoder (Slow, not recommended)=H264 szoftveres kódoló (lassú, nem ajánlott)
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=Videóminőség:
|
||||
Very high=Nagyon magas
|
||||
Video bitrate (Kbps):=Videó bitráta (Kbps):
|
||||
Constant bitrate=Állandó bitráta
|
||||
Constant bitrate (Recommended)=Állandó bitráta (ajánlott)
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=Képkockasebesség:
|
||||
Frame rate mode:=Képkockasebesség mód:
|
||||
Auto (Recommended)=Automatikus (ajánlott)
|
||||
Constant=Állandó
|
||||
Variable=Változó
|
||||
Sync to content=Tartalomhoz igazítás
|
||||
Sync to content (Only X11 or desktop portal capture)=Tartalomhoz igazítás (csak X11 vagy asztali portál rögzítésnél)
|
||||
|
||||
# Color range
|
||||
Color range:=Színtartomány:
|
||||
Limited=Korlátozott
|
||||
Full=Teljes
|
||||
|
||||
# Container format
|
||||
Container:=Konténer:
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=Felvétel energiatakarékos módban
|
||||
Record cursor=Egérmutató felvétele
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=Ne kényszerítse a GPU-t nagy teljesítményű módba felvétel közben.\nEz befolyásolhatja a felvétel teljesítményét, különösen videólejátszás közben.\nHa engedélyezve van, ajánlott a tartalomhoz igazított képkockasebesség mód használata az energiafogyasztás csökkentésére üresjáratban.
|
||||
|
||||
Show %s notifications=%s értesítések megjelenítése
|
||||
Show %s status with scroll lock LED=%s állapot jelzése Scroll Lock LED-del
|
||||
Recording indicator=Felvétel-jelző
|
||||
recording=felvétel
|
||||
|
||||
Simple=Egyszerű
|
||||
Audio track #%d=Hangsáv #%d
|
||||
Output device=Kimeneti eszköz
|
||||
Input device: =Bemeneti eszköz:
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=Várható videófájl-méret percenként (hang nélkül): %.2fMB
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=Visszajátszások mentési mappája:
|
||||
Replay indicator=Visszajátszás-jelző
|
||||
replay=visszajátszás
|
||||
Turn on replay when starting a fullscreen application%s=Visszajátszás bekapcsolása teljes képernyős alkalmazás indításakor%s
|
||||
Autostart=Automatikus indítás
|
||||
in RAM=RAM-ban
|
||||
Replay duration in seconds:=Visszajátszás hossza másodpercben:
|
||||
Where should temporary replay data be stored?=Hol legyenek tárolva az ideiglenes visszajátszási adatok?
|
||||
RAM=RAM
|
||||
Disk (Not recommended on SSDs)=Lemez (SSD-n nem ajánlott)
|
||||
Turn on replay when this program starts=Visszajátszás bekapcsolása a program indításakor
|
||||
Turn on replay when power supply is connected=Visszajátszás bekapcsolása tápellátás csatlakoztatásakor
|
||||
Don't turn on replay automatically=Ne kapcsolja be automatikusan a visszajátszást
|
||||
Restart replay on save=Visszajátszás újraindítása mentéskor
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=Becsült maximális videófájl-méret %s: %.2fMB.\nA fájlméret módosításához változtasd meg a bitrátát vagy a visszajátszás hosszát.
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=Közvetítési szolgáltatás:
|
||||
Twitch=Twitch
|
||||
YouTube=YouTube
|
||||
Kick=Kick
|
||||
Rumble=Rumble
|
||||
Custom=Egyéni
|
||||
Stream URL:=Közvetítési URL:
|
||||
Stream key:=Közvetítési kulcs:
|
||||
Streaming info=Közvetítési információk
|
||||
Streaming indicator=Közvetítés-jelző
|
||||
streaming=közvetítés
|
||||
413
translations/ru.txt
Normal file
@@ -0,0 +1,413 @@
|
||||
# GPU Screen Recorder UI - Russian Translation
|
||||
|
||||
# General UI
|
||||
Record=Запись
|
||||
Instant Replay=Мгновенный повтор
|
||||
Livestream=Прямая трансляция
|
||||
Settings=Настройки
|
||||
|
||||
# Status messages
|
||||
Off=Выкл
|
||||
On=Вкл
|
||||
Not recording=Запись неактивна
|
||||
Recording=Идёт запись
|
||||
Not streaming=Трансляция неактивна
|
||||
Streaming=Идёт трансляция
|
||||
Paused=Приостановлено
|
||||
|
||||
# Button labels
|
||||
Start=Пуск
|
||||
Stop=Остановить
|
||||
Stop and save=Стоп и сохранить
|
||||
Pause=Пауза
|
||||
Unpause=Продолжить
|
||||
Save=Сохранить
|
||||
Save 1 min=Сохр. 1 мин
|
||||
Save 10 min=Сохр. 10 мин
|
||||
Turn on=Включить
|
||||
Turn off=Выключить
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=Запись приостановлена
|
||||
Recording has been unpaused=Запись возобновлена
|
||||
Started recording %s=Начата запись %s
|
||||
Saved a %s recording of %s\nto "%s"=Сохранено %s записи %s\nв "%s"
|
||||
Saved a %s recording of %s=Сохранено %s записи %s
|
||||
Failed to start/save recording=Не удалось начать/сохранить запись
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=Повтор остановлен
|
||||
Started replaying %s=Начат повтор %s
|
||||
Saving replay, this might take some time=Сохранение повтора, это может занять некоторое время
|
||||
Saved a %s replay of %s\nto "%s"=Сохранено %s повтора %s\nв "%s"
|
||||
Saved a %s replay of %s=Сохранено %s повтора %s
|
||||
Replay stopped because of an error=Повтор остановлен из-за ошибки
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=Настройки повтора изменены.\nВозможно, потребуется перезапустить повтор для применения изменений.
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=Трансляция остановлена
|
||||
Started streaming %s=Начата трансляция %s
|
||||
Streaming stopped because of an error=Трансляция остановлена из-за ошибки
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=Настройки трансляции изменены.\nВозможно, потребуется перезапустить трансляцию для применения изменений.
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=Сохранён снимок %s\nв "%s"
|
||||
Saved a screenshot of %s=Сохранён снимок %s
|
||||
Failed to take a screenshot=Не удалось сделать снимок экрана
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=Один экземпляр GPU Screen Recorder UI уже запущен.\nНажмите Alt+Z, чтобы открыть интерфейс.
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=GPU Screen Recorder CLI уже запущен в другом процессе.\nПожалуйста, закройте его перед использованием GPU Screen Recorder UI.
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=Не удалось начать повтор, цель захвата "%s" недействительна.\nПожалуйста, измените цель захвата в настройках
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=Не удалось начать запись, цель захвата "%s" недействительна.\nПожалуйста, измените цель захвата в настройках
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=Не удалось начать трансляцию, цель захвата "%s" недействительна.\nПожалуйста, измените цель захвата в настройках
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=Не удалось сделать снимок экрана, цель захвата "%s" недействительна.\nПожалуйста, измените цель захвата в настройках
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=Невозможно начать запись при включённом повторе.\nВыключите повтор перед началом записи.
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=Невозможно начать трансляцию при включённом повторе.\nВыключите повтор перед началом трансляции.
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=Невозможно начать трансляцию при записи.\nОстановите запись перед началом трансляции.
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=Невозможно начать запись при трансляции.\nОстановите трансляцию перед началом записи.
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=Невозможно начать повтор при записи.\nОстановите запись перед началом повтора.
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=Невозможно начать повтор при трансляции.\nОстановите трансляцию перед началом повтора.
|
||||
|
||||
Started recording in the replay session=Начата запись в сеансе повтора
|
||||
Started recording in the streaming session=Начата запись в сеансе трансляции
|
||||
|
||||
Failed to start region capture=Не удалось начать захват региона
|
||||
Failed to start window capture=Не удалось начать захват окна
|
||||
No window selected=Окно не выбрано
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=Трансляция остановлена из-за ошибки. Проверьте правильность настроек
|
||||
%s. Verify if settings are correct=%s. Проверьте правильность настроек
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=Захват портала рабочего стола не удался.\nВы либо отменили портал рабочего стола, либо ваш композитор Wayland не поддерживает захват портала рабочего стола\nили он неправильно настроен в вашей системе.
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=Захват монитора не удался.\nМонитор, который вы пытаетесь захватить, недействителен.\nПожалуйста, проверьте настройки захвата.
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=Захват не удался. Ни один из видеокодеков H264, HEVC или AV1 не поддерживается\nвашей системой или вы пытаетесь захватить видео с разрешением выше, чем\nподдерживает ваша система для каждого видеокодека.
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=Захват не удался. Ваша система не поддерживает разрешение, с которым вы пытаетесь\nзаписывать с выбранным видеокодеком.\nИзмените разрешение захвата или видеокодек и попробуйте снова.\nПримечание: AV1 поддерживает максимальное разрешение, затем HEVC, затем H264.
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=Захват не удался. Ваша система не поддерживает выбранный видеокодек.\nИзмените видеокодек и попробуйте снова.
|
||||
Stopped capture because the user canceled the desktop portal=Захват остановлен, так как пользователь отменил портал рабочего стола
|
||||
Failed to take a screenshot. Verify if settings are correct=Не удалось сделать снимок экрана. Проверьте правильность настроек
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=Не удалось запустить gpu-screen-recorder для начала повтора
|
||||
Failed to launch gpu-screen-recorder to start recording=Не удалось запустить gpu-screen-recorder для начала записи
|
||||
Failed to launch gpu-screen-recorder to start streaming=Не удалось запустить gpu-screen-recorder для начала трансляции
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=Не удалось запустить gpu-screen-recorder для снимка экрана
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=Не удалось добавить GPU Screen Recorder в автозагрузку системы
|
||||
Failed to remove GPU Screen Recorder from system startup=Не удалось удалить GPU Screen Recorder из автозагрузки системы
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=Для включения автозапуска: установите и настройте 'dex' (рекомендуется) или вручную добавьте '%s' в записи автозапуска рабочего стола.
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=Автозапуск GPU Screen Recorder UI переключён с сервиса systemd на XDG-автозапуск.
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=Автозапуск GPU Screen Recorder UI через systemd устарел.\nДля миграции: установите и настройте 'dex' (рекомендуется)\nили вручную добавьте '%s' в записи автозапуска рабочего стола.
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=Wayland не поддерживает интерфейс GPU Screen Recorder должным образом,\nнекоторые функции могут не работать. Используйте X11, если возникнут проблемы.
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=Некоторое программное обеспечение переназначения клавиатуры конфликтует с GPU Screen Recorder в вашей системе.\nЗахват клавиатуры отключён, приложения теперь будут получать нажатия горячих клавиш.
|
||||
|
||||
# Capture targets
|
||||
this monitor=этого монитора
|
||||
window=окна
|
||||
window "%s"=окна "%s"
|
||||
window %s=окна %s
|
||||
focused=активного
|
||||
region=региона
|
||||
portal=портала
|
||||
|
||||
# Time durations (used in recording/replay saved notifications)
|
||||
%d second_one=%d секунда
|
||||
%d second_few=%d секунды
|
||||
%d second_many=%d секунд
|
||||
%d minute_one=%d минута
|
||||
%d minute_few=%d минуты
|
||||
%d minute_many=%d минут
|
||||
%d hour_one=%d час
|
||||
%d hour_few=%d часа
|
||||
%d hour_many=%d часов
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=Акцентный цвет
|
||||
Red=Красный
|
||||
Green=Зелёный
|
||||
Blue=Синий
|
||||
|
||||
Start program on system startup?=Запускать программу при старте системы?
|
||||
Yes=Да
|
||||
No=Нет
|
||||
|
||||
Enable keyboard hotkeys?=Включить горячие клавиши?
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=Да, но только виртуальные устройства (поддержка некоторых программ ремаппинга клавиш)
|
||||
Yes, but don't grab devices (supports all input remapping software)=Да, но не захватывать устройства (поддержка всех программ ремаппинга клавиш)
|
||||
|
||||
Show/hide UI:=Показать/скрыть интерфейс:
|
||||
Turn replay on/off:=Включить/выключить повтор:
|
||||
Save replay:=Сохранить повтор:
|
||||
Save 1 minute replay:=Сохранить 1 минуту повтора:
|
||||
Save 10 minute replay:=Сохранить 10 минут повтора:
|
||||
Start/stop recording:=Начать/остановить запись:
|
||||
Pause/unpause recording:=Приостановить/возобновить запись:
|
||||
Start/stop recording a region:=Начать/остановить запись региона:
|
||||
Start/stop streaming:=Начать/остановить трансляцию:
|
||||
Take a screenshot:=Сделать снимок экрана:
|
||||
Take a screenshot of a region:=Сделать снимок региона:
|
||||
Start/stop recording with desktop portal:=Запись через портал:
|
||||
Take a screenshot with desktop portal:=Сделать снимок через портал рабочего стола:
|
||||
Start/stop recording a window:=Начать/остановить запись окна:
|
||||
Take a screenshot of a window:=Сделать снимок окна:
|
||||
|
||||
Clear hotkeys=Очистить горячие клавиши
|
||||
Reset hotkeys to default=Сбросить горячие клавиши по умолчанию
|
||||
|
||||
Enable controller hotkeys?=Включить горячие клавиши геймпада?
|
||||
Press=Нажмите
|
||||
and=и
|
||||
|
||||
Notification speed=Скорость уведомлений
|
||||
Normal=Обычная
|
||||
Fast=Быстрая
|
||||
|
||||
Language=Язык
|
||||
System language=Системный язык
|
||||
|
||||
Exit program=Выйти из программы
|
||||
Go back to the old UI=Вернуться к старому интерфейсу
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=Если вы хотите поддержать проект, вы можете сделать это на https://buymeacoffee.com/dec05eba:
|
||||
Donate=Поддержать
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=Все донаты идут на разработку ПО (включая GPU Screen Recorder) и покупку\nоборудования для тестирования.
|
||||
|
||||
# Subsection headers
|
||||
Global=Глобальные
|
||||
Back=Назад
|
||||
|
||||
Appearance=Внешний вид
|
||||
Startup=Автозапуск
|
||||
Keyboard hotkeys=Горячие клавиши клавиатуры
|
||||
Controller hotkeys=Горячие клавиши геймпада
|
||||
Application options=Настройки приложения
|
||||
Application info=Информация о приложении
|
||||
Donate=Поддержать
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=Версия GSR: %s
|
||||
GSR-UI version: %s=Версия GSR-UI: %s
|
||||
Flatpak version: %s=Версия Flatpak: %s
|
||||
GPU vendor: %s=Производитель GPU: %s
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=Нажмите комбинацию клавиш для использования в качестве горячей клавиши: "%s"
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=Буквенно-цифровые клавиши нельзя использовать отдельно, их нужно комбинировать с Alt, Ctrl, Shift или Super.\nНажмите Esc для отмены или Backspace для удаления горячей клавиши.
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=Показать/скрыть интерфейс
|
||||
Turn replay on/off=Включить/выключить повтор
|
||||
Save replay=Сохранить повтор
|
||||
Save 1 minute replay=Сохранить 1 минуту повтора
|
||||
Save 10 minute replay=Сохранить 10 минут повтора
|
||||
Start/stop recording=Начать/остановить запись
|
||||
Pause/unpause recording=Приостановить/возобновить запись
|
||||
Start/stop recording a region=Начать/остановить запись региона
|
||||
Start/stop recording a window=Начать/остановить запись окна
|
||||
Start/stop recording with desktop portal=Запись через портал
|
||||
Start/stop streaming=Начать/остановить трансляцию
|
||||
Take a screenshot=Сделать снимок экрана
|
||||
Take a screenshot of a region=Сделать снимок региона
|
||||
Take a screenshot of a window=Сделать снимок окна
|
||||
Take a screenshot with desktop portal=Сделать снимок через портал рабочего стола
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=для показа/скрытия интерфейса
|
||||
to take a screenshot=для снимка экрана
|
||||
to save a replay=для сохранения повтора
|
||||
to start/stop recording=для начала/остановки записи
|
||||
to turn replay on/off=для включения/выключения повтора
|
||||
to save a 1 minute replay=для сохранения 1 минуты повтора
|
||||
to save a 10 minute replay=для сохранения 10 минут повтора
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=Горячая клавиша %s уже используется для другого действия
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=Снимок экрана
|
||||
Capture=Захват
|
||||
Image=Изображение
|
||||
File info=Информация о файле
|
||||
General=Общие
|
||||
Screenshot indicator=Индикатор снимка
|
||||
Script=Скрипт
|
||||
File=Файл
|
||||
|
||||
Back=Назад
|
||||
Save=Сохранить
|
||||
Cancel=Отмена
|
||||
|
||||
Capture source:=Источник захвата:
|
||||
Window=Окно
|
||||
Region=Регион
|
||||
Desktop portal=Портал рабочего стола
|
||||
Monitor %s (%dx%d)=Монитор %s (%dx%d)
|
||||
Screen=Экран
|
||||
|
||||
Image resolution limit:=Ограничение разрешения изображения:
|
||||
Change image resolution=Изменять разрешение изображения
|
||||
Restore portal session=Восстановить сессию портала
|
||||
|
||||
Image quality:=Качество изображения:
|
||||
Medium=Среднее
|
||||
High=Высокое
|
||||
Very high (Recommended)=Очень высокое (рекомендуется)
|
||||
Ultra=Ультра
|
||||
|
||||
Record cursor=Записывать курсор
|
||||
|
||||
Directory to save screenshots:=Каталог для сохранения снимков:
|
||||
Image format:=Формат изображения:
|
||||
|
||||
Save screenshot in a folder based on the games name=Сохраните скриншот в папку, названную в честь игры
|
||||
|
||||
Save screenshot to clipboard=Сохранять снимок в буфер обмена
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=Сохранять снимок в буфер обмена (в Wayland поддерживается некорректно)
|
||||
Save screenshot to disk=Сохранять снимок на диск
|
||||
|
||||
Show screenshot notifications=Показывать уведомления о снимках
|
||||
Blink scroll lock led when taking a screenshot=Мигать индикатором Scroll Lock при создании снимка
|
||||
|
||||
Command to open the screenshot with:=Команда для открытия снимка:
|
||||
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=Простой вид
|
||||
Advanced view=Расширенный вид
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=Следовать за активным окном
|
||||
Focused monitor=Активный монитор
|
||||
|
||||
Area size:=Размер области:
|
||||
Video resolution limit:=Ограничение разрешения видео:
|
||||
Change video resolution=Изменять разрешение видео
|
||||
Restore portal session=Восстановить сессию портала
|
||||
|
||||
# Webcam settings
|
||||
Webcam=Веб-камера
|
||||
Webcam source:=Источник веб-камеры:
|
||||
None=Нет
|
||||
Video format:=Формат видео:
|
||||
Auto (recommended)=Авто (рекомендуется)
|
||||
YUYV=YUYV
|
||||
Motion-JPEG=Motion-JPEG
|
||||
Video setup:=Настройка видео:
|
||||
* Right click in the bottom right corner to resize the webcam=* Нажмите правой кнопкой мыши в нижнем правом углу для изменения размера веб-камеры
|
||||
Flip camera horizontally=Отразить камеру горизонтально
|
||||
|
||||
# Audio settings
|
||||
Audio=Аудио
|
||||
Audio codec:=Аудиокодек:
|
||||
Opus (Recommended)=Opus (рекомендуется)
|
||||
AAC=AAC
|
||||
|
||||
Directory to save videos:=Каталог для сохранения видео:
|
||||
Output device:=Выходное устройство:
|
||||
Input device: =Входное устройство:
|
||||
Application: =Приложение:
|
||||
Custom...=Другое...
|
||||
|
||||
Save video in a folder based on the games name%s=Сохраните видео в папку, названную в честь игры%s
|
||||
(X11 applications only)= (только X11-приложения)
|
||||
|
||||
Add audio track=Добавить аудиодорожку
|
||||
Add input device=Добавить вход
|
||||
Add output device=Добавить вывод
|
||||
Add application audio=Добавить аудио приложения
|
||||
Record audio from all applications except the selected ones=Записывать аудио из всех приложений кроме выбранных
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=Запись выходных устройств и аудио приложений может записать весь выходной звук,\nчто, вероятно, не то, что вы хотите. Удалите выходные устройства.
|
||||
|
||||
Video=Видео
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=Видеокодек:
|
||||
H264=H264
|
||||
HEVC=HEVC
|
||||
HEVC (10 bit, reduces banding)=HEVC (10 бит, уменьшает полосы)
|
||||
HEVC (HDR)=HEVC (HDR)
|
||||
AV1=AV1
|
||||
AV1 (10 bit, reduces banding)=AV1 (10 бит, уменьшает полосы)
|
||||
AV1 (HDR)=AV1 (HDR)
|
||||
VP8=VP8
|
||||
VP9=VP9
|
||||
H264 Software Encoder (Slow, not recommended)=H264 программный кодировщик \n(медленно, не рекомендуется)
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=Качество видео:
|
||||
Very high=Очень высокое
|
||||
Video bitrate (Kbps):=Битрейт видео (Кбит/с):
|
||||
Constant bitrate=Постоянный битрейт
|
||||
Constant bitrate (Recommended)=Постоянный битрейт (рекомендуется)
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=Частота кадров:
|
||||
Frame rate mode:=Режим частоты кадров:
|
||||
Auto (Recommended)=Авто (рекомендуется)
|
||||
Constant=Постоянный
|
||||
Variable=Переменный
|
||||
Sync to content=Синхр. с контентом
|
||||
Sync to content (Only X11 or desktop portal capture)=Синхр. с контентом (только X11 или захват портала)
|
||||
|
||||
# Color range
|
||||
Color range:=Цветовой диапазон:
|
||||
Limited=Ограниченный
|
||||
Full=Полный
|
||||
|
||||
# Container format
|
||||
Container:=Формат:
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=Записывать в режиме низкого энергопотребления
|
||||
Record cursor=Записывать курсор
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=Не заставлять GPU переходить в режим высокой производительности при записи.\nМожет повлиять на производительность записи, особенно при одновременном воспроизведении видео.\nЕсли включено, рекомендуется использовать режим частоты кадров синхронизации с контентом\nдля снижения энергопотребления в режиме ожидания.
|
||||
|
||||
Show %s notifications=Показывать уведомления %s
|
||||
Show %s status with scroll lock LED=Показывать статус %s с помощью индикатора Scroll Lock
|
||||
Recording indicator=Индикатор записи
|
||||
|
||||
Simple=Простой
|
||||
Audio track #%d=Аудиодорожка #%d
|
||||
Output device=Выходное устройство
|
||||
Input device: =Входное устройство:
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=Примерный размер видео файла в минуту (без учёта аудио): %.2fMB
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=Каталог для сохранения повторов:
|
||||
Replay indicator=Индикатор повтора
|
||||
Turn on replay when starting a fullscreen application%s=Включать повтор при запуске полноэкранного приложения%s
|
||||
Autostart=Автозапуск
|
||||
in RAM=в ОЗУ
|
||||
Replay duration in seconds:=Длительность повтора в секундах:
|
||||
Where should temporary replay data be stored?=Где должны храниться временные данные повтора?
|
||||
RAM=ОЗУ
|
||||
Disk (Not recommended on SSDs)=Диск (не рекомендуется для SSD)
|
||||
Turn on replay when this program starts=Включать повтор при запуске программы
|
||||
Turn on replay when power supply is connected=Включать повтор при подключении источника питания
|
||||
Don't turn on replay automatically=Не включать повтор автоматически
|
||||
Restart replay on save=Перезапускать повтор при сохранении
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=Примерный максимальный размер видео файла %s: %.2fMB.\nИзмените битрейт видео или длительность повтора, чтобы изменить размер файла.
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=Сервис трансляции:
|
||||
Twitch=Twitch
|
||||
YouTube=YouTube
|
||||
Kick=Kick
|
||||
Rumble=Rumble
|
||||
Custom=Другое
|
||||
Stream URL:=URL трансляции:
|
||||
Stream key:=Ключ трансляции:
|
||||
Streaming info=Информация о трансляции
|
||||
Streaming indicator=Индикатор трансляции
|
||||
430
translations/template.txt
Normal file
@@ -0,0 +1,430 @@
|
||||
# GPU Screen Recorder UI - translation template
|
||||
|
||||
# Important warning: we f'ed up a little bit and used %s for both strings and numbers in some places, such as time durations (they're fixed by the moment).
|
||||
# When translating, be careful to use the %d format specifier for numbers in those places.
|
||||
# Note that all translation strings should be on one line in these translation files. Some translations need to be on multiple lines and the newline character
|
||||
# should be replaced with \n.
|
||||
|
||||
# General UI
|
||||
Record=
|
||||
Instant Replay=
|
||||
Livestream=
|
||||
Settings=
|
||||
|
||||
# Status messages
|
||||
Off=
|
||||
On=
|
||||
Not recording=
|
||||
Recording=
|
||||
Not streaming=
|
||||
Streaming=
|
||||
Paused=
|
||||
|
||||
# Button labels
|
||||
Start=
|
||||
Stop=
|
||||
Stop and save=
|
||||
Pause=
|
||||
Unpause=
|
||||
Save=
|
||||
Save 1 min=
|
||||
Save 10 min=
|
||||
Turn on=
|
||||
Turn off=
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=
|
||||
Recording has been unpaused=
|
||||
Started recording %s=
|
||||
Saved a %s recording of %s\nto "%s"=
|
||||
Saved a %s recording of %s=
|
||||
Failed to start/save recording=
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=
|
||||
Started replaying %s=
|
||||
Saving replay, this might take some time=
|
||||
Saved a %s replay of %s\nto "%s"=
|
||||
Saved a %s replay of %s=
|
||||
Replay stopped because of an error=
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=
|
||||
Started streaming %s=
|
||||
Streaming stopped because of an error=
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=
|
||||
Saved a screenshot of %s=
|
||||
Failed to take a screenshot=
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=
|
||||
|
||||
Started recording in the replay session=
|
||||
Started recording in the streaming session=
|
||||
|
||||
Failed to start region capture=
|
||||
Failed to start window capture=
|
||||
No window selected=
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=
|
||||
%s. Verify if settings are correct=
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=
|
||||
Stopped capture because the user canceled the desktop portal=
|
||||
Failed to take a screenshot. Verify if settings are correct=
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=
|
||||
Failed to launch gpu-screen-recorder to start recording=
|
||||
Failed to launch gpu-screen-recorder to start streaming=
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=
|
||||
Failed to remove GPU Screen Recorder from system startup=
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=
|
||||
|
||||
# Capture targets
|
||||
this monitor=
|
||||
window=
|
||||
window "%s"=
|
||||
window %s=
|
||||
focused=
|
||||
region=
|
||||
portal=
|
||||
|
||||
# Time durations (used in recording/replay saved notifications, remember to use %d for numbers)
|
||||
# if you have complex plural forms in your language, please use the following format:
|
||||
%d second_one=
|
||||
%d second_few=
|
||||
%d second_many=
|
||||
%d minute_one=
|
||||
%d minute_few=
|
||||
%d minute_many=
|
||||
%d hour_one=
|
||||
%d hour_few=
|
||||
%d hour_many=
|
||||
# if your language has simple plural forms, you can just use:
|
||||
%d second=
|
||||
%d minute=
|
||||
%d hour=
|
||||
%d seconds=
|
||||
%d minutes=
|
||||
%d hours=
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=
|
||||
Red=
|
||||
Green=
|
||||
Blue=
|
||||
|
||||
Start program on system startup?=
|
||||
Yes=
|
||||
No=
|
||||
|
||||
Enable keyboard hotkeys?=
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=
|
||||
Yes, but don't grab devices (supports all input remapping software)=
|
||||
|
||||
Show/hide UI:=
|
||||
Turn replay on/off:=
|
||||
Save replay:=
|
||||
Save 1 minute replay:=
|
||||
Save 10 minute replay:=
|
||||
Start/stop recording:=
|
||||
Pause/unpause recording:=
|
||||
Start/stop recording a region:=
|
||||
Start/stop streaming:=
|
||||
Take a screenshot:=
|
||||
Take a screenshot of a region:=
|
||||
Start/stop recording with desktop portal:=
|
||||
Take a screenshot with desktop portal:=
|
||||
Start/stop recording a window:=
|
||||
Take a screenshot of a window:=
|
||||
|
||||
Clear hotkeys=
|
||||
Reset hotkeys to default=
|
||||
|
||||
Enable controller hotkeys?=
|
||||
Press=
|
||||
and=
|
||||
|
||||
Notification speed=
|
||||
Normal=
|
||||
Fast=
|
||||
|
||||
Language=
|
||||
System language=
|
||||
|
||||
Exit program=
|
||||
Go back to the old UI=
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=
|
||||
Donate=
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=
|
||||
|
||||
# Subsection headers
|
||||
Global=
|
||||
Back=
|
||||
|
||||
Appearance=
|
||||
Startup=
|
||||
Keyboard hotkeys=
|
||||
Controller hotkeys=
|
||||
Application options=
|
||||
Application info=
|
||||
Donate=
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=
|
||||
GSR-UI version: %s=
|
||||
Flatpak version: %s=
|
||||
GPU vendor: %s=
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=
|
||||
Turn replay on/off=
|
||||
Save replay=
|
||||
Save 1 minute replay=
|
||||
Save 10 minute replay=
|
||||
Start/stop recording=
|
||||
Pause/unpause recording=
|
||||
Start/stop recording a region=
|
||||
Start/stop recording a window=
|
||||
Start/stop recording with desktop portal=
|
||||
Start/stop streaming=
|
||||
Take a screenshot=
|
||||
Take a screenshot of a region=
|
||||
Take a screenshot of a window=
|
||||
Take a screenshot with desktop portal=
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=
|
||||
to take a screenshot=
|
||||
to save a replay=
|
||||
to start/stop recording=
|
||||
to turn replay on/off=
|
||||
to save a 1 minute replay=
|
||||
to save a 10 minute replay=
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=
|
||||
Capture=
|
||||
Image=
|
||||
File info=
|
||||
General=
|
||||
Screenshot indicator=
|
||||
Script=
|
||||
File=
|
||||
|
||||
Back=
|
||||
Save=
|
||||
Cancel=
|
||||
|
||||
Capture source:=
|
||||
Window=
|
||||
Region=
|
||||
Desktop portal=
|
||||
Monitor %s (%dx%d)=
|
||||
Screen=
|
||||
|
||||
Image resolution limit:=
|
||||
Change image resolution=
|
||||
Restore portal session=
|
||||
|
||||
Image quality:=
|
||||
Medium=
|
||||
High=
|
||||
Very high (Recommended)=
|
||||
Ultra=
|
||||
|
||||
Record cursor=
|
||||
|
||||
Directory to save screenshots:=
|
||||
Image format:=
|
||||
|
||||
Save screenshot in a folder based on the games name=
|
||||
|
||||
Save screenshot to clipboard=
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=
|
||||
Save screenshot to disk=
|
||||
|
||||
Show screenshot notifications=
|
||||
Blink scroll lock led when taking a screenshot=
|
||||
|
||||
Command to open the screenshot with:=
|
||||
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=
|
||||
Advanced view=
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=
|
||||
Focused monitor=
|
||||
|
||||
Area size:=
|
||||
Video resolution limit:=
|
||||
Change video resolution=
|
||||
Restore portal session=
|
||||
|
||||
# Webcam settings
|
||||
Webcam=
|
||||
Webcam source:=
|
||||
None=
|
||||
Video format:=
|
||||
Auto (recommended)=
|
||||
YUYV=
|
||||
Motion-JPEG=
|
||||
Video setup:=
|
||||
* Right click in the bottom right corner to resize the webcam=
|
||||
Flip camera horizontally=
|
||||
|
||||
# Audio settings
|
||||
Audio=
|
||||
Audio codec:=
|
||||
Opus (Recommended)=
|
||||
AAC=
|
||||
|
||||
Directory to save videos:=
|
||||
Output device:=
|
||||
Input device: =
|
||||
# yes, these spaces are intentional
|
||||
Application: =
|
||||
Custom...=
|
||||
|
||||
Save video in a folder based on the games name%s=
|
||||
(X11 applications only)=
|
||||
|
||||
Add audio track=
|
||||
Add input device=
|
||||
Add output device=
|
||||
Add application audio=
|
||||
Record audio from all applications except the selected ones=
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=
|
||||
|
||||
Video=
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=
|
||||
H264=
|
||||
HEVC=
|
||||
HEVC (10 bit, reduces banding)=
|
||||
HEVC (HDR)=
|
||||
AV1=
|
||||
AV1 (10 bit, reduces banding)=
|
||||
AV1 (HDR)=
|
||||
VP8=
|
||||
VP9=
|
||||
H264 Software Encoder (Slow, not recommended)=
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=
|
||||
Very high=
|
||||
Video bitrate (Kbps):=
|
||||
Constant bitrate=
|
||||
Constant bitrate (Recommended)=
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=
|
||||
Frame rate mode:=
|
||||
Auto (Recommended)=
|
||||
Constant=
|
||||
Variable=
|
||||
Sync to content=
|
||||
Sync to content (Only X11 or desktop portal capture)=
|
||||
|
||||
# Color range
|
||||
Color range:=
|
||||
Limited=
|
||||
Full=
|
||||
|
||||
# Container format
|
||||
Container:=
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=
|
||||
Record cursor=
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=
|
||||
|
||||
Show %s notifications=
|
||||
Show %s status with scroll lock LED=
|
||||
Recording indicator=
|
||||
recording=
|
||||
|
||||
Simple=
|
||||
Audio track #%d=
|
||||
Output device=
|
||||
Input device: =
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=
|
||||
Replay indicator=
|
||||
replay=
|
||||
Turn on replay when starting a fullscreen application%s=
|
||||
Autostart=
|
||||
in RAM=
|
||||
Replay duration in seconds:=
|
||||
Where should temporary replay data be stored?=
|
||||
RAM=
|
||||
Disk (Not recommended on SSDs)=
|
||||
Turn on replay when this program starts=
|
||||
Turn on replay when power supply is connected=
|
||||
Don't turn on replay automatically=
|
||||
Restart replay on save=
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=
|
||||
Twitch=
|
||||
YouTube=
|
||||
Kick=
|
||||
Rumble=
|
||||
Custom=
|
||||
Stream URL:=
|
||||
Stream key:=
|
||||
Streaming info=
|
||||
Streaming indicator=
|
||||
streaming=
|
||||
411
translations/uk.txt
Normal file
@@ -0,0 +1,411 @@
|
||||
# GPU Screen Recorder UI - Ukrainian Translation
|
||||
|
||||
# General UI
|
||||
Record=Запис
|
||||
Instant Replay=Миттєвий повтор
|
||||
Livestream=Пряма трансляція
|
||||
Settings=Налаштування
|
||||
|
||||
# Status messages
|
||||
Off=Вимкнено
|
||||
On=Увімкнено
|
||||
Not recording=Запис неактивний
|
||||
Recording=Йде запис
|
||||
Not streaming=Трансляція неактивна
|
||||
Streaming=Йде трансляція
|
||||
Paused=Призупинено
|
||||
|
||||
# Button labels
|
||||
Start=Пуск
|
||||
Stop=Зупинити
|
||||
Stop and save=Стоп та зберегти
|
||||
Pause=Пауза
|
||||
Unpause=Продовжити
|
||||
Save=Зберегти
|
||||
Save 1 min=Збер. 1 хв
|
||||
Save 10 min=Збер. 10 хв
|
||||
Turn on=Увімкнути
|
||||
Turn off=Вимкнути
|
||||
|
||||
# Notifications - Recording
|
||||
Recording has been paused=Запис призупинено
|
||||
Recording has been unpaused=Запис відновлено
|
||||
Started recording %s=Розпочато запис %s
|
||||
Saved a %s recording of %s\nto "%s"=Збережено %s запису %s\nу "%s"
|
||||
Saved a %s recording of %s=Збережено %s запису %s
|
||||
Failed to start/save recording=Не вдалося розпочати/зберегти запис
|
||||
|
||||
# Notifications - Replay
|
||||
Replay stopped=Повтор зупинено
|
||||
Started replaying %s=Розпочато повтор %s
|
||||
Saving replay, this might take some time=Збереження повтору, це може зайняти деякий час
|
||||
Saved a %s replay of %s\nto "%s"=Збережено %s повтору %s\nу "%s"
|
||||
Saved a %s replay of %s=Збережено %s повтору %s
|
||||
Replay stopped because of an error=Повтор зупинено через помилку
|
||||
Replay settings have been modified.\nYou may need to restart replay to apply the changes.=Налаштування повтору змінено.\nМожливо, знадобиться перезапустити повтор для застосування змін.
|
||||
|
||||
# Notifications - Streaming
|
||||
Streaming has stopped=Трансляцію зупинено
|
||||
Started streaming %s=Розпочато трансляцію %s
|
||||
Streaming stopped because of an error=Трансляцію зупинено через помилку
|
||||
Streaming settings have been modified.\nYou may need to restart streaming to apply the changes.=Налаштування трансляції змінено.\nМожливо, знадобиться перезапустити трансляцію для застосування змін.
|
||||
|
||||
# Notifications - Screenshot
|
||||
Saved a screenshot of %s\nto "%s"=Збережено знімок %s\nу "%s"
|
||||
Saved a screenshot of %s=Збережено знімок %s
|
||||
Failed to take a screenshot=Не вдалося зробити знімок екрана
|
||||
|
||||
# Error messages
|
||||
Another instance of GPU Screen Recorder UI is already running.\nPress Alt+Z to open the UI.=Один екземпляр GPU Screen Recorder UI вже запущено.\nНатисніть Alt+Z, щоб відкрити інтерфейс.
|
||||
GPU Screen Recorder is already running in another process.\nPlease close it before using GPU Screen Recorder UI.=GPU Screen Recorder CLI вже запущено в іншому процесі.\nБудь ласка, закрийте його перед використанням GPU Screen Recorder UI.
|
||||
Failed to start replay, capture target "%s" is invalid.\nPlease change capture target in settings=Не вдалося розпочати повтор, ціль захоплення "%s" недійсна.\nБудь ласка, змініть ціль захоплення в налаштуваннях
|
||||
Failed to start recording, capture target "%s" is invalid.\nPlease change capture target in settings=Не вдалося розпочати запис, ціль захоплення "%s" недійсна.\nБудь ласка, змініть ціль захоплення в налаштуваннях
|
||||
Failed to start streaming, capture target "%s" is invalid.\nPlease change capture target in settings=Не вдалося розпочати трансляцію, ціль захоплення "%s" недійсна.\nБудь ласка, змініть ціль захоплення в налаштуваннях
|
||||
Failed to take a screenshot, capture target "%s" is invalid.\nPlease change capture target in settings=Не вдалося зробити знімок екрана, ціль захоплення "%s" недійсна.\nБудь ласка, змініть ціль захоплення в налаштуваннях
|
||||
|
||||
Unable to start recording when replay is turned on.\nTurn off replay before starting recording.=Неможливо розпочати запис при увімкненому повторі.\nВимкніть повтор перед початком запису.
|
||||
Unable to start streaming when replay is turned on.\nTurn off replay before starting streaming.=Неможливо розпочати трансляцію при увімкненому повторі.\nВимкніть повтор перед початком трансляції.
|
||||
Unable to start streaming when recording.\nStop recording before starting streaming.=Неможливо розпочати трансляцію при записі.\nЗупиніть запис перед початком трансляції.
|
||||
Unable to start recording when streaming.\nStop streaming before starting recording.=Неможливо розпочати запис при трансляції.\nЗупиніть трансляцію перед початком запису.
|
||||
Unable to start replay when recording.\nStop recording before starting replay.=Неможливо розпочати повтор при записі.\nЗупиніть запис перед початком повтору.
|
||||
Unable to start replay when streaming.\nStop streaming before starting replay.=Неможливо розпочати повтор при трансляції.\nЗупиніть трансляцію перед початком повтору.
|
||||
|
||||
Started recording in the replay session=Розпочато запис у сеансі повтору
|
||||
Started recording in the streaming session=Розпочато запис у сеансі трансляції
|
||||
|
||||
Failed to start region capture=Не вдалося розпочати захоплення регіону
|
||||
Failed to start window capture=Не вдалося розпочати захоплення вікна
|
||||
No window selected=Вікно не вибрано
|
||||
|
||||
Streaming stopped because of an error. Verify if settings are correct=Трансляцію зупинено через помилку. Перевірте правильність налаштувань
|
||||
%s. Verify if settings are correct=%s. Перевірте правильність налаштувань
|
||||
|
||||
# GPU Screen Recorder errors
|
||||
Desktop portal capture failed.\nEither you canceled the desktop portal or your Wayland compositor doesn't support desktop portal capture\nor it's incorrectly setup on your system.=Захоплення порталу робочого стола не вдалося.\nВи або скасували портал робочого стола, або ваш композитор Wayland не підтримує захоплення порталу робочого стола\nабо він неправильно налаштований у вашій системі.
|
||||
Monitor capture failed.\nThe monitor you are trying to capture is invalid.\nPlease validate your capture settings.=Захоплення монітора не вдалося.\nМонітор, який ви намагаєтесь захопити, недійсний.\nБудь ласка, перевірте налаштування захоплення.
|
||||
Capture failed. Neither H264, HEVC nor AV1 video codecs are supported\non your system or you are trying to capture at a resolution higher than your\nsystem supports for each video codec.=Захоплення не вдалося. Жоден з відеокодеків H264, HEVC або AV1 не підтримується\nвашою системою або ви намагаєтесь захопити відео з роздільною здатністю вище, ніж\nпідтримує ваша система для кожного відеокодека.
|
||||
Capture failed. Your system doesn't support the resolution you are trying to\nrecord at with the video codec you have chosen.\nChange capture resolution or video codec and try again.\nNote: AV1 supports the highest resolution, then HEVC and then H264.=Захоплення не вдалося. Ваша система не підтримує роздільну здатність, з якою ви намагаєтесь\nзаписувати з обраним відеокодеком.\nЗмініть роздільну здатність захоплення або відеокодек і спробуйте знову.\nПримітка: AV1 підтримує максимальну роздільну здатність, потім HEVC, потім H264.
|
||||
Capture failed. Your system doesn't support the video codec you have chosen.\nChange video codec and try again.=Захоплення не вдалося. Ваша система не підтримує обраний відеокодек.\nЗмініть відеокодек і спробуйте знову.
|
||||
Stopped capture because the user canceled the desktop portal=Захоплення зупинено, оскільки користувач скасував портал робочого стола
|
||||
Failed to take a screenshot. Verify if settings are correct=Не вдалося зробити знімок екрана. Перевірте правильність налаштувань
|
||||
|
||||
# Launch errors
|
||||
Failed to launch gpu-screen-recorder to start replay=Не вдалося запустити gpu-screen-recorder для початку повтору
|
||||
Failed to launch gpu-screen-recorder to start recording=Не вдалося запустити gpu-screen-recorder для початку запису
|
||||
Failed to launch gpu-screen-recorder to start streaming=Не вдалося запустити gpu-screen-recorder для початку трансляції
|
||||
Failed to launch gpu-screen-recorder to take a screenshot=Не вдалося запустити gpu-screen-recorder для знімка екрана
|
||||
|
||||
# System startup notifications
|
||||
Failed to add GPU Screen Recorder to system startup=Не вдалося додати GPU Screen Recorder до автозавантаження системи
|
||||
Failed to remove GPU Screen Recorder from system startup=Не вдалося видалити GPU Screen Recorder з автозавантаження системи
|
||||
To enable autorun: install and configure 'dex' (recommended), or manually add '%s' to your desktop autostart entries.=Для увімкнення автозапуску: встановіть та налаштуйте 'dex' (рекомендовано) або вручну додайте '%s' до записів автозапуску робочого столу.
|
||||
GPU Screen Recorder UI startup has been switched from systemd service to XDG autostart.=Автозапуск GPU Screen Recorder UI переключено зі служби systemd на XDG-автозапуск.
|
||||
GPU Screen Recorder UI autostart via systemd is deprecated.\nTo migrate: install and configure 'dex' (recommended),\nor manually add '%s' to your desktop autostart entries.=Автозапуск GPU Screen Recorder UI через systemd застарів.\nДля міграції: встановіть та налаштуйте 'dex' (рекомендовано)\nабо вручну додайте '%s' до записів автозапуску робочого столу.
|
||||
|
||||
# Wayland warning
|
||||
Wayland doesn't support GPU Screen Recorder UI properly,\nthings may not work as expected. Use X11 if you experience issues.=Wayland не підтримує інтерфейс GPU Screen Recorder належним чином,\nдеякі функції можуть не працювати. Використовуйте X11, якщо виникнуть проблеми.
|
||||
|
||||
# Hotkey conflicts
|
||||
Some keyboard remapping software conflicts with GPU Screen Recorder on your system.\nKeyboards have been ungrabbed, applications will now receive the hotkeys you press.=Деяке програмне забезпечення перепризначення клавіатури конфліктує з GPU Screen Recorder у вашій системі.\nЗахоплення клавіатури вимкнено, додатки тепер отримуватимуть натискання гарячих клавіш.
|
||||
|
||||
# Capture targets
|
||||
this monitor=цього монітора
|
||||
window=вікна
|
||||
window "%s"=вікна "%s"
|
||||
window %s=вікна %s
|
||||
focused=активного
|
||||
region=регіону
|
||||
portal=порталу
|
||||
|
||||
# Time durations (used in recording/replay saved notifications)
|
||||
%d second_one=%d секунда
|
||||
%d second_few=%d секунди
|
||||
%d second_many=%d секунд
|
||||
%d minute_one=%d хвилина
|
||||
%d minute_few=%d хвилини
|
||||
%d minute_many=%d хвилин
|
||||
%d hour_one=%d година
|
||||
%d hour_few=%d години
|
||||
%d hour_many=%d годин
|
||||
|
||||
|
||||
# Global Settings Page UI elements
|
||||
Accent color=Акцентний колір
|
||||
Red=Червоний
|
||||
Green=Зелений
|
||||
Blue=Синій
|
||||
|
||||
Start program on system startup?=Запускати програму при старті системи?
|
||||
Yes=Так
|
||||
No=Ні
|
||||
|
||||
Enable keyboard hotkeys?=Увімкнути гарячі клавіші?
|
||||
Yes, but only grab virtual devices (supports some input remapping software)=Так, але лише віртуальні пристрої (підтримка деякого ПЗ ремапінгу клавіш)
|
||||
Yes, but don't grab devices (supports all input remapping software)=Так, але не захоплювати пристрої (підтримка всього ПЗ ремапінгу клавіш)
|
||||
|
||||
Show/hide UI:=Показати/сховати інтерфейс:
|
||||
Turn replay on/off:=Увімкнути/вимкнути повтор:
|
||||
Save replay:=Зберегти повтор:
|
||||
Save 1 minute replay:=Зберегти 1 хвилину повтору:
|
||||
Save 10 minute replay:=Зберегти 10 хвилин повтору:
|
||||
Start/stop recording:=Розпочати/зупинити запис:
|
||||
Pause/unpause recording:=Призупинити/відновити запис:
|
||||
Start/stop recording a region:=Розпочати/зупинити запис регіону:
|
||||
Start/stop streaming:=Розпочати/зупинити трансляцію:
|
||||
Take a screenshot:=Зробити знімок екрана:
|
||||
Take a screenshot of a region:=Зробити знімок регіону:
|
||||
Start/stop recording with desktop portal:=Запис через портал:
|
||||
Take a screenshot with desktop portal:=Зробити знімок через портал робочого стола:
|
||||
Start/stop recording a window:=Розпочати/зупинити запис вікна:
|
||||
Take a screenshot of a window:=Зробити знімок вікна:
|
||||
|
||||
Clear hotkeys=Очистити гарячі клавіші
|
||||
Reset hotkeys to default=Скинути гарячі клавіші за замовчуванням
|
||||
|
||||
Enable controller hotkeys?=Увімкнути гарячі клавіші геймпада?
|
||||
Press=Натисніть
|
||||
and=та
|
||||
|
||||
Notification speed=Швидкість сповіщень
|
||||
Normal=Звичайна
|
||||
Fast=Швидка
|
||||
|
||||
Language=Мова
|
||||
System language=Системна мова
|
||||
|
||||
Exit program=Вийти з програми
|
||||
Go back to the old UI=Повернутися до старого інтерфейсу
|
||||
|
||||
If you would like to donate you can do so by donating at https://buymeacoffee.com/dec05eba:=Якщо ви хочете підтримати проєкт, ви можете зробити це на https://buymeacoffee.com/dec05eba:
|
||||
Donate=Підтримати
|
||||
All donations go toward developing software (including GPU Screen Recorder)\nand buying hardware to test the software.=Всі донати йдуть на розробку ПЗ (включно з GPU Screen Recorder) та купівлю\nобладнання для тестування.
|
||||
|
||||
# Subsection headers
|
||||
Global=Глобальні
|
||||
Back=Назад
|
||||
|
||||
Appearance=Зовнішній вигляд
|
||||
Startup=Автозапуск
|
||||
Keyboard hotkeys=Гарячі клавіші клавіатури
|
||||
Controller hotkeys=Гарячі клавіші геймпада
|
||||
Application options=Налаштування програми
|
||||
Application info=Інформація про програму
|
||||
|
||||
# Version info strings
|
||||
GSR version: %s=Версія GSR: %s
|
||||
GSR-UI version: %s=Версія GSR-UI: %s
|
||||
Flatpak version: %s=Версія Flatpak: %s
|
||||
GPU vendor: %s=Виробник GPU: %s
|
||||
|
||||
# Hotkey configuration dialog
|
||||
Press a key combination to use for the hotkey: "%s"=Натисніть комбінацію клавіш для використання як гарячої клавіші: "%s"
|
||||
Alpha-numerical keys can't be used alone in hotkeys, they have to be used one or more of these keys: Alt, Ctrl, Shift and Super.\nPress Esc to cancel or Backspace to remove the hotkey.=Буквено-цифрові клавіші не можна використовувати окремо, їх потрібно комбінувати з Alt, Ctrl, Shift або Super.\nНатисніть Esc для скасування або Backspace для видалення гарячої клавіші.
|
||||
|
||||
# Hotkey action names (without colons - these appear in the dialog)
|
||||
Show/hide UI=Показати/сховати інтерфейс
|
||||
Turn replay on/off=Увімкнути/вимкнути повтор
|
||||
Save replay=Зберегти повтор
|
||||
Save 1 minute replay=Зберегти 1 хвилину повтору
|
||||
Save 10 minute replay=Зберегти 10 хвилин повтору
|
||||
Start/stop recording=Розпочати/зупинити запис
|
||||
Pause/unpause recording=Призупинити/відновити запис
|
||||
Start/stop recording a region=Розпочати/зупинити запис регіону
|
||||
Start/stop recording a window=Розпочати/зупинити запис вікна
|
||||
Start/stop recording with desktop portal=Запис через портал
|
||||
Start/stop streaming=Розпочати/зупинити трансляцію
|
||||
Take a screenshot=Зробити знімок екрана
|
||||
Take a screenshot of a region=Зробити знімок регіону
|
||||
Take a screenshot of a window=Зробити знімок вікна
|
||||
Take a screenshot with desktop portal=Зробити знімок через портал робочого стола
|
||||
|
||||
# Controller hotkey descriptions
|
||||
to show/hide the UI=для показу/приховування інтерфейсу
|
||||
to take a screenshot=для знімка екрана
|
||||
to save a replay=для збереження повтору
|
||||
to start/stop recording=для початку/зупинки запису
|
||||
to turn replay on/off=для увімкнення/вимкнення повтору
|
||||
to save a 1 minute replay=для збереження 1 хвилини повтору
|
||||
to save a 10 minute replay=для збереження 10 хвилин повтору
|
||||
|
||||
# Error message for duplicate hotkey
|
||||
The hotkey %s is already used for something else=Гаряча клавіша %s вже використовується для іншої дії
|
||||
|
||||
|
||||
# Screenshot settings page
|
||||
Screenshot=Знімок екрана
|
||||
Capture=Захоплення
|
||||
Image=Зображення
|
||||
File info=Інформація про файл
|
||||
General=Загальні
|
||||
Screenshot indicator=Індикатор знімка
|
||||
Script=Скрипт
|
||||
File=Файл
|
||||
|
||||
Back=Назад
|
||||
Save=Зберегти
|
||||
Cancel=Скасувати
|
||||
|
||||
Capture source:=Джерело захоплення:
|
||||
Window=Вікно
|
||||
Region=Регіон
|
||||
Desktop portal=Портал робочого стола
|
||||
Monitor %s (%dx%d)=Монітор %s (%dx%d)
|
||||
Screen=Екран
|
||||
|
||||
Image resolution limit:=Обмеження роздільної здатності зображення:
|
||||
Change image resolution=Змінювати роздільну здатність зображення
|
||||
Restore portal session=Відновити сесію порталу
|
||||
|
||||
Image quality:=Якість зображення:
|
||||
Medium=Середня
|
||||
High=Висока
|
||||
Very high (Recommended)=Дуже висока (рекомендовано)
|
||||
Ultra=Ультра
|
||||
|
||||
Record cursor=Записувати курсор
|
||||
|
||||
Directory to save screenshots:=Каталог для збереження знімків:
|
||||
Image format:=Формат зображення:
|
||||
|
||||
Save screenshot in a folder based on the games name=Зберегти скріншот у папці на основі назви гри
|
||||
|
||||
Save screenshot to clipboard=Зберігати знімок до буфера обміну
|
||||
Save screenshot to clipboard (Not supported properly by Wayland)=Зберігати знімок до буфера обміну (в Wayland підтримується некоректно)
|
||||
Save screenshot to disk=Зберігати знімок на диск
|
||||
|
||||
Show screenshot notifications=Показувати сповіщення про знімки
|
||||
Blink scroll lock led when taking a screenshot=Блимати індикатором Scroll Lock при створенні знімка
|
||||
|
||||
Command to open the screenshot with:=Команда для відкриття знімка:
|
||||
|
||||
# Settings Page UI elements - дополнения
|
||||
|
||||
# View modes
|
||||
Simple view=Простий вигляд
|
||||
Advanced view=Розширений вигляд
|
||||
|
||||
# Capture settings
|
||||
Follow focused window=Слідувати за активним вікном
|
||||
Focused monitor=Активний монітор
|
||||
|
||||
Area size:=Розмір області:
|
||||
Video resolution limit:=Обмеження роздільної здатності відео:
|
||||
Change video resolution=Змінювати роздільну здатність відео
|
||||
Restore portal session=Відновити сесію порталу
|
||||
|
||||
# Webcam settings
|
||||
Webcam=Веб-камера
|
||||
Webcam source:=Джерело веб-камери:
|
||||
None=Немає
|
||||
Video format:=Формат відео:
|
||||
Auto (recommended)=Авто (рекомендовано)
|
||||
YUYV=YUYV
|
||||
Motion-JPEG=Motion-JPEG
|
||||
Video setup:=Налаштування відео:
|
||||
* Right click in the bottom right corner to resize the webcam=* Натисніть правою кнопкою миші в нижньому правому куті для зміни розміру веб-камери
|
||||
Flip camera horizontally=Відобразити камеру горизонтально
|
||||
|
||||
# Audio settings
|
||||
Audio=Аудіо
|
||||
Audio codec:=Аудіокодек:
|
||||
Opus (Recommended)=Opus (рекомендовано)
|
||||
AAC=AAC
|
||||
|
||||
Directory to save videos:=Каталог для збереження відео:
|
||||
Output device:=Вихідний пристрій:
|
||||
Input device: =Вхідний пристрій:
|
||||
Application: =Програма:
|
||||
Custom...=Інше...
|
||||
|
||||
Save video in a folder based on the games name%s=Збереження відео в папці на основі назви гри%s
|
||||
(X11 applications only)= (лише X11-програми)
|
||||
|
||||
Add audio track=Додати аудіодоріжку
|
||||
Add input device=Додати вхід
|
||||
Add output device=Додати вихід
|
||||
Add application audio=Додати аудіо програми
|
||||
Record audio from all applications except the selected ones=Записувати аудіо з усіх програм окрім вибраних
|
||||
Recording output devices and application audio may record all output audio, which is likely\nnot what you want to do. Remove the output devices.=Запис вихідних пристроїв та аудіо програм може записати весь вихідний звук,\nщо, ймовірно, не те, що ви хочете. Видаліть вихідні пристрої.
|
||||
|
||||
Video=Відео
|
||||
|
||||
# Video codec settings
|
||||
Video codec:=Відеокодек:
|
||||
H264=H264
|
||||
HEVC=HEVC
|
||||
HEVC (10 bit, reduces banding)=HEVC (10 біт, зменшує смуги)
|
||||
HEVC (HDR)=HEVC (HDR)
|
||||
AV1=AV1
|
||||
AV1 (10 bit, reduces banding)=AV1 (10 біт, зменшує смуги)
|
||||
AV1 (HDR)=AV1 (HDR)
|
||||
VP8=VP8
|
||||
VP9=VP9
|
||||
H264 Software Encoder (Slow, not recommended)=H264 програмний кодувальник \n(повільно, не рекомендовано)
|
||||
|
||||
# Video quality and bitrate
|
||||
Video quality:=Якість відео:
|
||||
Very high=Дуже висока
|
||||
Video bitrate (Kbps):=Бітрейт відео (Кбіт/с):
|
||||
Constant bitrate=Постійний бітрейт
|
||||
Constant bitrate (Recommended)=Постійний бітрейт (рекомендовано)
|
||||
|
||||
# Frame rate settings
|
||||
Frame rate:=Частота кадрів:
|
||||
Frame rate mode:=Режим частоти кадрів:
|
||||
Auto (Recommended)=Авто (рекомендовано)
|
||||
Constant=Постійний
|
||||
Variable=Змінний
|
||||
Sync to content=Синхр. з контентом
|
||||
Sync to content (Only X11 or desktop portal capture)=Синхр. з контентом (лише X11 або захоплення порталу)
|
||||
|
||||
# Color range
|
||||
Color range:=Колірний діапазон:
|
||||
Limited=Обмежений
|
||||
Full=Повний
|
||||
|
||||
# Container format
|
||||
Container:=Формат:
|
||||
|
||||
# Recording settings
|
||||
Record in low-power mode=Записувати в режимі низького енергоспоживання
|
||||
Record cursor=Записувати курсор
|
||||
|
||||
Do not force the GPU to go into high performance mode when recording.\nMay affect recording performance, especially when playing a video at the same time.\nIf enabled then it's recommended to use sync to content frame rate mode to reduce power usage when idle.=Не змушувати GPU переходити в режим високої продуктивності при записі.\nМоже вплинути на продуктивність запису, особливо при одночасному відтворенні відео.\nЯкщо увімкнено, рекомендується використовувати режим частоти кадрів синхронізації з контентом\nдля зниження енергоспоживання в режимі очікування.
|
||||
|
||||
Show %s notifications=Показувати сповіщення %s
|
||||
Show %s status with scroll lock LED=Показувати статус %s за допомогою індикатора Scroll Lock
|
||||
Recording indicator=Індикатор запису
|
||||
|
||||
Simple=Простий
|
||||
Audio track #%d=Аудіодоріжка #%d
|
||||
Output device=Вихідний пристрій
|
||||
Input device: =Вхідний пристрій:
|
||||
Estimated video file size per minute (excluding audio): %.2fMB=Приблизний розмір відео файлу за хвилину (без урахування аудіо): %.2fMB
|
||||
|
||||
# Replay settings
|
||||
Directory to save replays:=Каталог для збереження повторів:
|
||||
Replay indicator=Індикатор повтору
|
||||
Turn on replay when starting a fullscreen application%s=Увімкнути повтор при запуску повноекранної програми%s
|
||||
Autostart=Автозапуск
|
||||
in RAM=в ОЗП
|
||||
Replay duration in seconds:=Тривалість повтору в секундах:
|
||||
Where should temporary replay data be stored?=Де мають зберігатися тимчасові дані повтору?
|
||||
RAM=ОЗП
|
||||
Disk (Not recommended on SSDs)=Диск (не рекомендовано для SSD)
|
||||
Turn on replay when this program starts=Увімкнути повтор при запуску програми
|
||||
Turn on replay when power supply is connected=Увімкнути повтор при підключенні джерела живлення
|
||||
Don't turn on replay automatically=Не вмикати повтор автоматично
|
||||
Restart replay on save=Перезапускати повтор при збереженні
|
||||
Estimated video max file size %s: %.2fMB.\nChange video bitrate or replay duration to change file size.=Приблизний максимальний розмір відео файлу %s: %.2fMB.\nЗмініть бітрейт відео або тривалість повтору, щоб змінити розмір файлу.
|
||||
|
||||
# Streaming settings
|
||||
Stream service:=Сервіс трансляції:
|
||||
Twitch=Twitch
|
||||
YouTube=YouTube
|
||||
Kick=Kick
|
||||
Rumble=Rumble
|
||||
Custom=Інше
|
||||
Stream URL:=URL трансляції:
|
||||
Stream key:=Ключ трансляції:
|
||||
Streaming info=Інформація про трансляцію
|
||||
Streaming indicator=Індикатор трансляції
|
||||