/******************************************************************************* * Copyright (C) 2011, Google Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html *******************************************************************************/ package com.google.eclipse.mechanic.core.keybinding; import java.io.FileNotFoundException; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IStatus; import org.eclipse.swt.SWT; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.keys.IBindingService; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.eclipse.mechanic.core.keybinding.KbaChangeSet.Action; import com.google.eclipse.mechanic.core.keybinding.KbaChangeSet.KbaBindingList; import com.google.eclipse.mechanic.core.keybinding.KeyBindingsManualFormatter.BindingType; import com.google.eclipse.mechanic.plugin.core.MechanicLog; /** * Dumps existing keyboard bindings to a bootstrap file. */ public final class KbaBootstrapper { private final IBindingService bindingService; private final MechanicLog log; private final String currentPlatform; public KbaBootstrapper() { this(MechanicLog.getDefault(), (IBindingService) PlatformUI.getWorkbench().getService(IBindingService.class), SWT.getPlatform()); } KbaBootstrapper( final MechanicLog log, final IBindingService bindingService, final String currentPlatform) { this.log = log; this.bindingService = bindingService; this.currentPlatform = currentPlatform; } private static final Predicate<EclBinding> ACCEPT_SYSTEM_BINDINGS_FILTER = new Predicate<EclBinding>() { public boolean apply(EclBinding b) { if (b.getType() != BindingType.SYSTEM) { return false; } if (!b.hasCommand()) { // Uh? I have no idea what these mean, but they exist -- they are system // bindings to no command. return false; } return true; } }; private static final Predicate<EclBinding> ACCEPT_USER_BINDINGS_FILTER = new Predicate<EclBinding>() { public boolean apply(EclBinding b) { if (b.getType() != BindingType.USER) { return false; } if (!b.hasCommand()) { // TODO: support removing commands if (!KeyboardBindingsTask.ENABLE_EXP_REM()) { return false; } } return true; } }; public void evaluate(IPath outputLocation, String description) throws FileNotFoundException, IOException { Iterable<EclBinding> allBindings = EclBinding.from(Arrays.asList(bindingService.getBindings())); // Because of the weird way Eclipse stores removed system bindings, we // first need to build the entire system bindings map, before we build the // user bindings map -- to build the user bindings map, we need an instance // of the system bindings map. Map<KbaChangeSetQualifier, KbaChangeSet> systemBindings = buildSystemBindingsMap(Iterables.filter(allBindings, ACCEPT_SYSTEM_BINDINGS_FILTER)); Map<KbaChangeSetQualifier, KbaChangeSet> userBindings = buildUserBindingsMap(currentPlatform, systemBindings, Iterables.filter(allBindings, ACCEPT_USER_BINDINGS_FILTER), log); new KeyBindingsManualFormatter(userBindings, systemBindings) .dumpBindingsToFile(outputLocation, description); } private static KbaBinding bindingToRemoveKbaBinding( final String currentPlatform, final Map<KbaChangeSetQualifier, KbaChangeSet> systemBindingsMap, final EclBinding binding, final MechanicLog log) { if (binding.hasCommand()) { throw new IllegalStateException(); } final KbaChangeSetQualifier q = binding.with(Action.REMOVE); KbaBinding doppleGanger = findDoppleganger(q.platform, systemBindingsMap, q, binding); if (doppleGanger == null) { // Sigh... If the "rem" binding's platform is null, it means apply to all // platforms. So, if we can't find a doppleganger for "null", we should // also look for a doppleganger with the current platform. if (q.platform == null) { doppleGanger = findDoppleganger(currentPlatform, systemBindingsMap, q, binding); } if (doppleGanger == null) { log.log(IStatus.ERROR, "doppleganger is null"); } } return doppleGanger; } /** * To remove a system binding, in Eclipse, one creates a user binding with * the same scheme/platform/key sequence, and a null command. * * <p>This method, given a null-command, rem user binding {@code q/rem}, finds and * returns the equivalent system binding. */ private static KbaBinding findDoppleganger( String platform, Map<KbaChangeSetQualifier, KbaChangeSet> systemBindingsMap, KbaChangeSetQualifier q, EclBinding rem) { KbaChangeSetQualifier equivalentAdd = new KbaChangeSetQualifier( q.scheme, platform, q.context, Action.ADD.toString()); KbaChangeSet kbaChangeSet = systemBindingsMap.get(equivalentAdd); if (kbaChangeSet == null) { // It seems we're trying to remove a system key binding that simply does // not exist. Possibly this removing was recorded against an earlier // version of Eclipse (when the binding existed). In any case, just return // null. return null; } for (KbaBinding binding : kbaChangeSet.getBindingList()) { String keySequence = rem.getKeySequence(); if (binding.getKeySequence().equals(keySequence)) { return new KbaBinding( binding.getKeySequence(), binding.getCid(), binding.getParameters()); } } return null; } static Map<KbaChangeSetQualifier, KbaChangeSet> buildUserBindingsMap( final String currentPlatform, final Map<KbaChangeSetQualifier, KbaChangeSet> systemBindingsMap, Iterable<EclBinding> bindingList, MechanicLog log) { // Naughty eclipse's bindings are not uniquefied bindingList = Sets.newHashSet(bindingList); Map<KbaChangeSetQualifier, KbaChangeSet> result = Maps.newHashMap(); for (final EclBinding b : bindingList) { if (!b.hasCommand()) { if (!KeyboardBindingsTask.ENABLE_EXP_REM()) { continue; } KbaBinding kbaBinding = bindingToRemoveKbaBinding(currentPlatform, systemBindingsMap, b, log); if (kbaBinding == null) { // It seems the user has configured a certain (system) binding to be // removed, but no such system binding exists at the moment. I guess // it could be that the user configured it in an Eclipse instance // where there was a plugin that registered that system binding, and // this plugin is not part of this install. We'll just ignore these. continue; } KbaChangeSetQualifier q = b.with(Action.REMOVE); KbaChangeSet kbaChangeSetForQ = result.get(q); if (kbaChangeSetForQ == null) { kbaChangeSetForQ = new KbaChangeSet(q, Lists.newArrayList(kbaBinding)); } else { kbaChangeSetForQ = add(kbaChangeSetForQ, kbaBinding); } result.put(q, kbaChangeSetForQ); } else { insertIntoMapAnAddBinding(result, b); } } result = purgeCycles(result); return ImmutableMap.copyOf(result); } /** * Take a deep breath: let's say the user removes a system binding; then the * user creates the exact same binding again (instead of choosing to "restore" * the system binding). What Eclipse does behind the scenes (yuck!) is to * create this aforementioned user binding as well as a user binding to remove * the system binding. * * <p>Spouting those two to the kbd file would cause a cycle -- the mechanic * will prompt the user to apply one and the other in alternate passes (if * the user chooses to keep applying the fix). * * <p>Note that this is <em>not</em> the only way to achieve a cycle, but it * is the only way we'll generate a single .kbd with a cycle in it. A cycle * can also happen with, say, two .kbd files. But this is the most likely way * for the user to make this sort of mistake. * * <p>What this method does is to return a modified version of the given * {@code source} -- it finds these cycles (where a removed system binding is * also present as a user binding) and removes them from the returned result. */ static Map<KbaChangeSetQualifier, KbaChangeSet> purgeCycles( final Map<KbaChangeSetQualifier, KbaChangeSet> source) { final Map<KbaChangeSetQualifier, KbaChangeSet> result = Maps.newHashMap(); for (final KbaChangeSetQualifier q : source.keySet()) { final KbaChangeSet sourceKbaChangeSet = source.get(q); // For each REMOVE sections... if (q.getAction() == Action.REMOVE) { final KbaChangeSetQualifier doppleganger = new KbaChangeSetQualifier(q.scheme, q.platform, q.context, Action.ADD); // ... that have a corresponding ADD section (i.e. same scheme/platform/context), // referred to here as doppleganger, ... if (source.containsKey(doppleganger)) { final List<KbaBinding> toPurge = Lists.newArrayList(); // ... we iterate over the rem bindings ... for (final KbaBinding remBinding: sourceKbaChangeSet.getBindingList()) { // ... and try to find an exact match in the doppleganger ... if (source.get(doppleganger).getBindingList().contains(remBinding)) { // ... if we find one, we mark the original rem binding as a binding // to be removed. toPurge.add(remBinding); } } // ... finally, after iterating over all the rem bindings, if the // list of bindings to purge is not empty ... if (!toPurge.isEmpty()) { final List<KbaBinding> temp = Lists.newArrayList(); temp.addAll(sourceKbaChangeSet.getBindingList()); temp.removeAll(toPurge); // ... we create a new KbaChangeSet that is equal to the original one, // except with the "toPurge" bindinging removed ... KbaChangeSet replacementChangeSet = new KbaChangeSet(q, temp); result.put(q, replacementChangeSet); // ... and we restart the master loop ... continue; } } } // ... under *all* other circumstances (if q is not a REM, if no doppleganger // found, or if no matching add/rem bindings exist), we add the original // KbaChangeSet to the result. If no cycles exist (which should be most // of the time, this statement will be reached for every iteration in the // loop. result.put(q, sourceKbaChangeSet); } return result; } static Map<KbaChangeSetQualifier, KbaChangeSet> buildSystemBindingsMap( Iterable<EclBinding> bindingList) { // Naughty eclipse's bindings are not uniquefied bindingList = Sets.newHashSet(bindingList); Map<KbaChangeSetQualifier, KbaChangeSet> result = Maps.newHashMap(); for (final EclBinding b : bindingList) { insertIntoMapAnAddBinding(result, b); } return ImmutableMap.copyOf(result); } private static void insertIntoMapAnAddBinding( Map<KbaChangeSetQualifier, KbaChangeSet> result, final EclBinding b) { KbaChangeSetQualifier q = b.with(Action.ADD); KbaBinding kbaBinding = b.toKbaBinding(); KbaChangeSet kbaChangeSetForQ = result.get(q); if (kbaChangeSetForQ == null) { kbaChangeSetForQ = new KbaChangeSet(q, Lists.newArrayList(kbaBinding)); } else { kbaChangeSetForQ = add(kbaChangeSetForQ, kbaBinding); } result.put(q, kbaChangeSetForQ); } private static KbaChangeSet add(KbaChangeSet orig, KbaBinding toAddToOrig) { KbaBindingList bindingList = new KbaBindingList(concat(orig.getBindingList(), toAddToOrig)); KbaChangeSet result = new KbaChangeSet(orig.getSchemeId(), orig.getPlatform(), orig.getContextId(), orig.getActionLabel(), bindingList); return result; } private static <T> Iterable<T> concat(ImmutableList<T> list, T... toConcat) { List<T> result = Lists.newArrayList(list); for (T t : toConcat) { // Naughtly Eclipse // if (!result.contains(t)) { result.add(t); // } } return result; } // private static <T> Iterable<T> concat(Iterable<T> iterable, T... toConcat) { // return Iterables.concat(iterable, Lists.newArrayList(toConcat)); // } }