package cn.yiiguxing.plugin.translate.ui;
import cn.yiiguxing.plugin.translate.*;
import cn.yiiguxing.plugin.translate.model.BasicExplain;
import cn.yiiguxing.plugin.translate.model.QueryResult;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.JBMenuItem;
import com.intellij.openapi.ui.JBPopupMenu;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.ui.*;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Consumer;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.util.ui.AnimatedIcon;
import com.intellij.util.ui.JBFont;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.PopupMenuEvent;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
public class TranslationDialog extends DialogWrapper implements
TranslationContract.View,
AppStorage.HistoriesChangedListener,
Settings.SettingsChangeListener {
private static final int MIN_WIDTH = 400;
private static final int MIN_HEIGHT = 450;
private static final Border BORDER_ACTIVE = new LineBorder(new JBColor(JBColor.GRAY, Gray._35));
private static final Border BORDER_PASSIVE = new LineBorder(new JBColor(JBColor.LIGHT_GRAY, Gray._75));
private static final String CARD_MSG = "msg";
private static final String CARD_PROCESS = "process";
private static final String CARD_RESULT = "result";
private final Project mProject;
private JPanel mTitlePanel;
private JPanel mContentPane;
private JButton mQueryBtn;
private JPanel mMsgPanel;
private JTextPane mResultText;
private JScrollPane mScrollPane;
@SuppressWarnings("Since15")
private JComboBox<String> mQueryComboBox;
private JPanel mTextPanel;
private JPanel mProcessPanel;
private AnimatedIcon mProcessIcon;
private JLabel mQueryingLabel;
private JEditorPane mMessage;
private CardLayout mLayout;
private final MyModel mModel;
private final TranslationContract.Presenter mTranslationPresenter;
private String mLastSuccessfulQuery;
private QueryResult mLastSuccessfulResult;
private boolean mBroadcast;
private boolean mLastMoveWasInsideDialog;
private final AWTEventListener mAwtActivityListener = new AWTEventListener() {
@Override
public void eventDispatched(AWTEvent e) {
final int id = e.getID();
if (e instanceof MouseEvent && id == MouseEvent.MOUSE_MOVED) {
final boolean inside = isInside(new RelativePoint((MouseEvent) e));
if (inside != mLastMoveWasInsideDialog) {
mLastMoveWasInsideDialog = inside;
((MyTitlePanel) mTitlePanel).myButton.repaint();
}
}
if (e instanceof KeyEvent && id == KeyEvent.KEY_RELEASED) {
final KeyEvent ke = (KeyEvent) e;
// Close the dialog if ESC is pressed
if (ke.getKeyCode() == KeyEvent.VK_ESCAPE) {
close(CLOSE_EXIT_CODE);
}
}
}
};
public TranslationDialog(@Nullable Project project) {
super(project);
this.mProject = project;
setUndecorated(true);
setModal(false);
getPeer().setContentPane(createCenterPanel());
mTranslationPresenter = new TranslationPresenter(this);
mModel = new MyModel(mTranslationPresenter.getHistory());
initViews();
getRootPane().setOpaque(false);
Toolkit.getDefaultToolkit().addAWTEventListener(mAwtActivityListener, AWTEvent.MOUSE_MOTION_EVENT_MASK
| AWTEvent.KEY_EVENT_MASK);
Disposer.register(getDisposable(), new Disposable() {
@Override
public void dispose() {
Toolkit.getDefaultToolkit().removeAWTEventListener(mAwtActivityListener);
}
});
// 在对话框上打开此对话框时,关闭主对话框时导致此对话框也跟着关闭,
// 但资源没有释放干净,回调也没回完整,再次打开的话就会崩溃
getWindow().addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
close(CLOSE_EXIT_CODE);
}
});
MessageBusConnection messageBusConn = ApplicationManager
.getApplication()
.getMessageBus()
.connect(getDisposable());
messageBusConn.subscribe(Settings.SettingsChangeListener.TOPIC, this);
messageBusConn.subscribe(AppStorage.HistoriesChangedListener.TOPIC, this);
}
@Nullable
@Override
protected JComponent createCenterPanel() {
mContentPane.setPreferredSize(JBUI.size(MIN_WIDTH, MIN_HEIGHT));
mContentPane.setBorder(BORDER_ACTIVE);
return mContentPane;
}
private void createUIComponents() {
final MyTitlePanel panel = new MyTitlePanel();
panel.setText("Translation");
panel.setActive(true);
WindowMoveListener windowListener = new WindowMoveListener(panel);
panel.addMouseListener(windowListener);
panel.addMouseMotionListener(windowListener);
getWindow().addWindowListener(new WindowAdapter() {
@Override
public void windowActivated(WindowEvent e) {
panel.setActive(true);
mContentPane.setBorder(BORDER_ACTIVE);
}
@Override
public void windowDeactivated(WindowEvent e) {
panel.setActive(false);
mContentPane.setBorder(BORDER_PASSIVE);
}
});
getWindow().addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
// 放到这里是因为在Android Studio上第一次显示会被queryBtn抢去焦点。
mQueryComboBox.requestFocus();
}
});
}
});
mTitlePanel = panel;
mTitlePanel.requestFocus();
mProcessIcon = new ProcessIcon();
mMessage = new JEditorPane();
mMessage.setContentType("text/html");
mMessage.setEditorKit(getErrorHTMLKit());
mMessage.setEditable(false);
mMessage.setOpaque(false);
mMessage.addHyperlinkListener(new HyperlinkAdapter() {
@Override
protected void hyperlinkActivated(HyperlinkEvent hyperlinkEvent) {
if (Constants.HTML_DESCRIPTION_SETTINGS.equals(hyperlinkEvent.getDescription())) {
close(CLOSE_EXIT_CODE);
TranslationOptionsConfigurable.showSettingsDialog(mProject);
}
}
});
}
@NotNull
private static HTMLEditorKit getErrorHTMLKit() {
HTMLEditorKit kit = UIUtil.getHTMLEditorKit();
JBFont font = JBUI.Fonts.label(14);
StyleSheet styleSheet = kit.getStyleSheet();
styleSheet.addRule(String.format("body {font-family: %s;font-size: %s; text-align: center;}",
font.getFamily(), font.getSize()));
return kit;
}
private boolean isInside(@NotNull RelativePoint target) {
Component cmp = target.getOriginalComponent();
if (!cmp.isShowing()) return true;
if (cmp instanceof MenuElement) return false;
Window window = this.getWindow();
if (UIUtil.isDescendingFrom(cmp, window)) return true;
if (!isShowing()) return false;
Point point = target.getScreenPoint();
SwingUtilities.convertPointFromScreen(point, window);
return window.contains(point);
}
private void initViews() {
mQueryBtn.setIcon(Icons.Translate);
mQueryBtn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
onQueryButtonClick();
}
});
getRootPane().setDefaultButton(mQueryBtn);
initQueryComboBox();
setFont(Settings.getInstance());
mTextPanel.setBorder(BORDER_ACTIVE);
mScrollPane.setVerticalScrollBar(mScrollPane.createVerticalScrollBar());
JBColor background = new JBColor(new Color(0xFFFFFFFF), new Color(0xFF2B2B2B));
mProcessPanel.setBackground(background);
mMsgPanel.setBackground(background);
mResultText.setBackground(background);
mScrollPane.setBackground(background);
mScrollPane.setBorder(new EmptyBorder(0, 0, 0, 0));
mLayout = (CardLayout) mTextPanel.getLayout();
mLayout.show(mTextPanel, CARD_MSG);
mQueryingLabel.setForeground(new JBColor(new Color(0xFF4C4C4C), new Color(0xFFCDCDCD)));
setComponentPopupMenu();
}
@Override
public void onOverrideFontChanged(@NotNull Settings settings) {
setFont(settings);
}
private void setFont(Settings settings) {
if (settings.isOverrideFont()) {
final String fontFamily = settings.getPrimaryFontFamily();
if (!Utils.isEmptyOrBlankString(fontFamily)) {
mResultText.setFont(JBUI.Fonts.create(fontFamily, 14));
} else {
mResultText.setFont(JBUI.Fonts.label(14));
}
} else {
mResultText.setFont(JBUI.Fonts.label(14));
}
if (mLastSuccessfulResult != null) {
setResultText(mLastSuccessfulResult);
}
}
private void onQueryButtonClick() {
String query = mResultText.getSelectedText();
if (Utils.isEmptyOrBlankString(query)) {
query = mQueryComboBox.getEditor().getItem().toString();
}
query(query);
}
private void initQueryComboBox() {
mQueryComboBox.setModel(mModel);
final JTextField field = (JTextField) mQueryComboBox.getEditor().getEditorComponent();
field.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
field.selectAll();
}
@Override
public void focusLost(FocusEvent e) {
field.select(0, 0);
}
});
mQueryComboBox.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {
if (e.getStateChange() == ItemEvent.SELECTED && !mBroadcast) {
onQuery();
}
}
});
mQueryComboBox.setRenderer(new ComboRenderer());
}
private void setComponentPopupMenu() {
JBPopupMenu menu = new JBPopupMenu();
final JBMenuItem copy = new JBMenuItem("Copy", Icons.Copy);
copy.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
mResultText.copy();
}
});
final JBMenuItem query = new JBMenuItem("Query", Icons.Translate);
query.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
query(mResultText.getSelectedText());
}
});
menu.add(copy);
menu.add(query);
menu.addPopupMenuListener(new PopupMenuListenerAdapter() {
@Override
public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
boolean hasSelectedText = !Utils.isEmptyOrBlankString(mResultText.getSelectedText());
copy.setEnabled(hasSelectedText);
query.setEnabled(hasSelectedText);
}
});
mResultText.setComponentPopupMenu(menu);
}
public void show() {
if (!isShowing()) {
super.show();
}
update();
IdeFocusManager.getInstance(mProject).requestFocus(getContentPane(), true);
}
public void update() {
if (isShowing()) {
if (mModel.getSize() > 0) {
query(mModel.getElementAt(0));
}
}
}
public void query(String query) {
if (!Utils.isEmptyOrBlankString(query)) {
mQueryComboBox.getEditor().setItem(query);
onQuery();
}
}
private void onQuery() {
String text = mQueryComboBox.getEditor().getItem().toString();
if (!Utils.isEmptyOrBlankString(text) && !text.equals(mLastSuccessfulQuery)) {
mResultText.setText("");
mProcessIcon.resume();
mLayout.show(mTextPanel, CARD_PROCESS);
mTranslationPresenter.query(text);
}
}
@Override
public void onHistoriesChanged() {
mModel.fireContentsChanged();
mBroadcast = true;// 防止递归查询
mModel.setSelectedItem(mLastSuccessfulQuery);
mBroadcast = false;
}
@Override
public void onHistoryItemChanged(@NotNull String newHistory) {
mModel.fireContentsChanged();
mBroadcast = true;// 防止递归查询
mModel.setSelectedItem(newHistory);
mBroadcast = false;
}
@Override
public void showResult(@NotNull final String query, @NotNull QueryResult result) {
mLastSuccessfulQuery = query;
mLastSuccessfulResult = result;
setResultText(result);
mLayout.show(mTextPanel, CARD_RESULT);
mProcessIcon.suspend();
}
private void setResultText(@NotNull QueryResult result) {
Styles.insertStylishResultText(mResultText, result, new Styles.OnTextClickListener() {
@Override
public void onTextClick(@NotNull JTextPane textPane, @NotNull String text) {
query(text);
}
});
mResultText.setCaretPosition(0);
}
@Override
public void showError(@NotNull String query, @NotNull String error) {
mLastSuccessfulQuery = null;
mLastSuccessfulResult = null;
mMessage.setText(error);
mLayout.show(mTextPanel, CARD_MSG);
}
@SuppressWarnings("Since15")
private static class MyModel extends AbstractListModel<String> implements ComboBoxModel<String> {
private final List<String> myFullList;
private Object mySelectedItem;
MyModel(@NotNull List<String> list) {
myFullList = list;
}
@Override
public String getElementAt(int index) {
return this.myFullList.get(index);
}
@Override
public int getSize() {
return myFullList.size();
}
@Override
public Object getSelectedItem() {
return this.mySelectedItem;
}
@Override
public void setSelectedItem(Object anItem) {
this.mySelectedItem = anItem;
this.fireContentsChanged();
}
void fireContentsChanged() {
this.fireContentsChanged(this, -1, -1);
}
}
private final class ComboRenderer extends ListCellRendererWrapper<String> {
private final StringBuilder builder = new StringBuilder();
private final StringBuilder tipBuilder = new StringBuilder();
@Override
public void customize(JList list, String value, int index, boolean isSelected, boolean cellHasFocus) {
if (list.getWidth() == 0 // 在没有确定大小之前不设置真正的文本,否则控件会被过长的文本撑大.
|| Utils.isEmptyOrBlankString(value)) {
setText("");
} else {
setRenderText(value);
}
}
private void setRenderText(@NotNull String value) {
final StringBuilder builder = this.builder;
final StringBuilder tipBuilder = this.tipBuilder;
builder.setLength(0);
tipBuilder.setLength(0);
builder.append("<html><b>")
.append(value)
.append("</b>");
tipBuilder.append(builder);
final QueryResult cache = mTranslationPresenter.getCache(value);
if (cache != null) {
BasicExplain basicExplain = cache.getBasicExplain();
String[] translation = basicExplain != null ? basicExplain.getExplains() : cache.getTranslation();
if (translation != null && translation.length > 0) {
builder.append(" - <i><small>");
tipBuilder.append("<p/><i>");
for (String tran : translation) {
builder.append(tran).append("; ");
tipBuilder.append(tran).append("<br/>");
}
builder.setLength(builder.length() - 2);
builder.append("</small></i>");
tipBuilder.setLength(builder.length() - 5);
tipBuilder.append("</i>");
}
}
builder.append("</html>");
setText(builder.toString());
tipBuilder.append("</html>");
setToolTipText(tipBuilder.toString());
}
}
private class MyTitlePanel extends TitlePanel {
final CloseButton myButton;
MyTitlePanel() {
super();
myButton = new CloseButton();
add(myButton, BorderLayout.EAST);
int offset = JBUI.scale(2);
setBorder(new EmptyBorder(0, myButton.getPreferredSize().width + offset, 0, offset));
setActive(false);
}
@Override
public void setActive(boolean active) {
super.setActive(active);
if (myButton != null) {
myButton.setActive(active);
}
}
}
private class CloseButton extends IconButton {
CloseButton() {
super(Icons.Close, Icons.ClosePressed, new Consumer<MouseEvent>() {
@Override
public void consume(MouseEvent mouseEvent) {
if (mouseEvent.getClickCount() == 1) {
TranslationDialog.this.close(CLOSE_EXIT_CODE);
}
}
});
}
protected boolean hasPaint() {
return super.hasPaint() && mLastMoveWasInsideDialog;
}
}
}