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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +27 -0
  3. data/README.md +303 -0
  4. data/Rakefile +94 -0
  5. data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
  6. data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
  7. data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
  8. data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
  9. data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
  10. data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
  11. data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
  12. data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
  13. data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
  14. data/ext/qt_ruby_bridge/extconf.rb +75 -0
  15. data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
  16. data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
  17. data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
  18. data/lib/qt/application_lifecycle.rb +44 -0
  19. data/lib/qt/bridge.rb +95 -0
  20. data/lib/qt/children_tracking.rb +15 -0
  21. data/lib/qt/constants.rb +10 -0
  22. data/lib/qt/date_time_codec.rb +104 -0
  23. data/lib/qt/errors.rb +6 -0
  24. data/lib/qt/event_runtime.rb +139 -0
  25. data/lib/qt/event_runtime_dispatch.rb +35 -0
  26. data/lib/qt/event_runtime_qobject_methods.rb +41 -0
  27. data/lib/qt/generated_constants_runtime.rb +33 -0
  28. data/lib/qt/inspectable.rb +29 -0
  29. data/lib/qt/key_sequence_codec.rb +22 -0
  30. data/lib/qt/native.rb +93 -0
  31. data/lib/qt/shortcut_compat.rb +30 -0
  32. data/lib/qt/string_codec.rb +44 -0
  33. data/lib/qt/variant_codec.rb +78 -0
  34. data/lib/qt/version.rb +5 -0
  35. data/lib/qt.rb +47 -0
  36. data/scripts/generate_bridge/ast_introspection.rb +267 -0
  37. data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
  38. data/scripts/generate_bridge/auto_methods.rb +438 -0
  39. data/scripts/generate_bridge/core_utils.rb +114 -0
  40. data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
  41. data/scripts/generate_bridge/ffi_api.rb +46 -0
  42. data/scripts/generate_bridge/free_function_specs.rb +289 -0
  43. data/scripts/generate_bridge/spec_discovery.rb +313 -0
  44. data/scripts/generate_bridge.rb +1113 -0
  45. metadata +99 -0
@@ -0,0 +1,408 @@
1
+ #include "qt_ruby_runtime.hpp"
2
+
3
+ #include <QApplication>
4
+ #include <QCoreApplication>
5
+ #include <QEvent>
6
+ #include <QEventLoop>
7
+ #include <QKeyEvent>
8
+ #include <QMouseEvent>
9
+ #include <QObject>
10
+ #include <QPoint>
11
+ #include <QResizeEvent>
12
+ #include <QByteArray>
13
+ #include <QWidget>
14
+ #include <cstdio>
15
+ #include <cstdlib>
16
+ #include <thread>
17
+ #include <unordered_map>
18
+ #include <unordered_set>
19
+
20
+ namespace QtRubyRuntime {
21
+ EventCallback& event_callback_ref() {
22
+ static EventCallback callback = nullptr;
23
+ return callback;
24
+ }
25
+
26
+ std::unordered_map<QObject*, std::unordered_set<int>>& watched_events() {
27
+ static std::unordered_map<QObject*, std::unordered_set<int>> events;
28
+ return events;
29
+ }
30
+
31
+ std::unordered_set<QObject*>& watched_cleanup_hooks() {
32
+ static std::unordered_set<QObject*> hooks;
33
+ return hooks;
34
+ }
35
+
36
+ QApplication*& tracked_qapplication_ref() {
37
+ static QApplication* app = nullptr;
38
+ return app;
39
+ }
40
+
41
+ std::thread::id& gui_thread_id_ref() {
42
+ static std::thread::id id;
43
+ return id;
44
+ }
45
+
46
+ bool& qapplication_disposed_ref() {
47
+ static bool disposed = true;
48
+ return disposed;
49
+ }
50
+
51
+ bool strict_thread_contract_enabled() {
52
+ static const bool enabled = [] {
53
+ const char* raw = std::getenv("QT_RUBY_STRICT_THREAD_CONTRACT");
54
+ if (!raw) {
55
+ return false;
56
+ }
57
+ return raw[0] != '\0' && raw[0] != '0';
58
+ }();
59
+ return enabled;
60
+ }
61
+
62
+ bool on_gui_thread() {
63
+ return gui_thread_id_ref() == std::this_thread::get_id();
64
+ }
65
+
66
+ void runtime_warn(const char* message) {
67
+ std::fprintf(stderr, "[qt-ruby-runtime] %s\n", message);
68
+ std::fflush(stderr);
69
+ }
70
+
71
+ [[noreturn]] void strict_thread_contract_abort(const char* message) {
72
+ runtime_warn(message);
73
+ std::abort();
74
+ }
75
+
76
+ bool event_debug_enabled() {
77
+ static const bool enabled = [] {
78
+ const char* raw = std::getenv("QT_RUBY_EVENT_DEBUG");
79
+ if (!raw) {
80
+ return false;
81
+ }
82
+ return raw[0] != '\0' && raw[0] != '0';
83
+ }();
84
+ return enabled;
85
+ }
86
+
87
+ bool ancestor_mouse_move_enabled() {
88
+ static const bool enabled = [] {
89
+ const char* raw = std::getenv("QT_RUBY_EVENT_ANCESTOR_MOUSE_MOVE");
90
+ if (!raw) {
91
+ return false;
92
+ }
93
+ return raw[0] != '\0' && raw[0] != '0';
94
+ }();
95
+ return enabled;
96
+ }
97
+
98
+ const char* event_name(int et) {
99
+ switch (static_cast<QEvent::Type>(et)) {
100
+ case QEvent::MouseButtonPress:
101
+ return "MouseButtonPress";
102
+ case QEvent::MouseButtonRelease:
103
+ return "MouseButtonRelease";
104
+ case QEvent::MouseMove:
105
+ return "MouseMove";
106
+ case QEvent::KeyPress:
107
+ return "KeyPress";
108
+ case QEvent::KeyRelease:
109
+ return "KeyRelease";
110
+ case QEvent::FocusIn:
111
+ return "FocusIn";
112
+ case QEvent::FocusOut:
113
+ return "FocusOut";
114
+ case QEvent::Enter:
115
+ return "Enter";
116
+ case QEvent::Leave:
117
+ return "Leave";
118
+ case QEvent::Resize:
119
+ return "Resize";
120
+ default:
121
+ return "Other";
122
+ }
123
+ }
124
+
125
+ const char* class_name(QObject* obj) {
126
+ return (obj && obj->metaObject()) ? obj->metaObject()->className() : "null";
127
+ }
128
+
129
+ void log_event_dispatch(QObject* target, QObject* dispatch_target, int event_type, bool accepted, const char* stage) {
130
+ if (!event_debug_enabled()) {
131
+ return;
132
+ }
133
+
134
+ const QByteArray target_name = target ? target->objectName().toUtf8() : QByteArray();
135
+ const QByteArray dispatch_name = dispatch_target ? dispatch_target->objectName().toUtf8() : QByteArray();
136
+ std::fprintf(stderr,
137
+ "[qt-ruby-event] stage=%s type=%d(%s) target=%p target_class=%s target_name=%s dispatch=%p "
138
+ "dispatch_class=%s dispatch_name=%s accepted=%d\n",
139
+ stage,
140
+ event_type,
141
+ event_name(event_type),
142
+ static_cast<void*>(target),
143
+ class_name(target),
144
+ target_name.constData(),
145
+ static_cast<void*>(dispatch_target),
146
+ class_name(dispatch_target),
147
+ dispatch_name.constData(),
148
+ accepted ? 1 : 0);
149
+ }
150
+
151
+ bool supports_ancestor_dispatch(int event_type) {
152
+ switch (static_cast<QEvent::Type>(event_type)) {
153
+ case QEvent::MouseButtonPress:
154
+ case QEvent::MouseButtonRelease:
155
+ return true;
156
+ case QEvent::MouseMove:
157
+ return ancestor_mouse_move_enabled();
158
+ case QEvent::KeyPress:
159
+ case QEvent::KeyRelease:
160
+ case QEvent::FocusIn:
161
+ case QEvent::FocusOut:
162
+ case QEvent::Enter:
163
+ case QEvent::Leave:
164
+ return true;
165
+ default:
166
+ return false;
167
+ }
168
+ }
169
+
170
+ QObject* resolve_dispatch_target(QObject* target, int event_type) {
171
+ auto& watched = watched_events();
172
+ auto exact = watched.find(target);
173
+ if (exact != watched.end() && exact->second.count(event_type) > 0) {
174
+ return target;
175
+ }
176
+
177
+ if (!supports_ancestor_dispatch(event_type)) {
178
+ return nullptr;
179
+ }
180
+
181
+ for (QObject* cur = target ? target->parent() : nullptr; cur; cur = cur->parent()) {
182
+ auto it = watched.find(cur);
183
+ if (it != watched.end() && it->second.count(event_type) > 0) {
184
+ return cur;
185
+ }
186
+ }
187
+
188
+ return nullptr;
189
+ }
190
+
191
+ void ensure_cleanup_hook(QObject* obj) {
192
+ if (!obj || watched_cleanup_hooks().count(obj) > 0) {
193
+ return;
194
+ }
195
+ watched_cleanup_hooks().insert(obj);
196
+
197
+ QObject::connect(obj, &QObject::destroyed, [obj]() {
198
+ watched_events().erase(obj);
199
+ watched_cleanup_hooks().erase(obj);
200
+ });
201
+ }
202
+
203
+ class EventFilter : public QObject {
204
+ protected:
205
+ bool eventFilter(QObject* watched, QEvent* event) override {
206
+ if (watched_events().empty()) {
207
+ return QObject::eventFilter(watched, event);
208
+ }
209
+
210
+ const int et = static_cast<int>(event->type());
211
+ QObject* dispatch_target = resolve_dispatch_target(watched, et);
212
+ if (!dispatch_target) {
213
+ log_event_dispatch(watched, nullptr, et, event->isAccepted(), "skip");
214
+ return QObject::eventFilter(watched, event);
215
+ }
216
+
217
+ if (!event_callback_ref()) {
218
+ log_event_dispatch(watched, dispatch_target, et, event->isAccepted(), "no_callback");
219
+ return QObject::eventFilter(watched, event);
220
+ }
221
+
222
+ int a = 0;
223
+ int b = 0;
224
+ int c = 0;
225
+ int d = 0;
226
+
227
+ switch (event->type()) {
228
+ case QEvent::MouseButtonPress:
229
+ case QEvent::MouseButtonRelease:
230
+ case QEvent::MouseMove: {
231
+ auto* mouse_event = static_cast<QMouseEvent*>(event);
232
+ const QPoint p = mouse_event->position().toPoint();
233
+ a = p.x();
234
+ b = p.y();
235
+ c = static_cast<int>(mouse_event->button());
236
+ d = static_cast<int>(mouse_event->buttons());
237
+ break;
238
+ }
239
+ case QEvent::KeyPress:
240
+ case QEvent::KeyRelease: {
241
+ auto* key_event = static_cast<QKeyEvent*>(event);
242
+ a = key_event->key();
243
+ b = static_cast<int>(key_event->modifiers());
244
+ c = key_event->isAutoRepeat() ? 1 : 0;
245
+ d = key_event->count();
246
+ break;
247
+ }
248
+ case QEvent::Resize: {
249
+ auto* resize_event = static_cast<QResizeEvent*>(event);
250
+ a = resize_event->size().width();
251
+ b = resize_event->size().height();
252
+ c = resize_event->oldSize().width();
253
+ d = resize_event->oldSize().height();
254
+ break;
255
+ }
256
+ default:
257
+ break;
258
+ }
259
+
260
+ log_event_dispatch(watched, dispatch_target, et, event->isAccepted(), "dispatch");
261
+ event_callback_ref()(dispatch_target, et, a, b, c, d);
262
+ return QObject::eventFilter(watched, event);
263
+ }
264
+ };
265
+
266
+ EventFilter* event_filter_instance() {
267
+ static EventFilter filter;
268
+ return &filter;
269
+ }
270
+
271
+ void ensure_event_filter_installed() {
272
+ static bool installed = false;
273
+ if (!installed && qApp) {
274
+ qApp->installEventFilter(event_filter_instance());
275
+ installed = true;
276
+ }
277
+ }
278
+
279
+ void close_top_level_windows_for_shutdown() {
280
+ // Explicit close first: this lets widgets enqueue their own teardown work
281
+ // before we start draining posted events.
282
+ const auto windows = QApplication::topLevelWidgets();
283
+ for (QWidget* window : windows) {
284
+ if (!window) {
285
+ continue;
286
+ }
287
+ window->close();
288
+ }
289
+ }
290
+
291
+ void drain_qt_events_for_shutdown() {
292
+ // Bounded drain loop:
293
+ // - flush posted events
294
+ // - process pending loop work
295
+ // - repeat with a small cap to avoid hanging on continuously posted tasks
296
+ // This is intentionally finite and deterministic for tests and CI.
297
+ constexpr int kDrainIterations = 12;
298
+ QCoreApplication::sendPostedEvents(nullptr, 0);
299
+ for (int i = 0; i < kDrainIterations; ++i) {
300
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
301
+ QCoreApplication::sendPostedEvents(nullptr, 0);
302
+ }
303
+ QCoreApplication::sendPostedEvents(nullptr, 0);
304
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 5);
305
+ }
306
+ } // namespace QtRubyRuntime
307
+
308
+ QApplication* QtRubyRuntime::qapplication_new(const char* argv0) {
309
+ if (tracked_qapplication_ref() && !qapplication_disposed_ref()) {
310
+ // Runtime owns a single QApplication instance. Reuse if still active.
311
+ runtime_warn("qapplication_new called while QApplication is still active; reusing current instance");
312
+ return tracked_qapplication_ref();
313
+ }
314
+
315
+ static int argc = 1;
316
+ static QByteArray argv0_storage;
317
+ static char* argv[] = {nullptr, nullptr};
318
+ argv0_storage = QByteArray(argv0 ? argv0 : "ruby");
319
+ if (argv0_storage.isEmpty()) {
320
+ argv0_storage = QByteArray("ruby");
321
+ }
322
+ argv[0] = argv0_storage.data();
323
+
324
+ auto* app = new QApplication(argc, argv);
325
+ // GUI-thread contract: new/delete must happen on the same thread.
326
+ tracked_qapplication_ref() = app;
327
+ gui_thread_id_ref() = std::this_thread::get_id();
328
+ qapplication_disposed_ref() = false;
329
+ return app;
330
+ }
331
+
332
+ bool QtRubyRuntime::qapplication_delete(void* app_handle) {
333
+ // Idempotent no-op for null handles.
334
+ if (!app_handle) {
335
+ return true;
336
+ }
337
+
338
+ auto* app = static_cast<QApplication*>(app_handle);
339
+ auto* tracked = tracked_qapplication_ref();
340
+ // Already disposed (or never tracked): treat as idempotent success.
341
+ if (!tracked || qapplication_disposed_ref()) {
342
+ return true;
343
+ }
344
+
345
+ if (app != tracked) {
346
+ // Defensive guard against deleting foreign or stale QApplication pointers.
347
+ if (strict_thread_contract_enabled()) {
348
+ strict_thread_contract_abort("qapplication_delete received non-tracked QApplication handle");
349
+ }
350
+ runtime_warn("qapplication_delete received non-tracked QApplication handle");
351
+ return false;
352
+ }
353
+
354
+ if (!on_gui_thread()) {
355
+ // Teardown from non-GUI thread is unsafe for Qt internals/thread storage.
356
+ if (strict_thread_contract_enabled()) {
357
+ strict_thread_contract_abort("qapplication_delete called from non-GUI thread");
358
+ }
359
+ runtime_warn("qapplication_delete called from non-GUI thread");
360
+ return false;
361
+ }
362
+
363
+ // Safe shutdown order for bridge-owned lifecycle:
364
+ // 1) close top-level windows
365
+ // 2) drain posted/pending events
366
+ // 3) delete QApplication
367
+ close_top_level_windows_for_shutdown();
368
+ drain_qt_events_for_shutdown();
369
+
370
+ delete app;
371
+ tracked_qapplication_ref() = nullptr;
372
+ gui_thread_id_ref() = std::thread::id{};
373
+ qapplication_disposed_ref() = true;
374
+ return true;
375
+ }
376
+
377
+ void QtRubyRuntime::set_event_callback(void* callback_ptr) {
378
+ event_callback_ref() = reinterpret_cast<EventCallback>(callback_ptr);
379
+ ensure_event_filter_installed();
380
+ }
381
+
382
+ void QtRubyRuntime::watch_qobject_event(void* object_handle, int event_type) {
383
+ if (!object_handle) {
384
+ return;
385
+ }
386
+ auto* obj = static_cast<QObject*>(object_handle);
387
+ watched_events()[obj].insert(event_type);
388
+ ensure_cleanup_hook(obj);
389
+ log_event_dispatch(obj, obj, event_type, false, "watch");
390
+ ensure_event_filter_installed();
391
+ }
392
+
393
+ void QtRubyRuntime::unwatch_qobject_event(void* object_handle, int event_type) {
394
+ if (!object_handle) {
395
+ return;
396
+ }
397
+ auto* obj = static_cast<QObject*>(object_handle);
398
+ auto it = watched_events().find(obj);
399
+ if (it == watched_events().end()) {
400
+ log_event_dispatch(obj, nullptr, event_type, false, "unwatch_miss");
401
+ return;
402
+ }
403
+ it->second.erase(event_type);
404
+ if (it->second.empty()) {
405
+ watched_events().erase(it);
406
+ }
407
+ log_event_dispatch(obj, obj, event_type, false, "unwatch");
408
+ }
@@ -0,0 +1,212 @@
1
+ #include "qt_ruby_runtime.hpp"
2
+
3
+ #include <QByteArray>
4
+ #include <QDateTimeEdit>
5
+ #include <QMetaMethod>
6
+ #include <QObject>
7
+ #include <QSignalMapper>
8
+ #include <QString>
9
+ #include <unordered_map>
10
+ #include <vector>
11
+
12
+ namespace QtRubyRuntime {
13
+ SignalCallback& signal_callback_ref() {
14
+ static SignalCallback callback = nullptr;
15
+ return callback;
16
+ }
17
+
18
+ struct SignalHandler {
19
+ int signal_index = -1;
20
+ QMetaObject::Connection signal_connection;
21
+ QMetaObject::Connection mapped_connection;
22
+ QSignalMapper* mapper = nullptr;
23
+ };
24
+
25
+ using SignalHandlersByIndex = std::unordered_map<int, std::vector<SignalHandler>>;
26
+
27
+ std::unordered_map<QObject*, SignalHandlersByIndex>& signal_handlers() {
28
+ static std::unordered_map<QObject*, SignalHandlersByIndex> handlers;
29
+ return handlers;
30
+ }
31
+
32
+ int resolve_signal_index(QObject* obj, const char* signal_name) {
33
+ if (!obj || !signal_name) {
34
+ return -1;
35
+ }
36
+
37
+ const QMetaObject* mo = obj->metaObject();
38
+ QString requested = QString::fromUtf8(signal_name).trimmed();
39
+ if (requested.isEmpty()) {
40
+ return -1;
41
+ }
42
+
43
+ if (!requested.contains('(')) {
44
+ requested += "()";
45
+ }
46
+
47
+ QByteArray normalized = QMetaObject::normalizedSignature(requested.toUtf8().constData());
48
+ int index = mo->indexOfSignal(normalized.constData());
49
+ if (index >= 0) {
50
+ return index;
51
+ }
52
+
53
+ const int left = requested.indexOf('(');
54
+ const QByteArray signal_name_only = requested.left(left).toUtf8();
55
+ int fallback_index = -1;
56
+ int fallback_count = 0;
57
+ for (int i = mo->methodOffset(); i < mo->methodCount(); ++i) {
58
+ QMetaMethod method = mo->method(i);
59
+ if (method.methodType() != QMetaMethod::Signal) {
60
+ continue;
61
+ }
62
+ if (method.name() == signal_name_only) {
63
+ fallback_index = i;
64
+ fallback_count += 1;
65
+ }
66
+ }
67
+
68
+ if (fallback_count == 1) {
69
+ return fallback_index;
70
+ }
71
+
72
+ return -1;
73
+ }
74
+
75
+ const char* signal_payload_for(QObject* obj, int signal_index) {
76
+ if (!obj) {
77
+ return nullptr;
78
+ }
79
+
80
+ const QMetaMethod method = obj->metaObject()->method(signal_index);
81
+ const QByteArray signature = method.methodSignature();
82
+ auto* date_time_edit = qobject_cast<QDateTimeEdit*>(obj);
83
+ if (!date_time_edit) {
84
+ return nullptr;
85
+ }
86
+
87
+ thread_local QByteArray payload;
88
+ if (signature.startsWith("dateTimeChanged(")) {
89
+ payload = QStringLiteral("qtdt:").append(date_time_edit->dateTime().toString(Qt::ISODateWithMs)).toUtf8();
90
+ return payload.constData();
91
+ }
92
+ if (signature.startsWith("dateChanged(")) {
93
+ payload = QStringLiteral("qtdate:")
94
+ .append(date_time_edit->date().toString(QStringLiteral("yyyy-MM-dd")))
95
+ .toUtf8();
96
+ return payload.constData();
97
+ }
98
+ if (signature.startsWith("timeChanged(")) {
99
+ payload = QStringLiteral("qttime:")
100
+ .append(date_time_edit->time().toString(QStringLiteral("HH:mm:ss")))
101
+ .toUtf8();
102
+ return payload.constData();
103
+ }
104
+
105
+ return nullptr;
106
+ }
107
+ } // namespace QtRubyRuntime
108
+
109
+ void QtRubyRuntime::set_signal_callback(void* callback_ptr) {
110
+ signal_callback_ref() = reinterpret_cast<SignalCallback>(callback_ptr);
111
+ }
112
+
113
+ int QtRubyRuntime::qobject_connect_signal(void* object_handle, const char* signal_name) {
114
+ if (!object_handle || !signal_name) {
115
+ return -1;
116
+ }
117
+
118
+ auto* obj = static_cast<QObject*>(object_handle);
119
+ int signal_index = resolve_signal_index(obj, signal_name);
120
+ if (signal_index < 0) {
121
+ return -2;
122
+ }
123
+
124
+ const QMetaMethod signal_method = obj->metaObject()->method(signal_index);
125
+ auto* mapper = new QSignalMapper(obj);
126
+ mapper->setMapping(obj, signal_index);
127
+
128
+ const int map_slot_index = mapper->metaObject()->indexOfSlot("map()");
129
+ if (map_slot_index < 0) {
130
+ mapper->deleteLater();
131
+ return -3;
132
+ }
133
+
134
+ const QMetaMethod map_slot = mapper->metaObject()->method(map_slot_index);
135
+ QMetaObject::Connection signal_connection = QObject::connect(obj, signal_method, mapper, map_slot);
136
+ if (!signal_connection) {
137
+ mapper->deleteLater();
138
+ return -4;
139
+ }
140
+
141
+ QMetaObject::Connection mapped_connection =
142
+ QObject::connect(mapper, &QSignalMapper::mappedInt, mapper, [obj](int mapped_signal_index) {
143
+ if (!signal_callback_ref()) {
144
+ return;
145
+ }
146
+ signal_callback_ref()(obj, mapped_signal_index, signal_payload_for(obj, mapped_signal_index));
147
+ });
148
+
149
+ if (!mapped_connection) {
150
+ QObject::disconnect(signal_connection);
151
+ mapper->deleteLater();
152
+ return -5;
153
+ }
154
+
155
+ auto& by_index = signal_handlers()[obj];
156
+ by_index[signal_index].push_back(
157
+ SignalHandler{signal_index, signal_connection, mapped_connection, mapper});
158
+ return signal_index;
159
+ }
160
+
161
+ int QtRubyRuntime::qobject_disconnect_signal(void* object_handle, const char* signal_name) {
162
+ if (!object_handle) {
163
+ return -1;
164
+ }
165
+
166
+ auto* obj = static_cast<QObject*>(object_handle);
167
+ auto it = signal_handlers().find(obj);
168
+ if (it == signal_handlers().end()) {
169
+ return 0;
170
+ }
171
+
172
+ if (!signal_name) {
173
+ int disconnected = 0;
174
+ for (auto& [_, handlers] : it->second) {
175
+ for (const auto& handler : handlers) {
176
+ QObject::disconnect(handler.signal_connection);
177
+ QObject::disconnect(handler.mapped_connection);
178
+ if (handler.mapper) {
179
+ handler.mapper->deleteLater();
180
+ }
181
+ disconnected += 1;
182
+ }
183
+ }
184
+ signal_handlers().erase(it);
185
+ return disconnected;
186
+ }
187
+
188
+ int signal_index = resolve_signal_index(obj, signal_name);
189
+ if (signal_index < 0) {
190
+ return -2;
191
+ }
192
+
193
+ auto by_index_it = it->second.find(signal_index);
194
+ if (by_index_it == it->second.end()) {
195
+ return 0;
196
+ }
197
+
198
+ int disconnected = 0;
199
+ for (const auto& handler : by_index_it->second) {
200
+ QObject::disconnect(handler.signal_connection);
201
+ QObject::disconnect(handler.mapped_connection);
202
+ if (handler.mapper) {
203
+ handler.mapper->deleteLater();
204
+ }
205
+ disconnected += 1;
206
+ }
207
+ it->second.erase(by_index_it);
208
+ if (it->second.empty()) {
209
+ signal_handlers().erase(it);
210
+ }
211
+ return disconnected;
212
+ }
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Tracks QApplication creation/disposal lifecycle from Ruby side.
5
+ module ApplicationLifecycle
6
+ def initialize(_argc = 0, _argv = [])
7
+ @windows = []
8
+ # Propagate real argv0 into native QApplication creation so desktop
9
+ # environments can derive app identity/window class from process intent.
10
+ argv0 = if _argv.respond_to?(:[]) && !_argv.empty?
11
+ _argv[0]
12
+ else
13
+ $PROGRAM_NAME
14
+ end
15
+ argv0 = 'ruby' if argv0.nil? || argv0.to_s.empty?
16
+ @handle = Native.qapplication_new(Qt::StringCodec.to_qt_text(argv0))
17
+ self.class.current = self
18
+ end
19
+
20
+ def register_window(window)
21
+ @windows << window unless @windows.include?(window)
22
+ end
23
+
24
+ def exec
25
+ @windows.each(&:show)
26
+ Native.qapplication_exec(@handle)
27
+ ensure
28
+ dispose
29
+ end
30
+
31
+ def dispose
32
+ return if @handle.nil? || (@handle.respond_to?(:null?) && @handle.null?)
33
+
34
+ # Native returns false when teardown is rejected by safety guards
35
+ # (e.g. non-GUI thread dispose attempt). Keep handle intact in that case.
36
+ deleted = Native.qapplication_delete(@handle)
37
+ return false unless deleted
38
+
39
+ @handle = nil
40
+ self.class.current = nil if self.class.current.equal?(self)
41
+ true
42
+ end
43
+ end
44
+ end