-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathPakettiFuzzySampleSearch.lua
More file actions
1502 lines (1321 loc) · 53.8 KB
/
PakettiFuzzySampleSearch.lua
File metadata and controls
1502 lines (1321 loc) · 53.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
-- PakettiFuzzySampleSearch.lua
-- Fuzzy Sample Search Dialog for Paketti
-- Recursive directory scanning with fuzzy search and quick loading
local dialog = nil
local current_files = {}
local filtered_files = {}
local selected_index = 1
local search_query = ""
local current_directory = ""
local max_results_per_column = 30
local columns_count = 5
local current_page = 1
local results_per_page = 150 -- Maximum buttons to create at once
local max_safe_results_per_page = 50 -- CRITICAL FIX: Much smaller for very large file sets to prevent unresponsiveness
-- Removed esc_just_closed_dialog flag - no longer needed since ESC doesn't close dialog when search is empty
-- Colors for button highlighting (from PakettiGater.lua)
local normal_color = {0, 0, 0} -- Default black
local selected_color = {0x80, 0x00, 0x80} -- Deep purple for selected
-- Optimization: Cache search results and pagination data
local cached_search_query = ""
local cached_search_results = {}
local cached_pagination_info = {total_results = 0, max_pages = 1}
-- ViewBuilder instance for use in update functions
local vb = renoise.ViewBuilder()
-- Store button references for efficient updates
local file_buttons = {}
-- Supported file extensions
local supported_extensions = {
"wav", "flac", "aiff", "aif", "ogg", "mp3", "m4a", "mp4",
"xrni", "sf2", "rex", "rx2", "pti", "iti", "iff", "8svx", "16sv", "snd"
}
-- Cache system variables
local cache_filename = "paketti_fuzzy_sample_cache.json"
-- Get the last used directory from preferences
function PakettiFuzzySampleSearchGetLastDirectory()
return preferences.PakettiFuzzySampleSearchPath.value or ""
end
-- Save the current directory to preferences
function PakettiFuzzySampleSearchSaveLastDirectory(directory)
preferences.PakettiFuzzySampleSearchPath.value = directory
preferences:save_as("preferences.xml")
end
-- Optimized: Create extension lookup table for O(1) checking
local supported_ext_lookup = {}
for _, ext in ipairs(supported_extensions) do
supported_ext_lookup[ext] = true
end
-- Check if file has supported extension (optimized)
function PakettiFuzzySampleSearchIsSupported(filename)
local ext = filename:match("%.([^%.]+)$")
if not ext then return false end
return supported_ext_lookup[ext:lower()] == true
end
-- Optimized bulk file processing
function PakettiFuzzySampleSearchProcessFilesBulk(filepaths, directory, start_idx, end_idx)
local results = {}
local directory_prefix = directory .. "/"
local dir_len = #directory_prefix
-- Pre-escape directory pattern once
local dir_pattern = "^" .. directory:gsub("([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1") .. "/?$"
for i = start_idx, end_idx do
local filepath = filepaths[i]
local filename = filepath:match("[^/]+$")
if filename then
local ext = filename:match("%.([^%.]+)$")
if ext and supported_ext_lookup[ext:lower()] then
-- Fast relative path calculation
local rel_path = filepath:gsub(dir_pattern, "")
if rel_path == filepath then
-- Fallback to substring method if gsub didn't work
if filepath:sub(1, dir_len) == directory_prefix then
rel_path = filepath:sub(dir_len + 1)
else
rel_path = filename
end
end
results[#results + 1] = {
name = filename,
full_path = filepath,
relative_path = rel_path,
display_name = rel_path
}
end
end
end
return results
end
-- Get cache file path
function PakettiFuzzySampleSearchGetCacheFilePath(directory)
-- Create a unique cache filename based on directory path
local dir_hash = ""
for i = 1, #directory do
dir_hash = dir_hash .. string.byte(directory:sub(i, i))
end
return renoise.tool().bundle_path .. "cache_" .. dir_hash .. ".json"
end
-- Get directory modification time (using stat command)
function PakettiFuzzySampleSearchGetDirectoryModTime(directory)
local success, result = pcall(function()
local handle = io.popen('stat -f "%m" "' .. directory .. '" 2>/dev/null')
if handle then
local modtime = handle:read("*l")
handle:close()
return tonumber(modtime) or 0
end
return 0
end)
return success and result or 0
end
-- Save cache to file
function PakettiFuzzySampleSearchSaveCache(directory, files)
local cache_data = {
directory = directory,
directory_modtime = PakettiFuzzySampleSearchGetDirectoryModTime(directory),
cache_time = os.time(),
files = files
}
local cache_file_path = PakettiFuzzySampleSearchGetCacheFilePath(directory)
local success, err = pcall(function()
local file = io.open(cache_file_path, "w")
if file then
-- Simple JSON-like serialization
local cache_str = "{\n"
cache_str = cache_str .. ' "directory": "' .. directory:gsub('\\', '\\\\'):gsub('"', '\\"') .. '",\n'
cache_str = cache_str .. ' "directory_modtime": ' .. cache_data.directory_modtime .. ',\n'
cache_str = cache_str .. ' "cache_time": ' .. cache_data.cache_time .. ',\n'
cache_str = cache_str .. ' "file_count": ' .. #files .. ',\n'
cache_str = cache_str .. ' "files": [\n'
for i, file_data in ipairs(files) do
cache_str = cache_str .. ' {\n'
cache_str = cache_str .. ' "name": "' .. file_data.name:gsub('\\', '\\\\'):gsub('"', '\\"') .. '",\n'
cache_str = cache_str .. ' "full_path": "' .. file_data.full_path:gsub('\\', '\\\\'):gsub('"', '\\"') .. '",\n'
cache_str = cache_str .. ' "relative_path": "' .. file_data.relative_path:gsub('\\', '\\\\'):gsub('"', '\\"') .. '",\n'
cache_str = cache_str .. ' "display_name": "' .. file_data.display_name:gsub('\\', '\\\\'):gsub('"', '\\"') .. '"\n'
cache_str = cache_str .. ' }' .. (i < #files and ',' or '') .. '\n'
end
cache_str = cache_str .. ' ]\n'
cache_str = cache_str .. '}\n'
file:write(cache_str)
file:close()
renoise.app():show_status("Cache saved: " .. #files .. " files cached for future use")
end
end)
if not success then
print("Failed to save cache: " .. tostring(err))
end
end
-- Load cache from file
function PakettiFuzzySampleSearchLoadCache(directory)
local cache_file_path = PakettiFuzzySampleSearchGetCacheFilePath(directory)
local success, cache_data = pcall(function()
local file = io.open(cache_file_path, "r")
if not file then return nil end
local content = file:read("*a")
file:close()
-- Simple JSON parsing for our specific format
local cache = {}
cache.directory = content:match('"directory":%s*"([^"]*)"')
cache.directory_modtime = tonumber(content:match('"directory_modtime":%s*(%d+)'))
cache.cache_time = tonumber(content:match('"cache_time":%s*(%d+)'))
cache.file_count = tonumber(content:match('"file_count":%s*(%d+)'))
-- Parse files array
cache.files = {}
local files_section = content:match('"files":%s*%[(.-)%]')
if files_section then
for file_obj in files_section:gmatch('{.-}') do
local file_data = {}
file_data.name = file_obj:match('"name":%s*"([^"]*)"') or ""
file_data.full_path = file_obj:match('"full_path":%s*"([^"]*)"') or ""
file_data.relative_path = file_obj:match('"relative_path":%s*"([^"]*)"') or ""
file_data.display_name = file_obj:match('"display_name":%s*"([^"]*)"') or ""
-- Unescape strings
file_data.name = file_data.name:gsub('\\"', '"'):gsub('\\\\', '\\')
file_data.full_path = file_data.full_path:gsub('\\"', '"'):gsub('\\\\', '\\')
file_data.relative_path = file_data.relative_path:gsub('\\"', '"'):gsub('\\\\', '\\')
file_data.display_name = file_data.display_name:gsub('\\"', '"'):gsub('\\\\', '\\')
table.insert(cache.files, file_data)
end
end
return cache
end)
return success and cache_data or nil
end
-- Check if cache is valid for directory
function PakettiFuzzySampleSearchIsCacheValid(directory)
local cache_data = PakettiFuzzySampleSearchLoadCache(directory)
if not cache_data then return false end
-- Check if directory matches
if cache_data.directory ~= directory then return false end
-- Check if directory was modified since cache was created
local current_modtime = PakettiFuzzySampleSearchGetDirectoryModTime(directory)
if current_modtime ~= cache_data.directory_modtime then return false end
-- Cache is valid if less than 24 hours old and directory hasn't changed
local cache_age = os.time() - (cache_data.cache_time or 0)
return cache_age < (24 * 60 * 60) -- 24 hours
end
-- Process Slicer scanning variables
local scan_process_slicer = nil
local scan_dialog = nil
local scan_vb = nil
-- Recursively scan directory for supported files using ProcessSlicer
function PakettiFuzzySampleSearchScanDirectory(directory, callback)
-- Check cache first
if PakettiFuzzySampleSearchIsCacheValid(directory) then
local cache_data = PakettiFuzzySampleSearchLoadCache(directory)
if cache_data and cache_data.files then
renoise.app():show_status("Loaded " .. #cache_data.files .. " files from cache (instant!)")
if callback then callback(cache_data.files) end
return
end
end
-- Cancel any existing scan
if scan_process_slicer and scan_process_slicer:running() then
scan_process_slicer:cancel()
scan_process_slicer:stop()
end
local files = {}
local total_processed = 0
local function scan_coroutine()
-- Optimized: Use shell command to pre-filter by extension for massive speedup
local extensions_pattern = "\\.(" .. table.concat(supported_extensions, "|") .. ")$"
local find_cmd = string.format('find "%s" -type f | grep -iE "%s" 2>/dev/null', directory, extensions_pattern)
local all_filepaths = {}
local success, items = pcall(function()
return io.popen(find_cmd):lines()
end)
if success then
for filepath in items do
all_filepaths[#all_filepaths + 1] = filepath
end
else
-- Fallback to original method if grep fails
success, items = pcall(function()
return io.popen('find "' .. directory .. '" -type f 2>/dev/null'):lines()
end)
if success then
for filepath in items do
all_filepaths[#all_filepaths + 1] = filepath
end
else
renoise.app():show_status("Could not scan directory. Please ensure the path is accessible.")
if callback then callback({}) end
return
end
end
local total_files = #all_filepaths
if total_files == 0 then
if callback then callback({}) end
return
end
-- Process files in massive chunks - no reason to be conservative
local chunk_size = 25000 -- Massive chunks for maximum performance
for i = 1, total_files, chunk_size do
-- Check for cancellation
if scan_process_slicer and scan_process_slicer:was_cancelled() then
renoise.app():show_status("Directory scan cancelled")
if scan_dialog and scan_dialog.visible then
scan_dialog:close()
scan_dialog = nil
end
return
end
-- Process chunk with optimized bulk function
local end_index = math.min(i + chunk_size - 1, total_files)
local chunk_results = PakettiFuzzySampleSearchProcessFilesBulk(all_filepaths, directory, i, end_index)
-- Bulk append results (much faster than individual inserts)
for _, file_data in ipairs(chunk_results) do
files[#files + 1] = file_data
end
total_processed = end_index
-- Update progress
if scan_dialog and scan_dialog.visible and scan_vb then
local progress_text = string.format("Scanning... %d/%d files (%d samples found)",
total_processed, total_files, #files)
scan_vb.views.progress_text.text = progress_text
end
-- Yield to maintain responsiveness
coroutine.yield()
end
-- Scan complete
if scan_dialog and scan_dialog.visible then
scan_dialog:close()
scan_dialog = nil
end
-- Save results to cache
if #files > 0 then
PakettiFuzzySampleSearchSaveCache(directory, files)
end
if callback then
callback(files)
end
renoise.app():show_status(string.format("Scan complete: Found %d samples", #files))
end
-- Start the process with progress dialog
scan_process_slicer = ProcessSlicer(scan_coroutine)
scan_dialog, scan_vb = scan_process_slicer:create_dialog("Scanning Directory...")
scan_process_slicer:start()
end
-- Optimization: Calculate and cache search results efficiently
function PakettiFuzzySampleSearchCalculateFilteredResults(force_recalc)
-- Only recalculate if search query changed or forced
if not force_recalc and search_query == cached_search_query then
return cached_search_results
end
cached_search_query = search_query
if search_query == "" then
cached_search_results = current_files
else
-- CRITICAL FIX: For very large file sets, use simple substring matching instead of fuzzy search
-- This is much faster and prevents timeouts with 185k+ files
if #current_files > 10000 then
cached_search_results = {}
local query_lower = search_query:lower()
-- Use more efficient string matching for large file sets
for _, file in ipairs(current_files) do
if file.display_name:lower():find(query_lower, 1, true) or
file.name:lower():find(query_lower, 1, true) then
table.insert(cached_search_results, file)
end
end
else
-- Use fuzzy search for smaller file sets
cached_search_results = PakettiFuzzySearchUtil(current_files, search_query, {
search_type = "substring",
field_extractor = function(file)
return {file.display_name, file.name}
end
})
end
end
-- Update pagination info - use appropriate page size for large file sets
cached_pagination_info.total_results = #cached_search_results
-- CRITICAL FIX: Use much smaller page sizes for very large file sets to prevent unresponsiveness
local effective_page_size = results_per_page
if #cached_search_results > 100000 then
effective_page_size = max_safe_results_per_page
elseif #cached_search_results > 50000 then
effective_page_size = 75 -- Medium page size for large file sets
end
cached_pagination_info.max_pages = math.ceil(cached_pagination_info.total_results / effective_page_size)
return cached_search_results
end
-- Update the file list display efficiently without recreating dialog
function PakettiFuzzySampleSearchUpdateDisplay(force_recalc)
if not dialog or not dialog.visible then return end
-- Get filtered results (uses cache if possible)
local all_filtered = PakettiFuzzySampleSearchCalculateFilteredResults(force_recalc)
-- Use cached pagination info
local total_results = cached_pagination_info.total_results
local max_pages = cached_pagination_info.max_pages
if current_page > max_pages then
current_page = max_pages
end
if current_page < 1 then
current_page = 1
end
-- Get current page of results - use smaller page size for very large file sets
local effective_results_per_page = results_per_page
if total_results > 100000 then
effective_results_per_page = max_safe_results_per_page
elseif total_results > 50000 then
effective_results_per_page = 75 -- Medium page size for large file sets
end
local start_index = ((current_page - 1) * effective_results_per_page) + 1
local end_index = math.min(start_index + effective_results_per_page - 1, total_results)
local page_files = {}
for i = start_index, end_index do
table.insert(page_files, all_filtered[i])
end
filtered_files = page_files
print("DEBUG: Pagination - current_page=" .. current_page .. ", start_index=" .. start_index .. ", end_index=" .. end_index .. ", page_files_count=" .. #page_files .. ", total_results=" .. total_results)
-- Ensure selected index is valid
if selected_index > #filtered_files then
selected_index = #filtered_files
end
if selected_index < 1 and #filtered_files > 0 then
selected_index = 1
end
-- CRITICAL FIX: Always use in-place updates to avoid dialog recreation
-- Dialog recreation is the main cause of unresponsiveness with large file sets
PakettiFuzzySampleSearchUpdateButtonsInPlace()
PakettiFuzzySampleSearchUpdateStatusInPlace()
PakettiFuzzySampleSearchUpdateSelection()
end
-- Get current popup value based on max_results_per_column setting
function PakettiFuzzySampleSearchGetCurrentPerColumnValue()
local items_options = {15, 20, 25, 30, 35, 40, 45, 50}
for i, option in ipairs(items_options) do
if option == max_results_per_column then
return i
end
end
return 4 -- Default to 30 if not found (index 4)
end
-- Get status text with pagination info
function PakettiFuzzySampleSearchGetStatusText()
-- Use cached results instead of recalculating
PakettiFuzzySampleSearchCalculateFilteredResults(false)
local total_filtered = cached_pagination_info.total_results
if total_filtered == 0 then
if #current_files > 0 then
return "No matches"
else
return "Choose directory to see files"
end
end
local max_pages = cached_pagination_info.max_pages
local showing_count = #filtered_files
if max_pages > 1 then
-- Calculate the actual range of files being displayed
local effective_results_per_page = results_per_page
if total_filtered > 100000 then
effective_results_per_page = max_safe_results_per_page
elseif total_filtered > 50000 then
effective_results_per_page = 75 -- Medium page size for large file sets
end
local start_index = ((current_page - 1) * effective_results_per_page) + 1
local end_index = math.min(start_index + showing_count - 1, total_filtered)
return string.format("Files: %d-%d/%d", start_index, end_index, total_filtered)
else
return string.format("Files: %d", total_filtered)
end
end
-- Create compact pagination controls for the control row
function PakettiFuzzySampleSearchCreatePaginationControlsCompact()
-- Use cached pagination info
local max_pages = cached_pagination_info.max_pages
if max_pages <= 1 then
return vb:space{width = 1} -- No pagination needed
end
return vb:row{
vb:button{
text = "<<",
width = 25,
active = current_page > 1,
notifier = function()
if current_page > 1 then
current_page = 1
selected_index = 1
PakettiFuzzySampleSearchUpdateDisplay(true)
end
end
},
vb:button{
text = "<",
width = 25,
active = current_page > 1,
notifier = function()
if current_page > 1 then
current_page = current_page - 1
selected_index = 1
PakettiFuzzySampleSearchUpdateDisplay(true)
end
end
},
vb:text{
id = "pagination_text",
text = string.format("P%d/%d", current_page, max_pages),
width = 40,
style = "strong",
font = "bold"
},
vb:button{
text = ">",
width = 25,
active = current_page < max_pages,
notifier = function()
if current_page < max_pages then
current_page = current_page + 1
selected_index = 1
PakettiFuzzySampleSearchUpdateDisplay(true)
end
end
},
vb:button{
text = ">>",
width = 25,
active = current_page < max_pages,
notifier = function()
if current_page < max_pages then
current_page = max_pages
selected_index = 1
PakettiFuzzySampleSearchUpdateDisplay(true)
end
end
}
}
end
-- Create the file display section
function PakettiFuzzySampleSearchCreateFileDisplay()
print("DEBUG: PakettiFuzzySampleSearchCreateFileDisplay called")
print("DEBUG: #filtered_files = " .. #filtered_files)
print("DEBUG: results_per_page = " .. results_per_page)
print("DEBUG: columns_count = " .. columns_count)
print("DEBUG: max_results_per_column = " .. max_results_per_column)
local file_columns = {}
-- CRITICAL FIX: Always create maximum possible buttons to avoid recreation
local max_buttons_needed = results_per_page
local files_to_display = math.min(#filtered_files, max_buttons_needed)
print("DEBUG: max_buttons_needed = " .. max_buttons_needed)
print("DEBUG: files_to_display = " .. files_to_display)
-- Create columns
for col = 1, columns_count do
local column_start = (col - 1) * max_results_per_column + 1
local column_end = math.min(col * max_results_per_column, max_buttons_needed)
print("DEBUG: Column " .. col .. " - start: " .. column_start .. ", end: " .. column_end)
if column_start <= max_buttons_needed then
local column_views = {}
-- Create buttons for this column (up to max_buttons_needed)
for i = column_start, column_end do
local file = filtered_files[i]
local is_selected = (i == selected_index)
local button_text = ""
if file then
button_text = file.display_name
print("DEBUG: Creating button " .. i .. " for file: " .. file.display_name)
else
-- Create empty button for future use
button_text = ""
print("DEBUG: Creating empty button " .. i .. " for future use")
end
-- Show full text when selected, truncate when not selected
if is_selected and file then
-- Selected: show full filename (but cap it for the narrower buttons)
if #button_text > 42 then
button_text = button_text:sub(1, 39) .. "..."
end
elseif file then
-- Not selected: truncate for consistent layout
if #button_text > 38 then
button_text = button_text:sub(1, 35) .. "..."
end
end
local button = vb:button{
text = button_text,
width = 240, -- Reasonable width without waste
height = 22,
align = "left", -- Left-align the filename text
font = "normal", -- Keep font consistent
color = is_selected and selected_color or normal_color,
visible = file ~= nil, -- Only show if we have a file
notifier = function()
if file then -- Only allow selection if file exists
selected_index = i
PakettiFuzzySampleSearchUpdateSelection()
end
end
}
-- Store button reference for efficient updates
file_buttons[i] = button
table.insert(column_views, button)
end
table.insert(file_columns, vb:column{
views = column_views
})
end
end
print("DEBUG: Created " .. #file_columns .. " columns")
print("DEBUG: Total file_buttons stored: " .. #file_buttons)
return vb:horizontal_aligner{
mode = "left",
vb:row{
views = file_columns
}
}
end
-- Create the main dialog content
function PakettiFuzzySampleSearchCreateDialog()
print("DEBUG: PakettiFuzzySampleSearchCreateDialog called")
print("DEBUG: #current_files = " .. #current_files)
print("DEBUG: #filtered_files = " .. #filtered_files)
-- Apply pagination to filtered_files before creating dialog
if #filtered_files > 0 then
local all_filtered = filtered_files -- Store all files
local total_results = #all_filtered
local max_pages = math.ceil(total_results / results_per_page)
if current_page > max_pages then
current_page = max_pages
end
if current_page < 1 then
current_page = 1
end
local effective_results_per_page = results_per_page
if total_results > 100000 then
effective_results_per_page = max_safe_results_per_page
end
local start_index = ((current_page - 1) * effective_results_per_page) + 1
local end_index = math.min(start_index + effective_results_per_page - 1, total_results)
local page_files = {}
for i = start_index, end_index do
table.insert(page_files, all_filtered[i])
end
filtered_files = page_files
print("DEBUG: Pagination applied - current_page=" .. current_page .. ", start_index=" .. start_index .. ", end_index=" .. end_index .. ", page_files_count=" .. #page_files .. ", total_results=" .. total_results)
end
-- Create a new ViewBuilder instance each time to avoid ID conflicts
vb = renoise.ViewBuilder()
-- Clear button references for fresh dialog
file_buttons = {}
local dialog_content = vb:column{
vb:row{
vb:text{
id = "current_directory",
text = current_directory ~= "" and ("Directory: " .. current_directory) or "No directory selected",
width = 400,
font = "bold",
style = "strong"
},
vb:text{
text = "Search:",
width = 50,
style = "strong",
font = "bold"
},
vb:text{
id = "search_display",
text = search_query == "" and "(type to search)" or search_query,
width = 200,
style = search_query == "" and "disabled" or "normal",
font = "mono"
}
},
vb:row{
vb:button{
text = "Browse...",
width = 70,
notifier = PakettiFuzzySampleSearchBrowseDirectory
},
current_directory ~= "" and vb:button{
text = "Scan",
tooltip = "Scan the displayed directory",
width = 50,
notifier = function()
if current_directory ~= "" then
-- Clear existing files and update display
current_files = {}
filtered_files = {}
search_query = ""
selected_index = 1
current_page = 1
-- Update status to show scanning state
if dialog and vb and vb.views.file_count then
vb.views.file_count.text = "Scanning directory..."
end
-- Scan directory with callback
PakettiFuzzySampleSearchScanDirectory(current_directory, function(files)
current_files = files
filtered_files = files
search_query = ""
selected_index = 1
current_page = 1
-- Set up search cache properly for loaded files
cached_search_query = ""
cached_search_results = files
local total_files = #files
local effective_page_size = (total_files > 100000) and max_safe_results_per_page or results_per_page
local max_pages = math.ceil(total_files / effective_page_size)
cached_pagination_info = {total_results = total_files, max_pages = max_pages}
if dialog and dialog.visible then
-- CRITICAL FIX: Use in-place updates instead of dialog recreation
PakettiFuzzySampleSearchUpdateDisplay(true)
end
end)
end
end
} or vb:space{width = 50},
current_directory ~= "" and vb:button{
text = "Clear Cache",
tooltip = "Force rescan by clearing cached results",
width = 70,
notifier = function()
local cache_file_path = PakettiFuzzySampleSearchGetCacheFilePath(current_directory)
local success = pcall(function()
os.remove(cache_file_path)
end)
if success then
renoise.app():show_status("Cache cleared - next scan will be fresh")
else
renoise.app():show_status("No cache to clear")
end
end
} or vb:space{width = 70},
vb:space{width = 1}, -- Spacer
vb:popup{
id = "columns_popup",
items = {"2 Columns", "3 Columns", "4 Columns", "5 Columns", "6 Columns"},
value = columns_count - 1, -- Convert to 1-based index (5 columns = index 4)
width = 80,
notifier = function(value)
columns_count = value + 1 -- Convert back to actual column count
-- CRITICAL FIX: Recreate dialog only when column count changes
-- This is necessary because the layout structure changes
if dialog and dialog.visible then
local was_visible = dialog.visible
dialog:close()
if was_visible then
PakettiFuzzySampleSearchCreateDialog()
end
end
end
},
vb:popup{
id = "items_per_column",
items = {"15 per column", "20 per column", "25 per column", "30 per column", "35 per column", "40 per column", "45 per column", "50 per column"},
value = PakettiFuzzySampleSearchGetCurrentPerColumnValue(), -- Set to current value
width = 90,
notifier = function(value)
local items_options = {15, 20, 25, 30, 35, 40, 45, 50}
max_results_per_column = items_options[value]
-- CRITICAL FIX: Recreate dialog only when items per column changes
-- This is necessary because the layout structure changes
if dialog and dialog.visible then
local was_visible = dialog.visible
dialog:close()
if was_visible then
PakettiFuzzySampleSearchCreateDialog()
end
end
end
},
PakettiFuzzySampleSearchCreatePaginationControlsCompact(),
vb:text{
id = "file_count",
text = PakettiFuzzySampleSearchGetStatusText(),
width = 150,
style = "strong",
font = "bold"
}
},
vb:text{
text = "Use ↑↓←→ to navigate, PageUp/PageDown for pages, Enter to load, Esc to clear search/close",
style = "disabled"
},
PakettiFuzzySampleSearchCreateFileDisplay(),
}
-- Create custom keyhandler that combines close functionality with navigation
local keyhandler = function(dialog_ref, key)
-- First handle our custom keys
local handled_key = PakettiFuzzySampleSearchKeyHandler(dialog_ref, key)
if handled_key == nil then
return nil -- Key was handled by our function, stop processing
end
-- If not handled by our function, check for dialog close preference
local closer = preferences.pakettiDialogClose.value
if key.modifiers == "" and key.name == closer then
dialog_ref:close()
dialog = nil
return nil
end
return key -- Pass through unhandled keys
end
dialog = renoise.app():show_custom_dialog("Paketti Fuzzy Sample Search", dialog_content, keyhandler)
-- Show initial selection in status bar
PakettiFuzzySampleSearchUpdateSelection()
end
-- Load the selected sample
function PakettiFuzzySampleSearchLoadSelected()
if selected_index < 1 or selected_index > #filtered_files then
renoise.app():show_status("No file selected")
return
end
local file = filtered_files[selected_index]
local file_path = file.full_path
local file_ext = file_path:match("%.([^%.]+)$"):lower()
-- Load different file types appropriately
if file_ext == "xrni" then
-- Load XRNI following Paketti conventions
local song = renoise.song()
if not safeInsertInstrumentAt(song, song.selected_instrument_index + 1) then return end
song.selected_instrument_index = song.selected_instrument_index + 1
-- Apply Paketti default instrument configuration before loading XRNI
if pakettiPreferencesDefaultInstrumentLoader then
pakettiPreferencesDefaultInstrumentLoader()
end
renoise.app():load_instrument(file_path)
-- Clean up any "Placeholder sample" left behind
local instrument = song.selected_instrument
for i = #instrument.samples, 1, -1 do
if instrument.samples[i].name == "Placeholder sample" then
instrument:delete_sample_at(i)
end
end
renoise.app():show_status("Loaded XRNI: " .. file.name)
elseif file_ext == "sf2" then
-- Load SF2 files using Paketti SF2 Loader
if import_sf2 then
import_sf2(file_path)
renoise.app():show_status("Loaded SF2: " .. file.name)
else
renoise.app():show_status("SF2 loader not available")
end
elseif file_ext == "rex" or file_ext == "rx2" then
-- Load REX/RX2 files using rx2_loadsample function
if rx2_loadsample then
rx2_loadsample(file_path)
renoise.app():show_status("Loaded " .. file_ext:upper() .. ": " .. file.name)
else
renoise.app():show_status(file_ext:upper() .. " loader not available")
end
elseif file_ext == "pti" then
-- Load PTI files using Paketti PTI Loader
if pti_loadsample then
pti_loadsample(file_path)
renoise.app():show_status("Loaded PTI: " .. file.name)
else
renoise.app():show_status("PTI loader not available")
end
elseif file_ext == "iti" then
-- Load ITI files using Paketti ITI Loader
if iti_loadinstrument then
iti_loadinstrument(file_path)
renoise.app():show_status("Loaded ITI: " .. file.name)
else
renoise.app():show_status("ITI loader not available")
end
elseif file_ext == "iff" or file_ext == "8svx" or file_ext == "16sv" then
-- Load IFF/8SVX/16SV files using Paketti IFF Loader
if loadIFFSample then
loadIFFSample(file_path)
renoise.app():show_status("Loaded IFF: " .. file.name)
else
renoise.app():show_status("IFF loader not available")
end
elseif file_ext == "snd" then
-- Load MPC2000 SND files using Paketti MPC2000 Loader
if importMPC2000Sample then
importMPC2000Sample(file_path)
renoise.app():show_status("Loaded SND: " .. file.name)
else
renoise.app():show_status("MPC2000 SND loader not available")
end
else
-- Load regular audio files (wav, flac, aiff, mp3, etc.)
local song = renoise.song()
-- Create new instrument following Paketti conventions
if not safeInsertInstrumentAt(song, song.selected_instrument_index + 1) then return end
song.selected_instrument_index = song.selected_instrument_index + 1
-- Apply Paketti default instrument configuration
if pakettiPreferencesDefaultInstrumentLoader then
pakettiPreferencesDefaultInstrumentLoader()
end
local instrument = song.selected_instrument
-- Load the sample
local sample_index = #instrument.samples + 1
instrument:insert_sample_at(sample_index)
song.selected_sample_index = sample_index
local sample_buffer = song.selected_sample.sample_buffer
sample_buffer:load_from(file_path)
-- Set both instrument and sample name to filename (without extension)
local name_without_ext = file.name:match("(.+)%..+$") or file.name
instrument.name = name_without_ext
song.selected_sample.name = name_without_ext
-- Clean up any "Placeholder sample" left behind by default instrument loader
for i = #instrument.samples, 1, -1 do
if instrument.samples[i].name == "Placeholder sample" then
instrument:delete_sample_at(i)
end
end
renoise.app():show_status("Loaded sample: " .. file.name)
end
-- Don't close dialog - user requested to keep it open
end
-- Navigate between pages