Darcula Look and Feel。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

403 lines
15 KiB

/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* 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.bulenkov.darcula;
import com.bulenkov.iconloader.IconLoader;
import com.bulenkov.iconloader.util.ColorUtil;
import com.bulenkov.iconloader.util.EmptyIcon;
import com.bulenkov.iconloader.util.StringUtil;
import com.bulenkov.iconloader.util.SystemInfo;
import sun.awt.AppContext;
import javax.swing.*;
import javax.swing.plaf.ColorUIResource;
import javax.swing.plaf.IconUIResource;
import javax.swing.plaf.InsetsUIResource;
import javax.swing.plaf.basic.BasicLookAndFeel;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.text.DefaultEditorKit;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.StyleSheet;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;
/**
* @author Konstantin Bulenkov
*/
public final class DarculaLaf extends BasicLookAndFeel {
public static final String NAME = "Darcula";
BasicLookAndFeel base;
public DarculaLaf() {
try {
if (SystemInfo.isWindows || SystemInfo.isLinux) {
base = new MetalLookAndFeel();
MetalLookAndFeel.setCurrentTheme(new DarculaMetalTheme());
} else {
final String name = UIManager.getSystemLookAndFeelClassName();
base = (BasicLookAndFeel)Class.forName(name).newInstance();
}
}
catch (Exception ignore) {
log(ignore);
}
}
private void callInit(String method, UIDefaults defaults) {
try {
final Method superMethod = BasicLookAndFeel.class.getDeclaredMethod(method, UIDefaults.class);
superMethod.setAccessible(true);
superMethod.invoke(base, defaults);
}
catch (Exception ignore) {
log(ignore);
}
}
@SuppressWarnings("UnusedParameters")
private static void log(Throwable e) {
//everything is gonna be alright
//e.printStackTrace();
}
@Override
public UIDefaults getDefaults() {
try {
final Method superMethod = BasicLookAndFeel.class.getDeclaredMethod("getDefaults");
superMethod.setAccessible(true);
final UIDefaults metalDefaults =
(UIDefaults)superMethod.invoke(new MetalLookAndFeel());
final UIDefaults defaults = (UIDefaults)superMethod.invoke(base);
initInputMapDefaults(defaults);
initIdeaDefaults(defaults);
patchStyledEditorKit();
patchComboBox(metalDefaults, defaults);
defaults.remove("Spinner.arrowButtonBorder");
defaults.put("Spinner.arrowButtonSize", new Dimension(16, 5));
defaults.put("Tree.collapsedIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/treeNodeCollapsed.png")));
defaults.put("Tree.expandedIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/treeNodeExpanded.png")));
defaults.put("Menu.arrowIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/menuItemArrowIcon.png")));
defaults.put("CheckBoxMenuItem.checkIcon", EmptyIcon.create(16));
defaults.put("RadioButtonMenuItem.checkIcon", EmptyIcon.create(16));
defaults.put("InternalFrame.icon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/internalFrame.png")));
defaults.put("OptionPane.informationIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/option_pane_info.png")));
defaults.put("OptionPane.questionIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/option_pane_question.png")));
defaults.put("OptionPane.warningIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/option_pane_warning.png")));
defaults.put("OptionPane.errorIcon", new IconUIResource(IconLoader.getIcon("/com/bulenkov/darcula/icons/option_pane_error.png")));
if (SystemInfo.isMac && !"true".equalsIgnoreCase(System.getProperty("apple.laf.useScreenMenuBar", "false"))) {
defaults.put("MenuBarUI", "com.bulenkov.darcula.ui.DarculaMenuBarUI");
defaults.put("MenuUI", "javax.swing.plaf.basic.BasicMenuUI");
}
return defaults;
}
catch (Exception ignore) {
log(ignore);
}
return super.getDefaults();
}
private static void patchComboBox(UIDefaults metalDefaults, UIDefaults defaults) {
defaults.remove("ComboBox.ancestorInputMap");
defaults.remove("ComboBox.actionMap");
defaults.put("ComboBox.ancestorInputMap", metalDefaults.get("ComboBox.ancestorInputMap"));
defaults.put("ComboBox.actionMap", metalDefaults.get("ComboBox.actionMap"));
}
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
private static void patchStyledEditorKit() {
try {
StyleSheet defaultStyles = new StyleSheet();
InputStream is = DarculaLaf.class.getResourceAsStream("darcula.css");
Reader r = new BufferedReader(new InputStreamReader(is, "UTF-8"));
defaultStyles.loadRules(r, null);
r.close();
final Field keyField = HTMLEditorKit.class.getDeclaredField("DEFAULT_STYLES_KEY");
keyField.setAccessible(true);
final Object key = keyField.get(null);
AppContext.getAppContext().put(key, defaultStyles);
} catch (Throwable e) {
log(e);
}
}
private void call(String method) {
try {
final Method superMethod = BasicLookAndFeel.class.getDeclaredMethod(method);
superMethod.setAccessible(true);
superMethod.invoke(base);
}
catch (Exception ignore) {
log(ignore);
}
}
public void initComponentDefaults(UIDefaults defaults) {
callInit("initComponentDefaults", defaults);
}
@SuppressWarnings({"HardCodedStringLiteral"})
static void initIdeaDefaults(UIDefaults defaults) {
loadDefaults(defaults);
defaults.put("Table.ancestorInputMap", new UIDefaults.LazyInputMap(new Object[] {
"ctrl C", "copy",
"ctrl V", "paste",
"ctrl X", "cut",
"COPY", "copy",
"PASTE", "paste",
"CUT", "cut",
"control INSERT", "copy",
"shift INSERT", "paste",
"shift DELETE", "cut",
"RIGHT", "selectNextColumn",
"KP_RIGHT", "selectNextColumn",
"LEFT", "selectPreviousColumn",
"KP_LEFT", "selectPreviousColumn",
"DOWN", "selectNextRow",
"KP_DOWN", "selectNextRow",
"UP", "selectPreviousRow",
"KP_UP", "selectPreviousRow",
"shift RIGHT", "selectNextColumnExtendSelection",
"shift KP_RIGHT", "selectNextColumnExtendSelection",
"shift LEFT", "selectPreviousColumnExtendSelection",
"shift KP_LEFT", "selectPreviousColumnExtendSelection",
"shift DOWN", "selectNextRowExtendSelection",
"shift KP_DOWN", "selectNextRowExtendSelection",
"shift UP", "selectPreviousRowExtendSelection",
"shift KP_UP", "selectPreviousRowExtendSelection",
"PAGE_UP", "scrollUpChangeSelection",
"PAGE_DOWN", "scrollDownChangeSelection",
"HOME", "selectFirstColumn",
"END", "selectLastColumn",
"shift PAGE_UP", "scrollUpExtendSelection",
"shift PAGE_DOWN", "scrollDownExtendSelection",
"shift HOME", "selectFirstColumnExtendSelection",
"shift END", "selectLastColumnExtendSelection",
"ctrl PAGE_UP", "scrollLeftChangeSelection",
"ctrl PAGE_DOWN", "scrollRightChangeSelection",
"ctrl HOME", "selectFirstRow",
"ctrl END", "selectLastRow",
"ctrl shift PAGE_UP", "scrollRightExtendSelection",
"ctrl shift PAGE_DOWN", "scrollLeftExtendSelection",
"ctrl shift HOME", "selectFirstRowExtendSelection",
"ctrl shift END", "selectLastRowExtendSelection",
"TAB", "selectNextColumnCell",
"shift TAB", "selectPreviousColumnCell",
//"ENTER", "selectNextRowCell",
"shift ENTER", "selectPreviousRowCell",
"ctrl A", "selectAll",
"meta A", "selectAll",
//"ESCAPE", "cancel",
"F2", "startEditing"
}));
}
@SuppressWarnings("IOResourceOpenedButNotSafelyClosed")
private static void loadDefaults(UIDefaults defaults) {
final Properties properties = new Properties();
final String osSuffix = SystemInfo.isMac ? "mac" : SystemInfo.isWindows ? "windows" : "linux";
try {
InputStream stream = DarculaLaf.class.getResourceAsStream("darcula.properties");
properties.load(stream);
stream.close();
stream = DarculaLaf.class.getResourceAsStream("darcula_" + osSuffix + ".properties");
properties.load(stream);
stream.close();
HashMap<String, Object> darculaGlobalSettings = new HashMap<String, Object>();
final String prefix = "darcula.";
for (String key : properties.stringPropertyNames()) {
if (key.startsWith(prefix)) {
darculaGlobalSettings.put(key.substring(prefix.length()), parseValue(key, properties.getProperty(key)));
}
}
for (Object key : defaults.keySet()) {
if (key instanceof String && ((String)key).contains(".")) {
final String s = (String)key;
final String darculaKey = s.substring(s.lastIndexOf('.') + 1);
if (darculaGlobalSettings.containsKey(darculaKey)) {
defaults.put(key, darculaGlobalSettings.get(darculaKey));
}
}
}
for (String key : properties.stringPropertyNames()) {
final String value = properties.getProperty(key);
defaults.put(key, parseValue(key, value));
}
}
catch (IOException e) {log(e);}
}
private static Object parseValue(String key, String value) {
if ("null".equals(value)) {
return null;
}
if (key.endsWith("Insets")) {
final List<String> numbers = StringUtil.split(value, ",");
return new InsetsUIResource(Integer.parseInt(numbers.get(0)),
Integer.parseInt(numbers.get(1)),
Integer.parseInt(numbers.get(2)),
Integer.parseInt(numbers.get(3)));
} else if (key.endsWith(".border")) {
try {
return Class.forName(value).newInstance();
} catch (Exception e) {log(e);}
} else {
final Color color = ColorUtil.fromHex(value, null);
final Integer invVal = getInteger(value);
final Boolean boolVal = "true".equals(value) ? Boolean.TRUE : "false".equals(value) ? Boolean.FALSE : null;
Icon icon = key.toLowerCase().endsWith("icon") ? null : null; //TODO: copy image loading
if (color != null) {
return new ColorUIResource(color);
} else if (invVal != null) {
return invVal;
} else if (icon != null) {
return new IconUIResource(icon);
} else if (boolVal != null) {
return boolVal;
}
}
return value;
}
private static Integer getInteger(String value) {
try {
return Integer.parseInt(value);
}
catch (NumberFormatException e) {
return null;
}
}
@Override
public String getName() {
return NAME;
}
@Override
public String getID() {
return getName();
}
@Override
public String getDescription() {
return "IntelliJ Dark Look and Feel";
}
@Override
public boolean isNativeLookAndFeel() {
return true;
}
@Override
public boolean isSupportedLookAndFeel() {
return true;
}
@Override
protected void initSystemColorDefaults(UIDefaults defaults) {
callInit("initSystemColorDefaults", defaults);
}
@Override
protected void initClassDefaults(UIDefaults defaults) {
callInit("initClassDefaults", defaults);
}
@Override
public void initialize() {
call("initialize");
}
@Override
public void uninitialize() {
call("uninitialize");
}
@Override
protected void loadSystemColors(UIDefaults defaults, String[] systemColors, boolean useNative) {
try {
final Method superMethod = BasicLookAndFeel.class.getDeclaredMethod("loadSystemColors",
UIDefaults.class,
String[].class,
boolean.class);
superMethod.setAccessible(true);
superMethod.invoke(base, defaults, systemColors, useNative);
}
catch (Exception ignore) {
log(ignore);
}
}
@Override
public boolean getSupportsWindowDecorations() {
return true;
}
@SuppressWarnings({"HardCodedStringLiteral"})
public static void initInputMapDefaults(UIDefaults defaults){
// Make ENTER work in JTrees
InputMap treeInputMap = (InputMap)defaults.get("Tree.focusInputMap");
if(treeInputMap!=null){ // it's really possible. For example, GTK+ doesn't have such map
treeInputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER,0),"toggle");
}
// Cut/Copy/Paste in JTextAreas
InputMap textAreaInputMap=(InputMap)defaults.get("TextArea.focusInputMap");
if(textAreaInputMap!=null){ // It really can be null, for example when LAF isn't properly initialized (Alloy license problem)
installCutCopyPasteShortcuts(textAreaInputMap, false);
}
// Cut/Copy/Paste in JTextFields
InputMap textFieldInputMap=(InputMap)defaults.get("TextField.focusInputMap");
if(textFieldInputMap!=null){ // It really can be null, for example when LAF isn't properly initialized (Alloy license problem)
installCutCopyPasteShortcuts(textFieldInputMap, false);
}
// Cut/Copy/Paste in JPasswordField
InputMap passwordFieldInputMap=(InputMap)defaults.get("PasswordField.focusInputMap");
if(passwordFieldInputMap!=null){ // It really can be null, for example when LAF isn't properly initialized (Alloy license problem)
installCutCopyPasteShortcuts(passwordFieldInputMap, false);
}
// Cut/Copy/Paste in JTables
InputMap tableInputMap=(InputMap)defaults.get("Table.ancestorInputMap");
if(tableInputMap!=null){ // It really can be null, for example when LAF isn't properly initialized (Alloy license problem)
installCutCopyPasteShortcuts(tableInputMap, true);
}
}
private static void installCutCopyPasteShortcuts(InputMap inputMap, boolean useSimpleActionKeys) {
String copyActionKey = useSimpleActionKeys ? "copy" : DefaultEditorKit.copyAction;
String pasteActionKey = useSimpleActionKeys ? "paste" : DefaultEditorKit.pasteAction;
String cutActionKey = useSimpleActionKeys ? "cut" : DefaultEditorKit.cutAction;
// Ctrl+Ins, Shift+Ins, Shift+Del
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.CTRL_MASK | InputEvent.CTRL_DOWN_MASK), copyActionKey);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_INSERT, InputEvent.SHIFT_MASK | InputEvent.SHIFT_DOWN_MASK), pasteActionKey);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, InputEvent.SHIFT_MASK | InputEvent.SHIFT_DOWN_MASK), cutActionKey);
// Ctrl+C, Ctrl+V, Ctrl+X
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, InputEvent.CTRL_MASK | InputEvent.CTRL_DOWN_MASK), copyActionKey);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.CTRL_MASK | InputEvent.CTRL_DOWN_MASK), pasteActionKey);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, InputEvent.CTRL_MASK | InputEvent.CTRL_DOWN_MASK), DefaultEditorKit.cutAction);
}
}