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,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}"
|