001package polaris.sdk.ui.samples; 002 003import polaris.sdk.ui.*; 004import polaris.sdk.ui.controls.*; 005import java.util.*; 006import static polaris.sdk.ui.samples.SceneSupport.*; 007 008/** An in-memory explorer: browse, filter, rename, create and delete demo entries, never real files. */ 009public final class FilesScene extends SceneSupport.AppScene { 010 public static final class Entry {public final int id,parent;public String name;public final boolean folder;public final int kilobytes;Entry(int i,int p,String n,boolean f,int kb){id=i;parent=p;name=n;folder=f;kilobytes=kb;}} 011 private final List<Entry> entries=new ArrayList<Entry>();private int nextId=10,folder,selected=-1; 012 private final Panel folders=new Panel(),body=new Panel();private TreeView tree; 013 private final Breadcrumb path=new Breadcrumb("工作空间");private final SearchField search=new SearchField(); 014 private final Button up=new Button("上一级"),create=new Button("新建文件夹"),rename=new Button("重命名"),open=new Button("打开 / 详情"); 015 private final ButtonGroup views=new ButtonGroup("列表","网格");private final FileTable table=new FileTable();private final GridView grid=new GridView();private final List<Entry> shown=new ArrayList<Entry>(); 016 private final Label summary=caption(""),storage=caption("虚拟文件,不读取或修改磁盘。");private final ProgressBar usage=new ProgressBar(); 017 public FilesScene(){super("文件工作空间","文件管理 / 目录树、面包屑、排序、筛选、列表网格与操作弹窗");id("scene.files"); 018 entries.add(new Entry(1,0,"文档",true,0));entries.add(new Entry(2,0,"项目",true,0));entries.add(new Entry(3,0,"设计",true,0));entries.add(new Entry(4,0,"下载",true,0)); 019 entries.add(new Entry(5,0,"开始阅读.md",false,8));entries.add(new Entry(6,0,"演示说明.txt",false,4));entries.add(new Entry(7,1,"会议记录.md",false,12));entries.add(new Entry(8,2,"界面草稿.json",false,24));entries.add(new Entry(9,3,"配色方案.txt",false,3)); 020 mount(this,folders,"files.folders");mount(this,body,"files.body");mount(this,storage,null);rebuildTree(); 021 mount(body,path,"files.breadcrumb");path.onNavigate(i->navigate(pathIds().get(i)));mount(body,up,"files.up");up.onClick(v->{if(folder!=0)navigate(entry(folder).parent);}); 022 mount(body,search,"files.search");search.placeholder("搜索当前目录…");search.onChange(v->refresh());mount(body,views,"files.views");views.select(0);views.onChange(v->{table.visible(views.selectedIndex()==0);grid.visible(views.selectedIndex()==1);}); 023 mount(body,create,"files.create");create.onClick(v->nameDialog(false));mount(body,rename,"files.rename");rename.onClick(v->nameDialog(true));mount(body,open,"files.open");open.onClick(v->openSelected()); 024 mount(body,table,"files.table");table.columns("名称","类型","大小 KB").editable(false);table.onChange(v->{selected=selectedFromTable();refreshSelection();}); 025 mount(body,grid,"files.grid");grid.columns(3);grid.glass(false).padding(0);grid.onSelection(i->{if(i>=0&&i<shown.size()){selected=shown.get(i).id;refreshSelection();}});grid.visible(false); 026 PopupMenu menu=new PopupMenu().item("打开 / 查看详情",this::openSelected).item("重命名",()->nameDialog(true)).separator().item("从演示中删除",this::deleteSelected); 027 table.context(menu);grid.context(menu);mount(body,summary,"files.summary");mount(folders,usage,"files.usage");usage.value(.31);refresh(); 028 } 029 public List<Entry> entries(){return Collections.unmodifiableList(entries);} 030 public int currentFolder(){return folder;} 031 public int selectedId(){return selected;} 032 public int shownCount(){return shown.size();} 033 private Entry entry(int id){for(Entry e:entries)if(e.id==id)return e;throw new IllegalArgumentException("entry "+id);} 034 public void navigate(int id){if(id!=0&&!entry(id).folder)throw new IllegalArgumentException("not a folder");folder=id;search.value("");selected=-1;refresh();} 035 private List<Integer> pathIds(){List<Integer> ids=new ArrayList<Integer>();int current=folder;while(current!=0){ids.add(0,current);current=entry(current).parent;}ids.add(0,0);return ids;} 036 private void refresh(){shown.clear();String q=search.value().trim().toLowerCase(Locale.ROOT);for(Entry e:entries)if(e.parent==folder&&e.name.toLowerCase(Locale.ROOT).contains(q))shown.add(e); 037 Collections.sort(shown,(a,b)->a.folder==b.folder?a.name.compareToIgnoreCase(b.name):(a.folder?-1:1)); 038 table.reload(shown);List<String> names=new ArrayList<String>();for(Entry e:shown)names.add(e.name);grid.items(names);selected=-1; 039 List<String> segments=new ArrayList<String>();for(Integer i:pathIds())segments.add(i==0?"工作空间":entry(i).name);path.segments(segments.toArray(new String[0]));up.enabled(folder!=0);refreshSelection(); 040 } 041 private void refreshSelection(){rename.enabled(selected>=0);open.enabled(selected>=0);summary.text(shown.size()+" 个项目"+(selected>=0?" · 选中 "+entry(selected).name:"")+" / 仅内存数据");} 042 private int selectedFromTable(){int r=table.selectedRow();if(r<0||r>=table.rowCount())return -1;String name=table.cell(r,0).value();for(Entry e:shown)if(e.name.equals(name))return e.id;return -1;} 043 public void openSelected(){if(selected<0)return;Entry e=entry(selected);if(e.folder){navigate(e.id);return;}Modal dialog=new Modal("文件详情 · 本地示例");dialog.bounds(0,0,440,247);dialog.id("files.details"); 044 Label info=new Label("名称:"+e.name+"\n类型:演示文档\n大小:"+e.kilobytes+" KB\n这里没有对应的真实磁盘文件。").size(14);place(info,16,72,408,108);dialog.add(info);Button done=new Button("关闭");place(done,308,197,116,34);done.onClick(v->dialog.close());dialog.add(done);if(ui!=null)dialog.open(ui); 045 } 046 private String validateName(String name,int except){if(name.trim().isEmpty())return "名称不能为空";if(name.length()>40)return "名称不能超过 40 个字符";if(name.matches(".*[\\\\/:*?\"<>|].*"))return "名称不能包含路径或保留字符";for(Entry e:entries)if(e.parent==folder&&e.id!=except&&e.name.equalsIgnoreCase(name.trim()))return "当前目录已有同名项目";return "";} 047 private void nameDialog(boolean editing){if(ui==null||editing&&selected<0)return;final int target=editing?selected:-1;Modal dialog=new Modal(editing?"重命名演示项目":"新建演示文件夹");dialog.id("files.nameDialog");dialog.bounds(0,0,440,245); 048 TextField text=new TextField(editing?entry(target).name:"新建文件夹");text.maxLength(40);text.id("files.nameInput");place(text,16,75,408,38);dialog.add(text);Label error=caption("只修改内存中的项目,不写入磁盘。");place(error,16,131,408,28);dialog.add(error); 049 Button cancel=new Button("取消"),save=new Button(editing?"保存名称":"创建").primary(true);save.id("files.nameCommit");place(cancel,16,189,128,38);place(save,156,189,268,38);dialog.add(cancel);dialog.add(save);cancel.onClick(v->dialog.close()); 050 Runnable commit=()->{String value=text.value().trim(),problem=validateName(value,target);if(!problem.isEmpty()){error.text(problem);return;}if(editing)entry(target).name=value;else entries.add(new Entry(nextId++,folder,value,true,0));refresh();rebuildTree();dialog.close();};save.onClick(v->commit.run());text.onSubmit(v->commit.run());dialog.open(ui);text.selectAll(); 051 } 052 public void deleteSelected(){if(selected<0)return;final int id=selected;Set<Integer> remove=new HashSet<Integer>();remove.add(id);boolean more;do{more=false;for(Entry e:entries)if(remove.contains(e.parent)&&!remove.contains(e.id)){remove.add(e.id);more=true;}}while(more);Iterator<Entry> i=entries.iterator();while(i.hasNext())if(remove.contains(i.next().id))i.remove();refresh();rebuildTree();notice(this,"演示项目已删除","真实文件与目录没有被读取或更改。");} 053 private void rebuildTree(){if(tree!=null)folders.remove(tree);tree=new TreeView();tree.id("files.tree");TreeNode root=new TreeNode("工作空间").data(0);for(Entry e:entries)if(e.folder&&e.parent==0)root.node(node(e));tree.root(root);tree.onChange(v->{if(tree.selected()!=null)navigate((Integer)tree.selected().data());});folders.add(tree);} 054 private TreeNode node(Entry entry){TreeNode node=new TreeNode(entry.name).data(entry.id);for(Entry e:entries)if(e.folder&&e.parent==entry.id)node.node(node(e));return node;} 055 @Override public float preferredHeight(float width){return width<760?746:574;} 056 @Override protected void layoutChildren(){header();boolean stack=w<760;float fw=stack?w:216,bw=stack?w:w-fw-GAP; 057 place(folders,0,76,fw,stack?160:450);place(tree,16,16,fw-32,stack?100:365);place(usage,16,folders.height()-32,fw-32,8); 058 place(body,stack?0:fw+GAP,stack?248:76,bw,450);place(path,16,16,bw-140,28);place(up,bw-112,12,96,32);place(search,16,58,bw-212,34);place(views,bw-184,58,168,34); 059 place(create,16,104,132,32);place(rename,160,104,100,32);place(open,272,104,124,32); 060 place(table,16,148,bw-32,247);place(grid,16,148,bw-32,247);grid.columns(bw<600?2:3);place(summary,16,412,bw-32,22); 061 place(storage,0,body.y()+body.height()+16,w,24);h=body.y()+body.height()+48; 062 } 063 private final class FileTable extends Table { 064 void reload(List<Entry> entries){data.clear();selected=-1;scroll=0;sortColumn=-1;for(Entry e:entries)row(e.name,e.folder?"文件夹":"文件",e.folder?"—":String.valueOf(e.kilobytes));} 065 @Override public void event(UiEvent e){super.event(e);if(e.type==UiEvent.Type.DOWN&&e.button==1&&e.clicks>=2&&e.y>=ay+35){FilesScene.this.selected=selectedFromTable();openSelected();}} 066 } 067}