CodToys - EP01:緣起
自己寫文章時習慣在文字中穿插顏文字。( •̀ ω •́ )✧
最近較常寫文章分享,輸入顏文字的次數也越來越多了。
雖然 Windows 可以按下「Window + .」也可以快速選擇顏文字
但是身為資深顏文字愛好者,一定有自己的顏文字庫啊!
於是萌生了自己做一個鍵盤快捷鍵小工具的念頭,此專案就這麼誕生了。ԅ(´∀` ԅ)
讓我們搶先看一下成果!ˋ( ° ▽、° )
路人:「聽說現在年輕一輩沒人在用顏文字的捏。('◉◞⊖◟◉` )」
鱈魚:「我就老,我就是要用。ᕕ( ゚ ∀。)ᕗ 」
背景
開始前來簡單了解一下相關背景。
PowerToys
PowerToys 是微軟釋出的開源專案,提供了一系列實用功能。例如:顏色選擇器、找不到命令等等。
其中 PowerToys Run 工具是我們的主要靈感來源,本專案
抄襲借鑑其設計。Electron
Electron 是一個可以用 HTML、CSS、JavaScript 建立跨平台桌面應用程式的框架。
此框架由 GitHub 開發,有許多知名應用程式也用 Electron 開發,例如:VS Code、Slack、Discord 等等。
特點
目前主要功能規劃與特點如下:
鍵盤優先
呼叫工具、選擇功能皆使用鍵盤操作,盡可能在不中斷輸入的情況下快速使用
關鍵字搜尋
根據輸入的關鍵字過濾,提供候選功能
功能:顏文字資料庫
從 Notion Database 取得顏文字資料,選擇後複製至剪貼簿
介面則模仿 PowerToys Run 的啟動器,按下 Ctrl + Space
就會跳出類似下圖輸入框。
讓我們開始吧!( •̀ ω •́ )✧
開發
專案基於以下技術或套件開發:
在現有 Vue 專案中加入 Electron
雖然官方推薦使用 Electron Forge 建立專案,不過我已經有一個設定完成的 Vue 模板,所以這次使用 vite-plugin-electron
來整合 Electron。
步驟沒什麼特別的部分,基本上完全依照文件操作。
一陣猛如虎的操作後,我們得到了初始環境!ˋ( ° ▽、° )
開啟視窗
第一步當然是先做一個簡單的輸入框。
src\pages\index.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
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
看看效果吧!
輸入框出現惹!(/≧▽≦)/
綁定全域快捷鍵
接著是最重要的一步,要綁定全域快捷鍵,當按下 Ctrl + Space
時顯示視窗。
這裡需要使用 globalShortcut
模組。
此模組可以註冊全域快捷鍵,即使視窗沒有聚焦,依然可以執行已註冊的按鍵事件。
一開始先保持簡單,按鍵組合先寫死 Ctrl + Space
,未來有需要再加入設定功能。
electron\main.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
。
可以看到視窗出現與隱藏了!✧⁑。٩(ˊᗜˋ*)و✧⁕。
現在讓我們加上一些小細節:
- 視窗失去焦點時自動隱藏視窗
- 視窗會出現在滑鼠所在螢幕中往上 1/3 處
- 視窗隱藏後,會自動回復焦點到原本的視窗,這樣才可以繼續輸入
electron\main.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()
})
// ...
})
// ...
可以注意到開關輸入框,並不會影響原本的輸入焦點。
看起來是不是很有模有樣啊!੭ ˙ᗜ˙ )੭
總結 🐟
以上程式碼可以在此取得
- 使用 Electron 開發桌面應用程式
globalShortcut
可以綁定全域快捷鍵
下一章讓我們來實作候選功能的部分吧。♪( ◜ω◝و(و
TIP
因為篇幅問題,某些部分簡單帶過。
若想看到更詳細的解釋,請不吝留言或寫信給我喔!(*´∀`)~♥