Skip to content

CodToys - EP01:緣起

快樂堆積木的魚

自己寫文章時習慣在文字中穿插顏文字。( •̀ ω •́ )✧

最近較常寫文章分享,輸入顏文字的次數也越來越多了。

雖然 Windows 可以按下「Window + .」也可以快速選擇顏文字

但是身為資深顏文字愛好者,一定有自己的顏文字庫啊!

於是萌生了自己做一個鍵盤快捷鍵小工具的念頭,此專案就這麼誕生了。ԅ(´∀` ԅ)

讓我們搶先看一下成果!ˋ( ° ▽、° )

shortcut-key-selection-options


路人:「聽說現在年輕一輩沒人在用顏文字的捏。('◉◞⊖◟◉` )」

鱈魚:「我就老,我就是要用。ᕕ( ゚ ∀。)ᕗ 」

背景

開始前來簡單了解一下相關背景。

  • PowerToys

    PowerToys 是微軟釋出的開源專案,提供了一系列實用功能。例如:顏色選擇器找不到命令等等。

    其中 PowerToys Run 工具是我們的主要靈感來源,本專案抄襲借鑑其設計。

  • Electron

    Electron 是一個可以用 HTML、CSS、JavaScript 建立跨平台桌面應用程式的框架。

    此框架由 GitHub 開發,有許多知名應用程式也用 Electron 開發,例如:VS CodeSlackDiscord 等等。

特點

目前主要功能規劃與特點如下:

  • 鍵盤優先

    呼叫工具、選擇功能皆使用鍵盤操作,盡可能在不中斷輸入的情況下快速使用

  • 關鍵字搜尋

    根據輸入的關鍵字過濾,提供候選功能

  • 功能:顏文字資料庫

    從 Notion Database 取得顏文字資料,選擇後複製至剪貼簿

介面則模仿 PowerToys Run 的啟動器,按下 Ctrl + Space 就會跳出類似下圖輸入框。

power-toys-run

讓我們開始吧!( •̀ ω •́ )✧

開發

專案基於以下技術或套件開發:

在現有 Vue 專案中加入 Electron

雖然官方推薦使用 Electron Forge 建立專案,不過我已經有一個設定完成的 Vue 模板,所以這次使用 vite-plugin-electron 來整合 Electron。

步驟沒什麼特別的部分,基本上完全依照文件操作。

一陣猛如虎的操作後,我們得到了初始環境ˋ( ° ▽、° )

開啟視窗

第一步當然是先做一個簡單的輸入框。

src\pages\index.vue

vue
<template>
  <div class="flex-col">
    <q-input
      v-model="inputValue"
      placeholder="要來點甚麼?...(´,,•ω•,,)"
      autofocus
      outlined
      square
    >
      <template #prepend>
        <q-icon name="search" />
      </template>
    </q-input>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const inputValue = ref('')
</script>

接著讓我們啟動 Electron 魔法,把網頁變成應用程式視窗!◝( •ω• )◟

electron\main.ts

ts
import process from 'node:process'
import {
  app,
  BrowserWindow,
} from 'electron'

async function createInputWindow() {
  const newWindow = new BrowserWindow({
    width: 400,
    height: 100,
    backgroundColor: '#fff',
    frame: false,
    resizable: false,
  })
  // 隱藏預設系統選單
  newWindow.setMenu(null)

  if (process.env.VITE_DEV_SERVER_URL) {
    await newWindow.loadURL(process.env.VITE_DEV_SERVER_URL)
  }
  else {
    await newWindow.loadFile('dist/index.html')
  }

  return newWindow
}

let mainWindow: BrowserWindow | undefined

app.whenReady().then(async () => {
  mainWindow = await createInputWindow()
})

// 當所有視窗關閉時退出應用程式
app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})

BrowserWindow 表示一個視窗,就像是開啟一個網頁一般。

app.whenReady 則表示在應用程式準備好時執行。

現在讓我們執行 npm run dev 看看效果吧!

first-window

輸入框出現惹!(/≧▽≦)/

綁定全域快捷鍵

接著是最重要的一步,要綁定全域快捷鍵,當按下 Ctrl + Space 時顯示視窗。

這裡需要使用 globalShortcut 模組

此模組可以註冊全域快捷鍵,即使視窗沒有聚焦,依然可以執行已註冊的按鍵事件。

一開始先保持簡單,按鍵組合先寫死 Ctrl + Space,未來有需要再加入設定功能。

electron\main.ts

ts
// ...

async function createInputWindow() {
  const display = screen.getPrimaryDisplay()

  const newWindow = new BrowserWindow({
    // 設為螢幕寬度的三分之一
    width: display.bounds.width / 3,
    height: 100,
    // 一開始隱藏視窗
    show: false,
    // ...
  })

  // ...
}

let mainWindow: BrowserWindow | undefined

app.whenReady().then(async () => {
  mainWindow = await createInputWindow()

  const ret = globalShortcut.register('Ctrl+Space', () => {
    if (!mainWindow)
      return

    if (mainWindow?.isVisible()) {
      mainWindow.hide()
      return
    }

    mainWindow.show()
  })

  if (!ret) {
    console.error('registration failed')
  }
})

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }

  globalShortcut.unregisterAll()
})

現在讓我們按按看 Ctrl + Space

shortcut-open-window

可以看到視窗出現與隱藏了!✧⁑。٩(ˊᗜˋ*)و✧⁕。

現在讓我們加上一些小細節:

  • 視窗失去焦點時自動隱藏視窗
  • 視窗會出現在滑鼠所在螢幕中往上 1/3 處
  • 視窗隱藏後,會自動回復焦點到原本的視窗,這樣才可以繼續輸入

electron\main.ts

ts
// ...

async function createInputWindow() {
  // ...
  newWindow.setMenu(null)

  // 失去焦點時自動隱藏視窗
  newWindow.on('blur', () => {
    newWindow.hide()
  })

  // ...
}

app.whenReady().then(async () => {
  mainWindow = await createInputWindow()

  const ret = globalShortcut.register('CmdOrCtrl+Space', () => {
    if (!mainWindow)
      return

    if (mainWindow?.isVisible()) {
      // focusable 設為 false,才可以讓焦點回到原本位置。例如正在輸入的編輯器
      mainWindow.setFocusable(false)
      mainWindow.hide()

      return
    }

    // 取得滑鼠所在螢幕
    const cursorPoint = screen.getCursorScreenPoint()
    const display = screen.getDisplayNearestPoint(cursorPoint)

    // 設定滑鼠位置之視窗中間往上 1/3 的位置
    const [width, height] = mainWindow.getSize()
    mainWindow?.setPosition(
      Math.floor(display.bounds.x + display.bounds.width / 2 - width / 2),
      Math.floor(display.bounds.y + display.bounds.height / 3 - height / 2),
    )

    mainWindow.setFocusable(true)
    mainWindow.show()
  })

  // ...
})

// ...

auto-refocus

可以注意到開關輸入框,並不會影響原本的輸入焦點。

看起來是不是很有模有樣啊!੭ ˙ᗜ˙ )੭

總結 🐟

以上程式碼可以在此取得

  • 使用 Electron 開發桌面應用程式
  • globalShortcut 可以綁定全域快捷鍵

下一章讓我們來實作候選功能的部分吧。♪( ◜ω◝و(و

TIP

因為篇幅問題,某些部分簡單帶過。

若想看到更詳細的解釋,請不吝留言或寫信給我喔!(*´∀`)~♥