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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6d1a405c62cfb867d7799098163f4dff1122b3c92aa065ee0397ca499c0c4137
4
+ data.tar.gz: 7d731f1d727f82a96d1e1f8b910aa953f0d727bcc0ee2026e8da5ba3e536ad67
5
+ SHA512:
6
+ metadata.gz: 0bcc6466742ddd75c5e81d38ccaf77b8767092f16de9abc4206735d5b7a34d8af2223c0a90efe67333f91aa7adca6f210901b121c32e6b8c7d1108a618065dc7
7
+ data.tar.gz: 1d55595b8845d119bbc11f60518281b1fea8ef5f3aa55698b174bf4e63d31e39d8771515c9980fc5cfe1d593a362193acd2159432a5be4b9e77218b870c78385
data/LICENSE ADDED
@@ -0,0 +1,27 @@
1
+ SPDX-License-Identifier: BSD-2-Clause
2
+
3
+ BSD 2-Clause License
4
+
5
+ Copyright (c) 2026, Maksim Veynberg
6
+ All rights reserved.
7
+
8
+ Redistribution and use in source and binary forms, with or without
9
+ modification, are permitted provided that the following conditions are met:
10
+
11
+ 1. Redistributions of source code must retain the above copyright notice, this
12
+ list of conditions and the following disclaimer.
13
+
14
+ 2. Redistributions in binary form must reproduce the above copyright notice,
15
+ this list of conditions and the following disclaimer in the documentation
16
+ and/or other materials provided with the distribution.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,303 @@
1
+ # qt
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)
5
+ ![Status](https://img.shields.io/badge/Status-Experimental-orange)
6
+ ![Bridge](https://img.shields.io/badge/Architecture-Ruby%20%E2%86%94%20Qt%20Bridge-blue)
7
+
8
+ Ruby-first Qt 6.10+ bridge.
9
+
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
+
12
+ ## Why It Hits Different
13
+
14
+ - Pure Ruby usage: no QML, no extra UI language.
15
+ - Real Qt power: `QApplication`, `QWidget`, `QLabel`, `QPushButton`, `QVBoxLayout`.
16
+ - Ruby ergonomics: Qt-style and snake_case/property style in parallel.
17
+ - Live GUI hacking: update widgets while the window is open.
18
+ - Generated bridge: API is derived from system Qt headers.
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
38
+
39
+ Before (typical static run):
40
+
41
+ ```ruby
42
+ app = QApplication.new(0, [])
43
+ window = QWidget.new
44
+ window.show
45
+ app.exec
46
+ ```
47
+
48
+ After (live dev loop):
49
+
50
+ ```ruby
51
+ # app already running
52
+ add_label("Dynamic block")
53
+ add_button("Ship")
54
+ gui { window.set_window_title("Changed live") }
55
+ ```
56
+
57
+ ## Install
58
+
59
+ ### Requirements
60
+
61
+ - Ruby 3.1+
62
+ - Qt 6.10+ dev packages (`Qt6Core`, `Qt6Gui`, `Qt6Widgets` via `pkg-config`)
63
+ - C++17 compiler
64
+
65
+ Check Qt:
66
+
67
+ ```bash
68
+ pkg-config --modversion Qt6Widgets
69
+ ```
70
+
71
+ ### Build from repo
72
+
73
+ ```bash
74
+ bundle install
75
+ bundle exec rake compile
76
+ bundle exec rake install
77
+ ```
78
+
79
+ `rake install` installs into your current Ruby environment (including active `rbenv` version).
80
+ `rake compile` builds the full bridge with `QT_RUBY_SCOPE=all` by default.
81
+
82
+ ### Gem usage
83
+
84
+ ```bash
85
+ gem install qt
86
+ ```
87
+
88
+ ## Hello Qt in Ruby
89
+
90
+ ```ruby
91
+ require 'qt'
92
+
93
+ app = QApplication.new(0, [])
94
+
95
+ window = QWidget.new do |w|
96
+ w.set_window_title('Qt Ruby App')
97
+ w.resize(800, 600)
98
+ end
99
+
100
+ label = QLabel.new(window)
101
+ label.text = 'Hello from Ruby + Qt'
102
+ label.set_alignment(Qt::AlignCenter)
103
+ label.set_geometry(0, 0, 800, 600)
104
+
105
+ app.exec
106
+ ```
107
+
108
+ ## API Style: Qt + Ruby
109
+
110
+ ```ruby
111
+ # Qt style
112
+ label.setText('A')
113
+ window.setWindowTitle('Main')
114
+
115
+ # Ruby style
116
+ label.text = 'B'
117
+ window.window_title = 'Main 2'
118
+ puts label.text
119
+ ```
120
+
121
+ ## API Compatibility Notes
122
+
123
+ Generated Ruby API is intentionally close to Qt API, but follows universal bridge policies.
124
+
125
+ - `snake_case` aliases are generated for Qt camelCase methods.
126
+ - Ruby keyword-safe renaming is applied when needed: `next` -> `next_`.
127
+ - Default C++ arguments are surfaced as optional Ruby arguments.
128
+ - Internal runtime name collisions are renamed consistently:
129
+ - Qt `handle(int)` is exposed as `handle_at(int)` because `handle` is used for native object pointer access.
130
+ - Property convenience API is generated from Qt setters/getters when available:
131
+ - `setText(...)` -> `text=(...)`, `text`.
132
+ - Runtime event/signal convenience methods are Ruby-layer helpers (not raw Qt method names):
133
+ - `on(event, &block)` / alias `on_event`
134
+ - `off(event = nil)` / alias `off_event`
135
+ - `connect(signal, &block)` / aliases `on_signal`, `slot`
136
+ - `disconnect(signal = nil)` / alias `off_signal`
137
+ - these helpers are mixed into generated `QObject` descendants (for example `QWidget`, `QPushButton`, `QTimer`)
138
+ - non-`QObject` value classes (`QIcon`, `QPixmap`, `QImage`) intentionally do not expose `connect`/`on`
139
+ - event delivery is target-first with nearest watched ancestor fallback for interactive events (mouse/key/focus/enter/leave)
140
+ - Introspection helpers are Ruby-layer helpers:
141
+ - `q_inspect`, aliases `qt_inspect`, `to_h`
142
+ - Top-level constant aliases are provided for convenience:
143
+ - `QApplication`, `QWidget`, `QLabel`, `QPushButton`, `QLineEdit`, `QVBoxLayout`, `QTableWidget`, `QTableWidgetItem`, `QScrollArea`
144
+ - Methods with unsupported signatures are skipped by policy:
145
+ - non-public, deprecated, operator/internal event hooks,
146
+ - non-FFI-safe argument/return types.
147
+
148
+ ## Introspection
149
+
150
+ Every generated object exposes API snapshot helpers:
151
+
152
+ ```ruby
153
+ label.q_inspect
154
+ label.qt_inspect
155
+ label.to_h
156
+ ```
157
+
158
+ Shape:
159
+
160
+ ```ruby
161
+ {
162
+ qt_class: "QLabel",
163
+ ruby_class: "Qt::QLabel",
164
+ qt_methods: ["setText", "setAlignment", "text", ...],
165
+ ruby_methods: [:setText, :set_text, :text, ...],
166
+ properties: { text: "A", alignment: 129 }
167
+ }
168
+ ```
169
+
170
+ ## Examples
171
+
172
+ ```bash
173
+ ruby examples/development_ordered_demos/01_dsl_hello.rb
174
+ ruby examples/development_ordered_demos/02_live_layout_console.rb
175
+ ```
176
+
177
+ QObject signal example:
178
+
179
+ ```ruby
180
+ timer = QTimer.new
181
+ timer.set_interval(1000)
182
+ timer.connect('timeout') { puts 'tick' }
183
+ timer.start
184
+ ```
185
+
186
+ ## Architecture
187
+
188
+ 1. `scripts/generate_bridge.rb` reads Qt API from system headers.
189
+ 2. Generates:
190
+ - `build/generated/qt_ruby_bridge.cpp`
191
+ - `build/generated/bridge_api.rb`
192
+ - `build/generated/widgets.rb`
193
+ 3. Compiles native extension into `build/qt/qt_ruby_bridge.so`.
194
+ 4. Ruby layer calls bridge functions via `ffi`.
195
+
196
+ Everything generated/build-related is under `build/` and should stay out of git.
197
+
198
+ ## Layout
199
+
200
+ - `lib/qt` public Ruby API
201
+ - `scripts/generate_bridge.rb` AST-driven bridge generator
202
+ - `ext/qt_ruby_bridge` native extension entrypoint
203
+ - `build/generated` generated sources
204
+ - `build/qt` compiled bridge `.so`
205
+ - `examples` demos
206
+ - `test` tests
207
+
208
+ ## Roadmap
209
+
210
+ ### Done
211
+
212
+ - AST-driven generation with scope support: `QT_RUBY_SCOPE=widgets|qobject|all`
213
+ - default compile path switched to `all` (`widgets + qobject`)
214
+ - generated Qt inheritance in Ruby classes (including intermediate Qt wrappers)
215
+ - Qt-native event/signal runtime wired to Ruby at QObject level (`on`, `connect`, `disconnect`)
216
+ - `QTimer` available in generated API with `connect('timeout')` support
217
+ - `06_timetrap_clockify` moved to `app.exec` + `QTimer` update loop (no manual polling loop)
218
+ - QObject styling hooks exposed for QSS selectors:
219
+ - `setObjectName` / `object_name=`
220
+ - `setProperty` / `property` (via QVariant bridge codec)
221
+ - window icon support from generated API:
222
+ - `QIcon.new(path)`
223
+ - `QWidget#setWindowIcon` / `set_window_icon`
224
+
225
+ ### Next
226
+
227
+ - typed signal payloads (not only raw/placeholder payload)
228
+ - richer QObject metaobject Ruby API (`meta_object`, methods/signatures/properties introspection)
229
+ - normalize signal naming rules for overloads and deterministic connect behavior
230
+
231
+ ### Later
232
+
233
+ - expand generated surface for additional Qt modules (network, sql, xml, etc.) using the same generator policy
234
+ - packaging hardening for Linux/macOS (install/build paths, gem install reliability)
235
+ - CI matrix for Ruby/Qt combinations and scope modes (`widgets`, `qobject`, `all`)
236
+ - add performance checks for generator traversal and compile size/time regression tracking
237
+
238
+ ## Development
239
+
240
+ ```bash
241
+ bundle exec rake test
242
+ bundle exec rake compile
243
+ bundle exec rake rubocop
244
+ ```
245
+
246
+ ### Test Environment Variables
247
+
248
+ Tests force `QT_QPA_PLATFORM=offscreen` by default to avoid opening GUI windows.
249
+
250
+ - `QT_QPA_PLATFORM_FORCE_XCB=true`
251
+ - override test default and run with `QT_QPA_PLATFORM=xcb` (real X11 backend)
252
+ - `QT_RUBY_MANUAL_MODIFIERS=1`
253
+ - enable manual keyboard-modifier smoke test (`Ctrl/Shift` must be pressed during test window)
254
+
255
+ Examples:
256
+
257
+ ```bash
258
+ # default headless test run
259
+ bundle exec rake test
260
+
261
+ # run tests on xcb backend
262
+ QT_QPA_PLATFORM_FORCE_XCB=true bundle exec rake test
263
+
264
+ # run only manual modifiers smoke test
265
+ QT_QPA_PLATFORM_FORCE_XCB=true QT_RUBY_MANUAL_MODIFIERS=1 \
266
+ bundle exec ruby -Itest test/application_test.rb \
267
+ --name test_qapplication_keyboard_modifiers_manual_ctrl_shift_smoke --verbose
268
+ ```
269
+
270
+ ### Generation Scope
271
+
272
+ Default build scope is `all`. You can still override scope manually with `QT_RUBY_SCOPE`:
273
+
274
+ - `widgets` (default): QWidget/QLayout-oriented classes.
275
+ - `qobject`: QObject descendants excluding QWidget/QLayout branch.
276
+ - `all`: combined public surface from `widgets` + `qobject` scopes (default build mode).
277
+
278
+ Examples:
279
+
280
+ ```bash
281
+ QT_RUBY_SCOPE=widgets bundle exec rake compile
282
+ QT_RUBY_SCOPE=qobject bundle exec rake compile
283
+ QT_RUBY_SCOPE=all bundle exec rake compile
284
+ ```
285
+
286
+ If Qt is in a custom prefix:
287
+
288
+ ```bash
289
+ export PKG_CONFIG_PATH="/path/to/qt/lib/pkgconfig:$PKG_CONFIG_PATH"
290
+ ```
291
+
292
+ Native event-runtime debug logs:
293
+
294
+ ```bash
295
+ QT_RUBY_EVENT_DEBUG=1 ruby your_app.rb
296
+ ```
297
+
298
+ Optional tuning:
299
+
300
+ ```bash
301
+ # enable ancestor fallback for MouseMove events (off by default)
302
+ QT_RUBY_EVENT_ANCESTOR_MOUSE_MOVE=1 ruby your_app.rb
303
+ ```
data/Rakefile ADDED
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rubygems'
6
+ require 'rubocop/rake_task'
7
+
8
+ CLEAN.include(
9
+ '**/*.o',
10
+ '**/*.so',
11
+ '**/*.bundle',
12
+ '**/*.dll',
13
+ '**/*.dylib',
14
+ 'ext/**/Makefile',
15
+ 'ext/**/mkmf.log',
16
+ 'ext/**/.sitearchdir.time',
17
+ 'ext/**/.sitelibdir.time',
18
+ 'tmp',
19
+ 'build'
20
+ )
21
+
22
+ EXT_DIR = File.expand_path('ext/qt_ruby_bridge', __dir__)
23
+ NATIVE_BUILD_DIR = File.expand_path('build/native', __dir__)
24
+ GENERATOR = File.expand_path('scripts/generate_bridge.rb', __dir__)
25
+ GEMSPEC_PATH = File.expand_path('qt.gemspec', __dir__)
26
+ GEM_SPEC = Gem::Specification.load(GEMSPEC_PATH)
27
+ GEM_FILE = "#{GEM_SPEC.name}-#{GEM_SPEC.version}.gem".freeze
28
+ PKG_DIR = File.expand_path('build/pkg', __dir__)
29
+ PKG_FILE = File.join(PKG_DIR, GEM_FILE)
30
+ GENERATED_CPP_PATH = File.expand_path('build/generated/qt_ruby_bridge.cpp', __dir__)
31
+ NATIVE_CPP_PATH = File.join(NATIVE_BUILD_DIR, 'qt_ruby_bridge.cpp')
32
+
33
+ desc 'Generate native bridge sources from system Qt headers'
34
+ task :generate_bindings do
35
+ sh "QT_RUBY_SCOPE=all ruby #{GENERATOR}"
36
+ end
37
+
38
+ namespace :generate_bindings do
39
+ desc 'Generate bindings for widgets scope'
40
+ task :widgets do
41
+ sh "QT_RUBY_SCOPE=widgets ruby #{GENERATOR}"
42
+ end
43
+
44
+ desc 'Generate bindings for qobject scope'
45
+ task :qobject do
46
+ sh "QT_RUBY_SCOPE=qobject ruby #{GENERATOR}"
47
+ end
48
+
49
+ desc 'Generate bindings for all scope'
50
+ task :all do
51
+ sh "QT_RUBY_SCOPE=all ruby #{GENERATOR}"
52
+ end
53
+ end
54
+
55
+ desc 'Compile native Qt bridge'
56
+ task compile: 'compile:all'
57
+
58
+ def compile_for_scope(scope)
59
+ sh "mkdir -p #{NATIVE_BUILD_DIR}"
60
+ sh "cp #{GENERATED_CPP_PATH} #{NATIVE_CPP_PATH}"
61
+ sh({ 'QT_RUBY_SCOPE' => scope }, "ruby #{File.join(EXT_DIR, 'extconf.rb')}", chdir: NATIVE_BUILD_DIR)
62
+ sh 'make', chdir: NATIVE_BUILD_DIR
63
+ sh 'mkdir -p ../qt', chdir: NATIVE_BUILD_DIR
64
+ sh 'cp qt_ruby_bridge.so ../qt/qt_ruby_bridge.so', chdir: NATIVE_BUILD_DIR
65
+ end
66
+
67
+ namespace :compile do
68
+ %w[widgets qobject all].each do |scope|
69
+ desc "Compile native Qt bridge for #{scope} scope"
70
+ task scope.to_sym => "generate_bindings:#{scope}" do
71
+ compile_for_scope(scope)
72
+ end
73
+ end
74
+ end
75
+
76
+ Rake::TestTask.new do |t|
77
+ t.libs << 'test'
78
+ t.pattern = 'test/**/*_test.rb'
79
+ end
80
+
81
+ desc 'Build gem package'
82
+ task :build_gem do
83
+ sh "mkdir -p #{PKG_DIR}"
84
+ sh "gem build #{GEMSPEC_PATH} --output #{PKG_FILE}"
85
+ end
86
+
87
+ desc 'Install gem locally (build + gem install --local)'
88
+ task install: :build_gem do
89
+ sh "gem install --local --force #{PKG_FILE}"
90
+ end
91
+
92
+ RuboCop::RakeTask.new(:rubocop)
93
+
94
+ task default: :test
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
4
+ require 'qt'
5
+
6
+ width = 640
7
+ height = 360
8
+
9
+ app = QApplication.new(0, [])
10
+
11
+ window = QWidget.new do |w|
12
+ w.setWindowTitle('Qt Ruby App')
13
+ w.resize(width, height)
14
+ end
15
+
16
+ QLabel.new(window) do |l|
17
+ l.setText('Hello from Ruby')
18
+ l.setAlignment(Qt::AlignCenter)
19
+ l.setGeometry(0, 0, width, height)
20
+ end
21
+
22
+ exit(app.exec)
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
4
+ require 'qt'
5
+ require 'irb'
6
+
7
+ app = QApplication.new(0, [])
8
+ window = QWidget.new do |w|
9
+ w.set_window_title('Qt Live Layout Console')
10
+ w.resize(700, 500)
11
+ end
12
+
13
+ layout = QVBoxLayout.new(window)
14
+ window.set_layout(layout)
15
+
16
+ items = []
17
+
18
+ banner = QLabel.new(window) do |l|
19
+ l.set_text('Use IRB helpers: add_label, add_button, remove_last')
20
+ l.set_alignment(Qt::AlignCenter)
21
+ end
22
+ layout.add_widget(banner)
23
+ items << banner
24
+
25
+ jobs = Queue.new
26
+ running = true
27
+
28
+ console = Object.new
29
+ console.instance_variable_set(:@app, app)
30
+ console.instance_variable_set(:@window, window)
31
+ console.instance_variable_set(:@layout, layout)
32
+ console.instance_variable_set(:@jobs, jobs)
33
+ console.instance_variable_set(:@items, items)
34
+
35
+ console.define_singleton_method(:app) { @app }
36
+ console.define_singleton_method(:window) { @window }
37
+ console.define_singleton_method(:layout) { @layout }
38
+ console.define_singleton_method(:items) { @items }
39
+
40
+ console.define_singleton_method(:gui) do |&block|
41
+ raise ArgumentError, 'pass block to gui { ... }' unless block
42
+
43
+ reply = Queue.new
44
+ @jobs << [block, reply]
45
+ ok, value = reply.pop
46
+ raise value unless ok
47
+
48
+ value
49
+ end
50
+
51
+ console.define_singleton_method(:add_label) do |text = 'new label'|
52
+ gui do
53
+ label = QLabel.new(@window)
54
+ label.set_text(text)
55
+ label.set_alignment(Qt::AlignCenter)
56
+ @layout.add_widget(label)
57
+ @items << label
58
+ label
59
+ end
60
+ end
61
+
62
+ console.define_singleton_method(:add_button) do |text = 'new button'|
63
+ gui do
64
+ button = QPushButton.new(@window)
65
+ button.set_text(text)
66
+ @layout.add_widget(button)
67
+ @items << button
68
+ button
69
+ end
70
+ end
71
+
72
+ console.define_singleton_method(:remove_last) do
73
+ gui do
74
+ widget = @items.pop
75
+ if widget
76
+ @layout.remove_widget(widget)
77
+ widget.hide if widget.respond_to?(:hide)
78
+ widget
79
+ end
80
+ end
81
+ end
82
+
83
+ console.define_singleton_method(:help) do
84
+ puts 'Examples:'
85
+ puts ' add_label("Header")'
86
+ puts ' add_button("Run")'
87
+ puts ' remove_last'
88
+ puts ' gui { window.resize(900, 600) }'
89
+ puts ' items'
90
+ puts ' exit'
91
+ end
92
+
93
+ window.show
94
+ QApplication.process_events
95
+
96
+ puts 'Starting IRB with live layout editor.'
97
+ puts 'Objects: app, window, layout, items'
98
+ console.help
99
+
100
+ irb_thread = Thread.new do
101
+ IRB.setup(nil)
102
+ workspace = IRB::WorkSpace.new(console.instance_eval { binding })
103
+ irb = IRB::Irb.new(workspace)
104
+ IRB.conf[:MAIN_CONTEXT] = irb.context
105
+
106
+ catch(:IRB_EXIT) { irb.eval_input }
107
+ ensure
108
+ jobs << :__exit__
109
+ end
110
+
111
+ # TODO: Replace manual process_events polling with app.exec + QTimer-driven integration.
112
+ while running
113
+ loop do
114
+ job = jobs.pop(true)
115
+
116
+ if job == :__exit__
117
+ running = false
118
+ break
119
+ end
120
+
121
+ block, reply = job
122
+ begin
123
+ reply << [true, block.call]
124
+ rescue StandardError => e
125
+ reply << [false, e]
126
+ end
127
+ rescue ThreadError
128
+ break
129
+ end
130
+
131
+ QApplication.process_events
132
+ running = false if window.is_visible.zero?
133
+ sleep(0.01)
134
+ end
135
+
136
+ irb_thread.kill if irb_thread&.alive?
137
+ app.dispose