/*
* Copyright 2015, RagingGoblin <http://raginggoblin.wordpress.com>
*
* This file is part of SpeechLess.
*
* SpeechLess is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* SpeechLess 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with SpeechLess. If not, see <http://www.gnu.org/licenses/>.
*/
package raging.goblin.speechless.ui;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.stream.Collectors;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.filechooser.FileFilter;
import javax.swing.text.JTextComponent;
import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;
import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;
import lombok.extern.slf4j.Slf4j;
import marytts.exceptions.MaryConfigurationException;
import raging.goblin.speechless.Messages;
import raging.goblin.speechless.UIProperties;
import raging.goblin.speechless.speech.Speeker;
import raging.goblin.speechless.speech.Speeker.EndOfSpeechListener;
@Slf4j
public class ClientWindow extends JFrame implements EndOfSpeechListener {
private static final Messages MESSAGES = Messages.getInstance();
private static final UIProperties PROPERTIES = UIProperties.getInstance();
private Speeker speeker;
private JTextField typingField;
private JTextArea speakingArea;
private JButton saveButton;
private JButton playButton;
private JMenuItem playMenuItem;
private JButton stopButton;
private JMenuItem stopMenuItem;
private JPanel parseTextButtonPanel;
private boolean shiftPressed = false;
private boolean ctrlPressed = false;
private ActionListener playListener = a -> {
setParsing(true);
List<String> speeches = Arrays.asList(speakingArea.getText().trim().split("\n"));
if (speeches.size() < 1) {
setParsing(false);
typingField.grabFocus();
} else {
speeker.speek(speeches);
}
};
private ActionListener stopListener = a -> {
speeker.stopSpeeking();
setParsing(false);
};
private ActionListener saveListener = a -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle(MESSAGES.get("save_to_wav"));
chooser.setFileFilter(new WaveFilter());
int returnValue = chooser.showOpenDialog(ClientWindow.this);
if (returnValue == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
if (!file.getName().endsWith("wav") || !file.getName().endsWith("WAV")) {
file = new File(file.getAbsolutePath() + ".wav");
}
speeker.save(speakingArea.getText(), file);
}
};
public ClientWindow(Speeker speeker) throws MaryConfigurationException {
super(MESSAGES.get("client_window_title"));
this.speeker = speeker;
speeker.addEndOfSpeechListener(this);
initGui();
if (PROPERTIES.isNativeHookEnabled()) {
initNativeHook();
}
setDefaultCloseOperation(EXIT_ON_CLOSE);
}
@Override
public void endOfSpeech() {
SwingUtilities.invokeLater(() -> {
setParsing(false);
typingField.grabFocus();
});
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
typingField.grabFocus();
}
private void initGui() {
setSize(600, 400);
ScreenPositioner.centerOnScreen(this);
BorderLayout layout = new BorderLayout(10, 10);
getContentPane().setLayout(layout);
Border padding = BorderFactory.createEmptyBorder(10, 10, 10, 10);
((JComponent) getContentPane()).setBorder(padding);
speakingArea = new JTextArea();
speakingArea.setWrapStyleWord(true);
speakingArea.setLineWrap(true);
JScrollPane speakingPane = new JScrollPane(speakingArea);
speakingPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
speakingArea.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_TAB) {
if (shiftPressed) {
typingField.grabFocus();
} else {
saveButton.grabFocus();
}
e.consume();
}
}
});
getContentPane().add(speakingPane, BorderLayout.CENTER);
new EditAdapter(speakingArea);
typingField = new JTextField();
getContentPane().add(typingField, BorderLayout.SOUTH);
typingField.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
speeker.speek(Arrays.asList(typingField.getText()));
speakingArea.setText(speakingArea.getText() + "\n" + typingField.getText());
speakingArea.setCaretPosition(speakingArea.getText().length());
typingField.setText("");
typingField.requestFocusInWindow();
}
}
});
new EditAdapter(typingField);
GridLayout buttonLayout = new GridLayout(2, 0);
buttonLayout.setVgap(20);
parseTextButtonPanel = new JPanel(buttonLayout);
parseTextButtonPanel.setPreferredSize(new Dimension(50, 50));
getContentPane().add(parseTextButtonPanel, BorderLayout.EAST);
saveButton = new JButton(Icon.getIcon("/icons/save.png"));
saveButton.setToolTipText(MESSAGES.get("save_tooltip"));
saveButton.addActionListener(saveListener);
parseTextButtonPanel.add(saveButton, BorderLayout.EAST);
playButton = new JButton(Icon.getIcon("/icons/control_play.png"));
playButton.setToolTipText(MESSAGES.get("play_tooltip"));
playButton.addActionListener(playListener);
parseTextButtonPanel.add(playButton, BorderLayout.EAST);
stopButton = new JButton(Icon.getIcon("/icons/control_stop.png"));
stopButton.setToolTipText(MESSAGES.get("stop_tooltip"));
stopButton.addActionListener(stopListener);
addGlobalKeyAdapters(typingField, speakingArea, saveButton, playButton);
setJMenuBar(createMenu());
}
private JMenuBar createMenu() {
JMenuBar menuBar = new JMenuBar();
JMenu fileMenu = new JMenu(MESSAGES.get("file"));
fileMenu.setMnemonic(KeyEvent.VK_F);
menuBar.add(fileMenu);
JMenuItem openFileItem = new JMenuItem(MESSAGES.get("open_file_menu"), Icon.getIcon("/icons/folder_page.png"));
openFileItem.setMnemonic(KeyEvent.VK_O);
openFileItem.addActionListener(a -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle(MESSAGES.get("open_file"));
chooser.setFileFilter(new TxtFilter());
int returnValue = chooser.showOpenDialog(ClientWindow.this);
if (returnValue == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try {
List<String> allLines = Files.readAllLines(file.toPath());
speakingArea.setText(allLines.stream().collect(Collectors.joining("\n")));
} catch (Exception e) {
JOptionPane.showMessageDialog(ClientWindow.this, MESSAGES.get("open_file_error"), MESSAGES.get("error"),
JOptionPane.ERROR_MESSAGE);
log.error("Unable to open text file", e);
}
}
});
fileMenu.add(openFileItem);
JMenuItem saveFileItem = new JMenuItem(MESSAGES.get("save_file_menu"), Icon.getIcon("/icons/page_save.png"));
saveFileItem.setMnemonic(KeyEvent.VK_S);
saveFileItem.addActionListener(a -> {
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle(MESSAGES.get("save_file"));
chooser.setFileFilter(new TxtFilter());
int returnValue = chooser.showOpenDialog(ClientWindow.this);
if (returnValue == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
if (!file.getName().endsWith("txt") || !file.getName().endsWith("TXT")) {
file = new File(file.getAbsolutePath() + ".txt");
}
try {
Files.write(file.toPath(), speakingArea.getText().getBytes());
} catch (Exception e) {
JOptionPane.showMessageDialog(ClientWindow.this, MESSAGES.get("save_file_error"), MESSAGES.get("error"),
JOptionPane.ERROR_MESSAGE);
log.error("Unable to save text file", e);
}
}
});
fileMenu.add(saveFileItem);
fileMenu.addSeparator();
JMenuItem exportItem = new JMenuItem(MESSAGES.get("export_menu"), Icon.getIcon("/icons/save.png"));
exportItem.setMnemonic(KeyEvent.VK_E);
exportItem.addActionListener(saveListener);
fileMenu.add(exportItem);
fileMenu.addSeparator();
JMenuItem quitItem = new JMenuItem(MESSAGES.get("quit"), Icon.getIcon("/icons/cross.png"));
quitItem.setMnemonic(KeyEvent.VK_Q);
quitItem.addActionListener(a -> {
System.exit(0);
});
fileMenu.add(quitItem);
JMenu configureMenu = new JMenu(MESSAGES.get("configure"));
configureMenu.setMnemonic(KeyEvent.VK_C);
menuBar.add(configureMenu);
JMenuItem configureVoiceItem = new JMenuItem(MESSAGES.get("configure_voice"),
Icon.getIcon("/icons/comment_edit.png"));
configureVoiceItem.setMnemonic(KeyEvent.VK_V);
configureVoiceItem.addActionListener(a -> {
VoiceConfigurationDialog dialog = new VoiceConfigurationDialog(ClientWindow.this);
dialog.setVisible(true);
if (dialog.isOkPressed()) {
try {
speeker.initMaryTTS();
} catch (Exception e) {
JOptionPane.showMessageDialog(ClientWindow.this, MESSAGES.get("init_marytts_error"),
MESSAGES.get("error"), JOptionPane.ERROR_MESSAGE);
log.error("Unable to reinitialize marytts", e);
}
}
});
configureMenu.add(configureVoiceItem);
JMenuItem configureGuiItem = new JMenuItem(MESSAGES.get("configure_gui"),
Icon.getIcon("/icons/application_form_edit.png"));
configureGuiItem.setMnemonic(KeyEvent.VK_G);
configureGuiItem.addActionListener(a -> {
GuiConfigDialog dialog = new GuiConfigDialog(ClientWindow.this);
dialog.setVisible(true);
if (dialog.isOkPressed() && dialog.isSettingsChanged()) {
JOptionPane.showMessageDialog(ClientWindow.this, MESSAGES.get("restart_message"),
MESSAGES.get("restart_title"), JOptionPane.INFORMATION_MESSAGE);
}
});
configureMenu.add(configureGuiItem);
JMenu playMenu = new JMenu(MESSAGES.get("play"));
playMenu.setMnemonic(KeyEvent.VK_P);
menuBar.add(playMenu);
playMenuItem = new JMenuItem(MESSAGES.get("play"), Icon.getIcon("/icons/control_play.png"));
playMenuItem.setMnemonic(KeyEvent.VK_L);
playMenuItem.addActionListener(playListener);
playMenu.add(playMenuItem);
stopMenuItem = new JMenuItem(MESSAGES.get("stop"), Icon.getIcon("/icons/control_stop.png"));
stopMenuItem.setMnemonic(KeyEvent.VK_T);
stopMenuItem.addActionListener(stopListener);
playMenu.add(stopMenuItem);
JMenu helpMenu = new JMenu(MESSAGES.get("help"));
helpMenu.setMnemonic(KeyEvent.VK_H);
menuBar.add(helpMenu);
JMenuItem helpItem = new JMenuItem(MESSAGES.get("help"), Icon.getIcon("/icons/help.png"));
helpItem.setMnemonic(KeyEvent.VK_F1);
helpItem.addActionListener(a -> HelpBrowser.getInstance().setVisible(true));
helpMenu.add(helpItem);
JMenuItem aboutItem = new JMenuItem(MESSAGES.get("about"), Icon.getIcon("/icons/star.png"));
aboutItem.setMnemonic(KeyEvent.VK_A);
aboutItem.addActionListener(a -> new AboutDialog(ClientWindow.this).setVisible(true));
helpMenu.add(aboutItem);
return menuBar;
}
private void initNativeHook() {
try {
java.util.logging.Logger logger = java.util.logging.Logger
.getLogger(GlobalScreen.class.getPackage().getName());
logger.setLevel(Level.OFF);
GlobalScreen.registerNativeHook();
GlobalScreen.addNativeKeyListener(new NativeKeyListener() {
private Set<Integer> pressedKeyCodes = new HashSet<>();
@Override
public void nativeKeyTyped(NativeKeyEvent e) {
// Nothing todo
}
@Override
public void nativeKeyReleased(NativeKeyEvent e) {
pressedKeyCodes.clear();
}
@Override
public void nativeKeyPressed(NativeKeyEvent e) {
if (Arrays.stream(PROPERTIES.getNativeHookKeyCodes()).anyMatch(new Integer(e.getKeyCode())::equals)) {
pressedKeyCodes.add(e.getKeyCode());
}
if (pressedKeyCodes.size() == 3) {
pressedKeyCodes.clear();
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (getExtendedState() == Frame.ICONIFIED) {
setExtendedState(Frame.NORMAL);
} else {
setExtendedState(Frame.ICONIFIED);
}
}
});
}
}
});
} catch (
NativeHookException ex)
{
log.warn("Unable to use native hook, disabling it");
PROPERTIES.setNativeHookEnabled(false);
}
}
private void setParsing(boolean parsing) {
typingField.setEnabled(!parsing);
speakingArea.setEnabled(!parsing);
saveButton.setEnabled(!parsing);
playButton.setVisible(!parsing);
playMenuItem.setEnabled(!parsing);
stopButton.setVisible(parsing);
stopMenuItem.setEnabled(parsing);
if (parsing) {
parseTextButtonPanel.remove(playButton);
parseTextButtonPanel.add(stopButton);
} else {
parseTextButtonPanel.add(playButton);
parseTextButtonPanel.remove(stopButton);
}
}
private void addGlobalKeyAdapters(Component... components) {
Arrays.stream(components).forEach(c -> {
c.addKeyListener(new SpecialKeyAdapter());
c.addKeyListener(new ShortCutsAdapter());
});
}
private class SpecialKeyAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
shiftPressed = true;
} else if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = true;
}
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_SHIFT) {
shiftPressed = false;
} else if (e.getKeyCode() == KeyEvent.VK_CONTROL) {
ctrlPressed = false;
}
}
}
private class ShortCutsAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
if (ctrlPressed && e.getKeyCode() == KeyEvent.VK_P) {
playListener.actionPerformed(null);
} else if (ctrlPressed && e.getKeyCode() == KeyEvent.VK_S) {
saveListener.actionPerformed(null);
}
}
}
private class EditAdapter extends MouseAdapter implements FocusListener {
private JTextComponent parent;
private JPopupMenu menu;
public EditAdapter(JTextComponent parent) {
this.parent = parent;
parent.addMouseListener(this);
parent.addFocusListener(this);
menu = createEditMenu();
}
@Override
public void mouseClicked(MouseEvent e) {
parent.requestFocus();
// Left button
if (e.getButton() == MouseEvent.BUTTON3) {
menu.setLocation(e.getXOnScreen() + 1, e.getYOnScreen() + 1);
menu.setVisible(true);
} else {
menu.setVisible(false);
}
}
private JPopupMenu createEditMenu() {
final JPopupMenu menu = new JPopupMenu(MESSAGES.get("edit"));
final JMenuItem cutItem = new JMenuItem(MESSAGES.get("cut"), Icon.getIcon("/icons/cut.png"));
cutItem.addActionListener(a -> {
StringSelection selection = new StringSelection(parent.getSelectedText());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
parent.replaceSelection("");
menu.setVisible(false);
});
menu.add(cutItem);
JMenuItem copyItem = new JMenuItem(MESSAGES.get("copy"), Icon.getIcon("/icons/page_copy.png"));
copyItem.addActionListener(a -> {
StringSelection selection = new StringSelection(parent.getSelectedText());
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null);
menu.setVisible(false);
});
menu.add(copyItem);
JMenuItem pasteItem = new JMenuItem(MESSAGES.get("paste"), Icon.getIcon("/icons/paste_plain.png"));
pasteItem.addActionListener(a -> {
Transferable clipboardContents = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
if (clipboardContents != null && clipboardContents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
try {
String pasted = (String) clipboardContents.getTransferData(DataFlavor.stringFlavor);
parent.replaceSelection(pasted);
} catch (UnsupportedFlavorException | IOException ex) {
log.error("Unable to paste content", ex);
}
}
menu.setVisible(false);
});
menu.add(pasteItem);
JMenuItem deleteItem = new JMenuItem(MESSAGES.get("delete"), Icon.getIcon("/icons/page_white_delete.png"));
deleteItem.addActionListener(a -> {
parent.replaceSelection("");
menu.setVisible(false);
});
menu.add(deleteItem);
menu.addSeparator();
JMenuItem selectAllItem = new JMenuItem(MESSAGES.get("select_all"), Icon.getIcon("/icons/accept.png"));
selectAllItem.addActionListener(a -> {
parent.setSelectionStart(0);
parent.setSelectionEnd(parent.getText().length());
menu.setVisible(false);
});
menu.add(selectAllItem);
return menu;
}
@Override
public void focusGained(FocusEvent e) {
// nothing todo
}
@Override
public void focusLost(FocusEvent e) {
menu.setVisible(false);
}
}
private class WaveFilter extends FileFilter {
@Override
public boolean accept(File file) {
return file.isDirectory() || file.getName().endsWith("wav") || file.getName().endsWith("WAV");
}
@Override
public String getDescription() {
return MESSAGES.get("wav");
}
}
private class TxtFilter extends FileFilter {
@Override
public boolean accept(File file) {
return file.isDirectory() || file.getName().endsWith("txt") || file.getName().endsWith("TXT");
}
@Override
public String getDescription() {
return MESSAGES.get("txt");
}
}
}