qt 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b476ba2b95707fc32d1995f8ae8dfc5c8068ab46020e00ea5442b1fb42dd3220
4
- data.tar.gz: 2ffb03be131be2c47d5d89f1094600d6e52fdb6891ffaabc40a3d8d7f6356c31
3
+ metadata.gz: 58b685a5934b6ab65de8085da77093cf9de7b608a2f047638e6662ece29f4f8a
4
+ data.tar.gz: 3ce44932b8d818b1068908d73189704bdd4b1e2f54e6d2e462c83d217b81c3f3
5
5
  SHA512:
6
- metadata.gz: 710c7df3d9c1950c14d35762a9223eb64cb4184573e0456fd864f5dd139f35224d03b144d0d121f465221e107e11a7c22e3053ee0b5508db72fe8e803ac3fc0c
7
- data.tar.gz: b8424801d5f61607f02ad9fb48cc5e6941f4dc811a38c4dbb5439d2e9f5dc561ed1ed814ec910d9ae81e49d84bf47fd40957ecfee07bb951900c80567ba49789
6
+ metadata.gz: aac76dc37741b86a72be2b8b5f1d671b3f0e4fc59ad95e2958264c7a9929f3dad7d445e3bded8f46d67bc304296b0610d7f427ca19eebaec99fd41dff4d454cb
7
+ data.tar.gz: 50d80970f96868a576ac445b84bbf45ea002bf8c55038b52d3e0c1f2a522d89c6cf2e2120a895b43963cc912900ab63a3520be8d91278d3a8f5a6ec08e364569
@@ -3,7 +3,7 @@
3
3
  #include <QApplication>
4
4
 
5
5
  namespace QtRubyRuntime {
6
- using EventCallback = void (*)(void*, int, int, int, int, int);
6
+ using EventCallback = int (*)(void*, int, const char*);
7
7
  using SignalCallback = void (*)(void*, int, const char*);
8
8
 
9
9
  // Creates/owns the singleton QApplication used by the Ruby runtime bridge.
@@ -1,15 +1,11 @@
1
1
  #include "qt_ruby_runtime.hpp"
2
2
 
3
3
  #include <QApplication>
4
+ #include <QByteArray>
4
5
  #include <QCoreApplication>
5
6
  #include <QEvent>
6
7
  #include <QEventLoop>
7
- #include <QKeyEvent>
8
- #include <QMouseEvent>
9
8
  #include <QObject>
10
- #include <QPoint>
11
- #include <QResizeEvent>
12
- #include <QByteArray>
13
9
  #include <QWidget>
14
10
  #include <cstdio>
15
11
  #include <cstdlib>
@@ -17,7 +13,13 @@
17
13
  #include <unordered_map>
18
14
  #include <unordered_set>
19
15
 
16
+ #include "../../build/generated/event_payloads.inc"
17
+
20
18
  namespace QtRubyRuntime {
19
+ constexpr int kEventCallbackIgnore = 0;
20
+ constexpr int kEventCallbackContinue = 1;
21
+ constexpr int kEventCallbackConsume = 2;
22
+
21
23
  EventCallback& event_callback_ref() {
22
24
  static EventCallback callback = nullptr;
23
25
  return callback;
@@ -117,6 +119,8 @@ const char* event_name(int et) {
117
119
  return "Leave";
118
120
  case QEvent::Resize:
119
121
  return "Resize";
122
+ case QEvent::Wheel:
123
+ return "Wheel";
120
124
  default:
121
125
  return "Other";
122
126
  }
@@ -161,6 +165,7 @@ bool supports_ancestor_dispatch(int event_type) {
161
165
  case QEvent::FocusOut:
162
166
  case QEvent::Enter:
163
167
  case QEvent::Leave:
168
+ case QEvent::Wheel:
164
169
  return true;
165
170
  default:
166
171
  return false;
@@ -219,46 +224,16 @@ class EventFilter : public QObject {
219
224
  return QObject::eventFilter(watched, event);
220
225
  }
221
226
 
222
- int a = 0;
223
- int b = 0;
224
- int c = 0;
225
- int d = 0;
226
-
227
- switch (event->type()) {
228
- case QEvent::MouseButtonPress:
229
- case QEvent::MouseButtonRelease:
230
- case QEvent::MouseMove: {
231
- auto* mouse_event = static_cast<QMouseEvent*>(event);
232
- const QPoint p = mouse_event->position().toPoint();
233
- a = p.x();
234
- b = p.y();
235
- c = static_cast<int>(mouse_event->button());
236
- d = static_cast<int>(mouse_event->buttons());
237
- break;
238
- }
239
- case QEvent::KeyPress:
240
- case QEvent::KeyRelease: {
241
- auto* key_event = static_cast<QKeyEvent*>(event);
242
- a = key_event->key();
243
- b = static_cast<int>(key_event->modifiers());
244
- c = key_event->isAutoRepeat() ? 1 : 0;
245
- d = key_event->count();
246
- break;
247
- }
248
- case QEvent::Resize: {
249
- auto* resize_event = static_cast<QResizeEvent*>(event);
250
- a = resize_event->size().width();
251
- b = resize_event->size().height();
252
- c = resize_event->oldSize().width();
253
- d = resize_event->oldSize().height();
254
- break;
255
- }
256
- default:
257
- break;
258
- }
259
-
227
+ const QByteArray payload_json = QtRubyGeneratedEventPayloads::serialize_event_payload(et, event);
260
228
  log_event_dispatch(watched, dispatch_target, et, event->isAccepted(), "dispatch");
261
- event_callback_ref()(dispatch_target, et, a, b, c, d);
229
+ const int callback_result = event_callback_ref()(dispatch_target, et, payload_json.constData());
230
+ if (callback_result == kEventCallbackIgnore) {
231
+ event->ignore();
232
+ }
233
+ if (callback_result == kEventCallbackConsume) {
234
+ event->accept();
235
+ return true;
236
+ }
262
237
  return QObject::eventFilter(watched, event);
263
238
  }
264
239
  };
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  module Qt
4
6
  # Event/signal subscription runtime backed by generated native callbacks.
5
7
  module EventRuntime
@@ -77,13 +79,7 @@ module Qt
77
79
 
78
80
  def event_type_for(event_name)
79
81
  key = event_name.to_sym
80
- map = {
81
- mouse_button_press: Qt::EventMouseButtonPress, mouse_button_release: Qt::EventMouseButtonRelease,
82
- mouse_move: Qt::EventMouseMove, key_press: Qt::EventKeyPress, key_release: Qt::EventKeyRelease,
83
- focus_in: Qt::EventFocusIn, focus_out: Qt::EventFocusOut, enter: Qt::EventEnter,
84
- leave: Qt::EventLeave, resize: Qt::EventResize
85
- }
86
- event_type = map[key]
82
+ event_type = Qt::GENERATED_EVENT_TYPES[key]
87
83
  raise ArgumentError, "unknown event: #{event_name.inspect}" unless event_type
88
84
 
89
85
  event_type
@@ -93,10 +89,9 @@ module Qt
93
89
  return if @event_callback
94
90
 
95
91
  @event_callback = FFI::Function.new(
96
- :void, %i[pointer int int int int int]
97
- ) do |object_handle, event_type, *args|
98
- a, b, c, d = args
99
- payload = { type: event_type, a: a, b: b, c: c, d: d }
92
+ :int, %i[pointer int string]
93
+ ) do |object_handle, event_type, payload_json|
94
+ payload = decode_event_payload(event_type, payload_json)
100
95
  EventRuntimeDispatch.dispatch_event(@event_handlers, object_handle, event_type, payload)
101
96
  end
102
97
 
@@ -120,6 +115,17 @@ module Qt
120
115
  widget.respond_to?(:handle) ? widget.handle : widget
121
116
  end
122
117
 
118
+ def decode_event_payload(event_type, payload_json)
119
+ payload =
120
+ if payload_json.nil? || payload_json.empty?
121
+ {}
122
+ else
123
+ JSON.parse(Qt::StringCodec.from_qt_text(payload_json), symbolize_names: true)
124
+ end
125
+ payload[:type] ||= event_type
126
+ payload
127
+ end
128
+
123
129
  def ensure_native_bridge_ready!
124
130
  Qt::Native.ensure_loaded!
125
131
  Qt::Native.define_bridge_wrappers!
@@ -4,18 +4,26 @@
4
4
  module Qt
5
5
  # Dispatch helpers for event/signal callbacks from the native bridge.
6
6
  module EventRuntimeDispatch
7
+ EVENT_RESULT_IGNORE = 0
8
+ EVENT_RESULT_CONTINUE = 1
9
+ EVENT_RESULT_CONSUME = 2
10
+
7
11
  module_function
8
12
 
9
13
  def dispatch_event(event_handlers, object_handle, event_type, payload)
10
- return unless object_handle && event_handlers
14
+ return EVENT_RESULT_CONTINUE unless object_handle && event_handlers
11
15
 
12
16
  per_widget = event_handlers[object_handle.address]
13
- return unless per_widget
17
+ return EVENT_RESULT_CONTINUE unless per_widget
14
18
 
15
19
  handlers = per_widget[event_type]
16
- return unless handlers && !handlers.empty?
20
+ return EVENT_RESULT_CONTINUE unless handlers && !handlers.empty?
21
+
22
+ results = handlers.map { |handler| handler.call(payload) }
23
+ return EVENT_RESULT_CONSUME if results.any? { |result| result == true || result == :consume }
24
+ return EVENT_RESULT_IGNORE if results.any? { |result| result == false || result == :ignore }
17
25
 
18
- handlers.each { |handler| handler.call(payload) }
26
+ EVENT_RESULT_CONTINUE
19
27
  end
20
28
 
21
29
  def dispatch_signal(signal_handlers, object_handle, signal_index, payload)
data/lib/qt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Qt
4
- VERSION = '0.1.4'
4
+ VERSION = '0.1.6'
5
5
  end
data/lib/qt.rb CHANGED
@@ -8,8 +8,10 @@ GENERATED_DIR = File.join(ROOT, 'build', 'generated')
8
8
  GENERATED_WIDGETS = File.join(GENERATED_DIR, 'widgets.rb')
9
9
  GENERATED_API = File.join(GENERATED_DIR, 'bridge_api.rb')
10
10
  GENERATED_CONSTANTS = File.join(GENERATED_DIR, 'constants.rb')
11
+ GENERATED_EVENT_PAYLOADS = File.join(GENERATED_DIR, 'event_payloads.rb')
11
12
 
12
- unless File.exist?(GENERATED_WIDGETS) && File.exist?(GENERATED_API) && File.exist?(GENERATED_CONSTANTS)
13
+ unless File.exist?(GENERATED_WIDGETS) && File.exist?(GENERATED_API) && File.exist?(GENERATED_CONSTANTS) &&
14
+ File.exist?(GENERATED_EVENT_PAYLOADS)
13
15
  ok = system(RbConfig.ruby, GENERATOR)
14
16
  raise 'Failed to generate Qt Ruby bindings. Run: bundle exec rake compile' unless ok
15
17
  end
@@ -30,6 +32,7 @@ require_relative 'qt/native'
30
32
  require_relative 'qt/event_runtime_dispatch'
31
33
  require_relative 'qt/event_runtime_qobject_methods'
32
34
  require_relative 'qt/event_runtime'
35
+ require GENERATED_EVENT_PAYLOADS
33
36
  require GENERATED_WIDGETS
34
37
  require_relative 'qt/shortcut_compat'
35
38
  Qt::GeneratedConstantsRuntime.apply_generated_scoped_constants!(Qt)
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ EVENT_PAYLOAD_COMPATIBILITY_ALIASES = {
4
+ 'QMouseEvent' => { a: :x, b: :y, c: :button, d: :buttons },
5
+ 'QKeyEvent' => { a: :key, b: :modifiers, c: :is_auto_repeat, d: :count },
6
+ 'QResizeEvent' => { a: :width, b: :height, c: :old_width, d: :old_height },
7
+ 'QWheelEvent' => { a: :pixel_delta_y, b: :angle_delta_y, c: :buttons, d: nil }
8
+ }.freeze
9
+
10
+ EVENT_PAYLOAD_EXCLUDED_METHODS = %w[
11
+ accepted clone ignore isAccepted registerEventType setAccepted spontaneous type
12
+ ].freeze
13
+
14
+ EVENT_PAYLOAD_SUPPORTED_COMPLEX_TYPES = %w[QPoint QPointF QSize].freeze
15
+
16
+ def event_payload_class_names(ast)
17
+ @event_payload_class_names ||= {}.compare_by_identity
18
+ return @event_payload_class_names[ast] if @event_payload_class_names.key?(ast)
19
+
20
+ index = ast_class_index(ast)
21
+ @event_payload_class_names[ast] = index[:methods_by_class].keys.select do |class_name|
22
+ class_name.start_with?('Q') && class_name.end_with?('Event') && class_inherits?(ast, class_name, 'QEvent')
23
+ end.sort
24
+ end
25
+
26
+ def event_payload_ctor_type_param?(ctor_decl)
27
+ Array(ctor_decl['inner']).any? do |node|
28
+ next false unless node['kind'] == 'ParmVarDecl'
29
+
30
+ raw_type = node.dig('type', 'desugaredQualType') || node.dig('type', 'qualType')
31
+ name = node['name']
32
+ %w[type t].include?(name) && %w[Type QEvent::Type].include?(raw_type)
33
+ end
34
+ end
35
+
36
+ def event_payload_type_family_class_names(ast)
37
+ @event_payload_type_family_class_names ||= {}.compare_by_identity
38
+ return @event_payload_type_family_class_names[ast] if @event_payload_type_family_class_names.key?(ast)
39
+
40
+ @event_payload_type_family_class_names[ast] = event_payload_class_names(ast).select do |class_name|
41
+ collect_constructor_decls(ast, class_name).any? { |decl| event_payload_ctor_type_param?(decl) }
42
+ end
43
+ end
44
+
45
+ def event_payload_class_stem(class_name)
46
+ class_name.sub(/\AQ/, '').sub(/Event\z/, '')
47
+ end
48
+
49
+ def event_payload_camel_tokens(name)
50
+ name.to_s.scan(/[A-Z]+(?=[A-Z][a-z]|\z)|[A-Z]?[a-z]+|\d+/)
51
+ end
52
+
53
+ def event_payload_token_sequence_present?(haystack, needle)
54
+ return false if haystack.empty? || needle.empty? || needle.length > haystack.length
55
+
56
+ 0.upto(haystack.length - needle.length).any? do |start_idx|
57
+ haystack[start_idx, needle.length] == needle
58
+ end
59
+ end
60
+
61
+ def event_payload_family_match?(event_name, class_name, mode)
62
+ event_tokens = event_payload_camel_tokens(event_name)
63
+ class_tokens = event_payload_camel_tokens(event_payload_class_stem(class_name))
64
+ return false if event_tokens.empty? || class_tokens.empty?
65
+
66
+ event_downcase = event_tokens.map(&:downcase)
67
+ class_downcase = class_tokens.map(&:downcase)
68
+ event_compact = event_downcase.join
69
+ class_compact = class_downcase.join
70
+
71
+ case mode
72
+ when :prefix
73
+ event_downcase.first(class_downcase.length) == class_downcase
74
+ when :suffix
75
+ event_downcase.last(class_downcase.length) == class_downcase
76
+ when :contiguous_tokens
77
+ event_payload_token_sequence_present?(event_downcase, class_downcase)
78
+ when :compact_substring
79
+ event_compact.include?(class_compact)
80
+ else
81
+ false
82
+ end
83
+ end
84
+
85
+ def event_payload_most_specific_class_names(ast, class_names)
86
+ class_names.reject do |class_name|
87
+ class_names.any? do |other|
88
+ next false if other == class_name
89
+
90
+ class_inherits?(ast, other, class_name)
91
+ end
92
+ end
93
+ end
94
+
95
+ def event_payload_longest_stem_class_names(class_names)
96
+ stems = class_names.to_h do |class_name|
97
+ [class_name, event_payload_camel_tokens(event_payload_class_stem(class_name)).length]
98
+ end
99
+ longest = stems.values.max
100
+ class_names.select { |class_name| stems[class_name] == longest }
101
+ end
102
+
103
+ def resolve_event_payload_family_class_name(ast, event_name, warnings)
104
+ %i[prefix suffix contiguous_tokens compact_substring].each do |mode|
105
+ matches = event_payload_type_family_class_names(ast).select do |class_name|
106
+ event_payload_family_match?(event_name, class_name, mode)
107
+ end
108
+ next if matches.empty?
109
+
110
+ matches = event_payload_most_specific_class_names(ast, matches)
111
+ matches = event_payload_longest_stem_class_names(matches)
112
+ return matches.first if matches.length == 1
113
+
114
+ warnings << "Qt::EventPayload: ambiguous #{mode} family match for #{event_name}: #{matches.sort.join(', ')}"
115
+ return nil
116
+ end
117
+
118
+ nil
119
+ end
120
+
121
+ def resolve_event_payload_class_name(ast, event_name, warnings = [])
122
+ exact_class_name = "Q#{event_name}Event"
123
+ return exact_class_name if event_payload_class_names(ast).include?(exact_class_name)
124
+
125
+ resolve_event_payload_family_class_name(ast, event_name, warnings)
126
+ end
127
+
128
+ def supported_event_payload_return?(return_type, int_cast_types)
129
+ return true if event_payload_scalar_return?(return_type, int_cast_types)
130
+ return true if EVENT_PAYLOAD_SUPPORTED_COMPLEX_TYPES.include?(normalized_cpp_type_name(return_type))
131
+
132
+ false
133
+ end
134
+
135
+ def event_payload_scalar_return?(return_type, int_cast_types)
136
+ raw = return_type.to_s.strip
137
+ return true if %w[bool int QString double float].include?(normalized_cpp_type_name(raw))
138
+
139
+ compact = raw.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
140
+ compact.include?('::') && int_cast_types.include?(compact)
141
+ end
142
+
143
+ def event_payload_method_decl(ast, class_name, method_name, int_cast_types)
144
+ collect_method_decls_with_bases(ast, class_name, method_name).find do |decl|
145
+ next false unless decl['__effective_access'] == 'public'
146
+ next false if deprecated_method_decl?(decl)
147
+
148
+ parsed = parse_method_signature(decl)
149
+ next false unless parsed && parsed[:params].empty?
150
+ next false if EVENT_PAYLOAD_EXCLUDED_METHODS.include?(method_name)
151
+
152
+ supported_event_payload_return?(parsed[:return_type], int_cast_types)
153
+ end
154
+ end
155
+
156
+ def event_payload_method_candidates(ast, class_name, int_cast_types)
157
+ collect_method_names_with_bases(ast, class_name).sort.filter_map do |method_name|
158
+ decl = event_payload_method_decl(ast, class_name, method_name, int_cast_types)
159
+ next unless decl
160
+
161
+ parsed = parse_method_signature(decl)
162
+ { method_name: method_name, return_type: parsed[:return_type] }
163
+ end
164
+ end
165
+
166
+ def event_payload_base_name(method_name, return_type)
167
+ normalized = normalized_cpp_type_name(return_type)
168
+ snake = to_snake(method_name)
169
+ return '' if %w[QPoint QPointF].include?(normalized) && %w[position pos local_pos].include?(snake)
170
+ return 'global_' if %w[QPoint QPointF].include?(normalized) && %w[global_position global_pos].include?(snake)
171
+ return '' if normalized == 'QSize' && snake == 'size'
172
+ return 'old_' if normalized == 'QSize' && snake == 'old_size'
173
+
174
+ "#{snake}_"
175
+ end
176
+
177
+ def event_payload_fields_for_method(method_name, return_type, int_cast_types)
178
+ normalized = normalized_cpp_type_name(return_type)
179
+ base_name = event_payload_base_name(method_name, return_type)
180
+ getter = "typed_event->#{method_name}()"
181
+
182
+ case normalized
183
+ when 'bool'
184
+ [{ name: to_snake(method_name), cpp_expr: getter, json_type: :bool }]
185
+ when 'int'
186
+ [{ name: to_snake(method_name), cpp_expr: getter, json_type: :int }]
187
+ when 'QString'
188
+ [{ name: to_snake(method_name), cpp_expr: getter, json_type: :string }]
189
+ when 'double', 'float'
190
+ [{ name: to_snake(method_name), cpp_expr: getter, json_type: :float }]
191
+ when 'QPoint'
192
+ [
193
+ { name: "#{base_name}x", cpp_expr: "#{getter}.x()", json_type: :int },
194
+ { name: "#{base_name}y", cpp_expr: "#{getter}.y()", json_type: :int }
195
+ ]
196
+ when 'QPointF'
197
+ [
198
+ { name: "#{base_name}x", cpp_expr: "#{getter}.x()", json_type: :float },
199
+ { name: "#{base_name}y", cpp_expr: "#{getter}.y()", json_type: :float }
200
+ ]
201
+ when 'QSize'
202
+ [
203
+ { name: "#{base_name}width", cpp_expr: "#{getter}.width()", json_type: :int },
204
+ { name: "#{base_name}height", cpp_expr: "#{getter}.height()", json_type: :int }
205
+ ]
206
+ else
207
+ compact = return_type.to_s.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
208
+ return [] unless compact.include?('::') && int_cast_types.include?(compact)
209
+
210
+ [{ name: to_snake(method_name), cpp_expr: "static_cast<int>(#{getter})", json_type: :int }]
211
+ end
212
+ end
213
+
214
+ def event_payload_compatibility_fields(class_name, field_names)
215
+ aliases = EVENT_PAYLOAD_COMPATIBILITY_ALIASES[class_name] || {}
216
+ aliases.each_with_object([]) do |(alias_name, target_name), out|
217
+ if target_name.nil?
218
+ out << { name: alias_name.to_s, source: nil, json_type: :int }
219
+ next
220
+ end
221
+
222
+ next unless field_names.include?(target_name.to_s)
223
+
224
+ out << { name: alias_name.to_s, source: target_name.to_s, json_type: :alias }
225
+ end
226
+ end
227
+
228
+ def collect_event_payload_schema_for(ast, event_name, event_value, int_cast_types, warnings)
229
+ class_name = resolve_event_payload_class_name(ast, event_name, warnings)
230
+ return nil if class_name.nil?
231
+ return nil unless ast_class_index(ast)[:methods_by_class].key?(class_name)
232
+
233
+ field_specs = event_payload_method_candidates(ast, class_name, int_cast_types).flat_map do |entry|
234
+ event_payload_fields_for_method(entry[:method_name], entry[:return_type], int_cast_types)
235
+ end
236
+ field_specs.uniq! { |field| field[:name] }
237
+
238
+ compatibility_fields = event_payload_compatibility_fields(class_name, field_specs.map { |field| field[:name] })
239
+ schema = {
240
+ symbol_name: qevent_symbol_name(event_name),
241
+ event_name: event_name,
242
+ event_value: event_value,
243
+ constant_name: "Event#{event_name}",
244
+ class_name: class_name,
245
+ fields: field_specs,
246
+ compatibility_fields: compatibility_fields
247
+ }
248
+
249
+ if field_specs.empty? && compatibility_fields.empty?
250
+ warnings << "Qt::EventPayload: #{event_name} resolved to #{class_name} but no payload fields were derived"
251
+ end
252
+
253
+ schema
254
+ end
255
+
256
+ def collect_event_payload_schemas(ast, warnings = [])
257
+ int_cast_types = ast_int_cast_type_set(ast)
258
+ collect_enum_constants_for_scope(ast, ['QEvent'], warnings).sort.filter_map do |event_name, event_value|
259
+ collect_event_payload_schema_for(ast, event_name, event_value, int_cast_types, warnings)
260
+ end
261
+ end
262
+
263
+ def generate_ruby_event_payloads(ast)
264
+ warnings = []
265
+ schemas = collect_event_payload_schemas(ast, warnings)
266
+ emit_generation_warnings(warnings)
267
+
268
+ lines = ['# frozen_string_literal: true', '', 'module Qt']
269
+ lines << ' GENERATED_EVENT_PAYLOAD_SCHEMAS = {'
270
+ schemas.each do |schema|
271
+ lines << " #{schema[:symbol_name].inspect} => {"
272
+ lines << " event_type: #{schema[:constant_name]},"
273
+ lines << " event_class: '#{schema[:class_name]}',"
274
+ lines << ' fields: ['
275
+ schema[:fields].each do |field|
276
+ lines << " { name: #{field[:name].inspect}, type: #{field[:json_type].inspect} },"
277
+ end
278
+ schema[:compatibility_fields].each do |field|
279
+ lines << " { name: #{field[:name].inspect}, type: #{field[:json_type].inspect} },"
280
+ end
281
+ lines << ' ]'
282
+ lines << ' },'
283
+ end
284
+ lines << ' }.freeze unless const_defined?(:GENERATED_EVENT_PAYLOAD_SCHEMAS, false)'
285
+ lines << 'end'
286
+ "#{lines.join("\n")}\n"
287
+ end
288
+
289
+ def cpp_json_insert_lines(field, source_expr = nil)
290
+ expr = source_expr || field[:cpp_expr]
291
+ case field[:json_type]
292
+ when :bool, :int, :float
293
+ ["payload.insert(\"#{field[:name]}\", #{expr});"]
294
+ when :string
295
+ ["payload.insert(\"#{field[:name]}\", #{expr});"]
296
+ else
297
+ []
298
+ end
299
+ end
300
+
301
+ def generate_cpp_event_payload_extractor(ast)
302
+ warnings = []
303
+ schemas = collect_event_payload_schemas(ast, warnings)
304
+ emit_generation_warnings(warnings)
305
+
306
+ includes = schemas.map { |schema| "#include <#{schema[:class_name]}>" }.uniq.sort
307
+ lines = []
308
+ lines.concat(includes)
309
+ lines << '#include <QEvent>'
310
+ lines << '#include <QJsonDocument>'
311
+ lines << '#include <QJsonObject>'
312
+ lines << ''
313
+ lines << 'namespace QtRubyGeneratedEventPayloads {'
314
+ lines << 'inline QByteArray serialize_event_payload(int event_type, QEvent* event) {'
315
+ lines << ' QJsonObject payload;'
316
+ lines << ' payload.insert("type", event_type);'
317
+ lines << ' switch (static_cast<QEvent::Type>(event_type)) {'
318
+ schemas.each do |schema|
319
+ lines << " case QEvent::#{schema[:event_name]}: {"
320
+ lines << " auto* typed_event = static_cast<#{schema[:class_name]}*>(event);"
321
+ schema[:fields].each do |field|
322
+ cpp_json_insert_lines(field).each { |line| lines << " #{line}" }
323
+ end
324
+ schema[:compatibility_fields].each do |field|
325
+ if field[:source]
326
+ lines << " payload.insert(\"#{field[:name]}\", payload.value(\"#{field[:source]}\"));"
327
+ else
328
+ lines << " payload.insert(\"#{field[:name]}\", 0);"
329
+ end
330
+ end
331
+ lines << ' break;'
332
+ lines << ' }'
333
+ end
334
+ lines << ' default:'
335
+ lines << ' break;'
336
+ lines << ' }'
337
+ lines << ' return QJsonDocument(payload).toJson(QJsonDocument::Compact);'
338
+ lines << '}'
339
+ lines << '} // namespace QtRubyGeneratedEventPayloads'
340
+ "#{lines.join("\n")}\n"
341
+ end
@@ -13,6 +13,8 @@ CPP_PATH = File.join(GENERATED_DIR, 'qt_ruby_bridge.cpp')
13
13
  API_PATH = File.join(GENERATED_DIR, 'bridge_api.rb')
14
14
  RUBY_WIDGETS_PATH = File.join(GENERATED_DIR, 'widgets.rb')
15
15
  RUBY_CONSTANTS_PATH = File.join(GENERATED_DIR, 'constants.rb')
16
+ RUBY_EVENT_PAYLOADS_PATH = File.join(GENERATED_DIR, 'event_payloads.rb')
17
+ CPP_EVENT_PAYLOADS_PATH = File.join(GENERATED_DIR, 'event_payloads.inc')
16
18
 
17
19
  # Universal generation policy: class set is discovered from AST per scope.
18
20
  GENERATOR_SCOPE = (ENV['QT_RUBY_SCOPE'] || 'all').freeze
@@ -50,6 +52,7 @@ require_relative 'generate_bridge/auto_method_spec_resolver'
50
52
  require_relative 'generate_bridge/cpp_method_return_emitter'
51
53
  require_relative 'generate_bridge/ast_introspection'
52
54
  require_relative 'generate_bridge/auto_methods'
55
+ require_relative 'generate_bridge/event_payloads'
53
56
  require_relative 'generate_bridge/spec_discovery'
54
57
 
55
58
  def next_trace_base(fetch_bases, cur, visited)
@@ -1017,6 +1020,30 @@ def collect_qt_namespace_enum_constants(ast, warnings = [])
1017
1020
  constants
1018
1021
  end
1019
1022
 
1023
+ def qevent_symbol_name(name)
1024
+ to_snake(name).to_sym
1025
+ end
1026
+
1027
+ def collect_qevent_symbol_map(ast, warnings = [])
1028
+ symbol_map = {}
1029
+
1030
+ collect_enum_constants_for_scope(ast, ['QEvent'], warnings).each do |name, value|
1031
+ symbol_name = qevent_symbol_name(name)
1032
+ existing = symbol_map[symbol_name]
1033
+ if existing.nil?
1034
+ symbol_map[symbol_name] = { constant_name: "Event#{name}", value: value }
1035
+ next
1036
+ end
1037
+
1038
+ next if existing[:value] == value && existing[:constant_name] == "Event#{name}"
1039
+
1040
+ warnings << "Qt::QEventSymbolMap: #{symbol_name}=Event#{name}(#{value}) conflicts with existing " \
1041
+ "#{existing[:constant_name]}(#{existing[:value]}); keeping existing #{existing[:constant_name]}"
1042
+ end
1043
+
1044
+ symbol_map
1045
+ end
1046
+
1020
1047
  def collect_qt_scoped_enum_constants(ast, warnings = [])
1021
1048
  constants_by_owner = Hash.new { |h, k| h[k] = {} }
1022
1049
 
@@ -1059,6 +1086,7 @@ def generate_ruby_constants(ast)
1059
1086
  warnings = []
1060
1087
  constants = collect_qt_namespace_enum_constants(ast, warnings)
1061
1088
  scoped_constants = collect_qt_scoped_enum_constants(ast, warnings)
1089
+ event_symbol_map = collect_qevent_symbol_map(ast, warnings)
1062
1090
  emit_generation_warnings(warnings)
1063
1091
  lines = ['# frozen_string_literal: true', '', 'module Qt']
1064
1092
 
@@ -1076,6 +1104,12 @@ def generate_ruby_constants(ast)
1076
1104
  lines << ' },'
1077
1105
  end
1078
1106
  lines << ' }.freeze unless const_defined?(:GENERATED_SCOPED_CONSTANTS, false)'
1107
+ lines << ''
1108
+ lines << ' GENERATED_EVENT_TYPES = {'
1109
+ event_symbol_map.sort.each do |symbol_name, entry|
1110
+ lines << " #{symbol_name.inspect} => #{entry[:constant_name]},"
1111
+ end
1112
+ lines << ' }.freeze unless const_defined?(:GENERATED_EVENT_TYPES, false)'
1079
1113
 
1080
1114
  lines << 'end'
1081
1115
  "#{lines.join("\n")}\n"
@@ -1104,6 +1138,14 @@ timed('write_ruby_constants') do
1104
1138
  FileUtils.mkdir_p(File.dirname(RUBY_CONSTANTS_PATH))
1105
1139
  File.write(RUBY_CONSTANTS_PATH, generate_ruby_constants(ast))
1106
1140
  end
1141
+ timed('write_ruby_event_payloads') do
1142
+ FileUtils.mkdir_p(File.dirname(RUBY_EVENT_PAYLOADS_PATH))
1143
+ File.write(RUBY_EVENT_PAYLOADS_PATH, generate_ruby_event_payloads(ast))
1144
+ end
1145
+ timed('write_cpp_event_payloads') do
1146
+ FileUtils.mkdir_p(File.dirname(CPP_EVENT_PAYLOADS_PATH))
1147
+ File.write(CPP_EVENT_PAYLOADS_PATH, generate_cpp_event_payload_extractor(ast))
1148
+ end
1107
1149
  timed('write_ruby_widgets') do
1108
1150
  FileUtils.mkdir_p(File.dirname(RUBY_WIDGETS_PATH))
1109
1151
  File.write(RUBY_WIDGETS_PATH, generate_ruby_widgets(effective_specs, super_qt_by_qt, wrapper_qt_classes))
@@ -1113,4 +1155,6 @@ debug_log("total=#{format('%.3fs', monotonic_now - total_start)}")
1113
1155
  puts "Generated #{CPP_PATH}"
1114
1156
  puts "Generated #{API_PATH}"
1115
1157
  puts "Generated #{RUBY_CONSTANTS_PATH}"
1158
+ puts "Generated #{RUBY_EVENT_PAYLOADS_PATH}"
1159
+ puts "Generated #{CPP_EVENT_PAYLOADS_PATH}"
1116
1160
  puts "Generated #{RUBY_WIDGETS_PATH}"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: qt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maksim Veynberg
@@ -72,6 +72,7 @@ files:
72
72
  - scripts/generate_bridge/auto_methods.rb
73
73
  - scripts/generate_bridge/core_utils.rb
74
74
  - scripts/generate_bridge/cpp_method_return_emitter.rb
75
+ - scripts/generate_bridge/event_payloads.rb
75
76
  - scripts/generate_bridge/ffi_api.rb
76
77
  - scripts/generate_bridge/free_function_specs.rb
77
78
  - scripts/generate_bridge/spec_discovery.rb
@@ -80,7 +81,7 @@ licenses:
80
81
  - BSD-2-Clause
81
82
  metadata:
82
83
  source_code_uri: https://github.com/CyJimmy264/qt
83
- changelog_uri: https://github.com/CyJimmy264/qt/releases
84
+ changelog_uri: https://github.com/CyJimmy264/qt/blob/master/CHANGELOG.md
84
85
  bug_tracker_uri: https://github.com/CyJimmy264/qt/issues
85
86
  rubygems_mfa_required: 'true'
86
87
  rdoc_options: []