File size: 6,282 Bytes
583c1c7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
/**

 * A service responsible for captruing keys within LiteGraph's canvas, and outside of it, allowing

 * nodes and other services to confidently determine what's going on.

 */
class KeyEventService extends EventTarget {
  readonly downKeys: { [key: string]: boolean } = {};

  ctrlKey = false;
  altKey = false;
  metaKey = false;
  shiftKey = false;

  private readonly isMac: boolean = !!(
    navigator.platform?.toLocaleUpperCase().startsWith("MAC") ||
    (navigator as any).userAgentData?.platform?.toLocaleUpperCase().startsWith("MAC")
  );

  constructor() {
    super();
    this.initialize();
  }

  initialize() {
    const that = this;
    // [🤮] Sometimes ComfyUI and/or LiteGraph stop propagation of key events which makes it hard
    // to determine if keys are currently pressed. To attempt to get around this, we'll hijack
    // LiteGraph's processKey to try to get better consistency.
    const processKey = LGraphCanvas.prototype.processKey;
    LGraphCanvas.prototype.processKey = function (e: KeyboardEvent) {
      if (e.type === "keydown" || e.type === "keyup") {
        that.handleKeyDownOrUp(e);
      }
      return processKey.apply(this, [...arguments] as any) as any;
    };

    // Now that ComfyUI has more non-canvas UI (like the top bar), we listen on window as well, and
    // de-dupe when we get multiple events from both window and/or LiteGraph.
    window.addEventListener("keydown", (e) => {
      that.handleKeyDownOrUp(e);
    });
    window.addEventListener("keyup", (e) => {
      that.handleKeyDownOrUp(e);
    });

    // If we get a visibilitychange, then clear the keys since we can't listen for keys up/down when
    // not visible.
    document.addEventListener("visibilitychange", (e) => {
      this.clearKeydowns();
    });

    // If we get a blur, then also clear the keys since we can't listen for keys up/down when
    // blurred. This can happen w/o a visibilitychange, like a browser alert.
    window.addEventListener("blur", (e) => {
      this.clearKeydowns();
    });
  }

  /**

   * Adds a new queue item, unless the last is the same.

   */
  handleKeyDownOrUp(e: KeyboardEvent) {
    const key = e.key.toLocaleUpperCase();
    // If we're already down, or already up, then ignore and don't fire.
    if ((e.type === 'keydown' && this.downKeys[key] === true)
        || (e.type === 'keyup' && this.downKeys[key] === undefined)) {
      return;
    }

    this.ctrlKey = !!e.ctrlKey;
    this.altKey = !!e.altKey;
    this.metaKey = !!e.metaKey;
    this.shiftKey = !!e.shiftKey;
    if (e.type === "keydown") {
      this.downKeys[key] = true;
      this.dispatchCustomEvent("keydown", { originalEvent: e });
    } else if (e.type === "keyup") {
      // See https://github.com/rgthree/rgthree-comfy/issues/238
      // A little bit of a hack, but Mac reportedly does something odd with copy/paste. ComfyUI
      // gobbles the copy event propagation, but it happens for paste too and reportedly 'Enter' which
      // I can't find a reason for in LiteGraph/comfy. So, for Mac only, whenever we lift a Command
      // (META) key, we'll also clear any other keys.
      if (key === "META" && this.isMac) {
        this.clearKeydowns();
      } else {
        delete this.downKeys[key];
        // this.debugRenderKeys();
      }
      this.dispatchCustomEvent("keyup", { originalEvent: e });
    }

  }

  private clearKeydowns() {
    this.ctrlKey = false;
    this.altKey = false;
    this.metaKey = false;
    this.shiftKey = false;
    for (const key in this.downKeys) delete this.downKeys[key];
  }

  /**

   * Wraps `dispatchEvent` for easier CustomEvent dispatching.

   */
  private dispatchCustomEvent(event: string, detail?: any) {
    if (detail != null) {
      return this.dispatchEvent(new CustomEvent(event, { detail }));
    }
    return this.dispatchEvent(new CustomEvent(event));
  }

  /**

   * Parses a shortcut string.

   *

   *   - 's' => ['S']

   *   - 'shift + c' => ['SHIFT', 'C']

   *   - 'shift + meta + @' => ['SHIFT', 'META', '@']

   *   - 'shift + + + @' => ['SHIFT', '__PLUS__', '=']

   *   - '+ + p' => ['__PLUS__', 'P']

   */
  private getKeysFromShortcut(shortcut: string | string[]) {
    let keys;
    if (typeof shortcut === "string") {
      // Rip all spaces out. Note, Comfy swallows space, so we don't have to handle it. Otherwise,
      // we would require space to be fed as "Space" or "Spacebar" instead of " ".
      shortcut = shortcut.replace(/\s/g, "");
      // Change a real "+" to something we can encode.
      shortcut = shortcut.replace(/^\+/, "__PLUS__").replace(/\+\+/, "+__PLUS__");
      keys = shortcut.split("+").map((i) => i.replace("__PLUS__", "+"));
    } else {
      keys = [...shortcut];
    }
    return keys.map((k) => k.toLocaleUpperCase());
  }

  /**

   * Checks if all keys passed in are down.

   */
  areAllKeysDown(keys: string | string[]) {
    keys = this.getKeysFromShortcut(keys);
    return keys.every((k) => {
      return this.downKeys[k];
    });
  }

  /**

   * Checks if only the keys passed in are down; optionally and additionally allowing "shift" key.

   */
  areOnlyKeysDown(keys: string | string[], alsoAllowShift = false) {
    keys = this.getKeysFromShortcut(keys);
    const allKeysDown = this.areAllKeysDown(keys);
    const downKeysLength = Object.values(this.downKeys).length;
    // All keys are down and they're the only ones.
    if (allKeysDown && keys.length === downKeysLength) {
      return true;
    }
    // Special case allowing the shift key in addition to the shortcut keys. This helps when a user
    // may had originally defined "$" as a shortcut, but needs to press "shift + $" since it's an
    // upper key character, etc.
    if (alsoAllowShift && !keys.includes("SHIFT") && keys.length === downKeysLength - 1) {
      // If we're holding down shift, have one extra key held down, and the original keys don't
      // include shift, then we're good to go.
      return allKeysDown && this.areAllKeysDown(["SHIFT"]);
    }
    return false;
  }
}

/** The KeyEventService singleton. */
export const SERVICE = new KeyEventService();