package cn.yiiguxing.plugin.translate;
import cn.yiiguxing.plugin.translate.model.BasicExplain;
import cn.yiiguxing.plugin.translate.model.QueryResult;
import cn.yiiguxing.plugin.translate.model.WebExplain;
import cn.yiiguxing.plugin.translate.ui.PhoneticButton;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.ui.JBColor;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.text.*;
import java.awt.*;
import java.awt.event.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 文本样式
*/
@SuppressWarnings("SpellCheckingInspection")
public final class Styles {
private static final Logger LOGGER = Logger.getInstance("#" + Styles.class.getCanonicalName());
private static final SimpleAttributeSet ATTR_QUERY = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_EXPLAIN_BASE = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_EXPLAIN = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_PRE_EXPLAINS = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_EXPLAINS = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_EXPLAINS_HOVER = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_WEB_EXPLAIN_TITLE = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_WEB_EXPLAIN_KEY = new SimpleAttributeSet();
private static final SimpleAttributeSet ATTR_WEB_EXPLAIN_VALUES = new SimpleAttributeSet();
private static final float QUERY_FONT_SCALE = 1.35f;
private static final float PRE_EXPLAINS_FONT_SCALE = 1.15f;
private static final float EXPLAINS_FONT_SCALE = 1.15f;
private static final Pattern PATTERN_WORD = Pattern.compile("[a-zA-Z]+");
static {
StyleConstants.setItalic(ATTR_QUERY, true);
StyleConstants.setBold(ATTR_QUERY, true);
StyleConstants.setForeground(ATTR_QUERY, new JBColor(0xFFEE6000, 0xFFCC7832));
JBColor fg = new JBColor(0xFF3E7EFF, 0xFF8CBCE1);
StyleConstants.setForeground(ATTR_EXPLAIN_BASE, fg);
StyleConstants.setForeground(ATTR_EXPLAIN, fg);
StyleConstants.setItalic(ATTR_PRE_EXPLAINS, true);
StyleConstants.setForeground(ATTR_PRE_EXPLAINS, new JBColor(0xFF7F0055, 0xFFEAB1FF));
StyleConstants.setForeground(ATTR_EXPLAINS, new JBColor(0xFF170591, 0xFFFFC66D));
StyleConstants.setForeground(ATTR_EXPLAINS_HOVER, new JBColor(0xA60EFF, 0xDF531F));
StyleConstants.setForeground(ATTR_WEB_EXPLAIN_TITLE, new JBColor(0xFF707070, 0xFF808080));
StyleConstants.setForeground(ATTR_WEB_EXPLAIN_KEY, new JBColor(0xFF4C4C4C, 0xFF77B767));
StyleConstants.setForeground(ATTR_WEB_EXPLAIN_VALUES, new JBColor(0xFF707070, 0xFF6A8759));
}
private Styles() {
}
private static void setMouseListeners(@NotNull JTextPane textPane) {
for (MouseListener listener : textPane.getMouseListeners()) {
if (listener instanceof ClickableStyleListener) {
textPane.removeMouseListener(listener);
}
}
for (MouseMotionListener listener : textPane.getMouseMotionListeners()) {
if (listener instanceof ClickableStyleListener) {
textPane.removeMouseMotionListener(listener);
}
}
MouseAdapter listener = new ClickableStyleListener();
textPane.addMouseListener(listener);
textPane.addMouseMotionListener(listener);
}
public static void insertStylishResultText(@NotNull final JTextPane textPane,
@NotNull QueryResult result,
@Nullable OnTextClickListener explainsClickListener) {
setMouseListeners(textPane);
final StyledDocument document = textPane.getStyledDocument();
try {
document.remove(0, document.getLength());
} catch (BadLocationException e) {
e.printStackTrace();
return;
}
insertHeader(textPane, result);
BasicExplain basicExplain = result.getBasicExplain();
if (basicExplain != null) {
insertExplain(textPane, document, basicExplain.getExplains(), true, explainsClickListener);
} else {
insertExplain(textPane, document, result.getTranslation(), false, explainsClickListener);
}
WebExplain[] webExplains = result.getWebExplains();
insertWebExplain(document, webExplains);
if (document.getLength() < 1)
return;
try {
int offset = document.getLength() - 1;
String text = document.getText(offset, 1);
if (text.charAt(0) == '\n') {
document.remove(offset, 1);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 不能静态设置,否则scale改变时不能即时更新
@NotNull
private static MutableAttributeSet updateFontSize(@NotNull MutableAttributeSet attr, Font font, float scale) {
StyleConstants.setFontSize(attr, Math.round(font.getSize() * scale));
return attr;
}
private static void insertHeader(@NotNull JTextPane textPane, QueryResult result) {
Document document = textPane.getDocument();
String query = result.getQuery();
try {
if (!Utils.isEmptyOrBlankString(query)) {
query = query.trim();
document.insertString(document.getLength(),
Character.toUpperCase(query.charAt(0)) + query.substring(1) + "\n",
updateFontSize(ATTR_QUERY, textPane.getFont(), QUERY_FONT_SCALE));
}
BasicExplain be = result.getBasicExplain();
if (be != null) {
boolean hasPhonetic = false;
String phoUK = be.getPhoneticUK();
if (!Utils.isEmptyOrBlankString(phoUK)) {
insertPhonetic(document, result.getQuery(), phoUK, Speech.Phonetic.UK);
hasPhonetic = true;
}
String phoUS = be.getPhoneticUS();
if (!Utils.isEmptyOrBlankString(phoUS)) {
insertPhonetic(document, result.getQuery(), phoUS, Speech.Phonetic.US);
hasPhonetic = true;
}
String pho = be.getPhonetic();
if (!Utils.isEmptyOrBlankString(pho) && !hasPhonetic) {
document.insertString(document.getLength(), "[" + pho + "]", ATTR_EXPLAIN);
hasPhonetic = true;
}
if (hasPhonetic) {
document.insertString(document.getLength(), "\n", null);
}
}
document.insertString(document.getLength(), "\n", null);
} catch (BadLocationException e) {
LOGGER.error("insertHeader ", e);
}
}
private static void insertPhonetic(@NotNull Document document,
@NotNull final String query,
@NotNull String phoneticText,
@NotNull final Speech.Phonetic phonetic) throws BadLocationException {
document.insertString(document.getLength(), phonetic == Speech.Phonetic.UK ? "英[" : "美[", ATTR_EXPLAIN_BASE);
final Settings settings = Settings.getInstance();
final String fontFamily = settings.getPhoneticFontFamily();
if (!settings.isOverrideFont() || Utils.isEmptyOrBlankString(fontFamily)) {
ATTR_EXPLAIN.removeAttribute(StyleConstants.FontFamily);
} else {
StyleConstants.setFontFamily(ATTR_EXPLAIN, fontFamily);
}
document.insertString(document.getLength(), phoneticText, ATTR_EXPLAIN);
document.insertString(document.getLength(), "]", ATTR_EXPLAIN_BASE);
SimpleAttributeSet attr = new SimpleAttributeSet();
StyleConstants.setComponent(attr, new PhoneticButton(new Consumer<MouseEvent>() {
@Override
public void consume(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() == 1) {
Speech.toSpeech(query, phonetic);
}
}
}));
document.insertString(document.getLength(), " ", attr);
}
private static void insertExplain(@NotNull final JTextPane textPane,
@NotNull StyledDocument doc,
@Nullable String[] explains,
boolean splitLabel,
@Nullable OnTextClickListener explainsClickListener) {
if (explains == null || explains.length == 0)
return;
MutableAttributeSet attrPre = updateFontSize(ATTR_PRE_EXPLAINS, textPane.getFont(), PRE_EXPLAINS_FONT_SCALE);
MutableAttributeSet attr = updateFontSize(ATTR_EXPLAINS, textPane.getFont(), EXPLAINS_FONT_SCALE);
try {
for (String exp : explains) {
if (Utils.isEmptyOrBlankString(exp))
continue;
if (splitLabel) {
String[] splits = Utils.splitExplain(exp);
if (splits[0] != null) {
doc.insertString(doc.getLength(), splits[0] + " ", attrPre);
exp = splits[1];
}
final int offset = doc.getLength();
doc.insertString(offset, exp + "\n", attr);
Matcher wordMatcher = PATTERN_WORD.matcher(exp);
String text;
int start;
ClickableStyle style;
while (wordMatcher.find()) {
text = wordMatcher.group();
start = wordMatcher.start() + offset;
style = new ClickableStyle(textPane, text, start, explainsClickListener);
doc.setCharacterAttributes(start, text.length(), setClickableStyle(attr, style), true);
}
} else {
doc.insertString(doc.getLength(), exp + "\n", attr);
}
}
doc.insertString(doc.getLength(), "\n", null);
} catch (BadLocationException e) {
LOGGER.error("insertExplain ", e);
}
}
private static void insertWebExplain(Document doc, WebExplain[] webExplains) {
if (webExplains == null || webExplains.length == 0)
return;
try {
doc.insertString(doc.getLength(), "网络释义:\n", ATTR_WEB_EXPLAIN_TITLE);
for (WebExplain webExplain : webExplains) {
doc.insertString(doc.getLength(), webExplain.getKey(), ATTR_WEB_EXPLAIN_KEY);
doc.insertString(doc.getLength(), " -", null);
String[] values = webExplain.getValues();
for (int i = 0; i < values.length; i++) {
doc.insertString(doc.getLength(), " " + values[i] + (i < values.length - 1 ? ";" : ""),
ATTR_WEB_EXPLAIN_VALUES);
}
doc.insertString(doc.getLength(), "\n", null);
}
} catch (BadLocationException e) {
LOGGER.error("insertWebExplain ", e);
}
}
private static MutableAttributeSet setClickableStyle(MutableAttributeSet attrSet, ClickableStyle style) {
attrSet = (MutableAttributeSet) attrSet.copyAttributes();
attrSet.addAttribute(ClickableStyle.class, style);
return attrSet;
}
public interface OnTextClickListener {
void onTextClick(@NotNull JTextPane textPane, @NotNull String text);
}
@SuppressWarnings("WeakerAccess")
private static final class ClickableStyle {
private final JTextPane mTextPane;
private final String mText;
private final int mStartOffset;
private final OnTextClickListener mListener;
private boolean mHover;
public ClickableStyle(@NotNull JTextPane textPane, @NotNull String text, int startOffset,
@Nullable OnTextClickListener listener) {
this.mTextPane = textPane;
this.mText = text;
this.mStartOffset = startOffset;
this.mListener = listener;
}
void performClick() {
if (mListener != null) {
mListener.onTextClick(mTextPane, mText);
}
}
void onHover() {
if (!mHover) {
StyledDocument document = mTextPane.getStyledDocument();
MutableAttributeSet attr = updateFontSize(ATTR_EXPLAINS_HOVER, mTextPane.getFont(), EXPLAINS_FONT_SCALE);
attr = setClickableStyle(attr, this);
document.setCharacterAttributes(mStartOffset, mText.length(), attr, true);
mHover = true;
}
}
void clearHover() {
if (mHover) {
StyledDocument document = mTextPane.getStyledDocument();
MutableAttributeSet attr = updateFontSize(ATTR_EXPLAINS, mTextPane.getFont(), EXPLAINS_FONT_SCALE);
attr = setClickableStyle(attr, this);
document.setCharacterAttributes(mStartOffset, mText.length(), attr, true);
mHover = false;
}
}
}
private static final class ClickableStyleListener extends MouseAdapter {
private ClickableStyle mLastHover;
@Override
public void mouseClicked(MouseEvent e) {
if ((e.getModifiers() & InputEvent.BUTTON1_MASK) == 0 || e.getClickCount() > 1)
return;
ClickableStyle clickableStyle = getClickableStyle(e);
if (clickableStyle != null) {
clickableStyle.performClick();
}
}
private ClickableStyle getClickableStyle(MouseEvent e) {
JTextPane textPane = (JTextPane) e.getComponent();
StyledDocument document = textPane.getStyledDocument();
Element ele = document.getCharacterElement(textPane.viewToModel(e.getPoint()));
MutableAttributeSet as = (MutableAttributeSet) ele.getAttributes();
return (ClickableStyle) as.getAttribute(ClickableStyle.class);
}
@Override
public void mouseMoved(MouseEvent e) {
final ClickableStyle lastHover = mLastHover;
final ClickableStyle hover = getClickableStyle(e);
if (lastHover != hover) {
mLastHover = hover;
if (lastHover != null) {
lastHover.clearHover();
}
if (hover != null) {
hover.onHover();
}
}
}
@Override
public void mouseExited(MouseEvent e) {
if (mLastHover != null) {
mLastHover.clearHover();
mLastHover = null;
}
}
}
}