001package polaris.sdk.ui.controls;
002import polaris.sdk.ui.*;
003
004import java.io.*;
005import java.nio.file.*;
006import java.util.*;
007import java.util.concurrent.*;
008import java.util.function.Consumer;
009/** Read-only asynchronous local file/directory chooser. Never deletes, uploads or overwrites files. */
010public class FileBrowser extends Modal {
011 private static final ExecutorService IO=Executors.newSingleThreadExecutor(r->{Thread t=new Thread(r,"LiquidGlass-FileBrowser");t.setDaemon(true);return t;});
012 private final boolean directories;private final Consumer<Path> result;private Path current,chosen;private int revision;private java.util.List<Path> entries=new ArrayList<Path>();
013 private final Button up=new Button("↑"),cancel=new Button("取消"),apply=new Button("选择").primary(true);
014 private final TextField pathField=new TextField();private final ListBox list=new ListBox();private final Label status=new Label("读取中…");private final ComboSelect roots=new ComboSelect();
015 public FileBrowser(boolean directoriesOnly,Path start,Consumer<Path> result){super(directoriesOnly?"选择目录":"选择文件");this.directories=directoriesOnly;this.result=result;w=610;h=478;pathField.bounds(81,64,507,35);pathField.onSubmit(s->{try{navigate(Paths.get(s));}catch(Exception e){status.text("无效路径");}});add(pathField);up.bounds(22,64,48,35);up.onClick(b->{if(current!=null&&current.getParent()!=null)navigate(current.getParent());});add(up);File[] fs=File.listRoots();String[] paths=new String[fs.length];for(int i=0;i<fs.length;i++)paths[i]=fs[i].toString();roots.items(paths);roots.bounds(22,108,126,32);roots.onChange(w->{if(roots.selectedIndex()>=0)navigate(Paths.get(roots.value()));});add(roots);status.bounds(162,116,426,22);status.size(11);add(status);list.bounds(22,150,566,251);list.onSelection(i->{if(i>=0&&i<entries.size())chosen=entries.get(i);});list.onDoubleClick(i->{if(i>=0&&i<entries.size()){Path p=entries.get(i);if(Files.isDirectory(p))navigate(p);else if(!directories)accept(p);}});add(list);cancel.bounds(366,423,100,35);cancel.onClick(b->close());add(cancel);apply.bounds(478,423,110,35);apply.onClick(b->{Path p=directories?(chosen!=null&&Files.isDirectory(chosen)?chosen:current):chosen;if(p!=null)accept(p);});add(apply);current=start==null?Paths.get(System.getProperty("user.home",".")):start;}
016 @Override protected void layoutChildren(){
017  float top=contentTop(),content=Math.max(0,w-2*padding),row=Spacing.CONTROL_HEIGHT;
018  float upWidth=Math.min(row,content),rootWidth=Math.min(128,content*.28f);
019  up.bounds(padding,top,upWidth,row);
020  pathField.bounds(padding+upWidth+gap,top,Math.max(0,content-upWidth-gap),row);
021  float second=top+row+gap;
022  roots.bounds(padding,second,rootWidth,32);
023  status.bounds(padding+rootWidth+gap,second+9,Math.max(0,content-rootWidth-gap),22);
024  float listTop=second+32+gap,footer=h-padding-row,bw=Math.min(104,Math.max(0,(content-gap)/2));
025  list.bounds(padding,listTop,content,Math.max(0,footer-gap-listTop));
026  cancel.bounds(w-padding-2*bw-gap,footer,bw,row);apply.bounds(w-padding-bw,footer,bw,row);
027 }
028 @Override public void attach(Ui context){super.attach(context);if(context!=null)navigate(current);}
029 private void accept(Path p){if(directories?!Files.isDirectory(p):!Files.isRegularFile(p)){status.text(directories?"请选择有效目录":"请选择有效文件");return;}close();if(result!=null)result.accept(p.toAbsolutePath().normalize());}
030 private void navigate(Path directory){final Ui context=ui;if(context==null){current=directory;return;}final int request=++revision;final Path target=directory.toAbsolutePath().normalize();status.text("正在读取本地目录…");IO.submit(()->{java.util.List<Path> found=new ArrayList<Path>();String message;try{try(DirectoryStream<Path> stream=Files.newDirectoryStream(target)){for(Path path:stream){if(directories&&!Files.isDirectory(path))continue;found.add(path);if(found.size()>=2000)break;}}found.sort((a,b)->{int k=Boolean.compare(Files.isDirectory(b),Files.isDirectory(a));return k!=0?k:a.getFileName().toString().compareToIgnoreCase(b.getFileName().toString());});message=found.size()>=2000?"已限制为前 2000 项":found.size()+" 个项目";}catch(Exception e){message="无法读取:"+e.getClass().getSimpleName();}final String msg=message;context.post(()->{if(ui!=context||request!=revision)return;if(msg.startsWith("无法")){status.text(msg);return;}current=target;chosen=null;entries=found;pathField.value(target.toString());java.util.List<String> labels=new ArrayList<String>();for(Path p:found)labels.add((Files.isDirectory(p)?"▸  ":"    ")+p.getFileName());list.items(labels);status.text(msg);});});}
031}