production-taskbar-client / src / main / utils / registerWinAppBar.js
registerWinAppBar.js
Raw
/* eslint-disable camelcase */
import os from "os";
import ffi from "@inigolabs/ffi-napi";
import ref from "@inigolabs/ref-napi";
import StructType from "ref-struct-di";
import { app, screen, ipcMain } from "electron";
import _ from "lodash";

import setWindowTransparency from "./winapi/setWindowLong";
import { findExactWindow } from "./winapi/getWindowData";

const Struct = StructType(ref);
const is64bit = os.arch() === "x64";
const isDev = process.env.NODE_ENV === "development";
const isTransparentWinbar = process.env.TRANSPARENT_WINBAR;
let metricLock = false;

const ABM_NEW = 0;
const ABM_REMOVE = 1;
const ABM_QUERYPOS = 2;
const ABM_SETPOS = 3;
const ABM_GETSTATE = 4;
const ABM_GETTASKBARPOS = 5;
const ABM_SETSTATE = 10;

const ABS_AUTOHIDE = 1;
const ABS_ALWAYSONTOP = 2;

const ABEdgeLeft = 0;
const ABEdgeTop = 1;
const ABEdgeRight = 2;
const ABEdgeBottom = 3;
const ABEdgeDev = 4;

const RECT_Struct = Struct({
  left: ref.types.long, // LONG: 32-bit signed integer, in twos-complement format (–2147483648 - 2147483647 decimal).
  top: ref.types.long, // LONG
  right: ref.types.long, // LONG
  bottom: ref.types.long, // LONG
});

// Total size must be 48 for x64 and 36 for x86
const APPBARDATA_Struct = Struct({
  cbSize: ref.types.uint32, // DWORD: 32-bit unsigned integer (range: 0 through 4294967295 decimal)
  hWnd: is64bit ? ref.types.longlong : ref.types.long, // HWND: unsigned integers
  uCallbackMessage: ref.types.uint32, // UINT: 32-bit unsigned integer (range: 0 through 4294967295 decimal)
  uEdge: ref.types.uint32, // UINT
  rc: RECT_Struct, // RECT
  lParam: is64bit ? ref.types.uint64 : ref.types.uint32, // LPARAM
});

const appbar = new APPBARDATA_Struct();
appbar.cbSize = APPBARDATA_Struct.size;

export const shell32 = ffi.Library("shell32.dll", {
  SHAppBarMessage: ["bool", ["int", "pointer"]],
});

export const user32 = ffi.Library("user32.dll", {
  RegisterWindowMessageA: ["long", ["string"]],
  GetWindowRect: ["bool", ["long", "pointer"]],
  MoveWindow: ["bool", ["long", "int", "int", "int", "int", "bool"]],
});

const hwndBufferToInt = (hbuf) => {
  if (os.endianness() === "LE") return hbuf.readInt32LE();
  return hbuf.readInt32BE();
};

const showWinAppbar = () => {
  const data = new APPBARDATA_Struct();
  data.cbSize = APPBARDATA_Struct.size;
  data.lParam = ABS_ALWAYSONTOP;
  shell32.SHAppBarMessage(ABM_SETSTATE, data.ref());

  if (isTransparentWinbar === "true") {
    const hwnd = findExactWindow("Shell_TrayWnd", null);
    setWindowTransparency(hwnd, 255);
  }
};

const hideWinAppbar = () => {
  const data = new APPBARDATA_Struct();
  data.cbSize = APPBARDATA_Struct.size;
  data.lParam = ABS_AUTOHIDE;
  shell32.SHAppBarMessage(ABM_SETSTATE, data.ref());

  if (isTransparentWinbar === "true") {
    const hwnd = findExactWindow("Shell_TrayWnd", null);
    setWindowTransparency(hwnd, 0);
  }
};

const isHiddenWinAppbar = () => {
  const data = new APPBARDATA_Struct();
  data.cbSize = APPBARDATA_Struct.size;
  return shell32.SHAppBarMessage(ABM_GETSTATE, data.ref());
};

// Rehide winappbar used to reset appbar area when app closed without call [deregisterAppBar]
// on kill/crash to prevent incorrect positioning on start.
const rehideWinAppbar = () => {
  const isHidden = isHiddenWinAppbar();
  if (isHidden) {
    showWinAppbar();
    hideWinAppbar();
  } else {
    hideWinAppbar();
  }
};

const setWinAppbar = (option = "hide") => {
  const isHidden = isHiddenWinAppbar();
  if (option === "hide" && !isHidden) {
    hideWinAppbar();
  }
  if (option === "show" && isHidden) {
    showWinAppbar();
  }
};

const setPos = (window, edge = ABEdgeBottom) => {
  const hwnd = hwndBufferToInt(window.getNativeWindowHandle());

  let { width: screenW, height: screenH } = screen.getPrimaryDisplay().size;
  const dpi = screen.getPrimaryDisplay().scaleFactor;

  let height = 80 * dpi;
  if (screenH <= 1024) {
    height = 70 * dpi;
  }
  let width = screenW;

  appbar.hWnd = hwnd;
  appbar.uEdge = edge;

  const winTaskbar = new APPBARDATA_Struct();
  winTaskbar.cbSize = APPBARDATA_Struct.size;
  shell32.SHAppBarMessage(ABM_GETTASKBARPOS, winTaskbar.ref());
  const isHidden = shell32.SHAppBarMessage(ABM_GETSTATE, winTaskbar.ref());
  const winTaskbarHeight = isHidden
    ? 0
    : winTaskbar.rc.bottom - winTaskbar.rc.top;

  screenW *= dpi;
  screenH *= dpi;
  width *= dpi;

  if (edge === ABEdgeLeft || edge === ABEdgeRight) {
    appbar.rc.top = 0;
    appbar.rc.bottom = screenH;
    if (edge === ABEdgeLeft) {
      appbar.rc.left = 0;
      appbar.rc.right = width;
    } else {
      appbar.rc.left = screenW - width;
      appbar.rc.right = screenW;
    }
  } else {
    appbar.rc.left = 0;
    appbar.rc.right = screenW;
    if (edge === ABEdgeTop) {
      appbar.rc.top = 0;
      appbar.rc.bottom = height;
    } else {
      appbar.rc.top = screenH - height;
      appbar.rc.bottom = screenH;
    }
  }

  if (edge === ABEdgeDev) {
    appbar.rc.left = 0;
    appbar.rc.right = screenW;
    appbar.rc.top = screenH - height - winTaskbarHeight - 2;
    appbar.rc.bottom = screenH - winTaskbarHeight - 2;
  } else {
    appbar.hWnd = hwndBufferToInt(window.getNativeWindowHandle());
    appbar.uCallbackMessage = user32.RegisterWindowMessageA("CustomTaskbarId");
    shell32.SHAppBarMessage(ABM_NEW, appbar.ref());
    shell32.SHAppBarMessage(ABM_QUERYPOS, appbar.ref());
    shell32.SHAppBarMessage(ABM_SETPOS, appbar.ref());
  }

  // This is done async, because windows will send a resize after a new appbar is added.
  setTimeout(() => {
    user32.MoveWindow(
      hwnd,
      appbar.rc.left,
      appbar.rc.top,
      appbar.rc.right - appbar.rc.left,
      appbar.rc.bottom - appbar.rc.top + winTaskbarHeight,
      false
    );
    window.show();
  }, 1000);
};

export function deregisterAppBar() {
  shell32.SHAppBarMessage(ABM_REMOVE, appbar.ref());
  setWinAppbar("show");
}

export function registerAppBar(window) {
  ipcMain.on("dev-quit", () => app.quit());
  const isDevPosition = process.env.DEV_POSITION;

  if (isDev && isDevPosition === "true") {
    setPos(window, ABEdgeDev);
    return;
  }

  rehideWinAppbar();

  // Delay need after rehide, see [rehideWinAppbar] comments
  setTimeout(() => {
    setWinAppbar("hide");
    setPos(window);
  }, 1000);

  screen.on("display-metrics-changed", (_event, _display, changedMetrics) => {
    // metric lock prevent infinity workArea change loop on win7
    if (
      !window.isDestroyed() &&
      !metricLock &&
      (_.includes(changedMetrics, "bounds") ||
        _.includes(changedMetrics, "workArea"))
    ) {
      metricLock = true;
      shell32.SHAppBarMessage(ABM_REMOVE, appbar.ref());
      setPos(window);
      setTimeout(() => {
        metricLock = false;
      }, 1000);
    }
  });

  app.on("quit", () => deregisterAppBar()); // on [dev-quit]
  app.on("before-quit", () => deregisterAppBar()); // on electron-updater quit
}