📄 richbox.java
字号:
/*
* LumaQQ - Java QQ Client
*
* Copyright (C) 2004 luma <stubma@163.com>
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.tsinghua.lumaqq.widgets.rich;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTError;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.MenuAdapter;
import org.eclipse.swt.events.MenuEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Cursor;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Caret;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.ScrollBar;
import edu.tsinghua.lumaqq.LumaQQ;
/**
* 新的聊天输入框,支持不同大小字体,颜色,显示任意大小图像
*
* @author luma
*/
public class RichBox extends Canvas {
// 光标
private Cursor beamCursor;
// 游标
private Caret caret;
// 只读标志
private boolean readonly;
// 事件监听器
private Listener listener;
// 垂直偏移
private int verticalScrollOffset;
// view顶部的行号
private int topLine;
// 上次重画时的最顶行
private int lastPaintTopLine;
// content and other tools
private IWrappedRichContent content;
private ContentHelper contentHelper;
// GC和gc引用次数
private GC gc;
private int gcReference;
// the client area height. Needed to calculate
// content width for new
private int clientAreaHeight;
// the client area width. Needed during Resize
// callback to determine
private int clientAreaWidth;
// margins
private int leftMargin;
private int topMargin;
private int rightMargin;
private int bottomMargin;
// 光标的X偏移位置,不包括左边缘,这个变量用来保证光标上下移动时有一个基本的基准点
private int columnX;
// 选择的起始和结束偏移,不包括结束位置
private int selectionStart, selectionEnd;
// 选择的锚点,选择开始的参考点
private int selectionAnchor;
// true表示运行用双击选择行
private boolean doubleClickEnabled;
// 右键菜单
private boolean enableContextMenu;
private Menu contextMenu;
// true表示禁止动画
private boolean forbiddenAnimation;
// caret offset,注意要处理图片的情况,过一个图片我们要把offset增加2
private int caretOffset;
// 自动滚动的方向
private int autoScrollDirection;
// 通过用这个颜色在图片上画一次,使图片成为被选择状态
private Color backXorColor;
// 剪贴板
private Clipboard clipboard;
static final char TAB = '\t';
// 按键处理的映射表,key是按键值,value是动作的id
private Map keyActionMap;
// 用户自定义按键的映射表,key是按键值,vlaue是IAction对象
private Map userActionMap;
// 按键动作id
/** 对应于Home */
public static final int LINE_START = 1;
/** 对应于End */
public static final int LINE_END = 2;
/** 上箭头 */
public static final int LINE_UP = 3;
/** 下箭头 */
public static final int LINE_DOWN = 4;
/** 左箭头 */
public static final int COLUMN_LEFT = 5;
/** 右箭头 */
public static final int COLUMN_RIGHT = 6;
/** Ctrl + Home */
public static final int TEXT_START = 7;
/** Ctrl + End */
public static final int TEXT_END = 8;
/** 上翻页 */
public static final int PAGE_UP = 9;
/** 下翻页 */
public static final int PAGE_DOWN = 10;
/** 向左选择一个字符 */
public static final int SELECT_COLUMN_LEFT = 11;
/** 向右选择一个字符 */
public static final int SELECT_COLUMN_RIGHT = 12;
/** 向上选择一行 */
public static final int SELECT_LINE_UP = 13;
/** 向下选择一行 */
public static final int SELECT_LINE_DOWN = 14;
/** 剪切 */
public static final int CUT = 15;
/** 拷贝 */
public static final int COPY = 16;
/** 粘贴 */
public static final int PASTE = 17;
/** 删除光标前一个字符 */
public static final int DELETE_PREVIOUS = 18;
/** 删除光标后一个字符 */
public static final int DELETE_NEXT = 19;
/** 全选 */
public static final int SELECT_ALL = 20;
/** 用户自定义按键 */
public static final int CUSTOM_ACTION = 21;
/** 回车换行 */
public static final int NEW_LINE = 22;
// 回车换行
private static final String CRLF = System.getProperty("line.separator");
// 自动滚动的间隔
private static final int AUTO_SCROLL_INTERVAL = 100;
// 是否是Mac
private static final boolean IS_CARBON;
// 是否windows
private static final boolean IS_WIN32;
// 是否做双缓冲
private static final boolean DOUBLE_BUFFERED;
static {
// 如果不是Mac,底层也不是gtk,则做双缓冲
String platform = SWT.getPlatform();
IS_CARBON = "carbon".equals(platform);
IS_WIN32 = "win32".equals(platform);
DOUBLE_BUFFERED = !IS_CARBON && !"gtk".equals(platform);
}
// 自动滚动的任务
private Runnable autoScrollTimer = new Runnable() {
public void run() {
if (autoScrollDirection == SWT.UP) {
doLineUp();
getDisplay().timerExec(AUTO_SCROLL_INTERVAL, this);
} else if (autoScrollDirection == SWT.DOWN) {
doLineDown();
getDisplay().timerExec(AUTO_SCROLL_INTERVAL, this);
}
}
};
// 事件类型定义
static final int VerifyKey = 3005;
private AnimationManager animationManager;
private ImageResolver imageResolver;
/**
* @param parent
* @param style
*/
public RichBox(Composite parent) {
super(parent, SWT.V_SCROLL | SWT.NO_REDRAW_RESIZE | SWT.NO_BACKGROUND);
initialVariables();
initialControl();
initialContextMenu();
installListeners();
setScrollBars();
createKeyBindings();
}
/**
* 初始化右键菜单
*/
private void initialContextMenu() {
contextMenu = new Menu(this);
MenuItem mi = new MenuItem(contextMenu, SWT.PUSH);
mi.setText(LumaQQ.getString("richbox.menu.copy"));
mi.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
copy();
}
});
mi = new MenuItem(contextMenu, SWT.PUSH);
mi.setText(LumaQQ.getString("richbox.menu.cut"));
mi.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
cut();
}
});
mi = new MenuItem(contextMenu, SWT.PUSH);
mi.setText(LumaQQ.getString("richbox.menu.paste"));
mi.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
paste();
}
});
mi = new MenuItem(contextMenu, SWT.SEPARATOR);
mi = new MenuItem(contextMenu, SWT.PUSH);
mi.setText(LumaQQ.getString("richbox.menu.select.all"));
mi.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
doSelectAll();
}
});
mi = new MenuItem(contextMenu, SWT.PUSH);
mi.setText(LumaQQ.getString("richbox.menu.clear"));
mi.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
clear();
}
});
contextMenu.addMenuListener(new MenuAdapter() {
public void menuShown(MenuEvent e) {
contextMenu.getItem(0).setEnabled(hasSelection());
contextMenu.getItem(1).setEnabled(!readonly & hasSelection());
contextMenu.getItem(2).setEnabled(!readonly);
}
});
}
/**
* Adds a verify key listener. A VerifyKey event is sent by the widget when
* a key is pressed. The widget ignores the key press if the listener sets
* the doit field of the event to false.
* <p>
*
* @param listener
* the listener
* @exception SWTException
* <ul>
* <li>ERROR_WIDGET_DISPOSED - if the receiver has been
* disposed</li>
* <li>ERROR_THREAD_INVALID_ACCESS - if not called from the
* thread that created the receiver</li>
* </ul>
* @exception IllegalArgumentException
* <ul>
* <li>ERROR_NULL_ARGUMENT when listener is null</li>
* </ul>
*/
public void addVerifyKeyListener(VerifyKeyListener listener) {
checkWidget();
if (listener == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
}
RichBoxListener typedListener = new RichBoxListener(listener);
addListener(VerifyKey, typedListener);
}
/**
* 装载事件监听器
*/
private void installListeners() {
listener = new Listener() {
public void handleEvent(Event event) {
switch (event.type) {
case SWT.Dispose:
handleDispose(event);
break;
case SWT.KeyDown:
handleKeyDown(event);
break;
case SWT.MouseDown:
handleMouseDown(event);
break;
case SWT.MouseUp:
handleMouseUp(event);
break;
case SWT.MouseDoubleClick:
handleMouseDoubleClick(event);
break;
case SWT.MouseMove:
handleMouseMove(event);
break;
case SWT.Paint:
handlePaint(event);
break;
case SWT.Resize:
handleResize(event);
break;
case SWT.Traverse:
handleTraverse(event);
break;
}
}
};
ScrollBar verticalBar = getVerticalBar();
if (verticalBar != null) {
verticalBar.addListener(SWT.Selection, new Listener() {
public void handleEvent(Event event) {
handleVerticalScroll(event);
}
});
}
addListener(SWT.Dispose, listener);
addListener(SWT.KeyDown, listener);
addListener(SWT.MouseDown, listener);
addListener(SWT.MouseUp, listener);
addListener(SWT.MouseDoubleClick, listener);
addListener(SWT.MouseMove, listener);
addListener(SWT.Paint, listener);
addListener(SWT.Resize, listener);
addListener(SWT.Traverse, listener);
addDisposeListener(animationManager);
addDisposeListener(imageResolver);
}
/**
* @param event
*/
protected void handleVerticalScroll(Event event) {
setVerticalScrollOffset(getVerticalBar().getSelection(), false);
}
/**
* Scrolls the widget vertically.
* <p>
*
* @param pixelOffset
* the new vertical scroll offset
* @param adjustScrollBar
* true= the scroll thumb will be moved to reflect the new scroll
* offset. false = the scroll thumb will not be moved
* @return true=the widget was scrolled false=the widget was not scrolled,
* the given offset is not valid.
*/
private boolean setVerticalScrollOffset(int pixelOffset, boolean adjustScrollBar) {
Rectangle clientArea;
ScrollBar verticalBar = getVerticalBar();
if (pixelOffset == verticalScrollOffset) {
return false;
}
if (verticalBar != null && adjustScrollBar) {
verticalBar.setSelection(pixelOffset);
}
int h = clientAreaHeight - topMargin - bottomMargin;
int w = clientAreaWidth - leftMargin - rightMargin;
int delta = pixelOffset - verticalScrollOffset;
int srcY = (delta > 0) ? delta : 0;
int destY = (delta > 0) ? 0 : -delta;
delta = Math.abs(delta);
int copyHeight = h - delta;
scroll(leftMargin, topMargin + destY, leftMargin, topMargin + srcY, w, copyHeight, true);
int gap = (delta << 1) - h;
if(gap > 0)
redraw(leftMargin, copyHeight + topMargin, w, gap, true);
verticalScrollOffset = pixelOffset;
calculateTopLine();
setCaretLocation();
if(verticalScrollOffset == 0)
setScrollBars();
return true;
}
/**
* 设置光标位置, 这个方法会修改光标X位置
*/
private void setCaretLocation() {
int lineIndex = content.getLineAtOffset(caretOffset);
int lineOffset = content.getLineStartOffset(lineIndex);
int offsetInLine = caretOffset - lineOffset;
int newCaretX = contentHelper.getXAtOffset(getGC(), lineIndex, offsetInLine);
releaseGC();
setCaretLocation(newCaretX, lineIndex);
}
/**
* 设置光标位置和大小,这个方法会修改光标X位置
*
* @param newCaretX
* 光标的X象素位置,不包括左边缘
* @param lineIndex
* 光标所在行号
*/
private void setCaretLocation(int newCaretX, int lineIndex) {
int y = contentHelper.getHeightOfLines(getGC(), 0, lineIndex) - verticalScrollOffset + topMargin;
int h = contentHelper.getLineHeight(getGC(), lineIndex);
releaseGC();
releaseGC();
// 如果光标和上下边缘有所重叠,则调整光标的大小
if(y < topMargin && topMargin - y < h) {
h -= topMargin - y;
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -