teek 0.1.3 → 0.1.5
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/Gemfile +2 -0
- data/README.md +47 -0
- data/Rakefile +55 -27
- data/ext/teek/extconf.rb +34 -1
- data/ext/teek/tcltkbridge.c +38 -2
- data/ext/teek/tcltkbridge.h +3 -0
- data/ext/teek/tkdrop.c +66 -0
- data/ext/teek/tkdrop.h +26 -0
- data/ext/teek/tkdrop_macos.m +141 -0
- data/ext/teek/tkdrop_win.c +232 -0
- data/ext/teek/tkdrop_x11.c +337 -0
- data/ext/teek/tkwin.c +42 -0
- data/lib/teek/platform.rb +29 -0
- data/lib/teek/version.rb +1 -1
- data/lib/teek.rb +49 -3
- data/teek.gemspec +4 -3
- metadata +8 -54
- data/sample/calculator.rb +0 -255
- data/sample/debug_demo.rb +0 -43
- data/sample/gamepad_viewer/assets/controller.png +0 -0
- data/sample/gamepad_viewer/gamepad_viewer.rb +0 -554
- data/sample/goldberg.rb +0 -1803
- data/sample/goldberg_helpers.rb +0 -170
- data/sample/optcarrot/thwaite.nes +0 -0
- data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
- data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
- data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
- data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
- data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
- data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
- data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
- data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
- data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
- data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
- data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
- data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
- data/sample/optcarrot/vendor/optcarrot.rb +0 -14
- data/sample/optcarrot.rb +0 -354
- data/sample/paint/assets/bucket.png +0 -0
- data/sample/paint/assets/cursor.png +0 -0
- data/sample/paint/assets/eraser.png +0 -0
- data/sample/paint/assets/pencil.png +0 -0
- data/sample/paint/assets/spray.png +0 -0
- data/sample/paint/layer.rb +0 -255
- data/sample/paint/layer_manager.rb +0 -179
- data/sample/paint/paint_demo.rb +0 -837
- data/sample/paint/sparse_pixel_buffer.rb +0 -202
- data/sample/sdl2_demo.rb +0 -318
- data/sample/threading_demo.rb +0 -494
- data/sample/yam/assets/MINESWEEPER_0.png +0 -0
- data/sample/yam/assets/MINESWEEPER_1.png +0 -0
- data/sample/yam/assets/MINESWEEPER_2.png +0 -0
- data/sample/yam/assets/MINESWEEPER_3.png +0 -0
- data/sample/yam/assets/MINESWEEPER_4.png +0 -0
- data/sample/yam/assets/MINESWEEPER_5.png +0 -0
- data/sample/yam/assets/MINESWEEPER_6.png +0 -0
- data/sample/yam/assets/MINESWEEPER_7.png +0 -0
- data/sample/yam/assets/MINESWEEPER_8.png +0 -0
- data/sample/yam/assets/MINESWEEPER_F.png +0 -0
- data/sample/yam/assets/MINESWEEPER_M.png +0 -0
- data/sample/yam/assets/MINESWEEPER_X.png +0 -0
- data/sample/yam/assets/click.wav +0 -0
- data/sample/yam/assets/explosion.wav +0 -0
- data/sample/yam/assets/flag.wav +0 -0
- data/sample/yam/assets/music.mp3 +0 -0
- data/sample/yam/assets/sweep.wav +0 -0
- data/sample/yam/yam.rb +0 -587
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7fdcad5500b3501e3d2b83d29678b76f8cc043b2f44e5e0af66a300fd8fa03ac
|
|
4
|
+
data.tar.gz: ca0b209ebe033d7e246271bda5491c8192279e063fdb5e45f2bb719fcd0cb5dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d69b03be50a1ad912ba7f8f0610ae7ba6163b19bc342a9f763112a467ddef39f2b8d0e5e2317463c91096d02674f6641d345370c35c09b71f37055d40ceab926
|
|
7
|
+
data.tar.gz: b720547bd7a11f6a3f7c15ec33c78d2c86c42e80ea9b273dd50803e5d2012c6dd286ae2ebd30275ca9b85eaa26866da00df8d0982a21cde97361d8fa98a1df10
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -102,6 +102,27 @@ Two other control flow signals are available for advanced use:
|
|
|
102
102
|
|
|
103
103
|
If a callback raises a Ruby exception, it becomes a Tcl error. The exception message is preserved and can be caught on the Tcl side with `catch`.
|
|
104
104
|
|
|
105
|
+
## Menus
|
|
106
|
+
|
|
107
|
+
Build a menu bar with standard `menu` widgets:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
app = Teek::App.new(title: 'My App')
|
|
111
|
+
|
|
112
|
+
app.command(:menu, '.menubar')
|
|
113
|
+
app.command('.', :configure, menu: '.menubar')
|
|
114
|
+
|
|
115
|
+
app.command(:menu, '.menubar.file', tearoff: 0)
|
|
116
|
+
app.command('.menubar', :add, :cascade, label: 'File', menu: '.menubar.file')
|
|
117
|
+
app.command('.menubar.file', :add, :command,
|
|
118
|
+
label: 'Quit', command: proc { app.command(:destroy, '.') })
|
|
119
|
+
|
|
120
|
+
app.show
|
|
121
|
+
app.mainloop
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
> **macOS note:** On macOS, Tk always displays a menu bar. If you don't configure one, Tk shows a default menu with items like "Run Widget Demo" that are meant for the Tcl interpreter shell. Attach a custom menu bar (even an empty one) to suppress it. See the [TkDocs menu tutorial](https://tkdocs.com/tutorial/menus.html) for details.
|
|
125
|
+
|
|
105
126
|
## List operations
|
|
106
127
|
|
|
107
128
|
Convert between Ruby arrays and Tcl list strings:
|
|
@@ -221,3 +242,29 @@ task.stop # Stop completely
|
|
|
221
242
|
The work block runs in a background Ractor and cannot access Tk directly. Use `t.yield()` to send results to `on_progress`, which runs on the main thread where Tk is available. Callbacks (`on_progress`, `on_done`) can be chained in any order.
|
|
222
243
|
|
|
223
244
|
See [`sample/threading_demo.rb`](sample/threading_demo.rb) for a complete file hasher example.
|
|
245
|
+
|
|
246
|
+
## File Drop Target
|
|
247
|
+
|
|
248
|
+
Register any widget as a file drop target to receive OS-native drag-and-drop:
|
|
249
|
+
|
|
250
|
+
```ruby
|
|
251
|
+
app = Teek::App.new(title: "Drop Demo")
|
|
252
|
+
app.show
|
|
253
|
+
|
|
254
|
+
app.register_drop_target('.')
|
|
255
|
+
|
|
256
|
+
app.bind('.', '<<DropFile>>', :data) do |data|
|
|
257
|
+
paths = app.split_list(data)
|
|
258
|
+
puts "Dropped: #{paths.inspect}"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
app.mainloop
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Dropped files arrive as a Tcl list in the `:data` substitution. Use `split_list` to get a Ruby array of paths. Works on macOS (Cocoa), Windows (OLE IDropTarget), and Linux (X11 XDND).
|
|
265
|
+
|
|
266
|
+
See [`sample/drop_demo.rb`](sample/drop_demo.rb) for a complete example.
|
|
267
|
+
|
|
268
|
+
## Known Issues
|
|
269
|
+
|
|
270
|
+
- **File drop on Linux/Wayland** — `register_drop_target` does not yet work under Wayland. The current implementation uses the X11 XDND protocol, which is not compatible with Wayland's native drag-and-drop. Workaround: select an Xorg/X11 session at the login screen (e.g., "GNOME on Xorg"). Native Wayland support via `wl_data_device` is planned.
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,9 @@ require "bundler/gem_tasks"
|
|
|
2
2
|
require 'rake/testtask'
|
|
3
3
|
require 'rake/clean'
|
|
4
4
|
|
|
5
|
+
# Sub-project Rakefiles (define sdl2:compile)
|
|
6
|
+
import 'teek-sdl2/Rakefile'
|
|
7
|
+
|
|
5
8
|
# Documentation tasks - all doc gems are in docs_site/Gemfile
|
|
6
9
|
namespace :docs do
|
|
7
10
|
desc "Install docs dependencies (docs_site/Gemfile)"
|
|
@@ -112,20 +115,15 @@ CLOBBER.include('teek-sdl2/lib/*.bundle', 'teek-sdl2/lib/*.so', 'teek-sdl2/ext/*
|
|
|
112
115
|
# Clean coverage artifacts before test runs to prevent accumulation
|
|
113
116
|
CLEAN.include('coverage/.resultset.json', 'coverage/results')
|
|
114
117
|
|
|
115
|
-
#
|
|
118
|
+
# rake compile = teek core only (tcltklib)
|
|
116
119
|
if Gem::Specification.find_all_by_name('rake-compiler').any?
|
|
117
120
|
require 'rake/extensiontask'
|
|
121
|
+
|
|
118
122
|
Rake::ExtensionTask.new do |ext|
|
|
119
123
|
ext.name = 'tcltklib'
|
|
120
124
|
ext.ext_dir = 'ext/teek'
|
|
121
125
|
ext.lib_dir = 'lib'
|
|
122
126
|
end
|
|
123
|
-
|
|
124
|
-
Rake::ExtensionTask.new do |ext|
|
|
125
|
-
ext.name = 'teek_sdl2'
|
|
126
|
-
ext.ext_dir = 'teek-sdl2/ext/teek_sdl2'
|
|
127
|
-
ext.lib_dir = 'teek-sdl2/lib'
|
|
128
|
-
end
|
|
129
127
|
end
|
|
130
128
|
|
|
131
129
|
namespace :screenshots do
|
|
@@ -209,55 +207,55 @@ end
|
|
|
209
207
|
|
|
210
208
|
task test: [:compile, :clean_coverage]
|
|
211
209
|
|
|
212
|
-
def detect_platform
|
|
213
|
-
case RUBY_PLATFORM
|
|
214
|
-
when /darwin/ then 'darwin'
|
|
215
|
-
when /linux/ then 'linux'
|
|
216
|
-
when /mingw|mswin/ then 'windows'
|
|
217
|
-
else 'unknown'
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
|
|
221
210
|
namespace :sdl2 do
|
|
222
|
-
desc "Compile teek-sdl2 C extension"
|
|
223
|
-
task compile: 'compile:teek_sdl2'
|
|
224
|
-
|
|
225
211
|
Rake::TestTask.new(:test) do |t|
|
|
226
212
|
t.libs << 'teek-sdl2/test' << 'teek-sdl2/lib'
|
|
227
213
|
t.test_files = FileList['teek-sdl2/test/**/test_*.rb'] - FileList['teek-sdl2/test/test_helper.rb']
|
|
228
214
|
t.ruby_opts << '-r test_helper'
|
|
229
215
|
t.verbose = true
|
|
230
216
|
end
|
|
231
|
-
task test: 'compile
|
|
217
|
+
task test: 'sdl2:compile'
|
|
232
218
|
end
|
|
233
219
|
|
|
234
220
|
task :default => :compile
|
|
235
221
|
|
|
236
222
|
namespace :release do
|
|
237
|
-
desc "
|
|
223
|
+
desc "Clean-slate smoke test: clobber, build gems, install fresh, verify"
|
|
238
224
|
task :smoke do
|
|
239
225
|
require 'tmpdir'
|
|
240
226
|
require 'fileutils'
|
|
241
227
|
|
|
228
|
+
# Clean slate — nuke compiled extensions so nothing local leaks in
|
|
229
|
+
puts "Clobbering local build artifacts..."
|
|
230
|
+
Rake::Task['clobber'].invoke
|
|
231
|
+
Rake::Task['sdl2:clobber'].invoke
|
|
232
|
+
|
|
242
233
|
Dir.mktmpdir('teek-smoke') do |tmpdir|
|
|
243
234
|
gem_home = File.join(tmpdir, 'gems')
|
|
244
235
|
|
|
245
236
|
# Build both gems
|
|
246
|
-
puts "
|
|
237
|
+
puts "\nBuilding gems..."
|
|
247
238
|
sh "gem build teek.gemspec -o #{tmpdir}/teek.gem 2>&1"
|
|
248
239
|
Dir.chdir('teek-sdl2') { sh "gem build teek-sdl2.gemspec -o #{tmpdir}/teek-sdl2.gem 2>&1" }
|
|
249
240
|
|
|
250
|
-
# Install into isolated GEM_HOME
|
|
251
|
-
puts "\nInstalling gems..."
|
|
241
|
+
# Install into isolated GEM_HOME (no system gems, no stale versions)
|
|
242
|
+
puts "\nInstalling gems into #{gem_home}..."
|
|
252
243
|
sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek.gem --no-document 2>&1"
|
|
253
244
|
sh "GEM_HOME=#{gem_home} gem install #{tmpdir}/teek-sdl2.gem --no-document 2>&1"
|
|
254
245
|
|
|
255
246
|
# Run smoke test using only the installed gems (no -I, no bundle)
|
|
256
|
-
puts "\nRunning
|
|
247
|
+
puts "\nRunning smoke test..."
|
|
257
248
|
smoke = <<~'RUBY'
|
|
258
249
|
require "teek"
|
|
259
250
|
require "teek/sdl2"
|
|
260
251
|
|
|
252
|
+
# Verify native extensions loaded from gem path, not local source
|
|
253
|
+
%w[tcltklib teek_sdl2].each do |ext|
|
|
254
|
+
path = $LOADED_FEATURES.find { |f| f.include?(ext) && f.end_with?(".bundle", ".so", ".dll") }
|
|
255
|
+
abort "#{ext}: native extension not found in $LOADED_FEATURES" unless path
|
|
256
|
+
abort "#{ext}: loaded from local source (#{path}), not installed gem" if path.include?("/ext/")
|
|
257
|
+
end
|
|
258
|
+
|
|
261
259
|
app = Teek::App.new
|
|
262
260
|
app.set_window_title("Release Smoke Test")
|
|
263
261
|
app.set_window_geometry("320x240")
|
|
@@ -266,6 +264,7 @@ namespace :release do
|
|
|
266
264
|
|
|
267
265
|
vp = Teek::SDL2::Viewport.new(app, width: 300, height: 200)
|
|
268
266
|
vp.pack
|
|
267
|
+
app.update
|
|
269
268
|
|
|
270
269
|
vp.render do |r|
|
|
271
270
|
r.clear(30, 30, 30)
|
|
@@ -280,7 +279,7 @@ namespace :release do
|
|
|
280
279
|
|
|
281
280
|
app.after(500) { vp.destroy; app.destroy }
|
|
282
281
|
app.mainloop
|
|
283
|
-
puts "
|
|
282
|
+
puts "release:smoke OK — teek #{Teek::VERSION}, teek-sdl2 #{Teek::SDL2::VERSION}"
|
|
284
283
|
RUBY
|
|
285
284
|
|
|
286
285
|
smoke_file = File.join(tmpdir, 'smoke.rb')
|
|
@@ -301,6 +300,15 @@ namespace :docker do
|
|
|
301
300
|
ruby_version == '4.0' ? base : "#{base}-ruby#{ruby_version}"
|
|
302
301
|
end
|
|
303
302
|
|
|
303
|
+
def warn_if_containers_running(image_name)
|
|
304
|
+
running = `docker ps --filter ancestor=#{image_name} --format '{{.ID}} {{.Status}}'`.strip
|
|
305
|
+
return if running.empty?
|
|
306
|
+
count = running.lines.size
|
|
307
|
+
warn "\n⚠ #{count} container(s) already running on #{image_name}:"
|
|
308
|
+
running.lines.each { |l| warn " #{l.strip}" }
|
|
309
|
+
warn " This usually means a previous test suite is stuck. Consider: docker kill $(docker ps -q --filter ancestor=#{image_name})\n"
|
|
310
|
+
end
|
|
311
|
+
|
|
304
312
|
def tcl_version_from_env
|
|
305
313
|
version = ENV.fetch('TCL_VERSION', '9.0')
|
|
306
314
|
unless ['8.6', '9.0'].include?(version)
|
|
@@ -345,6 +353,8 @@ namespace :docker do
|
|
|
345
353
|
require 'fileutils'
|
|
346
354
|
FileUtils.mkdir_p('coverage')
|
|
347
355
|
|
|
356
|
+
warn_if_containers_running(image_name)
|
|
357
|
+
|
|
348
358
|
puts "Running tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
349
359
|
cmd = "docker run --rm --init"
|
|
350
360
|
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
@@ -374,6 +384,22 @@ namespace :docker do
|
|
|
374
384
|
sh cmd
|
|
375
385
|
end
|
|
376
386
|
|
|
387
|
+
desc "Force rebuild Docker image (no cache)"
|
|
388
|
+
task :rebuild do
|
|
389
|
+
tcl_version = tcl_version_from_env
|
|
390
|
+
ruby_version = ruby_version_from_env
|
|
391
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
392
|
+
|
|
393
|
+
puts "Rebuilding Docker image (no cache) for Ruby #{ruby_version}, Tcl #{tcl_version}..."
|
|
394
|
+
cmd = "docker build -f #{DOCKERFILE} --no-cache"
|
|
395
|
+
cmd += " --label #{DOCKER_LABEL}"
|
|
396
|
+
cmd += " --build-arg RUBY_VERSION=#{ruby_version}"
|
|
397
|
+
cmd += " --build-arg TCL_VERSION=#{tcl_version}"
|
|
398
|
+
cmd += " -t #{image_name} ."
|
|
399
|
+
|
|
400
|
+
sh cmd
|
|
401
|
+
end
|
|
402
|
+
|
|
377
403
|
desc "Remove dangling Docker images from teek builds"
|
|
378
404
|
task :prune do
|
|
379
405
|
sh "docker image prune -f --filter label=#{DOCKER_LABEL}"
|
|
@@ -391,6 +417,8 @@ namespace :docker do
|
|
|
391
417
|
require 'fileutils'
|
|
392
418
|
FileUtils.mkdir_p('coverage')
|
|
393
419
|
|
|
420
|
+
warn_if_containers_running(image_name)
|
|
421
|
+
|
|
394
422
|
puts "Running teek-sdl2 tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
395
423
|
cmd = "docker run --rm --init"
|
|
396
424
|
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
@@ -416,7 +444,7 @@ namespace :docker do
|
|
|
416
444
|
FileUtils.rm_rf('coverage')
|
|
417
445
|
FileUtils.mkdir_p('coverage/results')
|
|
418
446
|
|
|
419
|
-
# Run
|
|
447
|
+
# Run all three test suites with coverage enabled and distinct COVERAGE_NAMEs
|
|
420
448
|
ENV['COVERAGE'] = '1'
|
|
421
449
|
|
|
422
450
|
ENV['COVERAGE_NAME'] = 'main'
|
data/ext/teek/extconf.rb
CHANGED
|
@@ -26,6 +26,21 @@ def find_tcltk
|
|
|
26
26
|
if File.exist?("#{inc}/tcl-tk/tcl.h")
|
|
27
27
|
inc = "#{inc}/tcl-tk"
|
|
28
28
|
end
|
|
29
|
+
|
|
30
|
+
# Check for versioned subdirectories (Debian/Ubuntu layout:
|
|
31
|
+
# /usr/include/tcl9.0/tcl.h, /usr/include/tcl8.6/tcl.h)
|
|
32
|
+
unless File.exist?("#{inc}/tcl.h")
|
|
33
|
+
versioned = Dir.glob("#{inc}/tcl*/tcl.h").max
|
|
34
|
+
if versioned
|
|
35
|
+
tcl_ver_dir = File.dirname(versioned)
|
|
36
|
+
tk_ver_dir = tcl_ver_dir.sub(/tcl/, 'tk')
|
|
37
|
+
$INCFLAGS << " -I#{tcl_ver_dir}"
|
|
38
|
+
$INCFLAGS << " -I#{tk_ver_dir}" if File.directory?(tk_ver_dir)
|
|
39
|
+
$LDFLAGS << " -L#{lib}"
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
29
44
|
if File.exist?("#{inc}/tcl.h") && File.exist?("#{inc}/tk.h")
|
|
30
45
|
$INCFLAGS << " -I#{inc}"
|
|
31
46
|
$LDFLAGS << " -L#{lib}"
|
|
@@ -69,11 +84,29 @@ def find_tcltk
|
|
|
69
84
|
|
|
70
85
|
abort "Tcl stub library not found" unless tcl_stub
|
|
71
86
|
abort "Tk stub library not found" unless tk_stub
|
|
87
|
+
|
|
88
|
+
# Also link the real Tcl shared library so it's loaded at runtime.
|
|
89
|
+
# Some distros (e.g. Fedora) ship Tcl symbols in a separate .so that
|
|
90
|
+
# stubs alone don't pull in, causing dlsym() to fail at bootstrap.
|
|
91
|
+
have_library('tcl9.0') || have_library('tcl8.6') || have_library('tcl')
|
|
72
92
|
end
|
|
73
93
|
|
|
74
94
|
find_tcltk
|
|
75
95
|
|
|
76
96
|
# Source files for the extension
|
|
77
|
-
$srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c', 'tkeventsource.c']
|
|
97
|
+
$srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c', 'tkeventsource.c', 'tkdrop.c']
|
|
98
|
+
|
|
99
|
+
# Platform-specific file drop target
|
|
100
|
+
case RbConfig::CONFIG['host_os']
|
|
101
|
+
when /darwin/
|
|
102
|
+
$srcs << 'tkdrop_macos.m'
|
|
103
|
+
$LDFLAGS << ' -framework Cocoa'
|
|
104
|
+
when /mingw|mswin|cygwin/
|
|
105
|
+
$srcs << 'tkdrop_win.c'
|
|
106
|
+
have_library('ole32')
|
|
107
|
+
have_library('shell32')
|
|
108
|
+
else
|
|
109
|
+
$srcs << 'tkdrop_x11.c'
|
|
110
|
+
end
|
|
78
111
|
|
|
79
112
|
create_makefile('tcltklib')
|
data/ext/teek/tcltkbridge.c
CHANGED
|
@@ -59,6 +59,39 @@ static void *get_tcl_proc(const char *name)
|
|
|
59
59
|
}
|
|
60
60
|
#endif
|
|
61
61
|
|
|
62
|
+
/* Try to dlopen the real Tcl shared library if it's not already loaded.
|
|
63
|
+
* Some distros (e.g. Fedora) ship Tcl/Tk in separate .so files and only
|
|
64
|
+
* the Tk combined library gets pulled in via stubs, leaving Tcl_CreateInterp
|
|
65
|
+
* unresolvable via RTLD_DEFAULT. */
|
|
66
|
+
#ifndef _WIN32
|
|
67
|
+
static void *tcl_dlhandle = NULL;
|
|
68
|
+
|
|
69
|
+
static void *
|
|
70
|
+
dlsym_tcl(const char *name)
|
|
71
|
+
{
|
|
72
|
+
void *sym = dlsym(RTLD_DEFAULT, name);
|
|
73
|
+
if (sym) return sym;
|
|
74
|
+
|
|
75
|
+
/* Try loading the real Tcl library explicitly */
|
|
76
|
+
if (!tcl_dlhandle) {
|
|
77
|
+
static const char *lib_names[] = {
|
|
78
|
+
"libtcl9.0.so", "libtcl8.6.so",
|
|
79
|
+
"libtcl9.0.dylib", "libtcl8.6.dylib",
|
|
80
|
+
NULL
|
|
81
|
+
};
|
|
82
|
+
int i;
|
|
83
|
+
for (i = 0; lib_names[i]; i++) {
|
|
84
|
+
tcl_dlhandle = dlopen(lib_names[i], RTLD_NOW | RTLD_GLOBAL);
|
|
85
|
+
if (tcl_dlhandle) break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (tcl_dlhandle) {
|
|
89
|
+
sym = dlsym(tcl_dlhandle, name);
|
|
90
|
+
}
|
|
91
|
+
return sym;
|
|
92
|
+
}
|
|
93
|
+
#endif
|
|
94
|
+
|
|
62
95
|
static void
|
|
63
96
|
find_executable_bootstrap(const char *argv0)
|
|
64
97
|
{
|
|
@@ -71,7 +104,7 @@ find_executable_bootstrap(const char *argv0)
|
|
|
71
104
|
#ifdef _WIN32
|
|
72
105
|
real_find_executable = (void (*)(const char *))get_tcl_proc("Tcl_FindExecutable");
|
|
73
106
|
#else
|
|
74
|
-
real_find_executable =
|
|
107
|
+
real_find_executable = dlsym_tcl("Tcl_FindExecutable");
|
|
75
108
|
#endif
|
|
76
109
|
if (real_find_executable) {
|
|
77
110
|
real_find_executable(argv0);
|
|
@@ -89,7 +122,7 @@ create_interp_bootstrap(void)
|
|
|
89
122
|
#ifdef _WIN32
|
|
90
123
|
real_create_interp = (Tcl_Interp *(*)(void))get_tcl_proc("Tcl_CreateInterp");
|
|
91
124
|
#else
|
|
92
|
-
real_create_interp =
|
|
125
|
+
real_create_interp = dlsym_tcl("Tcl_CreateInterp");
|
|
93
126
|
#endif
|
|
94
127
|
if (!real_create_interp) {
|
|
95
128
|
return NULL;
|
|
@@ -1490,6 +1523,9 @@ Init_tcltklib(void)
|
|
|
1490
1523
|
/* External event source integration (tkeventsource.c) */
|
|
1491
1524
|
Init_tkeventsource(mTeek);
|
|
1492
1525
|
|
|
1526
|
+
/* File drop target support (tkdrop.c) */
|
|
1527
|
+
Init_tkdrop(cInterp);
|
|
1528
|
+
|
|
1493
1529
|
/* Class methods for instance tracking */
|
|
1494
1530
|
rb_define_singleton_method(cInterp, "instance_count", tcltkip_instance_count, 0);
|
|
1495
1531
|
rb_define_singleton_method(cInterp, "instances", tcltkip_instances, 0);
|
data/ext/teek/tcltkbridge.h
CHANGED
|
@@ -42,4 +42,7 @@ void Init_tkwin(VALUE cInterp);
|
|
|
42
42
|
/* External event source integration - defined in tkeventsource.c */
|
|
43
43
|
void Init_tkeventsource(VALUE mTeek);
|
|
44
44
|
|
|
45
|
+
/* File drop target support - defined in tkdrop.c */
|
|
46
|
+
void Init_tkdrop(VALUE cInterp);
|
|
47
|
+
|
|
45
48
|
#endif /* TCLTKBRIDGE_H */
|
data/ext/teek/tkdrop.c
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* tkdrop.c - File drop target support (common entry point)
|
|
2
|
+
*
|
|
3
|
+
* Provides Interp#register_drop_target(window_path) which delegates
|
|
4
|
+
* to platform-specific teek_register_drop_target().
|
|
5
|
+
*
|
|
6
|
+
* Based on tkdnd (https://github.com/petasis/tkdnd) as reference.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
#include "tcltkbridge.h"
|
|
10
|
+
#include "tkdrop.h"
|
|
11
|
+
|
|
12
|
+
/* ---------------------------------------------------------
|
|
13
|
+
* Interp#register_drop_target(window_path)
|
|
14
|
+
*
|
|
15
|
+
* Register a Tk widget as a native file drop target.
|
|
16
|
+
* After registration, dropping a file onto the widget generates
|
|
17
|
+
* a <<DropFile>> virtual event with the file path in -data.
|
|
18
|
+
*
|
|
19
|
+
* Arguments:
|
|
20
|
+
* window_path - Tk widget path (e.g., ".", ".frame")
|
|
21
|
+
*
|
|
22
|
+
* Returns: nil
|
|
23
|
+
* --------------------------------------------------------- */
|
|
24
|
+
|
|
25
|
+
static VALUE
|
|
26
|
+
interp_register_drop_target(VALUE self, VALUE window_path)
|
|
27
|
+
{
|
|
28
|
+
struct tcltk_interp *tip = get_interp(self);
|
|
29
|
+
Tk_Window mainWin;
|
|
30
|
+
Tk_Window tkwin;
|
|
31
|
+
int result;
|
|
32
|
+
|
|
33
|
+
StringValue(window_path);
|
|
34
|
+
|
|
35
|
+
mainWin = Tk_MainWindow(tip->interp);
|
|
36
|
+
if (!mainWin) {
|
|
37
|
+
rb_raise(eTclError, "Tk not initialized (no main window)");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
tkwin = Tk_NameToWindow(tip->interp, StringValueCStr(window_path), mainWin);
|
|
41
|
+
if (!tkwin) {
|
|
42
|
+
rb_raise(eTclError, "window not found: %s", StringValueCStr(window_path));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
Tk_MakeWindowExist(tkwin);
|
|
46
|
+
|
|
47
|
+
result = teek_register_drop_target(tip->interp, tkwin, StringValueCStr(window_path));
|
|
48
|
+
if (result != TCL_OK) {
|
|
49
|
+
rb_raise(eTclError, "failed to register drop target: %s",
|
|
50
|
+
Tcl_GetStringResult(tip->interp));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return Qnil;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* ---------------------------------------------------------
|
|
57
|
+
* Init_tkdrop - Register drop target methods on Interp
|
|
58
|
+
*
|
|
59
|
+
* Called from Init_tcltklib in tcltkbridge.c.
|
|
60
|
+
* --------------------------------------------------------- */
|
|
61
|
+
|
|
62
|
+
void
|
|
63
|
+
Init_tkdrop(VALUE cInterp)
|
|
64
|
+
{
|
|
65
|
+
rb_define_method(cInterp, "register_drop_target", interp_register_drop_target, 1);
|
|
66
|
+
}
|
data/ext/teek/tkdrop.h
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* tkdrop.h - Native file drop target support
|
|
2
|
+
*
|
|
3
|
+
* Cross-platform abstraction for registering Tk widgets as file drop targets.
|
|
4
|
+
* Each platform implements teek_register_drop_target() which hooks into the
|
|
5
|
+
* OS drag-and-drop system and generates <<DropFile>> virtual events.
|
|
6
|
+
*
|
|
7
|
+
* Based on tkdnd (https://github.com/petasis/tkdnd) as reference.
|
|
8
|
+
* See THIRD_PARTY_NOTICES for attribution.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#ifndef TKDROP_H
|
|
12
|
+
#define TKDROP_H
|
|
13
|
+
|
|
14
|
+
#include <tcl.h>
|
|
15
|
+
#include <tk.h>
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Register a Tk window as a file drop target.
|
|
19
|
+
* When a file is dropped, generates <<DropFile>> with -data set to the path.
|
|
20
|
+
*
|
|
21
|
+
* Returns TCL_OK on success, TCL_ERROR on failure (with error in interp).
|
|
22
|
+
*/
|
|
23
|
+
int teek_register_drop_target(Tcl_Interp *interp, Tk_Window tkwin,
|
|
24
|
+
const char *widget_path);
|
|
25
|
+
|
|
26
|
+
#endif /* TKDROP_H */
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/* tkdrop_macos.m - macOS file drop target via Cocoa NSDraggingDestination
|
|
2
|
+
*
|
|
3
|
+
* Based on tkdnd (https://github.com/petasis/tkdnd) as reference.
|
|
4
|
+
* See THIRD_PARTY_NOTICES for attribution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
#import <Cocoa/Cocoa.h>
|
|
8
|
+
#include <tcl.h>
|
|
9
|
+
#include <tk.h>
|
|
10
|
+
#include "tkdrop.h"
|
|
11
|
+
|
|
12
|
+
#ifndef MAC_OSX_TK
|
|
13
|
+
#define MAC_OSX_TK
|
|
14
|
+
#endif
|
|
15
|
+
#include "tkPlatDecls.h"
|
|
16
|
+
|
|
17
|
+
/* --------------------------------------------------------- */
|
|
18
|
+
|
|
19
|
+
@interface TeekDropView : NSView <NSDraggingDestination>
|
|
20
|
+
{
|
|
21
|
+
Tcl_Interp *_interp;
|
|
22
|
+
char *_widgetPath;
|
|
23
|
+
}
|
|
24
|
+
- (instancetype)initWithFrame:(NSRect)frame
|
|
25
|
+
interp:(Tcl_Interp *)interp
|
|
26
|
+
widgetPath:(const char *)widgetPath;
|
|
27
|
+
@end
|
|
28
|
+
|
|
29
|
+
@implementation TeekDropView
|
|
30
|
+
|
|
31
|
+
- (instancetype)initWithFrame:(NSRect)frame
|
|
32
|
+
interp:(Tcl_Interp *)interp
|
|
33
|
+
widgetPath:(const char *)widgetPath
|
|
34
|
+
{
|
|
35
|
+
self = [super initWithFrame:frame];
|
|
36
|
+
if (self) {
|
|
37
|
+
_interp = interp;
|
|
38
|
+
_widgetPath = strdup(widgetPath);
|
|
39
|
+
[self setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable];
|
|
40
|
+
[self registerForDraggedTypes:@[NSPasteboardTypeFileURL]];
|
|
41
|
+
}
|
|
42
|
+
return self;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (void)dealloc
|
|
46
|
+
{
|
|
47
|
+
free(_widgetPath);
|
|
48
|
+
[super dealloc];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender
|
|
52
|
+
{
|
|
53
|
+
NSPasteboard *pb = [sender draggingPasteboard];
|
|
54
|
+
if ([pb canReadObjectForClasses:@[[NSURL class]]
|
|
55
|
+
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}]) {
|
|
56
|
+
return NSDragOperationCopy;
|
|
57
|
+
}
|
|
58
|
+
return NSDragOperationNone;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
- (BOOL)performDragOperation:(id<NSDraggingInfo>)sender
|
|
62
|
+
{
|
|
63
|
+
NSPasteboard *pb = [sender draggingPasteboard];
|
|
64
|
+
NSArray<NSURL *> *urls = [pb readObjectsForClasses:@[[NSURL class]]
|
|
65
|
+
options:@{NSPasteboardURLReadingFileURLsOnlyKey: @YES}];
|
|
66
|
+
if (!urls || [urls count] == 0) {
|
|
67
|
+
return NO;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* Build a Tcl list of all dropped file paths */
|
|
71
|
+
Tcl_Obj *listObj = Tcl_NewListObj(0, NULL);
|
|
72
|
+
Tcl_IncrRefCount(listObj);
|
|
73
|
+
|
|
74
|
+
for (NSURL *url in urls) {
|
|
75
|
+
NSString *path = [url path];
|
|
76
|
+
if (!path) continue;
|
|
77
|
+
Tcl_ListObjAppendElement(NULL, listObj,
|
|
78
|
+
Tcl_NewStringObj([path UTF8String], -1));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Generate single <<DropFile>> event with all paths as a Tcl list */
|
|
82
|
+
Tcl_Obj *script = Tcl_ObjPrintf(
|
|
83
|
+
"event generate %s <<DropFile>> -data {%s}",
|
|
84
|
+
_widgetPath, Tcl_GetString(listObj));
|
|
85
|
+
Tcl_IncrRefCount(script);
|
|
86
|
+
Tcl_EvalObjEx(_interp, script, TCL_EVAL_GLOBAL);
|
|
87
|
+
Tcl_DecrRefCount(script);
|
|
88
|
+
Tcl_DecrRefCount(listObj);
|
|
89
|
+
|
|
90
|
+
return YES;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/* Allow the drop view to be transparent to mouse events when not dragging */
|
|
94
|
+
- (NSView *)hitTest:(NSPoint)point
|
|
95
|
+
{
|
|
96
|
+
return nil;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@end
|
|
100
|
+
|
|
101
|
+
/* --------------------------------------------------------- */
|
|
102
|
+
|
|
103
|
+
int
|
|
104
|
+
teek_register_drop_target(Tcl_Interp *interp, Tk_Window tkwin,
|
|
105
|
+
const char *widget_path)
|
|
106
|
+
{
|
|
107
|
+
Drawable drawable = Tk_WindowId(tkwin);
|
|
108
|
+
if (!drawable) {
|
|
109
|
+
Tcl_SetResult(interp, "window has no native handle", TCL_STATIC);
|
|
110
|
+
return TCL_ERROR;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
void *nswindow = Tk_MacOSXGetNSWindowForDrawable(drawable);
|
|
114
|
+
if (!nswindow) {
|
|
115
|
+
Tcl_SetResult(interp, "could not get NSWindow", TCL_STATIC);
|
|
116
|
+
return TCL_ERROR;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
NSWindow *window = (__bridge NSWindow *)nswindow;
|
|
120
|
+
NSView *contentView = [window contentView];
|
|
121
|
+
if (!contentView) {
|
|
122
|
+
Tcl_SetResult(interp, "could not get content view", TCL_STATIC);
|
|
123
|
+
return TCL_ERROR;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Check if we already registered a drop view on this window */
|
|
127
|
+
for (NSView *subview in [contentView subviews]) {
|
|
128
|
+
if ([subview isKindOfClass:[TeekDropView class]]) {
|
|
129
|
+
return TCL_OK; /* Already registered */
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
TeekDropView *dropView = [[TeekDropView alloc]
|
|
134
|
+
initWithFrame:[contentView bounds]
|
|
135
|
+
interp:interp
|
|
136
|
+
widgetPath:widget_path];
|
|
137
|
+
|
|
138
|
+
[contentView addSubview:dropView];
|
|
139
|
+
|
|
140
|
+
return TCL_OK;
|
|
141
|
+
}
|