addShortcuts.js

import { getStr, isApple } from 'src/kernel/kernel'

// délai min entre deux frappes clavier exécutées (attention à rester supérieur
// à une vitesse de répétition de clavier un peu faible)
const minDelay = 500

const isCtrl = (event) => {
  if (isApple) return event.metaKey && !event.shiftKey && !event.altKey && !event.ctrlKey
  return event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey
}
const isCtrlShift = (event) => {
  if (isApple) return event.metaKey && event.shiftKey && !event.altKey && !event.ctrlKey
  return event.ctrlKey && event.shiftKey && !event.altKey && !event.metaKey
}
const isCtrlAlt = (event) => {
  if (isApple) return event.metaKey && !event.shiftKey && event.altKey && !event.ctrlKey
  return event.ctrlKey && !event.shiftKey && event.altKey && !event.metaKey
}
const isCtrlShiftAlt = (event) => {
  if (isApple) return event.metaKey && event.shiftKey && event.altKey && !event.ctrlKey
  return event.ctrlKey && event.shiftKey && event.altKey && !event.metaKey
}
const isNoneModifier = (event) => !event.ctrlKey && !event.shiftKey && !event.altKey && !event.metaKey

/**
 * @type {{c: {ctrlOnly: boolean, cb: shortcuts.c.cb, desc: string}}}
 */
/**
 * @callback isModOk
 * @param {KeyboardEvent}
 * @return {boolean}
 */
/**
 * @callback shortcutCb
 * @param {MtgApp} app
 * @return void
 */
/**
 * @typedef MtgShortcut
 * @property {shortcutCb} cb La callback qui fera l'action
 * @property {string} desc La clé à passer à getStr pour récupérer la description
 * @property {isModOk} isModOk La fct qui vérifie que les bonnes touches ctrl|shift|alt|meta sont là
 */
/**
 * La liste des raccourcis clavier gérés par mtgApp
 * @type {Object<string, MtgShortcut|MtgShortcut[]>}
 */
const shortcuts = {
  c: [
    // Ctrl + c pour copier la figure en png dans le presse-papiers
    {
      isModOk: isCtrlShift,
      cb: (app) => {
        app.copyFig()
      },
      desc: 'Copy'
    },
    // shift + ctrl + c pour copier avec unité
    {
      isModOk: isCtrlShiftAlt,
      cb: (app) => {
      // @todo gérer ici le cas de la copie en préservant l'unité
        app.outilCopyWithUnity.select()
      },
      // @todo ajouter une autre string en 3 langues pour ce cas
      desc: 'CopyWithUnity'
    },
    // ctrl+alt pour copier l'url pérenne
    {
      isModOk: isCtrlAlt,
      cb: (app) => app.outilPermanentUrl.select(),
      desc: 'PermanentUrl'
    }
  ],
  // F1 pour lancer l'aide dans le navigateur
  f1: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilHelp.select()
    },
    desc: 'Help'
  },
  // F2 pour l'outil précédent
  f2: {
    isModOk: isNoneModifier,
    cb: (app) => app.activeOutilPrec(),
    desc: 'PreviousTool'
  },
  // F10 pour réafficher la dernière indication fugitive
  f10: {
    isModOk: isNoneModifier,
    cb: (app) => app.lastIndication(),
    desc: 'LastInd'
  },
  // Ctrl + o pour ouvrir une figure
  o: {
    isModOk: isCtrl,
    cb: (app) => app.outilOpen.select(),
    desc: 'Open'
  },
  // ctrl + s pour sauvegarder
  s: {
    isModOk: isCtrl,
    cb: (app) => app.outilSave.select(),
    desc: 'Save'
  },
  z: [
    {
      isModOk: isCtrl,
      cb: (app) => {
        if (app.gestionnaire.annulationPossible()) {
          app.gestionnaire.annuleAction()
          app.activeOutilCapt()
          app.nameEditor.montre(false)
        }
      },
      desc: 'Annuler'
    }, {
      isModOk: isCtrlShift,
      cb: (app) => {
        if (app.gestionnaire.refairePossible()) {
          app.gestionnaire.refaitAction()
          app.activeOutilCapt()
        }
      },
      desc: 'Refaire'
    }
  ],
  // F3 pour lancer l'outil de modification d'objet numérique
  f3: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilModifObjNum.select()
    },
    desc: 'ModifObjNum'
  },
  // F4 pour lancer l'outil de nommage
  f4: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilNommer.select()
    },
    desc: 'Nommer'
  },
  // F1 pour lancer l'aide dans le navigateur
  f6: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilGomme.select()
    },
    desc: 'Gomme'
  },
  // F7 pour lancer l'outil d'exécution de macro
  f7: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilExecutionMacro.select()
    },
    desc: 'Rideau'
  },
  // F8 pour lancer l'outil rideau
  f8: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilRideau.select()
    },
    desc: 'ExecutionMacro'
  },
  // F9 pour lancer le protocole
  f9: {
    isModOk: isNoneModifier,
    cb: (app) => {
      app.outilProtocole.select()
    },
    desc: 'Protocole'
  },
  delete: {
    isModOk: isCtrl,
    cb: (app) => {
      app.outilSup.select()
    },
    desc: 'Sup'
  },
  p: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('PtLib'),
    desc: 'PtLib'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('PtLie'),
    desc: 'PtLie'
  },
  {
    isModOk: isCtrlAlt,
    cb: (app) => app.selectTool('PtParCoord'),
    desc: 'PtParCoord'
  }
  ],
  b: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('CerOA'),
    desc: 'CerOA'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('CerOR'),
    desc: 'CerOR'
  }
  ],
  d: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('DtAB'),
    desc: 'DtAB'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('Seg'),
    desc: 'Seg'
  },
  {
    isModOk: isCtrlAlt,
    cb: (app) => app.selectTool('DemiDt'),
    desc: 'DemiDt'
  }
  ],
  e: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Calcul'),
    desc: 'Calcul'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('CalculComp'),
    desc: 'CalculComp'
  }
  ],
  f: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Fonc'),
    desc: 'Fonc'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('FoncComp'),
    desc: 'FoncComp'
  }
  ],
  g: {
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Trans'),
    desc: 'Trans'
  },
  j: {
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Curseur'),
    desc: 'Curseur'
  },
  k: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Polygone'),
    desc: 'Polygone'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('Surface'),
    desc: 'Surface'
  }
  ],
  l: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('MesLong'),
    desc: 'MesLong'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('MesAngNor'),
    desc: 'MesAngNor'
  }
  ],
  m: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('MarqueSeg'),
    desc: 'MarqueSeg'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('MarqueAng'),
    desc: 'MarqueAng'
  }
  ],
  n: {
    isModOk: isCtrlAlt,
    cb: (app) => app.selectTool('New'),
    desc: 'New'
  },
  h: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('Commentaire'),
    desc: 'Commentaire'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('Latex'),
    desc: 'Latex'
  }
  ],
  u: [{
    isModOk: isCtrl,
    cb: (app) => app.selectTool('AffichageValeur'),
    desc: 'AffichageValeur'
  },
  {
    isModOk: isCtrlShift,
    cb: (app) => app.selectTool('AffichageValeurLiePt'),
    desc: 'AffichageValeurLiePt'
  }
  ]
}

const getModifiers = (shortcut) => {
  switch (shortcut.isModOk) {
    case isCtrl: return isApple ? ['cmd'] : ['ctrl']
    case isCtrlShift: return isApple ? ['cmd', 'shift'] : ['ctrl', 'shift']
    case isCtrlAlt: return isApple ? ['cmd', 'alt'] : ['ctrl', 'alt']
    case isCtrlShiftAlt: return isApple ? ['cmd', 'shift', 'alt'] : ['ctrl', 'shift', 'alt']
  }
  return []
}

/**
 * @typedef ShortcutDesc
 * @property {string} key La touche (en minuscule)
 * @property {string[]} modifiers La liste des modifiers qui valident le raccourci (parmi 'ctrl|cmd', 'shift', 'alt', avec cmd pour mac et ctrl pour les autres)
 * @property {string} desc La description du raccourci
 */
/**
 * Retourne la liste des shortcuts pour l'afficher à l'utilisateur
 * @returns {Array}
 */
export function getShortcuts () {
  const list = []
  const add = (key, shortcut) => {
    list.push({
      key,
      modifiers: getModifiers(shortcut),
      desc: getStr(shortcut.desc),
    })
  }
  for (const [key, shortcut] of Object.entries(shortcuts)) {
    if (Array.isArray(shortcut)) {
      for (const s of shortcut) add(key, s)
    } else {
      add(key, shortcut)
    }
  }
  return list
}

const listeners = []

/**
 * Ajoute les raccourcis clavier à l'appli
 * @param mtgApp
 */
function addShortcuts (mtgApp) {
  if (listeners.length > 0) {
    // faut les virer d'abord
    for (const listener of listeners) {
      window.removeEventListener('keydown', listener)
    }
  }
  let lastKeypress = 0
  // notre listener
  const listener = (event) => {
    if (!event.key) return // ça arrive quand on choisi une proposition d'autocomplétion dans un input texte…
    // faut passer en lower sinon avec shift on récupère des majuscules
    const key = event.key.toLowerCase()
    let shortcut = shortcuts[key]
    if (!shortcut) return
    if (Array.isArray(shortcut)) {
      // on prend le premier qui match le modifier
      shortcut = shortcut.find(s => s.isModOk(event))
      if (!shortcut) return
    } else {
      if (!shortcut.isModOk(event)) return
    }
    // on a un shortcut valide, on exécute si c'est pas trop rapide après le précédent
    const delay = Date.now() - lastKeypress
    // on met à jour même si on ignore ensuite, pour qu'une touche laissée enfoncée ne  déclenche pas une action toutes les minDelay ms
    lastKeypress = Date.now()
    if (delay < minDelay) return
    // Si event.target est un input pu un textearea c'est qu'on est dans une boîte de dialoge
    // et on ne traite pas le raccourci
    const target = event.target
    // Si on est en train d'éditer un nom à la volée, on traite les raccourcis claviers comme Ctrl + z ou Ctrl + Shift + z
    // en appelant la callback.
    // Mais si l'événement clavier vient d'un input ou d'un textarea c'est que le raccourci clavier a été
    // généré par un éditeur d'un boîte de dialogue et on n'appelle pas la callback
    if ((target.classList.contains('mtgnameinput')) || !['input', 'textarea'].includes(target.tagName.toLowerCase())) {
      shortcut.cb(mtgApp)
      event.preventDefault()
    }
  }
  // que l'on ajoute (sur window car sur le conteneur ça marche très moyen, et au keydown sinon le navigateur a pris le dessus sur ctrl+c, F1, etc.)
  window.addEventListener('keydown', listener)
  listeners.push(listener)
  // pour débug
  // console.debug('liste des raccourcis clavier : ', getShortcuts())
}

export default addShortcuts