feat(SelectionAssistant): add "Remember Window Size" functionality

- Introduced a new setting to remember the last adjusted size of the action window.
- Updated ConfigManager, SelectionService, and IPC channels to handle the new feature.
- Enhanced UI components to allow users to toggle the "Remember Size" option.
- Localized the new setting in multiple languages.
This commit is contained in:
fullex 2025-05-29 14:34:25 +08:00 committed by 亢奋猫
parent c9caa5f46b
commit a88bf104df
16 changed files with 142 additions and 22 deletions

View File

@ -189,6 +189,7 @@ export enum IpcChannel {
Selection_SetFilterMode = 'selection:set-filter-mode',
Selection_SetFilterList = 'selection:set-filter-list',
Selection_SetFollowToolbar = 'selection:set-follow-toolbar',
Selection_SetRemeberWinSize = 'selection:set-remeber-win-size',
Selection_ActionWindowClose = 'selection:action-window-close',
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',

View File

@ -20,6 +20,7 @@ export enum ConfigKeys {
SelectionAssistantEnabled = 'selectionAssistantEnabled',
SelectionAssistantTriggerMode = 'selectionAssistantTriggerMode',
SelectionAssistantFollowToolbar = 'selectionAssistantFollowToolbar',
SelectionAssistantRemeberWinSize = 'selectionAssistantRemeberWinSize',
SelectionAssistantFilterMode = 'selectionAssistantFilterMode',
SelectionAssistantFilterList = 'selectionAssistantFilterList'
}
@ -175,6 +176,14 @@ export class ConfigManager {
this.setAndNotify(ConfigKeys.SelectionAssistantFollowToolbar, value)
}
getSelectionAssistantRemeberWinSize(): boolean {
return this.get<boolean>(ConfigKeys.SelectionAssistantRemeberWinSize, false)
}
setSelectionAssistantRemeberWinSize(value: boolean) {
this.setAndNotify(ConfigKeys.SelectionAssistantRemeberWinSize, value)
}
getSelectionAssistantFilterMode(): string {
return this.get<string>(ConfigKeys.SelectionAssistantFilterMode, 'default')
}

View File

@ -60,6 +60,7 @@ export class SelectionService {
private triggerMode = 'selected'
private isFollowToolbar = true
private isRemeberWinSize = false
private filterMode = 'default'
private filterList: string[] = []
@ -86,6 +87,11 @@ export class SelectionService {
private readonly ACTION_WINDOW_WIDTH = 500
private readonly ACTION_WINDOW_HEIGHT = 400
private lastActionWindowSize: { width: number; height: number } = {
width: this.ACTION_WINDOW_WIDTH,
height: this.ACTION_WINDOW_HEIGHT
}
private constructor() {
try {
if (!SelectionHook) {
@ -140,6 +146,7 @@ export class SelectionService {
private initConfig() {
this.triggerMode = configManager.getSelectionAssistantTriggerMode()
this.isFollowToolbar = configManager.getSelectionAssistantFollowToolbar()
this.isRemeberWinSize = configManager.getSelectionAssistantRemeberWinSize()
this.filterMode = configManager.getSelectionAssistantFilterMode()
this.filterList = configManager.getSelectionAssistantFilterList()
@ -154,6 +161,17 @@ export class SelectionService {
this.isFollowToolbar = isFollowToolbar
})
configManager.subscribe(ConfigKeys.SelectionAssistantRemeberWinSize, (isRemeberWinSize: boolean) => {
this.isRemeberWinSize = isRemeberWinSize
//when off, reset the last action window size to default
if (!this.isRemeberWinSize) {
this.lastActionWindowSize = {
width: this.ACTION_WINDOW_WIDTH,
height: this.ACTION_WINDOW_HEIGHT
}
}
})
configManager.subscribe(ConfigKeys.SelectionAssistantFilterMode, (filterMode: string) => {
this.filterMode = filterMode
this.setHookGlobalFilterMode(this.filterMode, this.filterList)
@ -810,8 +828,8 @@ export class SelectionService {
*/
private createPreloadedActionWindow(): BrowserWindow {
const preloadedActionWindow = new BrowserWindow({
width: this.ACTION_WINDOW_WIDTH,
height: this.ACTION_WINDOW_HEIGHT,
width: this.isRemeberWinSize ? this.lastActionWindowSize.width : this.ACTION_WINDOW_WIDTH,
height: this.isRemeberWinSize ? this.lastActionWindowSize.height : this.ACTION_WINDOW_HEIGHT,
minWidth: 300,
minHeight: 200,
frame: false,
@ -885,6 +903,16 @@ export class SelectionService {
}
})
//remember the action window size
actionWindow.on('resized', () => {
if (this.isRemeberWinSize) {
this.lastActionWindowSize = {
width: actionWindow.getBounds().width,
height: actionWindow.getBounds().height
}
}
})
this.actionWindows.add(actionWindow)
// Asynchronously create a new preloaded window
@ -907,30 +935,58 @@ export class SelectionService {
* @param actionWindow Window to position and show
*/
private showActionWindow(actionWindow: BrowserWindow) {
let actionWindowWidth = this.ACTION_WINDOW_WIDTH
let actionWindowHeight = this.ACTION_WINDOW_HEIGHT
//if remember win size is true, use the last remembered size
if (this.isRemeberWinSize) {
actionWindowWidth = this.lastActionWindowSize.width
actionWindowHeight = this.lastActionWindowSize.height
}
//center way
if (!this.isFollowToolbar || !this.toolbarWindow) {
if (this.isRemeberWinSize) {
actionWindow.setBounds({
width: actionWindowWidth,
height: actionWindowHeight
})
}
actionWindow.show()
this.hideToolbar()
return
}
//follow toolbar
const toolbarBounds = this.toolbarWindow!.getBounds()
const display = screen.getDisplayNearestPoint({ x: toolbarBounds.x, y: toolbarBounds.y })
const workArea = display.workArea
const GAP = 6 // 6px gap from screen edges
//make sure action window is inside screen
if (actionWindowWidth > workArea.width - 2 * GAP) {
actionWindowWidth = workArea.width - 2 * GAP
}
if (actionWindowHeight > workArea.height - 2 * GAP) {
actionWindowHeight = workArea.height - 2 * GAP
}
// Calculate initial position to center action window horizontally below toolbar
let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - this.ACTION_WINDOW_WIDTH) / 2)
let posX = Math.round(toolbarBounds.x + (toolbarBounds.width - actionWindowWidth) / 2)
let posY = Math.round(toolbarBounds.y)
// Ensure action window stays within screen boundaries with a small gap
if (posX + this.ACTION_WINDOW_WIDTH > workArea.x + workArea.width) {
posX = workArea.x + workArea.width - this.ACTION_WINDOW_WIDTH - GAP
if (posX + actionWindowWidth > workArea.x + workArea.width) {
posX = workArea.x + workArea.width - actionWindowWidth - GAP
} else if (posX < workArea.x) {
posX = workArea.x + GAP
}
if (posY + this.ACTION_WINDOW_HEIGHT > workArea.y + workArea.height) {
if (posY + actionWindowHeight > workArea.y + workArea.height) {
// If window would go below screen, try to position it above toolbar
posY = workArea.y + workArea.height - this.ACTION_WINDOW_HEIGHT - GAP
posY = workArea.y + workArea.height - actionWindowHeight - GAP
} else if (posY < workArea.y) {
posY = workArea.y + GAP
}
@ -938,8 +994,8 @@ export class SelectionService {
actionWindow.setPosition(posX, posY, false)
//KEY to make window not resize
actionWindow.setBounds({
width: this.ACTION_WINDOW_WIDTH,
height: this.ACTION_WINDOW_HEIGHT,
width: actionWindowWidth,
height: actionWindowHeight,
x: posX,
y: posY
})
@ -1021,6 +1077,10 @@ export class SelectionService {
configManager.setSelectionAssistantFollowToolbar(isFollowToolbar)
})
ipcMain.handle(IpcChannel.Selection_SetRemeberWinSize, (_, isRemeberWinSize: boolean) => {
configManager.setSelectionAssistantRemeberWinSize(isRemeberWinSize)
})
ipcMain.handle(IpcChannel.Selection_SetFilterMode, (_, filterMode: string) => {
configManager.setSelectionAssistantFilterMode(filterMode)
})

View File

@ -218,6 +218,8 @@ const api = {
setTriggerMode: (triggerMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetTriggerMode, triggerMode),
setFollowToolbar: (isFollowToolbar: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetFollowToolbar, isFollowToolbar),
setRemeberWinSize: (isRemeberWinSize: boolean) =>
ipcRenderer.invoke(IpcChannel.Selection_SetRemeberWinSize, isRemeberWinSize),
setFilterMode: (filterMode: string) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterMode, filterMode),
setFilterList: (filterList: string[]) => ipcRenderer.invoke(IpcChannel.Selection_SetFilterList, filterList),
processAction: (actionItem: ActionItem) => ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem),

View File

@ -8,6 +8,7 @@ import {
setIsAutoPin,
setIsCompact,
setIsFollowToolbar,
setIsRemeberWinSize,
setSelectionEnabled,
setTriggerMode
} from '@renderer/store/selectionStore'
@ -40,6 +41,10 @@ export function useSelectionAssistant() {
dispatch(setIsFollowToolbar(isFollowToolbar))
window.api.selection.setFollowToolbar(isFollowToolbar)
},
setIsRemeberWinSize: (isRemeberWinSize: boolean) => {
dispatch(setIsRemeberWinSize(isRemeberWinSize))
window.api.selection.setRemeberWinSize(isRemeberWinSize)
},
setFilterMode: (mode: FilterMode) => {
dispatch(setFilterMode(mode))
window.api.selection.setFilterMode(mode)

View File

@ -1880,6 +1880,10 @@
"title": "Follow Toolbar",
"description": "Window position will follow the toolbar. When disabled, it will always be centered."
},
"remember_size": {
"title": "Remember Size",
"description": "Window will display at the last adjusted size during the application running"
},
"auto_close": {
"title": "Auto Close",
"description": "Automatically close the window when it's not pinned and loses focus"

View File

@ -1880,6 +1880,10 @@
"title": "ツールバーに追従",
"description": "ウィンドウ位置をツールバーに連動(無効時は中央表示)"
},
"remember_size": {
"title": "サイズを記憶",
"description": "アプリケーション実行中、ウィンドウは最後に調整されたサイズで表示されます"
},
"auto_close": {
"title": "自動閉じる",
"description": "最前面固定されていない場合、フォーカス喪失時に自動閉じる"

View File

@ -1880,6 +1880,10 @@
"title": "Следовать за панелью",
"description": "Окно будет следовать за панелью. Иначе - по центру."
},
"remember_size": {
"title": "Запомнить размер",
"description": "При отключенном режиме, окно будет восстанавливаться до последнего размера при запуске приложения"
},
"auto_close": {
"title": "Автозакрытие",
"description": "Закрывать окно при потере фокуса (если не закреплено)"

View File

@ -1880,6 +1880,10 @@
"title": "跟随工具栏",
"description": "窗口位置将跟随工具栏显示,禁用后则始终居中显示"
},
"remember_size": {
"title": "记住大小",
"description": "应用运行期间,窗口会按上次调整的大小显示"
},
"auto_close": {
"title": "自动关闭",
"description": "当窗口未置顶且失去焦点时,将自动关闭该窗口"

View File

@ -1880,6 +1880,10 @@
"title": "跟隨工具列",
"description": "視窗位置將跟隨工具列顯示,停用後則始終置中顯示"
},
"remember_size": {
"title": "記住大小",
"description": "應用運行期間,視窗會按上次調整的大小顯示"
},
"auto_close": {
"title": "自動關閉",
"description": "當視窗未置頂且失去焦點時,將自動關閉該視窗"

View File

@ -31,6 +31,7 @@ const SelectionAssistantSettings: FC = () => {
isAutoClose,
isAutoPin,
isFollowToolbar,
isRemeberWinSize,
actionItems,
actionWindowOpacity,
filterMode,
@ -41,6 +42,7 @@ const SelectionAssistantSettings: FC = () => {
setIsAutoClose,
setIsAutoPin,
setIsFollowToolbar,
setIsRemeberWinSize,
setActionWindowOpacity,
setActionItems,
setFilterMode,
@ -140,6 +142,16 @@ const SelectionAssistantSettings: FC = () => {
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.remember_size.title')}</SettingRowTitle>
<SettingDescription>{t('selection.settings.window.remember_size.description')}</SettingDescription>
</SettingLabel>
<Switch checked={isRemeberWinSize} onChange={(checked) => setIsRemeberWinSize(checked)} />
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingLabel>
<SettingRowTitle>{t('selection.settings.window.auto_close.title')}</SettingRowTitle>
@ -191,7 +203,7 @@ const SelectionAssistantSettings: FC = () => {
<SettingDescription>{t('selection.settings.advanced.filter_mode.description')}</SettingDescription>
</SettingLabel>
<Radio.Group
value={filterMode}
value={filterMode ?? 'default'}
onChange={(e) => setFilterMode(e.target.value as FilterMode)}
buttonStyle="solid">
<Radio.Button value="default">{t('selection.settings.advanced.filter_mode.default')}</Radio.Button>
@ -200,7 +212,7 @@ const SelectionAssistantSettings: FC = () => {
</Radio.Group>
</SettingRow>
{filterMode !== 'default' && (
{filterMode && filterMode !== 'default' && (
<>
<SettingDivider />
<SettingRow>

View File

@ -24,6 +24,7 @@ export const initialState: SelectionState = {
isAutoClose: false,
isAutoPin: false,
isFollowToolbar: true,
isRemeberWinSize: false,
filterMode: 'default',
filterList: [],
actionWindowOpacity: 100,
@ -52,6 +53,9 @@ const selectionSlice = createSlice({
setIsFollowToolbar: (state, action: PayloadAction<boolean>) => {
state.isFollowToolbar = action.payload
},
setIsRemeberWinSize: (state, action: PayloadAction<boolean>) => {
state.isRemeberWinSize = action.payload
},
setFilterMode: (state, action: PayloadAction<FilterMode>) => {
state.filterMode = action.payload
},
@ -74,6 +78,7 @@ export const {
setIsAutoClose,
setIsAutoPin,
setIsFollowToolbar,
setIsRemeberWinSize,
setFilterMode,
setFilterList,
setActionWindowOpacity,

View File

@ -19,6 +19,7 @@ export interface SelectionState {
isAutoClose: boolean
isAutoPin: boolean
isFollowToolbar: boolean
isRemeberWinSize: boolean
filterMode: FilterMode
filterList: string[]
actionWindowOpacity: number

View File

@ -221,10 +221,12 @@ const SelectionActionApp: FC = () => {
<WinButton type="text" icon={<X size={16} />} onClick={handleClose} className="close" />
</TitleBarButtons>
</TitleBar>
<Content ref={contentElementRef}>
{action.id == 'translate' && <ActionTranslate action={action} scrollToBottom={handleScrollToBottom} />}
{action.id != 'translate' && <ActionGeneral action={action} scrollToBottom={handleScrollToBottom} />}
</Content>
<MainContainer>
<Content ref={contentElementRef}>
{action.id == 'translate' && <ActionTranslate action={action} scrollToBottom={handleScrollToBottom} />}
{action.id != 'translate' && <ActionGeneral action={action} scrollToBottom={handleScrollToBottom} />}
</Content>
</MainContainer>
</WindowFrame>
)
}
@ -340,6 +342,14 @@ const WinButton = styled(Button)`
}
`
const MainContainer = styled.div`
display: flex;
justify-content: center;
width: 100%;
height: 100%;
overflow: auto;
`
const Content = styled.div`
display: flex;
flex-direction: column;
@ -349,7 +359,8 @@ const Content = styled.div`
font-size: 14px;
-webkit-app-region: none;
user-select: text;
width: 100%;
/* width: 100%; */
max-width: 1280px;
`
const OpacitySlider = styled.div`

View File

@ -266,13 +266,11 @@ const Container = styled.div`
const Result = styled.div`
margin-top: 4px;
width: 100%;
max-width: 960px;
`
const MenuContainer = styled.div`
display: flex;
width: 100%;
max-width: 960px;
flex-direction: row;
align-items: center;
justify-content: flex-end;
@ -309,7 +307,6 @@ const OriginalContent = styled.div`
white-space: pre-wrap;
word-break: break-word;
width: 100%;
max-width: 960px;
`
const OriginalContentCopyWrapper = styled.div`

View File

@ -155,7 +155,6 @@ const Result = styled.div`
white-space: pre-wrap;
word-break: break-word;
width: 100%;
max-width: 960px;
`
const MenuContainer = styled.div`
@ -164,7 +163,6 @@ const MenuContainer = styled.div`
flex-direction: row;
align-items: center;
justify-content: space-between;
max-width: 960px;
`
const OriginalHeader = styled.div`
@ -198,7 +196,6 @@ const OriginalContent = styled.div`
white-space: pre-wrap;
word-break: break-word;
width: 100%;
max-width: 960px;
`
const OriginalContentCopyWrapper = styled.div`