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"
},
"dependencies": {
"@wox-launcher/wox-plugin": "^0.0.78",
"@wox-launcher/wox-plugin": "^0.0.79",
"dayjs": "^1.11.9",
"promise-deferred": "^2.0.4",
"winston": "^3.10.0",

View File

@ -1,6 +1,6 @@
{
"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",
"repository": {
"type": "git",

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:uuid/v4.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/log.dart';
@ -41,107 +42,9 @@ class _WoxHotkeyRecorderState extends State<WoxHotkeyRecorder> {
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) {
// Logger.instance.debug(const UuidV4().generate(), "Hotkey: ${keyEvent}");
if (_isFocused == false) return false;
if (keyEvent is KeyUpEvent) return false;
// backspace to clear hotkey
if (keyEvent.logicalKey == LogicalKeyboardKey.backspace) {
@ -151,20 +54,12 @@ class _WoxHotkeyRecorderState extends State<WoxHotkeyRecorder> {
return true;
}
final physicalKeysPressed = HardwareKeyboard.instance.physicalKeysPressed;
var modifiers = HotKeyModifier.values.where((e) => e.physicalKeys.any(physicalKeysPressed.contains)).toList();
PhysicalKeyboardKey? key;
physicalKeysPressed.removeWhere((element) => !isAllowedKey(element));
if (physicalKeysPressed.isNotEmpty) {
key = physicalKeysPressed.last;
}
if (modifiers.isEmpty || key == null) {
var newHotkey = WoxHotkey.parseHotkeyFromEvent(keyEvent);
if (newHotkey == null) {
return false;
}
var newHotkey = HotKey(key: key, modifiers: modifiers, scope: HotKeyScope.system);
var hotkeyStr = getHotkeyString(newHotkey);
var hotkeyStr = WoxHotkey.toStr(newHotkey);
Logger.instance.debug(const UuidV4().generate(), "Hotkey str: $hotkeyStr");
WoxApi.instance.isHotkeyAvailable(hotkeyStr).then((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:from_css_color/from_css_color.dart';
import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:uuid/v4.dart';
import 'package:wox/components/wox_image_view.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/wox_setting_util.dart';
import 'wox_hotkey_view.dart';
class WoxListItemView extends StatelessWidget {
final bool isActive;
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
Widget build(BuildContext context) {
if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - container");
@ -131,47 +181,8 @@ class WoxListItemView extends StatelessWidget {
// Tails
Obx(() {
if (LoggerSwitch.enablePaintLog) Logger.instance.info(const UuidV4().generate(), "repaint: list item view $key - tails");
if (tails.isNotEmpty) {
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.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),
),
),
],
),
),
),
);
return buildTails();
} else {
return const SizedBox();
}

View File

@ -1,8 +1,11 @@
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
/// A hotkey in Wox at least consists of a modifier and a key.
class WoxHotkey {
static HotKey? parseHotkey(String value) {
static HotKey? parseHotkeyFromString(String value) {
final modifiers = <HotKeyModifier>[];
LogicalKeyboardKey? key;
value.split("+").forEach((element) {
@ -166,13 +169,182 @@ class WoxHotkey {
}
});
if (modifiers.isEmpty || key == null) {
if (key == null) {
return null;
}
return HotKey(
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:hotkey_manager/hotkey_manager.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/entity/wox_image.dart';
import 'package:wox/entity/wox_preview.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_query_type_enum.dart';
import 'package:wox/enums/wox_result_tail_type_enum.dart';
import 'package:wox/enums/wox_selection_type_enum.dart';
class PlainQuery {
@ -188,24 +191,68 @@ class WoxQueryResult {
class WoxQueryResultTail {
late String type;
late String text;
late WoxImage image;
late String? text;
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) {
type = json['Type'];
text = json['Text'];
image = WoxImage.fromJson(json['Image']);
if (json['Text'] != null) {
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() {
final Map<String, dynamic> data = <String, dynamic>{};
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;
}
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 {
@ -214,8 +261,9 @@ class WoxResultAction {
late Rx<WoxImage> icon;
late bool isDefault;
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) {
id = json['Id'];
@ -223,6 +271,9 @@ class WoxResultAction {
icon = (json['Icon'] != null ? WoxImage.fromJson(json['Icon']).obs : null)!;
isDefault = json['IsDefault'];
preventHideAfterAction = json['PreventHideAfterAction'];
if (json['Hotkey'] != null) {
hotkey = json['Hotkey'];
}
}
Map<String, dynamic> toJson() {
@ -232,11 +283,12 @@ class WoxResultAction {
data['Icon'] = icon.toJson();
data['IsDefault'] = isDefault;
data['PreventHideAfterAction'] = preventHideAfterAction;
data['Hotkey'] = hotkey;
return data;
}
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 {
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 value;

View File

@ -5,6 +5,7 @@ import 'package:get/get.dart';
import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.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/utils/log.dart';
@ -21,46 +22,63 @@ class WoxQueryBoxView extends GetView<WoxLauncherController> {
child: Focus(
autofocus: true,
onKeyEvent: (FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) {
switch (event.logicalKey) {
case LogicalKeyboardKey.escape:
controller.hideApp(const UuidV4().generate());
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowDown:
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());
var isAnyModifierPressed = WoxHotkey.isAnyModifierPressed();
if (!isAnyModifierPressed) {
if (event is KeyDownEvent) {
switch (event.logicalKey) {
case LogicalKeyboardKey.escape:
controller.hideApp(const UuidV4().generate());
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) {
switch (event.logicalKey) {
case LogicalKeyboardKey.arrowDown:
controller.handleQueryBoxArrowDown();
return KeyEventResult.handled;
case LogicalKeyboardKey.arrowUp:
controller.handleQueryBoxArrowUp();
return KeyEventResult.handled;
}
var pressedHotkey = WoxHotkey.parseHotkeyFromEvent(event);
if (pressedHotkey == null) {
return KeyEventResult.ignored;
}
// list all actions
if (WoxHotkey.equals(pressedHotkey, WoxHotkey.parseHotkeyFromString("cmd+J"))) {
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;

View File

@ -6,6 +6,7 @@ import 'package:get/get.dart';
import 'package:uuid/v4.dart';
import 'package:wox/components/wox_list_item_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/enums/wox_direction_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> {
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() {
return Obx(() {
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,
icon: woxResultAction.icon,
title: woxResultAction.name,
tails: RxList<WoxQueryResultTail>(),
tails: getHotkeyTails(woxResultAction),
subTitle: "".obs,
isActive: controller.isActionActiveByIndex(index),
listViewType: WoxListViewTypeEnum.WOX_LIST_VIEW_TYPE_ACTION.code,
@ -170,42 +182,58 @@ class WoxQueryResultView extends GetView<WoxLauncherController> {
Widget getActionQueryBox() {
return Focus(
onKeyEvent: (FocusNode node, KeyEvent event) {
if (event is KeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
controller.toggleActionPanel(const UuidV4().generate());
return KeyEventResult.handled;
var isAnyModifierPressed = WoxHotkey.isAnyModifierPressed();
if (!isAnyModifierPressed) {
if (event is KeyDownEvent) {
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(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code);
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
controller.changeActionScrollPosition(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code);
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.enter) {
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) {
switch (event.logicalKey) {
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;
}
}
}
if (event is KeyRepeatEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
controller.changeActionScrollPosition(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code);
return KeyEventResult.handled;
}
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
controller.changeActionScrollPosition(
const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code);
return KeyEventResult.handled;
}
var pressedHotkey = WoxHotkey.parseHotkeyFromEvent(event);
if (pressedHotkey == null) {
return KeyEventResult.ignored;
}
// list all actions
if (WoxHotkey.equals(pressedHotkey, WoxHotkey.parseHotkeyFromString("cmd+J"))) {
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;

View File

@ -1,11 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:from_css_color/from_css_color.dart';
import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:wox/components/wox_image_view.dart';
import 'package:wox/entity/wox_image.dart';
import 'package:wox/enums/wox_image_type_enum.dart';
import 'package:wox/components/wox_hotkey_view.dart';
import 'package:wox/entity/wox_hotkey.dart';
import 'package:wox/modules/launcher/wox_launcher_controller.dart';
import 'package:wox/utils/wox_theme_util.dart';
@ -22,10 +19,11 @@ class WoxQueryToolbarView extends GetView<WoxLauncherController> {
return const SizedBox();
}
var hotkey = WoxHotkey.parseHotkeyFromString(action.hotkey) ?? WoxHotkey.parseHotkeyFromString("enter");
return Row(
children: [
WoxImageView(woxImage: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_EMOJI.code, imageData: "↩️")),
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:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.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_preview.dart';
import 'package:wox/entity/wox_query.dart';
@ -236,17 +238,40 @@ class WoxLauncherController extends GetxController {
return actions[activeActionIndex.value];
}
Future<void> executeAction(String traceId) async {
Logger.instance.debug(traceId, "user execute result action");
/// given a hotkey, find the action in the result
WoxResultAction? getActionByHotkey(WoxQueryResult? result, HotKey hotkey) {
if (result == null) {
return null;
}
WoxQueryResult? woxQueryResult = getActiveResult();
if (woxQueryResult == null) {
var filteredActions = result.actions.where((action) {
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");
return;
}
WoxResultAction? activeAction = getActiveAction();
if (activeAction == null) {
if (action == null) {
Logger.instance.error(traceId, "active action is null");
return;
}
@ -257,12 +282,12 @@ class WoxLauncherController extends GetxController {
type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code,
method: WoxMsgMethodEnum.WOX_MSG_METHOD_ACTION.code,
data: {
"resultId": woxQueryResult.id,
"actionId": activeAction.id,
"resultId": result.id,
"actionId": action.id,
},
));
if (!activeAction.preventHideAfterAction) {
if (!action.preventHideAfterAction) {
hideApp(traceId);
}
hideActionPanel();

View File

@ -72,7 +72,7 @@ class WoxSettingGeneralView extends GetView<WoxSettingController> {
label: controller.tr("hotkey"),
tips: controller.tr("hotkey_tips"),
child: WoxHotkeyRecorder(
hotkey: WoxHotkey.parseHotkey(controller.woxSetting.value.mainHotkey),
hotkey: WoxHotkey.parseHotkeyFromString(controller.woxSetting.value.mainHotkey),
onHotKeyRecorded: (hotkey) {
controller.updateConfig("MainHotkey", hotkey);
},
@ -82,7 +82,7 @@ class WoxSettingGeneralView extends GetView<WoxSettingController> {
label: controller.tr("selection_hotkey"),
tips: controller.tr("selection_hotkey_tips"),
child: WoxHotkeyRecorder(
hotkey: WoxHotkey.parseHotkey(controller.woxSetting.value.selectionHotkey),
hotkey: WoxHotkey.parseHotkeyFromString(controller.woxSetting.value.selectionHotkey),
onHotKeyRecorded: (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
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.
# 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 {
result.Actions[0].IsDefault = true
result.Actions[0].Hotkey = "Enter"
}
var resultCache = &QueryResultCache{
@ -516,8 +517,26 @@ func (m *Manager) PolishResult(ctx context.Context, pluginInstance *Instance, qu
}
// store actions for ui invoke later
for actionId := range result.Actions {
var action = result.Actions[actionId]
for actionIndex := range result.Actions {
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 {
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
PreventHideAfterAction bool
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 {
@ -158,6 +162,7 @@ func (q *QueryResult) ToUI() QueryResultUI {
Icon: action.Icon,
IsDefault: action.IsDefault,
PreventHideAfterAction: action.PreventHideAfterAction,
Hotkey: action.Hotkey,
}
}),
RefreshInterval: q.RefreshInterval,
@ -186,6 +191,7 @@ type QueryResultActionUI struct {
Icon WoxImage
IsDefault bool
PreventHideAfterAction bool
Hotkey string
}
// store latest result value after query/refresh, so we can retrieve data later in action/refresh