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