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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -0
  3. data/Rakefile +120 -22
  4. data/ext/teek/extconf.rb +19 -1
  5. data/ext/teek/tcltkbridge.c +38 -2
  6. data/ext/teek/tcltkbridge.h +3 -0
  7. data/ext/teek/tkdrop.c +66 -0
  8. data/ext/teek/tkdrop.h +26 -0
  9. data/ext/teek/tkdrop_macos.m +141 -0
  10. data/ext/teek/tkdrop_win.c +232 -0
  11. data/ext/teek/tkdrop_x11.c +337 -0
  12. data/ext/teek/tkwin.c +42 -0
  13. data/lib/teek/platform.rb +29 -0
  14. data/lib/teek/version.rb +1 -1
  15. data/lib/teek.rb +49 -3
  16. data/teek.gemspec +3 -2
  17. metadata +7 -53
  18. data/sample/calculator.rb +0 -255
  19. data/sample/debug_demo.rb +0 -43
  20. data/sample/gamepad_viewer/assets/controller.png +0 -0
  21. data/sample/gamepad_viewer/gamepad_viewer.rb +0 -554
  22. data/sample/goldberg.rb +0 -1803
  23. data/sample/goldberg_helpers.rb +0 -170
  24. data/sample/optcarrot/thwaite.nes +0 -0
  25. data/sample/optcarrot/vendor/optcarrot/apu.rb +0 -856
  26. data/sample/optcarrot/vendor/optcarrot/config.rb +0 -257
  27. data/sample/optcarrot/vendor/optcarrot/cpu.rb +0 -1162
  28. data/sample/optcarrot/vendor/optcarrot/driver.rb +0 -144
  29. data/sample/optcarrot/vendor/optcarrot/mapper/cnrom.rb +0 -14
  30. data/sample/optcarrot/vendor/optcarrot/mapper/mmc1.rb +0 -105
  31. data/sample/optcarrot/vendor/optcarrot/mapper/mmc3.rb +0 -153
  32. data/sample/optcarrot/vendor/optcarrot/mapper/uxrom.rb +0 -14
  33. data/sample/optcarrot/vendor/optcarrot/nes.rb +0 -105
  34. data/sample/optcarrot/vendor/optcarrot/opt.rb +0 -168
  35. data/sample/optcarrot/vendor/optcarrot/pad.rb +0 -92
  36. data/sample/optcarrot/vendor/optcarrot/palette.rb +0 -65
  37. data/sample/optcarrot/vendor/optcarrot/ppu.rb +0 -1468
  38. data/sample/optcarrot/vendor/optcarrot/rom.rb +0 -143
  39. data/sample/optcarrot/vendor/optcarrot.rb +0 -14
  40. data/sample/optcarrot.rb +0 -354
  41. data/sample/paint/assets/bucket.png +0 -0
  42. data/sample/paint/assets/cursor.png +0 -0
  43. data/sample/paint/assets/eraser.png +0 -0
  44. data/sample/paint/assets/pencil.png +0 -0
  45. data/sample/paint/assets/spray.png +0 -0
  46. data/sample/paint/layer.rb +0 -255
  47. data/sample/paint/layer_manager.rb +0 -179
  48. data/sample/paint/paint_demo.rb +0 -837
  49. data/sample/paint/sparse_pixel_buffer.rb +0 -202
  50. data/sample/sdl2_demo.rb +0 -318
  51. data/sample/threading_demo.rb +0 -494
  52. data/sample/yam/assets/MINESWEEPER_0.png +0 -0
  53. data/sample/yam/assets/MINESWEEPER_1.png +0 -0
  54. data/sample/yam/assets/MINESWEEPER_2.png +0 -0
  55. data/sample/yam/assets/MINESWEEPER_3.png +0 -0
  56. data/sample/yam/assets/MINESWEEPER_4.png +0 -0
  57. data/sample/yam/assets/MINESWEEPER_5.png +0 -0
  58. data/sample/yam/assets/MINESWEEPER_6.png +0 -0
  59. data/sample/yam/assets/MINESWEEPER_7.png +0 -0
  60. data/sample/yam/assets/MINESWEEPER_8.png +0 -0
  61. data/sample/yam/assets/MINESWEEPER_F.png +0 -0
  62. data/sample/yam/assets/MINESWEEPER_M.png +0 -0
  63. data/sample/yam/assets/MINESWEEPER_X.png +0 -0
  64. data/sample/yam/assets/click.wav +0 -0
  65. data/sample/yam/assets/explosion.wav +0 -0
  66. data/sample/yam/assets/flag.wav +0 -0
  67. data/sample/yam/assets/music.mp3 +0 -0
  68. data/sample/yam/assets/sweep.wav +0 -0
  69. data/sample/yam/yam.rb +0 -587
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8fe2f5c480c472d6479ac1b92088a748e1216cb1a15472e9e98fbd3c2e4fd347
4
- data.tar.gz: c4d2da92180136ca756955258ea59402b27b9019e9d492f0969a7c346dcc053a
3
+ metadata.gz: b0f6bffdcb09e1d16bac89ae6d8a5595b72e33b0ae1e1d8b3a69bbad54e1cadc
4
+ data.tar.gz: 3d7ef3fe7168abd1f618577e01d02dfd9a43a91f2fe2a1fd27bff05c9cb73377
5
5
  SHA512:
6
- metadata.gz: 6a10e7f92ca08b1385288829cf0188beb408c90cdf52aecb30a36652f2b299954318c4b3a64434ea0158950f05e105db5a289c115dab4506fa91b7ab6e70f26e
7
- data.tar.gz: 7cd908cf513d6f92cad5c89d04cb49f7bf3fb3e9d7868af4d0d0fbb7db04e2a7a712c3e6e1e2fd107fd4df4b84140b49d720b2357cc9b9452067a8f5b7bfe2f6
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
- # Conditionally load rake-compiler
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:teek_sdl2'
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 all tests (teek + teek-sdl2) with coverage and generate report"
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 both test suites with coverage enabled and distinct COVERAGE_NAMEs
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')
@@ -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 = dlsym(RTLD_DEFAULT, "Tcl_FindExecutable");
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 = dlsym(RTLD_DEFAULT, "Tcl_CreateInterp");
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);
@@ -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
+ }