qt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +27 -0
- data/README.md +303 -0
- data/Rakefile +94 -0
- data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
- data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
- data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
- data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
- data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
- data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
- data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
- data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
- data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
- data/ext/qt_ruby_bridge/extconf.rb +75 -0
- data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
- data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
- data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
- data/lib/qt/application_lifecycle.rb +44 -0
- data/lib/qt/bridge.rb +95 -0
- data/lib/qt/children_tracking.rb +15 -0
- data/lib/qt/constants.rb +10 -0
- data/lib/qt/date_time_codec.rb +104 -0
- data/lib/qt/errors.rb +6 -0
- data/lib/qt/event_runtime.rb +139 -0
- data/lib/qt/event_runtime_dispatch.rb +35 -0
- data/lib/qt/event_runtime_qobject_methods.rb +41 -0
- data/lib/qt/generated_constants_runtime.rb +33 -0
- data/lib/qt/inspectable.rb +29 -0
- data/lib/qt/key_sequence_codec.rb +22 -0
- data/lib/qt/native.rb +93 -0
- data/lib/qt/shortcut_compat.rb +30 -0
- data/lib/qt/string_codec.rb +44 -0
- data/lib/qt/variant_codec.rb +78 -0
- data/lib/qt/version.rb +5 -0
- data/lib/qt.rb +47 -0
- data/scripts/generate_bridge/ast_introspection.rb +267 -0
- data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
- data/scripts/generate_bridge/auto_methods.rb +438 -0
- data/scripts/generate_bridge/core_utils.rb +114 -0
- data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
- data/scripts/generate_bridge/ffi_api.rb +46 -0
- data/scripts/generate_bridge/free_function_specs.rb +289 -0
- data/scripts/generate_bridge/spec_discovery.rb +313 -0
- data/scripts/generate_bridge.rb +1113 -0
- metadata +99 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
|
|
4
|
+
require 'qt'
|
|
5
|
+
require 'ffi'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
require 'securerandom'
|
|
8
|
+
require 'tmpdir'
|
|
9
|
+
require 'timeout'
|
|
10
|
+
|
|
11
|
+
CTRL_MODIFIER = 0x04000000
|
|
12
|
+
ALT_MODIFIER = 0x08000000
|
|
13
|
+
KEY_R = 0x52
|
|
14
|
+
KEY_Q = 0x51
|
|
15
|
+
LEFT_BUTTON = 1
|
|
16
|
+
|
|
17
|
+
# Qt::WindowType / Qt::WidgetAttribute values used as plain integers.
|
|
18
|
+
WINDOW_FLAG_WINDOW = 0x00000001
|
|
19
|
+
WINDOW_FLAG_FRAMELESS = 0x00000800
|
|
20
|
+
WINDOW_FLAG_ALWAYS_ON_TOP = 0x00040000
|
|
21
|
+
WINDOW_FLAG_TRANSPARENT_FOR_INPUT = 0x00080000
|
|
22
|
+
WA_TRANSPARENT_FOR_MOUSE_EVENTS = 51
|
|
23
|
+
WA_NO_SYSTEM_BACKGROUND = 9
|
|
24
|
+
WA_TRANSLUCENT_BACKGROUND = 120
|
|
25
|
+
|
|
26
|
+
FRAME_IDLE_STYLE = 'background-color: rgba(16, 185, 129, 0.08); border: 0px;'
|
|
27
|
+
FRAME_RECORDING_STYLE = 'background-color: rgba(239, 68, 68, 0.08); border: 0px;'
|
|
28
|
+
BORDER_IDLE_STYLE = 'background-color: rgba(0, 0, 0, 0); border: 1px solid #10b981;'
|
|
29
|
+
BORDER_RECORDING_STYLE = 'background-color: rgba(0, 0, 0, 0); border: 1px solid #ef4444;'
|
|
30
|
+
HANDLE_IDLE_STYLE = 'background-color: rgba(15, 23, 42, 0.88); border: 1px solid #334155; color: #e2e8f0; font-size: 12px; font-weight: 700;'
|
|
31
|
+
HANDLE_RECORDING_STYLE = 'background-color: rgba(127, 29, 29, 0.92); border: 1px solid #ef4444; color: #fee2e2; font-size: 12px; font-weight: 700;'
|
|
32
|
+
REC_INDICATOR_STYLE = 'background-color: #ef4444; border: 2px solid #fee2e2; border-radius: 9px;'
|
|
33
|
+
|
|
34
|
+
HANDLE_W = 320
|
|
35
|
+
HANDLE_H = 34
|
|
36
|
+
HANDLE_PADDING = 12
|
|
37
|
+
MIN_FRAME_W = 160
|
|
38
|
+
MIN_FRAME_H = 120
|
|
39
|
+
RESIZE_EDGE = 8
|
|
40
|
+
RESIZE_CORNER = 14
|
|
41
|
+
RESIZE_HANDLE_STYLE = 'background-color: rgba(148, 163, 184, 0.20); border: 1px solid rgba(100, 116, 139, 0.55);'
|
|
42
|
+
|
|
43
|
+
module X11GlobalHotkey
|
|
44
|
+
extend self
|
|
45
|
+
|
|
46
|
+
KEYSYM_R = 0x72
|
|
47
|
+
KEY_PRESS = 2
|
|
48
|
+
|
|
49
|
+
CONTROL_MASK = 1 << 2
|
|
50
|
+
ALT_MASK = 1 << 3
|
|
51
|
+
LOCK_MASK = 1 << 1
|
|
52
|
+
MOD2_MASK = 1 << 4
|
|
53
|
+
|
|
54
|
+
GRAB_MODE_ASYNC = 1
|
|
55
|
+
|
|
56
|
+
class XKeyEvent < FFI::Struct
|
|
57
|
+
layout :type, :int,
|
|
58
|
+
:serial, :ulong,
|
|
59
|
+
:send_event, :int,
|
|
60
|
+
:display, :pointer,
|
|
61
|
+
:window, :ulong,
|
|
62
|
+
:root, :ulong,
|
|
63
|
+
:subwindow, :ulong,
|
|
64
|
+
:time, :ulong,
|
|
65
|
+
:x, :int,
|
|
66
|
+
:y, :int,
|
|
67
|
+
:x_root, :int,
|
|
68
|
+
:y_root, :int,
|
|
69
|
+
:state, :uint,
|
|
70
|
+
:keycode, :uint,
|
|
71
|
+
:same_screen, :int
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class XEvent < FFI::Union
|
|
75
|
+
layout :type, :int,
|
|
76
|
+
:xkey, XKeyEvent,
|
|
77
|
+
:pad, [:long, 24]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
module Lib
|
|
81
|
+
extend FFI::Library
|
|
82
|
+
ffi_lib ['X11', 'libX11.so.6']
|
|
83
|
+
|
|
84
|
+
attach_function :x_open_display, :XOpenDisplay, [:pointer], :pointer
|
|
85
|
+
attach_function :x_default_root_window, :XDefaultRootWindow, [:pointer], :ulong
|
|
86
|
+
attach_function :x_keysym_to_keycode, :XKeysymToKeycode, %i[pointer ulong], :uint
|
|
87
|
+
attach_function :x_grab_key, :XGrabKey, %i[pointer int uint ulong int int int], :int
|
|
88
|
+
attach_function :x_ungrab_key, :XUngrabKey, %i[pointer int uint ulong], :int
|
|
89
|
+
attach_function :x_pending, :XPending, [:pointer], :int
|
|
90
|
+
attach_function :x_next_event, :XNextEvent, %i[pointer pointer], :int
|
|
91
|
+
attach_function :x_sync, :XSync, %i[pointer int], :int
|
|
92
|
+
attach_function :x_close_display, :XCloseDisplay, [:pointer], :int
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def start_listener
|
|
96
|
+
return nil if ENV['DISPLAY'].to_s.strip.empty?
|
|
97
|
+
|
|
98
|
+
display = Lib.x_open_display(nil)
|
|
99
|
+
return nil if display.null?
|
|
100
|
+
|
|
101
|
+
root = Lib.x_default_root_window(display)
|
|
102
|
+
keycode = Lib.x_keysym_to_keycode(display, KEYSYM_R)
|
|
103
|
+
return nil if keycode.zero?
|
|
104
|
+
|
|
105
|
+
base_mod = CONTROL_MASK | ALT_MASK
|
|
106
|
+
modifier_combinations = [
|
|
107
|
+
base_mod,
|
|
108
|
+
base_mod | LOCK_MASK,
|
|
109
|
+
base_mod | MOD2_MASK,
|
|
110
|
+
base_mod | LOCK_MASK | MOD2_MASK
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
modifier_combinations.each do |mod|
|
|
114
|
+
Lib.x_grab_key(display, keycode, mod, root, 1, GRAB_MODE_ASYNC, GRAB_MODE_ASYNC)
|
|
115
|
+
end
|
|
116
|
+
Lib.x_sync(display, 0)
|
|
117
|
+
|
|
118
|
+
{
|
|
119
|
+
display: display,
|
|
120
|
+
root: root,
|
|
121
|
+
keycode: keycode,
|
|
122
|
+
mods: modifier_combinations,
|
|
123
|
+
event: XEvent.new,
|
|
124
|
+
last_trigger_at: 0.0
|
|
125
|
+
}
|
|
126
|
+
rescue LoadError, FFI::NotFoundError => e
|
|
127
|
+
warn "[hotkey-listener] unavailable: #{e.message}"
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def poll(listener)
|
|
132
|
+
return 0 if listener.nil?
|
|
133
|
+
|
|
134
|
+
triggered = 0
|
|
135
|
+
while Lib.x_pending(listener[:display]).positive?
|
|
136
|
+
Lib.x_next_event(listener[:display], listener[:event].pointer)
|
|
137
|
+
next unless listener[:event][:type] == KEY_PRESS
|
|
138
|
+
|
|
139
|
+
key_event = XKeyEvent.new(listener[:event].pointer)
|
|
140
|
+
state = key_event[:state]
|
|
141
|
+
next unless (state & (CONTROL_MASK | ALT_MASK)) == (CONTROL_MASK | ALT_MASK)
|
|
142
|
+
next unless key_event[:keycode] == listener[:keycode]
|
|
143
|
+
|
|
144
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
145
|
+
next if (now - listener[:last_trigger_at]) < 0.25
|
|
146
|
+
|
|
147
|
+
listener[:last_trigger_at] = now
|
|
148
|
+
triggered += 1
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
triggered
|
|
152
|
+
rescue StandardError => e
|
|
153
|
+
warn "[hotkey-listener] poll failed: #{e.class}: #{e.message}"
|
|
154
|
+
0
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def stop_listener(listener)
|
|
158
|
+
return if listener.nil?
|
|
159
|
+
|
|
160
|
+
listener[:mods].each do |mod|
|
|
161
|
+
Lib.x_ungrab_key(listener[:display], listener[:keycode], mod, listener[:root])
|
|
162
|
+
end
|
|
163
|
+
Lib.x_sync(listener[:display], 0)
|
|
164
|
+
Lib.x_close_display(listener[:display]) unless listener[:display].null?
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
warn "[hotkey-listener] stop failed: #{e.class}: #{e.message}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def recordings_dir
|
|
171
|
+
File.join(Dir.home, 'Видео', 'Записи экрана')
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def position_config_path
|
|
175
|
+
File.join(Dir.home, '.config', 'qpeek', 'position.conf')
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def load_position(path)
|
|
179
|
+
return nil unless File.exist?(path)
|
|
180
|
+
|
|
181
|
+
values = {}
|
|
182
|
+
File.readlines(path, chomp: true).each do |line|
|
|
183
|
+
key, raw = line.split('=', 2)
|
|
184
|
+
next if key.nil? || raw.nil?
|
|
185
|
+
|
|
186
|
+
values[key.strip] = Integer(raw.strip, exception: false)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
left = values['left']
|
|
190
|
+
top = values['top']
|
|
191
|
+
width = values['width']
|
|
192
|
+
height = values['height']
|
|
193
|
+
return nil unless left && top && width && height
|
|
194
|
+
|
|
195
|
+
{ left: left, top: top, width: width, height: height }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def save_position(path, window)
|
|
199
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
200
|
+
File.write(
|
|
201
|
+
path,
|
|
202
|
+
"top=#{window.y}\nleft=#{window.x}\nwidth=#{window.width}\nheight=#{window.height}\n"
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def ffmpeg_available?
|
|
207
|
+
system('command -v ffmpeg >/dev/null 2>&1')
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def now_stamp
|
|
211
|
+
Time.now.strftime('%Y-%m-%d_%H-%M-%S')
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def ensure_even(value)
|
|
215
|
+
value.even? ? value : value - 1
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def selected_geometry(window)
|
|
219
|
+
x = window.x
|
|
220
|
+
y = window.y
|
|
221
|
+
w = ensure_even(window.width)
|
|
222
|
+
h = ensure_even(window.height)
|
|
223
|
+
[x, y, [w, 2].max, [h, 2].max]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def stop_recorder!(pid)
|
|
227
|
+
return unless pid
|
|
228
|
+
|
|
229
|
+
begin
|
|
230
|
+
Process.kill('INT', pid)
|
|
231
|
+
rescue Errno::ESRCH
|
|
232
|
+
return
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
begin
|
|
236
|
+
Timeout.timeout(5) { Process.wait(pid) }
|
|
237
|
+
rescue Timeout::Error
|
|
238
|
+
Process.kill('TERM', pid) rescue nil
|
|
239
|
+
Process.wait(pid) rescue nil
|
|
240
|
+
rescue Errno::ECHILD
|
|
241
|
+
nil
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def choose_output_path(parent)
|
|
246
|
+
FileUtils.mkdir_p(recordings_dir)
|
|
247
|
+
suggested = File.join(recordings_dir, "screen-record-#{now_stamp}.mp4")
|
|
248
|
+
|
|
249
|
+
dialog = Qt::QFileDialog.new(parent)
|
|
250
|
+
selected = dialog.get_save_file_name(
|
|
251
|
+
parent,
|
|
252
|
+
'Сохранить запись',
|
|
253
|
+
suggested,
|
|
254
|
+
'MP4 video (*.mp4)',
|
|
255
|
+
nil,
|
|
256
|
+
0
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return nil if selected.nil? || selected.strip.empty?
|
|
260
|
+
|
|
261
|
+
selected.end_with?('.mp4') ? selected : "#{selected}.mp4"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def start_recorder(window)
|
|
265
|
+
x, y, w, h = selected_geometry(window)
|
|
266
|
+
display = ENV.fetch('DISPLAY', ':0')
|
|
267
|
+
temp_file = File.join(Dir.tmpdir, "qt-peek-like-#{now_stamp}-#{SecureRandom.hex(4)}.mp4")
|
|
268
|
+
|
|
269
|
+
ffmpeg_args = [
|
|
270
|
+
'ffmpeg',
|
|
271
|
+
'-f', 'x11grab',
|
|
272
|
+
'-show_region', '0',
|
|
273
|
+
'-framerate', '25',
|
|
274
|
+
'-video_size', "#{w}x#{h}",
|
|
275
|
+
'-i', "#{display}+#{x},#{y}",
|
|
276
|
+
'-filter:v', 'scale=iw/1:-1,crop=iw-mod(iw\\,2):ih-mod(ih\\,2)',
|
|
277
|
+
'-codec:v', 'libx264',
|
|
278
|
+
'-preset:v', 'fast',
|
|
279
|
+
'-pix_fmt', 'yuv420p',
|
|
280
|
+
'-r', '25',
|
|
281
|
+
'-y', temp_file
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
log_file = File.join(Dir.tmpdir, "qt-peek-like-ffmpeg-#{now_stamp}.log")
|
|
285
|
+
log_io = File.open(log_file, 'a')
|
|
286
|
+
pid = Process.spawn(*ffmpeg_args, out: log_io, err: log_io)
|
|
287
|
+
log_io.close
|
|
288
|
+
|
|
289
|
+
[pid, temp_file, log_file, [x, y, w, h]]
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def floating_flags
|
|
293
|
+
WINDOW_FLAG_WINDOW | WINDOW_FLAG_FRAMELESS | WINDOW_FLAG_ALWAYS_ON_TOP
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
def apply_handle_state(label, recording)
|
|
297
|
+
if recording
|
|
298
|
+
label.set_style_sheet(HANDLE_RECORDING_STYLE)
|
|
299
|
+
label.set_text('REC | Ctrl+Alt+R stop | Drag here to move')
|
|
300
|
+
else
|
|
301
|
+
label.set_style_sheet(HANDLE_IDLE_STYLE)
|
|
302
|
+
label.set_text('Ctrl+Alt+R start/stop | Drag here to move')
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def set_edit_mode(frame, handle, resize_handles, enabled)
|
|
307
|
+
if enabled
|
|
308
|
+
frame.set_window_flag(WINDOW_FLAG_TRANSPARENT_FOR_INPUT, 0)
|
|
309
|
+
frame.set_attribute(WA_TRANSPARENT_FOR_MOUSE_EVENTS, 0)
|
|
310
|
+
handle.show
|
|
311
|
+
resize_handles.each_value(&:show)
|
|
312
|
+
else
|
|
313
|
+
frame.set_window_flag(WINDOW_FLAG_TRANSPARENT_FOR_INPUT, 1)
|
|
314
|
+
frame.set_attribute(WA_TRANSPARENT_FOR_MOUSE_EVENTS, 1)
|
|
315
|
+
handle.hide
|
|
316
|
+
resize_handles.each_value(&:hide)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
frame.show
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
app = QApplication.new(0, [])
|
|
323
|
+
|
|
324
|
+
frame = QWidget.new do |w|
|
|
325
|
+
w.set_window_flags(floating_flags)
|
|
326
|
+
w.set_attribute(WA_NO_SYSTEM_BACKGROUND, 1)
|
|
327
|
+
w.set_attribute(WA_TRANSLUCENT_BACKGROUND, 1)
|
|
328
|
+
w.set_geometry(260, 180, 1024, 576)
|
|
329
|
+
w.set_style_sheet(FRAME_IDLE_STYLE)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
saved_position = load_position(position_config_path)
|
|
333
|
+
if saved_position
|
|
334
|
+
frame.set_geometry(
|
|
335
|
+
saved_position[:left],
|
|
336
|
+
saved_position[:top],
|
|
337
|
+
[saved_position[:width], MIN_FRAME_W].max,
|
|
338
|
+
[saved_position[:height], MIN_FRAME_H].max
|
|
339
|
+
)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
border_overlay = QLabel.new(frame)
|
|
343
|
+
border_overlay.set_attribute(WA_TRANSPARENT_FOR_MOUSE_EVENTS, 1)
|
|
344
|
+
border_overlay.set_style_sheet(BORDER_IDLE_STYLE)
|
|
345
|
+
|
|
346
|
+
handle = QWidget.new(frame)
|
|
347
|
+
handle_label = QLabel.new(handle)
|
|
348
|
+
handle_label.set_alignment(Qt::AlignCenter)
|
|
349
|
+
apply_handle_state(handle_label, false)
|
|
350
|
+
|
|
351
|
+
record_indicator = QLabel.new(frame)
|
|
352
|
+
record_indicator.set_style_sheet(REC_INDICATOR_STYLE)
|
|
353
|
+
record_indicator.hide
|
|
354
|
+
|
|
355
|
+
resize_handles = {
|
|
356
|
+
n: QWidget.new(frame),
|
|
357
|
+
s: QWidget.new(frame),
|
|
358
|
+
e: QWidget.new(frame),
|
|
359
|
+
w: QWidget.new(frame),
|
|
360
|
+
ne: QWidget.new(frame),
|
|
361
|
+
nw: QWidget.new(frame),
|
|
362
|
+
se: QWidget.new(frame),
|
|
363
|
+
sw: QWidget.new(frame)
|
|
364
|
+
}
|
|
365
|
+
resize_handles.each_value { |grip| grip.set_style_sheet(RESIZE_HANDLE_STYLE) }
|
|
366
|
+
|
|
367
|
+
recording = false
|
|
368
|
+
ffmpeg_pid = nil
|
|
369
|
+
temp_output = nil
|
|
370
|
+
last_log = nil
|
|
371
|
+
last_geometry = nil
|
|
372
|
+
|
|
373
|
+
dragging = false
|
|
374
|
+
drag_local_x = 0
|
|
375
|
+
drag_local_y = 0
|
|
376
|
+
|
|
377
|
+
layout_overlays = lambda do
|
|
378
|
+
fw = frame.width
|
|
379
|
+
fh = frame.height
|
|
380
|
+
|
|
381
|
+
handle_w = [[HANDLE_W, fw - (HANDLE_PADDING * 2)].min, 120].max
|
|
382
|
+
border_overlay.set_geometry(0, 0, fw, fh)
|
|
383
|
+
handle.set_geometry(HANDLE_PADDING, HANDLE_PADDING, handle_w, HANDLE_H)
|
|
384
|
+
handle_label.set_geometry(0, 0, handle_w, HANDLE_H)
|
|
385
|
+
|
|
386
|
+
record_indicator.set_geometry(fw - 24, 8, 18, 18)
|
|
387
|
+
|
|
388
|
+
resize_handles[:n].set_geometry(RESIZE_CORNER, 0, [fw - (RESIZE_CORNER * 2), 16].max, RESIZE_EDGE)
|
|
389
|
+
resize_handles[:s].set_geometry(RESIZE_CORNER, fh - RESIZE_EDGE, [fw - (RESIZE_CORNER * 2), 16].max, RESIZE_EDGE)
|
|
390
|
+
resize_handles[:w].set_geometry(0, RESIZE_CORNER, RESIZE_EDGE, [fh - (RESIZE_CORNER * 2), 16].max)
|
|
391
|
+
resize_handles[:e].set_geometry(fw - RESIZE_EDGE, RESIZE_CORNER, RESIZE_EDGE, [fh - (RESIZE_CORNER * 2), 16].max)
|
|
392
|
+
resize_handles[:nw].set_geometry(0, 0, RESIZE_CORNER, RESIZE_CORNER)
|
|
393
|
+
resize_handles[:ne].set_geometry(fw - RESIZE_CORNER, 0, RESIZE_CORNER, RESIZE_CORNER)
|
|
394
|
+
resize_handles[:sw].set_geometry(0, fh - RESIZE_CORNER, RESIZE_CORNER, RESIZE_CORNER)
|
|
395
|
+
resize_handles[:se].set_geometry(fw - RESIZE_CORNER, fh - RESIZE_CORNER, RESIZE_CORNER, RESIZE_CORNER)
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
set_frame_geometry = lambda do |x, y, w, h|
|
|
399
|
+
frame.set_geometry(x, y, [w, MIN_FRAME_W].max, [h, MIN_FRAME_H].max)
|
|
400
|
+
layout_overlays.call
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
move_overlay_to = lambda do |new_frame_x, new_frame_y|
|
|
404
|
+
set_frame_geometry.call(new_frame_x, new_frame_y, frame.width, frame.height)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
toggle_recording = lambda do
|
|
408
|
+
next unless ffmpeg_available?
|
|
409
|
+
|
|
410
|
+
if recording
|
|
411
|
+
stop_recorder!(ffmpeg_pid)
|
|
412
|
+
ffmpeg_pid = nil
|
|
413
|
+
recording = false
|
|
414
|
+
frame.set_style_sheet(FRAME_IDLE_STYLE)
|
|
415
|
+
border_overlay.set_style_sheet(BORDER_IDLE_STYLE)
|
|
416
|
+
apply_handle_state(handle_label, false)
|
|
417
|
+
record_indicator.hide
|
|
418
|
+
set_edit_mode(frame, handle, resize_handles, true)
|
|
419
|
+
frame.activate_window
|
|
420
|
+
frame.grab_keyboard
|
|
421
|
+
|
|
422
|
+
save_to = choose_output_path(frame)
|
|
423
|
+
if save_to
|
|
424
|
+
FileUtils.mkdir_p(File.dirname(save_to))
|
|
425
|
+
FileUtils.mv(temp_output, save_to)
|
|
426
|
+
handle_label.set_text("Saved: #{save_to}")
|
|
427
|
+
puts "[saved] #{save_to}"
|
|
428
|
+
else
|
|
429
|
+
handle_label.set_text('Save canceled. Temp file kept.')
|
|
430
|
+
puts "[cancelled-save] temp file left at: #{temp_output}"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
puts "[ffmpeg-log] #{last_log}" if last_log
|
|
434
|
+
else
|
|
435
|
+
ffmpeg_pid, temp_output, last_log, last_geometry = start_recorder(frame)
|
|
436
|
+
recording = true
|
|
437
|
+
frame.set_style_sheet(FRAME_RECORDING_STYLE)
|
|
438
|
+
border_overlay.set_style_sheet(BORDER_RECORDING_STYLE)
|
|
439
|
+
apply_handle_state(handle_label, true)
|
|
440
|
+
record_indicator.show
|
|
441
|
+
set_edit_mode(frame, handle, resize_handles, false)
|
|
442
|
+
puts "[recording] pid=#{ffmpeg_pid} area=#{last_geometry.inspect} tmp=#{temp_output}"
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
handle.on(:mouse_button_press) do |evt|
|
|
447
|
+
next unless evt[:c] == LEFT_BUTTON
|
|
448
|
+
|
|
449
|
+
dragging = true
|
|
450
|
+
drag_local_x = evt[:a]
|
|
451
|
+
drag_local_y = evt[:b]
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
handle.on(:mouse_move) do |evt|
|
|
455
|
+
next unless dragging
|
|
456
|
+
next unless evt[:d].anybits?(LEFT_BUTTON)
|
|
457
|
+
|
|
458
|
+
delta_x = evt[:a] - drag_local_x
|
|
459
|
+
delta_y = evt[:b] - drag_local_y
|
|
460
|
+
move_overlay_to.call(frame.x + delta_x, frame.y + delta_y)
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
handle.on(:mouse_button_release) do |_evt|
|
|
464
|
+
dragging = false
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
resize_state = nil
|
|
468
|
+
resize_handles.each do |dir, grip|
|
|
469
|
+
grip.on(:mouse_button_press) do |evt|
|
|
470
|
+
next unless evt[:c] == LEFT_BUTTON
|
|
471
|
+
|
|
472
|
+
resize_state = {
|
|
473
|
+
dir: dir,
|
|
474
|
+
start_global_x: frame.x + grip.x + evt[:a],
|
|
475
|
+
start_global_y: frame.y + grip.y + evt[:b],
|
|
476
|
+
frame_x: frame.x,
|
|
477
|
+
frame_y: frame.y,
|
|
478
|
+
frame_w: frame.width,
|
|
479
|
+
frame_h: frame.height
|
|
480
|
+
}
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
grip.on(:mouse_move) do |evt|
|
|
484
|
+
next unless resize_state
|
|
485
|
+
next unless evt[:d].anybits?(LEFT_BUTTON)
|
|
486
|
+
|
|
487
|
+
current_global_x = frame.x + grip.x + evt[:a]
|
|
488
|
+
current_global_y = frame.y + grip.y + evt[:b]
|
|
489
|
+
dx = current_global_x - resize_state[:start_global_x]
|
|
490
|
+
dy = current_global_y - resize_state[:start_global_y]
|
|
491
|
+
|
|
492
|
+
new_x = resize_state[:frame_x]
|
|
493
|
+
new_y = resize_state[:frame_y]
|
|
494
|
+
new_w = resize_state[:frame_w]
|
|
495
|
+
new_h = resize_state[:frame_h]
|
|
496
|
+
|
|
497
|
+
new_w = resize_state[:frame_w] + dx if %i[e ne se].include?(resize_state[:dir])
|
|
498
|
+
new_h = resize_state[:frame_h] + dy if %i[s se sw].include?(resize_state[:dir])
|
|
499
|
+
|
|
500
|
+
if %i[w nw sw].include?(resize_state[:dir])
|
|
501
|
+
new_x = resize_state[:frame_x] + dx
|
|
502
|
+
new_w = resize_state[:frame_w] - dx
|
|
503
|
+
if new_w < MIN_FRAME_W
|
|
504
|
+
new_x -= (MIN_FRAME_W - new_w)
|
|
505
|
+
new_w = MIN_FRAME_W
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
if %i[n ne nw].include?(resize_state[:dir])
|
|
510
|
+
new_y = resize_state[:frame_y] + dy
|
|
511
|
+
new_h = resize_state[:frame_h] - dy
|
|
512
|
+
if new_h < MIN_FRAME_H
|
|
513
|
+
new_y -= (MIN_FRAME_H - new_h)
|
|
514
|
+
new_h = MIN_FRAME_H
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
set_frame_geometry.call(new_x, new_y, new_w, new_h)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
grip.on(:mouse_button_release) do |_evt|
|
|
522
|
+
resize_state = nil
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
# Fallback hotkey when frame has keyboard focus.
|
|
527
|
+
frame.on(:key_press) do |evt|
|
|
528
|
+
key = evt[:a]
|
|
529
|
+
modifiers = evt[:b]
|
|
530
|
+
ctrl_pressed = (modifiers & CTRL_MODIFIER) != 0
|
|
531
|
+
if ctrl_pressed && key == KEY_Q && !recording
|
|
532
|
+
frame.close
|
|
533
|
+
next
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
alt_pressed = (modifiers & ALT_MODIFIER) != 0
|
|
537
|
+
next unless key == KEY_R && ctrl_pressed && alt_pressed
|
|
538
|
+
|
|
539
|
+
toggle_recording.call
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
hotkey_listener = X11GlobalHotkey.start_listener
|
|
543
|
+
|
|
544
|
+
unless ffmpeg_available?
|
|
545
|
+
handle_label.set_text('ffmpeg not found in PATH. Install ffmpeg and restart.')
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
frame.show
|
|
549
|
+
layout_overlays.call
|
|
550
|
+
set_edit_mode(frame, handle, resize_handles, true)
|
|
551
|
+
frame.activate_window
|
|
552
|
+
frame.grab_keyboard
|
|
553
|
+
QApplication.process_events
|
|
554
|
+
|
|
555
|
+
hotkey_status_printed = false
|
|
556
|
+
last_persisted_geometry = [frame.x, frame.y, frame.width, frame.height]
|
|
557
|
+
while frame.is_visible != 0
|
|
558
|
+
QApplication.process_events
|
|
559
|
+
|
|
560
|
+
unless hotkey_status_printed
|
|
561
|
+
if hotkey_listener.nil?
|
|
562
|
+
puts '[hotkey] global Ctrl+Alt+R unavailable, fallback to focused window hotkey'
|
|
563
|
+
hotkey_status_printed = true
|
|
564
|
+
else
|
|
565
|
+
puts '[hotkey] global Ctrl+Alt+R listener active'
|
|
566
|
+
hotkey_status_printed = true
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
toggles = X11GlobalHotkey.poll(hotkey_listener)
|
|
571
|
+
toggles.times { toggle_recording.call }
|
|
572
|
+
|
|
573
|
+
geometry_now = [frame.x, frame.y, frame.width, frame.height]
|
|
574
|
+
if geometry_now != last_persisted_geometry
|
|
575
|
+
save_position(position_config_path, frame)
|
|
576
|
+
last_persisted_geometry = geometry_now
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
sleep(0.01)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
if recording
|
|
583
|
+
stop_recorder!(ffmpeg_pid)
|
|
584
|
+
ffmpeg_pid = nil
|
|
585
|
+
save_to = choose_output_path(frame)
|
|
586
|
+
if save_to
|
|
587
|
+
FileUtils.mkdir_p(File.dirname(save_to))
|
|
588
|
+
FileUtils.mv(temp_output, save_to)
|
|
589
|
+
puts "[saved-on-exit] #{save_to}"
|
|
590
|
+
else
|
|
591
|
+
puts "[cancelled-save-on-exit] temp file left at: #{temp_output}"
|
|
592
|
+
end
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
X11GlobalHotkey.stop_listener(hotkey_listener)
|
|
596
|
+
save_position(position_config_path, frame)
|
|
597
|
+
app.dispose
|