qt 0.1.3 → 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: 2b772dc4333d9bfcf419388425980a833e99d5253bbab7d50a506dd886334153
4
- data.tar.gz: 05133ee6f7bf9ad3a4ba582eb0f7bd214a1886005e7caa32939d5aa88c077f47
3
+ metadata.gz: 58b685a5934b6ab65de8085da77093cf9de7b608a2f047638e6662ece29f4f8a
4
+ data.tar.gz: 3ce44932b8d818b1068908d73189704bdd4b1e2f54e6d2e462c83d217b81c3f3
5
5
  SHA512:
6
- metadata.gz: 97f0df292c1559e78a67b4075dbf8bfa35cb3804cb8720f2b0a5ea4a0f645b1b5f89108c076677dce1699a376225174df246d8316fe58466ffb53ab14cd18eb4
7
- data.tar.gz: 2817088f7103798af9a8b6b71046cf8cf0570f4842265c18be8a4cfb3025c2e921b70e0ab95c1d6953137d7024b041496f4aeb331e0cf05cf2c57b73423c87d8
6
+ metadata.gz: aac76dc37741b86a72be2b8b5f1d671b3f0e4fc59ad95e2958264c7a9929f3dad7d445e3bded8f46d67bc304296b0610d7f427ca19eebaec99fd41dff4d454cb
7
+ data.tar.gz: 50d80970f96868a576ac445b84bbf45ea002bf8c55038b52d3e0c1f2a522d89c6cf2e2120a895b43963cc912900ab63a3520be8d91278d3a8f5a6ec08e364569
data/LICENSE CHANGED
@@ -1,5 +1,3 @@
1
- SPDX-License-Identifier: BSD-2-Clause
2
-
3
1
  BSD 2-Clause License
4
2
 
5
3
  Copyright (c) 2026, Maksim Veynberg
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # qt
2
2
 
3
- ![Ruby](https://img.shields.io/badge/Ruby-3.1%2B-CC342D)
3
+ ![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D)
4
4
  ![Qt](https://img.shields.io/badge/Qt-6.4.2%2B-41CD52)
5
5
  ![Status](https://img.shields.io/badge/Status-Experimental-orange)
6
6
  ![Bridge](https://img.shields.io/badge/Architecture-Ruby%20%E2%86%94%20Qt%20Bridge-blue)
@@ -9,7 +9,7 @@ Ruby-first Qt 6.4.2+ bridge.
9
9
 
10
10
  Build real Qt Widgets apps in pure Ruby, mutate them live from IRB, and keep C/C++ surface minimal via generated bridge code from system Qt headers.
11
11
 
12
- ## Why It Hits Different
12
+ ## Highlights
13
13
 
14
14
  - Pure Ruby usage: no QML, no extra UI language.
15
15
  - Real Qt power: `QApplication`, `QWidget`, `QLabel`, `QPushButton`, `QVBoxLayout`.
@@ -17,46 +17,25 @@ Build real Qt Widgets apps in pure Ruby, mutate them live from IRB, and keep C/C
17
17
  - Live GUI hacking: update widgets while the window is open.
18
18
  - Generated bridge: API is derived from system Qt headers.
19
19
 
20
- ## 30-Second Wow
21
-
22
- ```bash
23
- ruby examples/development_ordered_demos/02_live_layout_console.rb
24
- ```
25
-
26
- Then in IRB:
27
-
28
- ```ruby
29
- add_label("Release pipeline")
30
- add_button("Run")
31
- remove_last
32
-
33
- gui { window.resize(1100, 700) }
34
- items.last&.q_inspect
35
- ```
36
-
37
- ## Before -> After
20
+ ## Install
38
21
 
39
- Before (typical static run):
22
+ ### Quick install (RubyGems)
40
23
 
41
- ```ruby
42
- app = QApplication.new(0, [])
43
- window = QWidget.new
44
- window.show
45
- app.exec
24
+ ```bash
25
+ gem install qt
46
26
  ```
47
27
 
48
- After (live dev loop):
28
+ ### Quick install (Fedora, binary RPM via COPR)
49
29
 
50
- ```ruby
51
- # app already running
52
- add_label("Dynamic block")
53
- add_button("Ship")
54
- gui { window.set_window_title("Changed live") }
30
+ ```bash
31
+ sudo dnf copr enable cyjimmy264/ruby-qt -y
32
+ sudo dnf install -y ruby-qt
55
33
  ```
56
34
 
57
- ## Install
35
+ This installs a prebuilt package. Nothing is compiled on the target machine.
36
+ Package name: `ruby-qt`.
58
37
 
59
- ### Requirements
38
+ ### Requirements (build from source)
60
39
 
61
40
  - Ruby 3.2+
62
41
  - Qt 6.4.2+ dev packages (`Qt6Core`, `Qt6Gui`, `Qt6Widgets` via `pkg-config`)
@@ -94,10 +73,21 @@ bundle exec rake install
94
73
  `rake install` installs into your current Ruby environment (including active `rbenv` version).
95
74
  `rake compile` builds the full bridge with `QT_RUBY_SCOPE=all` by default.
96
75
 
97
- ### Gem usage
76
+ ## Quick Start
98
77
 
99
78
  ```bash
100
- gem install qt
79
+ bundle exec ruby examples/development_ordered_demos/02_live_layout_console.rb
80
+ ```
81
+
82
+ Optional: run interactive commands in IRB while the app is open:
83
+
84
+ ```ruby
85
+ add_label("Release pipeline")
86
+ add_button("Run")
87
+ remove_last
88
+
89
+ gui { window.resize(1100, 700) }
90
+ items.last&.q_inspect
101
91
  ```
102
92
 
103
93
  ## Hello Qt in Ruby
@@ -184,10 +174,7 @@ Shape:
184
174
 
185
175
  ## Examples
186
176
 
187
- ```bash
188
- ruby examples/development_ordered_demos/01_dsl_hello.rb
189
- ruby examples/development_ordered_demos/02_live_layout_console.rb
190
- ```
177
+ See all demos in [`examples/development_ordered_demos`](examples/development_ordered_demos).
191
178
 
192
179
  QObject signal example:
193
180
 
@@ -198,6 +185,10 @@ timer.connect('timeout') { puts 'tick' }
198
185
  timer.start
199
186
  ```
200
187
 
188
+ ## Projects
189
+
190
+ - [`qtimetrap`](https://github.com/CyJimmy264/qtimetrap) - timetrap desktop UI built with this bridge.
191
+
201
192
  ## Architecture
202
193
 
203
194
  1. `scripts/generate_bridge.rb` reads Qt API from system headers.
@@ -253,8 +244,8 @@ Everything generated/build-related is under `build/` and should stay out of git.
253
244
  ## Development
254
245
 
255
246
  ```bash
256
- bundle exec rake test
257
247
  bundle exec rake compile
248
+ bundle exec rake test
258
249
  bundle exec rake rubocop
259
250
  ```
260
251
 
@@ -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)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Wrap native QObject-derived pointers into generated Ruby wrapper instances.
5
+ module ObjectWrapper
6
+ module_function
7
+
8
+ def wrap(pointer, expected_qt_class = nil)
9
+ return nil if null_pointer?(pointer)
10
+ return pointer if pointer.respond_to?(:handle)
11
+
12
+ klass = resolve_wrapper_class(pointer, expected_qt_class) || fallback_wrapper_class(expected_qt_class)
13
+ return pointer unless klass
14
+
15
+ instantiate_wrapper(klass, pointer)
16
+ end
17
+
18
+ def null_pointer?(pointer)
19
+ pointer.nil? || (pointer.respond_to?(:null?) && pointer.null?)
20
+ end
21
+
22
+ def resolve_wrapper_class(pointer, expected_qt_class)
23
+ candidate_wrapper_classes(expected_qt_class).find do |klass|
24
+ Native.qobject_inherits(pointer, klass::QT_CLASS)
25
+ end
26
+ end
27
+
28
+ def candidate_wrapper_classes(expected_qt_class)
29
+ @candidate_wrapper_classes ||= {}
30
+ @candidate_wrapper_classes[expected_qt_class] ||= begin
31
+ base = fallback_wrapper_class(expected_qt_class)
32
+ wrappers = qobject_wrapper_classes
33
+ wrappers = wrappers.select { |klass| klass <= base } if base
34
+ wrappers.sort_by { |klass| -inheritance_depth(klass) }
35
+ end
36
+ end
37
+
38
+ def qobject_wrapper_classes
39
+ Qt.constants(false).filter_map do |const_name|
40
+ klass = Qt.const_get(const_name, false)
41
+ next unless klass.is_a?(Class)
42
+ next unless klass.const_defined?(:QT_CLASS, false)
43
+ next unless klass <= Qt::QObject
44
+
45
+ klass
46
+ end
47
+ end
48
+
49
+ def fallback_wrapper_class(expected_qt_class)
50
+ return nil if expected_qt_class.nil? || !Qt.const_defined?(expected_qt_class, false)
51
+
52
+ klass = Qt.const_get(expected_qt_class, false)
53
+ return nil unless klass.is_a?(Class)
54
+ return nil unless klass.const_defined?(:QT_CLASS, false)
55
+ return nil unless klass <= Qt::QObject
56
+
57
+ klass
58
+ end
59
+
60
+ def instantiate_wrapper(klass, pointer)
61
+ wrapped = klass.allocate
62
+ wrapped.instance_variable_set(:@handle, pointer)
63
+ wrapped.init_children_tracking! if wrapped.respond_to?(:init_children_tracking!, true)
64
+ wrapped
65
+ end
66
+
67
+ def inheritance_depth(klass)
68
+ depth = 0
69
+ current = klass
70
+ while current.is_a?(Class)
71
+ depth += 1
72
+ current = current.superclass
73
+ end
74
+ depth
75
+ end
76
+ end
77
+ end
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.3'
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
@@ -23,12 +25,14 @@ require_relative 'qt/key_sequence_codec'
23
25
  require_relative 'qt/variant_codec'
24
26
  require_relative 'qt/inspectable'
25
27
  require_relative 'qt/children_tracking'
28
+ require_relative 'qt/object_wrapper'
26
29
  require_relative 'qt/application_lifecycle'
27
30
  require_relative 'qt/bridge'
28
31
  require_relative 'qt/native'
29
32
  require_relative 'qt/event_runtime_dispatch'
30
33
  require_relative 'qt/event_runtime_qobject_methods'
31
34
  require_relative 'qt/event_runtime'
35
+ require GENERATED_EVENT_PAYLOADS
32
36
  require GENERATED_WIDGETS
33
37
  require_relative 'qt/shortcut_compat'
34
38
  Qt::GeneratedConstantsRuntime.apply_generated_scoped_constants!(Qt)
@@ -67,13 +67,13 @@ def normalized_cpp_type_name(type_name)
67
67
  type
68
68
  end
69
69
 
70
- def map_cpp_return_type(type_name)
70
+ def map_cpp_return_type(type_name, ast: nil)
71
71
  raw = type_name.to_s.strip
72
72
  return nil if unsupported_cpp_type?(raw)
73
73
  return nil if raw.start_with?('const ') && raw.end_with?('*')
74
74
 
75
75
  type = raw.sub(/\Aconst\s+/, '').sub(/\s*&\z/, '').strip
76
- map_scalar_cpp_return_type(type) || map_pointer_cpp_return_type(type)
76
+ map_scalar_cpp_return_type(type) || map_pointer_cpp_return_type(type, ast: ast)
77
77
  end
78
78
 
79
79
  def map_scalar_cpp_return_type(type)
@@ -89,10 +89,16 @@ def map_scalar_cpp_return_type(type)
89
89
  nil
90
90
  end
91
91
 
92
- def map_pointer_cpp_return_type(type)
93
- return { ffi_return: :pointer } if type.end_with?('*')
92
+ def map_pointer_cpp_return_type(type, ast: nil)
93
+ return nil unless type.end_with?('*')
94
94
 
95
- nil
95
+ info = { ffi_return: :pointer }
96
+ base_type = type.sub(/\s*\*\z/, '').strip
97
+ if ast && class_inherits?(ast, base_type, 'QObject')
98
+ info[:pointer_class] = base_type
99
+ end
100
+
101
+ info
96
102
  end
97
103
 
98
104
  def parse_method_param_nodes(method_decl)
@@ -165,14 +171,15 @@ def build_auto_method_hash(entry, ret_info, args, required_arg_count)
165
171
  required_arg_count: required_arg_count
166
172
  }
167
173
  method[:return_cast] = ret_info[:return_cast] if ret_info[:return_cast]
174
+ method[:pointer_class] = ret_info[:pointer_class] if ret_info[:pointer_class]
168
175
  method
169
176
  end
170
177
 
171
- def build_auto_method_from_decl(method_decl, entry, qt_class:, int_cast_types:)
178
+ def build_auto_method_from_decl(method_decl, entry, qt_class:, int_cast_types:, ast:)
172
179
  parsed = parse_method_signature(method_decl)
173
180
  return nil unless parsed
174
181
 
175
- ret_info = map_cpp_return_type(parsed[:return_type])
182
+ ret_info = map_cpp_return_type(parsed[:return_type], ast: ast)
176
183
  return nil unless ret_info
177
184
 
178
185
  args, required_arg_count = build_auto_method_args(parsed, entry, qt_class, int_cast_types)
@@ -224,11 +231,11 @@ def invalid_method_name_scope?(class_name, visited)
224
231
  class_name.nil? || class_name.empty? || visited[class_name]
225
232
  end
226
233
 
227
- def build_auto_method_candidate(decl, entry, qt_class, int_cast_types)
234
+ def build_auto_method_candidate(ast, decl, entry, qt_class, int_cast_types)
228
235
  parsed = parse_method_signature(decl)
229
236
  return nil unless parsed
230
237
 
231
- method = build_auto_method_from_decl(decl, entry, qt_class: qt_class, int_cast_types: int_cast_types)
238
+ method = build_auto_method_from_decl(decl, entry, qt_class: qt_class, int_cast_types: int_cast_types, ast: ast)
232
239
  return nil unless method
233
240
 
234
241
  {
@@ -276,11 +283,11 @@ def resolve_auto_method_cache_key(qt_class, entry)
276
283
  ]
277
284
  end
278
285
 
279
- def build_auto_method_candidates(decls, entry, qt_class, int_cast_types)
286
+ def build_auto_method_candidates(ast, decls, entry, qt_class, int_cast_types)
280
287
  decls.filter_map do |decl|
281
288
  next unless auto_method_decl_candidate?(decl)
282
289
 
283
- build_auto_method_candidate(decl, entry, qt_class, int_cast_types)
290
+ build_auto_method_candidate(ast, decl, entry, qt_class, int_cast_types)
284
291
  end
285
292
  end
286
293
 
@@ -325,7 +332,7 @@ def resolve_auto_method_built_candidates(ast, qt_class, entry)
325
332
  return nil if decls.empty?
326
333
 
327
334
  int_cast_types = ast_int_cast_type_set(ast)
328
- built = build_auto_method_candidates(decls, entry, qt_class, int_cast_types)
335
+ built = build_auto_method_candidates(ast, decls, entry, qt_class, int_cast_types)
329
336
  return nil if built.empty?
330
337
 
331
338
  built = filter_auto_method_candidates(built, entry)
@@ -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
@@ -2,7 +2,7 @@
2
2
 
3
3
  RUNTIME_HEADER_PATH = File.expand_path('../../ext/qt_ruby_bridge/qt_ruby_runtime.hpp', __dir__)
4
4
 
5
- SCALAR_CLASS_METHOD_FFI_RETURNS = %i[void int bool string].freeze
5
+ SCALAR_CLASS_METHOD_FFI_RETURNS = %i[void int bool string pointer].freeze
6
6
  QAPPLICATION_STATIC_METHOD_EXCLUSIONS = %w[exec].freeze
7
7
  QAPPLICATION_STATIC_TEXT_SETTERS = %w[
8
8
  setApplicationName
@@ -239,7 +239,8 @@ def build_qapplication_static_free_function_spec(ast, qt_name, int_cast_types)
239
239
  ruby_name: qt_name,
240
240
  native: native_name,
241
241
  args: [],
242
- return_cast: candidate[:return_cast]
242
+ return_cast: candidate[:return_cast],
243
+ pointer_class: candidate[:pointer_class]
243
244
  }
244
245
  }
245
246
  end
@@ -253,7 +254,7 @@ def resolve_qapplication_static_noarg_candidate(ast, qt_name, int_cast_types)
253
254
  parsed = parse_method_signature(decl)
254
255
  next unless parsed && parsed[:required_arg_count].zero?
255
256
 
256
- ret_info = qapplication_static_return_info(parsed[:return_type], int_cast_types)
257
+ ret_info = qapplication_static_return_info(parsed[:return_type], int_cast_types, ast: ast)
257
258
  next unless ret_info
258
259
 
259
260
  { decl: decl, param_count: parsed[:params].length }.merge(ret_info)
@@ -263,10 +264,15 @@ def resolve_qapplication_static_noarg_candidate(ast, qt_name, int_cast_types)
263
264
  candidates.min_by { |item| item[:param_count] }
264
265
  end
265
266
 
266
- def qapplication_static_return_info(return_type, int_cast_types)
267
- mapped = map_cpp_return_type(return_type)
267
+ def qapplication_static_return_info(return_type, int_cast_types, ast:)
268
+ mapped = map_cpp_return_type(return_type, ast: ast)
268
269
  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
+ return {
271
+ ffi_return: mapped[:ffi_return],
272
+ return_cast: mapped[:return_cast],
273
+ pointer_class: mapped[:pointer_class],
274
+ enum_cast: false
275
+ }
270
276
  end
271
277
 
272
278
  normalized = normalized_cpp_type_name(return_type)
@@ -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)
@@ -151,13 +154,13 @@ def find_getter_decl(ast, qt_class, property)
151
154
  parsed = parse_method_signature(decl)
152
155
  next false unless parsed && parsed[:params].empty?
153
156
 
154
- map_cpp_return_type(parsed[:return_type])
157
+ map_cpp_return_type(parsed[:return_type], ast: ast)
155
158
  end
156
159
  end
157
160
 
158
- def build_property_getter_method(getter_decl, property)
161
+ def build_property_getter_method(ast, getter_decl, property)
159
162
  parsed_getter = parse_method_signature(getter_decl)
160
- ret_info = map_cpp_return_type(parsed_getter[:return_type])
163
+ ret_info = map_cpp_return_type(parsed_getter[:return_type], ast: ast)
161
164
  return nil unless ret_info
162
165
 
163
166
  getter = {
@@ -168,6 +171,7 @@ def build_property_getter_method(getter_decl, property)
168
171
  property: property
169
172
  }
170
173
  getter[:return_cast] = ret_info[:return_cast] if ret_info[:return_cast]
174
+ getter[:pointer_class] = ret_info[:pointer_class] if ret_info[:pointer_class]
171
175
  getter
172
176
  end
173
177
 
@@ -180,7 +184,7 @@ def enrich_spec_with_property_getter!(methods, ast, spec, method)
180
184
  getter_decl = find_getter_decl(ast, spec[:qt_class], property)
181
185
  return unless getter_decl
182
186
 
183
- getter = build_property_getter_method(getter_decl, property)
187
+ getter = build_property_getter_method(ast, getter_decl, property)
184
188
  methods << getter if getter
185
189
  end
186
190
 
@@ -675,6 +679,7 @@ def ruby_native_method_body(method, rewritten_native_call)
675
679
  return "Qt::DateTimeCodec.decode_qdatetime(#{rewritten_native_call})" if method[:return_cast] == :qdatetime_to_utf8
676
680
  return "Qt::DateTimeCodec.decode_qdate(#{rewritten_native_call})" if method[:return_cast] == :qdate_to_utf8
677
681
  return "Qt::DateTimeCodec.decode_qtime(#{rewritten_native_call})" if method[:return_cast] == :qtime_to_utf8
682
+ return "Qt::ObjectWrapper.wrap(#{rewritten_native_call}, '#{method[:pointer_class]}')" if method[:pointer_class]
678
683
 
679
684
  rewritten_native_call
680
685
  end
@@ -824,6 +829,7 @@ def qapplication_class_method_body(method, native_call)
824
829
  return " Qt::DateTimeCodec.decode_qdatetime(#{native_call})" if method[:return_cast] == :qdatetime_to_utf8
825
830
  return " Qt::DateTimeCodec.decode_qdate(#{native_call})" if method[:return_cast] == :qdate_to_utf8
826
831
  return " Qt::DateTimeCodec.decode_qtime(#{native_call})" if method[:return_cast] == :qtime_to_utf8
832
+ return " Qt::ObjectWrapper.wrap(#{native_call}, '#{method[:pointer_class]}')" if method[:pointer_class]
827
833
 
828
834
  " #{native_call}"
829
835
  end
@@ -1014,6 +1020,30 @@ def collect_qt_namespace_enum_constants(ast, warnings = [])
1014
1020
  constants
1015
1021
  end
1016
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
+
1017
1047
  def collect_qt_scoped_enum_constants(ast, warnings = [])
1018
1048
  constants_by_owner = Hash.new { |h, k| h[k] = {} }
1019
1049
 
@@ -1056,6 +1086,7 @@ def generate_ruby_constants(ast)
1056
1086
  warnings = []
1057
1087
  constants = collect_qt_namespace_enum_constants(ast, warnings)
1058
1088
  scoped_constants = collect_qt_scoped_enum_constants(ast, warnings)
1089
+ event_symbol_map = collect_qevent_symbol_map(ast, warnings)
1059
1090
  emit_generation_warnings(warnings)
1060
1091
  lines = ['# frozen_string_literal: true', '', 'module Qt']
1061
1092
 
@@ -1073,6 +1104,12 @@ def generate_ruby_constants(ast)
1073
1104
  lines << ' },'
1074
1105
  end
1075
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)'
1076
1113
 
1077
1114
  lines << 'end'
1078
1115
  "#{lines.join("\n")}\n"
@@ -1101,6 +1138,14 @@ timed('write_ruby_constants') do
1101
1138
  FileUtils.mkdir_p(File.dirname(RUBY_CONSTANTS_PATH))
1102
1139
  File.write(RUBY_CONSTANTS_PATH, generate_ruby_constants(ast))
1103
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
1104
1149
  timed('write_ruby_widgets') do
1105
1150
  FileUtils.mkdir_p(File.dirname(RUBY_WIDGETS_PATH))
1106
1151
  File.write(RUBY_WIDGETS_PATH, generate_ruby_widgets(effective_specs, super_qt_by_qt, wrapper_qt_classes))
@@ -1110,4 +1155,6 @@ debug_log("total=#{format('%.3fs', monotonic_now - total_start)}")
1110
1155
  puts "Generated #{CPP_PATH}"
1111
1156
  puts "Generated #{API_PATH}"
1112
1157
  puts "Generated #{RUBY_CONSTANTS_PATH}"
1158
+ puts "Generated #{RUBY_EVENT_PAYLOADS_PATH}"
1159
+ puts "Generated #{CPP_EVENT_PAYLOADS_PATH}"
1113
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.3
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Maksim Veynberg
@@ -61,6 +61,7 @@ files:
61
61
  - lib/qt/inspectable.rb
62
62
  - lib/qt/key_sequence_codec.rb
63
63
  - lib/qt/native.rb
64
+ - lib/qt/object_wrapper.rb
64
65
  - lib/qt/shortcut_compat.rb
65
66
  - lib/qt/string_codec.rb
66
67
  - lib/qt/variant_codec.rb
@@ -71,6 +72,7 @@ files:
71
72
  - scripts/generate_bridge/auto_methods.rb
72
73
  - scripts/generate_bridge/core_utils.rb
73
74
  - scripts/generate_bridge/cpp_method_return_emitter.rb
75
+ - scripts/generate_bridge/event_payloads.rb
74
76
  - scripts/generate_bridge/ffi_api.rb
75
77
  - scripts/generate_bridge/free_function_specs.rb
76
78
  - scripts/generate_bridge/spec_discovery.rb
@@ -79,7 +81,7 @@ licenses:
79
81
  - BSD-2-Clause
80
82
  metadata:
81
83
  source_code_uri: https://github.com/CyJimmy264/qt
82
- changelog_uri: https://github.com/CyJimmy264/qt/releases
84
+ changelog_uri: https://github.com/CyJimmy264/qt/blob/master/CHANGELOG.md
83
85
  bug_tracker_uri: https://github.com/CyJimmy264/qt/issues
84
86
  rubygems_mfa_required: 'true'
85
87
  rdoc_options: []