All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.gui.ApplicationLauncherApp Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

There is a newer version: 1.1.7
Show newest version
/*
 * Copyright (c) 2021, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.gui;

import boofcv.BoofVersion;
import boofcv.gui.image.ShowImages;
import boofcv.io.UtilIO;
import boofcv.misc.BoofMiscOps;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeSelectionModel;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.event.*;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.URI;
import java.util.List;
import java.util.*;

/**
 * Application which lists most of the demonstration application in a GUI and allows the user to double click
 * to launch one in a new JVM.
 *
 * @author Peter Abeles
 */
@SuppressWarnings("ForLoopReplaceableByForEach")
public abstract class ApplicationLauncherApp extends JPanel implements ActionListener, ListDataListener {
	private final JTree tree;

	JButton bKill = new JButton("Kill");
	JCheckBox checkRemoveOnDeath = new JCheckBox("Auto Remove");
	JButton bKillAll = new JButton("Kill All");

	JList processList;
	DefaultListModel listModel = new DefaultListModel<>();
	JTabbedPane outputPanel = new JTabbedPane();

	protected int memoryMB = -1;
	final List processes = new ArrayList<>();

	protected ApplicationLauncherApp( boolean defaultRemoveTabOnDeath ) {
		setLayout(new BorderLayout());

		checkRemoveOnDeath.setSelected(defaultRemoveTabOnDeath);

		DefaultMutableTreeNode root = new DefaultMutableTreeNode("All Categories");
		createTree(root);

		tree = new JTree(root);
		tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);

		final JTextField searchBox = new JTextField();
		searchBox.setToolTipText("Search");
		searchBox.getDocument().addDocumentListener(new DocumentListener() {
			@Override public void insertUpdate( DocumentEvent e ) {
				String text = searchBox.getText();
				DefaultMutableTreeNode selection = (DefaultMutableTreeNode)tree.getModel().getRoot();

				TreePath path = searchTree(text, selection, true);
				if (path != null) {
					tree.setSelectionPath(path);
					tree.scrollPathToVisible(path);
				} else {
					tree.setSelectionPath(null);
				}
			}

			@Override public void removeUpdate( DocumentEvent e ) {
				String text = searchBox.getText();
				DefaultMutableTreeNode selection = (DefaultMutableTreeNode)tree.getModel().getRoot();
				TreePath path = searchTree(text, selection, true);
				if (path != null) {
					tree.setSelectionPath(path);
					tree.scrollPathToVisible(path);
				} else {
					tree.setSelectionPath(null);
				}
			}

			@Override public void changedUpdate( DocumentEvent e ) {}
		});
		// Prevent the vertical size from getting too tall and look silly
		searchBox.setMaximumSize(new Dimension(1000, 50));

		KeyStroke down = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true);
		KeyStroke up = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true);
		Action nextSearch = new AbstractAction() {
			@Override
			public void actionPerformed( ActionEvent e ) {
				DefaultMutableTreeNode currentSelection = tree.getLastSelectedPathComponent() != null
						? (DefaultMutableTreeNode)tree.getLastSelectedPathComponent()
						: (DefaultMutableTreeNode)tree.getModel().getRoot();
				if (currentSelection != null && searchBox.getText() != null) {
					TreePath path = searchTree(searchBox.getText(), currentSelection, true);
					if (path != null) {
						tree.setSelectionPath(path);
						tree.scrollPathToVisible(path);
					} else {
						tree.setSelectionPath(null);
					}
				}
			}
		};
		Action prevSearch = new AbstractAction() {
			@Override
			public void actionPerformed( ActionEvent e ) {
				DefaultMutableTreeNode currentSelection = tree.getLastSelectedPathComponent() != null
						? (DefaultMutableTreeNode)tree.getLastSelectedPathComponent()
						: (DefaultMutableTreeNode)tree.getModel().getRoot();
				if (currentSelection != null && searchBox.getText() != null) {
					TreePath path = searchTree(searchBox.getText(), currentSelection, false);
					if (path != null) {
						tree.setSelectionPath(path);
						tree.scrollPathToVisible(path);
					} else {
						tree.setSelectionPath(null);
					}
				}
			}
		};

		searchBox.getInputMap().put(down, "nextSearch");
		searchBox.getActionMap().put("nextSearch", nextSearch);
		searchBox.getInputMap().put(up, "prevSearch");
		searchBox.getActionMap().put("prevSearch", prevSearch);

		searchBox.addActionListener(e -> {
			//enter key goes to next match
			DefaultMutableTreeNode selection = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();
			if (selection != null) {
				handleClick(selection);
			}
			System.out.println("action");
		});

		MouseListener ml = new MouseAdapter() {
			@Override
			public void mousePressed( MouseEvent e ) {
				if (e.getClickCount() == 2) {
					handleClick((DefaultMutableTreeNode)tree.getLastSelectedPathComponent());
				} else if (SwingUtilities.isRightMouseButton(e)) {
					handleContextMenu(tree, e.getX(), e.getY());
				}
			}
		};
		tree.addMouseListener(ml);

		JTextArea infoPanel = new JTextArea();
		infoPanel.setLineWrap(true);
		infoPanel.setEditable(false);
		infoPanel.setPreferredSize(new Dimension(100, 80));
		infoPanel.setText("BoofCV " + BoofVersion.VERSION + "\nGIT SHA\n" + BoofVersion.GIT_SHA);

		JPanel searchPanel = new JPanel();
		searchPanel.setLayout(new BoxLayout(searchPanel, BoxLayout.X_AXIS));
		searchPanel.add(new JLabel("Search"));
		searchPanel.add(Box.createRigidArea(new Dimension(5, 5)));
		searchPanel.add(searchBox);

		JPanel leftPanel = new JPanel();
		leftPanel.setLayout(new BoxLayout(leftPanel, BoxLayout.Y_AXIS));
		leftPanel.add(searchPanel);
		JScrollPane treeView = new JScrollPane(tree);
		treeView.setPreferredSize(new Dimension(300, 600));
		leftPanel.add(treeView);
		leftPanel.add(infoPanel);

		JPanel actionPanel = new JPanel();
		actionPanel.setLayout(new BoxLayout(actionPanel, BoxLayout.X_AXIS));
		actionPanel.add(bKill);
		actionPanel.add(checkRemoveOnDeath);
		actionPanel.add(Box.createHorizontalGlue());
		actionPanel.add(bKillAll);
		bKill.addActionListener(this);
		bKillAll.addActionListener(this);

		processList = new JList<>(listModel);
		processList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
		processList.setLayoutOrientation(JList.VERTICAL);
		processList.setVisibleRowCount(-1);
		processList.getModel().addListDataListener(this);
		processList.setCellRenderer(new DefaultListCellRenderer() {
			@Override
			public Component getListCellRendererComponent( JList list, Object value, int index, boolean isSelected, boolean cellHasFocus ) {
				Component c = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
				ActiveProcess process = (ActiveProcess)value;
				if (!process.isAlive()) {
					c.setForeground(Color.RED);
				}
				return c;
			}
		});

		JPanel processPanel = new JPanel();
		processPanel.setLayout(new BorderLayout());
		processPanel.add(actionPanel, BorderLayout.NORTH);
		processPanel.add(processList, BorderLayout.CENTER);

		JSplitPane verticalSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, processPanel, outputPanel);
		verticalSplitPane.setDividerLocation(150);
		// divider location won't change when window is resized
		// most of the time you want to increase the view of the text
		verticalSplitPane.setResizeWeight(0.0);

		JSplitPane horizontalSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, verticalSplitPane);
		horizontalSplitPane.setDividerLocation(250);
		horizontalSplitPane.setResizeWeight(0.0);

		add(horizontalSplitPane, BorderLayout.CENTER);
		new ProcessStatusThread().start();

		// get the width of the monitor. This should work in multi-monitor systems
		GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
		// have it be a reasonable size of fill the display
		int width = Math.min(1200, gd.getDisplayMode().getWidth());

		setPreferredSize(new Dimension(width, 600));
	}

	protected abstract void createTree( DefaultMutableTreeNode root );

	protected void createNodes( DefaultMutableTreeNode root, String subjectName, Class... apps ) {
		DefaultMutableTreeNode top = new DefaultMutableTreeNode(subjectName);
		for (int i = 0; i < apps.length; i++) {
			DefaultMutableTreeNode node = new DefaultMutableTreeNode(new AppInfo(apps[i]));
			top.add(node);
		}
		root.add(top);
	}

	private void launch( AppInfo info ) {
		String path = System.getProperty("java.class.path");
		String[] arrayPath = path.split(":");

		List classPath = Arrays.asList(arrayPath);

		final var process = new ActiveProcess();
		process.info = info;
		process.launcher = new JavaRuntimeLauncher(classPath);
		process.launcher.setFrozenTime(-1);
		process.launcher.setMemoryInMB(memoryMB);
		// Having a fixed amount of memory caused problems with memory hungry apps

		synchronized (processes) {
			processes.add(process);
		}

		SwingUtilities.invokeLater(() -> {
			listModel.addElement(process);
			processList.invalidate();
		});

		process.start();
	}

	@SuppressWarnings("JdkObsolete")
	private @Nullable TreePath searchTree( String text, DefaultMutableTreeNode node, boolean forward ) {
		Enumeration e = ((DefaultMutableTreeNode)this.tree.getModel().getRoot()).breadthFirstEnumeration();
		if (!forward) {
			final var tmp = new ArrayDeque();
			while (e.hasMoreElements())
				tmp.push(e.nextElement());

			e = new Enumeration<>() {
				@Override public boolean hasMoreElements() {return !tmp.isEmpty();}
				@Override public TreeNode nextElement() {return tmp.pop();}
			};
		}

		while (e.hasMoreElements()) {
			var n = (DefaultMutableTreeNode)e.nextElement();
			if (n.equals(node)) {
				while (e.hasMoreElements()) {
					DefaultMutableTreeNode candidate = (DefaultMutableTreeNode)e.nextElement();
					if (candidate.getUserObject() instanceof AppInfo) {
						AppInfo candidateInfo = (AppInfo)candidate.getUserObject();
						if (candidateInfo.app.getSimpleName().toLowerCase().contains(text)) {
							return new TreePath(candidate.getPath());
						}
					}
				}
			}
		}

		return null;
	}

	public void handleClick( DefaultMutableTreeNode node ) {
		if (node == null)
			return;
		if (!node.isLeaf())
			return;
		var info = (AppInfo)node.getUserObject();
		launch(info);
		System.out.println("clicked " + info);
	}

	/**
	 * Displays a context menu for a class leaf node
	 * Allows copying of the name and the path to the source
	 */
	private void handleContextMenu( JTree tree, int x, int y ) {
		TreePath path = tree.getPathForLocation(x, y);
		tree.setSelectionPath(path);
		var node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent();

		if (node == null)
			return;
		if (!node.isLeaf()) {
			tree.setSelectionPath(null);
			return;
		}
		final AppInfo info = (AppInfo)node.getUserObject();

		var copyname = new JMenuItem("Copy Name");
		copyname.addActionListener(e -> {
			Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
			clipboard.setContents(new StringSelection(info.app.getSimpleName()), null);
		});

		var copypath = new JMenuItem("Copy Path");
		copypath.addActionListener(e -> {
			String path1 = UtilIO.getSourcePath(info.app.getPackage().getName(), info.app.getSimpleName());
			Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
			clipboard.setContents(new StringSelection(path1), null);
		});

		var github = new JMenuItem("Go to Github");
		github.addActionListener(e -> openInGitHub(info));

		var submenu = new JPopupMenu();
		submenu.add(copyname);
		submenu.add(copypath);
		submenu.add(github);
		submenu.show(tree, x, y);
	}

	/**
	 * Opens github page of source code up in a browser window
	 */
	private void openInGitHub( AppInfo info ) {
		if (Desktop.isDesktopSupported()) {
			try {

				URI uri = new URI(UtilIO.getGithubURL(info.app.getPackage().getName(), info.app.getSimpleName()));
				if (!uri.getPath().isEmpty())
					Desktop.getDesktop().browse(uri);
				else
					System.err.println("Bad URL received");
			} catch (Exception e1) {
				JOptionPane.showMessageDialog(this, "Open GitHub Error",
						"Error connecting: " + e1.getMessage(), JOptionPane.ERROR_MESSAGE);
				System.err.println(e1.getMessage());
			}
		}
	}

	@Override
	public void actionPerformed( ActionEvent e ) {
		if (e.getSource() == bKill) {
			ActiveProcess selected = processList.getSelectedValue();
			if (selected == null)
				return;

			if (selected.isAlive())
				selected.requestKill();
			else {
				removeProcessTab(selected, false);
			}
		} else if (e.getSource() == bKillAll) {
			killAllProcesses(0);
		}
	}

	/**
	 * Requests that all active processes be killed.
	 *
	 * @param blockTimeMS If > 0 then it will block until all processes are killed for the specified number
	 * of milliseconds
	 */
	public void killAllProcesses( long blockTimeMS ) {
		// remove already dead processes from the GUI
		SwingUtilities.invokeLater(() -> {
			DefaultListModel model = (DefaultListModel)processList.getModel();
			for (int i = model.size() - 1; i >= 0; i--) {
				removeProcessTab(model.get(i), false);
			}
		});

		// kill processes that are already running
		synchronized (processes) {
			for (int i = 0; i < processes.size(); i++) {
				processes.get(i).requestKill();
			}
		}

		// block until everything is dead
		if (blockTimeMS > 0) {
			long abortTime = System.currentTimeMillis() + blockTimeMS;
			while (abortTime > System.currentTimeMillis()) {
				int total = 0;
				synchronized (processes) {
					for (int i = 0; i < processes.size(); i++) {
						if (!processes.get(i).isActive()) {
							total++;
						}
					}
					if (processes.size() == total) {
						break;
					}
				}
			}
		}
	}

	/**
	 * Called after a new process is added to the process list
	 */
	@Override public void intervalAdded( ListDataEvent e ) {
		//retrieve the most recently added process and display it
		DefaultListModel listModel = (DefaultListModel)e.getSource();
		ActiveProcess process = (ActiveProcess)listModel.get(listModel.getSize() - 1);
		addProcessTab(process, outputPanel);
	}

	@Override public void intervalRemoved( ListDataEvent e ) {}

	@Override public void contentsChanged( ListDataEvent e ) {}

	private void displaySource( final JTextArea sourceTextArea, ActiveProcess process ) {
		if (sourceTextArea == null)
			return;

		// first try loading it as a regular file
		String path = UtilIO.getSourcePath(process.info.app.getPackage().getName(), process.info.app.getSimpleName());
		String code = UtilIO.readAsString(path);
		if (code == null) {
			code = "Failed to load " + path;
		}

		sourceTextArea.setText(code);

		sourceTextArea.addMouseListener(new MouseAdapter() {
			@Override
			public void mousePressed( MouseEvent e ) {
				super.mousePressed(e);
				if (SwingUtilities.isRightMouseButton(e) && !sourceTextArea.getText().isEmpty()) {
					JPopupMenu menu = new JPopupMenu();
					JMenuItem copy = new JMenuItem("Copy");
					copy.addActionListener(e1 -> {
						Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
						clipboard.setContents(new StringSelection(sourceTextArea.getText()), null);
					});
					menu.add(copy);
					menu.show(sourceTextArea, e.getX(), e.getY());
				}
			}
		});
	}

	private void addProcessTab( final ActiveProcess process, JTabbedPane pane ) {
		String title = process.info.app.getSimpleName();

		final var component = new ProcessTabPanel(process.getId());

		final var sourceTextArea = new RSyntaxTextArea();
		sourceTextArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
		sourceTextArea.setFont(new Font("monospaced", Font.PLAIN, 12));
		sourceTextArea.setCodeFoldingEnabled(true);
		sourceTextArea.setEditable(false);
		sourceTextArea.setLineWrap(true);
		sourceTextArea.setWrapStyleWord(true);

		final var outputTextArea = new JTextArea();
		outputTextArea.setFont(new Font("monospaced", Font.PLAIN, 12));
		outputTextArea.setEditable(false);
		outputTextArea.setLineWrap(true);
		outputTextArea.setWrapStyleWord(true);

		final var container = new JScrollPane();
		container.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

		component.bOpenInGitHub.addActionListener(e -> openInGitHub(process.info));
		component.options.addActionListener(e -> {
			if (component.options.getSelectedIndex() == 0) {
				container.getViewport().removeAll();
				container.getViewport().add(sourceTextArea);
				displaySource(sourceTextArea, process);

				// after the GUI has figured out the shape of everything move scroll to the start of the
				// source code and skip over the preamble stuff
				SwingUtilities.invokeLater(() -> {
					String code = sourceTextArea.getText();
					int scrollTo = UtilIO.indexOfSourceStart(code);
					sourceTextArea.setCaretPosition(scrollTo);
				});
			} else if (component.options.getSelectedIndex() == 1) {
				container.getViewport().removeAll();
				container.getViewport().add(outputTextArea);
			}
		});

		// redirect console output to the GUI
		process.launcher.setPrintOut(new PrintStream(new TextOutputStream(outputTextArea, false)));
		process.launcher.setPrintErr(new PrintStream(new TextOutputStream(outputTextArea, false)));

		// this triggers an event and causes it to be displayed
		component.options.setSelectedIndex(1);

		component.add(container, BorderLayout.CENTER);

		pane.add(title, component);

		pane.setSelectedIndex(pane.getComponentCount() - 1);
	}

	static class TextOutputStream extends OutputStream {
		private final JTextArea textArea;
		private final boolean mirror;

		public TextOutputStream( JTextArea textArea, boolean mirror ) {
			this.textArea = textArea;
			this.mirror = mirror;
		}

		@Override public void write( final int b ) throws IOException {
			if (mirror)
				System.out.write(b);
			SwingUtilities.invokeLater(() -> textArea.append(String.valueOf((char)b)));
		}
	}

	public static class AppInfo {
		Class app;

		public AppInfo( Class app ) {this.app = app;}

		@Override public String toString() {return app.getSimpleName();}
	}

	@SuppressWarnings({"NullAway.Init"})
	public static class ActiveProcess extends Thread {
		AppInfo info;
		JavaRuntimeLauncher launcher;

		volatile boolean active = false;
		JavaRuntimeLauncher.Exit exit;

		@Override public void run() {
			active = true;
			exit = launcher.launch(info.app);
			launcher.getPrintOut().println("\n\n~~~~~~~~~ Finished ~~~~~~~~~");
			System.out.println();
			System.out.println("------------------- Exit condition " + exit);
			active = false;
		}

		public void requestKill() {
			launcher.requestKill();
		}

		public boolean isActive() {
			return active;
		}

		@Override public String toString() {
			if (launcher.isKillRequested() && active) {
				return "Killing " + info;
			} else {
				return info.toString();
			}
		}
	}

	class ProcessStatusThread extends Thread {
		@Override public void run() {
			while (true) {
				synchronized (processes) {
					for (int i = processes.size() - 1; i >= 0; i--) {
						final ActiveProcess p = processes.get(i);

						if (!p.isActive()) {
							SwingUtilities.invokeLater(() -> removeProcessTab(p, true));
							processes.remove(i);
						}
					}
				}
				BoofMiscOps.sleep(250);
			}
		}
	}

	private void removeProcessTab( ActiveProcess process, boolean autoClose ) {
		//interval removed listener is called after element is removed, so the reference
		//can't be retrieved. we call removeProcessTab() here for that reason.
		int index = -1;
		//lookup tab by process, assume one tab per process
		for (int i = 0; i < outputPanel.getComponents().length; i++) {
			ProcessTabPanel component = (ProcessTabPanel)outputPanel.getComponent(i);
			if (component.getProcessId() == process.getId()) {
				if (autoClose && !checkRemoveOnDeath.isSelected()) {
					processList.repaint();
					return;
				}
				index = i;
			}
		}

		if (index == -1)
			return;
		outputPanel.remove(index);

		listModel.removeElement(process);
		processList.invalidate();
	}

	public void showWindow( String title ) {
		JFrame frame = ShowImages.showWindow(this, title, true);
		frame.addWindowListener(new WindowAdapter() {
			@Override
			public void windowClosing( WindowEvent e ) {
				ApplicationLauncherApp.this.killAllProcesses(2000);
			}
		});
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy