/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander 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. * * muCommander 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 com.mucommander.ui.viewer.text; import java.awt.Font; import java.awt.Insets; import java.awt.Toolkit; import java.awt.event.KeyEvent; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.io.BufferedWriter; import java.io.IOException; import java.io.Reader; import java.io.Writer; import javax.swing.JFrame; import javax.swing.JTextArea; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import javax.swing.text.DefaultEditorKit; import javax.swing.text.Document; import com.mucommander.ui.theme.ColorChangedEvent; import com.mucommander.ui.theme.FontChangedEvent; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeListener; import com.mucommander.ui.theme.ThemeManager; /** * Text editor implementation used by {@link TextViewer} and {@link TextEditor}. * * @author Maxence Bernard, Mariusz Jakubowski, Nicolas Rinaudo, Arik Hadas */ class TextEditorImpl implements ThemeListener { private String searchString; private JFrame frame; private JTextArea textArea; /** Indicates whether there is a line separator in the original file */ private boolean lineSeparatorExists; //////////////////// // Initialization // //////////////////// public TextEditorImpl(boolean isEditable) { // Initialize text area initTextArea(isEditable); // Listen to theme changes to update the text area if it is visible ThemeManager.addCurrentThemeListener(this); } private void initTextArea(boolean isEditable) { textArea = new JTextArea() { @Override public Insets getInsets() { return new Insets(4, 3, 4, 3); } }; textArea.setEditable(isEditable); // Use theme colors and font textArea.setForeground(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR)); textArea.setCaretColor(ThemeManager.getCurrentColor(Theme.EDITOR_FOREGROUND_COLOR)); textArea.setBackground(ThemeManager.getCurrentColor(Theme.EDITOR_BACKGROUND_COLOR)); textArea.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_FOREGROUND_COLOR)); textArea.setSelectionColor(ThemeManager.getCurrentColor(Theme.EDITOR_SELECTED_BACKGROUND_COLOR)); textArea.setFont(ThemeManager.getCurrentFont(Theme.EDITOR_FONT)); textArea.setWrapStyleWord(true); textArea.addMouseWheelListener(new MouseWheelListener() { /** * Mouse events bubble up until finding a component with a relative listener. * That's why in case we get an event that needs to initiate its default behavior, * we just bubble it up to the parent component of the JTextArea. */ public void mouseWheelMoved(MouseWheelEvent e) { boolean isCtrlPressed = (e.getModifiers() & KeyEvent.CTRL_MASK)!=0; if (isCtrlPressed) { Font currentFont = textArea.getFont(); int currentFontSize = currentFont.getSize(); boolean rotationUp = e.getWheelRotation() < 0; if ((!rotationUp && currentFontSize > 1) || rotationUp) { Font newFont = new Font(currentFont.getName(), currentFont.getStyle(), currentFontSize + (rotationUp ? 1 : -1)); textArea.setFont(newFont); } } else { textArea.getParent().dispatchEvent(e); } } }); } ///////////////// // Search code // ///////////////// void find() { FindDialog findDialog = new FindDialog(frame); if(findDialog.wasValidated()) { searchString = findDialog.getSearchString().toLowerCase(); if(!searchString.equals("")) doSearch(0, true); } // Request the focus on the text area which could be lost after the Find dialog was disposed textArea.requestFocus(); } void findNext() { doSearch(textArea.getSelectionEnd(), true); } void findPrevious() { doSearch(textArea.getSelectionStart() - 1, false); } private String getTextLC() { return textArea.getText().toLowerCase(); } private void doSearch(int startPos, boolean forward) { if (searchString == null || searchString.length() == 0) return; int pos; if (forward) { pos = getTextLC().indexOf(searchString, startPos); } else { pos = getTextLC().lastIndexOf(searchString, startPos); } if (pos >= 0) { textArea.select(pos, pos + searchString.length()); } else { // Beep when no match has been found. // The beep method is called from a separate thread because this method seems to lock until the beep has // been played entirely. If the 'Find next' shortcut is left pressed, a series of beeps will be played when // the end of the file is reached, and we don't want those beeps to played one after the other as to: // 1/ not lock the event thread // 2/ have those beeps to end rather sooner than later new Thread() { @Override public void run() { Toolkit.getDefaultToolkit().beep(); } }.start(); } } public boolean isWrap() { return textArea.getLineWrap(); } //////////////////////////// // Package-access methods // //////////////////////////// void wrap(boolean isWrap) { textArea.setLineWrap(isWrap); textArea.repaint(); } void copy() { textArea.copy(); } void cut() { textArea.cut(); } void paste() { textArea.paste(); } void selectAll() { textArea.selectAll(); } void requestFocus() { textArea.requestFocus(); } JTextArea getTextArea() { return textArea; } void addDocumentListener(DocumentListener documentListener) { textArea.getDocument().addDocumentListener(documentListener); } void read(Reader reader) throws IOException { // Feed the file's contents to text area textArea.read(reader, null); // If there are more than one lines, there is a line separator lineSeparatorExists = textArea.getLineCount() > 1; // Move cursor to the top textArea.setCaretPosition(0); } void write(Writer writer) throws IOException { Document document = textArea.getDocument(); // According to the documentation in DefaultEditorKit, the line separator is set to be as the system property // if no other line separator exists in the file, but in practice it is not, so this is a workaround for it if (!lineSeparatorExists) document.putProperty(DefaultEditorKit.EndOfLineStringProperty, System.getProperty("line.separator")); try { textArea.getUI().getEditorKit(textArea).write(new BufferedWriter(writer), document, 0, document.getLength()); } catch(BadLocationException e) { throw new IOException(e.getMessage()); } } ////////////////////////////////// // ThemeListener implementation // ////////////////////////////////// /** * Receives theme color changes notifications. */ public void colorChanged(ColorChangedEvent event) { switch(event.getColorId()) { case Theme.EDITOR_FOREGROUND_COLOR: textArea.setForeground(event.getColor()); break; case Theme.EDITOR_BACKGROUND_COLOR: textArea.setBackground(event.getColor()); break; case Theme.EDITOR_SELECTED_FOREGROUND_COLOR: textArea.setSelectedTextColor(event.getColor()); break; case Theme.EDITOR_SELECTED_BACKGROUND_COLOR: textArea.setSelectionColor(event.getColor()); break; } } /** * Receives theme font changes notifications. */ public void fontChanged(FontChangedEvent event) { if(event.getFontId() == Theme.EDITOR_FONT) textArea.setFont(event.getFont()); } }