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,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ RUNTIME_HEADER_PATH = File.expand_path('../../ext/qt_ruby_bridge/qt_ruby_runtime.hpp', __dir__)
4
+
5
+ SCALAR_CLASS_METHOD_FFI_RETURNS = %i[void int bool string].freeze
6
+ QAPPLICATION_STATIC_METHOD_EXCLUSIONS = %w[exec].freeze
7
+ QAPPLICATION_STATIC_TEXT_SETTERS = %w[
8
+ setApplicationName
9
+ setDesktopFileName
10
+ setOrganizationName
11
+ setApplicationDisplayName
12
+ ].freeze
13
+
14
+ def qt_free_function_specs(ast)
15
+ @qt_free_function_specs_cache ||= {}.compare_by_identity
16
+ return @qt_free_function_specs_cache[ast] if @qt_free_function_specs_cache.key?(ast)
17
+
18
+ qt_specs = []
19
+ qt_specs << qversion_free_function_spec(ast)
20
+ qt_specs.concat(qapplication_static_free_function_specs(ast))
21
+ qt_specs.concat(qapplication_static_text_setter_specs(ast))
22
+ qt_specs << qapplication_top_level_widgets_count_spec(ast)
23
+ runtime_specs = runtime_free_function_specs_from_header
24
+ @qt_free_function_specs_cache[ast] = merge_free_function_specs(qt_specs, runtime_specs).freeze
25
+ end
26
+
27
+ def merge_free_function_specs(primary_specs, secondary_specs)
28
+ merged = primary_specs.dup
29
+ existing_names = merged.to_set { |spec| spec[:name] }
30
+ secondary_specs.each do |spec|
31
+ if existing_names.include?(spec[:name])
32
+ warn "[gen][warn] skip runtime free-function #{spec[:name]}: already provided by Qt-derived spec"
33
+ next
34
+ end
35
+
36
+ merged << spec
37
+ existing_names << spec[:name]
38
+ end
39
+ merged
40
+ end
41
+
42
+ def runtime_free_function_specs_from_header
43
+ @runtime_free_function_specs_from_header ||= begin
44
+ raw = File.read(RUNTIME_HEADER_PATH)
45
+ parse_runtime_free_function_specs(raw).freeze
46
+ end
47
+ end
48
+
49
+ def parse_runtime_free_function_specs(raw)
50
+ in_namespace = false
51
+ specs = []
52
+ raw.each_line do |line|
53
+ in_namespace = true if line.include?('namespace QtRubyRuntime')
54
+ in_namespace = false if in_namespace && line.include?('} // namespace QtRubyRuntime')
55
+ next unless in_namespace
56
+
57
+ decl = parse_runtime_function_decl(line)
58
+ next unless decl
59
+
60
+ specs << runtime_decl_to_free_function_spec(decl)
61
+ end
62
+ specs
63
+ end
64
+
65
+ def parse_runtime_function_decl(line)
66
+ md = line.strip.match(/\A(void|int)\s+([a-zA-Z_]\w*)\s*\(([^)]*)\)\s*;\z/)
67
+ return nil unless md
68
+
69
+ {
70
+ return_type: md[1],
71
+ name: md[2],
72
+ args: parse_runtime_decl_args(md[3])
73
+ }
74
+ end
75
+
76
+ def parse_runtime_decl_args(args_raw)
77
+ stripped = args_raw.to_s.strip
78
+ return [] if stripped.empty?
79
+
80
+ stripped.split(',').map { |arg| parse_runtime_decl_arg(arg) }
81
+ end
82
+
83
+ def parse_runtime_decl_arg(arg)
84
+ normalized = arg.to_s.strip.gsub(/\s+/, ' ')
85
+ md = normalized.match(/\A(.+?)\s+([a-zA-Z_]\w*)\z/)
86
+ raise "Unsupported runtime declaration argument: #{arg.inspect}" unless md
87
+
88
+ { cpp_type: md[1].strip, name: md[2] }
89
+ end
90
+
91
+ def runtime_cpp_type_to_ffi(cpp_type)
92
+ normalized = cpp_type.to_s.strip.gsub(/\s+/, ' ').gsub(/\s*\*\s*/, '*')
93
+ return :pointer if normalized == 'void*'
94
+ return :string if normalized == 'const char*'
95
+ return :int if normalized == 'int'
96
+
97
+ raise "Unsupported runtime declaration type for FFI: #{cpp_type.inspect}"
98
+ end
99
+
100
+ def runtime_decl_to_free_function_spec(decl)
101
+ cpp_args = decl[:args].map { |arg| "#{arg[:cpp_type]} #{arg[:name]}" }.join(', ')
102
+ call_args = decl[:args].map { |arg| arg[:name] }.join(', ')
103
+ cpp_body =
104
+ if decl[:return_type] == 'void'
105
+ ["QtRubyRuntime::#{decl[:name]}(#{call_args});"]
106
+ else
107
+ ["return QtRubyRuntime::#{decl[:name]}(#{call_args});"]
108
+ end
109
+
110
+ {
111
+ name: "qt_ruby_#{decl[:name]}",
112
+ ffi_return: decl[:return_type].to_sym,
113
+ args: decl[:args].map { |arg| runtime_cpp_type_to_ffi(arg[:cpp_type]) },
114
+ cpp_return: decl[:return_type],
115
+ cpp_args: cpp_args,
116
+ cpp_body: cpp_body
117
+ }
118
+ end
119
+
120
+ def qversion_free_function_spec(ast)
121
+ found = false
122
+ walk_ast(ast) do |node|
123
+ next unless node['kind'] == 'FunctionDecl'
124
+ next unless node['name'] == 'qVersion'
125
+ next unless node.dig('type', 'qualType').to_s.include?('const char *()')
126
+
127
+ found = true
128
+ break
129
+ end
130
+ raise 'Unable to find qVersion() in AST' unless found
131
+
132
+ {
133
+ name: 'qt_ruby_qt_version',
134
+ ffi_return: :string,
135
+ args: [],
136
+ cpp_return: 'const char*',
137
+ cpp_args: '',
138
+ cpp_body: ['return qVersion();'],
139
+ qapplication_method: { ruby_name: 'qtVersion', native: 'qt_version', args: [], return_cast: :qstring_to_utf8 }
140
+ }
141
+ end
142
+
143
+ def qapplication_top_level_widgets_count_spec(ast)
144
+ has_top_level_widgets = collect_method_decls_with_bases(ast, 'QApplication', 'topLevelWidgets').any?
145
+ raise 'Unable to find QApplication::topLevelWidgets in AST' unless has_top_level_widgets
146
+
147
+ {
148
+ name: 'qt_ruby_qapplication_top_level_widgets_count',
149
+ ffi_return: :int,
150
+ args: [],
151
+ cpp_return: 'int',
152
+ cpp_args: '',
153
+ cpp_body: ['return QApplication::topLevelWidgets().size();'],
154
+ qapplication_method: { ruby_name: 'topLevelWidgetsCount', native: 'qapplication_top_level_widgets_count', args: [] }
155
+ }
156
+ end
157
+
158
+ def qapplication_static_free_function_specs(ast)
159
+ int_cast_types = ast_int_cast_type_set(ast)
160
+ method_names = collect_method_names_with_bases(ast, 'QApplication').uniq
161
+ method_names -= QAPPLICATION_STATIC_METHOD_EXCLUSIONS
162
+ method_names.filter_map do |qt_name|
163
+ build_qapplication_static_free_function_spec(ast, qt_name, int_cast_types)
164
+ end
165
+ end
166
+
167
+ def qapplication_static_text_setter_specs(ast)
168
+ QAPPLICATION_STATIC_TEXT_SETTERS.filter_map do |qt_name|
169
+ build_qapplication_static_text_setter_spec(ast, qt_name)
170
+ end
171
+ end
172
+
173
+ def build_qapplication_static_text_setter_spec(ast, qt_name)
174
+ return nil unless resolve_qapplication_static_text_setter_candidate(ast, qt_name)
175
+
176
+ native_name = "qapplication_#{to_snake(qt_name)}"
177
+ {
178
+ name: "qt_ruby_#{native_name}",
179
+ ffi_return: :void,
180
+ args: [:string],
181
+ cpp_return: 'void',
182
+ cpp_args: 'const char* value',
183
+ cpp_body: ["QApplication::#{qt_name}(as_qstring(value));"],
184
+ qapplication_method: {
185
+ ruby_name: qt_name,
186
+ native: native_name,
187
+ args: [{ name: :value, ffi: :string, cast: :qstring }],
188
+ required_arg_count: 1
189
+ }
190
+ }
191
+ end
192
+
193
+ def resolve_qapplication_static_text_setter_candidate(ast, qt_name)
194
+ decls = collect_method_decls_with_bases(ast, 'QApplication', qt_name)
195
+ decls.any? do |decl|
196
+ next false unless decl['storageClass'] == 'static'
197
+ next false unless decl['__effective_access'] == 'public'
198
+
199
+ parsed = parse_method_signature(decl)
200
+ next false unless parsed
201
+ next false unless parsed[:return_type].to_s.strip == 'void'
202
+ next false unless parsed[:required_arg_count] == 1
203
+ next false unless parsed[:params].length == 1
204
+
205
+ normalized = normalized_cpp_type_name(parsed[:params].first[:type])
206
+ normalized == 'QString'
207
+ end
208
+ end
209
+
210
+ def build_qapplication_static_free_function_spec(ast, qt_name, int_cast_types)
211
+ candidate = resolve_qapplication_static_noarg_candidate(ast, qt_name, int_cast_types)
212
+ return nil unless candidate
213
+
214
+ native_name = "qapplication_#{to_snake(qt_name)}"
215
+ cpp_body =
216
+ if candidate[:ffi_return] == :void
217
+ ["QApplication::#{qt_name}();"]
218
+ elsif candidate[:enum_cast]
219
+ ["return static_cast<int>(QApplication::#{qt_name}());"]
220
+ elsif candidate[:ffi_return] == :string
221
+ [
222
+ "const QString value = QApplication::#{qt_name}();",
223
+ 'thread_local QByteArray utf8;',
224
+ 'utf8 = value.toUtf8();',
225
+ 'return utf8.constData();'
226
+ ]
227
+ else
228
+ ["return QApplication::#{qt_name}();"]
229
+ end
230
+
231
+ {
232
+ name: "qt_ruby_#{native_name}",
233
+ ffi_return: candidate[:ffi_return],
234
+ args: [],
235
+ cpp_return: ffi_return_to_cpp(candidate[:ffi_return]),
236
+ cpp_args: '',
237
+ cpp_body: cpp_body,
238
+ qapplication_method: {
239
+ ruby_name: qt_name,
240
+ native: native_name,
241
+ args: [],
242
+ return_cast: candidate[:return_cast]
243
+ }
244
+ }
245
+ end
246
+
247
+ def resolve_qapplication_static_noarg_candidate(ast, qt_name, int_cast_types)
248
+ decls = collect_method_decls_with_bases(ast, 'QApplication', qt_name)
249
+ candidates = decls.filter_map do |decl|
250
+ next unless decl['storageClass'] == 'static'
251
+ next unless decl['__effective_access'] == 'public'
252
+
253
+ parsed = parse_method_signature(decl)
254
+ next unless parsed && parsed[:required_arg_count].zero?
255
+
256
+ ret_info = qapplication_static_return_info(parsed[:return_type], int_cast_types)
257
+ next unless ret_info
258
+
259
+ { decl: decl, param_count: parsed[:params].length }.merge(ret_info)
260
+ end
261
+ return nil if candidates.empty?
262
+
263
+ candidates.min_by { |item| item[:param_count] }
264
+ end
265
+
266
+ def qapplication_static_return_info(return_type, int_cast_types)
267
+ mapped = map_cpp_return_type(return_type)
268
+ if mapped && SCALAR_CLASS_METHOD_FFI_RETURNS.include?(mapped[:ffi_return])
269
+ return { ffi_return: mapped[:ffi_return], return_cast: mapped[:return_cast], enum_cast: false }
270
+ end
271
+
272
+ normalized = normalized_cpp_type_name(return_type)
273
+ return nil unless normalized.include?('::') && int_cast_types.include?(normalized)
274
+
275
+ { ffi_return: :int, return_cast: nil, enum_cast: true }
276
+ end
277
+
278
+ def qapplication_class_method_specs(ast)
279
+ qt_free_function_specs(ast).filter_map { |spec| spec[:qapplication_method] }
280
+ end
281
+
282
+ def append_cpp_free_function_definitions(lines, free_function_specs)
283
+ free_function_specs.each do |spec|
284
+ lines << "extern \"C\" #{spec[:cpp_return]} #{spec[:name]}(#{spec[:cpp_args]}) {"
285
+ Array(spec[:cpp_body]).each { |line| lines << " #{line}" }
286
+ lines << '}'
287
+ lines << ''
288
+ end
289
+ end
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ SCALAR_BRIDGED_QT_TYPES = Set.new(%w[QString QVariant QAnyStringView QByteArray QDateTime QDate QTime]).freeze
4
+
5
+ def constructor_supports_parent_only?(decl)
6
+ return false unless decl['__effective_access'] == 'public'
7
+
8
+ parsed = parse_method_signature(decl)
9
+ return false unless parsed
10
+
11
+ params = parsed[:params]
12
+ return false if params.empty?
13
+
14
+ first_type = normalized_cpp_type_name(params.first[:type])
15
+ return false unless %w[QWidget* QObject*].include?(first_type)
16
+
17
+ params.drop(1).all? { |param| param[:has_default] }
18
+ end
19
+
20
+ def constructor_keysequence_parent_type(decl)
21
+ return nil unless decl['__effective_access'] == 'public'
22
+
23
+ parsed = parse_method_signature(decl)
24
+ return nil unless parsed
25
+
26
+ params = parsed[:params]
27
+ return nil if params.length < 2
28
+ return nil unless normalized_cpp_type_name(params.first[:type]) == 'QKeySequence'
29
+
30
+ parent_type = normalized_cpp_type_name(params[1][:type])
31
+ return nil unless %w[QWidget* QObject*].include?(parent_type)
32
+ return nil unless params.drop(2).all? { |param| param[:has_default] }
33
+
34
+ parent_type
35
+ end
36
+
37
+ def parent_constructor_first_type(decl)
38
+ return nil unless decl['__effective_access'] == 'public'
39
+
40
+ parsed = parse_method_signature(decl)
41
+ return nil unless parsed
42
+
43
+ params = parsed[:params]
44
+ return nil if params.empty?
45
+
46
+ first_type = normalized_cpp_type_name(params.first[:type])
47
+ return nil unless %w[QWidget* QObject*].include?(first_type)
48
+ return nil unless params.drop(1).all? { |param| param[:has_default] }
49
+
50
+ first_type
51
+ end
52
+
53
+ def constructor_supports_no_args?(decl)
54
+ return false unless decl['__effective_access'] == 'public'
55
+
56
+ parsed = parse_method_signature(decl)
57
+ return false unless parsed
58
+
59
+ parsed[:required_arg_count].zero?
60
+ end
61
+
62
+ def constructor_supports_string_path?(decl)
63
+ return false unless decl['__effective_access'] == 'public'
64
+
65
+ parsed = parse_method_signature(decl)
66
+ return false unless parsed
67
+ return false if parsed[:params].empty?
68
+ return false unless parsed[:required_arg_count] <= 1
69
+
70
+ constructor_string_like_type?(parsed[:params].first[:type])
71
+ end
72
+
73
+ def constructor_string_like_arg_cast(raw_type)
74
+ compact = raw_type.to_s.gsub(/\s+/, ' ').strip
75
+ normalized = normalized_cpp_type_name(raw_type)
76
+ return :qstring if normalized == 'QString'
77
+ return :qany_string_view if normalized == 'QAnyStringView'
78
+ return :cstr if compact.match?(/\A(?:const\s+)?char\s*\*\z/)
79
+
80
+ nil
81
+ end
82
+
83
+ def constructor_string_like_type?(raw_type)
84
+ !constructor_string_like_arg_cast(raw_type).nil?
85
+ end
86
+
87
+ def template_qt_classes(ast)
88
+ @template_qt_classes_cache ||= {}.compare_by_identity
89
+ return @template_qt_classes_cache[ast] if @template_qt_classes_cache.key?(ast)
90
+
91
+ @template_qt_classes_cache[ast] = build_template_qt_classes(ast)
92
+ end
93
+
94
+ def build_template_qt_classes(ast)
95
+ set = Set.new
96
+ timed('discover_template_qt_classes') do
97
+ walk_ast(ast) do |node|
98
+ next unless node['kind'] == 'ClassTemplateDecl'
99
+
100
+ name = node['name']
101
+ set << name if name&.start_with?('Q')
102
+ end
103
+ end
104
+
105
+ debug_log("template_qt_classes count=#{set.length}")
106
+ set
107
+ end
108
+
109
+ def q_class_names(ast)
110
+ ast_class_index(ast)[:methods_by_class].keys.select { |name| name.start_with?('Q') }.uniq
111
+ end
112
+
113
+ def class_matches_scope?(ast, qt_class, scope)
114
+ case scope
115
+ when 'widgets' then widget_target_qt_class?(ast, qt_class)
116
+ when 'qobject' then qobject_target_qt_class?(ast, qt_class)
117
+ when 'all' then all_scope_target_qt_class?(ast, qt_class)
118
+ else
119
+ raise "Unsupported QT_RUBY_SCOPE=#{scope.inspect}. Supported: #{SUPPORTED_SCOPES.join(', ')}"
120
+ end
121
+ end
122
+
123
+ def all_scope_target_qt_class?(ast, qt_class)
124
+ widget_target_qt_class?(ast, qt_class) || qobject_target_qt_class?(ast, qt_class)
125
+ end
126
+
127
+ def discover_target_for_scope(ast, scope, all_classes, template_classes)
128
+ all_classes.select do |qt_class|
129
+ next false if template_classes.include?(qt_class)
130
+ next false unless class_matches_scope?(ast, qt_class, scope)
131
+
132
+ constructor_usable_for_codegen?(ast, qt_class)
133
+ end.sort
134
+ end
135
+
136
+ def discover_related_value_classes(ast, seed_classes, all_classes, template_classes)
137
+ discovered = seed_classes.to_set
138
+ queue = seed_classes.dup
139
+
140
+ until queue.empty?
141
+ klass = queue.shift
142
+ related_qt_type_names(ast, klass).each do |candidate|
143
+ next if discovered.include?(candidate)
144
+ next unless related_value_class_candidate?(ast, candidate, all_classes, template_classes)
145
+
146
+ discovered << candidate
147
+ queue << candidate
148
+ end
149
+ end
150
+
151
+ discovered.to_a
152
+ end
153
+
154
+ def related_value_class_candidate?(ast, qt_class, all_classes, template_classes)
155
+ return false if SCALAR_BRIDGED_QT_TYPES.include?(qt_class)
156
+ return false unless all_classes.include?(qt_class)
157
+ return false if template_classes.include?(qt_class)
158
+ return false if qt_class.end_with?('Private')
159
+ return false if qt_class == 'QApplication'
160
+ return false if abstract_class?(ast, qt_class)
161
+ return false if class_inherits?(ast, qt_class, 'QObject')
162
+
163
+ constructor_usable_for_codegen?(ast, qt_class)
164
+ end
165
+
166
+ def related_qt_type_names(ast, qt_class)
167
+ index = ast_class_index(ast)
168
+ decls = index[:methods_by_class].fetch(qt_class, {}).values.flatten
169
+ decls.each_with_object(Set.new) do |decl, out|
170
+ next unless decl['__effective_access'] == 'public'
171
+
172
+ parsed = parse_method_signature(decl)
173
+ next unless parsed
174
+
175
+ append_related_qt_types_from_decl(out, parsed, decl['name'])
176
+ end
177
+ end
178
+
179
+ def append_related_qt_types_from_decl(out, parsed, method_name)
180
+ candidates = []
181
+ candidates << normalized_cpp_type_name(parsed[:return_type]).to_s if parsed[:return_type]
182
+ parsed[:params].each { |param| candidates << normalized_cpp_type_name(param[:type]).to_s }
183
+ candidates.each do |candidate|
184
+ next if candidate.empty? || !candidate.start_with?('Q')
185
+ next unless related_type_name_matches_method?(candidate, method_name)
186
+
187
+ out << candidate
188
+ end
189
+ end
190
+
191
+ def related_type_name_matches_method?(qt_type, method_name)
192
+ token = qt_type.delete_prefix('Q').downcase
193
+ return false if token.empty?
194
+
195
+ method_name.to_s.downcase.include?(token)
196
+ end
197
+
198
+ def discover_target_qt_classes(ast, scope)
199
+ all_classes = q_class_names(ast)
200
+ template_classes = template_qt_classes(ast)
201
+
202
+ base_targets = timed("discover_target_qt_classes/#{scope}/base") do
203
+ discover_target_for_scope(ast, scope, all_classes, template_classes)
204
+ end
205
+
206
+ targets = timed("discover_target_qt_classes/#{scope}/related") do
207
+ discover_related_value_classes(ast, base_targets, all_classes, template_classes).sort
208
+ end
209
+ debug_log(
210
+ "discover_target_qt_classes scope=#{scope} total_q=#{all_classes.length} " \
211
+ "base=#{base_targets.length} targets=#{targets.length}"
212
+ )
213
+ targets
214
+ end
215
+
216
+ def widget_target_qt_class?(ast, qt_class)
217
+ return false if qt_class.end_with?('Private')
218
+ return false if qt_class == 'QApplication'
219
+ return false if abstract_class?(ast, qt_class)
220
+
221
+ class_inherits?(ast, qt_class, 'QWidget') ||
222
+ class_inherits?(ast, qt_class, 'QLayout') ||
223
+ qt_class == 'QTableWidgetItem'
224
+ end
225
+
226
+ def qobject_target_qt_class?(ast, qt_class)
227
+ return false if qt_class.end_with?('Private')
228
+ return false if qt_class == 'QApplication'
229
+ return false if abstract_class?(ast, qt_class)
230
+ return false unless class_inherits?(ast, qt_class, 'QObject')
231
+
232
+ # qobject scope is additive stage after widgets: exclude widget/layout branch.
233
+ return false if class_inherits?(ast, qt_class, 'QWidget')
234
+ return false if class_inherits?(ast, qt_class, 'QLayout')
235
+
236
+ true
237
+ end
238
+
239
+ def constructor_usable_for_codegen?(ast, qt_class)
240
+ ctor_decls = collect_constructor_decls(ast, qt_class)
241
+ ctor_decls.any? do |decl|
242
+ constructor_keysequence_parent_type(decl) ||
243
+ constructor_supports_parent_only?(decl) ||
244
+ constructor_supports_no_args?(decl) ||
245
+ constructor_supports_string_path?(decl)
246
+ end
247
+ end
248
+
249
+ def build_base_spec_for_qt_class(ast, qt_class)
250
+ ctor_decls = collect_constructor_decls(ast, qt_class)
251
+ keysequence_parent_type = ctor_decls.filter_map { |decl| constructor_keysequence_parent_type(decl) }.first
252
+ parent_type = ctor_decls.filter_map { |decl| parent_constructor_first_type(decl) }.first
253
+ string_path_cast = ctor_decls.filter_map do |decl|
254
+ parsed = parse_method_signature(decl)
255
+ next nil unless parsed && parsed[:params].first
256
+
257
+ constructor_string_like_arg_cast(parsed[:params].first[:type])
258
+ end.first
259
+ widget_child = qt_class != 'QWidget' && class_inherits?(ast, qt_class, 'QWidget')
260
+ parent_ctor =
261
+ if keysequence_parent_type
262
+ { parent: true, parent_type: keysequence_parent_type, mode: :keysequence_parent, register_in_parent: widget_child }
263
+ elsif parent_type
264
+ parent_constructor_for_type(parent_type, widget_child)
265
+ elsif string_path_cast
266
+ string_path_constructor(string_path_cast)
267
+ else
268
+ { parent: false }
269
+ end
270
+ base_spec_hash(qt_class, parent_ctor)
271
+ end
272
+
273
+ def base_spec_hash(qt_class, parent_ctor)
274
+ auto_method_rules = {}
275
+ # Prefer QShortcut#setKeys(StandardKey) in generated API and provide
276
+ # QKeySequence path via setKey(QKeySequence) + Ruby-level compatibility shim.
277
+ auto_method_rules[:setKeys] = { param_types: ['QKeySequence::StandardKey'] } if qt_class == 'QShortcut'
278
+
279
+ {
280
+ qt_class: qt_class,
281
+ ruby_class: qt_class,
282
+ include: qt_class,
283
+ prefix: prefix_for_qt_class(qt_class),
284
+ constructor: parent_ctor,
285
+ methods: [],
286
+ auto_methods: :all,
287
+ auto_method_rules: auto_method_rules,
288
+ # Constructor availability is already filtered during discovery.
289
+ # Keep validation lightweight for auto-discovered classes to avoid false negatives
290
+ # on template/specialized constructor names in Clang AST.
291
+ validate: { constructors: [], methods: [] }
292
+ }
293
+ end
294
+
295
+ def parent_constructor_for_type(parent_type, widget_child)
296
+ { parent: true, parent_type: parent_type, register_in_parent: widget_child }
297
+ end
298
+
299
+ def string_path_constructor(arg_cast)
300
+ { parent: false, mode: :string_path, arg_cast: arg_cast }
301
+ end
302
+
303
+ def build_base_specs(ast)
304
+ specs = [build_qapplication_spec(ast)]
305
+ target_qt_classes = discover_target_qt_classes(ast, GENERATOR_SCOPE)
306
+ debug_log("target_classes scope=#{GENERATOR_SCOPE} count=#{target_qt_classes.length}")
307
+
308
+ target_qt_classes.each do |qt_class|
309
+ specs << build_base_spec_for_qt_class(ast, qt_class)
310
+ end
311
+
312
+ specs
313
+ end