Wox/wox.ui.flutter/wox/lib/modules/launcher/wox_launcher_controller.dart

1121 lines
41 KiB
Dart

import 'dart:async';
import 'dart:io';
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';
import 'package:wox/entity/wox_setting.dart';
import 'package:wox/entity/wox_theme.dart';
import 'package:wox/entity/wox_toolbar.dart';
import 'package:wox/entity/wox_websocket_msg.dart';
import 'package:wox/enums/wox_direction_enum.dart';
import 'package:wox/enums/wox_event_device_type_enum.dart';
import 'package:wox/enums/wox_image_type_enum.dart';
import 'package:wox/enums/wox_last_query_mode_enum.dart';
import 'package:wox/enums/wox_msg_method_enum.dart';
import 'package:wox/enums/wox_msg_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_selection_type_enum.dart';
import 'package:wox/modules/setting/wox_setting_controller.dart';
import 'package:wox/utils/consts.dart';
import 'package:wox/utils/log.dart';
import 'package:wox/utils/picker.dart';
import 'package:wox/utils/wox_setting_util.dart';
import 'package:wox/utils/wox_theme_util.dart';
import 'package:wox/utils/wox_websocket_msg_util.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:wox/utils/linux_window_manager.dart';
class WoxLauncherController extends GetxController {
//query related variables
final currentQuery = PlainQuery.empty().obs;
final queryBoxFocusNode = FocusNode();
final queryBoxTextFieldController = TextEditingController();
final queryBoxScrollController = ScrollController(initialScrollOffset: 0.0);
//preview related variables
final currentPreview = WoxPreview.empty().obs;
final isShowPreviewPanel = false.obs;
// result related variables
/// The list of query results.
final results = <WoxQueryResult>[].obs;
final activeResultIndex = 0.obs;
final resultGlobalKeys = <GlobalKey>[]; // the global keys for each result item, used to calculate the position of the result item
final resultScrollerController = ScrollController(initialScrollOffset: 0.0);
final originalResults = <WoxQueryResult>[]; // the original results, used to filter and restore selection results
/// The timer to clear query results.
/// On every query changed, it will reset the timer and will clear the query results after N ms.
/// If there is no this delay mechanism, the window will flicker for fast typing.
Timer clearQueryResultsTimer = Timer(const Duration(), () => {});
final clearQueryResultDelay = 100;
// action related variables
/// The list of result actions for the active query result.
final actions = <WoxResultAction>[].obs;
final activeActionIndex = 0.obs;
final isShowActionPanel = false.obs;
final actionTextFieldController = TextEditingController();
final actionFocusNode = FocusNode();
final actionScrollerController = ScrollController(initialScrollOffset: 0.0);
/// This flag is used to control whether the user can arrow up to show history when the app is first shown.
var canArrowUpHistory = true;
final latestQueryHistories = <QueryHistory>[]; // the latest query histories
var currentQueryHistoryIndex = 0; // query history index, used to navigate query history
final Rx<WoxTheme> woxTheme = WoxThemeUtil.instance.currentTheme.obs;
var refreshCounter = 0;
var lastQueryMode = WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_PRESERVE.code;
final isInSettingView = false.obs;
var positionBeforeOpenSetting = const Offset(0, 0);
/// The icon at end of query box.
final queryIcon = QueryIconInfo.empty().obs;
/// The result of the doctor check.
var doctorCheckPassed = true;
// toolbar related variables
final toolbar = ToolbarInfo.empty().obs;
final toolbarCopyText = 'Copy'.obs;
// The timer to clean the toolbar when query changed
// on every query changed, it will reset the timer and will clear the toolbar after N ms
// If there is no this delay mechanism, the toolbar will flicker for fast typing
Timer cleanToolbarTimer = Timer(const Duration(), () => {});
final cleanToolbarDelay = 1000;
/// This flag is used to control whether the result item is selected by mouse hover.
/// This is used to prevent the result item from being selected when the mouse is just hovering over the item in the result list.
var isMouseMoved = false;
/// Triggered when received query results from the server.
void onReceivedQueryResults(String traceId, List<WoxQueryResult> receivedResults) {
if (receivedResults.isEmpty) {
return;
}
//ignore the results if the query id is not matched
if (currentQuery.value.queryId != receivedResults.first.queryId) {
return;
}
//cancel clear results timer
clearQueryResultsTimer.cancel();
//merge results
final existingQueryResults = results.where((item) => item.queryId == currentQuery.value.queryId).toList();
final finalResults = List<WoxQueryResult>.from(existingQueryResults)..addAll(receivedResults);
//group results
var finalResultsSorted = <WoxQueryResult>[];
final groups = finalResults.map((e) => e.group).toSet().toList();
groups.sort((a, b) => finalResults.where((element) => element.group == b).first.groupScore.compareTo(finalResults.where((element) => element.group == a).first.groupScore));
for (var group in groups) {
final groupResults = finalResults.where((element) => element.group == group).toList();
final groupResultsSorted = groupResults..sort((a, b) => b.score.compareTo(a.score));
if (group != "") {
finalResultsSorted.add(WoxQueryResult.empty()
..title.value = group
..isGroup = true
..score = groupResultsSorted.first.groupScore);
}
finalResultsSorted.addAll(groupResultsSorted);
}
// move default action to the first for every result
for (var element in finalResultsSorted) {
final defaultActionIndex = element.actions.indexWhere((element) => element.isDefault);
if (defaultActionIndex != -1) {
final defaultAction = element.actions[defaultActionIndex];
element.actions.removeAt(defaultActionIndex);
element.actions.insert(0, defaultAction);
}
}
results.assignAll(finalResultsSorted);
originalResults.assignAll(results);
for (var _ in results) {
resultGlobalKeys.add(GlobalKey());
}
// if current query already has results and active result is not the first one, then do not reset active result and action
// this will prevent the active result from being reset to the first one when the query results are received
if (existingQueryResults.isEmpty || activeResultIndex.value == 0) {
resetActiveResult();
resetActiveAction(traceId, "receive query results: ${currentQuery.value.queryText}");
}
resizeHeight();
}
Future<void> toggleApp(String traceId, ShowAppParams params) async {
var isVisible = await windowManager.isVisible();
if (isVisible) {
if (isInSettingView.value) {
isInSettingView.value = false;
showApp(traceId, params);
} else {
hideApp(traceId);
}
} else {
showApp(traceId, params);
}
}
Future<void> showApp(String traceId, ShowAppParams params) async {
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_INPUT.code) {
canArrowUpHistory = true;
if (lastQueryMode == WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_PRESERVE.code) {
//skip the first one, because it's the current query
currentQueryHistoryIndex = 0;
} else {
currentQueryHistoryIndex = -1;
}
}
// update some properties to latest for later use
latestQueryHistories.assignAll(params.queryHistories);
lastQueryMode = params.lastQueryMode;
if (params.selectAll) {
selectQueryBoxAllText();
}
if (params.position.type == WoxPositionTypeEnum.POSITION_TYPE_MOUSE_SCREEN.code) {
await windowManager.setPosition(Offset(params.position.x.toDouble(), params.position.y.toDouble()));
}
await windowManager.show();
if (Platform.isWindows) {
// on windows, it is somehow necessary to invoke show twice to make the window show
// otherwise, the window will not show up if it is the first time to invoke showApp
await windowManager.show();
}
await windowManager.focus();
queryBoxFocusNode.requestFocus();
WoxApi.instance.onShow();
}
Future<void> hideApp(String traceId) async {
await windowManager.hide();
//clear query box text if query type is selection or last query mode is empty
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code || lastQueryMode == WoxLastQueryModeEnum.WOX_LAST_QUERY_MODE_EMPTY.code) {
currentQuery.value = PlainQuery.emptyInput();
queryBoxTextFieldController.clear();
actionTextFieldController.clear();
await clearQueryResults();
}
await WoxApi.instance.onHide(currentQuery.value);
}
Future<void> toggleActionPanel(String traceId) async {
if (results.isEmpty) {
return;
}
if (isShowActionPanel.value) {
hideActionPanel(traceId);
} else {
showActionPanel(traceId);
}
}
bool isActionHotkey(HotKey hotkey) {
if (Platform.isMacOS) {
return WoxHotkey.equals(hotkey, WoxHotkey.parseHotkeyFromString("cmd+J"));
} else {
return WoxHotkey.equals(hotkey, WoxHotkey.parseHotkeyFromString("alt+J"));
}
}
void hideActionPanel(String traceId) {
isShowActionPanel.value = false;
actionTextFieldController.text = "";
queryBoxFocusNode.requestFocus();
resetActiveAction(traceId, "hide action panel");
resizeHeight();
}
void showActionPanel(String traceId) {
isShowActionPanel.value = true;
actionFocusNode.requestFocus();
resizeHeight();
}
WoxQueryResult? getActiveResult() {
if (activeResultIndex.value >= results.length || activeResultIndex.value < 0 || results.isEmpty) {
return null;
}
return results[activeResultIndex.value];
}
WoxResultAction? getActiveAction() {
if (actions.isEmpty || activeActionIndex.value >= actions.length || activeActionIndex.value < 0) {
return null;
}
return actions[activeActionIndex.value];
}
/// given a hotkey, find the action in the result
WoxResultAction? getActionByHotkey(WoxQueryResult? result, HotKey hotkey) {
if (result == null) {
return 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> onEnter(String traceId) async {
toolbar.value.action?.call();
}
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;
}
if (action == null) {
Logger.instance.error(traceId, "active action is null");
return;
}
var preventHideAfterAction = action.preventHideAfterAction;
Logger.instance.debug(traceId, "execute action: ${action.name}, prevent hide after action: $preventHideAfterAction");
await WoxWebsocketMsgUtil.instance.sendMessage(WoxWebsocketMsg(
requestId: const UuidV4().generate(),
traceId: traceId,
type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code,
method: WoxMsgMethodEnum.WOX_MSG_METHOD_ACTION.code,
data: {
"resultId": result.id,
"actionId": action.id,
},
));
if (!preventHideAfterAction) {
hideApp(traceId);
}
if (isShowActionPanel.value) {
hideActionPanel(traceId);
}
}
Future<void> autoCompleteQuery(String traceId) async {
var activeResult = getActiveResult();
if (activeResult == null) {
return;
}
onQueryChanged(
traceId,
PlainQuery(
queryId: const UuidV4().generate(),
queryType: WoxQueryTypeEnum.WOX_QUERY_TYPE_INPUT.code,
queryText: activeResult.title.value,
querySelection: Selection.empty(),
),
"auto complete query",
moveCursorToEnd: true,
);
}
void onQueryBoxTextChanged(String value) {
canArrowUpHistory = false;
isMouseMoved = false;
if (currentQuery.value.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code) {
// do local filter if query type is selection
filterSelectionResults(const UuidV4().generate(), value);
} else {
onQueryChanged(
const UuidV4().generate(),
PlainQuery(
queryId: const UuidV4().generate(),
queryType: WoxQueryTypeEnum.WOX_QUERY_TYPE_INPUT.code,
queryText: value,
querySelection: Selection.empty(),
),
"user input changed",
);
}
}
void filterSelectionResults(String traceId, String filterText) {
if (filterText.isEmpty) {
results.assignAll(originalResults);
} else {
var matchedResults = originalResults.where((result) {
return isFuzzyMatch(traceId, result.title.value, filterText) || isFuzzyMatch(traceId, result.subTitle.value, filterText);
}).toList();
results.assignAll(matchedResults);
}
resetActiveResult();
resetActiveAction(traceId, "filter selection results: $filterText");
}
void onQueryChanged(String traceId, PlainQuery query, String changeReason, {bool moveCursorToEnd = false}) {
Logger.instance.debug(traceId, "query changed: ${query.queryText}, reason: $changeReason");
if (query.queryId == "") {
query.queryId = const UuidV4().generate();
}
//hide setting view if query changed
if (isInSettingView.value) {
isInSettingView.value = false;
}
currentQuery.value = query;
isShowActionPanel.value = false;
if (query.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code) {
canArrowUpHistory = false;
}
if (queryBoxTextFieldController.text != query.queryText) {
queryBoxTextFieldController.text = query.queryText;
}
if (moveCursorToEnd) {
moveQueryBoxCursorToEnd();
}
updateQueryIconOnQueryChanged(traceId, query);
updateToolbarOnQueryChanged(traceId, query);
if (query.isEmpty) {
clearQueryResults();
return;
}
// delay clear results, otherwise windows height will shrink immediately,
// and then the query result is received which will expand the windows height. so it will causes window flicker
clearQueryResultsTimer.cancel();
clearQueryResultsTimer = Timer(
Duration(milliseconds: clearQueryResultDelay),
() {
clearQueryResults();
},
);
WoxWebsocketMsgUtil.instance.sendMessage(WoxWebsocketMsg(
requestId: const UuidV4().generate(),
traceId: traceId,
type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code,
method: WoxMsgMethodEnum.WOX_MSG_METHOD_QUERY.code,
data: {
"queryId": query.queryId,
"queryType": query.queryType,
"queryText": query.queryText,
"querySelection": query.querySelection.toJson(),
},
));
}
void onActionQueryBoxTextChanged(String traceId, String filteredActionName) {
// restore all actions if query is empty
var activeResult = getActiveResult();
if (activeResult == null) {
return;
}
if (filteredActionName.isEmpty) {
actions.assignAll(activeResult.actions);
updateToolbarByActiveAction(traceId);
return;
}
var filteredActions = activeResult.actions.where((element) {
return isFuzzyMatch(traceId, element.name.value, filteredActionName);
}).toList();
//if filtered actions is not changed, then return
if (filteredActions.length == actions.length && filteredActions.every((element) => actions.contains(element))) {
return;
}
actions.assignAll(filteredActions);
updateToolbarByActiveAction(traceId);
}
/// check if the query text is fuzzy match with the filter text based on the setting
bool isFuzzyMatch(String traceId, String queryText, String filterText) {
if (WoxSettingUtil.instance.currentSetting.usePinYin) {
queryText = transferChineseToPinYin(queryText).toLowerCase();
} else {
queryText = queryText.toLowerCase();
}
var score = weightedRatio(queryText, filterText.toLowerCase());
Logger.instance.debug(traceId, "calculate fuzzy match score, queryText: $queryText, filterText: $filterText, score: $score");
return score > 50;
}
void changeResultScrollPosition(String traceId, WoxEventDeviceType deviceType, WoxDirection direction) {
final prevResultIndex = activeResultIndex.value;
updateActiveResultIndex(traceId, direction);
if (results.length < MAX_LIST_VIEW_ITEM_COUNT) {
results.refresh();
return;
}
if (direction == WoxDirectionEnum.WOX_DIRECTION_DOWN.code) {
if (activeResultIndex.value < prevResultIndex) {
resultScrollerController.jumpTo(0);
} else {
bool shouldJump = deviceType == WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code
? isResultItemAtBottom(activeResultIndex.value - 1)
: !isResultItemAtBottom(results.length - 1);
if (shouldJump) {
resultScrollerController.jumpTo(resultScrollerController.offset.ceil() + WoxThemeUtil.instance.getResultItemHeight() * (activeResultIndex.value - prevResultIndex).abs());
}
}
}
if (direction == WoxDirectionEnum.WOX_DIRECTION_UP.code) {
if (activeResultIndex.value > prevResultIndex) {
resultScrollerController.jumpTo(WoxThemeUtil.instance.getResultListViewHeightByCount(results.length - MAX_LIST_VIEW_ITEM_COUNT));
} else {
bool shouldJump = deviceType == WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code ? isResultItemAtTop(activeResultIndex.value + 1) : !isResultItemAtTop(0);
if (shouldJump) {
resultScrollerController.jumpTo(resultScrollerController.offset.ceil() - WoxThemeUtil.instance.getResultItemHeight() * (activeResultIndex.value - prevResultIndex).abs());
}
}
}
results.refresh();
}
void changeActionScrollPosition(String traceId, WoxEventDeviceType deviceType, WoxDirection direction) {
updateActiveAction(traceId, direction);
actions.refresh();
}
Future<void> handleWebSocketMessage(WoxWebsocketMsg msg) async {
if (msg.method != WoxMsgMethodEnum.WOX_MSG_METHOD_QUERY.code && msg.type == WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code) {
Logger.instance.info(msg.traceId, "Received message: ${msg.method}");
}
if (msg.type == WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code) {
return handleWebSocketRequestMessage(msg);
} else if (msg.type == WoxMsgTypeEnum.WOX_MSG_TYPE_RESPONSE.code) {
return handleWebSocketResponseMessage(msg);
}
}
Future<void> handleWebSocketRequestMessage(WoxWebsocketMsg msg) async {
if (msg.method == "ToggleApp") {
toggleApp(msg.traceId, ShowAppParams.fromJson(msg.data));
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "HideApp") {
hideApp(msg.traceId);
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "ShowApp") {
showApp(msg.traceId, ShowAppParams.fromJson(msg.data));
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "ChangeQuery") {
onQueryChanged(msg.traceId, PlainQuery.fromJson(msg.data), "receive change query from wox", moveCursorToEnd: true);
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "ChangeTheme") {
final theme = WoxTheme.fromJson(msg.data);
WoxThemeUtil.instance.changeTheme(theme);
woxTheme.value = theme;
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "PickFiles") {
final pickFilesParams = FileSelectorParams.fromJson(msg.data);
final files = await FileSelector.pick(msg.traceId, pickFilesParams);
responseWoxWebsocketRequest(msg, true, files);
} else if (msg.method == "OpenSettingWindow") {
openSettingWindow(msg.traceId, SettingWindowContext.fromJson(msg.data));
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "ShowToolbarMsg") {
showToolbarMsg(msg.traceId, ToolbarMsg.fromJson(msg.data));
responseWoxWebsocketRequest(msg, true, null);
} else if (msg.method == "GetCurrentQuery") {
responseWoxWebsocketRequest(msg, true, currentQuery.value.toJson());
} else if (msg.method == "IsVisible") {
var isVisible = await windowManager.isVisible();
responseWoxWebsocketRequest(msg, true, isVisible);
}
}
Future<void> handleWebSocketResponseMessage(WoxWebsocketMsg msg) async {
if (msg.method == WoxMsgMethodEnum.WOX_MSG_METHOD_QUERY.code) {
var results = <WoxQueryResult>[];
for (var item in msg.data) {
results.add(WoxQueryResult.fromJson(item));
}
Logger.instance.info(msg.traceId, "Received websocket message: ${msg.method}, results count: ${results.length}");
onReceivedQueryResults(msg.traceId, results);
}
}
void responseWoxWebsocketRequest(WoxWebsocketMsg request, bool success, dynamic data) {
WoxWebsocketMsgUtil.instance.sendMessage(
WoxWebsocketMsg(
requestId: request.requestId,
traceId: request.traceId,
type: WoxMsgTypeEnum.WOX_MSG_TYPE_RESPONSE.code,
method: request.method,
data: data,
success: success,
),
);
}
bool isResultItemAtBottom(int index) {
RenderBox? renderBox = resultGlobalKeys[index].currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return false;
if (renderBox.localToGlobal(Offset.zero).dy.ceil() >=
WoxThemeUtil.instance.getQueryBoxHeight() + WoxThemeUtil.instance.getResultListViewHeightByCount(MAX_LIST_VIEW_ITEM_COUNT - 1)) {
return true;
}
return false;
}
bool isResultItemAtTop(int index) {
if (index < 0) {
return false;
}
RenderBox? renderBox = resultGlobalKeys[index].currentContext?.findRenderObject() as RenderBox?;
if (renderBox == null) return false;
if (renderBox.localToGlobal(Offset.zero).dy.ceil() <= WoxThemeUtil.instance.getQueryBoxHeight()) {
return true;
}
return false;
}
Future<void> clearQueryResults() async {
results.clear();
actions.clear();
toolbar.value = ToolbarInfo.empty();
isShowPreviewPanel.value = false;
isShowActionPanel.value = false;
resultGlobalKeys.clear();
await resizeHeight();
}
// select all text in query box
void selectQueryBoxAllText() {
queryBoxTextFieldController.selection = TextSelection(baseOffset: 0, extentOffset: queryBoxTextFieldController.text.length);
}
/// reset and jump active result to top of the list
void resetActiveResult() {
// reset active result index
if (results.isNotEmpty) {
if (results[0].isGroup) {
activeResultIndex.value = 1;
} else {
activeResultIndex.value = 0;
}
if (resultScrollerController.hasClients) {
resultScrollerController.jumpTo(0);
}
}
//reset preview
if (results.isNotEmpty) {
currentPreview.value = results[activeResultIndex.value].preview;
} else {
currentPreview.value = WoxPreview.empty();
}
isShowPreviewPanel.value = currentPreview.value.previewData != "";
}
Future<void> resizeHeight() async {
double resultHeight = WoxThemeUtil.instance.getResultListViewHeightByCount(results.length > 10 ? 10 : results.length);
if (isShowActionPanel.value || isShowPreviewPanel.value) {
resultHeight = WoxThemeUtil.instance.getResultListViewHeightByCount(10);
}
if (results.isNotEmpty) {
resultHeight += woxTheme.value.resultContainerPaddingTop + woxTheme.value.resultContainerPaddingBottom;
}
if (toolbar.value.isNotEmpty()) {
resultHeight += WoxThemeUtil.instance.getToolbarHeight();
}
final totalHeight = WoxThemeUtil.instance.getQueryBoxHeight() + resultHeight;
if (Platform.isWindows) {
final totalHeightFinal = totalHeight.toDouble() + (10 / PlatformDispatcher.instance.views.first.devicePixelRatio).ceil();
if (LoggerSwitch.enableSizeAndPositionLog) Logger.instance.info(const UuidV4().generate(), "Resize: window height to $totalHeightFinal");
await windowManager.setSize(Size(800, totalHeightFinal));
} else if (Platform.isLinux) {
// window manager setSize is not working on linux, so we need to implement it by ourselves
if (LoggerSwitch.enableSizeAndPositionLog) Logger.instance.info(const UuidV4().generate(), "Resize: window height to $totalHeight");
await LinuxWindowManager.instance.setSize(800, totalHeight.toDouble());
} else {
if (LoggerSwitch.enableSizeAndPositionLog) Logger.instance.info(const UuidV4().generate(), "Resize: window height to $totalHeight");
await windowManager.setSize(Size(800, totalHeight.toDouble()));
}
}
void updateActiveResultIndex(String traceId, WoxDirection woxDirection) {
if (results.isEmpty) {
return;
}
if (woxDirection == WoxDirectionEnum.WOX_DIRECTION_DOWN.code) {
// select next none group result
activeResultIndex.value++;
if (activeResultIndex.value == results.length) {
activeResultIndex.value = 0;
}
while (results[activeResultIndex.value].isGroup) {
activeResultIndex.value++;
if (activeResultIndex.value == results.length) {
activeResultIndex.value = 0;
break;
}
}
}
if (woxDirection == WoxDirectionEnum.WOX_DIRECTION_UP.code) {
// select previous none group result
activeResultIndex.value--;
if (activeResultIndex.value == -1) {
activeResultIndex.value = results.length - 1;
}
while (results[activeResultIndex.value].isGroup) {
activeResultIndex.value--;
if (activeResultIndex.value == -1) {
activeResultIndex.value = results.length - 1;
break;
}
}
}
currentPreview.value = results[activeResultIndex.value].preview;
isShowPreviewPanel.value = currentPreview.value.previewData != "";
resetActiveAction(traceId, "update active result index, direction: $woxDirection");
}
void updateActiveAction(String traceId, WoxDirection woxDirection) {
if (actions.isEmpty) {
return;
}
if (woxDirection == WoxDirectionEnum.WOX_DIRECTION_DOWN.code) {
if (activeActionIndex.value == actions.length - 1) {
activeActionIndex.value = 0;
} else {
activeActionIndex.value++;
}
}
if (woxDirection == WoxDirectionEnum.WOX_DIRECTION_UP.code) {
if (activeActionIndex.value == 0) {
activeActionIndex.value = actions.length - 1;
} else {
activeActionIndex.value--;
}
}
updateToolbarByActiveAction(traceId);
}
WoxQueryResult getQueryResultByIndex(int index) {
return results[index];
}
WoxResultAction geActionByIndex(int index) {
return actions[index];
}
GlobalKey getResultItemGlobalKeyByIndex(int index) {
return resultGlobalKeys[index];
}
bool isResultActiveByIndex(int index) {
return activeResultIndex.value == index;
}
bool isActionActiveByIndex(int index) {
return activeActionIndex.value == index;
}
void setActiveResultIndex(int index) {
activeResultIndex.value = index;
currentPreview.value = results[index].preview;
isShowPreviewPanel.value = currentPreview.value.previewData != "";
resetActiveAction(const UuidV4().generate(), "mouse hover");
results.refresh();
}
/// update active actions based on active result and reset active action index to 0
void resetActiveAction(String traceId, String reason, {bool remainIndex = false}) {
var activeQueryResult = getActiveResult();
if (activeQueryResult == null || activeQueryResult.actions.isEmpty) {
Logger.instance.info(traceId, "update active actions, reason: $reason, current active result: null");
activeActionIndex.value = -1;
actions.clear();
return;
}
Logger.instance.info(
traceId, "update active actions, reason: $reason, current active result: ${activeQueryResult.title.value}, active action: ${activeQueryResult.actions.first.name.value}");
String? previousActionName;
if (remainIndex && actions.isNotEmpty && activeActionIndex.value >= 0 && activeActionIndex.value < actions.length) {
previousActionName = actions[activeActionIndex.value].name.value;
}
final filterText = actionTextFieldController.text;
if (filterText.isNotEmpty) {
var filteredActions = activeQueryResult.actions.where((element) {
return isFuzzyMatch(traceId, element.name.value, filterText);
}).toList();
actions.assignAll(filteredActions);
} else {
actions.assignAll(activeQueryResult.actions);
}
// remain the same action index
if (remainIndex && previousActionName != null) {
final newIndex = actions.indexWhere((action) => action.name.value == previousActionName);
if (newIndex != -1) {
activeActionIndex.value = newIndex;
} else {
activeActionIndex.value = 0;
}
} else {
activeActionIndex.value = 0;
}
updateToolbarByActiveAction(traceId);
}
startRefreshSchedule() {
var isRequesting = <String, bool>{};
Timer.periodic(const Duration(milliseconds: 100), (timer) async {
var isVisible = await windowManager.isVisible();
if (!isVisible) {
return;
}
refreshCounter = refreshCounter + 100;
for (var result in results) {
if (result.refreshInterval > 0 && refreshCounter % result.refreshInterval == 0) {
if (isRequesting.containsKey(result.id)) {
continue;
} else {
isRequesting[result.id] = true;
}
final traceId = const UuidV4().generate();
final msg = WoxWebsocketMsg(
requestId: const UuidV4().generate(),
traceId: traceId,
type: WoxMsgTypeEnum.WOX_MSG_TYPE_REQUEST.code,
method: WoxMsgMethodEnum.WOX_MSG_METHOD_REFRESH.code,
data: {
"queryId": result.queryId,
"refreshableResult": WoxRefreshableResult(
resultId: result.id,
title: result.title.value,
subTitle: result.subTitle.value,
icon: result.icon.value,
preview: result.preview,
tails: result.tails,
contextData: result.contextData,
refreshInterval: result.refreshInterval,
actions: result.actions,
).toJson(),
},
);
final startTime = DateTime.now().millisecondsSinceEpoch;
WoxWebsocketMsgUtil.instance.sendMessage(msg).then((resp) {
final endTime = DateTime.now().millisecondsSinceEpoch;
if (endTime - startTime > 100) {
Logger.instance.warn(traceId, "refresh result <${result.title}> (resultId: ${result.id}) too slow, cost ${endTime - startTime} ms");
}
// check result id, because the result may be removed during the refresh
if (!results.any((element) => element.id == result.id)) {
isRequesting.remove(result.id);
Logger.instance.info(traceId, "result <${result.title}> (resultId: ${result.id}) is removed (maybe caused by new query) during refresh, skip update result");
return;
}
final refreshResult = WoxRefreshableResult.fromJson(resp);
result.title.value = refreshResult.title;
result.subTitle.value = refreshResult.subTitle;
result.icon.value = refreshResult.icon;
result.preview = refreshResult.preview;
result.tails.assignAll(refreshResult.tails);
result.actions.assignAll(refreshResult.actions);
// only update preview and toolbar when current result is active
final resultIndex = results.indexWhere((element) => element.id == result.id);
if (isResultActiveByIndex(resultIndex)) {
currentPreview.value = result.preview;
isShowPreviewPanel.value = currentPreview.value.previewData != "";
resetActiveAction(traceId, "refresh active result", remainIndex: true);
}
result.contextData = refreshResult.contextData;
result.refreshInterval = refreshResult.refreshInterval;
isRequesting.remove(result.id);
});
}
}
});
}
startDoctorCheckSchedule() {
Timer.periodic(const Duration(minutes: 1), (timer) async {
doctorCheckPassed = await WoxApi.instance.doctorCheck();
Logger.instance.debug(const UuidV4().generate(), "doctor check result: $doctorCheckPassed");
});
}
@override
void dispose() {
queryBoxFocusNode.dispose();
queryBoxTextFieldController.dispose();
resultScrollerController.dispose();
super.dispose();
}
Future<void> openSettingWindow(String traceId, SettingWindowContext context) async {
isInSettingView.value = true;
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (context.path == "/plugin/setting") {
var settingController = Get.find<WoxSettingController>();
await settingController.switchToPluginList(false);
settingController.filterPluginKeywordController.text = context.param;
settingController.filterPlugins();
settingController.setFirstFilteredPluginDetailActive();
WidgetsBinding.instance.addPostFrameCallback((_) async {
settingController.switchToPluginSettingTab();
});
}
// if user open setting window from silent query, the windows may not visible yet
var isVisible = await windowManager.isVisible();
if (!isVisible) {
await showApp(
traceId,
ShowAppParams(
queryHistories: latestQueryHistories,
lastQueryMode: lastQueryMode,
selectAll: true,
position: Position(
type: WoxPositionTypeEnum.POSITION_TYPE_LAST_LOCATION.code,
x: 0,
y: 0,
),
));
}
});
}
void showToolbarMsg(String traceId, ToolbarMsg msg) {
// cancel the timer if it is running
cleanToolbarTimer.cancel();
toolbar.value = ToolbarInfo(
text: msg.text,
icon: msg.icon,
action: toolbar.value.action,
actionName: toolbar.value.actionName,
hotkey: toolbar.value.hotkey,
);
if (msg.displaySeconds > 0) {
Future.delayed(Duration(seconds: msg.displaySeconds), () {
// only hide toolbar msg when the text is the same as the one we are showing
if (toolbar.value.text == msg.text) {
toolbar.value = ToolbarInfo(
text: "",
icon: WoxImage.empty(),
action: toolbar.value.action,
actionName: toolbar.value.actionName,
hotkey: toolbar.value.hotkey,
);
}
});
}
}
void moveQueryBoxCursorToStart() {
queryBoxTextFieldController.selection = TextSelection.fromPosition(const TextPosition(offset: 0));
if (queryBoxScrollController.hasClients) {
queryBoxScrollController.jumpTo(0);
}
}
void moveQueryBoxCursorToEnd() {
queryBoxTextFieldController.selection = TextSelection.collapsed(offset: queryBoxTextFieldController.text.length);
if (queryBoxScrollController.hasClients) {
queryBoxScrollController.jumpTo(queryBoxScrollController.position.maxScrollExtent);
}
}
void handleQueryBoxArrowUp() {
if (canArrowUpHistory) {
if (currentQueryHistoryIndex < latestQueryHistories.length - 1) {
currentQueryHistoryIndex = currentQueryHistoryIndex + 1;
var changedQuery = latestQueryHistories[currentQueryHistoryIndex].query;
if (changedQuery != null) {
onQueryChanged(const UuidV4().generate(), changedQuery, "user arrow up history");
selectQueryBoxAllText();
}
}
return;
}
changeResultScrollPosition(const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_UP.code);
}
void handleQueryBoxArrowDown() {
canArrowUpHistory = false;
changeResultScrollPosition(const UuidV4().generate(), WoxEventDeviceTypeEnum.WOX_EVENT_DEVEICE_TYPE_KEYBOARD.code, WoxDirectionEnum.WOX_DIRECTION_DOWN.code);
}
String transferChineseToPinYin(String str) {
RegExp regExp = RegExp(r'[\u4e00-\u9fa5]');
if (regExp.hasMatch(str)) {
return PinyinHelper.getPinyin(str, separator: "", format: PinyinFormat.WITHOUT_TONE);
}
return str;
}
Future<void> handleDropFiles(DropDoneDetails details) async {
Logger.instance.info(const UuidV4().generate(), "Received drop files: $details");
await windowManager.focus();
queryBoxFocusNode.requestFocus();
canArrowUpHistory = false;
PlainQuery woxChangeQuery = PlainQuery(
queryId: const UuidV4().generate(),
queryType: WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code,
queryText: "",
querySelection: Selection(type: WoxSelectionTypeEnum.WOX_SELECTION_TYPE_FILE.code, text: "", filePaths: details.files.map((e) => e.path).toList()),
);
onQueryChanged(const UuidV4().generate(), woxChangeQuery, "user drop files");
}
/// Change the query icon based on the query
Future<void> updateQueryIconOnQueryChanged(String traceId, PlainQuery query) async {
//if doctor check is not passed and query is empty, show doctor icon
if (query.isEmpty && !doctorCheckPassed) {
queryIcon.value = QueryIconInfo(
icon: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_BASE64.code, imageData: QUERY_ICON_DOCTOR_WARNING),
action: () {
onQueryChanged(traceId, PlainQuery.text("doctor "), "user click query icon");
},
);
return;
}
if (query.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_SELECTION.code) {
if (query.querySelection.type == WoxSelectionTypeEnum.WOX_SELECTION_TYPE_FILE.code) {
queryIcon.value = QueryIconInfo(
icon: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_SVG.code, imageData: QUERY_ICON_SELECTION_FILE),
);
}
if (query.querySelection.type == WoxSelectionTypeEnum.WOX_SELECTION_TYPE_TEXT.code) {
queryIcon.value = QueryIconInfo(
icon: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_SVG.code, imageData: QUERY_ICON_SELECTION_TEXT),
);
}
return;
}
if (query.queryType == WoxQueryTypeEnum.WOX_QUERY_TYPE_INPUT.code) {
var img = await WoxApi.instance.getQueryIcon(query);
queryIcon.value = QueryIconInfo(
icon: img,
);
return;
}
queryIcon.value = QueryIconInfo.empty();
}
void updateToolbarOnQueryChanged(String traceId, PlainQuery query) {
cleanToolbarTimer.cancel();
// if query is empty, we need to immediately update the toolbar
if (query.isEmpty) {
// if query is empty and doctor check is not passed, show doctor warning
if (!doctorCheckPassed) {
Logger.instance.debug(traceId, "update toolbar to doctor warning, query is empty and doctor check not passed");
toolbar.value = ToolbarInfo(
text: "Doctor check not passed",
icon: WoxImage(imageType: WoxImageTypeEnum.WOX_IMAGE_TYPE_BASE64.code, imageData: QUERY_ICON_DOCTOR_WARNING),
hotkey: "enter",
actionName: "Check",
action: () {
onQueryChanged(traceId, PlainQuery.text("doctor "), "user click query icon");
},
);
} else {
Logger.instance.debug(traceId, "update toolbar to empty because of query changed and is empty");
toolbar.value = ToolbarInfo.empty();
}
return;
}
// if query is not empty, update the toolbar after 100ms to avoid flickering
cleanToolbarTimer = Timer(Duration(milliseconds: cleanToolbarDelay), () {
Logger.instance.debug(traceId, "update toolbar to empty because of query changed");
toolbar.value = ToolbarInfo.empty();
});
}
/// Update the toolbar based on the active action
void updateToolbarByActiveAction(String traceId) {
var activeAction = getActiveAction();
if (activeAction != null) {
Logger.instance.debug(traceId, "update toolbar to active action: ${activeAction.name.value}");
// cancel the timer if it is running
cleanToolbarTimer.cancel();
// only update action and hotkey if it's different from the current one
if (toolbar.value.actionName != activeAction.name.value || toolbar.value.hotkey != activeAction.hotkey) {
toolbar.value = ToolbarInfo(
hotkey: "enter",
actionName: activeAction.name.value,
action: () {
executeAction(traceId, getActiveResult(), activeAction);
},
);
}
} else {
Logger.instance.debug(traceId, "update toolbar to empty, no active action");
toolbar.value = ToolbarInfo.empty();
}
}
}