teek 0.1.3 → 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 +4 -4
- data/README.md +21 -0
- data/Rakefile +120 -22
- data/ext/teek/extconf.rb +19 -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 +3 -2
- metadata +7 -53
- 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: b0f6bffdcb09e1d16bac89ae6d8a5595b72e33b0ae1e1d8b3a69bbad54e1cadc
|
|
4
|
+
data.tar.gz: 3d7ef3fe7168abd1f618577e01d02dfd9a43a91f2fe2a1fd27bff05c9cb73377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6604babdbafe5641544ea288c017722a5c08660445945e715dac47335849165bb3245b2b024c5067fd8013f14cc9aba43965fea5703dc9ff91f71e8e438ab518
|
|
7
|
+
data.tar.gz: 8e146399dbc27889ed3284e1de07bfffcc6e83637afb6bc69ab22120410e18e23fcf35f5b0d5e6670dc0ca5062d0ecc6f505fac26cea0c610f628103d065b07e
|
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:
|
data/Rakefile
CHANGED
|
@@ -2,6 +2,10 @@ require "bundler/gem_tasks"
|
|
|
2
2
|
require 'rake/testtask'
|
|
3
3
|
require 'rake/clean'
|
|
4
4
|
|
|
5
|
+
# Sub-project Rakefiles (define sdl2:compile, mgba:compile)
|
|
6
|
+
import 'teek-sdl2/Rakefile'
|
|
7
|
+
import 'teek-mgba/Rakefile'
|
|
8
|
+
|
|
5
9
|
# Documentation tasks - all doc gems are in docs_site/Gemfile
|
|
6
10
|
namespace :docs do
|
|
7
11
|
desc "Install docs dependencies (docs_site/Gemfile)"
|
|
@@ -108,24 +112,20 @@ task yard: 'docs:yard'
|
|
|
108
112
|
CLEAN.include('ext/teek/config_list')
|
|
109
113
|
CLOBBER.include('tmp', 'lib/*.bundle', 'lib/*.so', 'ext/**/*.o', 'ext/**/*.bundle', 'ext/**/*.bundle.dSYM')
|
|
110
114
|
CLOBBER.include('teek-sdl2/lib/*.bundle', 'teek-sdl2/lib/*.so', 'teek-sdl2/ext/**/*.o', 'teek-sdl2/ext/**/*.bundle')
|
|
115
|
+
CLOBBER.include('teek-mgba/lib/*.bundle', 'teek-mgba/lib/*.so', 'teek-mgba/ext/**/*.o', 'teek-mgba/ext/**/*.bundle')
|
|
111
116
|
|
|
112
117
|
# Clean coverage artifacts before test runs to prevent accumulation
|
|
113
118
|
CLEAN.include('coverage/.resultset.json', 'coverage/results')
|
|
114
119
|
|
|
115
|
-
#
|
|
120
|
+
# rake compile = teek core only (tcltklib)
|
|
116
121
|
if Gem::Specification.find_all_by_name('rake-compiler').any?
|
|
117
122
|
require 'rake/extensiontask'
|
|
123
|
+
|
|
118
124
|
Rake::ExtensionTask.new do |ext|
|
|
119
125
|
ext.name = 'tcltklib'
|
|
120
126
|
ext.ext_dir = 'ext/teek'
|
|
121
127
|
ext.lib_dir = 'lib'
|
|
122
128
|
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
129
|
end
|
|
130
130
|
|
|
131
131
|
namespace :screenshots do
|
|
@@ -209,26 +209,65 @@ end
|
|
|
209
209
|
|
|
210
210
|
task test: [:compile, :clean_coverage]
|
|
211
211
|
|
|
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
212
|
namespace :sdl2 do
|
|
222
|
-
desc "Compile teek-sdl2 C extension"
|
|
223
|
-
task compile: 'compile:teek_sdl2'
|
|
224
|
-
|
|
225
213
|
Rake::TestTask.new(:test) do |t|
|
|
226
214
|
t.libs << 'teek-sdl2/test' << 'teek-sdl2/lib'
|
|
227
215
|
t.test_files = FileList['teek-sdl2/test/**/test_*.rb'] - FileList['teek-sdl2/test/test_helper.rb']
|
|
228
216
|
t.ruby_opts << '-r test_helper'
|
|
229
217
|
t.verbose = true
|
|
230
218
|
end
|
|
231
|
-
task test: 'compile
|
|
219
|
+
task test: 'sdl2:compile'
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
namespace :mgba do
|
|
223
|
+
Rake::TestTask.new(:test) do |t|
|
|
224
|
+
t.libs << 'teek-mgba/test' << 'teek-mgba/lib' << 'teek-sdl2/lib'
|
|
225
|
+
t.test_files = FileList['teek-mgba/test/**/test_*.rb'] - FileList['teek-mgba/test/test_helper.rb']
|
|
226
|
+
t.ruby_opts << '-r test_helper'
|
|
227
|
+
t.verbose = true
|
|
228
|
+
end
|
|
229
|
+
task test: ['mgba:compile', 'sdl2:compile']
|
|
230
|
+
|
|
231
|
+
desc "Download and build libmgba from source (for macOS / platforms without libmgba-dev)"
|
|
232
|
+
task :deps do
|
|
233
|
+
require 'fileutils'
|
|
234
|
+
require 'etc'
|
|
235
|
+
|
|
236
|
+
vendor_dir = File.expand_path('teek-mgba/vendor')
|
|
237
|
+
mgba_src = File.join(vendor_dir, 'mgba')
|
|
238
|
+
build_dir = File.join(vendor_dir, 'build')
|
|
239
|
+
install_dir = File.join(vendor_dir, 'install')
|
|
240
|
+
|
|
241
|
+
unless File.directory?(mgba_src)
|
|
242
|
+
FileUtils.mkdir_p(vendor_dir)
|
|
243
|
+
sh "git clone --depth 1 --branch 0.10.3 https://github.com/mgba-emu/mgba.git #{mgba_src}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
FileUtils.mkdir_p(build_dir)
|
|
247
|
+
cmake_flags = %W[
|
|
248
|
+
-DBUILD_SHARED=OFF
|
|
249
|
+
-DBUILD_STATIC=ON
|
|
250
|
+
-DBUILD_QT=OFF
|
|
251
|
+
-DBUILD_SDL=OFF
|
|
252
|
+
-DBUILD_GL=OFF
|
|
253
|
+
-DBUILD_GLES2=OFF
|
|
254
|
+
-DBUILD_GLES3=OFF
|
|
255
|
+
-DBUILD_LIBRETRO=OFF
|
|
256
|
+
-DSKIP_FRONTEND=ON
|
|
257
|
+
-DUSE_SQLITE3=OFF
|
|
258
|
+
-DUSE_ELF=OFF
|
|
259
|
+
-DUSE_LZMA=OFF
|
|
260
|
+
-DUSE_EDITLINE=OFF
|
|
261
|
+
-DCMAKE_INSTALL_PREFIX=#{install_dir}
|
|
262
|
+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
|
|
263
|
+
].join(' ')
|
|
264
|
+
|
|
265
|
+
sh "cmake -S #{mgba_src} -B #{build_dir} #{cmake_flags}"
|
|
266
|
+
sh "cmake --build #{build_dir} -j #{Etc.nprocessors}"
|
|
267
|
+
sh "cmake --install #{build_dir}"
|
|
268
|
+
|
|
269
|
+
puts "libmgba built and installed to #{install_dir}"
|
|
270
|
+
end
|
|
232
271
|
end
|
|
233
272
|
|
|
234
273
|
task :default => :compile
|
|
@@ -301,6 +340,15 @@ namespace :docker do
|
|
|
301
340
|
ruby_version == '4.0' ? base : "#{base}-ruby#{ruby_version}"
|
|
302
341
|
end
|
|
303
342
|
|
|
343
|
+
def warn_if_containers_running(image_name)
|
|
344
|
+
running = `docker ps --filter ancestor=#{image_name} --format '{{.ID}} {{.Status}}'`.strip
|
|
345
|
+
return if running.empty?
|
|
346
|
+
count = running.lines.size
|
|
347
|
+
warn "\n⚠ #{count} container(s) already running on #{image_name}:"
|
|
348
|
+
running.lines.each { |l| warn " #{l.strip}" }
|
|
349
|
+
warn " This usually means a previous test suite is stuck. Consider: docker kill $(docker ps -q --filter ancestor=#{image_name})\n"
|
|
350
|
+
end
|
|
351
|
+
|
|
304
352
|
def tcl_version_from_env
|
|
305
353
|
version = ENV.fetch('TCL_VERSION', '9.0')
|
|
306
354
|
unless ['8.6', '9.0'].include?(version)
|
|
@@ -345,6 +393,8 @@ namespace :docker do
|
|
|
345
393
|
require 'fileutils'
|
|
346
394
|
FileUtils.mkdir_p('coverage')
|
|
347
395
|
|
|
396
|
+
warn_if_containers_running(image_name)
|
|
397
|
+
|
|
348
398
|
puts "Running tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
349
399
|
cmd = "docker run --rm --init"
|
|
350
400
|
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
@@ -374,6 +424,22 @@ namespace :docker do
|
|
|
374
424
|
sh cmd
|
|
375
425
|
end
|
|
376
426
|
|
|
427
|
+
desc "Force rebuild Docker image (no cache)"
|
|
428
|
+
task :rebuild do
|
|
429
|
+
tcl_version = tcl_version_from_env
|
|
430
|
+
ruby_version = ruby_version_from_env
|
|
431
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
432
|
+
|
|
433
|
+
puts "Rebuilding Docker image (no cache) for Ruby #{ruby_version}, Tcl #{tcl_version}..."
|
|
434
|
+
cmd = "docker build -f #{DOCKERFILE} --no-cache"
|
|
435
|
+
cmd += " --label #{DOCKER_LABEL}"
|
|
436
|
+
cmd += " --build-arg RUBY_VERSION=#{ruby_version}"
|
|
437
|
+
cmd += " --build-arg TCL_VERSION=#{tcl_version}"
|
|
438
|
+
cmd += " -t #{image_name} ."
|
|
439
|
+
|
|
440
|
+
sh cmd
|
|
441
|
+
end
|
|
442
|
+
|
|
377
443
|
desc "Remove dangling Docker images from teek builds"
|
|
378
444
|
task :prune do
|
|
379
445
|
sh "docker image prune -f --filter label=#{DOCKER_LABEL}"
|
|
@@ -391,6 +457,8 @@ namespace :docker do
|
|
|
391
457
|
require 'fileutils'
|
|
392
458
|
FileUtils.mkdir_p('coverage')
|
|
393
459
|
|
|
460
|
+
warn_if_containers_running(image_name)
|
|
461
|
+
|
|
394
462
|
puts "Running teek-sdl2 tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
395
463
|
cmd = "docker run --rm --init"
|
|
396
464
|
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
@@ -406,7 +474,32 @@ namespace :docker do
|
|
|
406
474
|
sh cmd
|
|
407
475
|
end
|
|
408
476
|
|
|
409
|
-
desc "Run
|
|
477
|
+
desc "Run teek-mgba tests in Docker"
|
|
478
|
+
task mgba: :build do
|
|
479
|
+
tcl_version = tcl_version_from_env
|
|
480
|
+
ruby_version = ruby_version_from_env
|
|
481
|
+
image_name = docker_image_name(tcl_version, ruby_version)
|
|
482
|
+
|
|
483
|
+
require 'fileutils'
|
|
484
|
+
FileUtils.mkdir_p('coverage')
|
|
485
|
+
|
|
486
|
+
warn_if_containers_running(image_name)
|
|
487
|
+
|
|
488
|
+
puts "Running teek-mgba tests in Docker (Ruby #{ruby_version}, Tcl #{tcl_version})..."
|
|
489
|
+
cmd = "docker run --rm --init"
|
|
490
|
+
cmd += " -v #{Dir.pwd}/coverage:/app/coverage"
|
|
491
|
+
cmd += " -e TCL_VERSION=#{tcl_version}"
|
|
492
|
+
if ENV['COVERAGE'] == '1'
|
|
493
|
+
cmd += " -e COVERAGE=1"
|
|
494
|
+
cmd += " -e COVERAGE_NAME=#{ENV['COVERAGE_NAME'] || 'mgba'}"
|
|
495
|
+
end
|
|
496
|
+
cmd += " #{image_name}"
|
|
497
|
+
cmd += " xvfb-run -a bundle exec rake mgba:test"
|
|
498
|
+
|
|
499
|
+
sh cmd
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
desc "Run all tests (teek + teek-sdl2 + teek-mgba) with coverage and generate report"
|
|
410
503
|
task all: 'docker:build' do
|
|
411
504
|
tcl_version = tcl_version_from_env
|
|
412
505
|
ruby_version = ruby_version_from_env
|
|
@@ -416,7 +509,7 @@ namespace :docker do
|
|
|
416
509
|
FileUtils.rm_rf('coverage')
|
|
417
510
|
FileUtils.mkdir_p('coverage/results')
|
|
418
511
|
|
|
419
|
-
# Run
|
|
512
|
+
# Run all three test suites with coverage enabled and distinct COVERAGE_NAMEs
|
|
420
513
|
ENV['COVERAGE'] = '1'
|
|
421
514
|
|
|
422
515
|
ENV['COVERAGE_NAME'] = 'main'
|
|
@@ -427,6 +520,11 @@ namespace :docker do
|
|
|
427
520
|
Rake::Task['docker:build'].reenable
|
|
428
521
|
Rake::Task['docker:test:sdl2'].invoke
|
|
429
522
|
|
|
523
|
+
ENV['COVERAGE_NAME'] = 'mgba'
|
|
524
|
+
Rake::Task['docker:test:mgba'].reenable
|
|
525
|
+
Rake::Task['docker:build'].reenable
|
|
526
|
+
Rake::Task['docker:test:mgba'].invoke
|
|
527
|
+
|
|
430
528
|
# Collate inside Docker (paths match /app/lib/...)
|
|
431
529
|
puts "Collating coverage results..."
|
|
432
530
|
cmd = "docker run --rm --init"
|
data/ext/teek/extconf.rb
CHANGED
|
@@ -69,11 +69,29 @@ def find_tcltk
|
|
|
69
69
|
|
|
70
70
|
abort "Tcl stub library not found" unless tcl_stub
|
|
71
71
|
abort "Tk stub library not found" unless tk_stub
|
|
72
|
+
|
|
73
|
+
# Also link the real Tcl shared library so it's loaded at runtime.
|
|
74
|
+
# Some distros (e.g. Fedora) ship Tcl symbols in a separate .so that
|
|
75
|
+
# stubs alone don't pull in, causing dlsym() to fail at bootstrap.
|
|
76
|
+
have_library('tcl9.0') || have_library('tcl8.6') || have_library('tcl')
|
|
72
77
|
end
|
|
73
78
|
|
|
74
79
|
find_tcltk
|
|
75
80
|
|
|
76
81
|
# Source files for the extension
|
|
77
|
-
$srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c', 'tkeventsource.c']
|
|
82
|
+
$srcs = ['tcltkbridge.c', 'tkphoto.c', 'tkfont.c', 'tkwin.c', 'tkeventsource.c', 'tkdrop.c']
|
|
83
|
+
|
|
84
|
+
# Platform-specific file drop target
|
|
85
|
+
case RbConfig::CONFIG['host_os']
|
|
86
|
+
when /darwin/
|
|
87
|
+
$srcs << 'tkdrop_macos.m'
|
|
88
|
+
$LDFLAGS << ' -framework Cocoa'
|
|
89
|
+
when /mingw|mswin|cygwin/
|
|
90
|
+
$srcs << 'tkdrop_win.c'
|
|
91
|
+
have_library('ole32')
|
|
92
|
+
have_library('shell32')
|
|
93
|
+
else
|
|
94
|
+
$srcs << 'tkdrop_x11.c'
|
|
95
|
+
end
|
|
78
96
|
|
|
79
97
|
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
|
+
}
|