/* * Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.library; import com.trollworks.gcs.advantage.AdvantageList; import com.trollworks.gcs.advantage.AdvantagesDockable; import com.trollworks.gcs.character.GURPSCharacter; import com.trollworks.gcs.character.SheetDockable; import com.trollworks.gcs.common.ListCollectionListener; import com.trollworks.gcs.common.ListCollectionThread; import com.trollworks.gcs.common.Workspace; import com.trollworks.gcs.equipment.EquipmentDockable; import com.trollworks.gcs.equipment.EquipmentList; import com.trollworks.gcs.pdfview.PdfDockable; import com.trollworks.gcs.pdfview.PdfRef; import com.trollworks.gcs.skill.SkillList; import com.trollworks.gcs.skill.SkillsDockable; import com.trollworks.gcs.spell.SpellList; import com.trollworks.gcs.spell.SpellsDockable; import com.trollworks.gcs.template.Template; import com.trollworks.gcs.template.TemplateDockable; import com.trollworks.toolkit.annotation.Localize; import com.trollworks.toolkit.io.Log; import com.trollworks.toolkit.ui.image.StdImage; import com.trollworks.toolkit.ui.menu.edit.Openable; import com.trollworks.toolkit.ui.menu.file.RecentFilesMenu; import com.trollworks.toolkit.ui.widget.IconButton; import com.trollworks.toolkit.ui.widget.StdFileDialog; import com.trollworks.toolkit.ui.widget.Toolbar; import com.trollworks.toolkit.ui.widget.dock.Dock; import com.trollworks.toolkit.ui.widget.dock.DockContainer; import com.trollworks.toolkit.ui.widget.dock.DockLayout; import com.trollworks.toolkit.ui.widget.dock.DockLocation; import com.trollworks.toolkit.ui.widget.dock.Dockable; import com.trollworks.toolkit.ui.widget.search.Search; import com.trollworks.toolkit.ui.widget.search.SearchTarget; import com.trollworks.toolkit.ui.widget.tree.FieldAccessor; import com.trollworks.toolkit.ui.widget.tree.IconAccessor; import com.trollworks.toolkit.ui.widget.tree.TextTreeColumn; import com.trollworks.toolkit.ui.widget.tree.TreeContainerRow; import com.trollworks.toolkit.ui.widget.tree.TreePanel; import com.trollworks.toolkit.ui.widget.tree.TreeRoot; import com.trollworks.toolkit.ui.widget.tree.TreeRow; import com.trollworks.toolkit.ui.widget.tree.TreeRowViewIterator; import com.trollworks.toolkit.utility.FileProxy; import com.trollworks.toolkit.utility.FileType; import com.trollworks.toolkit.utility.Localization; import com.trollworks.toolkit.utility.PathUtils; import com.trollworks.toolkit.utility.notification.Notifier; import java.awt.BorderLayout; import java.awt.KeyboardFocusManager; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.swing.Icon; import javax.swing.ListCellRenderer; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; /** A list of available library files. */ public class LibraryExplorerDockable extends Dockable implements DocumentListener, SearchTarget, ListCollectionListener, FieldAccessor, IconAccessor, Openable { @Localize("Library Explorer") @Localize(locale = "de", value = "Listen-Bibliothek") @Localize(locale = "ru", value = "Библиотека") @Localize(locale = "es", value = "Listado de la librería") private static String TITLE; @Localize("Enter text here to narrow the list to only those rows containing matching items") @Localize(locale = "de", value = "Hier Text eingeben, um eine Liste der passenden Einträge anzuzeigen") @Localize(locale = "ru", value = "Введите текст здесь, чтобы сузить список до содержащих подходящие элементы") @Localize(locale = "es", value = "Escribe un texto aquí para acortar la lista y mostrar sólo las filas que contengan el texto") private static String SEARCH_FIELD_TOOLTIP; @Localize("Opens/closes all hierarchical rows") @Localize(locale = "de", value = "Öffnet / Schließt alle Untereinträge") @Localize(locale = "ru", value = "Развернуть/свернуть все вложенные строки") @Localize(locale = "es", value = "Pliega/despliega todas las filas jerarquicamente") private static String TOGGLE_ROWS_OPEN_TOOLTIP; static { Localization.initialize(); } private Toolbar mToolbar; private Search mSearch; private TreePanel mTreePanel; private Notifier mNotifier; public static LibraryExplorerDockable get() { for (Dockable dockable : Workspace.get().getDock().getDockables()) { if (dockable instanceof LibraryExplorerDockable) { return (LibraryExplorerDockable) dockable; } } // Shouldn't be possible return null; } public LibraryExplorerDockable() { super(new BorderLayout()); ListCollectionThread listCollectionThread = ListCollectionThread.get(); mNotifier = new Notifier(); TreeRoot root = new TreeRoot(mNotifier); fillTree(listCollectionThread.getLists(), root); mTreePanel = new TreePanel(root); mTreePanel.setShowHeader(false); mTreePanel.addColumn(new TextTreeColumn(TITLE, this, this)); mTreePanel.setAllowColumnDrag(false); mTreePanel.setAllowColumnResize(false); mTreePanel.setAllowColumnContextMenu(false); mTreePanel.setAllowRowDropFromExternal(false); mTreePanel.setAllowedRowDragTypes(0); // Turns off row dragging mTreePanel.setShowRowDivider(false); mTreePanel.setShowColumnDivider(false); mTreePanel.setUseBanding(false); mTreePanel.setUserSortable(false); mTreePanel.setOpenableProxy(this); mToolbar = new Toolbar(); mSearch = new Search(this); mToolbar.add(mSearch, Toolbar.LAYOUT_FILL); mToolbar.add(new IconButton(StdImage.TOGGLE_OPEN, TOGGLE_ROWS_OPEN_TOOLTIP, () -> mTreePanel.toggleDisclosure())); add(mToolbar, BorderLayout.NORTH); add(mTreePanel, BorderLayout.CENTER); listCollectionThread.addListener(this); } @Override public String getDescriptor() { return "library_explorer"; //$NON-NLS-1$ } @Override public Icon getTitleIcon() { return StdImage.FOLDER.getImage(16); } @Override public String getTitle() { return TITLE; } @Override public String getTitleTooltip() { return TITLE; } @Override public String getField(TreeRow row) { return ((LibraryExplorerRow) row).getName(); } @Override public StdImage getIcon(TreeRow row) { return ((LibraryExplorerRow) row).getIcon(); } @Override public void changedUpdate(DocumentEvent event) { documentChanged(); } @Override public void insertUpdate(DocumentEvent event) { documentChanged(); } @Override public void removeUpdate(DocumentEvent event) { documentChanged(); } private void documentChanged() { // mOutline.reapplyRowFilter(); } private void fillTree(List<?> lists, TreeContainerRow parent) { int count = lists.size(); for (int i = 1; i < count; i++) { Object entry = lists.get(i); if (entry instanceof List<?>) { List<?> subList = (List<?>) entry; LibraryDirectoryRow dir = new LibraryDirectoryRow((String) subList.get(0)); fillTree(subList, dir); parent.addRow(dir); } else { parent.addRow(new LibraryFileRow((Path) entry)); } } } @Override public void dataFileListUpdated(List<Object> lists) { TreeRoot root = mTreePanel.getRoot(); Set<String> selected = new HashSet<>(); for (TreeRow row : mTreePanel.getExplicitlySelectedRows()) { selected.add(((LibraryExplorerRow) row).getSelectionKey()); } Set<String> open = new HashSet<>(); for (TreeRow row : new TreeRowViewIterator(mTreePanel, root)) { if (row instanceof TreeContainerRow && mTreePanel.isOpen((TreeContainerRow) row)) { open.add(((LibraryExplorerRow) row).getSelectionKey()); } } mNotifier.startBatch(); root.removeRow(new ArrayList<>(root.getChildren())); fillTree(lists, root); mNotifier.endBatch(); mTreePanel.setOpen(true, collectRowsToOpen(root, open, null)); mTreePanel.select(collectRows(root, selected, null)); } private List<TreeContainerRow> collectRowsToOpen(TreeContainerRow parent, Set<String> selectors, List<TreeContainerRow> list) { if (list == null) { list = new ArrayList<>(); } for (TreeRow row : parent.getChildren()) { if (row instanceof TreeContainerRow) { TreeContainerRow container = (TreeContainerRow) row; if (selectors.contains(((LibraryExplorerRow) row).getSelectionKey())) { list.add(container); } collectRowsToOpen(container, selectors, list); } } return list; } private List<TreeRow> collectRows(TreeContainerRow parent, Set<String> selectors, List<TreeRow> list) { if (list == null) { list = new ArrayList<>(); } for (TreeRow row : parent.getChildren()) { if (selectors.contains(((LibraryExplorerRow) row).getSelectionKey())) { list.add(row); } if (row instanceof TreeContainerRow) { collectRows((TreeContainerRow) row, selectors, list); } } return list; } @Override public boolean canOpenSelection() { return true; } @Override public void openSelection() { List<TreeContainerRow> containers = new ArrayList<>(); boolean hadFile = false; for (TreeRow row : mTreePanel.getExplicitlySelectedRows()) { if (row instanceof TreeContainerRow) { containers.add((TreeContainerRow) row); } else { open(((LibraryFileRow) row).getPath()); hadFile = true; } } if (!hadFile) { for (TreeContainerRow container : containers) { mTreePanel.setOpen(!mTreePanel.isOpen(container), container); } } } public Dockable getDockableFor(Path path) { for (Dockable dockable : getDockContainer().getDock().getDockables()) { if (dockable instanceof FileProxy) { File file = ((FileProxy) dockable).getBackingFile(); if (file != null) { try { if (Files.isSameFile(path, file.toPath())) { return dockable; } } catch (IOException ioe) { Log.error(ioe); } } } } return null; } public FileProxy open(Path path) { // See if it is already open FileProxy proxy = (FileProxy) getDockableFor(path); if (proxy == null) { // If it wasn't, load it and put it into the dock try { switch (PathUtils.getExtension(path)) { case AdvantageList.EXTENSION: proxy = openAdvantageList(path); break; case EquipmentList.EXTENSION: proxy = openEquipmentList(path); break; case SkillList.EXTENSION: proxy = openSkillList(path); break; case SpellList.EXTENSION: proxy = openSpellList(path); break; case LibraryFile.EXTENSION: proxy = openLibrary(path); break; case GURPSCharacter.EXTENSION: proxy = dockSheet(new SheetDockable(new GURPSCharacter(path.toFile()))); break; case Template.EXTENSION: proxy = dockTemplate(new TemplateDockable(new Template(path.toFile()))); break; case FileType.PDF_EXTENSION: proxy = dockPdf(new PdfDockable(new PdfRef(null, path.toFile(), 0), 1, null)); break; default: break; } } catch (Throwable throwable) { StdFileDialog.showCannotOpenMsg(this, PathUtils.getLeafName(path, true), throwable); proxy = null; } } else { Dockable dockable = (Dockable) proxy; dockable.getDockContainer().setCurrentDockable(dockable); } if (proxy != null) { File file = proxy.getBackingFile(); if (file != null) { RecentFilesMenu.addRecent(file); } } return proxy; } private FileProxy openAdvantageList(Path path) throws IOException { AdvantageList list = new AdvantageList(); list.load(path.toFile()); list.getModel().setLocked(true); return dockLibrary(new AdvantagesDockable(list)); } private FileProxy openEquipmentList(Path path) throws IOException { EquipmentList list = new EquipmentList(); list.load(path.toFile()); list.getModel().setLocked(true); return dockLibrary(new EquipmentDockable(list)); } private FileProxy openSkillList(Path path) throws IOException { SkillList list = new SkillList(); list.load(path.toFile()); list.getModel().setLocked(true); return dockLibrary(new SkillsDockable(list)); } private FileProxy openSpellList(Path path) throws IOException { SpellList list = new SpellList(); list.load(path.toFile()); list.getModel().setLocked(true); return dockLibrary(new SpellsDockable(list)); } private FileProxy openLibrary(Path path) throws IOException { FileProxy proxy = null; LibraryFile library = new LibraryFile(path.toFile()); SpellList spells = library.getSpellList(); if (!spells.isEmpty()) { spells.setModified(true); proxy = dockLibrary(new SpellsDockable(spells)); } SkillList skills = library.getSkillList(); if (!skills.isEmpty()) { skills.setModified(true); proxy = dockLibrary(new SkillsDockable(skills)); } EquipmentList equipment = library.getEquipmentList(); if (!equipment.isEmpty()) { equipment.setModified(true); proxy = dockLibrary(new EquipmentDockable(equipment)); } AdvantageList adq = library.getAdvantageList(); if (!adq.isEmpty()) { adq.setModified(true); proxy = dockLibrary(new AdvantagesDockable(adq)); } return proxy; } /** * @param library The {@link LibraryDockable} to dock. * @return The {@link LibraryDockable} that was passed in. */ public LibraryDockable dockLibrary(LibraryDockable library) { // Order of docking: // 1. Stack with another library // 2. Dock to the top of a template // 2. Dock to the right of a sheet // 3. Dock to the right of the library explorer Dockable template = null; Dockable sheet = null; Dock dock = getDockContainer().getDock(); for (Dockable dockable : dock.getDockables()) { if (dockable instanceof LibraryDockable) { dockable.getDockContainer().stack(library); return library; } if (template == null && dockable instanceof TemplateDockable) { template = dockable; } if (sheet == null && dockable instanceof SheetDockable) { sheet = dockable; } } if (template != null) { dock.dock(library, template, DockLocation.NORTH); } else if (sheet != null) { dock.dock(library, sheet, DockLocation.EAST); } else { dock.dock(library, this, DockLocation.EAST); } return library; } /** * @param sheet The {@link SheetDockable} to dock. * @return The {@link SheetDockable} that was passed in. */ public SheetDockable dockSheet(SheetDockable sheet) { // Order of docking: // 1. Stack with another sheet // 2. Dock to the left of a library or template // 3. Dock to the right of the library explorer Dockable other = null; Dock dock = getDockContainer().getDock(); for (Dockable dockable : dock.getDockables()) { if (dockable instanceof SheetDockable) { dockable.getDockContainer().stack(sheet); return sheet; } if (other == null && (dockable instanceof TemplateDockable || dockable instanceof LibraryDockable)) { other = dockable; } } if (other != null) { DockContainer dc = other.getDockContainer(); DockLayout layout = dc.getDock().getLayout().findLayout(dc); if (layout.isVertical()) { dock.dock(sheet, layout, DockLocation.WEST); } else { dock.dock(sheet, other, DockLocation.WEST); } } else { dock.dock(sheet, this, DockLocation.EAST); } return sheet; } /** * @param template The {@link TemplateDockable} to dock. * @return The {@link TemplateDockable} that was passed in. */ public TemplateDockable dockTemplate(TemplateDockable template) { // Order of docking: // 1. Stack with another template // 2. Dock to the bottom of a library // 3. Dock to the right of a sheet // 4. Dock to the right of the library explorer Dockable sheet = null; Dockable library = null; Dock dock = getDockContainer().getDock(); for (Dockable dockable : dock.getDockables()) { if (dockable instanceof TemplateDockable) { dockable.getDockContainer().stack(template); return template; } if (sheet == null && dockable instanceof SheetDockable) { sheet = dockable; } if (library == null && dockable instanceof LibraryDockable) { library = dockable; } } if (library != null) { dock.dock(template, library, DockLocation.SOUTH); } else if (sheet != null) { dock.dock(template, sheet, DockLocation.EAST); } else { dock.dock(template, this, DockLocation.EAST); } return template; } /** * @param pdf The {@link PdfDockable} to dock. * @return The {@link PdfDockable} that was passed in. */ public PdfDockable dockPdf(PdfDockable pdf) { // Order of docking: // 1. Stack with another pdf // 2. Dock to the right of a sheet // 2. Dock to the left of a library or template // 3. Dock to the right of the library explorer Dockable sheet = null; Dockable other = null; Dock dock = getDockContainer().getDock(); for (Dockable dockable : dock.getDockables()) { if (dockable instanceof PdfDockable) { dockable.getDockContainer().stack(pdf); return pdf; } if (sheet == null && dockable instanceof SheetDockable) { sheet = dockable; } if (other == null && (dockable instanceof TemplateDockable || dockable instanceof LibraryDockable)) { other = dockable; } } if (sheet != null) { dock.dock(pdf, sheet, DockLocation.EAST); } else if (other != null) { DockContainer dc = other.getDockContainer(); DockLayout layout = dc.getDock().getLayout().findLayout(dc); if (layout.isVertical()) { dock.dock(pdf, layout, DockLocation.WEST); } else { dock.dock(pdf, other, DockLocation.WEST); } } else { dock.dock(pdf, this, DockLocation.EAST); } return pdf; } @Override public boolean isJumpToSearchAvailable() { return mSearch.isEnabled() && mSearch != KeyboardFocusManager.getCurrentKeyboardFocusManager().getPermanentFocusOwner(); } @Override public void jumpToSearchField() { mSearch.requestFocusInWindow(); } @Override public ListCellRenderer<Object> getSearchRenderer() { return new LibraryExplorerRowRenderer(); } @Override public List<Object> search(String filter) { ArrayList<Object> list = new ArrayList<>(); filter = filter.toLowerCase(); collect(mTreePanel.getRoot(), filter, list); return list; } private static void collect(TreeRow row, String text, ArrayList<Object> list) { if (row instanceof LibraryExplorerRow) { if (((LibraryExplorerRow) row).getName().toLowerCase().contains(text)) { list.add(row); } } if (row instanceof TreeContainerRow) { for (TreeRow child : ((TreeContainerRow) row).getChildren()) { collect(child, text, list); } } } @Override public void searchSelect(List<Object> selection) { List<TreeRow> list = new ArrayList<>(); for (Object one : selection) { if (one instanceof TreeRow) { list.add((TreeRow) one); } } mTreePanel.setParentsOpen(list); mTreePanel.select(list); mTreePanel.requestFocus(); } }