Skip to content

Instantly share code, notes, and snippets.

@LeeDDHH
Last active July 19, 2021 11:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LeeDDHH/972b9eca9b278b6f245abb9482851f94 to your computer and use it in GitHub Desktop.
Save LeeDDHH/972b9eca9b278b6f245abb9482851f94 to your computer and use it in GitHub Desktop.
Electronでグローバルショートカット機能を使うための例

type定義

declare type HotKeys = string[] | [];

declare type GlobalShortCutRegisterResultItem = {
  index:
    | 'canNotRegistered'
    | 'alreadyRegisteredButUse'
    | 'registerSuccess'
    | 'registerable';
  text: string;
};

declare type CanNotRegistered = {
  index: 'canNotRegistered';
  text: string;
};

declare type AlreadyRegisteredButUse = {
  index: 'alreadyRegisteredButUse';
  text: string;
};

declare type RegisterSuccess = {
  index: 'registerSuccess';
  text: string;
};

declare type Registerable = {
  index: 'registerable';
  text: string;
};

declare interface GlobalShortCutRegisterResult {
  [key: string]: GlobalShortCutRegisterResultItem;
  canNotRegistered: CanNotRegistered;
  alreadyRegisteredButUse: AlreadyRegisteredButUse;
  registerSuccess: RegisterSuccess;
  registerable: Registerable;
}

declare type GlobalShortCutRegisterIndex =
  | 'canNotRegistered'
  | 'alreadyRegisteredButUse'
  | 'registerSuccess'
  | 'registerable';

declare type GlobalShortcut = {
  globalShortcut: string;
};

定数(Const.ts)

const globalShortCutRegisterResult: GlobalShortCutRegisterResult = {
  canNotRegistered: {
    index: 'canNotRegistered',
    text: '登録できませんでした',
  },
  alreadyRegisteredButUse: {
    index: 'alreadyRegisteredButUse',
    text: 'すでに登録されていますが、アプリを使用している間は使えます',
  },
  registerSuccess: {
    index: 'registerSuccess',
    text: '現在、登録されているショートカットキーです',
  },
  registerable: {
    index: 'registerable',
    text: 'ショートカットキーを登録してください',
  },
};

// グローバルショートカットキーの解除をしないステータスの一覧
const doNotUnregisterProcessList: GlobalShortCutRegisterIndex[] = [
  globalShortCutRegisterResult.canNotRegistered.index,
  globalShortCutRegisterResult.registerable.index,
];

// 修飾キーの一覧
const modifierKeyArray = [
  'Command',
  'Control',
  'Alt',
  'Option',
  'AltGr',
  'Shift',
  'Super',
  'Meta',
];

const globalSettingJsonName = 'global_setting.json';

const defaultMainViewToggleShortcut = 'Control+Space';

export {
  globalShortCutRegisterResult,
  doNotUnregisterProcessList,
  modifierKeyArray,
  globalSettingJsonName,
  defaultMainViewToggleShortcut
}

Util系の機能(Util.ts)

const isExistFile = (filePath: string): boolean => {
  try {
    fs.statSync(filePath);
  } catch (e) {
    if (e.code === 'ENOENT') {
      console.log('file is not exists');
    }
    return false;
  }
  return true;
};

const writeSync = async <T>(filePath: string, data: T): Promise<boolean> => {
  try {
    await fs.promises.writeFile(filePath, JSON.stringify(data, null, 2), {
      encoding: 'utf-8',
    });
  } catch (e) {
    console.log('write file failed: ' + e);
    return false;
  }
  return true;
};

const readJsonFile = async (filePath: string): Promise<any | null> => {
  let content;
  try {
    content = await fs.promises.readFile(filePath, 'utf-8');
    const result = JSON.parse(content);
    return result;
  } catch (e) {
    console.log('read file failed: ' + e);
    return null;
  }
};

export {
  isExistFile,
  writeSync,
  readJsonFile
}

グローバルショートカット機能を使うための処理

  • globalShortcut.isRegistered は、アプリケーションにグローバルショートカットの取り合いをさせないため、他のアプリケーションによってすでにグローバルショートカットが使用されている場合、 false を返す
  • なので、使用環境によっては基本的に false しか返さないケースが多発する
  • そこで、以下のコードではアプリを使っている間はグローバルショートカットキーが重複されるけど、指定したショートカットキーが使えるように処理を書いている
import path from 'path';
import * as fs from 'fs-extra';
import { app, globalShortcut } from 'electron';

import { isExistFile, writeSync, readJsonFile } from 'Util';

import {
  globalShortCutRegisterResult,
  doNotUnregisterProcessList,
  modifierKeyArray,
  globalSettingJsonName,
  defaultMainViewToggleShortcut
} from 'Const'

/*
  グローバルショートカット登録時のステータス
  canNotRegistered         登録できなかったとき
  alreadyRegisteredButUse  システムで登録されたショートカットを使う
  registerSuccess          登録に成功したとき
  registerable             空の文字列を登録しよとしたとき
*/
let _globalShortcutStatus: GlobalShortCutRegisterIndex;

// 現在登録したショートカット
let _currentGlobalShortcut = '';

// グローバルショートカットキーのデータを保存するファイルのパス
const _getGlobalSettingsFilePath: string = path.join(
  app.getPath('userData'),
  globalSettingJsonName
);

/*
  登録しようとするショートカットがすでにシステムで登録されているキーなのかを判定する
  true  すでに登録されている
  false 登録されていない
*/
const _isGlobalShortCutAlreadyRegistered = (shortcut: string): boolean => {
  return !globalShortcut.isRegistered(shortcut);
};

// ショートカットキーが空かどうかを判定する
const _isShortCutKeyIsEmpty = (shortcut: string | string[]): boolean => {
  return shortcut.length < 1;
};

// 引数のグローバルショートカットキーを登録する
const _registerGlobalShortCut = (
  shortcut: string
): GlobalShortCutRegisterIndex => {
  // グローバルショートカットキーの登録済みフラグ
  let alreadyRegistered = false;

  // 引数のショートカットキーが空の場合
  if (_isShortCutKeyIsEmpty(shortcut)) {
    // グローバルショートカット登録ステータスを「登録可能な状態」にして、ステータスを返す
    _globalShortcutStatus = globalShortCutRegisterResult.registerable.index;
    return globalShortCutRegisterResult.registerable.index;
  }

  // すでに登録されているショートカットキーの場合、グローバルショートカットキーの登録済みフラグを有効にする
  if (_isGlobalShortCutAlreadyRegistered(shortcut)) alreadyRegistered = true;

  // 登録の結果を格納する
  const ret: boolean = globalShortcut.register(shortcut, () => {
    /*

      グローバルショートカットで使いたい機能

    */
  });

  // グローバルショートカットキーの登録に失敗した場合
  if (!ret) {
    // 現在登録したショートカットを空にする
    _currentGlobalShortcut = '';
    // グローバルショートカット登録ステータスを「登録できなかった」にする
    _globalShortcutStatus = globalShortCutRegisterResult.canNotRegistered.index;
  } else {
    // グローバルショートカットキーの登録に成功した場合
    // 現在登録したショートカットを登録したショートカットにする
    _currentGlobalShortcut = shortcut;
    /*
      グローバルショートカットキーの登録済みフラグの状態を参照してグローバルショートカット登録ステータスを更新する
      - 有効の場合は、「システムで登録されたショートカットを使う」に更新する
      - 無効の場合は、「登録に成功」に更新する
    */
    _globalShortcutStatus = alreadyRegistered
      ? globalShortCutRegisterResult.alreadyRegisteredButUse.index
      : globalShortCutRegisterResult.registerSuccess.index;
  }

  // グローバルショートカット登録ステータスを返す
  return _globalShortcutStatus;
};

// 引数のショートカットをファイルに保存する
const _saveGlobalShortCut = async (shortcut: string) => {
  // 保存用のオブジェクト形式にする
  const globalShortcut = { globalShortcut: shortcut };
  try {
    // 保存する
    return writeSync<GlobalShortcut>(
      _getGlobalSettingsFilePath,
      globalShortcut
    );
  } catch (e) {
    console.log('[GlobalShortcut]' + '_saveGlobalShortCut: ' + e);
    return false;
  }
};

/*
  ショートカットのキー配列が修飾キーのみなのかを判定する
  true  すべての要素のが要素のが修飾キー
  false 修飾キー以外の要素あり
*/
const _isOnlyModifiers = (keyArray: string[]) => {
  return keyArray.every((key) => modifierKeyArray.includes(key));
};

// グローバルショートカットキーを解除する
const _unRegisterGlobalShortCut = (): void => {
  /*
    以下の条件をすべて満たしているときにグローバルショートカットキーを解除する
    - 現在のグローバルショートカット登録ステータスが「登録できなかった」、「登録可能な状態」以外
    - 現在のグローバルショートカットキーが空ではない
  */
  if (
    !doNotUnregisterProcessList.includes(_globalShortcutStatus) &&
    !_isShortCutKeyIsEmpty(_currentGlobalShortcut)
  ) {
    // グローバルショートカットキーを解除し、「登録可能な状態」にグローバルショートカット登録ステータスを更新する
    globalShortcut.unregister(_currentGlobalShortcut);
    _globalShortcutStatus = globalShortCutRegisterResult.registerable.index;
  }
};

// グローバルショートカットキーをファイルから読み込む
const _getGlobalShortcutFromFile = async (): Promise<string> => {
  // グローバルショートカットのデフォルトデータ
  let mainViewToggleShortcut = defaultMainViewToggleShortcut;

  // グローバルショートカットキーが保存されたデータが存在する場合
  if (isExistFile(_getGlobalSettingsFilePath)) {
    let data;
    try {
      data = await readJsonFile(_getGlobalSettingsFilePath);
      // 現在のグローバルショートカットキーと返り値を読み込んだデータにする
      _currentGlobalShortcut = mainViewToggleShortcut = data.globalShortcut;
    } catch (e) {
      console.log('[GlobalShortcut]' + 'getMainViewToggleShortcut: ' + e);
    }
  }

  return mainViewToggleShortcut;
};

// グローバルショートカットキーを更新する
const updateGlobalShortCut = async (
  keys: HotKeys
): Promise<GlobalShortCutRegisterIndex> => {
  // 現在のグローバルショートカットキーを解除する
  _unRegisterGlobalShortCut();

  // 引数のショートカットキーが空なのかを判定する
  if (_isShortCutKeyIsEmpty(keys)) {
    // 引数のショートカットキーが空の場合
    // ショートカットを空にしてファイルに保存する
    await _saveGlobalShortCut('');
    // 「登録可能な状態」のグローバルショートカット登録ステータスを返す
    return globalShortCutRegisterResult.registerable.index;
  }

  // ショートカットのキー配列が修飾キーのみの場合
  if (_isOnlyModifiers(keys)) {
    // グローバルショートカット登録ステータスを「登録できなかった」にする
    _globalShortcutStatus = globalShortCutRegisterResult.canNotRegistered.index;
    return _globalShortcutStatus;
  }

  // ショートカットキーの配列を「+」に連結する
  const shortcut = keys.join('+');

  // グローバルショートカットキーを登録し、
  const result = _registerGlobalShortCut(shortcut);

  // グローバルショートカットキーの登録結果が「登録できなかった」場合
  if (result === globalShortCutRegisterResult.canNotRegistered.index) {
    // ショートカットを空にしてファイルに保存する
    await _saveGlobalShortCut('');
  } else {
    // ショートカットをファイルに保存する
    await _saveGlobalShortCut(shortcut);
  }

  // グローバルショートカット登録ステータスを返す
  return result;
};

// グローバルショートカットキーの初期化をする
const initGlobalShortCut = async (): Promise<void> => {
  // グローバルショートカットキーをファイルから読み込み、グローバルショートカットキーに登録する
  const recordShortCut: string = await _getGlobalShortcutFromFile();
  const result: GlobalShortCutRegisterIndex = _registerGlobalShortCut(
    recordShortCut
  );

  console.log('【GlobalShortCut】 ' + 'initGlobalShortCut: ' + result);
};

// アプリの終了前にグローバルショートカットキーを解除する
const unSetAllGlobalShortcut = (): void => {
  globalShortcut.unregister(_currentGlobalShortcut);
  globalShortcut.unregisterAll();
};

// 現在のグローバルショートカットステータスを返す
const getCurrentGlobalShortcutStatus = (): GlobalShortCutRegisterIndex => {
  return _globalShortcutStatus;
};

const getCurrentGlobalShortcut = (): string => {
  return _currentGlobalShortcut;
};

export {
  updateGlobalShortCut,
  initGlobalShortCut,
  unSetAllGlobalShortcut,
  getCurrentGlobalShortcutStatus,
  getCurrentGlobalShortcut,
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment