Add hotkey support for actions

This update introduces hotkey support, allowing actions and queries to be triggered via hotkeys. New fields and helper methods were added to handle hotkey parsing and execution. Additionally, modifications were made to handle key events and map them to the corresponding actions.
This commit is contained in:
qianlifeng 2024-08-02 16:13:35 +08:00
parent d85832c346
commit 889bfb2020
No known key found for this signature in database
18 changed files with 769 additions and 430 deletions

View File

@ -32,7 +32,7 @@
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, },
"dependencies": { "dependencies": {
"@wox-launcher/wox-plugin": "^0.0.78", "@wox-launcher/wox-plugin": "^0.0.79",
"dayjs": "^1.11.9", "dayjs": "^1.11.9",
"promise-deferred": "^2.0.4", "promise-deferred": "^2.0.4",
"winston": "^3.10.0", "winston": "^3.10.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@wox-launcher/wox-plugin", "name": "@wox-launcher/wox-plugin",
"version": "0.0.78", "version": "0.0.79",
"description": "All nodejs plugin for Wox should use types in this package", "description": "All nodejs plugin for Wox should use types in this package",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,246 +1,253 @@
import { MetadataCommand, PluginSettingDefinitionItem } from "./setting.js" import {MetadataCommand, PluginSettingDefinitionItem} from "./setting.js"
import { AI } from "./ai.js" import {AI} from "./ai.js"
export type MapString = { [key: string]: string } export type MapString = { [key: string]: string }
export type Platform = "windows" | "darwin" | "linux" export type Platform = "windows" | "darwin" | "linux"
export interface Plugin { export interface Plugin {
init: (ctx: Context, initParams: PluginInitParams) => Promise<void> init: (ctx: Context, initParams: PluginInitParams) => Promise<void>
query: (ctx: Context, query: Query) => Promise<Result[]> query: (ctx: Context, query: Query) => Promise<Result[]>
} }
export interface Selection { export interface Selection {
Type: "text" | "file" Type: "text" | "file"
// Only available when Type is text // Only available when Type is text
Text: string Text: string
// Only available when Type is file // Only available when Type is file
FilePaths: string[] FilePaths: string[]
} }
export interface QueryEnv { export interface QueryEnv {
/** /**
* Active window title when user query * Active window title when user query
*/ */
ActiveWindowTitle: string ActiveWindowTitle: string
} }
export interface Query { export interface Query {
/** /**
* By default, Wox will only pass input query to plugin. * By default, Wox will only pass input query to plugin.
* plugin author need to enable MetadataFeatureQuerySelection feature to handle selection query * plugin author need to enable MetadataFeatureQuerySelection feature to handle selection query
*/ */
Type: "input" | "selection" Type: "input" | "selection"
/** /**
* Raw query, this includes trigger keyword if it has * Raw query, this includes trigger keyword if it has
* We didn't recommend use this property directly. You should always use Search property. * We didn't recommend use this property directly. You should always use Search property.
* *
* NOTE: Only available when query type is input * NOTE: Only available when query type is input
*/ */
RawQuery: string RawQuery: string
/** /**
* Trigger keyword of a query. It can be empty if user is using global trigger keyword. * Trigger keyword of a query. It can be empty if user is using global trigger keyword.
* *
* NOTE: Only available when query type is input * NOTE: Only available when query type is input
*/ */
TriggerKeyword?: string TriggerKeyword?: string
/** /**
* Command part of a query. * Command part of a query.
* *
* NOTE: Only available when query type is input * NOTE: Only available when query type is input
*/ */
Command?: string Command?: string
/** /**
* Search part of a query. * Search part of a query.
* *
* NOTE: Only available when query type is input * NOTE: Only available when query type is input
*/ */
Search: string Search: string
/** /**
* User selected or drag-drop data, can be text or file or image etc * User selected or drag-drop data, can be text or file or image etc
* *
* NOTE: Only available when query type is selection * NOTE: Only available when query type is selection
*/ */
Selection: Selection Selection: Selection
/** /**
* Additional query environment data * Additional query environment data
* expose more context env data to plugin, E.g. plugin A only show result when active window title is "Chrome" * expose more context env data to plugin, E.g. plugin A only show result when active window title is "Chrome"
*/ */
Env: QueryEnv Env: QueryEnv
/** /**
* Whether current query is global query * Whether current query is global query
*/ */
IsGlobalQuery(): boolean IsGlobalQuery(): boolean
} }
export interface Result { export interface Result {
Id?: string Id?: string
Title: string Title: string
SubTitle?: string SubTitle?: string
Icon: WoxImage Icon: WoxImage
Preview?: WoxPreview Preview?: WoxPreview
Score?: number Score?: number
Group?: string Group?: string
GroupScore?: number GroupScore?: number
Tails?: ResultTail[] Tails?: ResultTail[]
ContextData?: string ContextData?: string
Actions?: ResultAction[] Actions?: ResultAction[]
// refresh result after specified interval, in milliseconds. If this value is 0, Wox will not refresh this result // refresh result after specified interval, in milliseconds. If this value is 0, Wox will not refresh this result
// interval can only divisible by 100, if not, Wox will use the nearest number which is divisible by 100 // interval can only divisible by 100, if not, Wox will use the nearest number which is divisible by 100
// E.g. if you set 123, Wox will use 200, if you set 1234, Wox will use 1300 // E.g. if you set 123, Wox will use 200, if you set 1234, Wox will use 1300
RefreshInterval?: number RefreshInterval?: number
// refresh result by calling OnRefresh function // refresh result by calling OnRefresh function
OnRefresh?: (current: RefreshableResult) => Promise<RefreshableResult> OnRefresh?: (current: RefreshableResult) => Promise<RefreshableResult>
} }
export interface ResultTail { export interface ResultTail {
Type: "text" | "image" Type: "text" | "image"
Text?: string Text?: string
Image?: WoxImage Image?: WoxImage
} }
export interface RefreshableResult { export interface RefreshableResult {
Title: string Title: string
SubTitle: string SubTitle: string
Icon: WoxImage Icon: WoxImage
Preview: WoxPreview Preview: WoxPreview
ContextData: string ContextData: string
RefreshInterval: number RefreshInterval: number
} }
export interface ResultAction { export interface ResultAction {
/** /**
* Result id, should be unique. It's optional, if you don't set it, Wox will assign a random id for you * Result id, should be unique. It's optional, if you don't set it, Wox will assign a random id for you
*/ */
Id?: string Id?: string
Name: string Name: string
Icon?: WoxImage Icon?: WoxImage
/** /**
* If true, Wox will use this action as default action. There can be only one default action in results * If true, Wox will use this action as default action. There can be only one default action in results
* This can be omitted, if you don't set it, Wox will use the first action as default action * This can be omitted, if you don't set it, Wox will use the first action as default action
*/ */
IsDefault?: boolean IsDefault?: boolean
/** /**
* If true, Wox will not hide after user select this result * If true, Wox will not hide after user select this result
*/ */
PreventHideAfterAction?: boolean PreventHideAfterAction?: boolean
Action: (actionContext: ActionContext) => Promise<void> Action: (actionContext: ActionContext) => Promise<void>
/**
* Hotkey to trigger this action. E.g. "ctrl+Shift+Space", "Ctrl+1", "Command+K"
* Case insensitive, space insensitive
*
* If IsDefault is true, Hotkey will be set to enter key by default
*/
Hotkey?: string
} }
export interface ActionContext { export interface ActionContext {
ContextData: string ContextData: string
} }
export interface PluginInitParams { export interface PluginInitParams {
API: PublicAPI API: PublicAPI
PluginDirectory: string PluginDirectory: string
} }
export interface ChangeQueryParam { export interface ChangeQueryParam {
QueryType: "input" | "selection" QueryType: "input" | "selection"
QueryText?: string QueryText?: string
QuerySelection?: Selection QuerySelection?: Selection
} }
export interface PublicAPI { export interface PublicAPI {
/** /**
* Change Wox query * Change Wox query
*/ */
ChangeQuery: (ctx: Context, query: ChangeQueryParam) => Promise<void> ChangeQuery: (ctx: Context, query: ChangeQueryParam) => Promise<void>
/** /**
* Hide Wox * Hide Wox
*/ */
HideApp: (ctx: Context) => Promise<void> HideApp: (ctx: Context) => Promise<void>
/** /**
* Show Wox * Show Wox
*/ */
ShowApp: (ctx: Context) => Promise<void> ShowApp: (ctx: Context) => Promise<void>
/** /**
* Notify message * Notify message
*/ */
Notify: (ctx: Context, title: string, description?: string) => Promise<void> Notify: (ctx: Context, title: string, description?: string) => Promise<void>
/** /**
* Write log * Write log
*/ */
Log: (ctx: Context, level: "Info" | "Error" | "Debug" | "Warning", msg: string) => Promise<void> Log: (ctx: Context, level: "Info" | "Error" | "Debug" | "Warning", msg: string) => Promise<void>
/** /**
* Get translation of current language * Get translation of current language
*/ */
GetTranslation: (ctx: Context, key: string) => Promise<string> GetTranslation: (ctx: Context, key: string) => Promise<string>
/** /**
* Get customized setting * Get customized setting
* *
* will try to get platform specific setting first, if not found, will try to get global setting * will try to get platform specific setting first, if not found, will try to get global setting
*/ */
GetSetting: (ctx: Context, key: string) => Promise<string> GetSetting: (ctx: Context, key: string) => Promise<string>
/** /**
* Save customized setting * Save customized setting
* *
* @isPlatformSpecific If true, setting will be only saved in current platform. If false, setting will be available in all platforms * @isPlatformSpecific If true, setting will be only saved in current platform. If false, setting will be available in all platforms
*/ */
SaveSetting: (ctx: Context, key: string, value: string, isPlatformSpecific: boolean) => Promise<void> SaveSetting: (ctx: Context, key: string, value: string, isPlatformSpecific: boolean) => Promise<void>
/** /**
* Register setting changed callback * Register setting changed callback
*/ */
OnSettingChanged: (ctx: Context, callback: (key: string, value: string) => void) => Promise<void> OnSettingChanged: (ctx: Context, callback: (key: string, value: string) => void) => Promise<void>
/** /**
* Get dynamic setting definition * Get dynamic setting definition
*/ */
OnGetDynamicSetting: (ctx: Context, callback: (key: string) => PluginSettingDefinitionItem) => Promise<void> OnGetDynamicSetting: (ctx: Context, callback: (key: string) => PluginSettingDefinitionItem) => Promise<void>
/** /**
* Register deep link callback * Register deep link callback
*/ */
OnDeepLink: (ctx: Context, callback: (arguments: MapString) => void) => Promise<void> OnDeepLink: (ctx: Context, callback: (arguments: MapString) => void) => Promise<void>
/** /**
* Register on load event * Register on load event
*/ */
OnUnload: (ctx: Context, callback: () => Promise<void>) => Promise<void> OnUnload: (ctx: Context, callback: () => Promise<void>) => Promise<void>
/** /**
* Register query commands * Register query commands
*/ */
RegisterQueryCommands: (ctx: Context, commands: MetadataCommand[]) => Promise<void> RegisterQueryCommands: (ctx: Context, commands: MetadataCommand[]) => Promise<void>
/** /**
* Chat using LLM * Chat using LLM
*/ */
LLMStream: (ctx: Context, conversations: AI.Conversation[], callback: AI.ChatStreamFunc) => Promise<void> LLMStream: (ctx: Context, conversations: AI.Conversation[], callback: AI.ChatStreamFunc) => Promise<void>
} }
export type WoxImageType = "absolute" | "relative" | "base64" | "svg" | "url" | "emoji" | "lottie" export type WoxImageType = "absolute" | "relative" | "base64" | "svg" | "url" | "emoji" | "lottie"
export interface WoxImage { export interface WoxImage {
ImageType: WoxImageType ImageType: WoxImageType
ImageData: string ImageData: string
} }
export type WoxPreviewType = "markdown" | "text" | "image" | "url" | "file" export type WoxPreviewType = "markdown" | "text" | "image" | "url" | "file"
export interface WoxPreview { export interface WoxPreview {
PreviewType: WoxPreviewType PreviewType: WoxPreviewType
PreviewData: string PreviewData: string
PreviewProperties: Record<string, string> PreviewProperties: Record<string, string>
} }
export declare interface Context { export declare interface Context {
Values: { [key: string]: string } Values: { [key: string]: string }
Get: (key: string) => string | undefined Get: (key: string) => string | undefined
Set: (key: string, value: string) => void Set: (key: string, value: string) => void
Exists: (key: string) => boolean Exists: (key: string) => boolean
} }
export function NewContext(): Context export function NewContext(): Context

View File

@ -178,7 +178,7 @@ class _WoxSettingPluginTableUpdateState extends State<WoxSettingPluginTableUpdat
); );
case PluginSettingValueType.pluginSettingValueTableColumnTypeHotkey: case PluginSettingValueType.pluginSettingValueTableColumnTypeHotkey:
return WoxHotkeyRecorder( return WoxHotkeyRecorder(
hotkey: WoxHotkey.parseHotkey(getValue(column.key)), hotkey: WoxHotkey.parseHotkeyFromString(getValue(column.key)),
onHotKeyRecorded: (hotkey) { onHotKeyRecorded: (hotkey) {
updateValue(column.key, hotkey); updateValue(column.key, hotkey);
setState(() {}); setState(() {});

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:wox/api/wox_api.dart'; import 'package:wox/api/wox_api.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/utils/colors.dart'; import 'package:wox/utils/colors.dart';
import 'package:wox/utils/log.dart'; import 'package:wox/utils/log.dart';
@ -41,107 +42,9 @@ class _WoxHotkeyRecorderState extends State<WoxHotkeyRecorder> {
HardwareKeyboard.instance.removeHandler(_handleKeyEvent); HardwareKeyboard.instance.removeHandler(_handleKeyEvent);
} }
String getHotkeyString(HotKey hotKey) {
var modifiers = [];
if (hotKey.modifiers != null) {
for (var modifier in hotKey.modifiers!) {
if (modifier == HotKeyModifier.shift) {
modifiers.add("shift");
} else if (modifier == HotKeyModifier.control) {
modifiers.add("ctrl");
} else if (modifier == HotKeyModifier.alt) {
if (Platform.isMacOS) {
modifiers.add("option");
} else {
modifiers.add("alt");
}
} else if (modifier == HotKeyModifier.meta) {
if (Platform.isMacOS) {
modifiers.add("cmd");
} else {
modifiers.add("win");
}
}
}
}
var keyStr = hotKey.key.keyLabel.toLowerCase();
if (hotKey.key == PhysicalKeyboardKey.space) {
keyStr = "space";
} else if (hotKey.key == PhysicalKeyboardKey.enter) {
keyStr = "enter";
} else if (hotKey.key == PhysicalKeyboardKey.backspace) {
keyStr = "backspace";
} else if (hotKey.key == PhysicalKeyboardKey.delete) {
keyStr = "delete";
} else if (hotKey.key == PhysicalKeyboardKey.arrowLeft) {
keyStr = "left";
} else if (hotKey.key == PhysicalKeyboardKey.arrowDown) {
keyStr = "down";
} else if (hotKey.key == PhysicalKeyboardKey.arrowRight) {
keyStr = "right";
} else if (hotKey.key == PhysicalKeyboardKey.arrowUp) {
keyStr = "up";
}
return "${modifiers.join("+")}+$keyStr";
}
bool isAllowedKey(PhysicalKeyboardKey key) {
var allowedKeys = [
PhysicalKeyboardKey.keyA,
PhysicalKeyboardKey.keyB,
PhysicalKeyboardKey.keyC,
PhysicalKeyboardKey.keyD,
PhysicalKeyboardKey.keyE,
PhysicalKeyboardKey.keyF,
PhysicalKeyboardKey.keyG,
PhysicalKeyboardKey.keyH,
PhysicalKeyboardKey.keyI,
PhysicalKeyboardKey.keyJ,
PhysicalKeyboardKey.keyK,
PhysicalKeyboardKey.keyL,
PhysicalKeyboardKey.keyM,
PhysicalKeyboardKey.keyN,
PhysicalKeyboardKey.keyO,
PhysicalKeyboardKey.keyP,
PhysicalKeyboardKey.keyQ,
PhysicalKeyboardKey.keyR,
PhysicalKeyboardKey.keyS,
PhysicalKeyboardKey.keyT,
PhysicalKeyboardKey.keyU,
PhysicalKeyboardKey.keyV,
PhysicalKeyboardKey.keyW,
PhysicalKeyboardKey.keyX,
PhysicalKeyboardKey.keyY,
PhysicalKeyboardKey.keyZ,
PhysicalKeyboardKey.digit1,
PhysicalKeyboardKey.digit2,
PhysicalKeyboardKey.digit3,
PhysicalKeyboardKey.digit4,
PhysicalKeyboardKey.digit5,
PhysicalKeyboardKey.digit6,
PhysicalKeyboardKey.digit7,
PhysicalKeyboardKey.digit8,
PhysicalKeyboardKey.digit9,
PhysicalKeyboardKey.digit0,
PhysicalKeyboardKey.space,
PhysicalKeyboardKey.enter,
PhysicalKeyboardKey.backspace,
PhysicalKeyboardKey.delete,
PhysicalKeyboardKey.arrowLeft,
PhysicalKeyboardKey.arrowDown,
PhysicalKeyboardKey.arrowRight,
PhysicalKeyboardKey.arrowUp,
];
return allowedKeys.contains(key);
}
bool _handleKeyEvent(KeyEvent keyEvent) { bool _handleKeyEvent(KeyEvent keyEvent) {
// Logger.instance.debug(const UuidV4().generate(), "Hotkey: ${keyEvent}"); // Logger.instance.debug(const UuidV4().generate(), "Hotkey: ${keyEvent}");
if (_isFocused == false) return false; if (_isFocused == false) return false;
if (keyEvent is KeyUpEvent) return false;
// backspace to clear hotkey // backspace to clear hotkey
if (keyEvent.logicalKey == LogicalKeyboardKey.backspace) { if (keyEvent.logicalKey == LogicalKeyboardKey.backspace) {
@ -151,20 +54,12 @@ class _WoxHotkeyRecorderState extends State<WoxHotkeyRecorder> {
return true; return true;
} }
final physicalKeysPressed = HardwareKeyboard.instance.physicalKeysPressed; var newHotkey = WoxHotkey.parseHotkeyFromEvent(keyEvent);
var modifiers = HotKeyModifier.values.where((e) => e.physicalKeys.any(physicalKeysPressed.contains)).toList(); if (newHotkey == null) {
PhysicalKeyboardKey? key;
physicalKeysPressed.removeWhere((element) => !isAllowedKey(element));
if (physicalKeysPressed.isNotEmpty) {
key = physicalKeysPressed.last;
}
if (modifiers.isEmpty || key == null) {
return false; return false;
} }
var newHotkey = HotKey(key: key, modifiers: modifiers, scope: HotKeyScope.system); var hotkeyStr = WoxHotkey.toStr(newHotkey);
var hotkeyStr = getHotkeyString(newHotkey);
Logger.instance.debug(const UuidV4().generate(), "Hotkey str: $hotkeyStr"); Logger.instance.debug(const UuidV4().generate(), "Hotkey str: $hotkeyStr");
WoxApi.instance.isHotkeyAvailable(hotkeyStr).then((isAvailable) { WoxApi.instance.isHotkeyAvailable(hotkeyStr).then((isAvailable) {
Logger.instance.debug(const UuidV4().generate(), "Hotkey available: $isAvailable"); Logger.instance.debug(const UuidV4().generate(), "Hotkey available: $isAvailable");

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
class WoxHotkeyView extends StatelessWidget {
final HotKey hotkey;
const WoxHotkeyView({super.key, required this.hotkey});
Widget buildSingleView(String key) {
return Container(
constraints: BoxConstraints.tight(const Size(24, 24)),
decoration: BoxDecoration(color: Colors.grey[200], border: Border.all(color: Colors.grey[400]!), borderRadius: BorderRadius.circular(5)),
child: Center(
child: Text(
key,
style: const TextStyle(fontSize: 10),
),
),
);
}
String getModifierName(HotKeyModifier modifier) {
if (modifier == HotKeyModifier.meta) {
return "";
} else if (modifier == HotKeyModifier.alt) {
return "";
} else if (modifier == HotKeyModifier.control) {
return "";
} else if (modifier == HotKeyModifier.shift) {
return "";
}
return modifier.name;
}
String getKeyName(KeyboardKey key) {
if (key == LogicalKeyboardKey.enter) {
return "";
} else if (key == LogicalKeyboardKey.escape) {
return "";
} else if (key == LogicalKeyboardKey.backspace) {
return "";
} else if (key == LogicalKeyboardKey.delete) {
return "";
} else if (key == LogicalKeyboardKey.arrowUp) {
return "";
} else if (key == LogicalKeyboardKey.arrowDown) {
return "";
} else if (key == LogicalKeyboardKey.arrowLeft) {
return "";
} else if (key == LogicalKeyboardKey.arrowRight) {
return "";
} else if (key == LogicalKeyboardKey.pageUp) {
return "";
} else if (key == LogicalKeyboardKey.pageDown) {
return "";
} else if (key == LogicalKeyboardKey.home) {
return "";
} else if (key == LogicalKeyboardKey.end) {
return "";
} else if (key == LogicalKeyboardKey.tab) {
return "";
} else if (key == LogicalKeyboardKey.capsLock) {
return "";
} else if (key == LogicalKeyboardKey.insert) {
return "";
} else if (key == LogicalKeyboardKey.numLock) {
return "";
} else if (key == LogicalKeyboardKey.scrollLock) {
return "";
} else if (key == LogicalKeyboardKey.pause) {
return "";
} else if (key == LogicalKeyboardKey.printScreen) {
return "";
} else if (key == LogicalKeyboardKey.f1) {
return "F1";
} else if (key == LogicalKeyboardKey.f2) {
return "F2";
} else if (key == LogicalKeyboardKey.f3) {
return "F3";
} else if (key == LogicalKeyboardKey.f4) {
return "F4";
} else if (key == LogicalKeyboardKey.f5) {
return "F5";
}
return key.keyLabel;
}
@override
Widget build(BuildContext context) {
var hotkeyWidgets = <Widget>[];
if (hotkey.modifiers != null) {
hotkeyWidgets.addAll(hotkey.modifiers!.map((o) => buildSingleView(getModifierName(o))));
}
hotkeyWidgets.add(buildSingleView(getKeyName(hotkey.key)));
return Row(children: [
for (final widget in hotkeyWidgets)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: widget,
)
]);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:from_css_color/from_css_color.dart'; import 'package:from_css_color/from_css_color.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:wox/components/wox_image_view.dart'; import 'package:wox/components/wox_image_view.dart';
import 'package:wox/entity/wox_image.dart'; import 'package:wox/entity/wox_image.dart';
@ -12,6 +13,8 @@ import 'package:wox/enums/wox_result_tail_type_enum.dart';
import 'package:wox/utils/log.dart'; import 'package:wox/utils/log.dart';
import 'package:wox/utils/wox_setting_util.dart'; import 'package:wox/utils/wox_setting_util.dart';
import 'wox_hotkey_view.dart';
class WoxListItemView extends StatelessWidget { class WoxListItemView extends StatelessWidget {
final bool isActive; final bool isActive;
final Rx<WoxImage> icon; final Rx<WoxImage> icon;
@ -46,6 +49,53 @@ class WoxListItemView extends StatelessWidget {
} }
} }
Widget buildTails() {
return ConstrainedBox(
constraints: BoxConstraints(maxWidth: WoxSettingUtil.instance.currentSetting.appWidth / 2),
child: Padding(
padding: const EdgeInsets.only(left: 10.0, right: 5.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final tail in tails)
if (tail.type == WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_TEXT.code && tail.text != null)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
tail.text!,
style: TextStyle(
color: fromCssColor(isActive ? woxTheme.resultItemActiveTailTextColor : woxTheme.resultItemTailTextColor),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
strutStyle: const StrutStyle(
forceStrutHeight: true,
),
),
)
else if (tail.type == WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_HOTKEY.code && tail.hotkey != null)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: WoxHotkeyView(hotkey: tail.hotkey!),
)
else if (tail.type == WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_IMAGE.code && tail.image != null && tail.image!.imageData.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: WoxImageView(
woxImage: tail.image!,
width: getImageSize(tail.image!, 24),
height: getImageSize(tail.image!, 24),
),
),
],
),
),
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - container"); if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - container");
@ -131,47 +181,8 @@ class WoxListItemView extends StatelessWidget {
// Tails // Tails
Obx(() { Obx(() {
if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - tails"); if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - tails");
if (tails.isNotEmpty) { if (tails.isNotEmpty) {
return ConstrainedBox( return buildTails();
constraints: BoxConstraints(maxWidth: WoxSettingUtil.instance.currentSetting.appWidth / 2),
child: Padding(
padding: const EdgeInsets.only(left: 10.0, right: 5.0),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (final tail in tails)
if (tail.type == WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_TEXT.code && tail.text.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Text(
tail.text,
style: TextStyle(
color: fromCssColor(isActive ? woxTheme.resultItemActiveTailTextColor : woxTheme.resultItemTailTextColor),
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
strutStyle: const StrutStyle(
forceStrutHeight: true,
),
),
)
else if (tail.type == WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_IMAGE.code && tail.image.imageData.isNotEmpty)
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: WoxImageView(
woxImage: tail.image,
width: getImageSize(tail.image, 24),
height: getImageSize(tail.image, 24),
),
),
],
),
),
),
);
} else { } else {
return const SizedBox(); return const SizedBox();
} }

View File

@ -1,8 +1,11 @@
import 'dart:io';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
/// A hotkey in Wox at least consists of a modifier and a key.
class WoxHotkey { class WoxHotkey {
static HotKey? parseHotkey(String value) { static HotKey? parseHotkeyFromString(String value) {
final modifiers = <HotKeyModifier>[]; final modifiers = <HotKeyModifier>[];
LogicalKeyboardKey? key; LogicalKeyboardKey? key;
value.split("+").forEach((element) { value.split("+").forEach((element) {
@ -166,13 +169,182 @@ class WoxHotkey {
} }
}); });
if (modifiers.isEmpty || key == null) { if (key == null) {
return null; return null;
} }
return HotKey( return HotKey(
key: key!, key: key!,
modifiers: modifiers, modifiers: modifiers.isEmpty ? null : modifiers,
); );
} }
static HotKey? parseHotkeyFromEvent(KeyEvent event) {
if (event is KeyUpEvent) return null;
if (!WoxHotkey.isAllowedKey(event.physicalKey)) {
return null;
}
List<HotKeyModifier> modifiers = [];
if (HardwareKeyboard.instance.isAltPressed) {
modifiers.add(HotKeyModifier.alt);
}
if (HardwareKeyboard.instance.isControlPressed) {
modifiers.add(HotKeyModifier.control);
}
if (HardwareKeyboard.instance.isShiftPressed) {
modifiers.add(HotKeyModifier.shift);
}
if (HardwareKeyboard.instance.isMetaPressed) {
modifiers.add(HotKeyModifier.meta);
}
if (modifiers.isEmpty) {
return null;
}
return HotKey(key: event.physicalKey, modifiers: modifiers, scope: HotKeyScope.system);
}
static bool isAnyModifierPressed() {
return HardwareKeyboard.instance.physicalKeysPressed.any((element) => HotKeyModifier.values.any((e) => e.physicalKeys.contains(element)));
}
static List<HotKeyModifier> getPressedModifiers() {
final modifiers = <HotKeyModifier>[];
if (HardwareKeyboard.instance.isAltPressed) {
modifiers.add(HotKeyModifier.alt);
}
if (HardwareKeyboard.instance.isControlPressed) {
modifiers.add(HotKeyModifier.control);
}
if (HardwareKeyboard.instance.isShiftPressed) {
modifiers.add(HotKeyModifier.shift);
}
if (HardwareKeyboard.instance.isMetaPressed) {
modifiers.add(HotKeyModifier.meta);
}
return modifiers;
}
static bool isAllowedKey(PhysicalKeyboardKey key) {
var allowedKeys = [
PhysicalKeyboardKey.keyA,
PhysicalKeyboardKey.keyB,
PhysicalKeyboardKey.keyC,
PhysicalKeyboardKey.keyD,
PhysicalKeyboardKey.keyE,
PhysicalKeyboardKey.keyF,
PhysicalKeyboardKey.keyG,
PhysicalKeyboardKey.keyH,
PhysicalKeyboardKey.keyI,
PhysicalKeyboardKey.keyJ,
PhysicalKeyboardKey.keyK,
PhysicalKeyboardKey.keyL,
PhysicalKeyboardKey.keyM,
PhysicalKeyboardKey.keyN,
PhysicalKeyboardKey.keyO,
PhysicalKeyboardKey.keyP,
PhysicalKeyboardKey.keyQ,
PhysicalKeyboardKey.keyR,
PhysicalKeyboardKey.keyS,
PhysicalKeyboardKey.keyT,
PhysicalKeyboardKey.keyU,
PhysicalKeyboardKey.keyV,
PhysicalKeyboardKey.keyW,
PhysicalKeyboardKey.keyX,
PhysicalKeyboardKey.keyY,
PhysicalKeyboardKey.keyZ,
PhysicalKeyboardKey.digit1,
PhysicalKeyboardKey.digit2,
PhysicalKeyboardKey.digit3,
PhysicalKeyboardKey.digit4,
PhysicalKeyboardKey.digit5,
PhysicalKeyboardKey.digit6,
PhysicalKeyboardKey.digit7,
PhysicalKeyboardKey.digit8,
PhysicalKeyboardKey.digit9,
PhysicalKeyboardKey.digit0,
PhysicalKeyboardKey.space,
PhysicalKeyboardKey.enter,
PhysicalKeyboardKey.backspace,
PhysicalKeyboardKey.delete,
PhysicalKeyboardKey.arrowLeft,
PhysicalKeyboardKey.arrowDown,
PhysicalKeyboardKey.arrowRight,
PhysicalKeyboardKey.arrowUp,
];
return allowedKeys.contains(key);
}
static bool equals(HotKey? a, HotKey? b) {
if (a == null || b == null) {
return false;
}
return a.key.keyLabel == b.key.keyLabel && isModifiersEquals(a.modifiers, b.modifiers);
}
static bool isModifiersEquals(List<HotKeyModifier>? a, List<HotKeyModifier>? b) {
if (a == null || b == null) {
return false;
}
if (a.length != b.length) {
return false;
}
// check if all elements in a are in b
// and all elements in b are in a
return a.every((element) => b.map((o) => o.name).contains(element.name)) && b.every((element) => a.map((o) => o.name).contains(element.name));
}
static String toStr(HotKey hotKey) {
var modifiers = [];
if (hotKey.modifiers != null) {
for (var modifier in hotKey.modifiers!) {
if (modifier == HotKeyModifier.shift) {
modifiers.add("shift");
} else if (modifier == HotKeyModifier.control) {
modifiers.add("ctrl");
} else if (modifier == HotKeyModifier.alt) {
if (Platform.isMacOS) {
modifiers.add("option");
} else {
modifiers.add("alt");
}
} else if (modifier == HotKeyModifier.meta) {
if (Platform.isMacOS) {
modifiers.add("cmd");
} else {
modifiers.add("win");
}
}
}
}
var keyStr = hotKey.key.keyLabel.toLowerCase();
if (hotKey.key == PhysicalKeyboardKey.space) {
keyStr = "space";
} else if (hotKey.key == PhysicalKeyboardKey.enter) {
keyStr = "enter";
} else if (hotKey.key == PhysicalKeyboardKey.backspace) {
keyStr = "backspace";
} else if (hotKey.key == PhysicalKeyboardKey.delete) {
keyStr = "delete";
} else if (hotKey.key == PhysicalKeyboardKey.arrowLeft) {
keyStr = "left";
} else if (hotKey.key == PhysicalKeyboardKey.arrowDown) {
keyStr = "down";
} else if (hotKey.key == PhysicalKeyboardKey.arrowRight) {
keyStr = "right";
} else if (hotKey.key == PhysicalKeyboardKey.arrowUp) {
keyStr = "up";
}
return "${modifiers.join("+")}+$keyStr";
}
} }

View File

@ -1,9 +1,12 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_image.dart'; import 'package:wox/entity/wox_image.dart';
import 'package:wox/entity/wox_preview.dart'; import 'package:wox/entity/wox_preview.dart';
import 'package:wox/enums/wox_last_query_mode_enum.dart'; import 'package:wox/enums/wox_last_query_mode_enum.dart';
import 'package:wox/enums/wox_position_type_enum.dart'; import 'package:wox/enums/wox_position_type_enum.dart';
import 'package:wox/enums/wox_query_type_enum.dart'; import 'package:wox/enums/wox_query_type_enum.dart';
import 'package:wox/enums/wox_result_tail_type_enum.dart';
import 'package:wox/enums/wox_selection_type_enum.dart'; import 'package:wox/enums/wox_selection_type_enum.dart';
class PlainQuery { class PlainQuery {
@ -188,24 +191,68 @@ class WoxQueryResult {
class WoxQueryResultTail { class WoxQueryResultTail {
late String type; late String type;
late String text; late String? text;
late WoxImage image; late WoxImage? image;
late HotKey? hotkey;
WoxQueryResultTail({required this.type, required this.text, required this.image}); WoxQueryResultTail({required this.type, this.text, this.image, this.hotkey});
WoxQueryResultTail.fromJson(Map<String, dynamic> json) { WoxQueryResultTail.fromJson(Map<String, dynamic> json) {
type = json['Type']; type = json['Type'];
text = json['Text']; if (json['Text'] != null) {
image = WoxImage.fromJson(json['Image']); text = json['Text'];
} else {
text = null;
}
if (json['Image'] != null) {
image = WoxImage.fromJson(json['Image']);
} else {
image = null;
}
if (json['Hotkey'] != null) {
hotkey = WoxHotkey.parseHotkeyFromString(json['Hotkey']);
} else {
hotkey = null;
}
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{}; final Map<String, dynamic> data = <String, dynamic>{};
data['Type'] = type; data['Type'] = type;
data['Text'] = text;
data['Image'] = image.toJson(); if (text != null) {
data['Text'] = text;
} else {
data['Text'] = null;
}
if (image != null) {
data['Image'] = image!.toJson();
} else {
data['Image'] = null;
}
if (hotkey != null) {
data['Hotkey'] = hotkey!.toString();
} else {
data['Hotkey'] = null;
}
return data; return data;
} }
factory WoxQueryResultTail.text(String text) {
return WoxQueryResultTail(type: WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_TEXT.code, text: text);
}
factory WoxQueryResultTail.hotkey(HotKey hotkey) {
return WoxQueryResultTail(type: WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_HOTKEY.code, hotkey: hotkey);
}
factory WoxQueryResultTail.image(WoxImage image) {
return WoxQueryResultTail(type: WoxQueryResultTailTypeEnum.WOX_QUERY_RESULT_TAIL_TYPE_IMAGE.code, image: image);
}
} }
class WoxResultAction { class WoxResultAction {
@ -214,8 +261,9 @@ class WoxResultAction {
late Rx<WoxImage> icon; late Rx<WoxImage> icon;
late bool isDefault; late bool isDefault;
late bool preventHideAfterAction; late bool preventHideAfterAction;
late String hotkey;
WoxResultAction({required this.id, required this.name, required this.icon, required this.isDefault, required this.preventHideAfterAction}); WoxResultAction({required this.id, required this.name, required this.icon, required this.isDefault, required this.preventHideAfterAction, required this.hotkey});
WoxResultAction.fromJson(Map<String, dynamic> json) { WoxResultAction.fromJson(Map<String, dynamic> json) {
id = json['Id']; id = json['Id'];
@ -223,6 +271,9 @@ class WoxResultAction {
icon = (json['Icon'] != null ? WoxImage.fromJson(json['Icon']).obs : null)!; icon = (json['Icon'] != null ? WoxImage.fromJson(json['Icon']).obs : null)!;
isDefault = json['IsDefault']; isDefault = json['IsDefault'];
preventHideAfterAction = json['PreventHideAfterAction']; preventHideAfterAction = json['PreventHideAfterAction'];
if (json['Hotkey'] != null) {
hotkey = json['Hotkey'];
}
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -232,11 +283,12 @@ class WoxResultAction {
data['Icon'] = icon.toJson(); data['Icon'] = icon.toJson();
data['IsDefault'] = isDefault; data['IsDefault'] = isDefault;
data['PreventHideAfterAction'] = preventHideAfterAction; data['PreventHideAfterAction'] = preventHideAfterAction;
data['Hotkey'] = hotkey;
return data; return data;
} }
static WoxResultAction empty() { static WoxResultAction empty() {
return WoxResultAction(id: "", name: "".obs, icon: WoxImage.empty().obs, isDefault: false, preventHideAfterAction: false); return WoxResultAction(id: "", name: "".obs, icon: WoxImage.empty().obs, isDefault: false, preventHideAfterAction: false, hotkey: "");
} }
} }

View File

@ -2,7 +2,8 @@ typedef WoxQueryResultTailType = String;
enum WoxQueryResultTailTypeEnum { enum WoxQueryResultTailTypeEnum {
WOX_QUERY_RESULT_TAIL_TYPE_TEXT("text", "text"), WOX_QUERY_RESULT_TAIL_TYPE_TEXT("text", "text"),
WOX_QUERY_RESULT_TAIL_TYPE_IMAGE("image", "image"); WOX_QUERY_RESULT_TAIL_TYPE_IMAGE("image", "image"),
WOX_QUERY_RESULT_TAIL_TYPE_HOTKEY("hotkey", "hotkey");
final String code; final String code;
final String value; final String value;

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:wox/components/wox_image_view.dart'; import 'package:wox/components/wox_image_view.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/modules/launcher/wox_launcher_controller.dart'; import 'package:wox/modules/launcher/wox_launcher_controller.dart';
import 'package:wox/utils/log.dart'; import 'package:wox/utils/log.dart';
@ -21,46 +22,63 @@ class WoxQueryBoxView extends GetView<WoxLauncherController> {
child: Focus( child: Focus(
autofocus: true, autofocus: true,
onKeyEvent: (FocusNode node, KeyEvent event) { onKeyEvent: (FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) { var isAnyModifierPressed = WoxHotkey.isAnyModifierPressed();
switch (event.logicalKey) { if (!isAnyModifierPressed) {
case LogicalKeyboardKey.escape: if (event is KeyDownEvent) {
controller.hideApp(const UuidV4().generate()); switch (event.logicalKey) {
return KeyEventResult.handled; case LogicalKeyboardKey.escape:
case LogicalKeyboardKey.arrowDown: controller.hideApp(const UuidV4().generate());
controller.handleQueryBoxArrowDown();
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp:
controller.handleQueryBoxArrowUp();
return KeyEventResult.handled;
case LogicalKeyboardKey.enter:
controller.executeAction(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.tab:
controller.autoCompleteQuery(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.home:
controller.moveQueryBoxCursorToStart();
return KeyEventResult.handled;
case LogicalKeyboardKey.end:
controller.moveQueryBoxCursorToEnd();
return KeyEventResult.handled;
case LogicalKeyboardKey.keyJ:
if (HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isAltPressed) {
controller.toggleActionPanel(const UuidV4().generate());
return KeyEventResult.handled; return KeyEventResult.handled;
} case LogicalKeyboardKey.arrowDown:
controller.handleQueryBoxArrowDown();
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp:
controller.handleQueryBoxArrowUp();
return KeyEventResult.handled;
case LogicalKeyboardKey.enter:
controller.executeActiveAction(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.tab:
controller.autoCompleteQuery(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.home:
controller.moveQueryBoxCursorToStart();
return KeyEventResult.handled;
case LogicalKeyboardKey.end:
controller.moveQueryBoxCursorToEnd();
return KeyEventResult.handled;
}
}
if (event is KeyRepeatEvent) {
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowDown:
controller.handleQueryBoxArrowDown();
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp:
controller.handleQueryBoxArrowUp();
return KeyEventResult.handled;
}
} }
} }
if (event is KeyRepeatEvent) { var pressedHotkey = WoxHotkey.parseHotkeyFromEvent(event);
switch (event.logicalKey) { if (pressedHotkey == null) {
case LogicalKeyboardKey.arrowDown: return KeyEventResult.ignored;
controller.handleQueryBoxArrowDown(); }
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp: // list all actions
controller.handleQueryBoxArrowUp(); if (WoxHotkey.equals(pressedHotkey, WoxHotkey.parseHotkeyFromString("cmd+J"))) {
return KeyEventResult.handled; controller.toggleActionPanel(const UuidV4().generate());
} return KeyEventResult.handled;
}
// check if the pressed hotkey is the action hotkey
var result = controller.getActiveResult();
var action = controller.getActionByHotkey(result, pressedHotkey);
if (action != null) {
controller.executeAction(const UuidV4().generate(), result, action);
return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -6,6 +6,7 @@ import 'package:get/get.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:wox/components/wox_list_item_view.dart'; import 'package:wox/components/wox_list_item_view.dart';
import 'package:wox/components/wox_preview_view.dart'; import 'package:wox/components/wox_preview_view.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_query.dart'; import 'package:wox/entity/wox_query.dart';
import 'package:wox/enums/wox_direction_enum.dart'; import 'package:wox/enums/wox_direction_enum.dart';
import 'package:wox/enums/wox_event_device_type_enum.dart'; import 'package:wox/enums/wox_event_device_type_enum.dart';
@ -18,6 +19,17 @@ import '../wox_launcher_controller.dart';
class WoxQueryResultView extends GetView<WoxLauncherController> { class WoxQueryResultView extends GetView<WoxLauncherController> {
const WoxQueryResultView({super.key}); const WoxQueryResultView({super.key});
RxList<WoxQueryResultTail> getHotkeyTails(WoxResultAction action) {
var tails = <WoxQueryResultTail>[];
if (action.hotkey != "") {
var hotkey = WoxHotkey.parseHotkeyFromString(action.hotkey);
if (hotkey != null) {
tails.add(WoxQueryResultTail.hotkey(hotkey));
}
}
return tails.obs;
}
Widget getActionListView() { Widget getActionListView() {
return Obx(() { return Obx(() {
if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: action list view container"); if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: action list view container");
@ -40,7 +52,7 @@ class WoxQueryResultView extends GetView<WoxLauncherController> {
woxTheme: controller.woxTheme.value, woxTheme: controller.woxTheme.value,
icon: woxResultAction.icon, icon: woxResultAction.icon,
title: woxResultAction.name, title: woxResultAction.name,
tails: RxList<WoxQueryResultTail>(), tails: getHotkeyTails(woxResultAction),
subTitle: "".obs, subTitle: "".obs,
isActive: controller.isActionActiveByIndex(index), isActive: controller.isActionActiveByIndex(index),
listViewType: WoxListViewTypeEnum.WOX_LIST_VIEW_TYPE_ACTION.code, listViewType: WoxListViewTypeEnum.WOX_LIST_VIEW_TYPE_ACTION.code,
@ -170,42 +182,58 @@ class WoxQueryResultView extends GetView<WoxLauncherController> {
Widget getActionQueryBox() { Widget getActionQueryBox() {
return Focus( return Focus(
onKeyEvent: (FocusNode node, KeyEvent event) { onKeyEvent: (FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) { var isAnyModifierPressed = WoxHotkey.isAnyModifierPressed();
if (event.logicalKey == LogicalKeyboardKey.escape) { if (!isAnyModifierPressed) {
controller.toggleActionPanel(const UuidV4().generate()); if (event is KeyDownEvent) {
return KeyEventResult.handled; switch (event.logicalKey) {
case LogicalKeyboardKey.escape:
controller.toggleActionPanel(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowDown:
controller.changeActionScrollPosition(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code);
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp:
controller.changeActionScrollPosition(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code);
return KeyEventResult.handled;
case LogicalKeyboardKey.enter:
controller.executeActiveAction(const UuidV4().generate());
return KeyEventResult.handled;
}
} }
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
controller.changeActionScrollPosition( if (event is KeyRepeatEvent) {
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code); switch (event.logicalKey) {
return KeyEventResult.handled; case LogicalKeyboardKey.arrowDown:
} controller.changeActionScrollPosition(
if (event.logicalKey == LogicalKeyboardKey.arrowUp) { const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code);
controller.changeActionScrollPosition( return KeyEventResult.handled;
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code); case LogicalKeyboardKey.arrowUp:
return KeyEventResult.handled; controller.changeActionScrollPosition(
} const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code);
if (event.logicalKey == LogicalKeyboardKey.enter) { return KeyEventResult.handled;
controller.executeAction(const UuidV4().generate()); }
return KeyEventResult.handled;
}
if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isAltPressed) && event.logicalKey == LogicalKeyboardKey.keyJ) {
controller.toggleActionPanel(const UuidV4().generate());
return KeyEventResult.handled;
} }
} }
if (event is KeyRepeatEvent) { var pressedHotkey = WoxHotkey.parseHotkeyFromEvent(event);
if (event.logicalKey == LogicalKeyboardKey.arrowDown) { if (pressedHotkey == null) {
controller.changeActionScrollPosition( return KeyEventResult.ignored;
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code); }
return KeyEventResult.handled;
} // list all actions
if (event.logicalKey == LogicalKeyboardKey.arrowUp) { if (WoxHotkey.equals(pressedHotkey, WoxHotkey.parseHotkeyFromString("cmd+J"))) {
controller.changeActionScrollPosition( controller.toggleActionPanel(const UuidV4().generate());
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code); return KeyEventResult.handled;
return KeyEventResult.handled; }
}
// check if the pressed hotkey is the action hotkey
var result = controller.getActiveResult();
var action = controller.getActionByHotkey(result, pressedHotkey);
if (action != null) {
controller.executeAction(const UuidV4().generate(), result, action);
return KeyEventResult.handled;
} }
return KeyEventResult.ignored; return KeyEventResult.ignored;

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:from_css_color/from_css_color.dart'; import 'package:from_css_color/from_css_color.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:wox/components/wox_hotkey_view.dart';
import 'package:wox/components/wox_image_view.dart'; import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_image.dart';
import 'package:wox/enums/wox_image_type_enum.dart';
import 'package:wox/modules/launcher/wox_launcher_controller.dart'; import 'package:wox/modules/launcher/wox_launcher_controller.dart';
import 'package:wox/utils/wox_theme_util.dart'; import 'package:wox/utils/wox_theme_util.dart';
@ -22,10 +19,11 @@ class WoxQueryToolbarView extends GetView<WoxLauncherController> {
return const SizedBox(); return const SizedBox();
} }
var hotkey = WoxHotkey.parseHotkeyFromString(action.hotkey) ?? WoxHotkey.parseHotkeyFromString("enter");
return Row( return Row(
children: [ children: [
WoxImageView(woxImage: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_EMOJI.code, imageData: "↩️")),
Text(action.name.value, style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor))), Text(action.name.value, style: TextStyle(color: fromCssColor(controller.woxTheme.value.toolbarFontColor))),
WoxHotkeyView(hotkey: hotkey!),
], ],
); );
} }

View File

@ -5,10 +5,12 @@ import 'dart:ui';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:lpinyin/lpinyin.dart'; import 'package:lpinyin/lpinyin.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:wox/api/wox_api.dart'; import 'package:wox/api/wox_api.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_image.dart'; import 'package:wox/entity/wox_image.dart';
import 'package:wox/entity/wox_preview.dart'; import 'package:wox/entity/wox_preview.dart';
import 'package:wox/entity/wox_query.dart'; import 'package:wox/entity/wox_query.dart';
@ -236,17 +238,40 @@ class WoxLauncherController extends GetxController {
return actions[activeActionIndex.value]; return actions[activeActionIndex.value];
} }
Future<void> executeAction(String traceId) async { /// given a hotkey, find the action in the result
Logger.instance.debug(traceId, "user execute result action"); WoxResultAction? getActionByHotkey(WoxQueryResult? result, HotKey hotkey) {
if (result == null) {
return null;
}
WoxQueryResult? woxQueryResult = getActiveResult(); var filteredActions = result.actions.where((action) {
if (woxQueryResult == null) { var actionHotkey = WoxHotkey.parseHotkeyFromString(action.hotkey);
if (WoxHotkey.equals(actionHotkey, hotkey)) {
return true;
}
return false;
});
if (filteredActions.isEmpty) {
return null;
}
return filteredActions.first;
}
Future<void> executeActiveAction(String traceId) async {
executeAction(traceId, getActiveResult(), getActiveAction());
}
Future<void> executeAction(String traceId, WoxQueryResult? result, WoxResultAction? action) async {
Logger.instance.debug(traceId, "user execute result action: ${action?.name}");
if (result == null) {
Logger.instance.error(traceId, "active query result is null"); Logger.instance.error(traceId, "active query result is null");
return; return;
} }
if (action == null) {
WoxResultAction? activeAction = getActiveAction();
if (activeAction == null) {
Logger.instance.error(traceId, "active action is null"); Logger.instance.error(traceId, "active action is null");
return; return;
} }
@ -257,12 +282,12 @@ class WoxLauncherController extends GetxController {
type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code, type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code,
method: WoxMsgMethodEnum.WOX_MSG_METHOD_ACTION.code, method: WoxMsgMethodEnum.WOX_MSG_METHOD_ACTION.code,
data: { data: {
"resultId": woxQueryResult.id, "resultId": result.id,
"actionId": activeAction.id, "actionId": action.id,
}, },
)); ));
if (!activeAction.preventHideAfterAction) { if (!action.preventHideAfterAction) {
hideApp(traceId); hideApp(traceId);
} }
hideActionPanel(); hideActionPanel();

View File

@ -72,7 +72,7 @@ class WoxSettingGeneralView extends GetView<WoxSettingController> {
label: controller.tr("hotkey"), label: controller.tr("hotkey"),
tips: controller.tr("hotkey_tips"), tips: controller.tr("hotkey_tips"),
child: WoxHotkeyRecorder( child: WoxHotkeyRecorder(
hotkey: WoxHotkey.parseHotkey(controller.woxSetting.value.mainHotkey), hotkey: WoxHotkey.parseHotkeyFromString(controller.woxSetting.value.mainHotkey),
onHotKeyRecorded: (hotkey) { onHotKeyRecorded: (hotkey) {
controller.updateConfig("MainHotkey", hotkey); controller.updateConfig("MainHotkey", hotkey);
}, },
@ -82,7 +82,7 @@ class WoxSettingGeneralView extends GetView<WoxSettingController> {
label: controller.tr("selection_hotkey"), label: controller.tr("selection_hotkey"),
tips: controller.tr("selection_hotkey_tips"), tips: controller.tr("selection_hotkey_tips"),
child: WoxHotkeyRecorder( child: WoxHotkeyRecorder(
hotkey: WoxHotkey.parseHotkey(controller.woxSetting.value.selectionHotkey), hotkey: WoxHotkey.parseHotkeyFromString(controller.woxSetting.value.selectionHotkey),
onHotKeyRecorded: (hotkey) { onHotKeyRecorded: (hotkey) {
controller.updateConfig("SelectionHotkey", hotkey); controller.updateConfig("SelectionHotkey", hotkey);
}, },

View File

@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1 version: 1.0.0+1
environment: environment:
sdk: '>=3.1.3 <4.0.0' sdk: '>=3.4.0 <4.0.0'
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions

View File

@ -503,6 +503,7 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
}) })
if defaultActionCount == 0 && len(result.Actions) > 0 { if defaultActionCount == 0 && len(result.Actions) > 0 {
result.Actions[0].IsDefault = true result.Actions[0].IsDefault = true
result.Actions[0].Hotkey = "Enter"
} }
var resultCache = &QueryResultCache{ var resultCache = &QueryResultCache{
@ -516,8 +517,26 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
} }
// store actions for ui invoke later // store actions for ui invoke later
for actionId := range result.Actions { for actionIndex := range result.Actions {
var action = result.Actions[actionId] var action = result.Actions[actionIndex]
// if default action's hotkey is empty, set it as Enter
if action.IsDefault && action.Hotkey == "" {
result.Actions[actionIndex].Hotkey = "Enter"
}
// replace hotkey modifiers for platform specific, E.g. replace win to cmd on macos, replace cmd to win on windows
if util.IsMacOS() {
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "win", "cmd")
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "windows", "cmd")
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "alt", "option")
}
if util.IsWindows() || util.IsLinux() {
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "cmd", "win")
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "command", "win")
result.Actions[actionIndex].Hotkey = strings.ReplaceAll(result.Actions[actionIndex].Hotkey, "option", "alt")
}
if action.Action != nil { if action.Action != nil {
resultCache.Actions.Store(action.Id, action.Action) resultCache.Actions.Store(action.Id, action.Action)
} }

View File

@ -132,6 +132,10 @@ type QueryResultAction struct {
// If true, Wox will not hide after user select this result // If true, Wox will not hide after user select this result
PreventHideAfterAction bool PreventHideAfterAction bool
Action func(ctx context.Context, actionContext ActionContext) Action func(ctx context.Context, actionContext ActionContext)
// Hotkey to trigger this action. E.g. "ctrl+Shift+Space", "Ctrl+1", "Command+K"
// Case insensitive, space insensitive
// If IsDefault is true, Hotkey will be set to enter key by default
Hotkey string
} }
type ActionContext struct { type ActionContext struct {
@ -158,6 +162,7 @@ func (q *QueryResult) ToUI() QueryResultUI {
Icon: action.Icon, Icon: action.Icon,
IsDefault: action.IsDefault, IsDefault: action.IsDefault,
PreventHideAfterAction: action.PreventHideAfterAction, PreventHideAfterAction: action.PreventHideAfterAction,
Hotkey: action.Hotkey,
} }
}), }),
RefreshInterval: q.RefreshInterval, RefreshInterval: q.RefreshInterval,
@ -186,6 +191,7 @@ type QueryResultActionUI struct {
Icon WoxImage Icon WoxImage
IsDefault bool IsDefault bool
PreventHideAfterAction bool PreventHideAfterAction bool
Hotkey string
} }
// store latest result value after query/refresh, so we can retrieve data later in action/refresh // store latest result value after query/refresh, so we can retrieve data later in action/refresh