/* * Copyright 2016 Igor Maznitsa. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.igormaznitsa.sciareto.ui.editors; import java.awt.BorderLayout; import java.awt.Font; import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.StringSelection; import java.io.File; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileFilter; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.fife.ui.rtextarea.RTextScrollPane; import org.fife.ui.rtextarea.RUndoManager; import com.igormaznitsa.mindmap.model.logger.Logger; import com.igormaznitsa.mindmap.model.logger.LoggerFactory; import com.igormaznitsa.sciareto.Context; import com.igormaznitsa.sciareto.preferences.PreferencesManager; import com.igormaznitsa.sciareto.preferences.SpecificKeys; import com.igormaznitsa.sciareto.ui.DialogProviderManager; import com.igormaznitsa.sciareto.ui.SystemUtils; import com.igormaznitsa.sciareto.ui.tabs.TabTitle; import com.igormaznitsa.sciareto.ui.FindTextScopeProvider; public final class SourceTextEditor extends AbstractEditor { private static final Logger LOGGER = LoggerFactory.getLogger(SourceTextEditor.class); public static final Font DEFAULT_FONT = new Font(Font.MONOSPACED, Font.PLAIN, 14); private final RSyntaxTextArea editor; private final TabTitle title; private boolean ignoreChange; private final RUndoManager undoManager; private final JPanel mainPanel; private static final Map<String, List<String>> SRC_EXTENSIONS = new HashMap<String, List<String>>(); private static final Map<String, String> MAP_EXTENSION2TYPE = new HashMap<String, String>(); public static final Set<String> SUPPORTED_EXTENSIONS; private static final String ALLEXTENSIONS; static { SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_ACTIONSCRIPT, Arrays.asList("as")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_C, Arrays.asList("c","h")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_CLOJURE, Arrays.asList("clj", "cljs", "cljc", "edn")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_CPLUSPLUS, Arrays.asList("cc", "cpp", "cxx", "c++","hpp")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_CSHARP, Arrays.asList("cs")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_CSS, Arrays.asList("css")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_DELPHI, Arrays.asList("pas")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_DTD, Arrays.asList("dtd")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_FORTRAN, Arrays.asList("f", "for", "f90", "f95")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_GROOVY, Arrays.asList("groovy")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_HTML, Arrays.asList("htm", "html")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_JAVA, Arrays.asList("java")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_JAVASCRIPT, Arrays.asList("js")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_JSON, Arrays.asList("json")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_JSP, Arrays.asList("jsp")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_LATEX, Arrays.asList("tex")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_LISP, Arrays.asList("lisp", "lsp")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_LUA, Arrays.asList("lua")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_MXML, Arrays.asList("mxml")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_PERL, Arrays.asList("pl")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_PHP, Arrays.asList("php")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_PROPERTIES_FILE, Arrays.asList("properties")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_PYTHON, Arrays.asList("py")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_RUBY, Arrays.asList("rb")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_SCALA, Arrays.asList("scala", "sc")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_SQL, Arrays.asList("sql")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_TCL, Arrays.asList("tcl")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_UNIX_SHELL, Arrays.asList("sh")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_VISUAL_BASIC, Arrays.asList("vb")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_WINDOWS_BATCH, Arrays.asList("bat", "cmd")); //NOI18N SRC_EXTENSIONS.put(SyntaxConstants.SYNTAX_STYLE_XML, Arrays.asList("xml")); //NOI18N final StringBuilder acc = new StringBuilder(); final Set<String> allEtensinsions = new HashSet<String>(); for (final Map.Entry<String, List<String>> e : SRC_EXTENSIONS.entrySet()) { final String type = e.getKey(); for (final String s : e.getValue()) { if (MAP_EXTENSION2TYPE.put(s, type) != null) { throw new Error("Detected duplicated extension : " + s); //NOI18N } if (acc.length() > 0) { acc.append(','); } acc.append("*.").append(s); //NOI18N allEtensinsions.add(s); } } SUPPORTED_EXTENSIONS = Collections.unmodifiableSet(allEtensinsions); ALLEXTENSIONS = acc.toString(); } public static final FileFilter SRC_FILE_FILTER = new FileFilter() { @Override public boolean accept(@Nonnull final File f) { if (f.isDirectory()) { return true; } return MAP_EXTENSION2TYPE.containsKey(FilenameUtils.getExtension(f.getName()).toLowerCase(Locale.ENGLISH)); } @Override @Nonnull public String getDescription() { return "Source files"; } }; @Override @Nonnull public FileFilter getFileFilter() { return SRC_FILE_FILTER; } public SourceTextEditor(@Nonnull final Context context, @Nullable File file) throws IOException { super(); this.editor = new RSyntaxTextArea(); this.editor.setPopupMenu(null); final String syntaxType = file == null ? null : MAP_EXTENSION2TYPE.get(FilenameUtils.getExtension(file.getName()).toLowerCase(Locale.ENGLISH)); this.editor.setSyntaxEditingStyle(syntaxType == null ? SyntaxConstants.SYNTAX_STYLE_NONE : syntaxType); this.editor.setAntiAliasingEnabled(true); this.editor.setBracketMatchingEnabled(true); this.editor.setCodeFoldingEnabled(true); this.editor.getCaret().setSelectionVisible(true); this.editor.setFont(PreferencesManager.getInstance().getFont(PreferencesManager.getInstance().getPreferences(), SpecificKeys.PROPERTY_TEXT_EDITOR_FONT, DEFAULT_FONT)); this.mainPanel = new JPanel(new BorderLayout()); final RTextScrollPane scrollPane = new RTextScrollPane(this.editor, true); this.mainPanel.add(scrollPane, BorderLayout.CENTER); this.title = new TabTitle(context, this, file); this.editor.getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(@Nonnull final DocumentEvent e) { if (!ignoreChange) { title.setChanged(true); } context.notifyUpdateRedoUndo(); } @Override public void removeUpdate(@Nonnull final DocumentEvent e) { if (!ignoreChange) { title.setChanged(true); } context.notifyUpdateRedoUndo(); } @Override public void changedUpdate(@Nonnull final DocumentEvent e) { if (!ignoreChange) { title.setChanged(true); } context.notifyUpdateRedoUndo(); } }); this.undoManager = new RUndoManager(this.editor); loadContent(file); this.editor.discardAllEdits(); this.undoManager.discardAllEdits(); this.undoManager.updateActions(); this.editor.getDocument().addUndoableEditListener(this.undoManager); } @Override public void focusToEditor() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { editor.requestFocusInWindow(); } }); } @Override public boolean isRedo() { return this.undoManager.canRedo(); } @Override public boolean isUndo() { return this.undoManager.canUndo(); } @Override public boolean redo() { if (this.undoManager.canRedo()) { this.undoManager.redo(); } return this.undoManager.canRedo(); } @Override public boolean undo() { if (this.undoManager.canUndo()) { this.undoManager.undo(); } return this.undoManager.canUndo(); } @Override @Nonnull public JComponent getMainComponent() { return this.editor; } @Override public boolean isEditable() { return true; } @Override public boolean isSaveable() { return true; } @Override public void updateConfiguration() { this.editor.setFont(PreferencesManager.getInstance().getFont(PreferencesManager.getInstance().getPreferences(), SpecificKeys.PROPERTY_TEXT_EDITOR_FONT, DEFAULT_FONT)); this.editor.revalidate(); this.editor.repaint(); } @Override public void loadContent(@Nullable final File file) throws IOException { this.ignoreChange = true; try { if (file != null) { this.editor.setText(FileUtils.readFileToString(file, "UTF-8")); //NOI18N this.editor.setCaretPosition(0); } } finally { this.ignoreChange = false; } this.undoManager.discardAllEdits(); this.title.setChanged(false); this.mainPanel.revalidate(); this.mainPanel.repaint(); } @Override public boolean saveDocument() throws IOException { boolean result = false; if (this.title.isChanged()) { File file = this.title.getAssociatedFile(); if (file == null) { file = DialogProviderManager.getInstance().getDialogProvider().msgSaveFileDialog("sources-editor", "Save sources", null, true, getFileFilter(), "Save"); if (file == null) { return result; } } SystemUtils.saveUTFText(file, this.editor.getText()); this.title.setChanged(false); result = true; } else { result = true; } return result; } @Override @Nonnull public TabTitle getTabTitle() { return this.title; } @Override @Nonnull public EditorContentType getEditorContentType() { return EditorContentType.SOURCES; } @Override @Nonnull public JComponent getContainerToShow() { return this.mainPanel; } @Override @Nonnull public AbstractEditor getEditor() { return this; } private boolean searchSubstring(@Nonnull final Pattern pattern, final boolean next) { final String currentText = this.editor.getText(); int cursorPos = this.editor.getCaretPosition(); final Matcher matcher = pattern.matcher(currentText); boolean result = false; if (next) { if (cursorPos < currentText.length()) { if (matcher.find(cursorPos) || matcher.find(0)) { final int foundPosition = matcher.start(); this.editor.select(foundPosition, matcher.end()); this.editor.getCaret().setSelectionVisible(true); result = true; } } } else { int lastFound = -1; int lastFoundEnd = -1; int maxPos = this.editor.getCaret().getMark() == this.editor.getCaret().getDot() ? this.editor.getCaretPosition() : this.editor.getSelectionStart(); for (int i = 0; i < 2; i++) { while (matcher.find()) { final int pos = matcher.start(); if (pos < maxPos) { lastFound = pos; lastFoundEnd = matcher.end(); } else { break; } } if (lastFound >= 0) { break; } maxPos = currentText.length(); } if (lastFound >= 0) { this.editor.select(lastFound, lastFoundEnd); this.editor.getCaret().setSelectionVisible(true); result = true; } } return result; } @Override public boolean findNext(@Nonnull final Pattern pattern, @Nonnull final FindTextScopeProvider provider) { return searchSubstring(pattern, true); } @Override public boolean findPrev(@Nonnull final Pattern pattern, @Nonnull final FindTextScopeProvider provider) { return searchSubstring(pattern, false); } @Override public boolean doesSupportPatternSearch() { return true; } @Override public boolean doCopy() { boolean result = false; final String selected = this.editor.getSelectedText(); if (selected != null && !selected.isEmpty()) { final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(selected), null); } return result; } @Override public boolean doesSupportCutCopyPaste() { return true; } @Override public boolean isCutAllowed() { final String selected = this.editor.getSelectedText(); return selected != null && !selected.isEmpty(); } @Override public boolean doCut() { boolean result = false; final String selected = this.editor.getSelectedText(); if (selected != null && !selected.isEmpty()) { final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(new StringSelection(selected), null); this.editor.replaceSelection(""); //NOI18N } return result; } @Override public boolean doPaste() { boolean result = false; final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); String text = null; try { if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) { text = clipboard.getData(DataFlavor.stringFlavor).toString(); } } catch (Exception ex) { LOGGER.warn("Can't get data from clipboard : " + ex.getMessage()); //NOI18N } if (text != null) { this.editor.replaceSelection(text); result = true; } return result; } @Override public boolean isCopyAllowed() { final String selected = this.editor.getSelectedText(); return selected != null && !selected.isEmpty(); } @Override public boolean isPasteAllowed() { final Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); return clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor); } }