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
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); |
|
} |
|
}
|
|
|