qt 0.1.2 → 0.1.4

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: 2a35b004deae222f888e382dd0e2a6071d6061e48983f804667d79e2ac8fb0f0
4
- data.tar.gz: 641ce5442ba340e56797d998e6175f5f11bc406525a5e5fb7e9077ddd40db352
3
+ metadata.gz: b476ba2b95707fc32d1995f8ae8dfc5c8068ab46020e00ea5442b1fb42dd3220
4
+ data.tar.gz: 2ffb03be131be2c47d5d89f1094600d6e52fdb6891ffaabc40a3d8d7f6356c31
5
5
  SHA512:
6
- metadata.gz: c87b792b00123721d6119fe50305e6c8a98c2f3675df1704358be7e418ec5a96634147cbd6d83c96b5af30891acfea571431cb0a037c66092b6cf6299d749cfe
7
- data.tar.gz: 2fbd78f39288ba102283c73b5c218805ec68ac96ff8229555a477468db253434a1cfe7527a24ac997e0eda6bf84def1e5c7f7f3b6e79fd93b7cfbde3426aa166
6
+ metadata.gz: 710c7df3d9c1950c14d35762a9223eb64cb4184573e0456fd864f5dd139f35224d03b144d0d121f465221e107e11a7c22e3053ee0b5508db72fe8e803ac3fc0c
7
+ data.tar.gz: b8424801d5f61607f02ad9fb48cc5e6941f4dc811a38c4dbb5439d2e9f5dc561ed1ed814ec910d9ae81e49d84bf47fd40957ecfee07bb951900c80567ba49789
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,15 +1,15 @@
1
1
  # qt
2
2
 
3
- ![Ruby](https://img.shields.io/badge/Ruby-3.1%2B-CC342D)
4
- ![Qt](https://img.shields.io/badge/Qt-6.10%2B-41CD52)
3
+ ![Ruby](https://img.shields.io/badge/Ruby-3.2%2B-CC342D)
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)
7
7
 
8
- Ruby-first Qt 6.10+ bridge.
8
+ 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,49 +17,28 @@ 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
- - Qt 6.10+ dev packages (`Qt6Core`, `Qt6Gui`, `Qt6Widgets` via `pkg-config`)
41
+ - Qt 6.4.2+ dev packages (`Qt6Core`, `Qt6Gui`, `Qt6Widgets` via `pkg-config`)
63
42
  - C++17 compiler
64
43
 
65
44
  ### System Requirements
@@ -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
 
@@ -5,7 +5,7 @@ require 'fileutils'
5
5
 
6
6
  PKG_CONFIG = RbConfig::CONFIG['PKG_CONFIG'] || 'pkg-config'
7
7
  QT_PACKAGES = %w[Qt6Core Qt6Gui Qt6Widgets].freeze
8
- MINIMUM_QT_VERSION = Gem::Version.new('6.10.0')
8
+ MINIMUM_QT_VERSION = Gem::Version.new('6.4.2')
9
9
 
10
10
  def pkg_config(*)
11
11
  system(PKG_CONFIG, *, out: File::NULL, err: File::NULL)
@@ -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.2'
4
+ VERSION = '0.1.4'
5
5
  end
data/lib/qt.rb CHANGED
@@ -23,6 +23,7 @@ require_relative 'qt/key_sequence_codec'
23
23
  require_relative 'qt/variant_codec'
24
24
  require_relative 'qt/inspectable'
25
25
  require_relative 'qt/children_tracking'
26
+ require_relative 'qt/object_wrapper'
26
27
  require_relative 'qt/application_lifecycle'
27
28
  require_relative 'qt/bridge'
28
29
  require_relative 'qt/native'
@@ -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)
@@ -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)
@@ -151,13 +151,13 @@ def find_getter_decl(ast, qt_class, property)
151
151
  parsed = parse_method_signature(decl)
152
152
  next false unless parsed && parsed[:params].empty?
153
153
 
154
- map_cpp_return_type(parsed[:return_type])
154
+ map_cpp_return_type(parsed[:return_type], ast: ast)
155
155
  end
156
156
  end
157
157
 
158
- def build_property_getter_method(getter_decl, property)
158
+ def build_property_getter_method(ast, getter_decl, property)
159
159
  parsed_getter = parse_method_signature(getter_decl)
160
- ret_info = map_cpp_return_type(parsed_getter[:return_type])
160
+ ret_info = map_cpp_return_type(parsed_getter[:return_type], ast: ast)
161
161
  return nil unless ret_info
162
162
 
163
163
  getter = {
@@ -168,6 +168,7 @@ def build_property_getter_method(getter_decl, property)
168
168
  property: property
169
169
  }
170
170
  getter[:return_cast] = ret_info[:return_cast] if ret_info[:return_cast]
171
+ getter[:pointer_class] = ret_info[:pointer_class] if ret_info[:pointer_class]
171
172
  getter
172
173
  end
173
174
 
@@ -180,7 +181,7 @@ def enrich_spec_with_property_getter!(methods, ast, spec, method)
180
181
  getter_decl = find_getter_decl(ast, spec[:qt_class], property)
181
182
  return unless getter_decl
182
183
 
183
- getter = build_property_getter_method(getter_decl, property)
184
+ getter = build_property_getter_method(ast, getter_decl, property)
184
185
  methods << getter if getter
185
186
  end
186
187
 
@@ -675,6 +676,7 @@ def ruby_native_method_body(method, rewritten_native_call)
675
676
  return "Qt::DateTimeCodec.decode_qdatetime(#{rewritten_native_call})" if method[:return_cast] == :qdatetime_to_utf8
676
677
  return "Qt::DateTimeCodec.decode_qdate(#{rewritten_native_call})" if method[:return_cast] == :qdate_to_utf8
677
678
  return "Qt::DateTimeCodec.decode_qtime(#{rewritten_native_call})" if method[:return_cast] == :qtime_to_utf8
679
+ return "Qt::ObjectWrapper.wrap(#{rewritten_native_call}, '#{method[:pointer_class]}')" if method[:pointer_class]
678
680
 
679
681
  rewritten_native_call
680
682
  end
@@ -824,6 +826,7 @@ def qapplication_class_method_body(method, native_call)
824
826
  return " Qt::DateTimeCodec.decode_qdatetime(#{native_call})" if method[:return_cast] == :qdatetime_to_utf8
825
827
  return " Qt::DateTimeCodec.decode_qdate(#{native_call})" if method[:return_cast] == :qdate_to_utf8
826
828
  return " Qt::DateTimeCodec.decode_qtime(#{native_call})" if method[:return_cast] == :qtime_to_utf8
829
+ return " Qt::ObjectWrapper.wrap(#{native_call}, '#{method[:pointer_class]}')" if method[:pointer_class]
827
830
 
828
831
  " #{native_call}"
829
832
  end
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.2
4
+ version: 0.1.4
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
@@ -98,5 +99,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
98
99
  requirements: []
99
100
  rubygems_version: 3.6.7
100
101
  specification_version: 4
101
- summary: Ruby bindings for Qt 6.10+
102
+ summary: Ruby bindings for Qt 6.4.2+
102
103
  test_files: []