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 +4 -4
- data/LICENSE +0 -2
- data/README.md +32 -41
- data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +1 -1
- data/ext/qt_ruby_bridge/runtime_events.cpp +19 -44
- data/lib/qt/event_runtime.rb +17 -11
- data/lib/qt/event_runtime_dispatch.rb +12 -4
- data/lib/qt/object_wrapper.rb +77 -0
- data/lib/qt/version.rb +1 -1
- data/lib/qt.rb +5 -1
- data/scripts/generate_bridge/auto_methods.rb +19 -12
- data/scripts/generate_bridge/event_payloads.rb +341 -0
- data/scripts/generate_bridge/free_function_specs.rb +12 -6
- data/scripts/generate_bridge.rb +51 -4
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 58b685a5934b6ab65de8085da77093cf9de7b608a2f047638e6662ece29f4f8a
|
|
4
|
+
data.tar.gz: 3ce44932b8d818b1068908d73189704bdd4b1e2f54e6d2e462c83d217b81c3f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aac76dc37741b86a72be2b8b5f1d671b3f0e4fc59ad95e2958264c7a9929f3dad7d445e3bded8f46d67bc304296b0610d7f427ca19eebaec99fd41dff4d454cb
|
|
7
|
+
data.tar.gz: 50d80970f96868a576ac445b84bbf45ea002bf8c55038b52d3e0c1f2a522d89c6cf2e2120a895b43963cc912900ab63a3520be8d91278d3a8f5a6ec08e364569
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# qt
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|

|
|
5
5
|

|
|
6
6
|

|
|
@@ -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
|
-
##
|
|
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
|
-
##
|
|
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
|
-
|
|
22
|
+
### Quick install (RubyGems)
|
|
40
23
|
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
window = QWidget.new
|
|
44
|
-
window.show
|
|
45
|
-
app.exec
|
|
24
|
+
```bash
|
|
25
|
+
gem install qt
|
|
46
26
|
```
|
|
47
27
|
|
|
48
|
-
|
|
28
|
+
### Quick install (Fedora, binary RPM via COPR)
|
|
49
29
|
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
## Quick Start
|
|
98
77
|
|
|
99
78
|
```bash
|
|
100
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
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
|
};
|
data/lib/qt/event_runtime.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
:
|
|
97
|
-
) do |object_handle, event_type,
|
|
98
|
-
|
|
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
|
-
|
|
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
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
|
|
92
|
+
def map_pointer_cpp_return_type(type, ast: nil)
|
|
93
|
+
return nil unless type.end_with?('*')
|
|
94
94
|
|
|
95
|
-
|
|
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 {
|
|
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)
|
data/scripts/generate_bridge.rb
CHANGED
|
@@ -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.
|
|
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/
|
|
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: []
|