@@ -36,7 +36,8 @@ def __init__(self, connection):
3636 "Filesystem Browser" ,
3737 qml_folder = "qml/FilesystemBrowser.1"
3838 )
39-
39+ # Load Configuration
40+ self .config = self .load_config ()
4041
4142 # self.main_executor = MainThreadExecutor()
4243
@@ -161,14 +162,40 @@ def __init__(self, connection):
161162 )
162163 self .scanned_dirs_attr .expose_in_ui_attrs_group ("Filesystem Browser" )
163164
164- # New: Recursion limit attribute
165+ # New: Directory Query Result (for Tree View)
166+ self .directory_query_result = self .add_attribute (
167+ "directory_query_result" ,
168+ "{}" ,
169+ {"title" : "directory_query_result" },
170+ register_as_preference = False
171+ )
172+ self .directory_query_result .expose_in_ui_attrs_group ("Filesystem Browser" )
173+
165174 self .depth_limit_attr = self .add_attribute (
166175 "recursion_limit" ,
167- 6 ,
176+ self . config . get ( "max_recursion_depth" , 6 ) ,
168177 {"title" : "Recursion Limit" },
169178 register_as_preference = True
170179 )
171180 self .depth_limit_attr .expose_in_ui_attrs_group ("Filesystem Browser" )
181+
182+ # New: Scan Required flag (for manual scan mode)
183+ self .scan_required_attr = self .add_attribute (
184+ "scan_required" ,
185+ False ,
186+ {"title" : "scan_required" },
187+ register_as_preference = False
188+ )
189+ self .scan_required_attr .expose_in_ui_attrs_group ("Filesystem Browser" )
190+
191+ # Auto-scan threshold (read-only for UI logic)
192+ self .auto_scan_threshold_attr = self .add_attribute (
193+ "auto_scan_threshold" ,
194+ self .config .get ("auto_scan_threshold" , 4 ),
195+ {"title" : "auto_scan_threshold" },
196+ register_as_preference = False
197+ )
198+ self .auto_scan_threshold_attr .expose_in_ui_attrs_group ("Filesystem Browser" )
172199
173200 # New: Filter attributes
174201 self .filter_time_attr = self .add_attribute (
@@ -277,8 +304,10 @@ def __init__(self, connection):
277304 # attribute_changed method handles all.
278305
279306 # Internal state
280- self .extensions = {".mov" , ".exr" , ".png" , ".mp4" }
281- self .ignore_dirs = {".git" , ".svn" , "__pycache__" , ".DS_Store" }
307+ # Load extensions and ignore dirs from config
308+ self .extensions = set (self .config .get ("extensions" , []))
309+ self .ignore_dirs = set (self .config .get ("ignore_dirs" , []))
310+ self .root_ignore_dirs = set (self .config .get ("root_ignore_dirs" , []))
282311 self .search_thread = None
283312 self .cancel_search = False
284313 self .results_lock = threading .Lock () # Protects current_scan_results
@@ -339,7 +368,13 @@ def attribute_changed(self, attribute, role):
339368
340369 # Check if it's our command attribute and the Value changed
341370 if attribute .uuid == self .command_attr .uuid and role == AttributeRole .Value :
342- val = self .command_attr .value ()
371+ # Safely get value
372+ try :
373+ val = self .command_attr .value ()
374+ except TypeError :
375+ # Can happen if connection is shutting down or not ready
376+ return
377+
343378 if not val :
344379 return # Empty command
345380
@@ -389,13 +424,28 @@ def attribute_changed(self, attribute, role):
389424 self .depth_limit_attr .set_value (attr_value )
390425
391426 elif action == "add_pin" :
427+ path = data .get ("path" )
428+ self ._add_pin (path )
429+
430+ elif action == "remove_pin" :
431+ path = data .get ("path" )
432+ self ._remove_pin (path )
433+
434+ elif action == "force_scan" :
435+ # User clicked "Scan" button
436+ current = self .current_path_attr .value ()
437+ self .start_search (current , force = True )
392438 name = data .get ("name" )
393439 path = data .get ("path" )
394440 self ._add_pin (name , path )
395441
396442 elif action == "remove_pin" :
397443 path = data .get ("path" )
398444 self ._remove_pin (path )
445+
446+ elif action == "get_subdirs" :
447+ path = data .get ("path" )
448+ self ._get_subdirs (path )
399449
400450 # Clear command channel
401451 self .command_attr .set_value ("" )
@@ -454,6 +504,13 @@ def compute_completions(self, partial_path):
454504
455505 # Sort and limit
456506 candidates .sort ()
507+
508+ except Exception as e :
509+ print (f"Search thread error: { e } " )
510+ self .searching_attr .set_value (False )
511+
512+ # Sort and limit
513+ candidates .sort ()
457514 import json
458515 self .completions_attr .set_value (json .dumps (candidates [:10 ]))
459516
@@ -462,6 +519,65 @@ def compute_completions(self, partial_path):
462519 self .completions_attr .set_value ("[]" )
463520
464521
522+
523+ def load_config (self ):
524+ """Load configuration from config.json in the plugin directory."""
525+ config_path = os .path .join (os .path .dirname (__file__ ), "config.json" )
526+ default_config = {
527+ "extensions" : [".mov" , ".mp4" , ".mkv" , ".exr" , ".jpg" , ".jpeg" , ".png" ,
528+ ".dpx" , ".tiff" , ".tif" , ".wav" , ".mp3" ],
529+ "ignore_dirs" : [".git" , ".quarantine" , "eryx_unreal_plugin" , ".DS_Store" ],
530+ "root_ignore_dirs" : [],
531+ "max_recursion_depth" : 6 ,
532+ "auto_scan_threshold" : 4
533+ }
534+
535+ if os .path .exists (config_path ):
536+ try :
537+ with open (config_path , 'r' ) as f :
538+ loaded_config = json .load (f )
539+ # Merge with defaults
540+ for key , value in loaded_config .items ():
541+ default_config [key ] = value
542+ print (f"FilesystemBrowser: Loaded config from { config_path } " )
543+ except Exception as e :
544+ print (f"FilesystemBrowser: Error loading config: { e } " )
545+
546+ return default_config
547+
548+ def _get_subdirs (self , path ):
549+ """Fetch subdirectories for the given path and update attribute."""
550+ print (f"FilesystemBrowser: _get_subdirs called for { path } " )
551+ result = {"path" : path , "dirs" : []}
552+ try :
553+ if os .path .exists (path ) and os .path .isdir (path ):
554+ dirs = []
555+ with os .scandir (path ) as it :
556+ for entry in it :
557+ # Check ignore dirs (names)
558+ if entry .name in self .ignore_dirs or entry .name .startswith ('.' ):
559+ continue
560+
561+ # Check root ignore dirs (paths)
562+ if entry .path in self .root_ignore_dirs :
563+ continue
564+
565+ if entry .is_dir ():
566+ dirs .append ({
567+ "name" : entry .name ,
568+ "path" : entry .path
569+ })
570+ # Sort alphabetically
571+ dirs .sort (key = lambda x : x ["name" ].lower ())
572+ result ["dirs" ] = dirs
573+ print (f"FilesystemBrowser: Found { len (dirs )} subdirs in { path } " )
574+ except Exception as e :
575+ print (f"Error getting subdirs for { path } : { e } " )
576+
577+ # Ensure we use JSON dumping
578+ import json
579+ self .directory_query_result .set_value (json .dumps (result ))
580+
465581 def load_file (self , path ):
466582 # Handle directory navigation
467583 if os .path .isdir (path ):
@@ -648,7 +764,59 @@ def load_file(self, path):
648764 print (f"Error loading file: { e } " )
649765
650766
651- def start_search (self , start_path ):
767+ def start_search (self , start_path , force = False ):
768+ """
769+ Start the file search in a separate thread.
770+ If force=False and depth <= 4, skip auto-scan and ask user to confirm.
771+ """
772+ if not start_path :
773+ return
774+
775+ # Check path depth
776+ # Normalize path
777+ norm_path = os .path .normpath (start_path )
778+ # Split path
779+ parts = norm_path .strip (os .sep ).split (os .sep )
780+ # On Mac/Linux, root is empty string at start if absolute?
781+ # len(parts) for "/Users/sam" -> ["Users", "sam"] -> 2
782+ # Root "/" -> [""] -> 1 (empty string)
783+ # Let's count non-empty parts
784+ depth = len ([p for p in parts if p ])
785+
786+ # User requested: top 4 levels don't auto-scan.
787+ # e.g. / (0), /Users (1), /Users/sam (2), /Users/sam/Desktop (3) -> Auto scan?
788+ # Requirement: "looking at any directory in the top 4 levels ... shouldnt start recursive search"
789+ # So depth <= threshold skips scan.
790+ threshold = self .config .get ("auto_scan_threshold" , 4 )
791+
792+ if not force and depth <= threshold :
793+ print (f"FilesystemBrowser: Path '{ start_path } ' (depth { depth } ) requires manual scan." )
794+ self .scan_required_attr .set_value (True )
795+ self .searching_attr .set_value (False )
796+ self .progress_attr .set_value ("0" )
797+
798+ # Clear previous results
799+ with self .results_lock :
800+ self .current_scan_results = []
801+ self .scanned_attr .set_value ("0" )
802+ self .scanned_dirs_attr .set_value ("[]" )
803+
804+ # Update UI with empty list but correct path
805+ self .apply_filters () # Will clear list
806+
807+ # Ensure we cancel any running thread
808+ if self .search_thread and self .search_thread .is_alive ():
809+ self .cancel_search = True
810+ if hasattr (self , 'scanner' ):
811+ self .scanner .stop ()
812+ self .search_thread .join ()
813+
814+ return
815+
816+ # Proceed with scan
817+ self .scan_required_attr .set_value (False )
818+
819+ # Stop existing search if running
652820 if self .search_thread and self .search_thread .is_alive ():
653821 self .cancel_search = True
654822 if hasattr (self , 'scanner' ):
@@ -666,6 +834,10 @@ def _search_worker(self, start_path):
666834
667835 from .scanner import FileScanner
668836
837+ # Cache current filter values for this search to avoid threading issues with attribute access
838+ self .cached_filter_time = self .filter_time_attr .value ()
839+ self .cached_filter_version = self .filter_version_attr .value ()
840+
669841 # Config (could be loaded from prefs)
670842 max_depth = self .depth_limit_attr .value ()
671843 config = {
@@ -696,7 +868,7 @@ def progress_callback(results, info):
696868 # to make it feel more linear to the user.
697869 biased_progress = pow (progress / 100.0 , 2.0 )* 100
698870 self .progress_attr .set_value (str (biased_progress ))
699- self .scanned_attr .set_value (str (scanned ))
871+ self .scanned_attr .set_value (str (scanned )) # Fixed type to string
700872 #self.scanProgress.set_value(str(progress))
701873
702874 # Accumulate scanned dirs
@@ -745,16 +917,34 @@ def progress_callback(results, info):
745917 self .searching_attr .set_value (False )
746918
747919 def apply_filters (self ):
748- # Filtering logic
749-
750- with self .results_lock :
751- results = list (self .current_scan_results )
920+ """Re-run filtering logic on the current results cache."""
921+ try :
922+ with self .results_lock :
923+ results = list (self .current_scan_results )
752924
753- self ._apply_filters_logic (results )
754-
925+ # Offload heavy filtering if list is huge?
926+ # For now, do it in main thread or worker?
927+ # Safe to do in main thread if count < 100k?
928+ # Better to spawn a thread if we want UI responsiveness.
929+
930+ # Doing it synchronously for now, but catching errors
931+ self ._apply_filters_logic (results )
932+ except Exception as e :
933+ print (f"Error applying filters: { e } " )
934+
755935 def _apply_filters_logic (self , results ):
756- filter_time = self .filter_time_attr .value () if hasattr (self , 'filter_time_attr' ) else "Any"
757- filter_version = self .filter_version_attr .value () if hasattr (self , 'filter_version_attr' ) else "All Versions"
936+ # Use cached values if available (from worker), else fetch live (UI update)
937+ if hasattr (self , 'cached_filter_time' ):
938+ filter_time = self .cached_filter_time
939+ else :
940+ filter_time = self .filter_time_attr .value () if hasattr (self , 'filter_time_attr' ) else "Any"
941+
942+ if hasattr (self , 'cached_filter_version' ):
943+ filter_version = self .cached_filter_version
944+ else :
945+ filter_version = self .filter_version_attr .value () if hasattr (self , 'filter_version_attr' ) else "All Versions"
946+
947+ print (f"Applying filters: Time={ filter_time } , Version={ filter_version } , Count={ len (results )} " )
758948
759949 # Separate directories and files
760950 dirs = []
0 commit comments