/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.keybindings; import java.awt.*; import java.io.*; import java.text.*; import java.util.*; import java.util.List; import javax.swing.*; import net.java.sip.communicator.service.keybindings.*; import net.java.sip.communicator.util.*; import org.jitsi.service.configuration.*; import org.jitsi.service.fileaccess.*; import org.osgi.framework.*; //disambiguation /** * Service that concerns keybinding mappings used by various parts of the UI. * Persistence is handled as follows when started: *
    *
  1. Load default bindings from relative directory
  2. *
  3. Attempt to load custom bindings and resolve any duplicates
  4. *
  5. If merged bindings differ from the custom bindings then this attempts to * save the merged version
  6. *
* Custom bindings attempt to be written again whenever they're changed if the * service is running. Each category of keybindings are stored in its own file. * * @author Damian Johnson */ class KeybindingsServiceImpl implements KeybindingsService, Observer { /** * The Logger instance used by the * KeybindingsServiceImpl class and its instances for logging * output. */ private static final Logger logger = Logger.getLogger(KeybindingsServiceImpl.class); /** * Name of the relative directory that holds default bindings. */ private static final String DEFAULT_KEYBINDING_DIR = "/resources/config/defaultkeybindings"; /** * Name of the directory that holds custom bindings. */ private static final String CUSTOM_KEYBINDING_DIR = "keybindings"; /** * Path where to store the global shortcuts. */ private final static String CONFIGURATION_PATH = "net.java.sip.communicator.impl.keybinding.global"; /** * Path where to store the global shortcuts defaults values. */ private final static String DEFAULTS_VALUES_PATH = "impl.keybinding.global"; /** * Flag indicating if service is running. */ private boolean isRunning = false; /** * Loaded keybinding mappings, maps to null if defaults failed to be loaded. */ private final HashMap bindings = new HashMap(); /** * Loaded global keybinding mappings. */ private GlobalKeybindingSet globalBindings = null; /** * Starts the KeybindingService, for each keybinding category retrieving the * default bindings then overwriting them with any custom bindings that can * be retrieved. This writes the merged copy back if it differs from the * custom bindings. This is a no-op if the service has already been started. * * @param bc the currently valid OSGI bundle context. */ synchronized void start(BundleContext bc) { if (this.isRunning) return; for (KeybindingSet.Category category : KeybindingSet.Category.values()) { // Retrieves default bindings Persistence format = category.getFormat(); LinkedHashMap defaultBindings; try { String defaultPath = DEFAULT_KEYBINDING_DIR + "/" + category.getResource(); InputStream in = getClass().getResourceAsStream(defaultPath); defaultBindings = format.load(in); } catch (IOException exc) { logger.error("default bindings set missing: " + category.getResource(), exc); this.bindings.put(category, null); continue; } catch (ParseException exc) { logger.error("unable to parse default bindings set: " + category.getResource(), exc); this.bindings.put(category, null); continue; } // Attempts to retrieve custom bindings String customPath = CUSTOM_KEYBINDING_DIR + "/" + category.getResource(); File customFile; try { ServiceReference faServiceReference = bc.getServiceReference(FileAccessService.class.getName()); FileAccessService faService = (FileAccessService) bc.getService(faServiceReference); // Makes directory for custom bindings if it doesn't exist File customDir = faService .getPrivatePersistentDirectory(CUSTOM_KEYBINDING_DIR, FileCategory.PROFILE); if (!customDir.exists()) customDir.mkdir(); // Gets file access service to reference persistent storage // of the user customFile = faService.getPrivatePersistentFile(customPath, FileCategory.PROFILE); } catch (Exception exc) { String msg = "unable to secure file for custom bindings (" + customPath + "), using defaults but won't be able to save changes"; logger.error(msg, exc); KeybindingSetImpl newSet = new KeybindingSetImpl(defaultBindings, category, null); this.bindings.put(category, newSet); newSet.addObserver(this); continue; } LinkedHashMap customBindings = null; if (customFile.exists()) { try { FileInputStream in = new FileInputStream(customFile); customBindings = format.load(in); in.close(); } catch (Exception exc) { // If either an IO or ParseException occur then we skip // loading custom bindings } } // Merges custom bindings LinkedHashMap merged = new LinkedHashMap(); if (customBindings != null) { Map customTmp = new LinkedHashMap(customBindings); for (Map.Entry shortcut2action : defaultBindings.entrySet()) { String action = shortcut2action.getValue(); if (customTmp.containsValue(action)) { KeyStroke custom = null; for(Map.Entry customShortcut2action : customTmp.entrySet()) { if (customShortcut2action.getValue().equals(action)) { custom = customShortcut2action.getKey(); break; } } assert custom != null; customTmp.remove(custom); merged.put(custom, action); } else { merged.put(shortcut2action.getKey(), action); } } } else { merged = defaultBindings; } // Writes merged result if (!merged.equals(customBindings)) { try { FileOutputStream out = new FileOutputStream(customFile); format.save(out, merged); out.close(); } catch (IOException exc) { logger.error("unable to write to: " + customFile.getAbsolutePath(), exc); } } KeybindingSetImpl newSet = new KeybindingSetImpl(merged, category, customFile); this.bindings.put(category, newSet); newSet.addObserver(this); } // global shortcut initialization globalBindings = new GlobalKeybindingSetImpl(); globalBindings.setBindings(getGlobalShortcutFromConfiguration()); this.isRunning = true; } /** * Invalidates references to custom bindings, preventing further writes. */ synchronized void stop() { if (!this.isRunning) return; for (KeybindingSetImpl bindingSet : this.bindings.values()) { bindingSet.invalidate(); } this.bindings.clear(); this.saveGlobalShortcutFromConfiguration(); this.isRunning = false; } /** * Provides the bindings associated with a given category. This may be null * if the default bindings failed to be loaded. * * @param category segment of the UI for which bindings should be retrieved * @return mappings of keystrokes to the string representation of their * actions * @throws UnsupportedOperationException if the service isn't running */ public synchronized KeybindingSet getBindings( KeybindingSet.Category category) { if (!this.isRunning) throw new UnsupportedOperationException(); // Started services should have all categories assert this.bindings.containsKey(category); return this.bindings.get(category); } /** * Listens for changes in binding sets so changes can be written. */ public void update(Observable obs, Object arg) { if (obs instanceof KeybindingSetImpl) { KeybindingSetImpl changedBindings = (KeybindingSetImpl) obs; // Attempts to avoid lock if unwritable (this works since bindings // can't become re-writable) if (changedBindings.isWritable()) { synchronized (this) { if (changedBindings.isWritable()) { // Writes new bindings to custom file File customFile = changedBindings.getCustomFile(); try { FileOutputStream out = new FileOutputStream(customFile); Persistence format = changedBindings.getCategory().getFormat(); format.save(out, changedBindings.getBindings()); out.close(); } catch (IOException exc) { logger.error("unable to write to: " + customFile.getAbsolutePath(), exc); } } } } } } /** * Returns list of global shortcuts from the configuration file. * * @return list of global shortcuts. */ public Map> getGlobalShortcutFromConfiguration() { Map> gBindings = new LinkedHashMap>(); ConfigurationService configService = KeybindingsActivator.getConfigService(); String shortcut = null; String shortcut2 = null; String propName = null; String propName2 = null; String names[] = new String[]{"answer", "hangup", "answer_hangup", "contactlist", "mute", "push_to_talk"}; Object configured = configService.getProperty( "net.java.sip.communicator.impl.keybinding.global.configured"); if(configured == null) { // default keystrokes for(String name : names) { List kss = new ArrayList(); propName = DEFAULTS_VALUES_PATH + "." + name + ".1"; shortcut = propName != null ? KeybindingsActivator.getResourceService().getSettingsString( propName) : null; if(shortcut != null) { kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut)); } gBindings.put(name, kss); } configService.setProperty( "net.java.sip.communicator.impl.keybinding.global.configured", "true"); return gBindings; } for(String name : names) { List kss = new ArrayList(); propName = CONFIGURATION_PATH + "." + name + ".1"; propName2 = CONFIGURATION_PATH + "." + name + ".2"; shortcut = propName != null ? (String)configService.getProperty(propName) : null; shortcut2 = propName2 != null ? (String)configService.getProperty(propName2) : null; if(shortcut != null) { kss.add(AWTKeyStroke.getAWTKeyStroke(shortcut)); } // second shortcut is always "special" if(shortcut2 != null) { //16367 is the combination of all possible modifiers //(shift ctrl meta alt altGraph button1 button2 button3 pressed) //it is used to distinguish special key (headset) and others int nb = Integer.parseInt(shortcut2); kss.add(AWTKeyStroke.getAWTKeyStroke(nb, 16367)); } gBindings.put(name, kss); } return gBindings; } /** * Save the configuration file. */ public void saveGlobalShortcutFromConfiguration() { ConfigurationService configService = KeybindingsActivator.getConfigService(); String shortcut = null; String shortcut2 = null; for(Map.Entry> entry : globalBindings.getBindings().entrySet()) { String key = entry.getKey(); List kss = entry.getValue(); String path = CONFIGURATION_PATH; path += "." + key; shortcut = path + ".1"; shortcut2 = path + ".2"; configService.setProperty(shortcut, kss.size() > 0 ? kss.get(0) : null); // second shortcut is special configService.setProperty(shortcut2, (kss.size() > 1 && kss.get(1) != null) ? kss.get(1).getKeyCode() : null); } } /** * Provides the bindings associated with the global category. * * @return global keybinding set */ public GlobalKeybindingSet getGlobalBindings() { return globalBindings; } }