private void build() {
		tableModel = new BatchImportTableModel(batchInfo) {
			@Override
			public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
				super.setValueAt(aValue, rowIndex, columnIndex);
				refreshButtons();
			}

		};
		table = new GTable(tableModel);
		table.getAccessibleContext().setAccessibleName("Batch Content");
		table.addMouseListener(new MouseAdapter() {
			@Override
			public void mouseClicked(MouseEvent e) {

				Point p = e.getPoint();
				int row = table.rowAtPoint(p);
				int col = table.columnAtPoint(p);
				TableColumnModel columnModel = table.getColumnModel();
				TableColumn column = columnModel.getColumn(col);
				int modelIndex = column.getModelIndex();
				if (modelIndex == BatchImportTableModel.COLS.FILES.ordinal()) {
					showFiles(row);
				}
			}
		});

		// Turn off all grid lines - this is a problem on windows.
		table.setShowGrid(false);
		table.setIntercellSpacing(new Dimension(0, 0));

		TableColumn selectedColumn =
			table.getColumnModel().getColumn(BatchImportTableModel.COLS.SELECTED.ordinal());
		selectedColumn.setResizable(false);
		// TODO: automagically get necessary col width
		selectedColumn.setMaxWidth(50);

		TableColumn filesColumn =
			table.getColumnModel().getColumn(BatchImportTableModel.COLS.FILES.ordinal());

		filesColumn.setCellEditor(createFilesColumnCellEditor());
		filesColumn.setCellRenderer(createFilesColumnCellRenderer());

		TableColumn langColumn =
			table.getColumnModel().getColumn(BatchImportTableModel.COLS.LANG.ordinal());
		langColumn.setCellEditor(createLangColumnCellEditor());
		langColumn.setCellRenderer(createLangColumnCellRenderer());

		JScrollPane scrollPane = new JScrollPane(table);

		JPanel filesPanel = new JPanel();
		filesPanel.setLayout(new BorderLayout());
		filesPanel.add(scrollPane, BorderLayout.CENTER);
		filesPanel.setBorder(createTitledBorder("Files to Import", true));

		JPanel sourceListPanel = new JPanel();
		sourceListPanel.setLayout(new BorderLayout());
		sourceListPanel.setBorder(createTitledBorder("Import Sources", false));
		sourceListPanel.getAccessibleContext().setAccessibleName("Source List");

		sourceListModel = new SourcesListModel();

		JList<String> sourceList = new JList<>(sourceListModel);
		sourceList.setName("batch.import.source.list");
		sourceList.getAccessibleContext().setAccessibleName("Batch Import Source List");
		sourceList.addListSelectionListener(e -> {
			if (!e.getValueIsAdjusting()) {
				boolean hasSelection = sourceList.getSelectedIndices().length > 0;
				removeSourceButton.setEnabled(hasSelection);
			}
		});
		JScrollPane sourceListScrollPane = new JScrollPane(sourceList);
		sourceListPanel.add(sourceListScrollPane, BorderLayout.CENTER);
		sourceListScrollPane.getAccessibleContext().setAccessibleName("Source List Scroll");

		JPanel sourceOptionsPanel = new JPanel();
		sourceOptionsPanel.getAccessibleContext().setAccessibleName("Source Options");

		// some padding before the files table
		sourceOptionsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
		sourceListPanel.add(sourceOptionsPanel, BorderLayout.SOUTH);

		JPanel maxDepthPanel = new JPanel();
		JLabel maxDepthLabel = new GDLabel("Depth limit:");
		String maxDepthTip = "Maximum container (ie. nested zip, tar, etc) depth in the " +
			"source file to recursively descend into";
		maxDepthLabel.setToolTipText(maxDepthTip);
		maxDepthPanel.add(maxDepthLabel);

		SpinnerNumberModel spinnerNumberModel =
			new SpinnerNumberModel(batchInfo.getMaxDepth(), 0, 99, 1);
		maxDepthSpinner = new JSpinner(spinnerNumberModel);
		maxDepthSpinner.setToolTipText(maxDepthTip);
		rescanButton = new JButton("Rescan");
		rescanButton.setToolTipText(
			"Clear Files to Import list and rescan Import Sources for applications to import");

		spinnerNumberModel.addChangeListener(e -> {
			rescanButton.setEnabled(
				spinnerNumberModel.getNumber().intValue() != batchInfo.getMaxDepth());
		});
		rescanButton.addActionListener(e -> {
			// NOTE: using invokeLater to avoid event handling issues where
			// the spinner model gets updated several times (ie. multi-decrement when
			// it should be just 1 dec) if we do anything modal.
			SystemUtilities.runSwingLater(() -> {
				setMaxDepth(spinnerNumberModel.getNumber().intValue());
			});
		});
		maxDepthPanel.add(maxDepthSpinner);
		maxDepthPanel.add(rescanButton);
		sourceOptionsPanel.add(maxDepthPanel);

		JPanel sourceListButtonsPanel = new JPanel();
		sourceListButtonsPanel.setLayout(new BorderLayout());
		sourceListButtonsPanel.getAccessibleContext().setAccessibleName("Source List Buttons");

		JButton addSourceButton = new JButton("Add");
		addSourceButton.getAccessibleContext().setAccessibleName("Add Source");
		this.removeSourceButton = new JButton("Remove");
		removeSourceButton.setEnabled(false);
		removeSourceButton.getAccessibleContext().setAccessibleName("Remove");

		addSourceButton.addActionListener(e -> {
			addSources();
		});

		removeSourceButton.addActionListener(e -> {
			List<FSRL> sourcesToRemove = new ArrayList<>();
			for (int index : sourceList.getSelectedIndices()) {
				if (index >= 0 && index < batchInfo.getUserAddedSources().size()) {
					UserAddedSourceInfo uasi = batchInfo.getUserAddedSources().get(index);
					sourcesToRemove.add(uasi.getFSRL());
				}
			}
			for (FSRL fsrl : sourcesToRemove) {
				batchInfo.remove(fsrl);
			}
			refreshData();
		});

		sourceListButtonsPanel.add(addSourceButton, BorderLayout.NORTH);
		sourceListButtonsPanel.add(removeSourceButton, BorderLayout.SOUTH);

		// another wrapping panel so the borderlayout'd sourceListButtonsPanel doesn't
		// get forced to take up the full EAST cell of the containing panel.
		JPanel buttonWrapperPanel = new JPanel();
		buttonWrapperPanel.add(sourceListButtonsPanel);
		sourceListPanel.add(buttonWrapperPanel, BorderLayout.EAST);

		sourceListModel.addListDataListener(new ListDataListener() {
			@Override
			public void intervalRemoved(ListDataEvent e) {
				contentsChanged(e);
			}

			@Override
			public void intervalAdded(ListDataEvent e) {
				contentsChanged(e);
			}

			@Override
			public void contentsChanged(ListDataEvent e) {
				boolean hasSelection = sourceList.getSelectedIndices().length > 0;
				removeSourceButton.setEnabled(hasSelection);
			}
		});

		JPanel outputOptionsPanel = buildOutputOptionsPanel();
		outputOptionsPanel.getAccessibleContext().setAccessibleName("Output Options");

		Box box = Box.createVerticalBox();
		box.add(sourceListPanel);
		box.add(filesPanel);
		box.add(outputOptionsPanel);
		box.getAccessibleContext().setAccessibleName("Batch Import");

		addOKButton();
		addCancelButton();

		addWorkPanel(box);
	}
Shows the batch import dialog (via runSwingLater) and prompts the user to select
a file if the supplied {@code batchInfo} is empty.
<p>
The dialog will chain to the {@link ImportBatchTask} when the user clicks the
OK button.

@param tool {@link PluginTool} that will be the parent of the dialog
@param batchInfo optional {@link BatchInfo} instance with already discovered applications, or null.
@param initialFiles optional {@link List} of {@link FSRL files} to add to the batch import dialog, or null.
@param defaultFolder optional default destination folder for imported files or null for root folder.
@param programManager optional {@link ProgramManager} that will be used to open the newly imported
binaries.
Ghidra/Features/Base/src/main/java/ghidra/plugins/importer/batch/BatchImportDialog.java