From 3cdf3fd8a60d22a592e1cd0634cb108faa1f5f9f Mon Sep 17 00:00:00 2001 From: Niki Roo Date: Tue, 7 Apr 2020 20:07:58 +0200 Subject: [PATCH] Initial commit (missing a lot of things): - program starts and loads actual data - network libraries also work - backend is fanfix, so no problem there - you can read the stories with external launcher only for now - lots of missing options/menus/... - no i18n yet --- src/be/nikiroo/fanfix_swing/Actions.java | 154 +++ src/be/nikiroo/fanfix_swing/Main.java | 52 + .../nikiroo/fanfix_swing/gui/BooksPanel.java | 336 +++++ .../fanfix_swing/gui/BrowserPanel.java | 218 +++ .../fanfix_swing/gui/DetailsPanel.java | 104 ++ .../nikiroo/fanfix_swing/gui/MainFrame.java | 116 ++ .../nikiroo/fanfix_swing/gui/SearchBar.java | 142 ++ .../fanfix_swing/gui/book/BookBlock.java | 95 ++ .../gui/book/BookCoverImager.java | 232 ++++ .../fanfix_swing/gui/book/BookInfo.java | 311 +++++ .../fanfix_swing/gui/book/BookLine.java | 168 +++ .../fanfix_swing/gui/browser/AuthorTab.java | 54 + .../fanfix_swing/gui/browser/BasicTab.java | 244 ++++ .../fanfix_swing/gui/browser/SourceTab.java | 83 ++ .../fanfix_swing/gui/browser/TagsTab.java | 62 + .../gui/utils/TreeCellSpanner.java | 169 +++ .../gui/utils/TreeModelTransformer.java | 1217 +++++++++++++++++ .../fanfix_swing/gui/utils/UiHelper.java | 48 + .../fanfix_swing/images/IconGenerator.java | 121 ++ .../fanfix_swing/images/arrow_down-16x16.png | Bin 0 -> 4335 bytes .../fanfix_swing/images/arrow_down-24x24.png | Bin 0 -> 4239 bytes .../fanfix_swing/images/arrow_down-32x32.png | Bin 0 -> 4794 bytes .../fanfix_swing/images/arrow_down-64x64.png | Bin 0 -> 6110 bytes .../fanfix_swing/images/arrow_down.png | Bin 0 -> 3095 bytes .../fanfix_swing/images/arrow_left-16x16.png | Bin 0 -> 4325 bytes .../fanfix_swing/images/arrow_left-24x24.png | Bin 0 -> 4239 bytes .../fanfix_swing/images/arrow_left-32x32.png | Bin 0 -> 4784 bytes .../fanfix_swing/images/arrow_left-64x64.png | Bin 0 -> 6008 bytes .../fanfix_swing/images/arrow_left.png | Bin 0 -> 2900 bytes .../fanfix_swing/images/arrow_right-16x16.png | Bin 0 -> 624 bytes .../fanfix_swing/images/arrow_right-24x24.png | Bin 0 -> 537 bytes .../fanfix_swing/images/arrow_right-32x32.png | Bin 0 -> 1088 bytes .../fanfix_swing/images/arrow_right-64x64.png | Bin 0 -> 2259 bytes .../fanfix_swing/images/arrow_right.png | Bin 0 -> 386 bytes .../fanfix_swing/images/arrow_up-16x16.png | Bin 0 -> 4322 bytes .../fanfix_swing/images/arrow_up-24x24.png | Bin 0 -> 4229 bytes .../fanfix_swing/images/arrow_up-32x32.png | Bin 0 -> 4773 bytes .../fanfix_swing/images/arrow_up-64x64.png | Bin 0 -> 5976 bytes .../nikiroo/fanfix_swing/images/arrow_up.png | Bin 0 -> 3029 bytes .../fanfix_swing/images/clear-16x16.png | Bin 0 -> 4627 bytes .../fanfix_swing/images/clear-24x24.png | Bin 0 -> 5286 bytes .../fanfix_swing/images/clear-32x32.png | Bin 0 -> 5979 bytes .../fanfix_swing/images/clear-64x64.png | Bin 0 -> 8778 bytes src/be/nikiroo/fanfix_swing/images/clear.png | Bin 0 -> 4412 bytes src/be/nikiroo/fanfix_swing/images/convert.sh | 15 + .../fanfix_swing/images/empty-16x16.png | Bin 0 -> 3994 bytes .../fanfix_swing/images/empty-24x24.png | Bin 0 -> 4216 bytes .../fanfix_swing/images/empty-32x32.png | Bin 0 -> 3994 bytes .../nikiroo/fanfix_swing/images/empty-4x4.png | Bin 0 -> 3993 bytes .../fanfix_swing/images/empty-64x64.png | Bin 0 -> 3997 bytes .../nikiroo/fanfix_swing/images/empty-8x8.png | Bin 0 -> 3993 bytes src/be/nikiroo/fanfix_swing/images/empty.png | Bin 0 -> 2855 bytes .../fanfix_swing/images/search-16x16.png | Bin 0 -> 864 bytes .../fanfix_swing/images/search-24x24.png | Bin 0 -> 1095 bytes .../fanfix_swing/images/search-32x32.png | Bin 0 -> 1302 bytes .../fanfix_swing/images/search-64x64.png | Bin 0 -> 1927 bytes src/be/nikiroo/fanfix_swing/images/search.png | Bin 0 -> 3786 bytes .../fanfix_swing/images/unknown-16x16.png | Bin 0 -> 555 bytes .../fanfix_swing/images/unknown-24x24.png | Bin 0 -> 754 bytes .../fanfix_swing/images/unknown-32x32.png | Bin 0 -> 978 bytes .../fanfix_swing/images/unknown-64x64.png | Bin 0 -> 2190 bytes .../nikiroo/fanfix_swing/images/unknown.png | Bin 0 -> 8098 bytes 62 files changed, 3941 insertions(+) create mode 100644 src/be/nikiroo/fanfix_swing/Actions.java create mode 100644 src/be/nikiroo/fanfix_swing/Main.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/BooksPanel.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/BrowserPanel.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/DetailsPanel.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/MainFrame.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/SearchBar.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/book/BookBlock.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/book/BookCoverImager.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/book/BookInfo.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/book/BookLine.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/browser/AuthorTab.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/browser/BasicTab.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/browser/SourceTab.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/browser/TagsTab.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/utils/TreeCellSpanner.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/utils/TreeModelTransformer.java create mode 100644 src/be/nikiroo/fanfix_swing/gui/utils/UiHelper.java create mode 100644 src/be/nikiroo/fanfix_swing/images/IconGenerator.java create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_down-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_down-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_down-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_down-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_down.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_left-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_left-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_left-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_left-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_left.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_right-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_right-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_right-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_right-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_right.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_up-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_up-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_up-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_up-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/arrow_up.png create mode 100644 src/be/nikiroo/fanfix_swing/images/clear-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/clear-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/clear-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/clear-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/clear.png create mode 100755 src/be/nikiroo/fanfix_swing/images/convert.sh create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-4x4.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty-8x8.png create mode 100644 src/be/nikiroo/fanfix_swing/images/empty.png create mode 100644 src/be/nikiroo/fanfix_swing/images/search-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/search-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/search-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/search-64x64.png create mode 100644 src/be/nikiroo/fanfix_swing/images/search.png create mode 100644 src/be/nikiroo/fanfix_swing/images/unknown-16x16.png create mode 100644 src/be/nikiroo/fanfix_swing/images/unknown-24x24.png create mode 100644 src/be/nikiroo/fanfix_swing/images/unknown-32x32.png create mode 100644 src/be/nikiroo/fanfix_swing/images/unknown-64x64.png create mode 100755 src/be/nikiroo/fanfix_swing/images/unknown.png diff --git a/src/be/nikiroo/fanfix_swing/Actions.java b/src/be/nikiroo/fanfix_swing/Actions.java new file mode 100644 index 00000000..dedf4bad --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/Actions.java @@ -0,0 +1,154 @@ +package be.nikiroo.fanfix_swing; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Window; +import java.io.File; +import java.io.IOException; + +import javax.swing.JDialog; +import javax.swing.JLabel; +import javax.swing.SwingWorker; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.UiConfig; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.library.BasicLibrary; + +public class Actions { + static public void openExternal(final BasicLibrary lib, MetaData meta, Container parent, final Runnable onDone) { + while (!(parent instanceof Window) && parent != null) { + parent = parent.getParent(); + } + + // TODO: UI + final JDialog wait = new JDialog((Window) parent); + wait.setTitle("Opening story"); + wait.setSize(400, 300); + wait.setLayout(new BorderLayout()); + wait.add(new JLabel("Waiting...")); + + // TODO: pg? + + final Object waitLock = new Object(); + final Boolean[] waitScreen = new Boolean[] { false }; + new Thread(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + } + + synchronized (waitLock) { + if (!waitScreen[0]) { + waitScreen[0] = true; + wait.setVisible(true); + } + } + } + }).start(); + + final String luid = meta.getLuid(); + final boolean isImageDocument = meta.isImageDocument(); + + final SwingWorker worker = new SwingWorker() { + private File target; + + @Override + protected File doInBackground() throws Exception { + target = lib.getFile(luid, null); + return null; + } + + @Override + protected void done() { + try { + openExternal(target, isImageDocument); + } catch (IOException e) { + // TODO: error? + e.printStackTrace(); + } + + synchronized (waitLock) { + if (waitScreen[0]) { + wait.setVisible(false); + } + waitScreen[0] = true; + } + + if (onDone != null) { + onDone.run(); + } + } + }; + + worker.execute(); + } + + /** + * Open the {@link Story} with an external reader (the program will be passed + * the given target file). + * + * @param target the target {@link File} + * @param isImageDocument TRUE for image documents, FALSE for not-images + * documents + * + * @throws IOException in case of I/O error + */ + static public void openExternal(File target, boolean isImageDocument) throws IOException { + String program = null; + if (isImageDocument) { + program = Instance.getInstance().getUiConfig().getString(UiConfig.IMAGES_DOCUMENT_READER); + } else { + program = Instance.getInstance().getUiConfig().getString(UiConfig.NON_IMAGES_DOCUMENT_READER); + } + + if (program != null && program.trim().isEmpty()) { + program = null; + } + + start(target, program, false); + } + + /** + * Start a file and open it with the given program if given or the first default + * system starter we can find. + * + * @param target the target to open + * @param program the program to use or NULL for the default system starter + * @param sync execute the process synchronously (wait until it is terminated + * before returning) + * + * @throws IOException in case of I/O error + */ + static protected void start(File target, String program, boolean sync) throws IOException { + Process proc = null; + if (program == null) { + boolean ok = false; + for (String starter : new String[] { "xdg-open", "open", "see", "start", "run" }) { + try { + Instance.getInstance().getTraceHandler().trace("starting external program"); + proc = Runtime.getRuntime().exec(new String[] { starter, target.getAbsolutePath() }); + ok = true; + break; + } catch (IOException e) { + } + } + if (!ok) { + throw new IOException("Cannot find a program to start the file"); + } + } else { + Instance.getInstance().getTraceHandler().trace("starting external program"); + proc = Runtime.getRuntime().exec(new String[] { program, target.getAbsolutePath() }); + } + + if (proc != null && sync) { + try { + proc.waitFor(); + } catch (InterruptedException e) { + } + } + } +} diff --git a/src/be/nikiroo/fanfix_swing/Main.java b/src/be/nikiroo/fanfix_swing/Main.java new file mode 100644 index 00000000..c3d87a1c --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/Main.java @@ -0,0 +1,52 @@ +package be.nikiroo.fanfix_swing; + +import javax.swing.JFrame; + +import be.nikiroo.fanfix.DataLoader; +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.Config; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix.library.LocalLibrary; +import be.nikiroo.fanfix_swing.gui.MainFrame; +import be.nikiroo.utils.ui.UIUtils; + +public class Main { + public static void main(String[] args) { + UIUtils.setLookAndFeel(); + + final String forceLib = null; + // = "$HOME/Books/local"; + + if (forceLib == null) { + Instance.init(); + } else { + Instance.init(new Instance() { + private DataLoader cache; + private BasicLibrary lib; + + @Override + public DataLoader getCache() { + if (cache == null) { + cache = new DataLoader(getConfig().getString(Config.NETWORK_USER_AGENT)); + } + + return cache; + } + + @Override + public BasicLibrary getLibrary() { + if (lib == null) { + lib = new LocalLibrary(getFile(forceLib), getConfig()) { + }; + } + + return lib; + } + }); + } + + JFrame main = new MainFrame(true, true); + main.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + main.setVisible(true); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java new file mode 100644 index 00000000..5a2b9949 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/BooksPanel.java @@ -0,0 +1,336 @@ +package be.nikiroo.fanfix_swing.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.Image; +import java.awt.Point; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ExecutionException; + +import javax.swing.DefaultListModel; +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.ListCellRenderer; +import javax.swing.ListSelectionModel; +import javax.swing.SwingUtilities; +import javax.swing.SwingWorker; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix_swing.Actions; +import be.nikiroo.fanfix_swing.gui.book.BookBlock; +import be.nikiroo.fanfix_swing.gui.book.BookInfo; +import be.nikiroo.fanfix_swing.gui.book.BookLine; +import be.nikiroo.fanfix_swing.gui.utils.UiHelper; + +public class BooksPanel extends JPanel { + class ListModel extends DefaultListModel { + public void fireElementChanged(BookInfo element) { + int index = indexOf(element); + if (index >= 0) { + fireContentsChanged(element, index, index); + } + } + } + + private List bookInfos = new ArrayList(); + private Map books = new HashMap(); + private boolean seeWordCount; + private boolean listMode; + + private JList list; + private int hoveredIndex = -1; + private ListModel data = new ListModel(); + + private SearchBar searchBar; + + private Queue updateBookQueue = new LinkedList(); + private Object updateBookQueueLock = new Object(); + + public BooksPanel(boolean listMode) { + setLayout(new BorderLayout()); + + searchBar = new SearchBar(); + add(searchBar, BorderLayout.NORTH); + + searchBar.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + reload(searchBar.getText()); + } + }); + + add(UiHelper.scroll(initList(listMode)), BorderLayout.CENTER); + + Thread bookBlocksUpdater = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + BasicLibrary lib = Instance.getInstance().getLibrary(); + while (true) { + final BookBlock book; + synchronized (updateBookQueueLock) { + if (!updateBookQueue.isEmpty()) { + book = updateBookQueue.remove(); + } else { + book = null; + break; + } + } + + try { + final Image coverImage = BookBlock.generateCoverImage(lib, book.getInfo()); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + try { + book.setCoverImage(coverImage); + data.fireElementChanged(book.getInfo()); + } catch (Exception e) { + } + } + }); + } catch (Exception e) { + } + } + + try { + Thread.sleep(10); + } catch (InterruptedException e) { + } + } + } + }); + bookBlocksUpdater.setName("BookBlocks visual updater"); + bookBlocksUpdater.setDaemon(true); + bookBlocksUpdater.start(); + } + + // null or empty -> all sources + // sources hierarchy supported ("source/" will includes all "source" and + // "source/*") + public void load(final List sources, final List authors, final List tags) { + new SwingWorker, Void>() { + @Override + protected List doInBackground() throws Exception { + List bookInfos = new ArrayList(); + BasicLibrary lib = Instance.getInstance().getLibrary(); + for (MetaData meta : lib.getList(null).filter(sources, authors, tags)) { + bookInfos.add(BookInfo.fromMeta(lib, meta)); + } + + return bookInfos; + } + + @Override + protected void done() { + try { + load(get()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + // TODO: error + } + }.execute(); + } + + public void load(List bookInfos) { + this.bookInfos.clear(); + this.bookInfos.addAll(bookInfos); + synchronized (updateBookQueueLock) { + updateBookQueue.clear(); + } + + reload(searchBar.getText()); + } + + // cannot be NULL + private void reload(String filter) { + data.clear(); + for (BookInfo bookInfo : bookInfos) { + if (filter.isEmpty() || bookInfo.getMainInfo().toLowerCase().contains(filter.toLowerCase())) { + data.addElement(bookInfo); + } + } + list.repaint(); + } + + /** + * The secondary value content: word count or author. + * + * @return TRUE to see word counts, FALSE to see authors + */ + public boolean isSeeWordCount() { + return seeWordCount; + } + + /** + * The secondary value content: word count or author. + * + * @param seeWordCount TRUE to see word counts, FALSE to see authors + */ + public void setSeeWordCount(boolean seeWordCount) { + if (this.seeWordCount != seeWordCount) { + if (books != null) { + for (BookLine book : books.values()) { + book.setSeeWordCount(seeWordCount); + } + + list.repaint(); + } + } + } + + private JList initList(boolean listMode) { + final JList list = new JList(data); + + final JPopupMenu popup = new JPopupMenu(); + JMenuItem open = popup.add("Open"); + open.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int[] selected = list.getSelectedIndices(); + if (selected.length == 1) { + final BookInfo book = data.get(selected[0]); + BasicLibrary lib = Instance.getInstance().getLibrary(); + Actions.openExternal(lib, book.getMeta(), BooksPanel.this, new Runnable() { + @Override + public void run() { + data.fireElementChanged(book); + } + }); + } + } + }); + + list.addMouseMotionListener(new MouseAdapter() { + @Override + public void mouseMoved(MouseEvent me) { + if (popup.isShowing()) + return; + + Point p = new Point(me.getX(), me.getY()); + int index = list.locationToIndex(p); + if (index != hoveredIndex) { + hoveredIndex = index; + list.repaint(); + } + } + }); + list.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + check(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + check(e); + } + + @Override + public void mouseExited(MouseEvent e) { + if (popup.isShowing()) + return; + + if (hoveredIndex > -1) { + hoveredIndex = -1; + list.repaint(); + } + } + + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + if (e.getClickCount() == 2) { + int index = list.locationToIndex(e.getPoint()); + list.setSelectedIndex(index); + + final BookInfo book = data.get(index); + BasicLibrary lib = Instance.getInstance().getLibrary(); + + Actions.openExternal(lib, book.getMeta(), BooksPanel.this, new Runnable() { + @Override + public void run() { + data.fireElementChanged(book); + } + }); + } + } + + private void check(MouseEvent e) { + if (e.isPopupTrigger()) { + if (list.getSelectedIndices().length <= 1) { + list.setSelectedIndex(list.locationToIndex(e.getPoint())); + } + + popup.show(list, e.getX(), e.getY()); + } + } + }); + + list.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + list.setSelectedIndex(0); + list.setCellRenderer(generateRenderer()); + list.setVisibleRowCount(0); + + this.list = list; + setListMode(listMode); + return this.list; + } + + private ListCellRenderer generateRenderer() { + return new ListCellRenderer() { + @Override + public Component getListCellRendererComponent(JList list, BookInfo value, int index, + boolean isSelected, boolean cellHasFocus) { + BookLine book = books.get(value); + if (book == null) { + if (listMode) { + book = new BookLine(value, seeWordCount); + } else { + book = new BookBlock(value, seeWordCount); + synchronized (updateBookQueueLock) { + updateBookQueue.add((BookBlock) book); + } + } + books.put(value, book); + } + + book.setSelected(isSelected); + book.setHovered(index == hoveredIndex); + return book; + } + }; + } + + public boolean isListMode() { + return listMode; + } + + public void setListMode(boolean listMode) { + this.listMode = listMode; + books.clear(); + list.setLayoutOrientation(listMode ? JList.VERTICAL : JList.HORIZONTAL_WRAP); + + if (listMode) { + synchronized (updateBookQueueLock) { + updateBookQueue.clear(); + } + } + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/BrowserPanel.java b/src/be/nikiroo/fanfix_swing/gui/BrowserPanel.java new file mode 100644 index 00000000..47e55e42 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/BrowserPanel.java @@ -0,0 +1,218 @@ +package be.nikiroo.fanfix_swing.gui; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.List; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTabbedPane; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix_swing.gui.book.BookInfo; +import be.nikiroo.fanfix_swing.gui.browser.AuthorTab; +import be.nikiroo.fanfix_swing.gui.browser.BasicTab; +import be.nikiroo.fanfix_swing.gui.browser.SourceTab; +import be.nikiroo.fanfix_swing.gui.browser.TagsTab; +import be.nikiroo.fanfix_swing.gui.utils.UiHelper; + +/** + * Panel dedicated to browse the stories through different means: by authors, by + * tags or by sources. + * + * @author niki + */ +public class BrowserPanel extends JPanel { + private static final long serialVersionUID = 1L; + + /** + * The {@link ActionEvent} you receive from + * {@link BrowserPanel#addActionListener(ActionListener)} can return this as a + * command (see {@link ActionEvent#getActionCommand()}) if they were created in + * the scope of a source. + */ + static public final String SOURCE_SELECTION = "source_selection"; + /** + * The {@link ActionEvent} you receive from + * {@link BrowserPanel#addActionListener(ActionListener)} can return this as a + * command (see {@link ActionEvent#getActionCommand()}) if they were created in + * the scope of an author. + */ + static public final String AUTHOR_SELECTION = "author_selection"; + /** + * The {@link ActionEvent} you receive from + * {@link BrowserPanel#addActionListener(ActionListener)} can return this as a + * command (see {@link ActionEvent#getActionCommand()}) if they were created in + * the scope of a tag. + */ + static public final String TAGS_SELECTION = "tags_selection"; + + private JTabbedPane tabs; + private SourceTab sourceTab; + private AuthorTab authorTab; + private TagsTab tagsTab; + + private boolean keepSelection; + + /** + * Create a nesw {@link BrowserPanel}. + */ + public BrowserPanel() { + this.setPreferredSize(new Dimension(200, 800)); + + this.setLayout(new BorderLayout()); + tabs = new JTabbedPane(); + + int index = 0; + tabs.add(sourceTab = new SourceTab(index++, SOURCE_SELECTION)); + tabs.add(authorTab = new AuthorTab(index++, AUTHOR_SELECTION)); + tabs.add(tagsTab = new TagsTab(index++, TAGS_SELECTION)); + + setText(tabs, sourceTab, "Sources", "Tooltip for Sources"); + setText(tabs, authorTab, "Authors", "Tooltip for Authors"); + setText(tabs, tagsTab, "Tags", "Tooltip for Tags"); + + JPanel options = new JPanel(); + options.setLayout(new BorderLayout()); + + final JButton keep = new JButton("Keep selection"); + UiHelper.setButtonPressed(keep, keepSelection); + keep.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + keepSelection = !keepSelection; + UiHelper.setButtonPressed(keep, keepSelection); + keep.setSelected(keepSelection); + if (!keepSelection) { + unselect(); + } + } + }); + + options.add(keep, BorderLayout.CENTER); + + add(tabs, BorderLayout.CENTER); + add(options, BorderLayout.SOUTH); + + tabs.addChangeListener(new ChangeListener() { + @Override + public void stateChanged(ChangeEvent e) { + if (!keepSelection) { + unselect(); + } + } + }); + } + + @SuppressWarnings("rawtypes") + private void unselect() { + for (int i = 0; i < tabs.getTabCount(); i++) { + if (i == tabs.getSelectedIndex()) + continue; + + BasicTab tab = (BasicTab) tabs.getComponent(i); + tab.unselect(); + } + } + + private void setText(JTabbedPane tabs, @SuppressWarnings("rawtypes") BasicTab tab, String name, String tooltip) { + tab.setBaseTitle(name); + tabs.setTitleAt(tab.getIndex(), tab.getTitle()); + tabs.setToolTipTextAt(tab.getIndex(), tooltip); + listenTitleChange(tabs, tab); + } + + private void listenTitleChange(final JTabbedPane tabs, @SuppressWarnings("rawtypes") final BasicTab tab) { + tab.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + tabs.setTitleAt(tab.getIndex(), tab.getTitle()); + } + }); + } + + /** + * Get the {@link BookInfo} to highlight, even if more than one are selected. + *

+ * Return NULL when nothing is selected. + * + * @return the {@link BookInfo} to highlight, can be NULL + */ + public BookInfo getHighlight() { + BasicLibrary lib = Instance.getInstance().getLibrary(); + if (tabs.getSelectedComponent() == sourceTab) { + List sel = sourceTab.getSelectedElements(); + if (!sel.isEmpty()) { + return BookInfo.fromSource(lib, sel.get(0)); + } + } else if (tabs.getSelectedComponent() == authorTab) { + List sel = authorTab.getSelectedElements(); + if (!sel.isEmpty()) { + return BookInfo.fromAuthor(lib, sel.get(0)); + } + } else if (tabs.getSelectedComponent() == tagsTab) { + List sel = tagsTab.getSelectedElements(); + if (!sel.isEmpty()) { + return BookInfo.fromTag(lib, sel.get(0)); + } + } + + return null; + } + + /** + * The currently selected sources, or an empty list. + * + * @return the sources (cannot be NULL) + */ + public List getSelectedSources() { + return sourceTab.getSelectedElements(); + } + + /** + * The currently selected authors, or an empty list. + * + * @return the sources (cannot be NULL) + */ + public List getSelectedAuthors() { + return authorTab.getSelectedElements(); + } + + /** + * The currently selected tags, or an empty list. + * + * @return the sources (cannot be NULL) + */ + public List getSelectedTags() { + return tagsTab.getSelectedElements(); + } + + /** + * Adds the specified action listener to receive action events from this + * {@link SearchBar}. + * + * @param listener the action listener to be added + */ + public synchronized void addActionListener(ActionListener listener) { + sourceTab.addActionListener(listener); + authorTab.addActionListener(listener); + tagsTab.addActionListener(listener); + } + + /** + * Removes the specified action listener so that it no longer receives action + * events from this {@link SearchBar}. + * + * @param listener the action listener to be removed + */ + public synchronized void removeActionListener(ActionListener listener) { + sourceTab.removeActionListener(listener); + authorTab.removeActionListener(listener); + tagsTab.removeActionListener(listener); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/DetailsPanel.java b/src/be/nikiroo/fanfix_swing/gui/DetailsPanel.java new file mode 100644 index 00000000..da1af4c4 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/DetailsPanel.java @@ -0,0 +1,104 @@ +package be.nikiroo.fanfix_swing.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Image; +import java.util.concurrent.ExecutionException; + +import javax.swing.ImageIcon; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.SwingWorker; +import javax.swing.border.EmptyBorder; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix_swing.gui.book.BookBlock; +import be.nikiroo.fanfix_swing.gui.book.BookInfo; + +/** + * Display detailed informations about a {@link BookInfo}. + *

+ * Actually, just its name, the number of stories it contains and a small image + * if possible. + * + * @author niki + */ +public class DetailsPanel extends JPanel { + private static final long serialVersionUID = 1L; + + private JLabel icon; + private JLabel name; + private JLabel opt; + + /** + * Create a new {@link DetailsPanel}. + */ + public DetailsPanel() { + this.setLayout(new BorderLayout()); + + this.setPreferredSize(new Dimension(300, 300)); + this.setMinimumSize(new Dimension(200, 200)); + + icon = config(new JLabel(), Color.black); + name = config(new JLabel(), Color.black); + opt = config(new JLabel(), Color.gray); + + JPanel panel = new JPanel(new BorderLayout()); + panel.add(name, BorderLayout.NORTH); + panel.add(opt, BorderLayout.SOUTH); + panel.setBorder(new EmptyBorder(0, 0, 10, 0)); + + this.add(icon, BorderLayout.CENTER); + this.add(panel, BorderLayout.SOUTH); + + setBook(null); + } + + /** + * Configure a {@link JLabel} with the given colour. + * + * @param label the label to configure + * @param color the colour to use + * + * @return the (same) configured label + */ + private JLabel config(JLabel label, Color color) { + label.setAlignmentX(CENTER_ALIGNMENT); + label.setHorizontalAlignment(JLabel.CENTER); + label.setHorizontalTextPosition(JLabel.CENTER); + label.setForeground(color); + return label; + } + + /** + * Set the {@link BookInfo} you want to see displayed here. + * + * @param info the {@link BookInfo} to display + */ + public void setBook(final BookInfo info) { + icon.setIcon(null); + if (info == null) { + name.setText(null); + opt.setText(null); + } else { + name.setText(info.getMainInfo()); + opt.setText(info.getSecondaryInfo(true)); + new SwingWorker() { + @Override + protected Image doInBackground() throws Exception { + return BookBlock.generateCoverImage(Instance.getInstance().getLibrary(), info); + } + + @Override + protected void done() { + try { + icon.setIcon(new ImageIcon(get())); + } catch (InterruptedException e) { + } catch (ExecutionException e) { + } + } + }.execute(); + } + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/MainFrame.java b/src/be/nikiroo/fanfix_swing/gui/MainFrame.java new file mode 100644 index 00000000..0f94fa57 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/MainFrame.java @@ -0,0 +1,116 @@ +package be.nikiroo.fanfix_swing.gui; + +import java.awt.Color; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.IOException; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JSplitPane; +import javax.swing.SwingWorker; + +import be.nikiroo.utils.Version; + +public class MainFrame extends JFrame { + private BooksPanel books; + private DetailsPanel details; + + public MainFrame(boolean sidePanel, boolean detailsPanel) { + super("Fanfix " + Version.getCurrentVersion()); + setSize(800, 600); + setJMenuBar(createMenuBar()); + + sidePanel = true; + detailsPanel = true; + + final BrowserPanel browser = new BrowserPanel(); + + JComponent other = null; + boolean orientationH = true; + if (sidePanel && !detailsPanel) { + other = browser; + } else if (sidePanel && detailsPanel) { + JComponent side = browser; + details = new DetailsPanel(); + other = split(side, details, false, 0.5, 1); + } else if (!sidePanel && !detailsPanel) { + orientationH = false; + other = new JLabel("<< Go back"); + } else if (!sidePanel && detailsPanel) { + JComponent goBack = new JLabel("<< Go back"); + details = new DetailsPanel(); + other = split(goBack, details, false, 0.5, 1); + } + + books = new BooksPanel(true); + browser.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + books.load(browser.getSelectedSources(), browser.getSelectedAuthors(), browser.getSelectedTags()); + details.setBook(browser.getHighlight()); + } + }); + + JSplitPane split = split(other, books, orientationH, 0.5, 0); + + this.add(split); + } + + private JSplitPane split(JComponent leftTop, JComponent rightBottom, boolean horizontal, double ratio, + double weight) { + JSplitPane split = new JSplitPane(horizontal ? JSplitPane.HORIZONTAL_SPLIT : JSplitPane.VERTICAL_SPLIT, leftTop, + rightBottom); + split.setOneTouchExpandable(true); + split.setResizeWeight(weight); + split.setContinuousLayout(true); + split.setDividerLocation(ratio); + + return split; + } + + private JMenuBar createMenuBar() { + JMenuBar bar = new JMenuBar(); + + JMenu file = new JMenu("File"); + file.setMnemonic(KeyEvent.VK_F); + + JMenuItem item1 = new JMenuItem("Uuu", KeyEvent.VK_U); + item1.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + System.out.println("Uuu: ACTION"); + } + }); + + file.add(item1); + + JMenu edit = new JMenu("Edit"); + edit.setMnemonic(KeyEvent.VK_E); + + JMenu view = new JMenu("View"); + view.setMnemonic(KeyEvent.VK_V); + + JMenuItem listMode = new JMenuItem("List mode", KeyEvent.VK_L); + listMode.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + books.setListMode(!books.isListMode()); + } + }); + + view.add(listMode); + + bar.add(file); + bar.add(edit); + bar.add(view); + + return bar; + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/SearchBar.java b/src/be/nikiroo/fanfix_swing/gui/SearchBar.java new file mode 100644 index 00000000..ee5896e1 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/SearchBar.java @@ -0,0 +1,142 @@ + +package be.nikiroo.fanfix_swing.gui; + +import java.awt.BorderLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; + +import be.nikiroo.fanfix_swing.gui.utils.UiHelper; +import be.nikiroo.fanfix_swing.images.IconGenerator; +import be.nikiroo.fanfix_swing.images.IconGenerator.Icon; +import be.nikiroo.fanfix_swing.images.IconGenerator.Size; + +/** + * A generic search/filter bar. + * + * @author niki + */ +public class SearchBar extends JPanel { + static private final long serialVersionUID = 1L; + + private JButton search; + private JTextField text; + private JButton clear; + + private boolean realTime; + + /** + * Create a new {@link SearchBar}. + */ + public SearchBar() { + setLayout(new BorderLayout()); + + search = new JButton(IconGenerator.get(Icon.search, Size.x16)); + UiHelper.setButtonPressed(search, realTime); + search.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + realTime = !realTime; + UiHelper.setButtonPressed(search, realTime); + text.requestFocus(); + + if (realTime) { + fireActionPerformed(); + } + } + }); + + text = new JTextField(); + text.addKeyListener(new KeyAdapter() { + @Override + public void keyTyped(final KeyEvent e) { + super.keyTyped(e); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + boolean empty = (text.getText().isEmpty()); + clear.setVisible(!empty); + + if (realTime) { + fireActionPerformed(); + } + } + }); + } + }); + text.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + if (!realTime) { + fireActionPerformed(); + } + } + }); + + clear = new JButton(IconGenerator.get(Icon.clear, Size.x16)); + clear.setBackground(text.getBackground()); + clear.setVisible(false); + clear.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + text.setText(""); + clear.setVisible(false); + text.requestFocus(); + + fireActionPerformed(); + } + }); + + add(search, BorderLayout.WEST); + add(text, BorderLayout.CENTER); + add(clear, BorderLayout.EAST); + } + + /** + * Adds the specified action listener to receive action events from this + * {@link SearchBar}. + * + * @param listener the action listener to be added + */ + public synchronized void addActionListener(ActionListener listener) { + listenerList.add(ActionListener.class, listener); + } + + /** + * Removes the specified action listener so that it no longer receives action + * events from this {@link SearchBar}. + * + * @param listener the action listener to be removed + */ + public synchronized void removeActionListener(ActionListener listener) { + listenerList.remove(ActionListener.class, listener); + } + + /** + * Notify the listeners of an action. + */ + protected void fireActionPerformed() { + ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, getText()); + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ActionListener.class) { + ((ActionListener) listeners[i + 1]).actionPerformed(e); + } + } + } + + /** + * Return the current text displayed by this {@link SearchBar}. + * + * @return the text + */ + public String getText() { + return text.getText(); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/book/BookBlock.java b/src/be/nikiroo/fanfix_swing/gui/book/BookBlock.java new file mode 100644 index 00000000..8c83144f --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/book/BookBlock.java @@ -0,0 +1,95 @@ +package be.nikiroo.fanfix_swing.gui.book; + +import java.awt.BorderLayout; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Image; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix_swing.gui.BooksPanel; + +/** + * A book item presented in a {@link BooksPanel}. + *

+ * Can be a story, or a comic or... a group. + * + * @author niki + */ +public class BookBlock extends BookLine { + static private final long serialVersionUID = 1L; + static private Image empty = BookCoverImager.generateCoverImage(null, (BookInfo) null); + + private JLabel title; + private Image coverImage; + + /** + * Create a new {@link BookBlock} item for the given {@link Story}. + * + * @param info the information about the story to represent + * @param seeWordCount TRUE to see word counts, FALSE to see authors + */ + public BookBlock(BookInfo info, boolean seeWordCount) { + super(info, seeWordCount); + } + + @Override + protected void init() { + coverImage = empty; + title = new JLabel(); + updateMeta(); + + JPanel filler = new JPanel(); + filler.setPreferredSize(new Dimension(BookCoverImager.getCoverWidth(), BookCoverImager.getCoverHeight())); + filler.setOpaque(false); + + setLayout(new BorderLayout(10, 10)); + add(filler, BorderLayout.CENTER); + add(title, BorderLayout.SOUTH); + } + + /** + * the cover image to use a base (see + * {@link BookCoverImager#generateCoverImage(BasicLibrary, BookInfo)}) + * + * @param coverImage the image + */ + public void setCoverImage(Image coverImage) { + this.coverImage = coverImage; + } + + @Override + public void paint(Graphics g) { + super.paint(g); + g.drawImage(coverImage, BookCoverImager.TEXT_WIDTH - BookCoverImager.COVER_WIDTH, 0, null); + BookCoverImager.paintOverlay(g, isEnabled(), isSelected(), isHovered(), getInfo().isCached()); + } + + @Override + protected void updateMeta() { + String main = getInfo().getMainInfo(); + String optSecondary = getInfo().getSecondaryInfo(isSeeWordCount()); + String color = String.format("#%X%X%X", AUTHOR_COLOR.getRed(), AUTHOR_COLOR.getGreen(), AUTHOR_COLOR.getBlue()); + title.setText(String.format( + "" + "" + "%s" + "
" + + "" + "%s" + "" + "" + "", + BookCoverImager.TEXT_WIDTH, BookCoverImager.TEXT_HEIGHT, main, color, optSecondary)); + + setBackground(BookCoverImager.getBackground(isEnabled(), isSelected(), isHovered())); + } + + /** + * Generate a cover icon based upon the given {@link BookInfo}. + * + * @param lib the library the meta comes from + * @param info the {@link BookInfo} + * + * @return the image + */ + static public java.awt.Image generateCoverImage(BasicLibrary lib, BookInfo info) { + return BookCoverImager.generateCoverImage(lib, info); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/book/BookCoverImager.java b/src/be/nikiroo/fanfix_swing/gui/book/BookCoverImager.java new file mode 100644 index 00000000..9d3aa9f3 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/book/BookCoverImager.java @@ -0,0 +1,232 @@ +package be.nikiroo.fanfix_swing.gui.book; + +import java.awt.Color; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; + +import javax.imageio.ImageIO; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix.reader.ui.GuiReaderBookInfo; +import be.nikiroo.utils.Image; +import be.nikiroo.utils.ui.ImageUtilsAwt; +import be.nikiroo.utils.ui.UIUtils; + +/** + * This class can create a cover icon ready to use for the graphical + * application. + * + * @author niki + */ +class BookCoverImager { + // TODO: export some of the configuration options? + static final int COVER_WIDTH = 100; + static final int COVER_HEIGHT = 150; + static final int SPINE_WIDTH = 5; + static final int SPINE_HEIGHT = 5; + static final int HOFFSET = 20; + static final Color SPINE_COLOR_BOTTOM = new Color(180, 180, 180); + static final Color SPINE_COLOR_RIGHT = new Color(100, 100, 100); + static final Color BORDER = Color.black; + + public static final int TEXT_HEIGHT = 50; + public static final int TEXT_WIDTH = COVER_WIDTH + 40; + + // + + static public Color getBackground(boolean enabled, boolean selected, boolean hovered) { + Color color = new Color(255, 255, 255, 0); + if (!enabled) { + } else if (selected && !hovered) { + color = new Color(80, 80, 100, 40); + } else if (!selected && hovered) { + color = new Color(230, 230, 255, 100); + } else if (selected && hovered) { + color = new Color(200, 200, 255, 100); + } + + return color; + } + + /** + * Draw a partially transparent overlay if needed depending upon the selection + * and mouse-hover states on top of the normal component, as well as a possible + * "cached" icon if the item is cached. + * + * @param g the {@link Graphics} to paint onto + * @param enabled draw an enabled overlay + * @param selected draw a selected overlay + * @param hovered draw a hovered overlay + * @param cached draw a non-cached overlay if needed + */ + static public void paintOverlay(Graphics g, boolean enabled, boolean selected, boolean hovered, boolean cached) { + Rectangle clip = g.getClipBounds(); + if (clip.getWidth() <= 0 || clip.getHeight() <= 0) { + return; + } + + int h = COVER_HEIGHT; + int w = COVER_WIDTH; + int xOffset = (TEXT_WIDTH - COVER_WIDTH) - 1; + int yOffset = HOFFSET; + + if (BORDER != null) { + if (BORDER != null) { + g.setColor(BORDER); + g.drawRect(xOffset, yOffset, COVER_WIDTH, COVER_HEIGHT); + } + + xOffset++; + yOffset++; + } + + int[] xs = new int[] { xOffset, xOffset + SPINE_WIDTH, xOffset + w + SPINE_WIDTH, xOffset + w }; + int[] ys = new int[] { yOffset + h, yOffset + h + SPINE_HEIGHT, yOffset + h + SPINE_HEIGHT, yOffset + h }; + g.setColor(SPINE_COLOR_BOTTOM); + g.fillPolygon(new Polygon(xs, ys, xs.length)); + xs = new int[] { xOffset + w, xOffset + w + SPINE_WIDTH, xOffset + w + SPINE_WIDTH, xOffset + w }; + ys = new int[] { yOffset, yOffset + SPINE_HEIGHT, yOffset + h + SPINE_HEIGHT, yOffset + h }; + g.setColor(SPINE_COLOR_RIGHT); + g.fillPolygon(new Polygon(xs, ys, xs.length)); + + Color color = getBackground(enabled, selected, hovered); + + g.setColor(color); + g.fillRect(clip.x, clip.y, clip.width, clip.height); + + if (!cached) { + UIUtils.drawEllipse3D(g, new Color(0, 80, 220), COVER_WIDTH + HOFFSET + 30, 10, 20, 20); + } + } + + /** + * Generate a cover icon based upon the given {@link MetaData}. + * + * @param lib the library the meta comes from + * @param meta the {@link MetaData} + * + * @return the image + */ + static public java.awt.Image generateCoverImage(BasicLibrary lib, MetaData meta) { + return generateCoverImage(lib, BookInfo.fromMeta(lib, meta)); + } + + /** + * The width of a cover image. + * + * @return the width + */ + static public int getCoverWidth() { + return SPINE_WIDTH + COVER_WIDTH; + } + + /** + * The height of a cover image. + * + * @return the height + */ + static public int getCoverHeight() { + return COVER_HEIGHT + HOFFSET; + } + + /** + * Generate a cover icon based upon the given {@link GuiReaderBookInfo}. + * + * @param lib the library the meta comes from (can be NULL) + * @param info the {@link GuiReaderBookInfo} + * + * @return the image + */ + static public java.awt.Image generateCoverImage(BasicLibrary lib, BookInfo info) { + BufferedImage resizedImage = null; + String id = getIconId(info); + + InputStream in = Instance.getInstance().getCache().getFromCache(id); + if (in != null) { + try { + resizedImage = ImageUtilsAwt.fromImage(new Image(in)); + in.close(); + in = null; + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + if (resizedImage == null) { + try { + Image cover = null; + if (info != null) { + cover = info.getBaseImage(lib); + } + + resizedImage = new BufferedImage(getCoverWidth(), getCoverHeight(), BufferedImage.TYPE_4BYTE_ABGR); + + Graphics2D g = resizedImage.createGraphics(); + try { + g.setColor(Color.white); + g.fillRect(0, HOFFSET, COVER_WIDTH, COVER_HEIGHT); + + if (cover != null) { + BufferedImage coverb = ImageUtilsAwt.fromImage(cover); + g.drawImage(coverb, 0, HOFFSET, COVER_WIDTH, COVER_HEIGHT, null); + } else { + g.setColor(Color.black); + g.drawLine(0, HOFFSET, COVER_WIDTH, HOFFSET + COVER_HEIGHT); + g.drawLine(COVER_WIDTH, HOFFSET, 0, HOFFSET + COVER_HEIGHT); + } + } finally { + g.dispose(); + } + + // Only save image with a cover, not the X thing + if (id != null && cover != null) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(resizedImage, "png", out); + byte[] imageBytes = out.toByteArray(); + in = new ByteArrayInputStream(imageBytes); + Instance.getInstance().getCache().addToCache(in, id); + in.close(); + in = null; + } + } catch (MalformedURLException e) { + Instance.getInstance().getTraceHandler().error(e); + } catch (IOException e) { + Instance.getInstance().getTraceHandler().error(e); + } + } + + return resizedImage; + } + + /** + * Manually clear the icon set for this item. + * + * @param info the info about the story or source/type or author + */ + static public void clearIcon(BookInfo info) { + String id = getIconId(info); + Instance.getInstance().getCache().removeFromCache(id); + } + + /** + * Get a unique ID from this {@link GuiReaderBookInfo} (note that it can be a + * story, a fake item for a source/type or a fake item for an author). + * + * @param info the info or NULL for a generic (non unique!) ID + * @return the unique ID + */ + static private String getIconId(BookInfo info) { + return (info == null ? "" : info.getId() + ".") + "book-thumb_" + SPINE_WIDTH + "x" + COVER_WIDTH + "+" + + SPINE_HEIGHT + "+" + COVER_HEIGHT + "@" + HOFFSET; + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/book/BookInfo.java b/src/be/nikiroo/fanfix_swing/gui/book/BookInfo.java new file mode 100644 index 00000000..b7dc509f --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/book/BookInfo.java @@ -0,0 +1,311 @@ +package be.nikiroo.fanfix_swing.gui.book; + +import java.awt.print.Book; +import java.io.IOException; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.bundles.StringIdGui; +import be.nikiroo.fanfix.data.MetaData; +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix.library.BasicLibrary; +import be.nikiroo.fanfix.library.CacheLibrary; +import be.nikiroo.utils.Image; +import be.nikiroo.utils.StringUtils; + +/** + * Some meta information related to a "book" (which can either be a + * {@link Story}, a fake-story grouping some authors or a fake-story grouping + * some sources/types). + * + * @author niki + */ +public class BookInfo { + /** + * The type of {@link Book} (i.e., related to a story or to something else that + * can encompass stories). + * + * @author niki + */ + public enum Type { + /** A normal story, which can be "read". */ + STORY, + /** + * A special, empty story that represents a source/type common to one or more + * normal stories. + */ + SOURCE, + /** A special, empty story that represents an author. */ + AUTHOR, + /** A special, empty story that represents a tag. **/ + TAG + } + + private Type type; + private String id; + private String value; + private String count; + + private boolean cached; + + private MetaData meta; + + /** + * For private use; see the "fromXXX" constructors instead for public use. + * + * @param type the type of book + * @param id the main id, which must uniquely identify this book and will be + * used as a unique ID later on + * @param value the main value to show (see {@link BookInfo#getMainInfo()}) + */ + protected BookInfo(Type type, String id, String value) { + this.type = type; + this.id = id; + this.value = value; + } + + /** + * The type of {@link BookInfo}. + * + * @return the type + */ + public Type getType() { + return type; + } + + /** + * Get the main info to display for this book (a title, an author, a source/type + * name...). + *

+ * Note that when {@link MetaData} about the book are present, the title inside + * is returned instead of the actual value (that way, we can update the + * {@link MetaData} and see the changes here). + * + * @return the main info, usually the title + */ + public String getMainInfo() { + if (meta != null) { + return meta.getTitle(); + } + + return value; + } + + /** + * Get the secondary info, of the given type. + * + * @param seeCount TRUE for word/image/story count, FALSE for author name + * + * @return the secondary info, never NULL + */ + public String getSecondaryInfo(boolean seeCount) { + String author = meta == null ? null : meta.getAuthor(); + String secondaryInfo = seeCount ? count : author; + + if (secondaryInfo != null && !secondaryInfo.trim().isEmpty()) { + secondaryInfo = "(" + secondaryInfo + ")"; + } else { + secondaryInfo = ""; + } + + return secondaryInfo; + } + + /** + * A unique ID for this {@link BookInfo}. + * + * @return the unique ID + */ + public String getId() { + return id; + } + + /** + * This item library cache state. + * + * @return TRUE if it is present in the {@link GuiReader} cache + */ + public boolean isCached() { + return cached; + } + + /** + * This item library cache state. + * + * @param cached TRUE if it is present in the {@link GuiReader} cache + */ + public void setCached(boolean cached) { + this.cached = cached; + } + + /** + * The {@link MetaData} associated with this book, if this book is a + * {@link Story}. + *

+ * Can be NULL for non-story books (authors or sources/types). + * + * @return the {@link MetaData} or NULL + */ + public MetaData getMeta() { + return meta; + } + + /** + * Get the base image to use to represent this book. + *

+ * The image is NOT resized in any way, this is the original version. + *

+ * It can be NULL if no image can be found for this book. + * + * @param lib the {@link BasicLibrary} to use to fetch the image (can be NULL) + * + * @return the base image, or NULL if no library or no image + * + * @throws IOException in case of I/O error + */ + public Image getBaseImage(BasicLibrary lib) throws IOException { + if (lib != null) { + switch (type) { + case STORY: + if (meta.getCover() != null) { + return meta.getCover(); + } + + if (meta.getLuid() != null) { + return lib.getCover(meta.getLuid()); + } + + return null; + case SOURCE: + return lib.getSourceCover(value); + case AUTHOR: + return lib.getAuthorCover(value); + case TAG: + return null; + } + } + + return null; + } + + /** + * Create a new book describing the given {@link Story}. + * + * @param lib the {@link BasicLibrary} to use to retrieve some more information + * about the source + * @param meta the {@link MetaData} representing the {@link Story} + * + * @return the book + */ + static public BookInfo fromMeta(BasicLibrary lib, MetaData meta) { + String uid = meta.getUuid(); + if (uid == null || uid.trim().isEmpty()) { + uid = meta.getLuid(); + } + if (uid == null || uid.trim().isEmpty()) { + uid = meta.getUrl(); + } + + BookInfo info = new BookInfo(Type.STORY, uid, meta.getTitle()); + + info.meta = meta; + info.count = StringUtils.formatNumber(meta.getWords()); + if (!info.count.isEmpty()) { + info.count = Instance.getInstance().getTransGui().getString( + meta.isImageDocument() ? StringIdGui.BOOK_COUNT_IMAGES : StringIdGui.BOOK_COUNT_WORDS, + new Object[] { info.count }); + } + + if (lib instanceof CacheLibrary) { + info.setCached(((CacheLibrary) lib).isCached(meta.getLuid())); + } else { + info.setCached(true); + } + + return info; + } + + /** + * Create a new book describing the given source/type. + * + * @param lib the {@link BasicLibrary} to use to retrieve some more + * information about the source + * @param source the source name + * + * @return the book + */ + static public BookInfo fromSource(BasicLibrary lib, String source) { + BookInfo info = new BookInfo(Type.SOURCE, "source_" + source, source); + + int size = 0; + try { + size = lib.getListBySource(source).size(); + } catch (IOException e) { + } + + info.count = StringUtils.formatNumber(size); + if (!info.count.isEmpty()) { + info.count = Instance.getInstance().getTransGui().getString(StringIdGui.BOOK_COUNT_STORIES, + new Object[] { info.count }); + } + + return info; + } + + /** + * Create a new book describing the given author. + * + * @param lib the {@link BasicLibrary} to use to retrieve some more + * information about the author + * @param author the author name + * + * @return the book + */ + static public BookInfo fromAuthor(BasicLibrary lib, String author) { + BookInfo info = new BookInfo(Type.AUTHOR, "author_" + author, author); + + int size = 0; + try { + size = lib.getListByAuthor(author).size(); + } catch (IOException e) { + } + + info.count = StringUtils.formatNumber(size); + if (!info.count.isEmpty()) { + info.count = Instance.getInstance().getTransGui().getString(StringIdGui.BOOK_COUNT_STORIES, + new Object[] { info.count }); + } + + return info; + } + + /** + * Create a new book describing the given tag. + * + * @param lib the {@link BasicLibrary} to use to retrieve some more information + * about the tag + * @param tag the tag name + * + * @return the book + */ + static public BookInfo fromTag(BasicLibrary lib, String tag) { + BookInfo info = new BookInfo(Type.TAG, "tag_" + tag, tag); + + int size = 0; + try { + for (MetaData meta : lib.getList()) { + if (meta.getTags().contains(tag)) { + size++; + } + } + } catch (IOException e) { + } + + info.count = StringUtils.formatNumber(size); + if (!info.count.isEmpty()) { + info.count = Instance.getInstance().getTransGui().getString(StringIdGui.BOOK_COUNT_STORIES, + new Object[] { info.count }); + } + + return info; + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/book/BookLine.java b/src/be/nikiroo/fanfix_swing/gui/book/BookLine.java new file mode 100644 index 00000000..6437f613 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/book/BookLine.java @@ -0,0 +1,168 @@ +package be.nikiroo.fanfix_swing.gui.book; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Graphics; + +import javax.swing.JLabel; +import javax.swing.JPanel; + +import be.nikiroo.fanfix.data.Story; +import be.nikiroo.fanfix_swing.gui.BooksPanel; + +/** + * A book item presented in a {@link BooksPanel}. + *

+ * Can be a story, or a comic or... a group. + * + * @author niki + */ +public class BookLine extends JPanel { + private static final long serialVersionUID = 1L; + + /** Colour used for the seconday item (author/word count). */ + protected static final Color AUTHOR_COLOR = new Color(128, 128, 128); + + private boolean selected; + private boolean hovered; + + private BookInfo info; + private boolean seeWordCount; + + private JLabel title; + private JLabel secondary; + private JLabel iconCached; + private JLabel iconNotCached; + + /** + * Create a new {@link BookLine} item for the given {@link Story}. + * + * @param info the information about the story to represent + * @param seeWordCount TRUE to see word counts, FALSE to see authors + */ + public BookLine(BookInfo info, boolean seeWordCount) { + this.info = info; + this.seeWordCount = seeWordCount; + + init(); + } + + /** + * Initialise this {@link BookLine}. + */ + protected void init() { + // TODO: image + iconCached = new JLabel(" "); + iconNotCached = new JLabel(" * "); + + title = new JLabel(); + secondary = new JLabel(); + secondary.setForeground(AUTHOR_COLOR); + + setLayout(new BorderLayout()); + add(title, BorderLayout.CENTER); + add(secondary, BorderLayout.EAST); + + updateMeta(); + } + + /** + * The book current selection state. + * + * @return the selection state + */ + public boolean isSelected() { + return selected; + } + + /** + * The book current selection state, + * + * @param selected TRUE if it is selected + */ + public void setSelected(boolean selected) { + if (this.selected != selected) { + this.selected = selected; + repaint(); + } + } + + /** + * The item mouse-hover state. + * + * @return TRUE if it is mouse-hovered + */ + public boolean isHovered() { + return this.hovered; + } + + /** + * The item mouse-hover state. + * + * @param hovered TRUE if it is mouse-hovered + */ + public void setHovered(boolean hovered) { + if (this.hovered != hovered) { + this.hovered = hovered; + repaint(); + } + } + + /** + * The secondary value content: word count or author. + * + * @return TRUE to see word counts, FALSE to see authors + */ + public boolean isSeeWordCount() { + return seeWordCount; + } + + /** + * The secondary value content: word count or author. + * + * @param seeWordCount TRUE to see word counts, FALSE to see authors + */ + public void setSeeWordCount(boolean seeWordCount) { + if (this.seeWordCount != seeWordCount) { + this.seeWordCount = seeWordCount; + repaint(); + } + } + + /** + * The information about the book represented by this item. + * + * @return the meta + */ + public BookInfo getInfo() { + return info; + } + + /** + * Update the title, paint the item. + */ + @Override + public void paint(Graphics g) { + updateMeta(); + super.paint(g); + } + + /** + * Update the title with the currently registered information. + */ + protected void updateMeta() { + String main = info.getMainInfo(); + String optSecondary = info.getSecondaryInfo(seeWordCount); + + //TODO: max size limit? + title.setText(main); + secondary.setText(optSecondary + " "); + + setBackground(BookCoverImager.getBackground(isEnabled(), isSelected(), isHovered())); + + remove(iconCached); + remove(iconNotCached); + add(getInfo().isCached() ? iconCached : iconNotCached, BorderLayout.WEST); + validate(); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/browser/AuthorTab.java b/src/be/nikiroo/fanfix_swing/gui/browser/AuthorTab.java new file mode 100644 index 00000000..2436e43a --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/browser/AuthorTab.java @@ -0,0 +1,54 @@ +package be.nikiroo.fanfix_swing.gui.browser; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import be.nikiroo.fanfix.Instance; + +public class AuthorTab extends BasicTab> { + public AuthorTab(int index, String listenerCommand) { + super(index, listenerCommand); + } + + @Override + protected List createEmptyData() { + return new ArrayList(); + } + + @Override + protected void fillData(List data) { + try { + List authors = Instance.getInstance().getLibrary().getAuthors(); + for (String author : authors) { + data.add(author); + } + } catch (Exception e) { + // TODO + e.printStackTrace(); + } + } + + @Override + protected String keyToElement(String key) { + return key; + } + + @Override + protected String keyToDisplay(String key) { + return key; + } + + @Override + protected int loadData(DefaultMutableTreeNode root, List authors, String filter) { + for (String author : authors) { + if (checkFilter(filter, author)) { + DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(author); + root.add(sourceNode); + } + } + + return authors.size(); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/browser/BasicTab.java b/src/be/nikiroo/fanfix_swing/gui/browser/BasicTab.java new file mode 100644 index 00000000..d467a917 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/browser/BasicTab.java @@ -0,0 +1,244 @@ +package be.nikiroo.fanfix_swing.gui.browser; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTree; +import javax.swing.SwingWorker; +import javax.swing.UIDefaults; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.plaf.TreeUI; +import javax.swing.plaf.basic.BasicTreeUI; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreeCellRenderer; +import javax.swing.tree.TreePath; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix_swing.gui.SearchBar; +import be.nikiroo.fanfix_swing.gui.utils.TreeCellSpanner; +import be.nikiroo.fanfix_swing.gui.utils.UiHelper; +import be.nikiroo.fanfix_swing.images.IconGenerator; +import be.nikiroo.fanfix_swing.images.IconGenerator.Icon; +import be.nikiroo.fanfix_swing.images.IconGenerator.Size; + +public abstract class BasicTab extends JPanel { + private int totalCount = 0; + private List selectedElements = new ArrayList(); + private T data; + private String baseTitle; + private String listenerCommand; + private int index; + + private JTree tree; + private SearchBar searchBar; + + public BasicTab(int index, String listenerCommand) { + setLayout(new BorderLayout()); + + this.index = index; + this.listenerCommand = listenerCommand; + + data = createEmptyData(); + totalCount = 0; + + final DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + + tree = new JTree(root); + tree.setUI(new BasicTreeUI()); + TreeCellSpanner spanner = new TreeCellSpanner(tree, generateCellRenderer()); + tree.setCellRenderer(spanner); + tree.setRootVisible(false); + tree.setShowsRootHandles(false); + + tree.addTreeSelectionListener(new TreeSelectionListener() { + @Override + public void valueChanged(TreeSelectionEvent e) { + List elements = new ArrayList(); + TreePath[] paths = tree.getSelectionPaths(); + if (paths != null) { + for (TreePath path : paths) { + String key = path.getLastPathComponent().toString(); + elements.add(keyToElement(key)); + } + } + + List selectedElements = new ArrayList(); + for (String element : elements) { + if (!selectedElements.contains(element)) { + selectedElements.add(element); + } + } + + BasicTab.this.selectedElements = selectedElements; + + fireActionPerformed(); + } + }); + + add(UiHelper.scroll(tree), BorderLayout.CENTER); + + searchBar = new SearchBar(); + add(searchBar, BorderLayout.NORTH); + searchBar.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + root.removeAllChildren(); + loadData(root, data, searchBar.getText()); + ((DefaultTreeModel) tree.getModel()).reload(); + fireActionPerformed(); + } + }); + + SwingWorker>, Integer> worker = new SwingWorker>, Integer>() { + @Override + protected Map> doInBackground() throws Exception { + return Instance.getInstance().getLibrary().getSourcesGrouped(); + } + + @Override + protected void done() { + fillData(data); + root.removeAllChildren(); + totalCount = loadData(root, data, searchBar.getText()); + ((DefaultTreeModel) tree.getModel()).reload(); + + fireActionPerformed(); + } + }; + worker.execute(); + } + + /** + * The currently selected elements, or an empty list. + * + * @return the sources (cannot be NULL) + */ + public List getSelectedElements() { + return selectedElements; + } + + public int getTotalCount() { + return totalCount; + } + + public String getBaseTitle() { + return baseTitle; + } + + public void setBaseTitle(String baseTitle) { + this.baseTitle = baseTitle; + } + + public String getTitle() { + String title = getBaseTitle(); + String count = ""; + if (totalCount > 0) { + int selected = selectedElements.size(); + count = " (" + (selected > 0 ? selected + "/" : "") + totalCount + ")"; + } + + return title + count; + } + + public int getIndex() { + return index; + } + + public void unselect() { + tree.clearSelection(); + } + + /** + * Adds the specified action listener to receive action events from this + * {@link SearchBar}. + * + * @param listener the action listener to be added + */ + public synchronized void addActionListener(ActionListener listener) { + listenerList.add(ActionListener.class, listener); + } + + /** + * Removes the specified action listener so that it no longer receives action + * events from this {@link SearchBar}. + * + * @param listener the action listener to be removed + */ + public synchronized void removeActionListener(ActionListener listener) { + listenerList.remove(ActionListener.class, listener); + } + + /** + * Notify the listeners of an action. + */ + protected void fireActionPerformed() { + ActionEvent e = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, listenerCommand); + Object[] listeners = listenerList.getListenerList(); + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == ActionListener.class) { + ((ActionListener) listeners[i + 1]).actionPerformed(e); + } + } + } + + protected boolean checkFilter(String filter, String value) { + return (filter == null || filter.isEmpty() || value.toLowerCase().contains(filter.toLowerCase())); + } + + protected boolean checkFilter(String filter, List list) { + for (String value : list) { + if (checkFilter(filter, value)) + return true; + } + return false; + } + + protected abstract T createEmptyData(); + + protected abstract void fillData(T data); + + protected abstract String keyToElement(String key); + + protected abstract String keyToDisplay(String key); + + protected abstract int loadData(DefaultMutableTreeNode root, T data, String filter); + + private TreeCellRenderer generateCellRenderer() { + DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer() { + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, + boolean leaf, int row, boolean hasFocus) { + if (value instanceof DefaultMutableTreeNode) { + if (((DefaultMutableTreeNode) value).getLevel() > 1) { + setLeafIcon(null); + setLeafIcon(IconGenerator.get(Icon.empty, Size.x4)); + } else { + setLeafIcon(IconGenerator.get(Icon.empty, Size.x16)); + } + } + + String display = value == null ? "" : value.toString(); + if (!display.isEmpty()) + display = keyToDisplay(display); + + return super.getTreeCellRendererComponent(tree, display, selected, expanded, leaf, row, hasFocus); + } + }; + + renderer.setClosedIcon(IconGenerator.get(Icon.arrow_right, Size.x16)); + renderer.setOpenIcon(IconGenerator.get(Icon.arrow_down, Size.x16)); + renderer.setLeafIcon(IconGenerator.get(Icon.empty, Size.x16)); + + return renderer; + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/browser/SourceTab.java b/src/be/nikiroo/fanfix_swing/gui/browser/SourceTab.java new file mode 100644 index 00000000..6abb4643 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/browser/SourceTab.java @@ -0,0 +1,83 @@ +package be.nikiroo.fanfix_swing.gui.browser; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.tree.DefaultMutableTreeNode; + +import be.nikiroo.fanfix.Instance; + +public class SourceTab extends BasicTab>> { + public SourceTab(int index, String listenerCommand) { + super(index, listenerCommand); + } + + @Override + protected Map> createEmptyData() { + return new HashMap>(); + } + + @Override + protected void fillData(Map> data) { + try { + Map> sourcesGrouped = Instance.getInstance().getLibrary().getSourcesGrouped(); + for (String group : sourcesGrouped.keySet()) { + data.put(group, sourcesGrouped.get(group)); + } + } catch (Exception e) { + // TODO + e.printStackTrace(); + } + } + + @Override + protected String keyToElement(String key) { + return key.substring(1); + } + + @Override + protected String keyToDisplay(String key) { + // Get and remove type + String type = key.substring(0, 1); + key = key.substring(1); + + if (!type.equals(">")) { + // Only display the final name + int pos = key.toString().lastIndexOf("/"); + if (pos >= 0) { + key = key.toString().substring(pos + 1); + } + } + + if (key.toString().isEmpty()) { + key = " "; + } + + return key; + } + + @Override + protected int loadData(DefaultMutableTreeNode root, Map> sourcesGrouped, String filter) { + int count = 0; + for (String source : sourcesGrouped.keySet()) { + if (checkFilter(filter, source) || checkFilter(filter, sourcesGrouped.get(source))) { + boolean hasChildren = sourcesGrouped.get(source).size() > 1; + DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(">" + source + (hasChildren ? "/" : "")); + root.add(sourceNode); + for (String subSource : sourcesGrouped.get(source)) { + if (checkFilter(filter, source) || checkFilter(filter, subSource)) { + count = count + 1; + if (subSource.isEmpty() && sourcesGrouped.get(source).size() > 1) { + sourceNode.add(new DefaultMutableTreeNode(" " + source)); + } else if (!subSource.isEmpty()) { + sourceNode.add(new DefaultMutableTreeNode(" " + source + "/" + subSource)); + } + } + } + } + } + + return count; + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/browser/TagsTab.java b/src/be/nikiroo/fanfix_swing/gui/browser/TagsTab.java new file mode 100644 index 00000000..746f2689 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/browser/TagsTab.java @@ -0,0 +1,62 @@ +package be.nikiroo.fanfix_swing.gui.browser; + +import java.util.ArrayList; +import java.util.List; + +import javax.swing.tree.DefaultMutableTreeNode; + +import be.nikiroo.fanfix.Instance; +import be.nikiroo.fanfix.data.MetaData; + +public class TagsTab extends BasicTab> { + public TagsTab(int index, String listenerCommand) { + super(index, listenerCommand); + } + + @Override + protected List createEmptyData() { + return new ArrayList(); + } + + @Override + protected void fillData(List data) { + try { + List metas = Instance.getInstance().getLibrary().getList(); + for (MetaData meta : metas) { + List tags = meta.getTags(); + if (tags != null) { + for (String tag : tags) { + if (!data.contains(tag)) { + data.add(tag); + } + } + } + } + } catch (Exception e) { + // TODO + e.printStackTrace(); + } + } + + @Override + protected String keyToElement(String key) { + return key; + } + + @Override + protected String keyToDisplay(String key) { + return key; + } + + @Override + protected int loadData(DefaultMutableTreeNode root, List tags, String filter) { + for (String tag : tags) { + if (checkFilter(filter, tag)) { + DefaultMutableTreeNode sourceNode = new DefaultMutableTreeNode(tag); + root.add(sourceNode); + } + } + + return tags.size(); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/utils/TreeCellSpanner.java b/src/be/nikiroo/fanfix_swing/gui/utils/TreeCellSpanner.java new file mode 100644 index 00000000..d3a7a84f --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/utils/TreeCellSpanner.java @@ -0,0 +1,169 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +// Can be found at: https://code.google.com/archive/p/aephyr/source/default/source +// package aephyr.swing; +package be.nikiroo.fanfix_swing.gui.utils; + +import java.awt.*; +import java.awt.event.*; + +import javax.swing.*; +import javax.swing.tree.*; + +import java.util.*; + +public class TreeCellSpanner extends Container implements TreeCellRenderer, ComponentListener { + + public TreeCellSpanner(JTree tree, TreeCellRenderer renderer) { + if (tree == null || renderer == null) + throw new NullPointerException(); + this.tree = tree; + this.renderer = renderer; + treeParent = tree.getParent(); + if (treeParent != null && treeParent instanceof JViewport) { + treeParent.addComponentListener(this); + } else { + treeParent = null; + tree.addComponentListener(this); + } + } + + protected final JTree tree; + + private TreeCellRenderer renderer; + + private Component rendererComponent; + + private Container treeParent; + + private Map offsets = new HashMap(); + + private TreePath path; + + public TreeCellRenderer getRenderer() { + return renderer; + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean selected, boolean expanded, boolean leaf, int row, + boolean hasFocus) { + path = tree.getPathForRow(row); + if (path != null && path.getLastPathComponent() != value) + path = null; + rendererComponent = renderer.getTreeCellRendererComponent( + tree, value, selected, expanded, leaf, row, hasFocus); + if (getComponentCount() < 1 || getComponent(0) != rendererComponent) { + removeAll(); + add(rendererComponent); + } + return this; + } + + @Override + public void doLayout() { + int x = getX(); + if (x < 0) + return; + if (path != null) { + Integer offset = offsets.get(path); + if (offset == null || offset.intValue() != x) { + offsets.put(path, x); + fireTreePathChanged(path); + } + } + rendererComponent.setBounds(getX(), getY(), getWidth(), getHeight()); + } + + @Override + public void paint(Graphics g) { + if (rendererComponent != null) + rendererComponent.paint(g); + } + + @Override + public Dimension getPreferredSize() { + Dimension s = rendererComponent.getPreferredSize(); + // check if path count is greater than 1 to exclude the root + if (path != null && path.getPathCount() > 1) { + Integer offset = offsets.get(path); + if (offset != null) { + int width; + if (tree.getParent() == treeParent) { + width = treeParent.getWidth(); + } else { + if (treeParent != null) { + treeParent.removeComponentListener(this); + tree.addComponentListener(this); + treeParent = null; + } + if (tree.getParent() instanceof JViewport) { + treeParent = tree.getParent(); + tree.removeComponentListener(this); + treeParent.addComponentListener(this); + width = treeParent.getWidth(); + } else { + width = tree.getWidth(); + } + } + s.width = width - offset; + } + } + return s; + } + + + protected void fireTreePathChanged(TreePath path) { + if (path.getPathCount() > 1) { + // this cannot be used for the root node or else + // the entire tree will keep being revalidated ad infinitum + TreeModel model = tree.getModel(); + Object node = path.getLastPathComponent(); + if (node instanceof TreeNode && (model instanceof DefaultTreeModel + || (model instanceof TreeModelTransformer && + (model=((TreeModelTransformer)model).getModel()) instanceof DefaultTreeModel))) { + ((DefaultTreeModel)model).nodeChanged((TreeNode)node); + } else { + model.valueForPathChanged(path, node.toString()); + } + } else { + // root! + + } + } + + + private int lastWidth; + + @Override + public void componentHidden(ComponentEvent e) {} + + @Override + public void componentMoved(ComponentEvent e) {} + + @Override + public void componentResized(ComponentEvent e) { + if (e.getComponent().getWidth() != lastWidth) { + lastWidth = e.getComponent().getWidth(); + for (int row=tree.getRowCount(); --row>=0;) { + fireTreePathChanged(tree.getPathForRow(row)); + } + } + } + + @Override + public void componentShown(ComponentEvent e) {} + +} diff --git a/src/be/nikiroo/fanfix_swing/gui/utils/TreeModelTransformer.java b/src/be/nikiroo/fanfix_swing/gui/utils/TreeModelTransformer.java new file mode 100644 index 00000000..b339f664 --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/utils/TreeModelTransformer.java @@ -0,0 +1,1217 @@ +/* + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ +// Can be found at: https://code.google.com/archive/p/aephyr/source/default/source +// package aephyr.swing; +package be.nikiroo.fanfix_swing.gui.utils; + +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.HashMap; +import java.util.Iterator; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.swing.JTree; +import javax.swing.SortOrder; +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeExpansionListener; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.ExpandVetoException; +import javax.swing.tree.TreeModel; +import javax.swing.tree.TreePath; + + +public class TreeModelTransformer implements TreeModel { + + public TreeModelTransformer(JTree tree, TreeModel model) { + if (tree == null) + throw new IllegalArgumentException(); + if (model == null) + throw new IllegalArgumentException(); + this.tree = tree; + this.model = model; + handler = createHandler(); + addListeners(); + } + + private JTree tree; + + private TreeModel model; + + private Handler handler; + + private Filter filter; + + private TreePath filterStartPath; + + private int filterDepthLimit; + + private SortOrder sortOrder = SortOrder.UNSORTED; + + private Map converters; + + protected EventListenerList listenerList = new EventListenerList(); + + protected Handler createHandler() { + return new Handler(); + } + + protected void addListeners() { + tree.addTreeExpansionListener(handler); + model.addTreeModelListener(handler); + } + + protected void removeListeners() { + tree.removeTreeExpansionListener(handler); + model.removeTreeModelListener(handler); + } + + public void dispose() { + removeListeners(); + } + + public TreeModel getModel() { + return model; + } + + private Converter getConverter(Object node) { + return converters == null ? null : converters.get(node); + } + + int convertRowIndexToView(Object parent, int index) { + Converter converter = getConverter(parent); + if (converter != null) + return converter.convertRowIndexToView(index); + return index; + } + + int convertRowIndexToModel(Object parent, int index) { + Converter converter = getConverter(parent); + if (converter != null) + return converter.convertRowIndexToModel(index); + return index; + } + + @Override + public Object getChild(Object parent, int index) { + return model.getChild(parent, convertRowIndexToModel(parent, index)); + } + + @Override + public int getChildCount(Object parent) { + Converter converter = getConverter(parent); + if (converter != null) + return converter.getChildCount(); + return model.getChildCount(parent); + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + int index = model.getIndexOfChild(parent, child); + if (index < 0) + return -1; + return convertRowIndexToView(parent, index); + } + + @Override + public Object getRoot() { + return model.getRoot(); + } + + @Override + public boolean isLeaf(Object node) { + return model.isLeaf(node); + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + model.valueForPathChanged(path, newValue); + } + + @Override + public void addTreeModelListener(TreeModelListener l) { + listenerList.add(TreeModelListener.class, l); + } + + @Override + public void removeTreeModelListener(TreeModelListener l) { + listenerList.remove(TreeModelListener.class, l); + } + + /** + * Set the comparator that compares nodes in sorting. + * @param comparator + * @see #getComparator() + */ + public void setComparator(Comparator comparator) { + handler.setComparator(comparator); + } + + /** + * @return comparator that compares nodes + * @see #setComparator(Comparator) + */ + public Comparator getComparator() { + return handler.getComparator(); + } + + public void setSortOrder(SortOrder newOrder) { + SortOrder oldOrder = sortOrder; + if (oldOrder == newOrder) + return; + sortOrder = newOrder; + ArrayList paths = null; + switch (newOrder) { + case ASCENDING: + if (oldOrder == SortOrder.DESCENDING) { + flip(); + } else { + paths = sort(); + } + break; + case DESCENDING: + if (oldOrder == SortOrder.ASCENDING) { + flip(); + } else { + paths = sort(); + } + break; + case UNSORTED: + unsort(); + break; + } + fireTreeStructureChangedAndExpand(new TreePath(getRoot()), paths, true); + } + + public SortOrder getSortOrder() { + return sortOrder; + } + + public void toggleSortOrder() { + setSortOrder(sortOrder == SortOrder.ASCENDING ? + SortOrder.DESCENDING : SortOrder.ASCENDING); + } + + + /** + * Flip all sorted paths. + */ + private void flip() { + for (Converter c : converters.values()) { + flip(c.viewToModel); + } + } + + /** + * Flip array. + * @param array + */ + private static void flip(int[] array) { + for (int left=0, right=array.length-1; + left cons = converters.values().iterator(); + while (cons.hasNext()) { + Converter converter = cons.next(); + if (!converter.isFiltered()) { + cons.remove(); + } else { + Arrays.sort(converter.viewToModel); + } + } + } + } + + /** + * Sort root and expanded descendants. + * @return list of paths that were sorted + */ + private ArrayList sort() { + if (converters == null) + converters = createConvertersMap(); //new IdentityHashMap(); + return sortHierarchy(new TreePath(model.getRoot())); + } + + /** + * Sort path and expanded descendants. + * @param path + * @return list of paths that were sorted + */ + private ArrayList sortHierarchy(TreePath path) { + ValueIndexPair[] pairs = createValueIndexPairArray(20); + ArrayList list = new ArrayList(); + pairs = sort(path.getLastPathComponent(), pairs); + list.add(path); + Enumeration paths = tree.getExpandedDescendants(path); + if (paths != null) + while (paths.hasMoreElements()) { + path = paths.nextElement(); + pairs = sort(path.getLastPathComponent(), pairs); + list.add(path); + } + return list; + } + + private ValueIndexPair[] sort(Object node, ValueIndexPair[] pairs) { + Converter converter = getConverter(node); + TreeModel mdl = model; + int[] vtm; + if (converter != null) { + vtm = converter.viewToModel; + if (pairs.length < vtm.length) + pairs = createValueIndexPairArray(vtm.length); + for (int i=vtm.length; --i>=0;) { + int idx = vtm[i]; + pairs[i].index = idx; + pairs[i].value = (N)mdl.getChild(node, idx); + } + } else { + int count = mdl.getChildCount(node); + if (count <= 0) + return pairs; + if (pairs.length < count) + pairs = createValueIndexPairArray(count); + for (int i=count; --i>=0;) { + pairs[i].index = i; + pairs[i].value = (N)mdl.getChild(node, i); + } + vtm = new int[count]; + } + Arrays.sort(pairs, 0, vtm.length, handler); + for (int i=vtm.length; --i>=0;) + vtm[i] = pairs[i].index; + if (converter == null) { + converters.put(node, new Converter(vtm, false)); + } + if (sortOrder == SortOrder.DESCENDING) + flip(vtm); + return pairs; + } + + private ValueIndexPair[] createValueIndexPairArray(int len) { + ValueIndexPair[] pairs = new ValueIndexPair[len]; + for (int i=len; --i>=0;) + pairs[i] = new ValueIndexPair(); + return pairs; + } + + public void setFilter(Filter filter) { + setFilter(filter, null); + } + + public void setFilter(Filter filter, TreePath startingPath) { + setFilter(filter, null, -1); + } + + public void setFilter(Filter filter, TreePath startingPath, int depthLimit) { + if (filter == null && startingPath != null) + throw new IllegalArgumentException(); + if (startingPath != null && startingPath.getPathCount() == 1) + startingPath = null; + Filter oldFilter = this.filter; + TreePath oldStartPath = filterStartPath; + this.filter = filter; + filterStartPath = startingPath; + filterDepthLimit = depthLimit; + applyFilter(oldFilter, oldStartPath, null, true); + } + + public Filter getFilter() { + return filter; + } + + public TreePath getFilterStartPath() { + return filterStartPath; + } + + private void applyFilter(Filter oldFilter, TreePath oldStartPath, Collection expanded, boolean sort) { + TreePath startingPath = filterStartPath; + ArrayList expand = null; + if (filter == null) { + converters = null; + } else { + if (converters == null || startingPath == null) { + converters = createConvertersMap(); + } else if (oldFilter != null) { + // unfilter the oldStartPath if oldStartPath isn't descendant of startingPath + if (oldStartPath == null) { + converters = createConvertersMap(); + fireTreeStructureChangedAndExpand(new TreePath(getRoot()), null, true); + } else if (!startingPath.isDescendant(oldStartPath)) { + Object node = oldStartPath.getLastPathComponent(); + handler.removeConverter(getConverter(node), node); + fireTreeStructureChangedAndExpand(oldStartPath, null, true); + } + } + expand = new ArrayList(); + TreePath path = startingPath != null ? startingPath : new TreePath(getRoot()); + if (!applyFilter(filter, path, expand, filterDepthLimit)) { + converters.put(path.getLastPathComponent(), new Converter(Converter.EMPTY, true)); + } + } + if (startingPath == null) + startingPath = new TreePath(getRoot()); + fireTreeStructureChanged(startingPath); + if (expanded != null) + expand.retainAll(expanded); + expandPaths(expand); + if (sort && sortOrder != SortOrder.UNSORTED) { + if (filter == null) + converters = createConvertersMap(); + if (startingPath.getPathCount() > 1 && oldFilter != null) { + // upgrade startingPath or sort oldStartPath + if (oldStartPath == null) { + startingPath = new TreePath(getRoot()); + } else if (oldStartPath.isDescendant(startingPath)) { + startingPath = oldStartPath; + } else if (!startingPath.isDescendant(oldStartPath)) { + expand = sortHierarchy(oldStartPath); + fireTreeStructureChanged(oldStartPath); + expandPaths(expand); + } + } + expand = sortHierarchy(startingPath); + fireTreeStructureChanged(startingPath); + expandPaths(expand); + } + + } + + private boolean applyFilter(Filter filter, TreePath path, ArrayList expand) { + int depthLimit = filterDepthLimit; + if (depthLimit >= 0) { + depthLimit -= filterStartPath.getPathCount() - path.getPathCount(); + if (depthLimit <= 0) + return false; + } + return applyFilter(filter, path, expand, depthLimit); + } + + private boolean applyFilter(Filter filter, TreePath path, ArrayList expand, int depthLimit) { + Object node = path.getLastPathComponent(); + int count = model.getChildCount(node); + int[] viewToModel = null; + int viewIndex = 0; + boolean needsExpand = false; + boolean isExpanded = false; + if (depthLimit > 0) + depthLimit--; + for (int i=0; i 1) { + expand.add(path); + } + if (viewToModel != null) { + if (viewIndex < viewToModel.length) + viewToModel = Arrays.copyOf(viewToModel, viewIndex); + // a node must have a converter to signify that tree modifications + // need to query the filter, so have to put in converter + // even if viewIndex == viewToModel.length + converters.put(node, new Converter(viewToModel, true)); + return true; + } + return false; + } + + + private void expandPaths(ArrayList paths) { + if (paths == null || paths.isEmpty()) + return; + JTree tre = tree; + for (TreePath path : paths) + tre.expandPath(path); + } + + + private void fireTreeStructureChangedAndExpand(TreePath path, ArrayList list, boolean retainSelection) { + Enumeration paths = list != null ? + Collections.enumeration(list) : tree.getExpandedDescendants(path); + TreePath[] sel = retainSelection ? tree.getSelectionPaths() : null; + fireTreeStructureChanged(path); + if (paths != null) + while (paths.hasMoreElements()) + tree.expandPath(paths.nextElement()); + if (sel != null) + tree.setSelectionPaths(sel); + } + + + + protected void fireTreeStructureChanged(TreePath path) { + Object[] listeners = listenerList.getListenerList(); + TreeModelEvent e = null; + for (int i = listeners.length-2; i>=0; i-=2) { + if (listeners[i]==TreeModelListener.class) { + if (e == null) + e = new TreeModelEvent(this, path, null, null); + ((TreeModelListener)listeners[i+1]).treeStructureChanged(e); + } + } + } + + protected void fireTreeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) { + Object[] listeners = listenerList.getListenerList(); + TreeModelEvent e = null; + for (int i = listeners.length-2; i>=0; i-=2) { + if (listeners[i]==TreeModelListener.class) { + if (e == null) + e = new TreeModelEvent(this, path, childIndices, childNodes); + ((TreeModelListener)listeners[i+1]).treeNodesChanged(e); + } + } + } + + protected void fireTreeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) { + Object[] listeners = listenerList.getListenerList(); + TreeModelEvent e = null; + for (int i = listeners.length-2; i>=0; i-=2) { + if (listeners[i]==TreeModelListener.class) { + if (e == null) + e = new TreeModelEvent(this, path, childIndices, childNodes); + ((TreeModelListener)listeners[i+1]).treeNodesInserted(e); + } + } + } + + protected void fireTreeNodesRemoved(TreePath path, int[] childIndices, Object[] childNodes) { + Object[] listeners = listenerList.getListenerList(); + TreeModelEvent e = null; + for (int i = listeners.length-2; i>=0; i-=2) { + if (listeners[i]==TreeModelListener.class) { + if (e == null) + e = new TreeModelEvent(this, path, childIndices, childNodes); + ((TreeModelListener)listeners[i+1]).treeNodesRemoved(e); + } + } + } + + + protected class Handler implements Comparator>, + TreeModelListener, TreeExpansionListener { + + private Comparator comparator; + + private Collator collator = Collator.getInstance(); + + void setComparator(Comparator cmp) { + comparator = cmp; + collator = cmp == null ? Collator.getInstance() : null; + } + + Comparator getComparator() { + return comparator; + } + + // TODO, maybe switch to TreeWillExpandListener? + // TreeExpansionListener was used in case an expanded node + // had children that would also be expanded, but it is impossible + // for hidden nodes' expansion state to survive a SortOrder change + // since they are all erased when the tree structure change event + // is fired after changing the SortOrder. + + @Override + public void treeCollapsed(TreeExpansionEvent e) {} + + @Override + public void treeExpanded(TreeExpansionEvent e) { + if (sortOrder != SortOrder.UNSORTED) { + TreePath path = e.getPath(); + Converter converter = getConverter(path.getLastPathComponent()); + if (converter == null) { + ArrayList paths = sortHierarchy(path); + fireTreeStructureChangedAndExpand(path, paths, false); + } + } + } + + private boolean isFiltered(Object node) { + Converter c = getConverter(node); + return c == null ? false : c.isFiltered(); + } + + private boolean acceptable(TreePath path, Object[] childNodes, int index, ArrayList expand) { + return acceptable(path, childNodes, index) || + applyFilter(filter, path.pathByAddingChild(childNodes[index]), expand); + } + + @Override + public void treeNodesChanged(TreeModelEvent e) { + treeNodesChanged(e.getTreePath(), e.getChildIndices(), e.getChildren()); + } + + protected void treeNodesChanged(TreePath path, int[] childIndices, Object[] childNodes) { + if (childIndices == null) { + // path should be root path + // reapply filter + if (filter != null) + applyFilter(null, null, null, true); + return; + } + Converter converter = getConverter(path.getLastPathComponent()); + ArrayList expand = null; + if (converter != null) { + expand = new ArrayList(); + int childIndex = 0; + for (int i=0; i= 0) { + // see if the filter dislikes the nodes new state + if (converter.isFiltered() && + !isFiltered(childNodes[i]) && + !acceptable(path, childNodes, i)) { + // maybe it likes a child nodes state + if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand)) + remove(path, childNodes[i]); + continue; + } + childNodes[childIndex] = childNodes[i]; + childIndices[childIndex++] = idx; + } else if (acceptable(path, childNodes, i, expand)) { + int viewIndex = insert(path.getLastPathComponent(), + childNodes[i], childIndices[i], converter); + fireTreeNodesInserted(path, indices(viewIndex), nodes(childNodes[i])); + } + } + if (childIndex == 0) { + maybeFireStructureChange(path, expand); + return; + } + if (sortOrder != SortOrder.UNSORTED && converter.getChildCount() > 1) { + sort(path.getLastPathComponent(), createValueIndexPairArray(converter.getChildCount())); + fireTreeStructureChangedAndExpand(path, null, true); + expandPaths(expand); + return; + } + if (childIndex != childIndices.length) { + childIndices = Arrays.copyOf(childIndices, childIndex); + childNodes = Arrays.copyOf(childNodes, childIndex); + } + } else if (filter != null && isFilteredOut(path)) { + // see if the filter likes the nodes new states + expand = new ArrayList(); + int[] vtm = null; + int idx = 0; + for (int i=0; i expand) { + if (expand != null && !expand.isEmpty()) { + Enumeration expanded = tree.getExpandedDescendants(path); + fireTreeStructureChanged(path); + if (expanded != null) + while (expanded.hasMoreElements()) + tree.expandPath(expanded.nextElement()); + expandPaths(expand); + } + } + + @Override + public void treeNodesInserted(TreeModelEvent e) { + treeNodesInserted(e.getTreePath(), e.getChildIndices(), e.getChildren()); + } + + protected void treeNodesInserted(TreePath path, int[] childIndices, Object[] childNodes) { + Object parent = path.getLastPathComponent(); + Converter converter = getConverter(parent); + ArrayList expand = null; + if (converter != null) { +// if (childIndices.length > 3 && !converter.isFiltered() +// && childIndices.length > converter.getChildCount()/10) { +// TreePath expand = sortHierarchy(path); +// fireTreeStructureChangedAndExpand(expand); +// return; +// } + int childIndex = 0; + for (int i=0; i(); + if (!applyFilter(filter, path.pathByAddingChild(childNodes[i]), expand)) + continue; + } + // shift the appropriate cached modelIndices + int[] vtm = converter.viewToModel; + int modelIndex = childIndices[i]; + for (int j=vtm.length; --j>=0;) { + if (vtm[j] >= modelIndex) + vtm[j] += 1; + } + // insert modelIndex to converter + int viewIndex = insert(parent, childNodes[i], modelIndex, converter); + childNodes[childIndex] = childNodes[i]; + childIndices[childIndex++] = viewIndex; + } + if (childIndex == 0) + return; + if (childIndex != childIndices.length) { + childIndices = Arrays.copyOf(childIndices, childIndex); + childNodes = Arrays.copyOf(childNodes, childIndex); + } + if (childIndex > 1 && sortOrder != SortOrder.UNSORTED) { + sort(childIndices, childNodes); + } + } else if (filter != null && isFilteredOut(path)) { + // apply filter to inserted nodes + int[] vtm = null; + int idx = 0; + expand = new ArrayList(); + for (int i=0; i= 0) { + childNodes[len] = childNodes[i]; + childIndices[len++] = viewIndex; + } + } + if (len == 0) + return; + if (converter.isFiltered() && converter.getChildCount() == len) { + ArrayList expand = new ArrayList(); + if (applyFilter(filter, path, expand)) { + expand.retainAll(getExpandedPaths(path)); + if (sortOrder != SortOrder.UNSORTED) + sortHierarchy(path); + fireTreeStructureChangedAndExpand(path, expand, true); + } else if (isFilterStartPath(path)) { + converters.put(parent, new Converter(Converter.EMPTY, true)); + fireTreeStructureChanged(path); + } else { + remove(path.getParentPath(), parent); + } + return; + } + if (len != childIndices.length) { + childIndices = Arrays.copyOf(childIndices, len); + childNodes = Arrays.copyOf(childNodes, len); + } + if (len > 1 && sortOrder != SortOrder.UNSORTED) { + sort(childIndices, childNodes); + } + if (childIndices.length == 1) { + converter.remove(converter.convertRowIndexToModel(childIndices[0])); + } else { + converter.remove(childIndices); + } + } else if (filter != null && isFilteredOut(path)) { + return; + } + fireTreeNodesRemoved(path, childIndices, childNodes); + } + + private Collection getExpandedPaths(TreePath path) { + Enumeration en = tree.getExpandedDescendants(path); + if (en == null) + return Collections.emptySet(); + HashSet expanded = new HashSet(); + while (en.hasMoreElements()) + expanded.add(en.nextElement()); + return expanded; + } + + @Override + public void treeStructureChanged(TreeModelEvent e) { + if (converters != null) { + // not enough information to properly clean up + // reapply filter/sort + converters = createConvertersMap(); + TreePath[] sel = tree.getSelectionPaths(); + if (filter != null) { + applyFilter(null, null, getExpandedPaths(new TreePath(getRoot())), false); + } + if (sortOrder != SortOrder.UNSORTED) { + TreePath path = new TreePath(getRoot()); + ArrayList expand = sortHierarchy(path); + fireTreeStructureChangedAndExpand(path, expand, false); + } + if (sel != null) { + tree.clearSelection(); + TreePath changedPath = e.getTreePath(); + for (TreePath path : sel) { + if (!changedPath.isDescendant(path)) + tree.addSelectionPath(path); + } + } + } else { + fireTreeStructureChanged(e.getTreePath()); + } + } + + + @Override + public final int compare(ValueIndexPair a, ValueIndexPair b) { + return compareNodes(a.value, b.value); + } + + + protected int compareNodes(N a, N b) { + if (comparator != null) + return comparator.compare(a, b); + return collator.compare(a.toString(), b.toString()); + } + + private void removeConverter(Object node) { + Converter c = getConverter(node); + if (c != null) + removeConverter(c, node); + } + + private void removeConverter(Converter converter, Object node) { + for (int i=converter.getChildCount(); --i>=0;) { + int index = converter.convertRowIndexToModel(i); + Object child = model.getChild(node, index); + Converter c = getConverter(child); + if (c != null) + removeConverter(c, child); + } + converters.remove(node); + } + + private boolean isFilteredOut(TreePath path) { + if (filterStartPath != null && !filterStartPath.isDescendant(path)) + return false; + TreePath parent = path.getParentPath(); + // root should always have a converter if filter is non-null, + // so if parent is ever null, there is a bug somewhere else + Converter c = getConverter(parent.getLastPathComponent()); + if (c != null) { + return getIndexOfChild( + parent.getLastPathComponent(), + path.getLastPathComponent()) < 0; + } + return isFilteredOut(parent); + } + + private void filterIn(int[] vtm, int vtmLength, TreePath path, ArrayList expand) { + Object node = path.getLastPathComponent(); + if (vtmLength != vtm.length) + vtm = Arrays.copyOf(vtm, vtmLength); + Converter converter = new Converter(vtm, true); + converters.put(node, converter); + insert(path.getParentPath(), node); + tree.expandPath(path); + expandPaths(expand); + } + + private boolean acceptable(TreePath path, Object[] nodes, int index) { + Object node = nodes[index]; + return filter.acceptNode(path, (N)node, model.isLeaf(node)); + } + + private int ascInsertionIndex(int[] vtm, Object parent, N node, int idx) { + for (int i=vtm.length; --i>=0;) { + int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i])); + if (cmp > 0 || (cmp == 0 && idx > vtm[i])) { + return i+1; + } + } + return 0; + } + + + private int dscInsertionIndex(int[] vtm, Object parent, N node, int idx) { + for (int i=vtm.length; --i>=0;) { + int cmp = compareNodes(node, (N)model.getChild(parent, vtm[i])); + if (cmp < 0) { + return i+1; + } else if (cmp == 0 && idx < vtm[i]) { + return i; + } + } + return 0; + } + + + /** + * Inserts the specified path and node and any parent paths as necessary. + *

+ * Fires appropriate event. + * @param path + * @param node + */ + private void insert(TreePath path, Object node) { + Object parent = path.getLastPathComponent(); + Converter converter = converters.get(parent); + int modelIndex = model.getIndexOfChild(parent, node); + if (converter == null) { + converter = new Converter(indices(modelIndex), true); + converters.put(parent, converter); + insert(path.getParentPath(), parent); + } else { + int viewIndex = insert(parent, node, modelIndex, converter); + fireTreeNodesInserted(path, indices(viewIndex), nodes(node)); + } + } + + /** + * Inserts node into parent in correct sort order. + *

+ * Responsibility of caller to fire appropriate event with the returned viewIndex. + * @param path + * @param node + * @param modelIndex + * @param converter + * @return viewIndex + */ + private int insert(Object parent, Object node, int modelIndex, Converter converter) { + int[] vtm = converter.viewToModel; + int viewIndex; + switch (sortOrder) { + case ASCENDING: + viewIndex = ascInsertionIndex(vtm, parent, (N)node, modelIndex); + break; + case DESCENDING: + viewIndex = dscInsertionIndex(vtm, parent, (N)node, modelIndex); + break; + default: case UNSORTED: + viewIndex = unsortedInsertionIndex(vtm, modelIndex); + break; + } + int[] a = new int[vtm.length+1]; + System.arraycopy(vtm, 0, a, 0, viewIndex); + System.arraycopy(vtm, viewIndex, a, viewIndex+1, vtm.length-viewIndex); + a[viewIndex] = modelIndex; + converter.viewToModel = a; + return viewIndex; + } + + private void remove(TreePath path, Object node) { + Object parent = path.getLastPathComponent(); + if (path.getPathCount() == 1 || (filterStartPath != null && filterStartPath.equals(path))) { + removeConverter(node); + converters.put(parent, new Converter(Converter.EMPTY, true)); + fireTreeNodesRemoved(path, indices(0), nodes(node)); + return; + } + Converter converter = converters.get(parent); + int modelIndex = model.getIndexOfChild(parent, node); + int viewIndex = converter.remove(modelIndex); + switch (viewIndex) { + default: + removeConverter(node); + fireTreeNodesRemoved(path, indices(viewIndex), nodes(node)); + break; + case Converter.ONLY_INDEX: +// if (path.getParentPath() == null) { +// // reached filter root +// removeConverter(node); +// converters.put(parent, new Converter(Converter.EMPTY, true)); +// fireTreeNodesRemoved(path, indices(0), nodes(node)); +// return; +// } + remove(path.getParentPath(), parent); + break; + case Converter.INDEX_NOT_FOUND: + removeConverter(node); + } + } + + + + } + + + + private static int unsortedInsertionIndex(int[] vtm, int idx) { + for (int i=vtm.length; --i>=0;) + if (vtm[i] < idx) + return i+1; + return 0; + } + + private static void sort(int[] childIndices, Object[] childNodes) { + int len = childIndices.length; + ValueIndexPair[] pairs = new ValueIndexPair[len]; + for (int i=len; --i>=0;) + pairs[i] = new ValueIndexPair(childIndices[i], childNodes[i]); + Arrays.sort(pairs); + for (int i=len; --i>=0;) { + childIndices[i] = pairs[i].index; + childNodes[i] = pairs[i].value; + } + } + + private static int[] indices(int...indices) { + return indices; + } + + private static Object[] nodes(Object...nodes) { + return nodes; + } + + + + + /** + * This class has a dual purpose, both related to comparing/sorting. + *

+ * The Handler class sorts an array of ValueIndexPair based on the value. + * Used for sorting the view. + *

+ * ValueIndexPair sorts itself based on the index. + * Used for sorting childIndices for fire* methods. + */ + private static class ValueIndexPair implements Comparable> { + ValueIndexPair() {} + + ValueIndexPair(int idx, N val) { + index = idx; + value = val; + } + + N value; + + int index; + + public int compareTo(ValueIndexPair o) { + return index - o.index; + } + } + + private static class Converter { + + static final int[] EMPTY = new int[0]; + + static final int ONLY_INDEX = -2; + + static final int INDEX_NOT_FOUND = -1; + + Converter(int[] vtm, boolean filtered) { + viewToModel = vtm; + isFiltered = filtered; + } + + private int[] viewToModel; + + private boolean isFiltered; + +// public boolean equals(Converter conv) { +// if (conv == null) +// return false; +// if (isFiltered != conv.isFiltered) +// return false; +// return Arrays.equals(viewToModel, conv.viewToModel); +// } + + boolean isFiltered() { + return isFiltered; + } + + void remove(int viewIndices[]) { + int len = viewToModel.length - viewIndices.length; + if (len == 0) { + viewToModel = EMPTY; + } else { + int[] oldVTM = viewToModel; + int[] newVTM = new int[len]; + for (int oldIndex=0, newIndex=0, removeIndex=0; + newIndex=0;) + if (newVTM[i] > idx) + newVTM[i]--; + for (int i=oldIndex; i idx) + oldVTM[i]--; + } + newVTM[newIndex] = oldVTM[oldIndex]; + } + viewToModel = newVTM; + } + } + + /** + * @param modelIndex + * @return viewIndex that was removed
+ * or ONLY_INDEX if the modelIndex is the only one in the view
+ * or INDEX_NOT_FOUND if the modelIndex is not in the view + */ + int remove(int modelIndex) { + int[] vtm = viewToModel; + for (int i=vtm.length; --i>=0;) { + if (vtm[i] > modelIndex) { + vtm[i] -= 1; + } else if (vtm[i] == modelIndex) { + if (vtm.length == 1) { + viewToModel = EMPTY; + return ONLY_INDEX; + } + int viewIndex = i; + while (--i>=0) { + if (vtm[i] > modelIndex) + vtm[i] -= 1; + } + int[] a = new int[vtm.length-1]; + if (viewIndex > 0) + System.arraycopy(vtm, 0, a, 0, viewIndex); + int len = a.length-viewIndex; + if (len > 0) + System.arraycopy(vtm, viewIndex+1, a, viewIndex, len); + viewToModel = a; + return viewIndex; + } + } + return INDEX_NOT_FOUND; + } + + + int getChildCount() { + return viewToModel.length; + } + + /** + * @param modelIndex + * @return viewIndex corresponding to modelIndex
+ * or INDEX_NOT_FOUND if the modelIndex is not in the view + */ + int convertRowIndexToView(int modelIndex) { + int[] vtm = viewToModel; + for (int i=vtm.length; --i>=0;) { + if (vtm[i] == modelIndex) + return i; + } + return INDEX_NOT_FOUND; + } + + int convertRowIndexToModel(int viewIndex) { + return viewToModel[viewIndex]; + } + } + + public interface Filter { + boolean acceptNode(TreePath parent, N node, boolean leaf); + } + + public static class RegexFilter implements Filter { + + public RegexFilter(Pattern pattern, boolean leaf) { + matcher = pattern.matcher(""); + leafOnly = leaf; + } + + private Matcher matcher; + + private boolean leafOnly; + + public boolean acceptNode(TreePath parent, N node, boolean leaf) { + if (leafOnly && !leaf) + return false; + matcher.reset(getStringValue(node)); + return matcher.find(); + } + + protected String getStringValue(N node) { + return node.toString(); + } + } + + + private static Map createConvertersMap() { + return new HashMap(); + } +} diff --git a/src/be/nikiroo/fanfix_swing/gui/utils/UiHelper.java b/src/be/nikiroo/fanfix_swing/gui/utils/UiHelper.java new file mode 100644 index 00000000..00f5f4ee --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/gui/utils/UiHelper.java @@ -0,0 +1,48 @@ +package be.nikiroo.fanfix_swing.gui.utils; + +import java.awt.Color; +import java.awt.Container; +import java.awt.Frame; +import java.awt.Window; +import java.awt.event.ActionListener; + +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JScrollPane; +import javax.swing.SwingWorker; + +import be.nikiroo.utils.Progress; + +public class UiHelper { + static private Color buttonNormal; + static private Color buttonPressed; + + static public void setButtonPressed(JButton button, boolean pressed) { + if (buttonNormal == null) { + JButton defButton = new JButton(" "); + buttonNormal = defButton.getBackground(); + if (buttonNormal.getBlue() >= 128) { + buttonPressed = new Color( // + Math.max(buttonNormal.getRed() - 100, 0), // + Math.max(buttonNormal.getGreen() - 100, 0), // + Math.max(buttonNormal.getBlue() - 100, 0)); + } else { + buttonPressed = new Color( // + Math.min(buttonNormal.getRed() + 100, 255), // + Math.min(buttonNormal.getGreen() + 100, 255), // + Math.min(buttonNormal.getBlue() + 100, 255)); + } + } + + button.setSelected(pressed); + button.setBackground(pressed ? buttonPressed : buttonNormal); + } + + static public JComponent scroll(JComponent pane) { + JScrollPane scroll = new JScrollPane(pane); + scroll.getVerticalScrollBar().setUnitIncrement(16); + scroll.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); + return scroll; + } +} diff --git a/src/be/nikiroo/fanfix_swing/images/IconGenerator.java b/src/be/nikiroo/fanfix_swing/images/IconGenerator.java new file mode 100644 index 00000000..780b1bbd --- /dev/null +++ b/src/be/nikiroo/fanfix_swing/images/IconGenerator.java @@ -0,0 +1,121 @@ +package be.nikiroo.fanfix_swing.images; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import javax.swing.ImageIcon; + +import be.nikiroo.utils.IOUtils; + +/** + * Icons generator for this project. + * + * @author niki + */ +public class IconGenerator { + /** + * The available icons. + * + * @author niki + */ + public enum Icon { + /** Icon used to clear text fields */ + clear, + /** Search icon (magnifying glass) */ + search, + /** An interrogation point */ + unknown, + /** A small, left-pointed arrow */ + arrow_left, + /** A small, right-pointed arrow */ + arrow_right, + /** A small, up-pointed arrow */ + arrow_up, + /** A small, down-pointed arrow */ + arrow_down, + /** An empty (transparent) icon */ + empty, + } + + /** + * The available sizes. + * + * @author niki + */ + public enum Size { + /** 4x4 pixels, only for {@link Icon#empty} */ + x4(4), + /** 8x8 pixels, only for {@link Icon#empty} */ + x8(8), + /** 16x16 pixels */ + x16(16), + /** 24x24 pixels */ + x24(24), + /** 32x32 pixels */ + x32(32), + /** 64x64 pixels */ + x64(64); + + private int size; + + private Size(int size) { + this.size = size; + } + + /** + * Return the size in pixels. + * + * @return the size + */ + public int getSize() { + return size; + } + } + + static private Map map = new HashMap(); + + /** + * Generate a new image. + * + * @param name the name of the resource + * @param size the requested size + * + * @return the image, or NULL if it does not exist or does not exist at that + * size + */ + static public ImageIcon get(Icon name, Size size) { + String key = String.format("%s-%dx%d.png", name.name(), size.getSize(), size.getSize()); + if (!map.containsKey(key)) { + map.put(key, generate(key)); + } + + return map.get(key); + } + + /** + * Generate a new image. + * + * @param filename the file name of the resource (no directory) + * + * @return the image, or NULL if it does not exist or does not exist at that + * size + */ + static private ImageIcon generate(String filename) { + try { + InputStream in = IOUtils.openResource(IconGenerator.class, filename); + if (in != null) { + try { + return new ImageIcon(IOUtils.toByteArray(in)); + } finally { + in.close(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + + return null; + } +} diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_down-16x16.png b/src/be/nikiroo/fanfix_swing/images/arrow_down-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcbf8661a6a54740524c6cbe86519c4172f809e GIT binary patch literal 4335 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 E0c4+WHUIzs literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_down-24x24.png b/src/be/nikiroo/fanfix_swing/images/arrow_down-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..182db90d5c286b569ae189d1b60eb75dc01a224a GIT binary patch literal 4239 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 e<gd literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_down-32x32.png b/src/be/nikiroo/fanfix_swing/images/arrow_down-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..873f56f2d9aca4788d6819aa9617421897023ff3 GIT binary patch literal 4794 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_down-64x64.png b/src/be/nikiroo/fanfix_swing/images/arrow_down-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..ca8a3b57366145abb01b697e13e20c7ceffa5a1d GIT binary patch literal 6110 zcmeHLcU)9UmaY!aM3E$tqXMExXvsM@h)NP9XGCps5Cs|}DI%zds34#S3J6G6LCIi1 z0ZES_K~$mwf|8oPTdnWCeKWhWznR&8r*5CF(^co(`<+x>buSz_WXR0G!vFwaCK&0N z!(NL@bhPlh(A$3$c33ZMQ*8jsQW(iDH1M~On~}LG0B3ds5ETo+1{{i-1|V1lfLUh% zRBiyk<(K)?Obt%Zx|$g30fc(xJiDI?N9g^Gjs*h1$Vw#?$=&G+9K;0?O!aXeu{gjj z&Smef0RWu=K~LNA^w5`^!@k4){Oy``=gU1z%0=wT-NZ{$hv@NGy99NQ>0t%u!_8T% zohwe$f{&0phrdKUlt>hooDIHyQq`Mx%wzrT#eEn1<2YD4XD|gVChYCW>@`~JsPnq4 z9AR%)LwC`${?AswyOJ^UK8DsAUFO#dIDP6lHUh6i;a0#>Vd@)3y6qU+Xcr;=WZLnB z;r(&Xwa+xr`*9@iKk)7*dgN)al+Ckz?lPObGhw4PMSVP~%E~X5lT)vl-@?u&F49hY ze-NrY#;e{y&*gv`(wehEXDUw|)>y}aW|?^Nf?odNYEo@^x!l%xW6V66Om;5nlWS^4 zMg($LcUqtnj-acfh&fDrY_Tnem3lB=$La+$-ES>f?m2LB_(wL|1wEI*z(7B5Z$h>0 zzNQT0NJqSoz}q~gpr}5-mA4y{H_`4D$3T#SVwB$Q)R&$WgLApLxij|C_k&gVn}Z#$ znZLrDez+dz!UH7w2J-7!jGuKd_Ek*GA1qYb3eDqqz91Ug6+V_e5jUiDyFtosqJJ{U zBwI47WNyoW>o-72H4+<&)44MqHoB53e)wTp8s~{Bp27J8qZwJd(+&woT$lf_Q9 z&ePk7^gCEEB|p1RFS94_(1mZv@&KM$IQ&#Cq4;QfHSx|h6`&C9F)pc1u95zPx<3sr zHbm}DJ%%hcvmgqJii$rLCz`c~tCf|Ng|j=C8*%k+Cr+?&JoaHJaDRL{ZaZ37Equ(d zpy3t+r?h2U`#IA+aaW`1?sr}(EIr@k&-bdfR>>jZXwj#&=k*NXtZxG`N_6T?BsZ2W ze6g=K3LpJ>dRn5vQab&$AV2?mNe%|lTW!v}Q@d$tY0tOrH86O`v~8Q~G;Px=YgWPl zm)2UeqMF?!y;oe=%Z*Dt^$}z3?C1ginGn^LGwOMBPm6dBq$tiGZO?q0RaJWxz~DYYN`wRPKc!vd`2I?G}^7(f$t9He_ejw&i})b z(sd7)W3qdt3_8lQkmO@6doSATCG)4S*X(tbWj7sIjBBVShJ>#ECY|PAb|od?C}DjBhT!TilKGR&A}d`v`7gA8+r%&B`T{(R|eW-)JyiCHBj9g)=uWzlJ94?fGM$YWV(RaIY83d1v$X6#`tS#{5Q z$okHw6uQD*-r!uW9piC+Lbng;c122O&cTDZT>NK^73^E zh4^j;pEcDz6Ld)ZE&=Z$bVXaqX!`q^UY5Cs#l;m>RbRde^3AIzSL9!_Men#~o}smG zrAhIt-hMvxePzyVSA~?nO61*A^ci`qATJ+iq3!A&b~7(;*ERElhYnqR>D~o|TbAG9 zw0m+no^Mx5ZeCiF&B(}j_)zWhOiy1()ls_;hgHV0RYBIr5tpWXkhYY`boywnO)T%D zz$cm8Mlx^Qcrh^dU3Dht%stxzCIzD1@W>Fuv)%e5<&Rut&lpdb(Ax@W4a}Gq_e-RQ zT6y1}7vQtB?Xx9X(3N31o;cqo@uz=_(%3)89=koE_-K);np$S&<4AS5<_z>hmDe`c z^)<3ZuNYxHx|g+j7V|nMCRoQv2n7TUI9fZJj)_Sy3_h##4T<^034XF7E7|;))9KEyO#k9Pgh? zST30hhlzF(+qt~O#gksRTsr9x-w?JDJC>B>US44NbSms_{}}+UG&x}&viSN^sDViFV#=(_6Wh>jBT+OIkl(WPoIhLkZ z5I6TfUqEz?*R!sDizW^udpQ^o3*!PQTcnTlLTQ%)(;8;c+WG2uaDRQ&cdyi1G;8D` zRFDwMLH2OZBDU}s$0@eh>5I;>%a`jGX;%xIghc+*J`A*_P|j+dy<;0hf%}^_4;z9f zfv^YA!5s%A6c(T$w-XZTFFYEB0T?*u1BnMk^HU}W^1Q$E;gFYrGE`lvoGc0rm|zdL zLO>4k9I(F(=^iAypBEqGsM3G*h6K|iLUPiwGSW&wPDV~fMFy`TqaY+Fry_SiMPB(Y z*r;lMV59PXU}Jzh=64L#of_?5c+S6IL!p1!xSgu|J6}x&3)?Sj$ZyCv*luhxjhnab=HAQ8zyF}1 zu)LzOs=DU!lc!D1Ev;?sFJ8WS*VEhAKk(b&(8PzykDoqIP0!3OeOq4n{$q7*eS>Nj z)y_ZKFU$VUE>>t48jHnXaa6ld=uoQRtXLXhSz0!2Gn|V*yNFyg9f!`9J7taZqVnd8 zoUQ@g3_HZ|6T6nErv0?+pEK;-zh&8qj3nnA!|JB9H14gy?`X|7mWEz^W%F|1XTo@Oz~ zFnRX{KbUiUqJ+C7H^jNs%1_~Gk_*n)7W~j0QiV#{P{{@XE0$HuG-uo6$%L?_F|*Vr zibOJ_Dz8m)&QSgPlRj>XI73?V62!s(KNEWd)Gs3NNCwu=B}#RL zGmbmRw`_CDoi3%=gZzR`utHgoJ4qsP;5bDc@wBBvnWd_Jiuzl$`r zU=D9`0Rb&XaLHzl%?Mz~)j>e^GeZhR909CmCW|$C2EgG2zv_!@vOWmo=bXb$#3$m& z5hny!mo(QB53e0n*-zebLcmxH0zOD!UP+W%At25xm(=+N0bAt56p_nhe0MYgQZf** zxt2{f1z`@HvxXDZNjP%&*zbzoniF=@2v}Z@Kmdsy0T*&ur0+h{q;S54Xf+kbR)3sb5Sv9m`u(oBHEpgyvL|Xm}Lc=C=8^_hsjl zOY@69v%q*83&40gLx?M4+;YFo{>XFe*a*xXH8 zXkX7>EahDjMu2GE)K(1CH86BsH}cZ_K~Jj7z6oO$Pn7fY(>3hrg$_U2V^<7ZHEzn4 zm>3RF2Mo8e_phcdL%+&LG=6^|fsxwOa9R-mOlrE(&o=dwsgf-2kCGc|vXA&k?p(9b zX#AyMd+FUxEpC!?`sParp)iy*T^<`ruqRj5+Sm?|BgMA@1v|P#1#Jh2K^O6lA18Yn zToz)DI^=gaKwXNF!oDR%iC7>l#KCkZ)D$4S*v7K4?zBZy)F}b(CQ3$`>2rX8MAk5d z6)EJ@H@cLja?pCyeqW#HQUQO7|b^&7LF5xcQ$Pstwha} zU`40C(WEq$!HO<8`I%ugg?a7j^5pYe@i?*>-;NN4GJb8b9*%anIZQ&kODlXhI?4Tl zZ({`ki*Gep5Rh~I2~4{Q1Wa0@MyyOqzsJlt(j$OzvQ=<-A{VCUP14$@GrrBG9Al>k znpb(Q=W*6q=m&M}8J61fh3-=JjnvGt1tTwl;)tMae92+9SiY<(r`G~2QN^Y{m1GOP zz=O1k#vb|+@^(!nQEYa6Tg*o&?=m*aD2C+Y7<66xhyCQF9SHb7Tff2E4WG+p4U6!k z;3h=0R@l^ERdhRKNxI~!SWuDyRx3J+@bz%9EO)P&KH&H#C^VbC?qvQNORKeQegVx$ zmx=q2+3E)m8!zU%fA!eeBtS`AZW69X9H@Cg=p7lVcM_m?KEq+F zqP*G5;q2AE%xhQNYwMl_MyBLPi@7SGW#|JELR@`*YvkLA;B=CS>>m4e1bTOq#t;Fy z9y!~Vn>ATiNZhmL-b2AvD;9?InN>|9C0#m`pIig4^A{C;v+K1!M`l96m7!HiV<%J+ zEzR*}dey3pFXD~Tp2^zNYF38;d(HRvXjv|j30^rfvJMW0s=04P(Yvr8iv&KNvF3!1 z)768HbI_%1-A4eIb)+=uMZrk~IK6~M5FF(XYU^`Yz$Bz5;y*{F@IGFG>*BW(w&L6o z&~0Yj!8v+b>ciV-VnJ;LBH%El+a6$YOKH~z;WBVLdn*Q3hylTnEZxaQ>P$|B;kpOolJ;Wleyif>zTV0D z`ovLXw^$u4Lt{w1+elWVn-P<-#N8V-9A;YoD)CP>`SQ#>$1GX(7ZhrT;@Jk(pB5KN z$+%7rs@^n^NvO{|D@ewN#=xTEEg_AaC#`)6Z`228ab)?FB+cnvcVOYH*RS>YMIKl@ znm4ZQ$-mz+>z((f8oRiY)}BZ5Zu*7p$^69PYrN_6+<7av{^K2J{SOAX;2XTw642D= zID+~Q{jWa2A%hv5X7G(O*cGI26Xfm^T81_ zF7Evs1){IJw`bTtRv?(kQx9=G)F*hH_V!fK@eT?+;t^osLi9W30hIO%Q9o>7G{m8l z@Sjq85QU{T@fVW6JJ;P>|NH<7v;QefAi8*Y9Q5$<3OWwt@Xu~r=)!5Xzn!-BcK zaB^>EX>4U6ba`-PAZ2)IW&i+q+U=NYk{mY~v)ZB9R{7Wsh*m+a^^Y6I^pYH4P$G6YI z=lkyCgUBu6D$k#JzvDai_4^k}dhd_V_g$IqJoTOE{ovb#Vk<}9yyNOSk;LbIc>i|o z{a#PMInMhW)Azf(ck-`)r$R86G9NLDTY`9AtF0o>v-3>%HQOK_ou~3D(taL0_wn{i z@IKysiJkB7efb;;=kq%te~Hn1Kj_b6cO&!(weKr``e5X5;r%go=d60peoog@CQ{kG zhx&Gu`;LKwP|E#U=23Vm@5_BO9+e|b5L+xacuW^sh(!B@6jtcrgnh3wOtzTfj)iNC zGs?YJrNkB|Fa+S>5l$@9WI3teF+okZ9Y6LG?z-)+HzDovD=;z!CJPtGFAw*#n}6l; zG0`~|+Yo&Hh!y*a#%Z=;$k{8S03@6@rh4OhAMQ82@xK99Ku~X(D+la;JW~t}-?=3h zXJMTBFhj8iUS0KYFxn`un{t$#2RgF zzEi>-8#O++krseZ$x?u(Ne8Ty6#kS{;ZRr6P1Q_T5>6+ zmR5R|nyS`ZORcrl-eMCZOj~Znwc1+iokcrScfQ>j(ff!)MjATuD5H)x`h-3+O`Um` zS!bJl#U+dMUwIX`>T0X+u%XgUTX)`N*WGqMaP7!RM^8TG)YDG?K9ndpa!PNvFIUUQ*R)^e6lBgr|#?n<%O(EIjh=COTW38Pc`oYK|p zPMPd!$sBo)!l$|KIeSP$;`V#13Mm!#+KpUUtIQeBNTrZ3 z9z_d7dB>_Tql6jrIBQP{XD1OM9wkJcb?)SMS*~j9@lB{Nl!Q`xgJoyd zd)&%~9dw92v(7EL=We2K9XU$pGg}I!-{A?&4cdBOT=CjVyyHR$WE=^-Jk{V$SWUwP> zE}+a%&S}wmhBB~uP=gWF!aMM^n^osI+e|k^YWwMl{fu1a-H*-lY02E2oE>!RhZ}OF z+_j!PGj%h-zyP=`tAft`1o$0VmZ6Trb(NJ?Zf36UN(M%LLwz??Z9}cZLTOwP-bOt0 zDcvUdwit6$*ER)i#bKy1030AJ=e}9Ie>v&gmmB-Y4pQnW3EqTLk)NTIk~3m96wt0_ zfMPFUTD$?Exzr8R4g+>qHut6bw1g56k?hIJG28P^a;)aOB;vqmdF+HMyiq8yc$0DnM4A+a1;IDbi*{g0bBRt)QD{$a z*+BP!Vk;QsuB%hJ#8?m8CZ2w!45tD~N6{w$O5uo}3U2h=Q0pn*&Q`khnhZI(z`NEPjinKB}IN6lME$r>gRyR15X zV#58Uu1UaXkNoXtaclg@9?EPQ2$fctsoOtX9$Q%an5=YpgtaxRfm=(@C^U(J)R^K> zVTuc!q@yiksw72)C~J$}0irJo*0SnGw=qZ9$I>UY4|3yw(h0CEpiyq18t*7`P79L+ ze^tUPlBTC0NY*I2Q<<p``X7>=UpR?Pz49Vo&oUIU%flWcX`j9G!-6?|jKfJPq7w=UZXD zq;S}e{jy@mmlcnJ3YWQd%Qz(d8;5FnIh0=x5tPT(;&_6LoF5*CoY49?8f_?5wdV?V z5kKsX5MdhJg2#(_FYqwR7AmM7C+vg`o)dxcVe#;$j<&vJeQF&^N56cy9q25TT2HA~ zcAj8g@vTjGG-JH%v;5fBaZAf#vE9Msm>?}Rp6RcP!{Kxjp~+UBr4au^c$)K=^Qj3N z&~c5xOdV>76aa*Azl;W)g{P-7VQ4TkU^UWs-Pq#=tDj0 z?jP#u)uMB)hy6x`Z>E1^fmL}Z2ekhMX>qPua4#ghX|~3 z%h$R!KG*%OR?c1R(W0R7_C@G&zyp$z6zW;1rfkP-FinkAwt%64bQI9kdZuRVBv{N_ zjb>FoovE*x?Z;zkygseW<7~o{A|~_K1ew{ptK)8}{Sw`e&&QPQT7_nB@I_B$xxdd^ zLEe+ina_1~H+eFxKMqwWsTFZ4s2epfu9e6>GrE28VV0Vb{Rq`(u%C3YEcCH#mj!8L z-@jA^|KRHW+!s)dG9&7T_R-mpfNHHm>zx>ta_b?j?dW}VEVW>DtUZ;1y~yHg83)3f zuww6VYVuS-OqzKtdWd2z#J;#*#T(}9g%XXaqbx^#`bxCHT7tT04hsTwyi$3LX_@qn zN6}%okc6!RO)&SRNR9fgR4{~;ers;jC7;xfHQ_&)%-i9c$uwraE_2KIi_-nLk-wq+ z|4j6MF_9@)bN>Y@R0yx&)Hf3V000JJOGiWi{{a60|De66lK=n!32;bRa{vGf5dZ)S z5dnW>Uy%R+00(qQO+^Rf1O^ltC$!HU_W%F@^+`lQR7l6|lFv#4Q51*2BQ5NCbl-y{ z^f2XCT~sd+)Ve1KDr)0cSy4;dl}gIs7&+&7TbMD(%!vJ4-470j%Z1;^J@qRt_)5&T>+JL- lbz6Q_@+aX75|8H}`2Y0D literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_left-16x16.png b/src/be/nikiroo/fanfix_swing/images/arrow_left-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..ad26008447242fa316ed76dcc64772ec745013b3 GIT binary patch literal 4325 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_left-32x32.png b/src/be/nikiroo/fanfix_swing/images/arrow_left-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..ff87b6cdfc0eebd818d62c991abf8d9ca87568df GIT binary patch literal 4784 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 CoR?z& literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_left.png b/src/be/nikiroo/fanfix_swing/images/arrow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a5cf56f3528cd248109f4ef8952ef7aa0bcef102 GIT binary patch literal 2900 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_right-32x32.png b/src/be/nikiroo/fanfix_swing/images/arrow_right-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..68e74c4300cc62619e597de936dc8502e6b6cfc4 GIT binary patch literal 1088 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LNrcpAc7|g8%>j|NZ;7 zsHh0YrUdZu@j|pSGBP?lI{`^+Yb$PUZlIvKnVGP#u)n__6BAQRbfl1wU_?ZiyqsKM zU?9+HpaZhAvmuNZFJ3?xlP6C;b?OvQDl04N-o1N3#-BfbfFwl0q)C$s3kxCEF*7s2 zdi4s(wzRaIJ9jQn7m!_EUjF0951{(*-@gM%E-o%d2YWw1zwE3mKR@5rmgdruB7Z;M zzTWQX)26hwHUpg+;O{@7zjxle*(;VWnK+>@C@^61#0iTQ&fmCU-KGueCQq6W92_)t z%A}=B7H!_NVaN6@J9lhdx_Dt|Nbrp5Q@3y3w0qa~eS3E9-MwSwie)}N-eF-Ovu90T zy=wWco!br`*n8&G@hzJ+n3|dZ{S_V-I)C2m%^TNWxqN=!+}VKv0nyQsyLW6mf9~{x z`Ezr!vl0{I*R5H#XXlQcJGO;{1ScmY?%1|v?V43)W~Ni7Ov%g5X>V)s^Yyi}vrSBh z7Z4EW>FEJ_xiX$X3rP8v1o;I6X$BT9b|xuD0|CXHRbfgsueJs8Fjl|b@5=xB*XeKj z1OIITQUBJLg!BH`C%ayO<=-6B&ucCB%deCF_d~V$%{uYaXA3zD`t2DR=N@EE0h-5{ zGe!KbOl#njw1bxPqb<`oJmCOS4wjMJ?fxRe$zN=^z5Dck`R)w5GH)Qt3%-3=5E z3c7sB*l4qf$<{5U5*sy7oa;P!_O!l+&ZVoDkFQ#=W672^dlqe4b?d+dj@3=GZteQ? ztIaK0IM~f?OEZNe^Yd$Hk3ak5W zJDO)7a&XGZ9dFpR1*XhMYd>8lu%WzQ&bt;MqoAy?^l+v<$BycXs)rNVn9}m)|8TJZ z^Y7BxruSX?Pu~Fsk!p!+L`h0wNvc(HQ7VvPFfuSQ&^0j8H82Y?GO{u>wK6r zFgW~i_Y)Kix%nxXX_Y_?hNij(hCn?bhL%8eRz{}U28KY!N7o8q)@NYwboFyt=akR{ E06~tY?EnA( literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_right-64x64.png b/src/be/nikiroo/fanfix_swing/images/arrow_right-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..ee2f9657d2345f79b6c921ae8db2ce0d364f50d6 GIT binary patch literal 2259 zcmV;^2rT!BP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_up-24x24.png b/src/be/nikiroo/fanfix_swing/images/arrow_up-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..45662b30e46a6f01e8ab3483072a1dff7a2786ec GIT binary patch literal 4229 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/arrow_up-32x32.png b/src/be/nikiroo/fanfix_swing/images/arrow_up-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..0ee7bba2565eeeb00943b55b801dd4b8be748bdb GIT binary patch literal 4773 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 XD|l~kf<}7400000NkvXXu0mjfts(KH literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/clear-16x16.png b/src/be/nikiroo/fanfix_swing/images/clear-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..40da198f9f815633f17bb8735d13e7d8d0aa28d2 GIT binary patch literal 4627 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 Qn*#vD*utpPU{~aS0qoB~;Q#;t literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/clear-24x24.png b/src/be/nikiroo/fanfix_swing/images/clear-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..5b48f8a6377217123376dc4fc0c628278f03b863 GIT binary patch literal 5286 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/clear-32x32.png b/src/be/nikiroo/fanfix_swing/images/clear-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..b857bbef27b0372ef30675d91e73d3106850fb69 GIT binary patch literal 5979 zcmd5=c|4Tu*S}^A!$eu15LwHNEQ1j##+pXiw@^uzEQPE^%tT5eyO6RcgoK1_Z6y1i z9){|%q@l7s%$Rxasi)`H^Zb6l_w#$-KitY}f^|FRuCS}u&qIc<3Hmx#N1st{_QZ=A3bZv_Z$j=6kHiPRJkd6B-1&@wO+u1h z!6AuglH@sdWcVTdL~-3?3Ec0VGoCRLSl;m>j0&_}3~%t7VU4n$dsS7U29rF{m%GZEPgU2Sc2r@x|N{@lazRFsT=KO(ZL0_ zYO`o(ttw%D`)UgkKmoU;J7uRwD5v-hXKYo1h1PA?H){{&s2(D1iUf=zccSZbWa^xZ zq~v81HykK{^Qv`wH)J2*`=K`8glM!o8 zhxJuJ@?POK@ypyNr?uJ7?S^#J_U@gSU87dG*{x=ApBfNUVe#Ega*R@E>z!Z8sYyF0 z!Z@!k5T*L&vVnmy-Uo-Oky6H%fZItBCCO`qIlpMm&w82Lp_ArkZJiVyuBX2U z(pe_aG3|m?3WXOXj0YSS7Z$?HM&@-etYxav?7GogRkC4bkAhYaBnwpcK6IgQS)g+) zp{(YlTR_9{a|^%LN#1zxlj3^8YNQ74D++cvA7cNan#oyDk~W2+t8XqH&2C{NO^}QfpH*IB<6-TtlL7*y z87Pv@P#A)-g3pKTW80ilTs=xlHjZFcZmiM6z#0&#;rT2Bs)ya@wqCn8fa|In_g3S2 zW(I&t{=gj2CWkf;;f%HQcN-66b6tJ5xcKQ4qUsH&tKF#sI~!iP_|=xrllRSdgWs!- zpAv2%1K=lJah(U&CL9U}A-gKM*OQG%6)8?V&>p8`d)r@eVPQebfA!OwU+u4#lwgJ9 z?#aE5&%ApfIRlUQ%t6`@4g$BMv#4ygmd*n*dDqrXoL{`E8y)9eyG7Fe({XNuN2Z&B z`7(7g{Ff^dB_&T=TVZzx<{V>{eE9mPl#ZL-^Ft_gEcRX(vG}&D_VcwF>!v^p`PpFpNyz9rv})JxI?sQgTG&1504r>0TY`>C~$#5p5Q3buVYY?rp_hZVf&b^5fS z+z@4U_TB`UjK{u=HYwsO@Q}TE#UoSKsBS9j?aIUsNe!22r6xJNmOzf?vDK$o8nkel zA75PmsuI~Mp4H`MX5@quc2dru)p%r*Llzg+jH@_KhMhgj)p^~g?OZI*n6GT*&RWy* z#G-R9X@LMDGP1iKeyYhspJ+J$wVtzY_JLEIQ0&gHfa} zDe6hm8eP-ttfh4-x9-K{=tnVm+5@!9dV0rM-f<7xrj!xuOO>4$dqVO4-TTT^_Y@jAcOr+!b^vc^ zv9;USfoRwnAP66Q`Yt@D(tY>Aqt(`IGDF6XB)X%x_k(iSD4Ut`($|w?J`{Nnek@zj zq3DfK|KmqDX7)JEw2dAtJ6>@-+MUv|+E=z&yge=_>}k->nu9yz`&O=sNBkn~&9K~2 z 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 HJsI&|VeQb1 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/clear-64x64.png b/src/be/nikiroo/fanfix_swing/images/clear-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..2a0bcadf3555b9a55de22137f8514e351fda5c45 GIT binary patch literal 8778 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 literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/clear.png b/src/be/nikiroo/fanfix_swing/images/clear.png new file mode 100644 index 0000000000000000000000000000000000000000..9042a08a0b16bce9bf53b5cad6181ca632a381f3 GIT binary patch literal 4412 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 + exit 1 +fi + +while [ "$1" != "" ]; do + name="`basename "$1" .png`" + for S in 8 16 24 32 64; do + convert -resize ${S}x${S} "$name".png "$name"-${S}x${S}.png + done + shift +done + diff --git a/src/be/nikiroo/fanfix_swing/images/empty-16x16.png b/src/be/nikiroo/fanfix_swing/images/empty-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..5e5307d23e6ae4090c0230069b654c1cdb83ba0a GIT binary patch literal 3994 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 E09j+u#sB~S literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/empty-24x24.png b/src/be/nikiroo/fanfix_swing/images/empty-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..e88444b2becbf83914654bd165b3c6f4281900ea GIT binary patch literal 4216 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 E0K=otk^lez literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/empty-4x4.png b/src/be/nikiroo/fanfix_swing/images/empty-4x4.png new file mode 100644 index 0000000000000000000000000000000000000000..28698d0103e0972b258d646291ff89a5087c13ea GIT binary patch literal 3993 zcmeHKX;f257QP{Yunw{*0vbS&O*Dx?G_r_>B`PhOf(x4vkVQZUvP!@OBy9`0w6@Zs zA|jw7D5A(B(zLDJnh{i7npVUH6xD4HA<{a&F=FI$^jS5|vx!YK0t9e7CRM0XFZwHJ|9MU*^aGQmaty1nS8}IjxQX zP*5R+Bnb4)K}IT{>ugVbPN9O9%A21n1CdFA%dzv2Z3*y8MIBin&CzXTAh_i`FK!T* zYl=*S8Auc$P-`L~P0*!CWFRA31QIQh^mq&(>DnLkWTYFT7-B3D#~?|8B5DycU?N=| zwb@7r#IoZ*9i$P_A6rMRvx_csHiJHUJ}~J_I-Aa7)92|jnQZ1fHp5~9He%REY=r(1 zn>^CVA27(EnPn#E8WXUQq~3F!M2!8QA0xn`>peF70sa&wEp^~Jpa&5!(G7nLOF&je zhAJa1OQllfM`tO)Li|k`kU05X*tJa zXDlwO*KoVjXE1kt^j10f$x}3^Y8lQnGBz<~SqJ)3eR4saZ9@7 zC%n<4amIV)*HruZUr#eKtZU33;Lw(vOvAy>JO;y|0f!$?ksp|KC_h?=EuEf+gWo9} z2E#818XV&ru(T@LnD{UbyIZL3zFMz*tnT~Yvv4xkJ7N}Fxw67xrRmheMlp=r>)s6> z{aeIe$<7s#bn?uW>V$mf;*hw+aJqfe{nwRl>@vUNhuW^`^!BC{1hE5#xSNwvSgUhd-FLJiuV9gZ?6?Y~9B@59f9oYS?VQ>9JX8x|LyaxJrPG-sc0 zt|~2|^*ilaI#r$2?%{b<+;z1oIGyDb8D3bu$MLe)&hPH-(g=9yXlq<*^W}{G>uaFA z6o(yp&wPimCPDA(ywQj&Su*1F@9HI-s+42q)#W!9}n-VtutqD0LJEBP`7@{#FvLF>hF7qo8ZZc92@Cb5i%X&=@{ju86k(Cf}T# z@T}y_9FHSCn)>BQJHk9yuCKon6=$aCFN$3H)>a%$qvG)QW$SRL>%*bH7~aYcZ+p8N zhdmjEI6N@59n&_a_1*c6j>FLiRCXydacHk7DR>!`9DXrUH9qIWh3bnjewin8bb>6T z7CA+AZstXZuj(YG#t29_JddwIg&v+KVVX*`E=4LNvzx3miheE%skCgmHEX8K+5-;W z1^dqroLX>m_fX50g0acjzZBoD!@;Mn2M2f0cUm{^cJMB{z{s}YL-5qb42Pf2gyG;@ zgz#9$4<&Xsnyen38!k)h7uVSSnmE8-FK>K2^|PecN~7uU!JSiXjog` z>h!8{Yf0cn2hE1vSVescdcDqPjqbg*1iMx)cya0!`C#9Z^j;fEC^J64QPnOXAXcTh z$(pfikDO*ENq^C{2>pnd+??x$D{xSn_7n$+Ojq8!XEbb~WA`-TwMX`Irv(zzHN#s| ztt~P2;LN(t5hs-VCP9A{+8i>bPp#<-&N$2Jc37Jbw5PjJDWdT}2xndL)tLPj4?Wbj z^bek3@V;NVTX{{!ZQmu%dwVd8X}Tr}KfeewbB}5XV^2RDy<~GC{*bAepzu zwk^NfA5GkaiK2j zTY)()6pc{puwA=rI%>W8Dut2NhDA37{uOaU~}le z*hm%*U!ToyNSV_-)XiR2;uYbl>ONJqcw08<_4WI;n@U%UeR0sgCmKt!#8UUAC+@Pm zvu**$lcG2v?e(kK43xF3bR42^V653k%Dc2Gc0}2FoWZJCR6j#F#7Pc8TdhH^c=IR7 zkA`TOw1L^gDn1%tMeomE#fd9T_3rD!92Bx7Ly==jX3ZsyEbq|H#nnkA9+|D}j2vSF zzw9KHmuu_#%3=k>Z_99~MIT$?QRT4HT{sMNq585|F%sH2l9S>%x?yD90pEo?D4Pa^ zi=us1>Yn>l;!s+FHkawu*J>6%GZAHU@P2k`YLTBgdpSNUhizp}iVj+CAb#_t9=UQZuAf4E$2l9^EUvK zRr9_36699>quVniaNIj3!O#GCr+)IfM+WiZ{G)j=;pE8O#X)Ck0IvO4oU@Dlw*CVx Ceb40p literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/empty-64x64.png b/src/be/nikiroo/fanfix_swing/images/empty-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..2009415f36741194dc411f876059b03eba782bbd GIT binary patch literal 3997 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 IOMKG)0cL#B*8l(j literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/empty-8x8.png b/src/be/nikiroo/fanfix_swing/images/empty-8x8.png new file mode 100644 index 0000000000000000000000000000000000000000..f298f4715d49f4b819226ec913c1b519f219a095 GIT binary patch literal 3993 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 CN6;+* literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/empty.png b/src/be/nikiroo/fanfix_swing/images/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..762b8f64e7805c3ea8fbd48b06db930bca16c300 GIT binary patch literal 2855 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 FV1ka$ccK6Q literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/search-16x16.png b/src/be/nikiroo/fanfix_swing/images/search-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..b3e9fb0de72f27a38c3546b6b10a1822433921f0 GIT binary patch literal 864 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>a|psbLvu(qzAr#CQY8Gu2tRyDW@$Vx2<@&iQz zGGO86Vq$Y!bmHhl14h=e4>wkBe*VDE{oOnTv$K~JnXk(`rygI$Bz$S=(RC6x`X@dy zOL!k-{OGikkXZZ^W5&SCA0PkTtEy5kGg4#0zkO;V3nw#+HJLFpwmr#81=_-xZhCs$g*9uVlF?hQAxvXk4D~8J=ivYa?RF5nI z^b~~4&dx3%05Tk+KwC$TlM@u$KoKB-YvJMo2?7Bz9t5f(Mj6C3K7IjKHa2ky$=)fm zY;5f^va-SUF#iAlUm{7r2*_zI3GxdD3nCFrtgMWjPW_wKRq8NnEq(v*UJ<|Mp>I1I z>!x4(`>Th8YuUXFr9~ejoxjJ4tvnzoVD-+d=~!|-*WSE!-BOqTgjHOM`}ugjsM(cC zO!dG2tgz$~kDb9IFyYJF3q8EtaVNh%TU7J1SHd|vNtYx3Rn_aKL6M=r@J@JT^8n}u z#w2fdm-gQ`xlaQ*oCO|{#S9GG!XV7ZFl!DdeX^H$`ns~;XJip!kbLw@@*+?Vo2QFo zh{pNk1O;gh4lXWE<0Ovmj;_vbZx1ie#hs^5oZ8CNxTk56P}h!4UaMEl+SRsf+P1!R za>6k)BJVG_dVym_$gLYL_pbCbvZ@`-(o0V~d4Me`Dsua+8%jPx*`b@8jo!-2$UJ=1 z^k}W0!M3@@tGPF|XrBo&{~pHQ(vrSfe9FmZZy&#I-*9-p=>dr|3F#a>Tw!h=9-3R) zT8az|3XED>x4S9WoEMc8l@*pw7oR^t;@+)UQ+l3!o^oZ&wUYGjJXg(rUR!WRM`h-l zId}G43w!&g>CmEU3zAYK+a_Hy>i^W`)wn9FL{eh{+o=~<3X;1wth%+UN-oFf5^t~X zo*R#5tl!x>zfp#Pp(M*9K=9k$WxzmEEpd$~Nl7e8wMs5Z1yT$~21W+D1}3@&W+6sK zR))q_MrPUuMpgy}(}dp2qG-s?PsvQH1Zpre)ip2#>IpHl1gf(#GSxOP1TsFlRsc%? O1_n=8KbLh*2~7a4Nf9Lg literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/search-32x32.png b/src/be/nikiroo/fanfix_swing/images/search-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..bba63a4ceed3496b0aef0f32cf44d896512c15b9 GIT binary patch literal 1302 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvVtU&J%W50 z7^>757#dm_7=8hT8eT9klo~KFyh>nTu$sZZAYL$MSD+081LNHQpAc7|g52DKoSb}K zUf!&nd<#o!c6JUvK7Ly}XJKIxK|vuOp8#zgU0y!El(b9*MP(iyURgPLdnZ?BW@cLl z=jjWV0ZovWmIac?fR&Y%lamu!1c}Xv#3q&rv`$`LfsqlrUZ6NPH;=Qk8y6QB6B82~ z8ym9iEG#S>9GpOsSj!j~#Kgss?Gh9c#?;Qq35psB0Lt1sH~}qXXJ-eB0Nnu84h#gK zd;b6b4^gA2q{7AqlEn&G+1QY6gs2e}6KBTjY+$eeJqsj7L`4HbBY-AIN=bWo`vJ8B z?X_F!(hX$IEeY}q#%d`8BO{kp?fiMO5|o+PoEN|P`}0bIq^%-VAAe#}!<`%%TCTs#3lwDK<`KEQ`bHzKR#LfkSweaF>p$V*j|;>n zxIfE$ex^L_{awW)>$sM36&L)`=j-nfap7E)+_Et&IWT5V1k!vD&%QZn)q8$1?ly^(TNYRgt*ewBtL&Cj>Wn9Su7U|ret-`}dF zbhhaS#{PM6U%nbUsHHHgy|%Pyc_{cy**)0M>|gqx6Gv2bXtD{YB~_(e`+fY!nU{vP z`P)7pQkpg`yVw2x)SV`hVb>!Ln6BKabn#ZpymzYR8)pBW{Ka?I(zeUFx&Bh- z$NLhm=bYbLD_W;wX|Y7sovS$MRmJNkTh@CQFSFUdW67PVuI!xq_q}|-Z!fp%lYa+2 zB1%4(e7gJbhy91UAHT>r{DlJ_%JmY_DDGYt$TK*6iEvhB1 z5hW>!C8<`)MX5lF!N|bKK-a)T*T5{q$jHjj*viOE+rY@mz+jrtTToen(2$#-l9^Ts p)L>|;YhVb}6JlryRA*&ms%>BhWPEh302XKr44$rjF6*2UngB#7U2FgV literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/search-64x64.png b/src/be/nikiroo/fanfix_swing/images/search-64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..9c15be7cd12c9e309c910c3b958ac518ce2e9e15 GIT binary patch literal 1927 zcmV;22YC32P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru-vI;^02$)? z8PNa$26jnAK~#9!?V4+BlT{eUf2VKz_PTXzWw6yL8RDF*+cG9z;w6F_MWc8lcu5dJ zO*DdtH;8CV{9vL+4H_>W{HQUCq7sY|;y{g;MCCFDgXt!S_vvJAV_Un|4^J!0ySJsR z?OW0QlJli~d(L_OJ?A;k^PCro6e&`qNRcA{S5%(*R6r%r47gK07z%~skw|1mXJ=>E z;NZY0U>wT=D8L7t4x9r_2QE&0W@VW2c)TMnm$S96uWuXh74RX@2aMV+M7D&1bAji9 zHXxQT!pNYF0&(CQ;1%FfU<&zJojJz`TnIc3>;Z-{G&)s_1N(rNfRg}6p$$xjAGihR z$bsSJ)(^Y`+yR6NZKxGqU@`D*jtw`rUw{YmDZ)hdnIBjHJOrGO;u!-NM*KlLr%4_Wr<{`mo5C-4Yx2H-)C!m&*UBEXHn zO5k(E`&;&@>w4F$S!Y~+aKO-3Km2IHj0bUe9chg31)cy-Lsm*AxPb=X0pNgDznZ2E z2LdIp`+UAyfRaFf*3F;VXSGrahrBQx7`87>@Q7JWIo|Nu(@0wBHIH#lY7d7w7`MDUf1h^ac$wGut;9bOj z*oFdT13N4xG~jSJ9*ab(%K!``XQ6xvumbqiV$AJ8ot*+WfQOOr);yV=z^putI~Di< zYe&11?6=!L#vO@DB(X6+I|RIu5FsCY54?&Tt78cwS*Fu20cIjuojLkw1KvZz)_f34 zh;Yc_J0~`-038W^&9nns^JILIZNNIlr^n5}MdTo3%qH7P?#X=eGlv7Ou~Y&zg%-dL zlGP%F4=5u`sq5 zagerDk*@*{GCm^P0@UYgP+fQ9c6(-Pn)3&Xu{R)v)2zv41?&J?8L#$C;1bf*{j*_s zc!;5)LlyCOtkmM`e&A2L1o##Sc+3L$fNOx-TpGUU=39vwhKfd;T3jyIBE|<{hmdsH zwz~rb%tPw-=B3z&RB}ou+PRs!x*60*8v->o)1LEqJi``aZUv%t8J{GGTv}LNb{z0} zy?0zP_cC97v>_K`>A-xY)E2!7@m7J2J>GZz)SMmDVw^cOS5CG<A4eMNu& zUsuOsu}MdmN)=L^=?17Nl@uJ0V~-)OuFsE=Vr@pNI29B9@gU-?{zA%A6^IA7LLvXI zxusiu$>TO%B6K`(GSCI=XM%xIHtG!mUjv^Z`}qVUEY7OR&b zHa7vUAtm$eh$9?I(e?swA?JG|hgVUOqkR>JB;p9MJ`%%pOFnK;6+Bwxp|O322x7_t&BAj6fA)e({* z=_Z^hBfxH?2G9tUrFhPPoDS2-&@tK}K&}`;0;*ml9Zn^JLVQsz;#j}`uLzJ7p&jS} znn=GA;>hLdH-(;bKJWv#lHy$@-R(Y59tz*(@pwxDful-Jk12e>60-=puK#dqeZ!-7 z-MeZsz?&O36?&d|#h(zN6EIx5zJK!M%H<0e-&ScDhT5>{qe9Qm7QDa`pWnYT+R*gW z?aNk70$9KC<3i8N9yCn@@JFhv&t16Ww&`tMKdL9zyin+A6)94rNRc8%jsgA#HPw3r zraxeU0000bbVXQnWMOn=I%9HWVRU5xGB7eQEig1KFg8>&GCDCcIx;pZFfuwYFqRVS z9RL6TC3HntbYx+4WjbwdWNBu305UK!H7zhPEig1xF*!OgFgh|dD=;xSFfj2&cufER N002ovPDHLkV1hEqW(@!U literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/search.png b/src/be/nikiroo/fanfix_swing/images/search.png new file mode 100644 index 0000000000000000000000000000000000000000..7601e2ec3e77ef1b85acc0a8d0732c64e783e75a GIT binary patch literal 3786 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 deIH literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/unknown-16x16.png b/src/be/nikiroo/fanfix_swing/images/unknown-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..ee66da425e46623a8d8aae055958b80c7baa5670 GIT binary patch literal 555 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP=YDR+ueoXe|!I#{Xiaj ziKnkC`+Y_h5eCUeza%d*FfbZ>x;TbtobL_v^=1kbv3-Ab`?-Wf*N8fnpotRR#%G^fJNf#!X-{_d(YS@RQI$g84@G6-m-zGc&6s-s;nM3WKAv5dUD<3 zK-;Q)wWnMsiMiJwE-LJs@>2E7^x5BB1uoWq;N2-1y>fleKTu?;mbgZgq$HN4S|t~y z0x1R~10w@n0~1{Xvk)UAD??)|BQtFSBP#=gX+m#hQ8eV{r(~v80yP+#>KYgV^@JE& e0@Ya=nQ9vt0vR7&D}b@gz~JfX=d#Wzp$PyecF}zR literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/unknown-24x24.png b/src/be/nikiroo/fanfix_swing/images/unknown-24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..6c7d9ac22f5472cf5522a979016a5ef5abddbf3f GIT binary patch literal 754 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VX;-`;;_Kaj^> z;_2(kexH#=ghBGrFUgAx42-`#T^vI!PEVbD)?e6B;@JF~H)p;T%S@at=9O{vn1HIo zEUDx_E6n6n4Ube@5|;7Z@oV~zbcwuqHdPNE9n@eeRV~@rxF9#vvZOq{Jl!VJvwY)2 zOW%9n|N72Lw`<_;Qn_X!(ZIlzC9`00OV_lkC9I;0*5_PpvSC*U&@9q6-mWyQdCr%` z>yxBywTqs(x1^WLcnRii%vrnreM!2t=K1G-|Ej98x0go7Jl!VFZgOIaWvqQ&mVPM% zlel+#_0ydzCoj{f_^lFhc;`aJQ!D@Lt(oT8$YAi%B~RW|^>eA?P5b9x-Y;sXbWZbE z-(r*Y#9xTfY+pg33HzMTTW&Yxw|)_t`M`E7|8KteTT9OWZ;}2HcIk`BUZ-n|MdtM~ zt)Eb|_S^a{g{tijt#2H7XR|uy*7|3w_Q$gXUfQ_p!L7JsPh0;j`_IU(8g+KN-11fb zUin2=toZ+YnWJL&ud}V+W^KH*w%-0zcKR_P2By?k<>E(+@03Zc4W21m$z}Mm<5T3% zfIGhn_b;9x`!cJrp;4mkcK%gs&$kh&+{`+cPc5;X;n^j=eG6B@=H$(*`xZYjft!l9QJTU|?*td44x0q@IC~V{5Q}hssP~!cZ-7jVMV;EJ?LW zE=mPb3`Pb<2D%0&x&~$;Mn+bK##TmV+6G2e1_slF-pZnA$jwj5OsfQHFf`RQFa+ue gF|-7#vobQ(HZTM-KDt%_6DtFQr>mdKI;Vst0Boc!^#A|> literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/unknown-32x32.png b/src/be/nikiroo/fanfix_swing/images/unknown-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..11df7b983482bc69672a25cf034de7e4af83fb74 GIT binary patch literal 978 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 QO#lD@07*qoM6N<$g4N;XJpcdz literal 0 HcmV?d00001 diff --git a/src/be/nikiroo/fanfix_swing/images/unknown.png b/src/be/nikiroo/fanfix_swing/images/unknown.png new file mode 100755 index 0000000000000000000000000000000000000000..563306d986bba22e2e3018cc697bcc889998fba2 GIT binary patch literal 8098 zcmZ{Jc{o&W*#8+bX^5=VP{fc>mXR2WEJG1uN-=hWY(t2$#K@8~$R44RB{9~FElXr5 zTavL%gEmvmh_W-x?|grMy#Kt{dtGzpT-W*IJm9zod6NAu1PmNnJ&^RKh znrWc{DHJONtg;@aCy5Th(qT$TuCejOXJwoX3@^y4!F`GhWME7gO*Od>B!rKZX?|?n z{;xN5LKOB>#_Gh@+uK?Lwkfpyd+oQbwTy*{indML?wC3J2)kuVZs!K>uw1xXY96FF z`qV@IlhDb&8+Dh6s6KrN^~g315yBR89sFMpWE1s>xA>98u}7!6dea$4k2WVi@tIS0 z{dW<10rJAh*>*RweW#4|Ioo{V+s58_tL7_{SEHu@kKMr7i}$UCznjAp;|qB%{5!!b z{G$f7RnlDj?uL^O5%T7cQPV@dY2LZ{nqE)H9H}l2$Kqwau5MKQ5yPDCoV_occf*1D zNRT?2LEAzSwYKEp%az(o>wJKdz+wclJMJf5G>`=R#IIO>Yxu9KOD|4Gmi6H8P~^?M1YSt z2P)znqG3!k6wR!rO^-C5H^6kDyv7mB#{q6UNuZG9`}`6tg#?XUhv3shUHc?Q()nig z2m?#$>2oznKaDY(1k#i{iJ(Evr#?~v95A0mj8*^)MqJFs-Tta*;DNi&?-0vEK+Z#vr!ttN zL95BJY1a`E*IfHW8L=o?j*!Tx69B{eL%aByQ7K7UDJT_LjDz`e;0{@7HyD{u(m6(B2_<+mJl+T+hxK!$+Ylg8!M%XK@&O)kq zCg0acYaBL&MrOhylL2VAn!s5hgFSgmX`-WM9`bey#m5Hfb5c;O)NlSl!*m?02&-K5 z=+TFQ5Kp>R)NFY~yVZ~2JSf17`ta`__PsxWP@!o#G(TUK{B06r{oy-JG5I=Y`zibS1g@)q>0^EIh3f{;uUJ(tt>qxrmh_LIuC4z@v z)Ui(eH;C;M_(4&T_5gbti&SSzr)a03q=v)oDz1E#fm>IOPp+)IQp_Fb4mx75oL!Rj z(Vb*p5E8qqB@Dd6Dv#qT(W!EHi)r7+Vy^~?8Zx=Jva-^ra{MFI!vOrO9M8XA4N3(1 zS{C+)VzRR-qjG>hbut{; z>S|9j8Qzp+zLtxJ3rU}SQ8}*sq^Rg%BW|dA+_y-$yMaK5&Z$$+mS$nR&L?SFN-ztU zvy~q;Efa*5h>t>~eaUTR88=%Q0hQyvq1xKoJqZa3ik6odzKu5#iGT(Yc&cehD9y?( z^HDL&@G1RNfSOkaMi9&Ez%Ve0nKk%)Ip6gsY_e`%h@Y#6S@8j!hBxmDf3+RnEM~P@ z;w=`tVxeBq(KGq^`5EKBjo-JUdnpTD2(h<8j^<;ZpfWGVec9p8xXN+&zMzjd@5Q#qGCJM<&D8LBF zy{KfLAwNu`U6qOVej9YQCQG-tu?Shrec8Bs-6Q{L!l1(BDf!70FxP$>N0g|{mX@2r!Po+HQ1KPjT&R*~N{F*%MoEUenUA|WQvc>p>us7t zAfx$L*<;;)>9w_)bDjL+(r>!wg$D)()*h&JphN$B)`{`)c^B-|#OJD)T-<0?-VpTX zbIb7i_vW9#kTy(drhe-}7`Hp81r2-FOe|-}=V8sI%*R$<&>~b__V32;20sKo;4cv! zHxi&!s5P(pc}&_VDlI`+Eo68kvZx8GoUNOgnTd{Ad2#C0smm2tl*Ooz?kgtyR-eTOe`8A8u{L5&g_iDopbsc~`;S$5xo*IePYO1ZZ7B%u09t|!Z zv)2omADr)faLBkc3+L=i!~9n>Mu%U~7etBg)CGim$Z2535=fh}#A^@JpFU2z{xkY+ zy8+RiAq0F%`l&>xg}ZGRR(bRPo69e4Gq29+}x~Zh6@4>KKZ=sGm(px$1fE*8frbp^pkiPg+)bHohzkkI}~ii&Cy%U;`Ay4^hD+_hsfH^*}T~G3Qhw* z;Ulm0Cj0ui!_w#*uB^UuyMI%R;6JuI@(T-3KX~w<9gHu9Lb>_J_(L_Tc!Cw08eI13#Zv5bU*F^j*6K@P_Xtl< z&$TsrpiB7Huku3=`ATbFp7~j`XfVrKJCs6}F|Iz4e5?e`pZceLeWrPWb?hAHi%{Bj z$5HTC?juR3kW=2xXkFkThxV=Sj62sbeaKMq>Z~`}<=x_A*>fgGatISebzGULom~kD5PHO!3>{rZyf)7fw;Jc#C@|%6py8 z3m9AA{0*NQ??0$p@Q70nL6-vWd*4SM7jj=Jo2AL>}r zAPRbPtOuoWdnaEZRUP738}%VNiN^gaLi1FSoD8BHPA4)?Af z>(7aw5xaD@?qvKV&c#K|KIaG9k(aoOecG6cW=i1)c|{u0%)Y{p7pY-hA)jXTTZ3vo3H6Ys$i3i^Q7C?hIw(iKV{@dhc7Bhao9CSe4;ki}d=LE6-=Zv!*zn#1kgrzC zl~&p3^!n2~*xQa1r-3UCQx*ukhz3uUOA4yPR^T$!8>pt~uANo09J>Nh4?1!=zs!}y z=dnK%G4;#>+Qo;=cOx14frjioY2Sz%ro*ks64bgJ6+oy|xK zaT9(6U;C?D3WhJQNg2x;d^wqpRYq!nFUD42+z?1O&w)fY`S?kTu@wZ9qh2qubMLbq z8)EH~9}MV=j5pF2WsCb#P+W;NdGFWsNs->S+1P`4wxyJ=8>0A%GMFOW<8`Lm4YILRA*!B2=pMglhh!Kf{=mwsz!AI zx{dfTzzTuq@u;5e9tdlc_78g4D}g^SylFB}Psyztc!TIE$vT^g51Q+WRo&uUum-`Z z_Q~!_`O8sn1f3$)7j!$nY%tVmoV6;dY8xy+_o}QBNrMcaM%0_0uS`8 zL2rPPHbIzzGUb=GE5+JiO8$igooP0P+ptUbAx4%hn&>Hx69hl<#8a%5$6{NG>2pwaZ&K(1ky1Ox&u`(Y;#ko^S|Y-udIihJ z@Po<1#ZCp+*-{0CyzBFgJeZpn)ePHJ{V{yg|6V`}R$sYI-TKef~In)2M6Y4QGPfk_Vl|5r_IM}ZcN4(@ zW;j>wgIidQSiKapHO5VfzLMVkP|P^7@Cc4wPVlIMH1_uPQo&5$@N1%nDrcKtHlLC& zRuwU^5id1@dy`v)yYGLS+}Be}X>PW=!`+^Zna{gkw5eCWysA(yT;Hj9*Zbbmv3fjR zqn_uA(2d~{yPVMJ`l@{$i-CNyAJM5z>&tNiV;s)w#qgP%mE*dgh1$3kvwn-aH?O?@ z!l9$SSz{kUpvz>*^XCLB_di`*e!}G})kLpW8V4OYQ?T4$Uj++~-2))aL{A79_k}WM z{!@^-e>%4RTuV9q)wTE}Yo_6b;D`||5Pjb%07V*|vldo!LVe6RT zbQ<^Rl8}_{cPmRv%SQS_97rEqg^N+lqp8w*t@rf}_r>&nQ1=(C&4k6RLVN*EX=&-4 zcIW2MTPihi*0=E_aKlNvVuID)_NUd)Cpfr1!QviCZ>DveL<{t37MOtliJrbSc zKgE4P+j5hAtAS^Oy&@NtPtmfR&n?EO-;%%-Uk?bVo<|RRWXLz*{MP?F!AApHe&o2H zX^p*Adjh1BGdzxF8<-V!PoLlA1iHRjsiR8#lkzZGIM4icM<{!;E@N?94D$)zbHgh* zSR)Fou-)(5y9!&ro#_0T%w218*vkW(gWOlb?&+v7P+2YmSMR?V(wP9g zG+WUnG zCWrc3;Y+cwyTbmheXqFv+MeH7s*XCIP^0u zb!<YK%O`i=-6Z$)K`Dnaew4IMzJ`{^B*!-A|vg?rbc%%`U$-8xDUy_RHd^Uc{@- zPnR#XgWH*_T9jE|auVv^ON|HG3Vv<(`-NZVAV85 zFI)bd1tM|xKcr(u{%N@`awj|Ho%B(A^mJqAXeWR-Bpylly$+xYmhU~3LKJ=}c#?~) z7PV;uMF-fu>jN1V!>F&y3uroX8EjTvF1OkV0QdChZ(Q^a%jn1A+kP?EL*tA5vb)oW z%glmK5Bgd-buyViU+``8Jc62c<5*LQ$;wZ*K9yU|MlQCSEN~Sx zLc31sL z=G65kJ4~^)OIWIQ0J(iB1aHm1J^?g^QI+1rDe1 zDrYA@%FFwkAiV0;pmR!3uP|zxKD(=;p&cO0kq*mxF13er5)$m6d-po&4$Q!tWIta} zvro1)#k9vRr&rD5AR!?mRv_^p!YUTx8 zzz_On$b+VC(p&RsmOIG3tksBdur>1gd7A!K8yFq;$4x3x?DRF&Yf!lg2rn&$$ADC~ zE;eckGIK+WyL-3Q*sCLbK?`s1iye}VT6*K@DA{T|$dR2H8t=Uz>bh5eD1n?F+~(`+ zWv3PQkK=NC)L(UOjo0k*?Z}L=XbtOOsNWX>*~h)dhQrS*jfXgC^AcYlj2#PT+KJmM zROF*6X>w*mZ))zZuiFvLt}2uQvRF&R#4JNm>6nR)d}Mpk;l8?UcEiZ-zOC+~E##SO z_4krpfVxqV0en=h7=$CGiBQXisS-^~vbum%=_eYYiE;JC4s1t393N{l0f5JJ6n7qU zeU@Y5#J_i*O5{meg;3wehhZ*`#4g+`Qtn z06eC^>zw+uWS78e-_opfY`$VL=^d{9A3C-7$b;Oap}9;VJit z1O#lm=gEVUokd^63Krk<*D!&v8TO2Trt~tUk$q-@?SUies0_)ckA^TMW16<5*!%PI z^K7~EskaS>s$^!euXQMf2s{*tQ z-(4AdXJ^X&7f1U3zID3AIKTmA;pAIs#Ys&8k_O}?F!QD7-tnu~t}z`a$O>j3FJ@pM zwRdH1oE@o*nu8vFA;U7#y*+bI3*pHF05Kp|4U?;@>rJ13xms!KP6D^^!ME|#RKxYi zC!Fuc+a{q>XYVE03<)TAcR6}UbRJlV+z-intZf?hyXjPa$VtbpJ*IhpNlP9>nFQ=e zCiFHkmp7_;O)~)K>3hUzS2%CV+2_#LjJ~@TB=J9Uqzl^+-e71f zV||m(`=MDPW{>Gd_-7mbvL!ya@jOhl zA8vC~I_+UXtPmBNdO*Z95ops4ReqVaw;wJHJlbRZt}V(5GY?@SFY+k8jCzZZ6D8#f zS`S7E-8r;Gz^tI~gW~@rsvEt4lStetD(7GqcW|~TFJ@Sa&s0k88HCphR6*tUf1;4O z{Rjtbs=o%c)x07r30+TH^`zQ4`7t!89O14?#7}J^3NCW^7?1dAA`cLX+~Q$=K3=?! z`p87~ZL4rGXsZiCa>^<)%Y1D8^m-6yUS!p$pws|6BZ@o*`1C@|H7kes#z;7(uygkW zGb%QuV^Aj$MRgxprX0SsPnLsDy_S9;8O=Xu2+SIHZg2GW?IrO;lAHB;iDILjAd=%r zJ5%*l6M7?lK4iIx&;tNEHO{%F?-fmoYIn`AkjEfHzK?{;Y}R7*t;0+cfIR|42L#>^ zbJNhg<%)84{+aD9z+mRKWR54HH)v1-uf;t30oUo`cPA`q{e~sVyxJ9NfTEV6nU$DV zMaSbi3^xpDx2KoS_98Ae(g<|Q1f)uX`Wx}Q>!FQom+NsXvRH*`HxbI{4`n!=NrTcE zeU0DHX5uC$>wk--hO8Z0ollTpkbkMg33Eo|Pz21-m%bL3`FkKX$)XfHDkdhDoMhzT z{N}9(ViWzwrXJFaN>or#Kn{mnD`(%XxPqi2$W>L2YnI{5dyz;`^(AWiEz~L~7;cw;6*RgV-kRryg9Av=3@5_BOfE= zYIb8q{=$kEVagD=bo9oTm|1_V<6udvmiTOK{r>G6Ng${=y12}Su|Kgwoyk{!a5zyP z7Qzk>d4*I@u<~4X|K)N8cV;@|YWA4k2TZtM%)v&W-tNY?m=mQIUS3`~CkJpT#V;8C zT5@=j0&+gQ2vmVl$b35ohsVy2jsis>$#a81Y^ZuV%l7%+6enj0dcvizC!bPkYt>gd z%Ol)25E~=fJACY~J`RzV#EXjrm1bFFyq*30xvI=ZxhU%2JC~S^o}H?(%Iv?KRFkUekc@dsIbNgG$0LUaiJGVMI z6QiNO>)weIs)R8+WGC@8EZzPjo zCeCwV=`Vsr^wnEoK&@Y@%!dh}3&DgPMELNqzYU>$i@NT-w)`Jt`9DY2oEZG>Q{CQ# SX=8Bq4w#u(VXBQ>@BTlVguPh+ literal 0 HcmV?d00001 -- 2.27.0