/*
* Copyright 2011 Luke Usherwood.
*
* 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 2.1 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 <http://www.gnu.org/licenses/>.
*/
package net.bettyluke.tracinstant.plugins;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.JTextComponent;
import net.bettyluke.tracinstant.data.Ticket;
/**
* Class containing the views and logic to:
* - locate ticket numbers in some (pasted) text,
* - further filter them according to the main user QUERY, and
* - format the result as an Outlook-style search query
*/
public class FindInTextPanel extends JPanel {
public static ToolPlugin createPlugin() {
return new FindInTextPanel().new Plugin();
}
/** The interface through which the application interacts with us. */
private class Plugin extends ToolPlugin {
@Override
public JComponent initialise(TicketUpdater updater) {
return FindInTextPanel.this;
}
@Override
public void ticketViewUpdated(Ticket[] inView, Ticket[] selected) {
filter.clear();
for (Ticket ticket : inView) {
filter.add(ticket.getNumber());
}
updateOutputFields();
}
@Override
public String toString() {
return "Find tickets in text";
}
}
private static final Pattern TICKET_PATTERN = Pattern.compile("\\#(\\d+)");
private final Map<String, Function<Set<Integer>, String>> formatters =
new LinkedHashMap<String, Function<Set<Integer>, String>>() {{
put("TracInstant search term", FindInTextPanel::formatFoundTicketText);
put("Outlook search (short)", ints -> formatOutlookQuery(ints, 8));
put("Outlook search (long)", ints -> formatOutlookQuery(ints, 22));
}};
/**
* Class that performs a single action 'later', following one or more modifications in the
* processing of a single EDT event.
*/
private static final class DocChangeListener implements DocumentListener {
private final Runnable wrappedRunner;
private boolean pending = false;
public DocChangeListener(final Runnable runnable) {
wrappedRunner = () -> {
runnable.run();
pending = false;
};
}
@Override
public void removeUpdate(DocumentEvent e) {
maybeRun();
}
@Override
public void insertUpdate(DocumentEvent e) {
maybeRun();
}
@Override
public void changedUpdate(DocumentEvent e) {
maybeRun();
}
private void maybeRun() {
if (!pending) {
pending = true;
SwingUtilities.invokeLater(wrappedRunner);
}
}
}
private final JTextComponent sourceTextEditor = createTextArea();
private final JTextComponent resultArea = createTextArea();
private final Set<Integer> filter = new TreeSet<>();
private final JComboBox<String> resultCombo;
private final JCheckBox filterCheck;
private Set<Integer> ticketsInText = Collections.emptySet();
private static JTextComponent createTextArea() {
JTextComponent ta = new JTextArea();
ta.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 14));
return ta;
}
public FindInTextPanel() {
super(new BorderLayout());
JScrollPane scroll1 = new JScrollPane(sourceTextEditor);
JScrollPane scroll2 = new JScrollPane(resultArea);
scroll1.setPreferredSize(new Dimension(50, 50));
scroll2.setPreferredSize(new Dimension(50, 50));
filterCheck = new JCheckBox("Apply filter", true);
resultCombo = new JComboBox<>(formatters.keySet().toArray(new String[0]));
Box box = Box.createHorizontalBox();
box.add(filterCheck);
box.add(new JLabel(" Show tickets as: "));
box.add(resultCombo);
JPanel north = new JPanel(new BorderLayout());
JPanel south= new JPanel(new BorderLayout());
north.add(new JLabel("Paste text to scan for ticket numbers:"), BorderLayout.NORTH);
north.add(scroll1);
south.add(box, BorderLayout.NORTH);
south.add(scroll2);
add(createSplit(north, south));
sourceTextEditor.getDocument().addDocumentListener(new DocChangeListener(() -> {
ticketsInText = scanText(sourceTextEditor.getText());
updateOutputFields();
}));
filterCheck.addActionListener(l -> updateOutputFields());
resultCombo.addActionListener(l -> updateOutputFields());
}
private void updateOutputFields() {
updateOutputFields(formatters.get(resultCombo.getSelectedItem()));
}
private void updateOutputFields(Function<Set<Integer>, String> formatter) {
Set<Integer> tickets = new TreeSet<>(ticketsInText);
if (filterCheck.isSelected()) {
tickets.retainAll(filter);
}
String newFoundText = formatter.apply(tickets);
if (!newFoundText.equals(resultArea.getText())) {
resultArea.setText(newFoundText);
}
}
private JSplitPane createSplit(JComponent top, JComponent bottom) {
JSplitPane split = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true, top, bottom);
split.setResizeWeight(.5f);
return split;
}
/**
* Prints ticket numbers as a (multiple) search queries that Microsoft Outlook can
* handle. It can only handle short strings!
*/
private static String formatOutlookQuery(Set<Integer> tickets, int maxPerLine) {
int count = 0;
StringBuilder sb = new StringBuilder("subject:(");
for (int ticket : tickets) {
if (count == maxPerLine) {
sb.append(")\nsubject:(");
count = 0;
} else if (count > 0) {
sb.append(" OR ");
}
sb.append(ticket);
++count;
}
sb.append(')');
return sb.toString();
}
protected final Set<Integer> scanText(String text) {
Set<Integer> result = new TreeSet<>();
Matcher m = TICKET_PATTERN.matcher(text);
while (m.find()) {
try {
result.add(Integer.parseInt(m.group(1)));
} catch (NumberFormatException ex) {
// Ignore. Probably just overflow.
}
}
return result;
}
protected static final String formatFoundTicketText(Set<Integer> numbers) {
StringBuilder sb = new StringBuilder();
String separator = "#:^(";
for (int i : numbers) {
sb.append(separator).append(i);
separator = "|";
}
if (sb.length() > 0) {
sb.append(")$");
}
return sb.toString();
}
/** A little interactive test. */
public static void main(String[] args) {
JDialog dialog = new JDialog();
dialog.getContentPane().add(new FindInTextPanel());
dialog.setSize(300, 600);
dialog.setModal(true);
dialog.setVisible(true);
System.exit(0);
}
}