qt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +27 -0
- data/README.md +303 -0
- data/Rakefile +94 -0
- data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
- data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
- data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
- data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
- data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
- data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
- data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
- data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
- data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
- data/ext/qt_ruby_bridge/extconf.rb +75 -0
- data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
- data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
- data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
- data/lib/qt/application_lifecycle.rb +44 -0
- data/lib/qt/bridge.rb +95 -0
- data/lib/qt/children_tracking.rb +15 -0
- data/lib/qt/constants.rb +10 -0
- data/lib/qt/date_time_codec.rb +104 -0
- data/lib/qt/errors.rb +6 -0
- data/lib/qt/event_runtime.rb +139 -0
- data/lib/qt/event_runtime_dispatch.rb +35 -0
- data/lib/qt/event_runtime_qobject_methods.rb +41 -0
- data/lib/qt/generated_constants_runtime.rb +33 -0
- data/lib/qt/inspectable.rb +29 -0
- data/lib/qt/key_sequence_codec.rb +22 -0
- data/lib/qt/native.rb +93 -0
- data/lib/qt/shortcut_compat.rb +30 -0
- data/lib/qt/string_codec.rb +44 -0
- data/lib/qt/variant_codec.rb +78 -0
- data/lib/qt/version.rb +5 -0
- data/lib/qt.rb +47 -0
- data/scripts/generate_bridge/ast_introspection.rb +267 -0
- data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
- data/scripts/generate_bridge/auto_methods.rb +438 -0
- data/scripts/generate_bridge/core_utils.rb +114 -0
- data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
- data/scripts/generate_bridge/ffi_api.rb +46 -0
- data/scripts/generate_bridge/free_function_specs.rb +289 -0
- data/scripts/generate_bridge/spec_discovery.rb +313 -0
- data/scripts/generate_bridge.rb +1113 -0
- metadata +99 -0
|
@@ -0,0 +1,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
|