qt 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +27 -0
  3. data/README.md +303 -0
  4. data/Rakefile +94 -0
  5. data/examples/development_ordered_demos/01_dsl_hello.rb +22 -0
  6. data/examples/development_ordered_demos/02_live_layout_console.rb +137 -0
  7. data/examples/development_ordered_demos/03_component_showcase.rb +235 -0
  8. data/examples/development_ordered_demos/04_paint_simple.rb +147 -0
  9. data/examples/development_ordered_demos/05_tetris_simple.rb +295 -0
  10. data/examples/development_ordered_demos/06_timetrap_clockify.rb +759 -0
  11. data/examples/development_ordered_demos/07_peek_like_recorder.rb +597 -0
  12. data/examples/qtproject/widgets/itemviews/spreadsheet/main.rb +252 -0
  13. data/examples/qtproject/widgets/widgetsgallery/main.rb +184 -0
  14. data/ext/qt_ruby_bridge/extconf.rb +75 -0
  15. data/ext/qt_ruby_bridge/qt_ruby_runtime.hpp +23 -0
  16. data/ext/qt_ruby_bridge/runtime_events.cpp +408 -0
  17. data/ext/qt_ruby_bridge/runtime_signals.cpp +212 -0
  18. data/lib/qt/application_lifecycle.rb +44 -0
  19. data/lib/qt/bridge.rb +95 -0
  20. data/lib/qt/children_tracking.rb +15 -0
  21. data/lib/qt/constants.rb +10 -0
  22. data/lib/qt/date_time_codec.rb +104 -0
  23. data/lib/qt/errors.rb +6 -0
  24. data/lib/qt/event_runtime.rb +139 -0
  25. data/lib/qt/event_runtime_dispatch.rb +35 -0
  26. data/lib/qt/event_runtime_qobject_methods.rb +41 -0
  27. data/lib/qt/generated_constants_runtime.rb +33 -0
  28. data/lib/qt/inspectable.rb +29 -0
  29. data/lib/qt/key_sequence_codec.rb +22 -0
  30. data/lib/qt/native.rb +93 -0
  31. data/lib/qt/shortcut_compat.rb +30 -0
  32. data/lib/qt/string_codec.rb +44 -0
  33. data/lib/qt/variant_codec.rb +78 -0
  34. data/lib/qt/version.rb +5 -0
  35. data/lib/qt.rb +47 -0
  36. data/scripts/generate_bridge/ast_introspection.rb +267 -0
  37. data/scripts/generate_bridge/auto_method_spec_resolver.rb +37 -0
  38. data/scripts/generate_bridge/auto_methods.rb +438 -0
  39. data/scripts/generate_bridge/core_utils.rb +114 -0
  40. data/scripts/generate_bridge/cpp_method_return_emitter.rb +93 -0
  41. data/scripts/generate_bridge/ffi_api.rb +46 -0
  42. data/scripts/generate_bridge/free_function_specs.rb +289 -0
  43. data/scripts/generate_bridge/spec_discovery.rb +313 -0
  44. data/scripts/generate_bridge.rb +1113 -0
  45. metadata +99 -0
data/lib/qt/bridge.rb ADDED
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+ require 'rbconfig'
5
+
6
+ module Qt
7
+ # Bridge helpers for registering and disposing wrapped native objects.
8
+ module Bridge
9
+ extend FFI::Library
10
+
11
+ module_function
12
+
13
+ def load!
14
+ return true if @loaded
15
+
16
+ ffi_lib(library_candidates)
17
+ attach_api
18
+ @loaded = true
19
+ rescue LoadError, FFI::NotFoundError => e
20
+ @loaded = false
21
+ @load_error = e
22
+ false
23
+ end
24
+
25
+ def loaded?
26
+ !!@loaded
27
+ end
28
+
29
+ def load_error
30
+ @load_error
31
+ end
32
+
33
+ def library_candidates
34
+ @library_candidates ||= begin
35
+ ext = RbConfig::CONFIG['DLEXT']
36
+ root = File.expand_path('../..', __dir__)
37
+ load_path_candidates = load_path_library_candidates(ext)
38
+ gem_extension_candidates = gem_extension_library_candidates(ext)
39
+
40
+ [
41
+ File.join(root, 'build', 'qt', "qt_ruby_bridge.#{ext}"),
42
+ File.join(root, 'lib', 'qt', "qt_ruby_bridge.#{ext}"),
43
+ *gem_extension_candidates,
44
+ *load_path_candidates,
45
+ 'qt_ruby_bridge'
46
+ ].uniq
47
+ end
48
+ end
49
+
50
+ def load_path_library_candidates(ext)
51
+ $LOAD_PATH.flat_map do |entry|
52
+ [
53
+ File.join(entry, 'qt', "qt_ruby_bridge.#{ext}"),
54
+ File.join(entry, "qt_ruby_bridge.#{ext}")
55
+ ]
56
+ end
57
+ end
58
+
59
+ def gem_extension_library_candidates(ext)
60
+ spec = Gem::Specification.find_by_name('qt')
61
+ extension_dir = spec&.extension_dir
62
+ return [] if extension_dir.nil? || extension_dir.empty?
63
+
64
+ [
65
+ File.join(extension_dir, 'qt', "qt_ruby_bridge.#{ext}"),
66
+ File.join(extension_dir, "qt_ruby_bridge.#{ext}")
67
+ ]
68
+ rescue Gem::LoadError
69
+ []
70
+ end
71
+
72
+ def attach_api
73
+ return if @api_attached
74
+
75
+ ensure_generated_api!
76
+ Qt::BridgeAPI::FUNCTIONS.each do |fn|
77
+ attach_function fn[:name], fn[:args], fn[:return]
78
+ end
79
+
80
+ @api_attached = true
81
+ end
82
+ private_class_method :attach_api
83
+
84
+ def ensure_generated_api!
85
+ return if defined?(Qt::BridgeAPI::FUNCTIONS)
86
+
87
+ root = File.expand_path('../..', __dir__)
88
+ generated_api = File.join(root, 'build', 'generated', 'bridge_api.rb')
89
+ generator = File.join(root, 'scripts', 'generate_bridge.rb')
90
+ system(RbConfig.ruby, generator) unless File.exist?(generated_api)
91
+ require generated_api
92
+ end
93
+ private_class_method :ensure_generated_api!
94
+ end
95
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Child object tracking to mirror Qt parent/child ownership in Ruby.
5
+ module ChildrenTracking
6
+ def init_children_tracking!
7
+ @children = []
8
+ end
9
+
10
+ def add_child(child)
11
+ @children ||= []
12
+ @children << child
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'generated_constants_runtime'
4
+
5
+ module Qt
6
+ GENERATED_CONSTANTS = File.expand_path('../../build/generated/constants.rb', __dir__)
7
+ require GENERATED_CONSTANTS if File.exist?(GENERATED_CONSTANTS)
8
+
9
+ GeneratedConstantsRuntime.apply_key_aliases!(self)
10
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'time'
5
+
6
+ module Qt
7
+ # Codec for typed QDateTime/QDate/QTime bridge payloads.
8
+ module DateTimeCodec
9
+ module_function
10
+
11
+ DATETIME_PREFIX = 'qtdt:'
12
+ DATE_PREFIX = 'qtdate:'
13
+ TIME_PREFIX = 'qttime:'
14
+
15
+ def encode_qdatetime(value)
16
+ time = coerce_to_time(value)
17
+ "#{DATETIME_PREFIX}#{time.iso8601(6)}"
18
+ end
19
+
20
+ def decode_qdatetime(value)
21
+ raw = StringCodec.from_qt_text(value.to_s)
22
+ payload = raw.start_with?(DATETIME_PREFIX) ? raw.delete_prefix(DATETIME_PREFIX) : raw
23
+ Time.iso8601(payload)
24
+ rescue ArgumentError
25
+ Time.at(0).utc
26
+ end
27
+
28
+ def encode_qdate(value)
29
+ date = coerce_to_date(value)
30
+ "#{DATE_PREFIX}#{date.strftime('%Y-%m-%d')}"
31
+ end
32
+
33
+ def decode_qdate(value)
34
+ raw = StringCodec.from_qt_text(value.to_s)
35
+ payload = raw.start_with?(DATE_PREFIX) ? raw.delete_prefix(DATE_PREFIX) : raw
36
+ Date.iso8601(payload)
37
+ rescue ArgumentError
38
+ Date.new(1970, 1, 1)
39
+ end
40
+
41
+ def encode_qtime(value)
42
+ time_string =
43
+ case value
44
+ when Time then value.strftime('%H:%M:%S')
45
+ else normalize_time_string(value.to_s)
46
+ end
47
+ "#{TIME_PREFIX}#{time_string}"
48
+ end
49
+
50
+ def decode_qtime(value)
51
+ raw = StringCodec.from_qt_text(value.to_s)
52
+ payload = raw.start_with?(TIME_PREFIX) ? raw.delete_prefix(TIME_PREFIX) : raw
53
+ normalize_time_string(payload)
54
+ rescue ArgumentError
55
+ '00:00:00'
56
+ end
57
+
58
+ def decode_for_signal(signal_name, payload)
59
+ return nil if payload.nil?
60
+
61
+ signature = signal_name.to_s
62
+ if signature.start_with?('dateTimeChanged(')
63
+ return decode_qdatetime(payload)
64
+ end
65
+ if signature.start_with?('dateChanged(')
66
+ return decode_qdate(payload)
67
+ end
68
+ if signature.start_with?('timeChanged(')
69
+ return decode_qtime(payload)
70
+ end
71
+
72
+ StringCodec.from_qt_text(payload)
73
+ end
74
+
75
+ def coerce_to_time(value)
76
+ return value if value.is_a?(Time)
77
+ return value.to_time if value.respond_to?(:to_time)
78
+
79
+ Time.iso8601(value.to_s)
80
+ rescue ArgumentError
81
+ Time.at(0).utc
82
+ end
83
+
84
+ def coerce_to_date(value)
85
+ return value if value.is_a?(Date)
86
+ return value.to_date if value.respond_to?(:to_date)
87
+
88
+ Date.iso8601(value.to_s)
89
+ rescue ArgumentError
90
+ Date.new(1970, 1, 1)
91
+ end
92
+
93
+ def normalize_time_string(value)
94
+ raw = value.to_s.strip
95
+ raise ArgumentError, 'time is empty' if raw.empty?
96
+
97
+ return "#{raw}:00" if raw.match?(/\A\d{2}:\d{2}\z/)
98
+ return raw if raw.match?(/\A\d{2}:\d{2}:\d{2}\z/)
99
+
100
+ parsed = Time.parse(raw)
101
+ parsed.strftime('%H:%M:%S')
102
+ end
103
+ end
104
+ end
data/lib/qt/errors.rb ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ class Error < StandardError; end
5
+ class NativeExtensionError < Error; end
6
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Event/signal subscription runtime backed by generated native callbacks.
5
+ module EventRuntime
6
+ module_function
7
+
8
+ def on_event(widget, event_name, &block)
9
+ raise ArgumentError, 'pass block to on_event' unless block
10
+
11
+ ensure_native_bridge_ready!
12
+ ensure_event_callback!
13
+
14
+ event_type = event_type_for(event_name)
15
+ handle = widget_handle(widget) || raise(ArgumentError, 'widget handle is required')
16
+
17
+ @event_handlers ||= {}
18
+ ((@event_handlers[handle.address] ||= {})[event_type] ||= []) << block
19
+
20
+ Qt::Native.watch_qobject_event(handle, event_type)
21
+ true
22
+ end
23
+
24
+ def on_signal(widget, signal_name, &block)
25
+ raise ArgumentError, 'pass block to on_signal' unless block
26
+
27
+ ensure_native_bridge_ready!
28
+ ensure_signal_callback!
29
+
30
+ handle = widget_handle(widget) || raise(ArgumentError, 'widget handle is required')
31
+ per_signal = prepare_signal_registration(handle, signal_name)
32
+ per_signal[:blocks] << block
33
+ true
34
+ end
35
+
36
+ def prepare_signal_registration(handle, signal_name)
37
+ signal_key = signal_name.to_s
38
+ raise ArgumentError, 'signal name is required' if signal_key.empty?
39
+
40
+ @signal_handlers ||= {}
41
+ per_signal = ((@signal_handlers[handle.address] ||= {})[signal_key] ||= { index: nil, blocks: [] })
42
+ if per_signal[:index].nil?
43
+ index = Qt::Native.qobject_connect_signal(handle, signal_key)
44
+ raise ArgumentError, "failed to connect signal #{signal_key.inspect} (code=#{index})" if index.negative?
45
+
46
+ per_signal[:index] = index
47
+ end
48
+ per_signal
49
+ end
50
+
51
+ def off_signal(widget, signal_name = nil)
52
+ ensure_native_bridge_ready!
53
+ handle = widget_handle(widget)
54
+ return false if handle.nil? || @signal_handlers.nil?
55
+
56
+ per_widget = @signal_handlers[handle.address]
57
+ return false if per_widget.nil?
58
+
59
+ signal_key = signal_name&.to_s
60
+ per_widget.delete(signal_key) if signal_key
61
+ per_widget.clear unless signal_key
62
+ Qt::Native.qobject_disconnect_signal(handle, signal_key)
63
+ true
64
+ end
65
+
66
+ def off_event(widget, event_name = nil)
67
+ ensure_native_bridge_ready!
68
+ handle = widget_handle(widget)
69
+ return false if handle.nil? || @event_handlers.nil?
70
+
71
+ per_widget = @event_handlers[handle.address]
72
+ return false unless per_widget
73
+
74
+ off_event_for_widget(handle, per_widget, event_name)
75
+ true
76
+ end
77
+
78
+ def event_type_for(event_name)
79
+ 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]
87
+ raise ArgumentError, "unknown event: #{event_name.inspect}" unless event_type
88
+
89
+ event_type
90
+ end
91
+
92
+ def ensure_event_callback!
93
+ return if @event_callback
94
+
95
+ @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 }
100
+ EventRuntimeDispatch.dispatch_event(@event_handlers, object_handle, event_type, payload)
101
+ end
102
+
103
+ Qt::Native.set_event_callback(@event_callback)
104
+ end
105
+
106
+ def ensure_signal_callback!
107
+ return if @signal_callback
108
+
109
+ @signal_callback = FFI::Function.new(:void, %i[pointer int string]) do |object_handle, signal_index, payload|
110
+ normalized_payload = payload.nil? ? nil : Qt::StringCodec.from_qt_text(payload)
111
+ EventRuntimeDispatch.dispatch_signal(@signal_handlers, object_handle, signal_index, normalized_payload)
112
+ end
113
+
114
+ Qt::Native.set_signal_callback(@signal_callback)
115
+ end
116
+
117
+ def widget_handle(widget)
118
+ return nil if widget.nil?
119
+
120
+ widget.respond_to?(:handle) ? widget.handle : widget
121
+ end
122
+
123
+ def ensure_native_bridge_ready!
124
+ Qt::Native.ensure_loaded!
125
+ Qt::Native.define_bridge_wrappers!
126
+ end
127
+
128
+ def off_event_for_widget(handle, per_widget, event_name)
129
+ if event_name
130
+ event_type = event_type_for(event_name)
131
+ per_widget.delete(event_type)
132
+ Qt::Native.unwatch_qobject_event(handle, event_type)
133
+ else
134
+ per_widget.each_key { |event_type| Qt::Native.unwatch_qobject_event(handle, event_type) }
135
+ @event_handlers.delete(handle.address)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Dispatch helpers for event/signal callbacks from the native bridge.
4
+ module Qt
5
+ # Dispatch helpers for event/signal callbacks from the native bridge.
6
+ module EventRuntimeDispatch
7
+ module_function
8
+
9
+ def dispatch_event(event_handlers, object_handle, event_type, payload)
10
+ return unless object_handle && event_handlers
11
+
12
+ per_widget = event_handlers[object_handle.address]
13
+ return unless per_widget
14
+
15
+ handlers = per_widget[event_type]
16
+ return unless handlers && !handlers.empty?
17
+
18
+ handlers.each { |handler| handler.call(payload) }
19
+ end
20
+
21
+ def dispatch_signal(signal_handlers, object_handle, signal_index, payload)
22
+ return unless object_handle && signal_handlers
23
+
24
+ per_widget = signal_handlers[object_handle.address]
25
+ return unless per_widget
26
+
27
+ per_widget.each do |signal_name, entry|
28
+ next unless entry[:index] == signal_index
29
+
30
+ typed_payload = Qt::DateTimeCodec.decode_for_signal(signal_name, payload)
31
+ entry[:blocks].each { |handler| handler.call(typed_payload) }
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ # QObject-level event/signal helper methods mixed into generated classes.
4
+ module Qt
5
+ module EventRuntime
6
+ # QObject-level event/signal helper methods mixed into generated classes.
7
+ module QObjectMethods
8
+ def on(event_name, &block)
9
+ raise ArgumentError, 'pass block to on' unless block
10
+
11
+ EventRuntime.on_event(self, event_name, &block)
12
+ self
13
+ end
14
+ alias on_event on
15
+
16
+ def connect(signal_name, &block)
17
+ raise ArgumentError, 'pass block to connect' unless block
18
+
19
+ EventRuntime.on_signal(self, signal_name, &block)
20
+ self
21
+ end
22
+ alias on_signal connect
23
+ alias slot connect
24
+
25
+ def off(event_name = nil)
26
+ EventRuntime.off_event(self, event_name)
27
+ self
28
+ end
29
+ alias off_event off
30
+
31
+ def disconnect(signal_name = nil)
32
+ EventRuntime.off_signal(self, signal_name)
33
+ self
34
+ end
35
+ alias off_signal disconnect
36
+ end
37
+
38
+ # Backward-compatible alias for already-generated code.
39
+ WidgetMethods = QObjectMethods
40
+ end
41
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ module GeneratedConstantsRuntime
5
+ module_function
6
+
7
+ def apply_generated_scoped_constants!(qt_module)
8
+ return unless qt_module.const_defined?(:GENERATED_SCOPED_CONSTANTS, false)
9
+
10
+ qt_module.const_get(:GENERATED_SCOPED_CONSTANTS, false).each do |owner_name, owner_constants|
11
+ next unless qt_module.const_defined?(owner_name, false)
12
+
13
+ owner = qt_module.const_get(owner_name, false)
14
+ owner_constants.each do |const_name, const_value|
15
+ next if owner.const_defined?(const_name, false)
16
+
17
+ owner.const_set(const_name, const_value)
18
+ end
19
+ end
20
+ end
21
+
22
+ def apply_key_aliases!(qt_module)
23
+ qt_module.constants(false).grep(/\AKey_[A-Za-z0-9_]+\z/).each do |source_name|
24
+ suffix = source_name.to_s.sub(/\AKey_/, '')
25
+ alias_name = "Key#{suffix.split('_').map(&:capitalize).join}"
26
+ next unless alias_name.match?(/\A[A-Z][A-Za-z0-9_]*\z/)
27
+ next if qt_module.const_defined?(alias_name, false)
28
+
29
+ qt_module.const_set(alias_name, qt_module.const_get(source_name, false))
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Common object inspection formatting for Qt wrapper instances.
5
+ module Inspectable
6
+ def q_inspect_property_values
7
+ property_values = {}
8
+ self.class::QT_API_PROPERTIES.each do |property|
9
+ property_values[property] = public_send(property)
10
+ rescue StandardError => e
11
+ property_values[property] = { error: e.class.name, message: e.message }
12
+ end
13
+ property_values
14
+ end
15
+
16
+ def q_inspect
17
+ {
18
+ qt_class: self.class::QT_CLASS,
19
+ ruby_class: self.class.name,
20
+ handle: @handle,
21
+ qt_methods: self.class::QT_API_QT_METHODS,
22
+ ruby_methods: self.class::QT_API_RUBY_METHODS,
23
+ properties: q_inspect_property_values
24
+ }
25
+ end
26
+ alias qt_inspect q_inspect
27
+ alias to_h q_inspect
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Conversion helpers for QKeySequence bridge arguments.
5
+ module KeySequenceCodec
6
+ module_function
7
+
8
+ def encode(value)
9
+ return '' if value.nil?
10
+ return Qt::StringCodec.to_qt_text(value) if value.is_a?(String)
11
+
12
+ if value.respond_to?(:to_string)
13
+ return Qt::StringCodec.to_qt_text(value.to_string)
14
+ end
15
+ if value.respond_to?(:toString)
16
+ return Qt::StringCodec.to_qt_text(value.toString)
17
+ end
18
+
19
+ Qt::StringCodec.to_qt_text(value.to_s)
20
+ end
21
+ end
22
+ end
data/lib/qt/native.rb ADDED
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Thin FFI access layer for generated bridge entry points.
5
+ module Native
6
+ require 'ffi'
7
+
8
+ ROOT = File.expand_path('../..', __dir__)
9
+ GENERATED_API = File.join(ROOT, 'build', 'generated', 'bridge_api.rb')
10
+
11
+ require GENERATED_API if File.exist?(GENERATED_API)
12
+
13
+ COERCERS = {
14
+ string: :to_s.to_proc,
15
+ int: ->(value) { Integer(value) },
16
+ bool: ->(value) { !value.nil? && value != false },
17
+ pointer: lambda { |value|
18
+ return nil if value.nil?
19
+
20
+ value.respond_to?(:handle) ? value.handle : value
21
+ }
22
+ }.freeze
23
+
24
+ module_function
25
+
26
+ def available?
27
+ return @available unless @available.nil?
28
+
29
+ @available = Bridge.load! && Bridge.loaded?
30
+ end
31
+
32
+ def ensure_loaded!
33
+ return if available?
34
+
35
+ detail = Bridge.load_error ? " (#{Bridge.load_error.message})" : ''
36
+ raise NativeExtensionError,
37
+ "Qt bridge is not available. Build it with: bundle exec rake compile#{detail}"
38
+ end
39
+
40
+ def define_bridge_wrappers!
41
+ return if @bridge_wrappers_defined
42
+ return unless defined?(Qt::BridgeAPI::FUNCTIONS)
43
+
44
+ Qt::BridgeAPI::FUNCTIONS.each do |fn|
45
+ define_bridge_wrapper(fn)
46
+ end
47
+
48
+ @bridge_wrappers_defined = true
49
+ end
50
+
51
+ def define_bridge_wrapper(function_spec)
52
+ native_name = function_spec[:name].to_s.sub(/\Aqt_ruby_/, '')
53
+ signature = function_spec[:args]
54
+ bridge_name = function_spec[:name]
55
+
56
+ define_singleton_method(native_name) do |*args|
57
+ ensure_loaded!
58
+ normalized = normalize_bridge_args(args, signature)
59
+ converted = coerce_bridge_args(normalized, signature)
60
+ Bridge.public_send(bridge_name, *converted)
61
+ end
62
+ end
63
+
64
+ def normalize_bridge_args(args, signature)
65
+ if args.length < signature.length
66
+ ensure_missing_args_optional!(args, signature)
67
+
68
+ return args + ([nil] * (signature.length - args.length))
69
+ end
70
+ raise_wrong_arity(args.length, signature.length) if args.length > signature.length
71
+
72
+ args
73
+ end
74
+
75
+ def ensure_missing_args_optional!(args, signature)
76
+ missing = signature[args.length..]
77
+ raise_wrong_arity(args.length, signature.length) unless missing.all? { |type| type == :pointer }
78
+ end
79
+
80
+ def raise_wrong_arity(given, expected)
81
+ raise ArgumentError, "wrong number of arguments (given #{given}, expected #{expected})"
82
+ end
83
+
84
+ def coerce_bridge_args(args, signature)
85
+ args.zip(signature).map do |value, type|
86
+ coercer = COERCERS[type]
87
+ coercer ? coercer.call(value) : value
88
+ end
89
+ end
90
+
91
+ define_bridge_wrappers!
92
+ end
93
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Qt
4
+ # Backward-compatible QShortcut#set_keys handling for QKeySequence inputs.
5
+ module ShortcutCompat
6
+ module_function
7
+
8
+ def key_sequence_like?(value)
9
+ value.is_a?(String) || value.respond_to?(:to_string) || value.respond_to?(:toString)
10
+ end
11
+ end
12
+ end
13
+
14
+ if defined?(Qt::QShortcut)
15
+ module Qt
16
+ class QShortcut
17
+ if instance_methods(false).include?(:set_keys) && !instance_methods(false).include?(:set_keys_without_qkeysequence_compat)
18
+ alias_method :set_keys_without_qkeysequence_compat, :set_keys
19
+ end
20
+
21
+ def set_keys(value)
22
+ if respond_to?(:set_key) && ShortcutCompat.key_sequence_like?(value)
23
+ set_key(value)
24
+ else
25
+ set_keys_without_qkeysequence_compat(value)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end