package de.extio.lm_launcher; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; import java.util.Hashtable; import java.util.List; import java.util.Map; import javax.swing.AbstractCellEditor; import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSlider; import javax.swing.JSplitPane; import javax.swing.JTable; import javax.swing.JTree; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; import javax.swing.WindowConstants; import javax.swing.border.EmptyBorder; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableCellEditor; import javax.swing.table.TableCellRenderer; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; public class LaunchFrame extends JFrame { private static final long serialVersionUID = 1L; private static final int DECIMAL_SLIDER_SCALE = 100; private final JPanel contentPane; JPanel panel; JPanel panel_1; JButton btnAdd; JButton btnEdit; JSplitPane splitPane; JScrollPane scrollPane; JScrollPane scrollPane_1; JList list; JTree modelTree; JButton btnDelete; JButton btnClone; JPanel panel_2; JButton btnAdd_1; JButton btnEdit_1; JButton btnDelete_1; JButton btnClone_1; JButton btnRun_1; DefaultListModel listModel; Component horizontalStrut; JSplitPane splitPane_1; JScrollPane scrollPane_2; JTable table; DefaultTableModel appArgumentsTableModel = new DefaultTableModel() { @Override public boolean isCellEditable(final int row, final int column) { return ((AppArgument) this.getValueAt(row, column)).optional(); }; }; JSplitPane splitPane_2; JPanel panel_3; JSlider slider_context; JLabel lblContextLength; Component verticalStrut; JLabel lblThreads; JSlider slider_threads; JLabel lblGpuLayers; JSlider slider_gpu; JLabel lblPromptTemplate; JLabel lblPromptTemplate_1; JLabel lblTemperature; JSlider slider_temperature; JLabel lblTemperatureValue; JLabel lblTopP; JSlider slider_topP; JLabel lblTopPValue; JLabel lblTopK; JSlider slider_topK; JLabel lblTopKValue; JLabel lblMinP; JSlider slider_minP; JLabel lblMinPValue; /** * Create the frame. */ public LaunchFrame() { this.setTitle("LM Launcher"); this.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); this.setPreferredSize(UIScaler.scale(new Dimension(650, 650))); this.contentPane = new JPanel(); this.contentPane.setBorder(new EmptyBorder(UIScaler.scale(5), UIScaler.scale(5), UIScaler.scale(5), UIScaler.scale(5))); this.setContentPane(this.contentPane); this.contentPane.setLayout(new BorderLayout(0, 0)); { this.panel = new JPanel(); this.contentPane.add(this.panel, BorderLayout.SOUTH); { this.btnEdit = new JButton("Edit"); this.btnEdit.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final String selectedPath = LaunchFrame.this.getSelectedModelPath(); if (selectedPath == null) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select a model first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } final Model existingModel = Data.findModelByPath(selectedPath); final Model editModel = existingModel != null ? existingModel : Data.defaultModel(selectedPath); final ModelPropertiesDialog mpDialog = new ModelPropertiesDialog(LaunchFrame.this, selectedPath, editModel, model -> { // Replace or add model params final int idx = Data.modelData.models().indexOf(existingModel); if (idx >= 0) { Data.modelData.models().set(idx, model); } else { Data.modelData.models().add(model); } try { Data.saveModels(); } finally { LaunchFrame.this.refreshModelSliders(model); } }); mpDialog.setLocationRelativeTo(LaunchFrame.this); mpDialog.setVisible(true); } }); this.panel.add(this.btnEdit); } } { this.panel_1 = new JPanel(); this.contentPane.add(this.panel_1, BorderLayout.CENTER); this.panel_1.setLayout(new BorderLayout(0, 0)); { this.splitPane = new JSplitPane(); this.splitPane.setResizeWeight(0.5); this.splitPane.setOrientation(JSplitPane.VERTICAL_SPLIT); this.panel_1.add(this.splitPane); { this.splitPane_1 = new JSplitPane(); this.splitPane_1.setResizeWeight(0.75); this.splitPane.setLeftComponent(this.splitPane_1); this.scrollPane = new JScrollPane(); this.splitPane_1.setLeftComponent(this.scrollPane); this.listModel = new DefaultListModel<>(); this.list = new JList(this.listModel); this.list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); this.list.getSelectionModel().addListSelectionListener(e -> { if (!e.getValueIsAdjusting()) { if (this.table.getCellEditor() != null) { this.table.getCellEditor().stopCellEditing(); } this.appArgumentsTableModel.setRowCount(0); final DefaultListSelectionModel selectionModel = (DefaultListSelectionModel) e.getSource(); if (selectionModel.getMinSelectionIndex() > -1) { final App app = Data.appData.apps().get(selectionModel.getMinSelectionIndex()); app.appArguments().forEach(arg -> this.appArgumentsTableModel.addRow(new Object[] { arg })); } } }); this.scrollPane.setViewportView(this.list); { this.scrollPane_2 = new JScrollPane(); this.splitPane_1.setRightComponent(this.scrollPane_2); { this.appArgumentsTableModel.addColumn("Arguments"); this.table = new JTable(this.appArgumentsTableModel); this.table.setRowSelectionAllowed(false); this.table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS); // Scale the table row height to accommodate larger checkboxes this.table.setRowHeight(UIScaler.scale(this.table.getRowHeight())); this.table.getColumnModel().getColumn(0).setCellRenderer(new AppArgumentsJCheckBoxRenderer(this.appArgumentsTableModel)); this.table.getColumnModel().getColumn(0).setCellEditor(new AppArgumentsJCheckBoxEditor()); this.scrollPane_2.setViewportView(this.table); } } } { this.splitPane_2 = new JSplitPane(); this.splitPane_2.setResizeWeight(0.75); this.splitPane.setRightComponent(this.splitPane_2); this.scrollPane_1 = new JScrollPane(); this.splitPane_2.setLeftComponent(this.scrollPane_1); // Tree view for models this.modelTree = new JTree(new DefaultMutableTreeNode("Models")); this.modelTree.setRootVisible(false); this.modelTree.setShowsRootHandles(true); this.modelTree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); this.modelTree.setCellRenderer(new ModelTreeCellRenderer()); // Set smaller font for tree to account for UI scaling this.modelTree.setFont(this.modelTree.getFont().deriveFont(10f)); this.modelTree.addTreeSelectionListener(e -> { final DefaultMutableTreeNode node = (DefaultMutableTreeNode) this.modelTree.getLastSelectedPathComponent(); if (node != null && node.isLeaf() && node.getUserObject() instanceof Path) { final Path p = (Path) node.getUserObject(); final Model saved = Data.findModelByPath(p.toString()); final Model model = saved != null ? saved : Data.defaultModel(p.toString()); this.refreshModelSliders(model); } }); this.scrollPane_1.setViewportView(this.modelTree); { final JScrollPane modelSettingsScrollPane = new JScrollPane(); modelSettingsScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); modelSettingsScrollPane.getVerticalScrollBar().setUnitIncrement(UIScaler.scale(16)); this.splitPane_2.setRightComponent(modelSettingsScrollPane); this.panel_3 = new JPanel(); modelSettingsScrollPane.setViewportView(this.panel_3); this.panel_3.setLayout(new GridLayout(0, 1, 0, 0)); { this.lblContextLength = new JLabel("Context Length"); this.lblContextLength.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblContextLength); } { this.slider_context = new JSlider(); this.slider_context.setValue(0); this.slider_context.setMaximum(131072); this.slider_context.setMajorTickSpacing(65536); this.slider_context.setMinorTickSpacing(8192); this.slider_context.setSnapToTicks(true); this.slider_context.setPaintLabels(true); this.slider_context.setPaintTicks(true); this.panel_3.add(this.slider_context); } { this.lblThreads = new JLabel("Threads"); this.lblThreads.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblThreads); } { this.slider_threads = new JSlider(); this.slider_threads.setSnapToTicks(true); this.slider_threads.setValue(0); this.slider_threads.setPaintLabels(true); this.slider_threads.setPaintTicks(true); this.slider_threads.setMajorTickSpacing(8); this.slider_threads.setMinorTickSpacing(2); this.slider_threads.setMaximum(24); this.panel_3.add(this.slider_threads); } { this.lblGpuLayers = new JLabel("GPU Layers"); this.lblGpuLayers.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblGpuLayers); } { this.slider_gpu = new JSlider(); this.slider_gpu.setPaintLabels(true); this.slider_gpu.setMajorTickSpacing(16); this.slider_gpu.setMinorTickSpacing(4); this.slider_gpu.setValue(100); this.slider_gpu.setPaintTicks(true); this.panel_3.add(this.slider_gpu); } { this.lblPromptTemplate = new JLabel("Prompt Template"); this.lblPromptTemplate.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblPromptTemplate); } { this.lblPromptTemplate_1 = new JLabel("-"); this.lblPromptTemplate_1.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblPromptTemplate_1); } { this.lblTemperature = new JLabel("Temperature"); this.lblTemperature.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTemperature); } { this.slider_temperature = new JSlider(); this.slider_temperature.setMinimum((int) (Model.MIN_TEMPERATURE * DECIMAL_SLIDER_SCALE)); this.slider_temperature.setMaximum((int) (Model.MAX_TEMPERATURE * DECIMAL_SLIDER_SCALE)); this.slider_temperature.setValue(LaunchFrame.doubleToSlider(Model.DEFAULT_TEMPERATURE)); this.slider_temperature.setMajorTickSpacing(50); this.slider_temperature.setMinorTickSpacing(5); this.slider_temperature.setLabelTable(LaunchFrame.createDecimalLabelTable(new int[] { 0, 50, 100, 150, 200 })); this.slider_temperature.setPaintLabels(true); this.slider_temperature.setPaintTicks(true); this.slider_temperature.addChangeListener(e -> LaunchFrame.this.updateSamplingValueLabels()); this.panel_3.add(this.slider_temperature); } { this.lblTemperatureValue = new JLabel("-"); this.lblTemperatureValue.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTemperatureValue); } { this.lblTopP = new JLabel("Top P"); this.lblTopP.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTopP); } { this.slider_topP = new JSlider(); this.slider_topP.setMinimum((int) (Model.MIN_TOP_P * DECIMAL_SLIDER_SCALE)); this.slider_topP.setMaximum((int) (Model.MAX_TOP_P * DECIMAL_SLIDER_SCALE)); this.slider_topP.setValue(LaunchFrame.doubleToSlider(Model.DEFAULT_TOP_P)); this.slider_topP.setMajorTickSpacing(25); this.slider_topP.setMinorTickSpacing(5); this.slider_topP.setLabelTable(LaunchFrame.createDecimalLabelTable(new int[] { 0, 25, 50, 75, 100 })); this.slider_topP.setPaintLabels(true); this.slider_topP.setPaintTicks(true); this.slider_topP.addChangeListener(e -> LaunchFrame.this.updateSamplingValueLabels()); this.panel_3.add(this.slider_topP); } { this.lblTopPValue = new JLabel("-"); this.lblTopPValue.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTopPValue); } { this.lblTopK = new JLabel("Top K"); this.lblTopK.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTopK); } { this.slider_topK = new JSlider(); this.slider_topK.setMinimum(Model.MIN_TOP_K); this.slider_topK.setMaximum(Model.MAX_TOP_K); this.slider_topK.setValue(Model.DEFAULT_TOP_K); this.slider_topK.setMajorTickSpacing(100); this.slider_topK.setMinorTickSpacing(25); this.slider_topK.setSnapToTicks(true); this.slider_topK.setPaintLabels(true); this.slider_topK.setPaintTicks(true); this.slider_topK.addChangeListener(e -> LaunchFrame.this.updateSamplingValueLabels()); this.panel_3.add(this.slider_topK); } { this.lblTopKValue = new JLabel("-"); this.lblTopKValue.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblTopKValue); } { this.lblMinP = new JLabel("Min P"); this.lblMinP.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblMinP); } { this.slider_minP = new JSlider(); this.slider_minP.setMinimum((int) (Model.MIN_MIN_P * DECIMAL_SLIDER_SCALE)); this.slider_minP.setMaximum((int) (Model.MAX_MIN_P * DECIMAL_SLIDER_SCALE)); this.slider_minP.setValue(LaunchFrame.doubleToSlider(Model.DEFAULT_MIN_P)); this.slider_minP.setMajorTickSpacing(25); this.slider_minP.setMinorTickSpacing(5); this.slider_minP.setLabelTable(LaunchFrame.createDecimalLabelTable(new int[] { 0, 25, 50, 75, 100 })); this.slider_minP.setPaintLabels(true); this.slider_minP.setPaintTicks(true); this.slider_minP.addChangeListener(e -> LaunchFrame.this.updateSamplingValueLabels()); this.panel_3.add(this.slider_minP); } { this.lblMinPValue = new JLabel("-"); this.lblMinPValue.setHorizontalAlignment(SwingConstants.CENTER); this.panel_3.add(this.lblMinPValue); } { this.verticalStrut = Box.createVerticalStrut(20); this.panel_3.add(this.verticalStrut); } } } } } { this.panel_2 = new JPanel(); this.contentPane.add(this.panel_2, BorderLayout.NORTH); { this.btnAdd_1 = new JButton("Add"); this.btnAdd_1.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { JFileChooser fileChooser = new JFileChooser(); fileChooser.setDialogTitle("Select executable"); fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); if (fileChooser.showOpenDialog(LaunchFrame.this) == JFileChooser.APPROVE_OPTION) { final Path executable = fileChooser.getSelectedFile().toPath(); Path interpreter = null; fileChooser = new JFileChooser(); fileChooser.setDialogTitle("Select interpreter or hit cancel if you don't need it"); fileChooser.setDialogType(JFileChooser.OPEN_DIALOG); if (fileChooser.showOpenDialog(LaunchFrame.this) == JFileChooser.APPROVE_OPTION) { interpreter = fileChooser.getSelectedFile().toPath(); } final App app = new App(executable, interpreter, new ArrayList<>(), new ArrayList<>()); Data.appData.apps().add(app); final AppPropertiesDialog appPropertiesDialog = new AppPropertiesDialog(LaunchFrame.this, app, () -> { Data.saveApps(); LaunchFrame.this.refresh(); }); appPropertiesDialog.setLocationRelativeTo(LaunchFrame.this); appPropertiesDialog.setVisible(true); } } }); this.panel_2.add(this.btnAdd_1); } { this.btnEdit_1 = new JButton("Edit"); this.btnEdit_1.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int index = LaunchFrame.this.list.getSelectedIndex(); if (index == -1) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select an app first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } final App app = Data.appData.apps().get(index); final AppPropertiesDialog appPropertiesDialog = new AppPropertiesDialog(LaunchFrame.this, app, () -> { Data.saveApps(); LaunchFrame.this.refresh(); }); appPropertiesDialog.setLocationRelativeTo(LaunchFrame.this); appPropertiesDialog.setVisible(true); } }); this.panel_2.add(this.btnEdit_1); } { this.btnDelete_1 = new JButton("Delete"); this.btnDelete_1.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int index = LaunchFrame.this.list.getSelectedIndex(); if (index == -1) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select an app first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } Data.appData.apps().remove(index); Data.saveApps(); LaunchFrame.this.refresh(); } }); this.panel_2.add(this.btnDelete_1); } { this.btnClone_1 = new JButton("Clone"); this.btnClone_1.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int index = LaunchFrame.this.list.getSelectedIndex(); if (index == -1) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select an app first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } final App app = Data.appData.apps().get(index); Data.appData.apps().add(index, new App(app.path(), app.interpreter(), new ArrayList<>(app.arguments()), new ArrayList<>(app.appArguments()))); Data.saveApps(); LaunchFrame.this.refresh(); } }); this.panel_2.add(this.btnClone_1); } { this.horizontalStrut = Box.createHorizontalStrut(20); this.panel_2.add(this.horizontalStrut); } { this.btnRun_1 = new JButton("Run"); this.btnRun_1.addActionListener(new ActionListener() { @Override public void actionPerformed(final ActionEvent e) { final int AppIndex = LaunchFrame.this.list.getSelectedIndex(); if (AppIndex == -1) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Select an app first", "Error Message", JOptionPane.ERROR_MESSAGE); return; } final App app = Data.appData.apps().get(AppIndex); Model model = null; final String selectedPath = LaunchFrame.this.getSelectedModelPath(); if (selectedPath != null) { final Model saved = Data.findModelByPath(selectedPath); final Model template = saved != null ? saved : Data.defaultModel(selectedPath); model = new Model( template.path(), LaunchFrame.this.slider_context.getValue(), template.maxContextSize(), LaunchFrame.this.slider_gpu.getValue(), LaunchFrame.this.slider_threads.getValue(), LaunchFrame.this.sliderValue(LaunchFrame.this.slider_temperature), LaunchFrame.this.sliderValue(LaunchFrame.this.slider_topP), LaunchFrame.this.slider_topK.getValue(), LaunchFrame.this.sliderValue(LaunchFrame.this.slider_minP), template.promptTemplate(), template.ctime()); } final List argumentOverrides = new ArrayList<>(); if (LaunchFrame.this.table.getCellEditor() != null) { LaunchFrame.this.table.getCellEditor().stopCellEditing(); } for (int i = 0; i < LaunchFrame.this.appArgumentsTableModel.getRowCount(); i++) { final AppArgument appArgument = (AppArgument) LaunchFrame.this.appArgumentsTableModel.getValueAt(i, 0); if (appArgument.optional()) { argumentOverrides.add(appArgument); } } try { app.run(model, argumentOverrides); } catch (final IllegalArgumentException iae) { JOptionPane.showMessageDialog(LaunchFrame.this, iae.getMessage(), "Error Message", JOptionPane.WARNING_MESSAGE); } catch (final IOException ioa) { JOptionPane.showMessageDialog(LaunchFrame.this, "Error: Could not execute program\n" + ioa.getMessage(), "Error Message", JOptionPane.ERROR_MESSAGE); } } }); this.panel_2.add(this.btnRun_1); } } this.pack(); // Apply font scaling to all components UIScaler.scaleComponentFonts(this); UIScaler.centerOnPrimaryDisplay(this); this.updateSamplingValueLabels(); this.repaint(); this.splitPane.setDividerLocation(0.33); this.splitPane_1.setDividerLocation(0.66); this.splitPane_2.setDividerLocation(0.66); this.refresh(); } /** Returns the absolute path string of the currently selected model leaf node, or null. */ private String getSelectedModelPath() { final DefaultMutableTreeNode node = (DefaultMutableTreeNode) this.modelTree.getLastSelectedPathComponent(); if (node != null && node.isLeaf() && node.getUserObject() instanceof Path) { return ((Path) node.getUserObject()).toString(); } return null; } private void refreshModelSliders(final Model model) { this.slider_context.setMaximum(model.maxContextSize()); this.slider_context.setMajorTickSpacing(Math.max(1, model.maxContextSize() / 4)); this.slider_context.setMinorTickSpacing(Math.max(1, model.maxContextSize() / 16)); this.slider_context.setValue(model.contextSize()); this.slider_context.setLabelTable(this.slider_context.createStandardLabels(Math.max(1, model.maxContextSize() / 4))); this.slider_context.setSnapToTicks(true); this.slider_context.repaint(); this.slider_threads.setValue(model.threads()); this.slider_gpu.setValue(model.gpuLayers()); this.slider_temperature.setValue(LaunchFrame.doubleToSlider(model.temperatureValue())); this.slider_topP.setValue(LaunchFrame.doubleToSlider(model.topPValue())); this.slider_topK.setValue(model.topKValue()); this.slider_minP.setValue(LaunchFrame.doubleToSlider(model.minPValue())); this.lblPromptTemplate_1.setText(model.promptTemplate() != null && !model.promptTemplate().isEmpty() ? model.promptTemplate() : "-"); this.updateSamplingValueLabels(); } private void updateSamplingValueLabels() { this.lblTemperatureValue.setText(Model.formatDecimal(this.sliderValue(this.slider_temperature))); this.lblTopPValue.setText(Model.formatDecimal(this.sliderValue(this.slider_topP))); this.lblTopKValue.setText(String.valueOf(this.slider_topK.getValue())); this.lblMinPValue.setText(Model.formatDecimal(this.sliderValue(this.slider_minP))); } private double sliderValue(final JSlider slider) { return slider.getValue() / (double) DECIMAL_SLIDER_SCALE; } private static int doubleToSlider(final double value) { return (int) Math.round(value * DECIMAL_SLIDER_SCALE); } private static Hashtable createDecimalLabelTable(final int[] values) { final Hashtable labels = new Hashtable<>(); for (final int value : values) { labels.put(value, new JLabel(Model.formatDecimal(value / (double) DECIMAL_SLIDER_SCALE))); } return labels; } private void refresh() { // Rebuild apps list this.listModel.clear(); Data.appData.apps() .stream() .map(app -> app.path().getFileName() + " " + app.path().getParent() + " " + app.interpreter()) .forEach(this.listModel::addElement); if (this.table.getCellEditor() != null) { this.table.getCellEditor().stopCellEditing(); } this.appArgumentsTableModel.setRowCount(0); // Rebuild model tree from filesystem final String baseStr = Data.props.getProperty("models"); if (baseStr != null) { final Path base = Path.of(baseStr).toAbsolutePath().normalize(); final DefaultMutableTreeNode root = new DefaultMutableTreeNode(base); final Map nodeMap = new HashMap<>(); nodeMap.put(base, root); for (final Path modelPath : Data.scanModelPaths()) { final Path rel = base.relativize(modelPath); DefaultMutableTreeNode current = root; Path currentAbs = base; for (int i = 0; i < rel.getNameCount(); i++) { currentAbs = currentAbs.resolve(rel.getName(i)); if (!nodeMap.containsKey(currentAbs)) { final DefaultMutableTreeNode node = new DefaultMutableTreeNode(currentAbs); current.add(node); nodeMap.put(currentAbs, node); } current = nodeMap.get(currentAbs); } } this.modelTree.setModel(new DefaultTreeModel(root)); } this.repaint(); } /** Tree cell renderer: shows only the last path component (file/folder name). */ static class ModelTreeCellRenderer extends DefaultTreeCellRenderer { @Override public Component getTreeCellRendererComponent(final JTree tree, final Object value, final boolean sel, final boolean expanded, final boolean leaf, final int row, final boolean hasFocus) { super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); if (value instanceof DefaultMutableTreeNode) { final Object userObj = ((DefaultMutableTreeNode) value).getUserObject(); if (userObj instanceof Path) { final Path p = (Path) userObj; final Path filename = p.getFileName(); setText(filename != null ? filename.toString() : p.toString()); // Highlight models with saved params if (leaf) { final Model saved = Data.findModelByPath(p.toString()); if (saved != null) { setForeground(sel ? Color.WHITE : new Color(0, 100, 0)); } } } } return this; } } static class AppArgumentsJCheckBoxRenderer extends JCheckBox implements TableCellRenderer { private final DefaultTableModel model; public AppArgumentsJCheckBoxRenderer(final DefaultTableModel model) { super(); this.model = model; // Apply comprehensive checkbox scaling UIScaler.scaleCheckBox(this); } @Override public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { this.setText(((AppArgument) value).argument()); this.setSelected(((AppArgument) value).optional() ? ((AppArgument) value).default_() : true); this.setEnabled(this.model.isCellEditable(row, column)); return this; } } static class AppArgumentsJCheckBoxEditor extends AbstractCellEditor implements TableCellEditor { private final JCheckBox checkBox = new JCheckBox(); private AppArgument appArgument; public AppArgumentsJCheckBoxEditor() { // Apply comprehensive checkbox scaling UIScaler.scaleCheckBox(this.checkBox); } @Override public Component getTableCellEditorComponent(final JTable table, final Object value, final boolean isSelected, final int row, final int column) { this.appArgument = (AppArgument) value; this.checkBox.setText(this.appArgument.argument()); this.checkBox.setSelected(this.appArgument.optional() ? this.appArgument.default_() : true); return this.checkBox; } @Override public Object getCellEditorValue() { return new AppArgument(this.appArgument.argument(), this.appArgument.optional(), this.checkBox.isSelected()); } } }