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,1113 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'fileutils'
5
+ require 'json'
6
+ require 'tempfile'
7
+ require_relative 'generate_bridge/free_function_specs'
8
+
9
+ ROOT = File.expand_path('..', __dir__)
10
+ BUILD_DIR = File.join(ROOT, 'build')
11
+ GENERATED_DIR = File.join(BUILD_DIR, 'generated')
12
+ CPP_PATH = File.join(GENERATED_DIR, 'qt_ruby_bridge.cpp')
13
+ API_PATH = File.join(GENERATED_DIR, 'bridge_api.rb')
14
+ RUBY_WIDGETS_PATH = File.join(GENERATED_DIR, 'widgets.rb')
15
+ RUBY_CONSTANTS_PATH = File.join(GENERATED_DIR, 'constants.rb')
16
+
17
+ # Universal generation policy: class set is discovered from AST per scope.
18
+ GENERATOR_SCOPE = (ENV['QT_RUBY_SCOPE'] || 'all').freeze
19
+ SUPPORTED_SCOPES = %w[widgets qobject all].freeze
20
+
21
+ def build_qapplication_spec(ast)
22
+ instance_methods = [
23
+ { qt_name: 'exec', ruby_name: 'exec', ffi_return: :int, args: [] }
24
+ ]
25
+ reserved_class_natives = instance_methods.map { |method| "qapplication_#{to_snake(method[:qt_name])}" }.to_set
26
+ class_methods = qapplication_class_method_specs(ast).reject { |method| reserved_class_natives.include?(method[:native]) }
27
+
28
+ {
29
+ qt_class: 'QApplication',
30
+ ruby_class: 'QApplication',
31
+ include: 'QApplication',
32
+ prefix: 'qapplication',
33
+ constructor: { parent: false, mode: :qapplication },
34
+ class_methods: class_methods,
35
+ methods: instance_methods,
36
+ validate: { constructors: ['QApplication'], methods: ['exec'] }
37
+ }
38
+ end
39
+ RUBY_RESERVED_WORDS = %w[
40
+ BEGIN END alias and begin break case class def defined? do else elsif end ensure false
41
+ for if in module next nil not or redo rescue retry return self super then true undef unless
42
+ until when while yield __ENCODING__ __FILE__ __LINE__
43
+ ].to_set.freeze
44
+ RUNTIME_METHOD_RENAMES = { 'handle' => 'handle_at' }.freeze
45
+ RUNTIME_RESERVED_RUBY_METHODS = Set['handle'].freeze
46
+
47
+ require_relative 'generate_bridge/core_utils'
48
+ require_relative 'generate_bridge/ffi_api'
49
+ require_relative 'generate_bridge/auto_method_spec_resolver'
50
+ require_relative 'generate_bridge/cpp_method_return_emitter'
51
+ require_relative 'generate_bridge/ast_introspection'
52
+ require_relative 'generate_bridge/auto_methods'
53
+ require_relative 'generate_bridge/spec_discovery'
54
+
55
+ def next_trace_base(fetch_bases, cur, visited)
56
+ bases = Array(fetch_bases.call(cur))
57
+ return nil if bases.empty?
58
+
59
+ base = bases.first
60
+ return nil if base.nil? || base.empty? || visited[base]
61
+
62
+ base
63
+ end
64
+
65
+ def trace_generated_super_chain(fetch_bases, known_qt, qt_class, super_qt_by_qt)
66
+ return if qt_class == 'QApplication'
67
+
68
+ visited = {}
69
+ prev = cur = qt_class
70
+ loop do
71
+ break unless (base = next_trace_base(fetch_bases, cur, visited))
72
+
73
+ visited[base] = true
74
+ super_qt_by_qt[prev] ||= base
75
+ break if known_qt.include?(base)
76
+
77
+ prev = cur = base
78
+ end
79
+ end
80
+
81
+ def build_generated_inheritance(ast, specs)
82
+ known_qt = specs.map { |s| s[:qt_class] }
83
+ base_cache = {}
84
+ fetch_bases = lambda do |qt_class|
85
+ base_cache[qt_class] ||= collect_class_bases(ast, qt_class)
86
+ end
87
+
88
+ super_qt_by_qt = {}
89
+ known_qt.each { |qt_class| trace_generated_super_chain(fetch_bases, known_qt, qt_class, super_qt_by_qt) }
90
+
91
+ wrapper_qt_classes = (super_qt_by_qt.keys + super_qt_by_qt.values - known_qt).uniq
92
+ [super_qt_by_qt, wrapper_qt_classes]
93
+ end
94
+
95
+ def widget_based_qt_class?(qt_class, super_qt_by_qt)
96
+ cur = qt_class
97
+ while (sup = super_qt_by_qt[cur])
98
+ return true if sup == 'QWidget'
99
+
100
+ cur = sup
101
+ end
102
+ false
103
+ end
104
+
105
+ def qobject_based_qt_class?(qt_class, super_qt_by_qt)
106
+ return true if qt_class == 'QObject'
107
+
108
+ cur = qt_class
109
+ while (sup = super_qt_by_qt[cur])
110
+ return true if sup == 'QObject'
111
+
112
+ cur = sup
113
+ end
114
+ false
115
+ end
116
+
117
+ def inherited_methods_for_spec(spec, specs_by_qt, super_qt_by_qt)
118
+ inherited = []
119
+ cur = spec[:qt_class]
120
+
121
+ while (sup = super_qt_by_qt[cur])
122
+ parent_spec = specs_by_qt[sup]
123
+ inherited.concat(parent_spec[:methods]) if parent_spec
124
+ cur = sup
125
+ end
126
+
127
+ inherited
128
+ end
129
+
130
+ def generate_ruby_wrapper_class(lines, qt_class, super_ruby)
131
+ class_decl = ruby_wrapper_class_decl(qt_class, super_ruby)
132
+ lines << class_decl
133
+ lines << " QT_CLASS = '#{qt_class}'.freeze"
134
+ lines << ' QT_API_QT_METHODS = [].freeze'
135
+ lines << ' QT_API_RUBY_METHODS = [].freeze'
136
+ lines << ' QT_API_PROPERTIES = [].freeze'
137
+ lines << ' end'
138
+ lines << ''
139
+ end
140
+
141
+ def ruby_wrapper_class_decl(qt_class, super_ruby)
142
+ return " class #{qt_class}" unless super_ruby
143
+
144
+ " class #{qt_class} < #{super_ruby}"
145
+ end
146
+
147
+ def find_getter_decl(ast, qt_class, property)
148
+ collect_method_decls_with_bases(ast, qt_class, property).find do |decl|
149
+ next false unless decl['__effective_access'] == 'public'
150
+
151
+ parsed = parse_method_signature(decl)
152
+ next false unless parsed && parsed[:params].empty?
153
+
154
+ map_cpp_return_type(parsed[:return_type])
155
+ end
156
+ end
157
+
158
+ def build_property_getter_method(getter_decl, property)
159
+ parsed_getter = parse_method_signature(getter_decl)
160
+ ret_info = map_cpp_return_type(parsed_getter[:return_type])
161
+ return nil unless ret_info
162
+
163
+ getter = {
164
+ qt_name: property,
165
+ ruby_name: property,
166
+ ffi_return: ret_info[:ffi_return],
167
+ args: [],
168
+ property: property
169
+ }
170
+ getter[:return_cast] = ret_info[:return_cast] if ret_info[:return_cast]
171
+ getter
172
+ end
173
+
174
+ def enrich_spec_with_property_getter!(methods, ast, spec, method)
175
+ property = property_name_from_setter(method[:qt_name])
176
+ return unless property_candidate?(method, ast, spec, property)
177
+
178
+ return if attach_existing_property_getter?(methods, property)
179
+
180
+ getter_decl = find_getter_decl(ast, spec[:qt_class], property)
181
+ return unless getter_decl
182
+
183
+ getter = build_property_getter_method(getter_decl, property)
184
+ methods << getter if getter
185
+ end
186
+
187
+ def attach_existing_property_getter?(methods, property)
188
+ existing_getter = methods.find { |method| method[:qt_name] == property && method[:args].empty? }
189
+ return false unless existing_getter
190
+
191
+ existing_getter[:property] ||= property
192
+ true
193
+ end
194
+
195
+ def property_candidate?(method, ast, spec, property)
196
+ return false unless method[:args].length == 1
197
+ return false unless property
198
+ return false unless class_has_method?(ast, spec[:qt_class], property)
199
+
200
+ true
201
+ end
202
+
203
+ def enrich_specs_with_properties(specs, ast)
204
+ specs.map do |spec|
205
+ methods = spec[:methods].dup
206
+
207
+ spec[:methods].each do |method|
208
+ enrich_spec_with_property_getter!(methods, ast, spec, method)
209
+ end
210
+
211
+ spec.merge(methods: methods)
212
+ end
213
+ end
214
+
215
+ def validate_spec_api(errors, spec, api)
216
+ req = spec[:validate]
217
+ req[:constructors].each do |ctor|
218
+ errors << "#{spec[:qt_class]}: constructor #{ctor} not found" unless api[:constructors].include?(ctor)
219
+ end
220
+ req[:methods].each do |method|
221
+ errors << "#{spec[:qt_class]}: method #{method} not found" unless api[:methods].include?(method)
222
+ end
223
+ end
224
+
225
+ def validate_qt_api!(ast, specs)
226
+ errors = []
227
+
228
+ specs.each do |spec|
229
+ api = collect_class_api(ast, spec[:qt_class])
230
+ validate_spec_api(errors, spec, api)
231
+ end
232
+
233
+ return if errors.empty?
234
+
235
+ raise "Qt AST validation failed:\n- #{errors.join("\n- ")}"
236
+ end
237
+
238
+ def arg_expr(arg)
239
+ case arg[:cast]
240
+ when :qstring then "as_qstring(#{arg[:name]})"
241
+ when :qdatetime_from_utf8 then "qdatetime_from_bridge_value(#{arg[:name]})"
242
+ when :qdate_from_utf8 then "qdate_from_bridge_value(#{arg[:name]})"
243
+ when :qtime_from_utf8 then "qtime_from_bridge_value(#{arg[:name]})"
244
+ when :qkeysequence_from_utf8 then "QKeySequence(as_qstring(#{arg[:name]}))"
245
+ when :qicon_ref then "*static_cast<QIcon*>(#{arg[:name]})"
246
+ when :qany_string_view then "QAnyStringView(as_qstring(#{arg[:name]}))"
247
+ when :qvariant_from_utf8 then "qvariant_from_bridge_value(#{arg[:name]})"
248
+ when :alignment then "static_cast<Qt::Alignment>(#{arg[:name]})"
249
+ when String then "static_cast<#{arg[:cast]}>(#{arg[:name]})"
250
+ else
251
+ arg[:name]
252
+ end
253
+ end
254
+
255
+ def emit_cpp_qapplication_constructor(lines, name)
256
+ lines << "extern \"C\" void* #{name}(const char* argv0) {"
257
+ lines << ' // Delegate QApplication ownership/thread-contract policy to runtime.'
258
+ lines << ' return QtRubyRuntime::qapplication_new(argv0);'
259
+ lines << '}'
260
+ end
261
+
262
+ def emit_cpp_default_constructor(lines, name, qt_class)
263
+ lines << "extern \"C\" void* #{name}() {"
264
+ lines << " return new #{qt_class}();"
265
+ lines << '}'
266
+ end
267
+
268
+ def string_ctor_arg_expr(var_name, cast)
269
+ case cast || :qstring
270
+ when :qany_string_view then "QAnyStringView(as_qstring(#{var_name}))"
271
+ when :cstr then var_name
272
+ else "as_qstring(#{var_name})"
273
+ end
274
+ end
275
+
276
+ def emit_cpp_string_path_constructor(lines, name, qt_class, arg_cast)
277
+ lines << "extern \"C\" void* #{name}(const char* path) {"
278
+ lines << ' const char* raw = path ? path : "";'
279
+ lines << " return new #{qt_class}(#{string_ctor_arg_expr('raw', arg_cast)});"
280
+ lines << '}'
281
+ end
282
+
283
+ def emit_cpp_parent_constructor(lines, name, spec)
284
+ parent_type = spec[:constructor][:parent_type]
285
+ parent_class = parent_type.delete('*')
286
+ lines << "extern \"C\" void* #{name}(void* parent_handle) {"
287
+ lines << " #{parent_class}* parent = static_cast<#{parent_type}>(parent_handle);"
288
+ lines << " return new #{spec[:qt_class]}(parent);"
289
+ lines << '}'
290
+ end
291
+
292
+ def emit_cpp_keysequence_parent_constructor(lines, name, spec)
293
+ parent_type = spec[:constructor][:parent_type]
294
+ parent_class = parent_type.delete('*')
295
+ lines << "extern \"C\" void* #{name}(const char* key, void* parent_handle) {"
296
+ lines << ' const char* raw = key ? key : "";'
297
+ lines << " #{parent_class}* parent = static_cast<#{parent_type}>(parent_handle);"
298
+ lines << " return new #{spec[:qt_class]}(QKeySequence(as_qstring(raw)), parent);"
299
+ lines << '}'
300
+ end
301
+
302
+ def generate_cpp_constructor(lines, spec)
303
+ name = ctor_function_name(spec)
304
+
305
+ if spec[:constructor][:mode] == :qapplication
306
+ emit_cpp_qapplication_constructor(lines, name)
307
+ return
308
+ end
309
+ if spec[:constructor][:mode] == :string_path
310
+ emit_cpp_string_path_constructor(lines, name, spec[:qt_class], spec[:constructor][:arg_cast])
311
+ return
312
+ end
313
+ if spec[:constructor][:mode] == :keysequence_parent
314
+ emit_cpp_keysequence_parent_constructor(lines, name, spec)
315
+ return
316
+ end
317
+
318
+ unless spec[:constructor][:parent]
319
+ emit_cpp_default_constructor(lines, name, spec[:qt_class])
320
+ return
321
+ end
322
+
323
+ emit_cpp_parent_constructor(lines, name, spec)
324
+ end
325
+
326
+ def generate_cpp_delete(lines)
327
+ lines << 'extern "C" bool qt_ruby_qapplication_delete(void* app_handle) {'
328
+ lines << ' // Runtime performs safe shutdown ordering and thread checks.'
329
+ lines << ' return QtRubyRuntime::qapplication_delete(app_handle);'
330
+ lines << '}'
331
+ end
332
+
333
+ def cpp_method_signature(method)
334
+ ['void* handle'] + method[:args].map { |arg| "#{ffi_to_cpp_type(arg[:ffi])} #{arg[:name]}" }
335
+ end
336
+
337
+ def cpp_null_handle_return(method)
338
+ case method[:ffi_return]
339
+ when :int
340
+ ' return -1;'
341
+ when :bool
342
+ ' return false;'
343
+ when :pointer, :string
344
+ ' return nullptr;'
345
+ else
346
+ ' return;'
347
+ end
348
+ end
349
+
350
+ def emit_cpp_method_return(lines, method, invocation)
351
+ CppMethodReturnEmitter.new(lines: lines, method: method, invocation: invocation).emit
352
+ end
353
+
354
+ def cpp_method_invocation(method)
355
+ call_args = method[:args].map { |arg| arg_expr(arg) }.join(', ')
356
+ "self_obj->#{method[:qt_name]}(#{call_args})"
357
+ end
358
+
359
+ def generate_cpp_method(lines, spec, method)
360
+ fn = method_function_name(spec, method)
361
+ ret = ffi_return_to_cpp(method[:ffi_return])
362
+ lines << "extern \"C\" #{ret} #{fn}(#{cpp_method_signature(method).join(', ')}) {"
363
+ lines << ' if (!handle) {'
364
+ lines << cpp_null_handle_return(method)
365
+ lines << ' }'
366
+ lines << ''
367
+ lines << " auto* self_obj = static_cast<#{spec[:qt_class]}*>(handle);"
368
+ emit_cpp_method_return(lines, method, cpp_method_invocation(method))
369
+ lines << '}'
370
+ end
371
+
372
+ def generate_cpp_bridge(specs, free_function_specs)
373
+ lines = required_includes(GENERATOR_SCOPE).map { |inc| "#include <#{inc}>" }
374
+ append_block(lines, cpp_bridge_prelude)
375
+ append_cpp_free_function_definitions(lines, free_function_specs)
376
+
377
+ specs.each { |spec| append_cpp_spec_methods(lines, spec) }
378
+
379
+ generate_cpp_delete(lines)
380
+ "#{lines.join("\n")}\n"
381
+ end
382
+
383
+ def append_cpp_spec_methods(lines, spec)
384
+ generate_cpp_constructor(lines, spec)
385
+ lines << ''
386
+ spec[:methods].each do |method|
387
+ generate_cpp_method(lines, spec, method)
388
+ lines << ''
389
+ end
390
+ end
391
+
392
+ def append_block(lines, block)
393
+ lines.concat(block.strip.split("\n"))
394
+ lines << ''
395
+ end
396
+
397
+ def cpp_bridge_prelude
398
+ <<~CPP
399
+ #include <QByteArray>
400
+ #include <QAnyStringView>
401
+ #include <QIcon>
402
+ #include <QJsonDocument>
403
+ #include <QJsonParseError>
404
+ #include <QDateTime>
405
+ #include <QDate>
406
+ #include <QTime>
407
+ #include <QKeySequence>
408
+ #include <QString>
409
+ #include <QVariant>
410
+ #include "qt_ruby_runtime.hpp"
411
+
412
+ namespace {
413
+
414
+ QString as_qstring(const char* value, const char* fallback = "") {
415
+ if (!value) {
416
+ return QString::fromUtf8(fallback);
417
+ }
418
+
419
+ return QString::fromUtf8(value);
420
+ }
421
+
422
+ QVariant qvariant_from_bridge_value(const char* value) {
423
+ const QString raw = as_qstring(value);
424
+ if (!raw.startsWith(QStringLiteral("qtv:"))) {
425
+ return QVariant(raw);
426
+ }
427
+
428
+ if (raw == QStringLiteral("qtv:nil")) {
429
+ return QVariant();
430
+ }
431
+
432
+ const int first_colon = raw.indexOf(':', 4);
433
+ if (first_colon < 0) {
434
+ return QVariant(raw);
435
+ }
436
+
437
+ const QString tag = raw.mid(4, first_colon - 4);
438
+ const QString payload = raw.mid(first_colon + 1);
439
+
440
+ if (tag == QStringLiteral("bool")) {
441
+ return QVariant(payload == QStringLiteral("1"));
442
+ }
443
+
444
+ if (tag == QStringLiteral("int")) {
445
+ bool ok = false;
446
+ const qlonglong parsed = payload.toLongLong(&ok);
447
+ return ok ? QVariant(parsed) : QVariant(raw);
448
+ }
449
+
450
+ if (tag == QStringLiteral("float")) {
451
+ bool ok = false;
452
+ const double parsed = payload.toDouble(&ok);
453
+ return ok ? QVariant(parsed) : QVariant(raw);
454
+ }
455
+
456
+ if (tag == QStringLiteral("str")) {
457
+ const QByteArray decoded = QByteArray::fromBase64(payload.toUtf8());
458
+ return QVariant(QString::fromUtf8(decoded));
459
+ }
460
+
461
+ if (tag == QStringLiteral("json")) {
462
+ const QByteArray decoded = QByteArray::fromBase64(payload.toUtf8());
463
+ QJsonParseError err{};
464
+ const QJsonDocument doc = QJsonDocument::fromJson(decoded, &err);
465
+ if (err.error == QJsonParseError::NoError) {
466
+ return doc.toVariant();
467
+ }
468
+ }
469
+
470
+ return QVariant(raw);
471
+ }
472
+
473
+ QDateTime qdatetime_from_bridge_value(const char* value) {
474
+ const QString raw = as_qstring(value);
475
+ const QString payload = raw.startsWith(QStringLiteral("qtdt:")) ? raw.mid(5) : raw;
476
+ QDateTime parsed = QDateTime::fromString(payload, Qt::ISODateWithMs);
477
+ if (!parsed.isValid()) {
478
+ parsed = QDateTime::fromString(payload, Qt::ISODate);
479
+ }
480
+ return parsed;
481
+ }
482
+
483
+ QDate qdate_from_bridge_value(const char* value) {
484
+ const QString raw = as_qstring(value);
485
+ const QString payload = raw.startsWith(QStringLiteral("qtdate:")) ? raw.mid(7) : raw;
486
+ QDate parsed = QDate::fromString(payload, QStringLiteral("yyyy-MM-dd"));
487
+ if (!parsed.isValid()) {
488
+ parsed = QDate::fromString(payload, Qt::ISODate);
489
+ }
490
+ return parsed;
491
+ }
492
+
493
+ QTime qtime_from_bridge_value(const char* value) {
494
+ const QString raw = as_qstring(value);
495
+ const QString payload = raw.startsWith(QStringLiteral("qttime:")) ? raw.mid(7) : raw;
496
+ QTime parsed = QTime::fromString(payload, QStringLiteral("HH:mm:ss"));
497
+ if (!parsed.isValid()) {
498
+ parsed = QTime::fromString(payload, QStringLiteral("HH:mm"));
499
+ }
500
+ return parsed;
501
+ }
502
+
503
+ QString qdatetime_to_bridge_string(const QDateTime& value) {
504
+ return QStringLiteral("qtdt:") + value.toString(Qt::ISODateWithMs);
505
+ }
506
+
507
+ QString qdate_to_bridge_string(const QDate& value) {
508
+ return QStringLiteral("qtdate:") + value.toString(QStringLiteral("yyyy-MM-dd"));
509
+ }
510
+
511
+ QString qtime_to_bridge_string(const QTime& value) {
512
+ return QStringLiteral("qttime:") + value.toString(QStringLiteral("HH:mm:ss"));
513
+ }
514
+
515
+ QString qvariant_to_bridge_string(const QVariant& value) {
516
+ if (!value.isValid() || value.isNull()) {
517
+ return QStringLiteral("qtv:nil");
518
+ }
519
+
520
+ switch (value.metaType().id()) {
521
+ case QMetaType::Bool:
522
+ return QStringLiteral("qtv:bool:") + (value.toBool() ? QStringLiteral("1") : QStringLiteral("0"));
523
+ case QMetaType::Int:
524
+ case QMetaType::UInt:
525
+ case QMetaType::LongLong:
526
+ case QMetaType::ULongLong:
527
+ return QStringLiteral("qtv:int:") + QString::number(value.toLongLong());
528
+ case QMetaType::Float:
529
+ case QMetaType::Double:
530
+ return QStringLiteral("qtv:float:") + QString::number(value.toDouble(), 'g', 17);
531
+ case QMetaType::QString: {
532
+ const QByteArray b64 = value.toString().toUtf8().toBase64();
533
+ return QStringLiteral("qtv:str:") + QString::fromUtf8(b64);
534
+ }
535
+ default:
536
+ break;
537
+ }
538
+
539
+ const QJsonDocument doc = QJsonDocument::fromVariant(value);
540
+ if (!doc.isNull()) {
541
+ const QByteArray b64 = doc.toJson(QJsonDocument::Compact).toBase64();
542
+ return QStringLiteral("qtv:json:") + QString::fromUtf8(b64);
543
+ }
544
+
545
+ const QByteArray fallback = value.toString().toUtf8().toBase64();
546
+ return QStringLiteral("qtv:str:") + QString::fromUtf8(fallback);
547
+ }
548
+ } // namespace
549
+ CPP
550
+ end
551
+
552
+ def generate_bridge_api(specs, free_function_specs)
553
+ lines = bridge_api_prelude_lines
554
+ append_bridge_api_function_lines(lines, specs, free_function_specs)
555
+ lines.concat(bridge_api_closure_lines)
556
+ "#{lines.join("\n")}\n"
557
+ end
558
+
559
+ def bridge_api_prelude_lines
560
+ [
561
+ '# frozen_string_literal: true',
562
+ '',
563
+ 'module Qt',
564
+ ' module BridgeAPI',
565
+ ' FUNCTIONS = ['
566
+ ]
567
+ end
568
+
569
+ def append_bridge_api_function_lines(lines, specs, free_function_specs)
570
+ all_ffi_functions(specs, free_function_specs: free_function_specs).each do |fn|
571
+ args = fn[:args].map { |arg| ":#{arg}" }.join(', ')
572
+ lines << " { name: :#{fn[:name]}, args: [#{args}], return: :#{fn[:ffi_return]} },"
573
+ end
574
+ end
575
+
576
+ def bridge_api_closure_lines
577
+ [
578
+ ' ].freeze',
579
+ ' end',
580
+ 'end'
581
+ ]
582
+ end
583
+
584
+ def ruby_api_metadata(methods)
585
+ qt_method_names = methods.map { |method| method[:qt_name] }.uniq
586
+ ruby_method_names = methods.flat_map do |method|
587
+ ruby_name = ruby_safe_method_name(method[:ruby_name])
588
+ snake = to_snake(ruby_name)
589
+ snake == ruby_name ? [ruby_name] : [ruby_name, snake]
590
+ end.uniq
591
+ properties = methods.filter_map { |method| method[:property] }.uniq
592
+
593
+ {
594
+ qt_method_names: qt_method_names,
595
+ ruby_method_names: ruby_method_names,
596
+ properties: properties
597
+ }
598
+ end
599
+
600
+ def append_ruby_class_api_constants(lines, qt_class:, metadata:, indent:)
601
+ lines << "#{indent}QT_CLASS = '#{qt_class}'.freeze"
602
+ lines << "#{indent}QT_API_QT_METHODS = #{metadata[:qt_method_names].inspect}.freeze"
603
+ lines << "#{indent}QT_API_RUBY_METHODS = #{metadata[:ruby_method_names].map(&:to_sym).inspect}.freeze"
604
+ lines << "#{indent}QT_API_PROPERTIES = #{metadata[:properties].map(&:to_sym).inspect}.freeze"
605
+ end
606
+
607
+ def ruby_method_arguments(method, arg_map, required_arg_count)
608
+ method[:args].each_with_index.map do |arg, idx|
609
+ safe = arg_map[arg[:name]]
610
+ idx < required_arg_count ? safe : "#{safe} = nil"
611
+ end.join(', ')
612
+ end
613
+
614
+ def optional_arg_replacement(arg, safe)
615
+ case arg[:ffi]
616
+ when :int then "(#{safe}.nil? ? 0 : #{safe})"
617
+ when :bool then "(#{safe}.nil? ? false : #{safe})"
618
+ when :pointer then safe
619
+ when :string
620
+ return "(#{safe}.nil? ? '' : Qt::VariantCodec.encode(#{safe}))" if arg[:cast] == :qvariant_from_utf8
621
+ return "(#{safe}.nil? ? '' : Qt::DateTimeCodec.encode_qdatetime(#{safe}))" if arg[:cast] == :qdatetime_from_utf8
622
+ return "(#{safe}.nil? ? '' : Qt::DateTimeCodec.encode_qdate(#{safe}))" if arg[:cast] == :qdate_from_utf8
623
+ return "(#{safe}.nil? ? '' : Qt::DateTimeCodec.encode_qtime(#{safe}))" if arg[:cast] == :qtime_from_utf8
624
+ return "(#{safe}.nil? ? '' : Qt::KeySequenceCodec.encode(#{safe}))" if arg[:cast] == :qkeysequence_from_utf8
625
+ return "(#{safe}.nil? ? '' : Qt::StringCodec.to_qt_text(#{safe}))" if text_bridge_arg?(arg)
626
+
627
+ "(#{safe}.nil? ? '' : #{safe})"
628
+ else "(#{safe}.nil? ? '' : #{safe})"
629
+ end
630
+ end
631
+
632
+ def ruby_arg_call_value(arg, safe, optional:)
633
+ return "Qt::StringCodec.to_qt_text(#{safe})" if text_bridge_arg?(arg) && !optional
634
+ return "Qt::VariantCodec.encode(#{safe})" if arg[:cast] == :qvariant_from_utf8 && !optional
635
+ return "Qt::DateTimeCodec.encode_qdatetime(#{safe})" if arg[:cast] == :qdatetime_from_utf8 && !optional
636
+ return "Qt::DateTimeCodec.encode_qdate(#{safe})" if arg[:cast] == :qdate_from_utf8 && !optional
637
+ return "Qt::DateTimeCodec.encode_qtime(#{safe})" if arg[:cast] == :qtime_from_utf8 && !optional
638
+ return "Qt::KeySequenceCodec.encode(#{safe})" if arg[:cast] == :qkeysequence_from_utf8 && !optional
639
+
640
+ optional ? optional_arg_replacement(arg, safe) : safe
641
+ end
642
+
643
+ def text_bridge_arg?(arg)
644
+ arg[:ffi] == :string && %i[qstring qany_string_view].include?(arg[:cast])
645
+ end
646
+
647
+ def rewrite_native_call_args(native_call, method, arg_map, required_arg_count)
648
+ rewritten_native_call = native_call
649
+ method[:args].each_with_index do |arg, idx|
650
+ safe = arg_map[arg[:name]]
651
+ replacement = ruby_arg_call_value(arg, safe, optional: idx >= required_arg_count)
652
+ rewritten_native_call = rewritten_native_call.gsub(/\b#{Regexp.escape(arg[:name])}\b/, replacement)
653
+ end
654
+ rewritten_native_call
655
+ end
656
+
657
+ def append_ruby_native_call_method(lines, method:, native_call:, indent:)
658
+ ruby_name = ruby_safe_method_name(method[:ruby_name])
659
+ snake_alias = to_snake(ruby_name)
660
+ arg_map = ruby_arg_name_map(method[:args])
661
+ required_arg_count = method.fetch(:required_arg_count, method[:args].length)
662
+ ruby_args = ruby_method_arguments(method, arg_map, required_arg_count)
663
+ rewritten_native_call = rewrite_native_call_args(native_call, method, arg_map, required_arg_count)
664
+ method_body = ruby_native_method_body(method, rewritten_native_call)
665
+
666
+ lines << "#{indent}def #{ruby_name}(#{ruby_args})"
667
+ lines << "#{indent} #{method_body}"
668
+ lines << "#{indent}end"
669
+ lines << "#{indent}alias_method :#{snake_alias}, :#{ruby_name}" if snake_alias != ruby_name
670
+ end
671
+
672
+ def ruby_native_method_body(method, rewritten_native_call)
673
+ return "Qt::StringCodec.from_qt_text(#{rewritten_native_call})" if method[:return_cast] == :qstring_to_utf8
674
+ return "Qt::VariantCodec.decode(#{rewritten_native_call})" if method[:return_cast] == :qvariant_to_utf8
675
+ return "Qt::DateTimeCodec.decode_qdatetime(#{rewritten_native_call})" if method[:return_cast] == :qdatetime_to_utf8
676
+ return "Qt::DateTimeCodec.decode_qdate(#{rewritten_native_call})" if method[:return_cast] == :qdate_to_utf8
677
+ return "Qt::DateTimeCodec.decode_qtime(#{rewritten_native_call})" if method[:return_cast] == :qtime_to_utf8
678
+
679
+ rewritten_native_call
680
+ end
681
+
682
+ def append_ruby_property_writer(lines, method:, indent:)
683
+ return unless method[:property]
684
+
685
+ snake_property = to_snake(method[:property])
686
+ lines << "#{indent}def #{method[:property]}=(value)"
687
+ lines << "#{indent} set#{method[:property][0].upcase}#{method[:property][1..]}(value)"
688
+ lines << "#{indent}end"
689
+ lines << "#{indent}alias_method :#{snake_property}=, :#{method[:property]}=" if snake_property != method[:property]
690
+ end
691
+
692
+ def append_widget_initializer(lines, spec:, widget_root:, indent:)
693
+ if spec[:constructor][:mode] == :string_path
694
+ append_string_path_initializer(lines, spec, indent)
695
+ elsif spec[:constructor][:mode] == :keysequence_parent
696
+ append_keysequence_parent_initializer(lines, spec, widget_root, indent)
697
+ elsif spec[:constructor][:parent]
698
+ append_parent_widget_initializer(lines, spec, widget_root, indent)
699
+ else
700
+ append_default_widget_initializer(lines, spec, indent)
701
+ end
702
+
703
+ lines << "#{indent} yield self if block_given?"
704
+ lines << "#{indent}end"
705
+ end
706
+
707
+ def append_parent_widget_initializer(lines, spec, widget_root, indent)
708
+ lines << "#{indent}def initialize(parent = nil)"
709
+ lines << "#{indent} @handle = Native.#{spec[:prefix]}_new(parent&.handle)"
710
+ lines << "#{indent} init_children_tracking!" if widget_root
711
+ append_parent_registration_logic(lines, spec, indent)
712
+ end
713
+
714
+ def append_default_widget_initializer(lines, spec, indent)
715
+ lines << "#{indent}def initialize(_argc = 0, _argv = [])"
716
+ lines << "#{indent} @handle = Native.#{spec[:prefix]}_new"
717
+ end
718
+
719
+ def append_string_path_initializer(lines, spec, indent)
720
+ lines << "#{indent}def initialize(path = nil)"
721
+ lines << "#{indent} @handle = Native.#{spec[:prefix]}_new(Qt::StringCodec.to_qt_text(path))"
722
+ end
723
+
724
+ def append_keysequence_parent_initializer(lines, spec, widget_root, indent)
725
+ lines << "#{indent}def initialize(key = nil, parent = nil)"
726
+ lines << "#{indent} if parent.nil? && (key.nil? || key.respond_to?(:handle))"
727
+ lines << "#{indent} parent = key"
728
+ lines << "#{indent} key = nil"
729
+ lines << "#{indent} end"
730
+ lines << "#{indent} @handle = Native.#{spec[:prefix]}_new(Qt::KeySequenceCodec.encode(key), parent&.handle)"
731
+ lines << "#{indent} init_children_tracking!" if widget_root
732
+ append_parent_registration_logic(lines, spec, indent)
733
+ end
734
+
735
+ def append_parent_registration_logic(lines, spec, indent)
736
+ if spec[:ruby_class] == 'QWidget'
737
+ lines << "#{indent} if parent"
738
+ lines << "#{indent} parent.add_child(self)"
739
+ lines << "#{indent} else"
740
+ lines << "#{indent} app = QApplication.current"
741
+ lines << "#{indent} app&.register_window(self)"
742
+ lines << "#{indent} end"
743
+ elsif spec[:constructor][:register_in_parent]
744
+ lines << "#{indent} parent.add_child(self) if parent&.respond_to?(:add_child)"
745
+ end
746
+ end
747
+
748
+ def append_ruby_qapplication_prelude(lines, spec, metadata)
749
+ lines << ' class QApplication'
750
+ append_ruby_class_api_constants(lines, qt_class: spec[:qt_class], metadata: metadata, indent: ' ')
751
+ lines << ''
752
+ lines << ' attr_reader :handle'
753
+ lines << ' include Inspectable'
754
+ lines << ' include ApplicationLifecycle'
755
+ lines << ''
756
+ end
757
+
758
+ def append_ruby_qapplication_singleton_accessors(lines)
759
+ lines << ' class << self'
760
+ lines << ' def current'
761
+ lines << ' Thread.current[:qt_ruby_current_app]'
762
+ lines << ' end'
763
+ lines << ''
764
+ lines << ' def current=(app)'
765
+ lines << ' Thread.current[:qt_ruby_current_app] = app'
766
+ lines << ' end'
767
+ end
768
+
769
+ def generate_ruby_qapplication(lines, spec)
770
+ metadata = ruby_api_metadata(spec[:methods])
771
+
772
+ append_ruby_qapplication_prelude(lines, spec, metadata)
773
+ append_ruby_qapplication_singleton_accessors(lines)
774
+
775
+ Array(spec[:class_methods]).each { |method| append_ruby_qapplication_class_method(lines, method) }
776
+
777
+ lines << ' end'
778
+ lines << ''
779
+ lines << ' end'
780
+ lines << ''
781
+ end
782
+
783
+ def qapplication_method_arguments(method)
784
+ arg_hashes = Array(method[:args]).each_with_index.map { |arg, idx| qapplication_arg_spec(arg, idx) }
785
+ arg_map = ruby_arg_name_map(arg_hashes)
786
+ rendered_args = arg_hashes.map { |arg| arg_map[arg[:name]] }.join(', ')
787
+ [arg_hashes, arg_map, rendered_args]
788
+ end
789
+
790
+ def qapplication_arg_spec(arg, idx)
791
+ return arg.transform_keys(&:to_sym) if arg.is_a?(Hash)
792
+
793
+ { name: (arg || "arg#{idx + 1}").to_sym }
794
+ end
795
+
796
+ def qapplication_method_call_suffix(arg_hashes, arg_map, method)
797
+ required_arg_count = method.fetch(:required_arg_count, arg_hashes.length)
798
+ native_args = arg_hashes.each_with_index.map do |arg, idx|
799
+ safe = arg_map[arg[:name]]
800
+ ruby_arg_call_value(arg, safe, optional: idx >= required_arg_count)
801
+ end.join(', ')
802
+ native_args.empty? ? '' : "(#{native_args})"
803
+ end
804
+
805
+ def append_ruby_qapplication_class_method(lines, method)
806
+ ruby_name = ruby_safe_method_name(method[:ruby_name])
807
+ snake_alias = to_snake(ruby_name)
808
+ method_arg_hashes, arg_map, args = qapplication_method_arguments(method)
809
+ call_suffix = qapplication_method_call_suffix(method_arg_hashes, arg_map, method)
810
+
811
+ lines << ''
812
+ lines << " def #{ruby_name}(#{args})"
813
+ lines << if method[:native]
814
+ qapplication_class_method_body(method, "Native.#{method[:native]}#{call_suffix}")
815
+ else
816
+ ' nil'
817
+ end
818
+ lines << ' end'
819
+ lines << " alias_method :#{snake_alias}, :#{ruby_name}" if snake_alias != ruby_name
820
+ end
821
+
822
+ def qapplication_class_method_body(method, native_call)
823
+ return " Qt::StringCodec.from_qt_text(#{native_call})" if method[:return_cast] == :qstring_to_utf8
824
+ return " Qt::DateTimeCodec.decode_qdatetime(#{native_call})" if method[:return_cast] == :qdatetime_to_utf8
825
+ return " Qt::DateTimeCodec.decode_qdate(#{native_call})" if method[:return_cast] == :qdate_to_utf8
826
+ return " Qt::DateTimeCodec.decode_qtime(#{native_call})" if method[:return_cast] == :qtime_to_utf8
827
+
828
+ " #{native_call}"
829
+ end
830
+
831
+ def generate_ruby_widget_class_header(lines, spec, metadata:, super_ruby:, class_flags:)
832
+ widget_root = class_flags[:widget_root]
833
+ qobject_based = class_flags[:qobject_based]
834
+ class_decl = super_ruby ? " class #{spec[:ruby_class]} < #{super_ruby}" : " class #{spec[:ruby_class]}"
835
+ lines << class_decl
836
+ append_ruby_class_api_constants(lines, qt_class: spec[:qt_class], metadata: metadata, indent: ' ')
837
+ lines << ''
838
+ lines << ' attr_reader :handle'
839
+ lines << ' attr_reader :children' if widget_root
840
+ lines << ' include Inspectable'
841
+ lines << ' include ChildrenTracking' if widget_root
842
+ lines << ' include EventRuntime::QObjectMethods' if qobject_based
843
+ lines << ''
844
+ end
845
+
846
+ def append_ruby_widget_methods(lines, spec)
847
+ spec[:methods].each do |method|
848
+ call_args = ['@handle'] + method[:args].map { |arg| arg[:name] }
849
+ native_call = "Native.#{spec[:prefix]}_#{to_snake(method[:qt_name])}(#{call_args.join(', ')})"
850
+ append_ruby_native_call_method(lines, method: method, native_call: native_call, indent: ' ')
851
+ append_ruby_property_writer(lines, method: method, indent: ' ')
852
+ lines << ''
853
+ end
854
+ end
855
+
856
+ def generate_ruby_widget_class(lines, spec, specs_by_qt, super_qt_by_qt, qt_to_ruby)
857
+ metadata = ruby_api_metadata_for_spec(spec, specs_by_qt, super_qt_by_qt)
858
+ super_ruby = ruby_super_class_for_spec(spec, super_qt_by_qt, qt_to_ruby)
859
+ widget_root = spec[:ruby_class] == 'QWidget'
860
+ qobject_based = qobject_based_qt_class?(spec[:qt_class], super_qt_by_qt)
861
+
862
+ generate_ruby_widget_class_header(
863
+ lines,
864
+ spec,
865
+ metadata: metadata,
866
+ super_ruby: super_ruby,
867
+ class_flags: { widget_root: widget_root, qobject_based: qobject_based }
868
+ )
869
+ append_widget_initializer(lines, spec: spec, widget_root: widget_root, indent: ' ')
870
+ lines << ''
871
+ append_ruby_widget_methods(lines, spec)
872
+
873
+ lines << ' end'
874
+ lines << ''
875
+ end
876
+
877
+ def ruby_api_metadata_for_spec(spec, specs_by_qt, super_qt_by_qt)
878
+ inherited_methods = inherited_methods_for_spec(spec, specs_by_qt, super_qt_by_qt)
879
+ all_methods = (inherited_methods + spec[:methods]).uniq { |method| method[:qt_name] }
880
+ ruby_api_metadata(all_methods)
881
+ end
882
+
883
+ def ruby_super_class_for_spec(spec, super_qt_by_qt, qt_to_ruby)
884
+ super_qt = super_qt_by_qt[spec[:qt_class]]
885
+ super_qt ? qt_to_ruby[super_qt] : nil
886
+ end
887
+
888
+ def build_qt_to_ruby_map(specs, wrapper_qt_classes)
889
+ qt_to_ruby = specs.each_with_object({}) { |s, map| map[s[:qt_class]] = s[:ruby_class] }
890
+ wrapper_qt_classes.each { |qt_class| qt_to_ruby[qt_class] = qt_class }
891
+ qt_to_ruby
892
+ end
893
+
894
+ def qts_to_emit(specs, wrapper_qt_classes)
895
+ (wrapper_qt_classes + specs.map { |s| s[:qt_class] }.reject { |q| q == 'QApplication' }).uniq
896
+ end
897
+
898
+ def emit_qt_classes(lines, qts_to_emit, specs_by_qt, super_qt_by_qt, qt_to_ruby)
899
+ emitted = {}
900
+ emit_qt = lambda do |qt_class|
901
+ return if emitted[qt_class]
902
+
903
+ super_qt = super_qt_by_qt[qt_class]
904
+ emit_qt.call(super_qt) if super_qt && qts_to_emit.include?(super_qt)
905
+ emit_qt_class_definition(lines, qt_class, specs_by_qt, super_qt_by_qt, qt_to_ruby)
906
+ emitted[qt_class] = true
907
+ end
908
+ qts_to_emit.sort.each { |qt_class| emit_qt.call(qt_class) }
909
+ end
910
+
911
+ def emit_qt_class_definition(lines, qt_class, specs_by_qt, super_qt_by_qt, qt_to_ruby)
912
+ spec = specs_by_qt[qt_class]
913
+ if spec
914
+ generate_ruby_widget_class(lines, spec, specs_by_qt, super_qt_by_qt, qt_to_ruby)
915
+ else
916
+ super_qt = super_qt_by_qt[qt_class]
917
+ generate_ruby_wrapper_class(lines, qt_class, super_qt ? qt_to_ruby[super_qt] : nil)
918
+ end
919
+ end
920
+
921
+ def ruby_widgets_prelude_lines
922
+ ['# frozen_string_literal: true', '', 'module Qt']
923
+ end
924
+
925
+ def append_ruby_widgets_classes(lines, specs, super_qt_by_qt, wrapper_qt_classes)
926
+ qapplication_spec = specs.find { |spec| spec[:ruby_class] == 'QApplication' }
927
+ generate_ruby_qapplication(lines, qapplication_spec)
928
+
929
+ specs_by_qt = specs.each_with_object({}) { |spec, map| map[spec[:qt_class]] = spec }
930
+ qt_to_ruby = build_qt_to_ruby_map(specs, wrapper_qt_classes)
931
+ emitted_qts = qts_to_emit(specs, wrapper_qt_classes)
932
+ emit_qt_classes(lines, emitted_qts, specs_by_qt, super_qt_by_qt, qt_to_ruby)
933
+ end
934
+
935
+ def generate_ruby_widgets(specs, super_qt_by_qt, wrapper_qt_classes)
936
+ lines = ruby_widgets_prelude_lines
937
+ append_ruby_widgets_classes(lines, specs, super_qt_by_qt, wrapper_qt_classes)
938
+
939
+ lines << 'end'
940
+ "#{lines.join("\n")}\n"
941
+ end
942
+
943
+ def ast_extract_first_value(node)
944
+ return nil unless node.is_a?(Hash)
945
+
946
+ value = node['value']
947
+ return value if value && !value.to_s.empty?
948
+
949
+ Array(node['inner']).each do |child|
950
+ nested = ast_extract_first_value(child)
951
+ return nested if nested
952
+ end
953
+ nil
954
+ end
955
+
956
+ def parse_ast_integer_value(raw)
957
+ return nil if raw.nil?
958
+
959
+ text = raw.to_s.strip
960
+ return nil if text.empty?
961
+
962
+ text = text.delete("'")
963
+ text = text.gsub(/([0-9A-Fa-fxX]+)(?:[uUlL]+)\z/, '\1')
964
+ Integer(text, 0)
965
+ rescue ArgumentError
966
+ nil
967
+ end
968
+
969
+ def append_constant_with_conflict_warning(constants, name, value, warnings, context)
970
+ existing = constants[name]
971
+ if existing.nil?
972
+ constants[name] = value
973
+ return
974
+ end
975
+
976
+ return if existing == value
977
+
978
+ warnings << "#{context}: #{name}=#{value} conflicts with existing #{existing}; keeping existing #{existing}"
979
+ end
980
+
981
+ def collect_enum_constants_for_scope(ast, target_scope, warnings = [])
982
+ constants = {}
983
+
984
+ walk_ast_scoped(ast) do |node, scope|
985
+ next unless node['kind'] == 'EnumDecl'
986
+ next unless scope == target_scope
987
+
988
+ Array(node['inner']).each do |entry|
989
+ next unless entry['kind'] == 'EnumConstantDecl'
990
+
991
+ name = entry['name'].to_s
992
+ next unless name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
993
+ next if constants.key?(name)
994
+
995
+ raw_value = ast_extract_first_value(entry)
996
+ value = parse_ast_integer_value(raw_value)
997
+ next if value.nil?
998
+
999
+ append_constant_with_conflict_warning(constants, name, value, warnings, target_scope.join('::'))
1000
+ end
1001
+ end
1002
+
1003
+ constants
1004
+ end
1005
+
1006
+ def collect_qt_namespace_enum_constants(ast, warnings = [])
1007
+ constants = collect_enum_constants_for_scope(ast, ['Qt'], warnings)
1008
+ collect_enum_constants_for_scope(ast, ['QEvent'], warnings).each do |name, value|
1009
+ alias_name = "Event#{name}"
1010
+ next unless alias_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
1011
+
1012
+ append_constant_with_conflict_warning(constants, alias_name, value, warnings, 'Qt::QEventAlias')
1013
+ end
1014
+ constants
1015
+ end
1016
+
1017
+ def collect_qt_scoped_enum_constants(ast, warnings = [])
1018
+ constants_by_owner = Hash.new { |h, k| h[k] = {} }
1019
+
1020
+ walk_ast_scoped(ast) do |node, scope|
1021
+ next unless node['kind'] == 'EnumDecl'
1022
+ next if scope.empty?
1023
+
1024
+ owner = scope.first
1025
+ next unless owner.match?(/\AQ[A-Z]\w*\z/)
1026
+ next if owner == 'Qt' || owner == 'QEvent'
1027
+
1028
+ Array(node['inner']).each do |entry|
1029
+ next unless entry['kind'] == 'EnumConstantDecl'
1030
+
1031
+ name = entry['name'].to_s
1032
+ next unless name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
1033
+
1034
+ raw_value = ast_extract_first_value(entry)
1035
+ value = parse_ast_integer_value(raw_value)
1036
+ next if value.nil?
1037
+
1038
+ append_constant_with_conflict_warning(
1039
+ constants_by_owner[owner],
1040
+ name,
1041
+ value,
1042
+ warnings,
1043
+ "Qt::#{owner}"
1044
+ )
1045
+ end
1046
+ end
1047
+
1048
+ constants_by_owner
1049
+ end
1050
+
1051
+ def emit_generation_warnings(warnings)
1052
+ warnings.uniq.each { |message| warn("WARNING: #{message}") }
1053
+ end
1054
+
1055
+ def generate_ruby_constants(ast)
1056
+ warnings = []
1057
+ constants = collect_qt_namespace_enum_constants(ast, warnings)
1058
+ scoped_constants = collect_qt_scoped_enum_constants(ast, warnings)
1059
+ emit_generation_warnings(warnings)
1060
+ lines = ['# frozen_string_literal: true', '', 'module Qt']
1061
+
1062
+ constants.sort.each do |name, value|
1063
+ lines << " #{name} = #{value} unless const_defined?(:#{name}, false)"
1064
+ end
1065
+
1066
+ lines << ''
1067
+ lines << ' GENERATED_SCOPED_CONSTANTS = {'
1068
+ scoped_constants.sort.each do |owner, owner_constants|
1069
+ lines << " '#{owner}' => {"
1070
+ owner_constants.sort.each do |name, value|
1071
+ lines << " '#{name}' => #{value},"
1072
+ end
1073
+ lines << ' },'
1074
+ end
1075
+ lines << ' }.freeze unless const_defined?(:GENERATED_SCOPED_CONSTANTS, false)'
1076
+
1077
+ lines << 'end'
1078
+ "#{lines.join("\n")}\n"
1079
+ end
1080
+
1081
+ total_start = monotonic_now
1082
+ ast = timed('ast_dump_total') { ast_dump }
1083
+ free_function_specs = timed('build_free_function_specs') { qt_free_function_specs(ast) }
1084
+ base_specs = timed('build_base_specs') { build_base_specs(ast) }
1085
+ timed('validate_qt_api') { validate_qt_api!(ast, base_specs) }
1086
+ expanded_specs = timed('expand_auto_methods') { expand_auto_methods(base_specs, ast) }
1087
+ effective_specs = timed('enrich_specs_with_properties') { enrich_specs_with_properties(expanded_specs, ast) }
1088
+ super_qt_by_qt, wrapper_qt_classes = timed('build_generated_inheritance') do
1089
+ build_generated_inheritance(ast, effective_specs)
1090
+ end
1091
+
1092
+ timed('write_cpp_bridge') do
1093
+ FileUtils.mkdir_p(File.dirname(CPP_PATH))
1094
+ File.write(CPP_PATH, generate_cpp_bridge(effective_specs, free_function_specs))
1095
+ end
1096
+ timed('write_bridge_api') do
1097
+ FileUtils.mkdir_p(File.dirname(API_PATH))
1098
+ File.write(API_PATH, generate_bridge_api(effective_specs, free_function_specs))
1099
+ end
1100
+ timed('write_ruby_constants') do
1101
+ FileUtils.mkdir_p(File.dirname(RUBY_CONSTANTS_PATH))
1102
+ File.write(RUBY_CONSTANTS_PATH, generate_ruby_constants(ast))
1103
+ end
1104
+ timed('write_ruby_widgets') do
1105
+ FileUtils.mkdir_p(File.dirname(RUBY_WIDGETS_PATH))
1106
+ File.write(RUBY_WIDGETS_PATH, generate_ruby_widgets(effective_specs, super_qt_by_qt, wrapper_qt_classes))
1107
+ end
1108
+ debug_log("total=#{format('%.3fs', monotonic_now - total_start)}")
1109
+
1110
+ puts "Generated #{CPP_PATH}"
1111
+ puts "Generated #{API_PATH}"
1112
+ puts "Generated #{RUBY_CONSTANTS_PATH}"
1113
+ puts "Generated #{RUBY_WIDGETS_PATH}"