/* * This file is part of Quelea, free projection software for churches. * * * This program 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. * * 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.quelea.services.languages.spelling; import javafx.animation.FadeTransition; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.control.MenuItem; import javafx.scene.control.Tooltip; import javafx.scene.image.ImageView; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.util.Duration; import org.quelea.services.languages.LabelGrabber; import org.quelea.services.utils.LineTypeChecker; import org.quelea.services.utils.LineTypeChecker.Type; import org.quelea.services.utils.QueleaProperties; import org.quelea.services.utils.UndoHandler; import org.quelea.windows.lyrics.LyricsTextArea; /** * The spell text area component - wraps a text area to provide spell check * capabilities. * <p/> * * @author Michael */ public class SpellTextArea extends StackPane { private static final int CHECK_FREQ = 1000; private static final double WARNING_OPACITY = 0.5; private SpellingDialog dialog; private LyricsTextArea area; private KeyCode runSpellKey; private Speller speller; private ImageView warning; private Thread checkerThread; private volatile SimpleBooleanProperty spellingOkProperty; private UndoHandler undoHandler; /** * Create a new spell text area. */ public SpellTextArea() { runSpellKey = KeyCode.F7; undoHandler = new UndoHandler(); if (QueleaProperties.get().isDictionaryEnabled()) { speller = new Speller(QueleaProperties.get().getDictionary()); } else { speller = new Speller(null); } area = new LyricsTextArea(); spellingOkProperty = new SimpleBooleanProperty(speller.checkText(area.getTextArea().getText(), true)); getChildren().add(area); warning = new ImageView("file:icons/warning.png"); Tooltip.install(warning, new Tooltip(LabelGrabber.INSTANCE.getLabel("spelling.errors.in.doc.label"))); StackPane.setAlignment(warning, Pos.TOP_RIGHT); StackPane.setMargin(warning, new Insets(5)); warning.setOpacity(0); getChildren().add(warning); dialog = new SpellingDialog(speller); MenuItem undoItem = new MenuItem(LabelGrabber.INSTANCE.getLabel("undo.label")); undoItem.setOnAction(e -> { undo(); }); area.getTextArea().getContextMenu().getItems().add(undoItem); MenuItem redoItem = new MenuItem(LabelGrabber.INSTANCE.getLabel("redo.label")); redoItem.setOnAction(e -> { redo(); }); area.getTextArea().getContextMenu().getItems().add(redoItem); area.getTextArea().getContextMenu().setOnShown(e -> { undoItem.setDisable(!undoHandler.canUndo()); redoItem.setDisable(!undoHandler.canRedo()); }); area.setOnKeyPressed((KeyEvent t) -> { if (t.getCode() == runSpellKey) { runSpellCheck(); } if (t.getCode() == KeyCode.ENTER && t.isShiftDown()) { area.getTextArea().replaceText(area.getTextArea().getCaretPosition(), area.getTextArea().getCaretPosition(), "\n<>"); area.refreshStyle(); } if (t.getCode() == KeyCode.Z && t.isShortcutDown()) { undo(); } if (t.getCode() == KeyCode.Y && t.isShortcutDown()) { redo(); } }); area.getTextArea().textProperty().addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ov, String t, final String t1) { updateSpelling(false); if (!undoHandler.isUndo()) { undoHandler.add(t, t1); } else { undoHandler.setUndo(false); } if (checkerThread != null && checkerThread.isAlive()) { checkerThread.interrupt(); } checkerThread = new Thread() { public void run() { try { Thread.sleep(CHECK_FREQ); Platform.runLater(new Runnable() { @Override public void run() { updateSpelling(true); } }); } catch (InterruptedException ex) { //Quit silently if interrupted } } }; checkerThread.start(); } }); } /** * Activate a spell check for this area. */ public void runSpellCheck() { dialog.check(SpellTextArea.this); } public void setDictionary(Dictionary dict) { speller.setDictionary(dict); updateSpelling(true); } /** * Check the spelling on this text area - called internally to update state, * but can be fired externally also. * <p/> * * @param lastWord true if the last word should be included in the spell * check. */ public void updateSpelling(boolean lastWord) { spellingOkProperty.set(speller.checkText(getText(), lastWord)); FadeTransition transition = new FadeTransition(Duration.seconds(0.2), warning); if (spellingOkProperty.get()) { transition.setFromValue(warning.getOpacity()); transition.setToValue(0); } else { transition.setFromValue(warning.getOpacity()); transition.setToValue(WARNING_OPACITY); } transition.play(); } /** * Get the boolean property representing whether the spelling is ok. * * @return true if the spelling is ok, false otherwise. */ public BooleanProperty spellingOkProperty() { return spellingOkProperty; } /** * Get the underlying text area object used in this control. * <p/> * * @return the text area object. */ public LyricsTextArea getArea() { return area; } /** * Get the text on this text area, excluding any chords. * <p/> * * @return the text area's text, without chord lines. */ public String getText() { String[] lines = area.getTextArea().getText().split("\n"); StringBuilder ret = new StringBuilder(); for (String line : lines) { if (new LineTypeChecker(line).getLineType() != Type.CHORDS) { ret.append(line).append("\n"); } } return ret.toString(); } /** * Get the text on this text area, including any chords. * <p/> * * @return the text area's text, with chord lines. */ public String getTextAndChords() { String[] lines = area.getTextArea().getText().split("\n"); StringBuilder ret = new StringBuilder(); for (String line : lines) { ret.append(line).append("\n"); } return ret.toString(); } /** * Get the key used to run the spell check. F7 by default. * <p/> * * @return the key used to run the spell check. */ public KeyCode getRunSpellKey() { return runSpellKey; } /** * Set the keycode used to run the spell check. F7 by default. * <p/> * * @param runSpellKey the key used to run the spell check. */ public void setRunSpellKey(KeyCode runSpellKey) { this.runSpellKey = runSpellKey; } public void clearUndo() { if (undoHandler != null) { undoHandler.clearUndo(); } } private void undo() { if (undoHandler.canUndo()) { String newText = undoHandler.undo(); if (!newText.equals(getTextAndChords())) { area.getTextArea().replaceText(0, area.getTextArea().getText().length(), newText); int pos = undoHandler.getCaretPos(true); area.getTextArea().selectRange(pos, pos); } } } private void redo() { if (undoHandler.canRedo()) { String newText = undoHandler.redo(); if (!newText.equals(getTextAndChords())) { area.getTextArea().replaceText(0, area.getTextArea().getText().length(), newText); int pos = undoHandler.getCaretPos(false); area.getTextArea().selectRange(pos, pos); } } } }