diff --git a/Easydict/Swift/Feature/Configuration/Configuration+Extension.swift b/Easydict/Swift/Feature/Configuration/Configuration+Extension.swift index fa81e440c2..a3431cf1f1 100644 --- a/Easydict/Swift/Feature/Configuration/Configuration+Extension.swift +++ b/Easydict/Swift/Feature/Configuration/Configuration+Extension.swift @@ -75,3 +75,15 @@ extension MyConfiguration { Defaults[windowConfigurationKey(key, windowType: windowType, defaultValue: true)] } } + +// MARK: Input Field Collapsed State + +extension MyConfiguration { + func inputFieldCollapsed(windowType: EZWindowType) -> Bool { + Defaults[.inputFieldCollapsed(for: windowType)] + } + + func setInputFieldCollapsed(_ collapsed: Bool, windowType: EZWindowType) { + Defaults[.inputFieldCollapsed(for: windowType)] = collapsed + } +} diff --git a/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift b/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift index 78cbb950e0..a7ac1fbbac 100644 --- a/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift +++ b/Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift @@ -261,6 +261,11 @@ extension Defaults.Keys { let key = "EZConfiguration_kWindowFrameKey_\(windowType)" return .init(key, default: .zero) } + + static func inputFieldCollapsed(for windowType: EZWindowType) -> Key { + let key = "EZConfiguration_kInputFieldCollapsedKey_\(windowType.rawValue)" + return .init(key, default: false) + } } // MARK: - EZQueryTextType + Defaults.Serializable diff --git a/Easydict/objc/ViewController/View/QueryView/EZQueryView.h b/Easydict/objc/ViewController/View/QueryView/EZQueryView.h index de81412016..aa43828827 100644 --- a/Easydict/objc/ViewController/View/QueryView/EZQueryView.h +++ b/Easydict/objc/ViewController/View/QueryView/EZQueryView.h @@ -33,6 +33,9 @@ static NSTimeInterval const EZDelayDetectTextLanguageInterval = 1.0; @property (nonatomic, assign) BOOL clearButtonHidden; @property (nonatomic, assign) BOOL isTypingChinese; +/// Whether the input text area is collapsed, only the bottom button bar is visible. +@property (nonatomic, assign) BOOL collapsed; + @property (nonatomic, copy) void (^enterActionBlock)(NSString *text); @property (nonatomic, copy) void (^playAudioBlock)(NSString *text); @@ -44,6 +47,9 @@ static NSTimeInterval const EZDelayDetectTextLanguageInterval = 1.0; @property (nonatomic, copy) void (^updateInputTextBlock)(NSString *text, CGFloat queryViewHeight); @property (nonatomic, copy) void (^selectedLanguageBlock)(EZLanguage language); +/// Called when user toggles the collapsed state, by clicking arrow button or typing in collapsed state. +@property (nonatomic, copy) void (^toggleCollapsedBlock)(BOOL collapsed); + - (CGFloat)heightOfQueryView; diff --git a/Easydict/objc/ViewController/View/QueryView/EZQueryView.m b/Easydict/objc/ViewController/View/QueryView/EZQueryView.m index 01b3508acf..0ce3c1d552 100644 --- a/Easydict/objc/ViewController/View/QueryView/EZQueryView.m +++ b/Easydict/objc/ViewController/View/QueryView/EZQueryView.m @@ -17,6 +17,7 @@ #import "EZSchemeParser.h" #import "EZCopyButton.h" #import "NSImage+EZSymbolmage.h" +#import "NSObject+EZDarkMode.h" /// Debounce delay before auto querying while the user keeps typing. /// Kept relatively long since each query is heavy, especially for @@ -30,6 +31,7 @@ @interface EZQueryView () @property (nonatomic, strong) EZDetectLanguageButton *detectButton; @property (nonatomic, strong) EZHoverButton *clearButton; +@property (nonatomic, strong) EZHoverButton *arrowButton; @property (nonatomic, strong) NSTextField *alertTextField; @property (nonatomic, assign) CGFloat textViewMinHeight; @@ -39,6 +41,11 @@ @interface EZQueryView () @property (nonatomic, assign) NSTimeInterval lastRecordTimestamp; @property (nonatomic, strong) EZSchemeParser *schemeParser; +/// Loading view is placed in the input text area when expanded, and in the button bar when collapsed. +@property (nonatomic, copy) NSArray *loadingViewExpandedConstraints; +@property (nonatomic, copy) NSArray *loadingViewCollapsedConstraints; +/// Keep loading view right of detect button in collapsed state, only active when detect button is visible. +@property (nonatomic, strong) MASConstraint *loadingViewAfterDetectButtonConstraint; @property (nonatomic, strong) id fontSizeObserver; @end @@ -124,12 +131,6 @@ - (void)setup { [self addSubview:loadingAnimationView]; self.loadingAnimationView = loadingAnimationView; - [self.loadingAnimationView mas_makeConstraints:^(MASConstraintMaker *make) { - make.top.equalTo(textView).offset(5); - make.left.equalTo(textView).offset(10); - make.height.mas_equalTo(30); - }]; - EZAudioButton *audioButton = [[EZAudioButton alloc] init]; [self addSubview:audioButton]; self.audioButton = audioButton; @@ -205,6 +206,45 @@ - (void)setup { self.clearBlock(self.copiedText); } }]; + + EZHoverButton *arrowButton = [[EZHoverButton alloc] init]; + [self addSubview:arrowButton]; + self.arrowButton = arrowButton; + arrowButton.mas_key = @"queryView_arrowButton"; + + [self executeOnAppearanceChange:^(EZQueryView *view, BOOL isDarkMode) { + [view updateArrowButton]; + }]; + + [arrowButton setClickBlock:^(EZButton *_Nonnull button) { + mm_strongify(self); + [self toggleCollapsed]; + }]; + + [self setupLoadingAnimationViewConstraints]; +} + +/// Do not remake loading view constraints, it would remove the internal width constraint of loading view. +- (void)setupLoadingAnimationViewConstraints { + self.loadingViewExpandedConstraints = [self.loadingAnimationView mas_makeConstraints:^(MASConstraintMaker *make) { + make.top.equalTo(self.textView).offset(5); + make.left.equalTo(self.textView).offset(10); + make.height.mas_equalTo(30); + }]; + + // Place loading view right after the left buttons of button bar. + CGFloat leftPadding = 6; + self.loadingViewCollapsedConstraints = [self.loadingAnimationView mas_makeConstraints:^(MASConstraintMaker *make) { + make.left.greaterThanOrEqualTo(self.textCopyButton.mas_right).offset(leftPadding); + make.left.equalTo(self.textCopyButton.mas_right).offset(leftPadding).priorityLow(); + make.centerY.height.equalTo(self.textCopyButton); + }]; + + [self.loadingAnimationView mas_makeConstraints:^(MASConstraintMaker *make) { + self.loadingViewAfterDetectButtonConstraint = make.left.greaterThanOrEqualTo(self.detectButton.mas_right).offset(leftPadding); + }]; + + [self updateLoadingAnimationViewConstraints]; } - (NSTextField *)alertTextField { @@ -230,6 +270,9 @@ - (NSTextField *)alertTextField { #pragma mark - Public Methods - (CGFloat)heightOfQueryView { + if (self.collapsed) { + return EZQueryViewExceptInputViewHeight; + } return [self heightOfTextView] + EZQueryViewExceptInputViewHeight; } @@ -266,7 +309,7 @@ - (void)setAlertTextHidden:(BOOL)hidden { self.textView.editable = hidden; }); - self.alertTextField.hidden = hidden; + self.alertTextField.hidden = hidden || self.collapsed; self.detectButton.showAutoLanguage = NO; [self updateDetectButton]; } @@ -297,19 +340,32 @@ - (void)updateConstraints { [self.scrollView mas_remakeConstraints:^(MASConstraintMaker *make) { - make.top.left.right.inset(0); + make.left.right.inset(0); + // Do not pin top in collapsed state, avoid conflicting with audio button bottom constraint when cell is taller than button bar. + if (!self.collapsed) { + make.top.inset(0); + } // Add a padding to audio button, avoid making users feel that there is still text below that has not been fully displayed. make.bottom.equalTo(self.audioButton.mas_top).offset(-EZAudioButtonInputViewTopPadding_4); - CGFloat textViewHeight = [self heightOfTextView]; - make.height.mas_greaterThanOrEqualTo(textViewHeight); + if (self.collapsed) { + make.height.mas_equalTo(0); + } else { + CGFloat textViewHeight = [self heightOfTextView]; + make.height.mas_greaterThanOrEqualTo(textViewHeight); + } }]; - [self.clearButton mas_remakeConstraints:^(MASConstraintMaker *make) { + [self.arrowButton mas_remakeConstraints:^(MASConstraintMaker *make) { make.right.bottom.equalTo(self).offset(-4); make.width.height.mas_equalTo(24); }]; + [self.clearButton mas_remakeConstraints:^(MASConstraintMaker *make) { + make.right.equalTo(self.arrowButton.mas_left).offset(-2); + make.bottom.width.height.equalTo(self.arrowButton); + }]; + [super updateConstraints]; } @@ -338,6 +394,16 @@ - (void)setQueryModel:(EZQueryModel *)model { [self updateButtonsDisplayState:queryText]; } +- (void)setCollapsed:(BOOL)collapsed { + _collapsed = collapsed; + + self.scrollView.hidden = collapsed; + self.alertTextField.hidden = collapsed || self.alertText.length == 0; + [self updateLoadingAnimationViewConstraints]; + [self updateArrowButton]; + [self setNeedsUpdateConstraints:YES]; +} + - (void)setAssociatedWindowType:(EZWindowType)windowType { [super setAssociatedWindowType:windowType]; @@ -501,6 +567,11 @@ - (void)textDidChange:(NSNotification *)notification { self.queryModel.actionType = EZActionTypeInputQuery; self.queryModel.needDetectLanguage = YES; + // Expand input text area when user types in collapsed state, otherwise the typed text is invisible. + if (self.collapsed) { + [self toggleCollapsed]; + } + // textView.string has been changed, we don't need to update it again. [self updateInputText:nil]; @@ -572,8 +643,7 @@ - (void)updateInputText:(nullable NSString *)text { [self updateButtonsDisplayState:text]; if (self.updateInputTextBlock) { - CGFloat textViewHeight = [self heightOfTextView]; - self.updateInputTextBlock(text, textViewHeight + EZQueryViewExceptInputViewHeight); + self.updateInputTextBlock(text, [self heightOfQueryView]); } } @@ -621,12 +691,49 @@ - (void)updatePlaceholderTextField { self.alertTextField.hidden = hidden; } +- (void)toggleCollapsed { + self.collapsed = !self.collapsed; + MMLogInfo(@"toggle query view collapsed: %@", @(self.collapsed)); + + if (self.toggleCollapsedBlock) { + self.toggleCollapsedBlock(self.collapsed); + } +} + +/// Move loading view to the button bar while collapsed, right after the left buttons, so loading progress is visible without expanding input text area. +- (void)updateLoadingAnimationViewConstraints { + NSArray *activeConstraints = self.collapsed ? self.loadingViewCollapsedConstraints : self.loadingViewExpandedConstraints; + NSArray *inactiveConstraints = self.collapsed ? self.loadingViewExpandedConstraints : self.loadingViewCollapsedConstraints; + for (MASConstraint *constraint in inactiveConstraints) { + [constraint deactivate]; + } + for (MASConstraint *constraint in activeConstraints) { + [constraint activate]; + } + + BOOL isDetectButtonVisible = self.detectButton.showAutoLanguage || ![self.detectButton.detectedLanguage isEqualToString:EZLanguageAuto]; + if (self.collapsed && isDetectButtonVisible) { + [self.loadingViewAfterDetectButtonConstraint activate]; + } else { + [self.loadingViewAfterDetectButtonConstraint deactivate]; + } +} + +- (void)updateArrowButton { + self.arrowButton.toolTip = self.collapsed ? NSLocalizedString(@"show", nil) : NSLocalizedString(@"hide", nil); + + NSImage *baseImage = self.collapsed ? [NSImage imageNamed:@"arrow-left"] : [NSImage imageNamed:@"arrow-down"]; + NSColor *tintColor = self.arrowButton.isDarkMode ? [NSColor ez_imageTintDarkColor] : [NSColor ez_imageTintLightColor]; + self.arrowButton.image = [baseImage imageWithTintColor:tintColor]; +} + - (void)updateDetectButton { // If user has designated source language, there is no meaning to detect language. self.detectButton.enabled = !self.queryModel.hasUserSourceLanguage; self.detectButton.showAutoLanguage = self.queryModel.showAutoLanguage; self.detectButton.detectedLanguage = self.queryModel.detectedLanguage; + [self updateLoadingAnimationViewConstraints]; CGFloat height = 20; self.detectButton.cornerRadius = height / 2; diff --git a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m index f3a76851b3..8c15740916 100644 --- a/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m +++ b/Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m @@ -85,6 +85,9 @@ @interface EZBaseQueryViewController () `, + 切换时更新行高与窗口高度。 +- 仅划词翻译(`EZActionTypeAutoSelectQuery`、`EZActionTypeShortcutQuery`,以及 PopClip、系统“服务” + 等经 URL scheme 调用的 `EZActionTypeInvokeQuery`)读取并保存折叠状态; + 输入翻译、OCR、剪贴板等其他动作一律展开,期间手动折叠只在当前会话生效、不写入偏好。 + `EZBaseQueryViewController` 在 `updateActionType:` / `updateQueryTextAndParagraphStyle:actionType:` + 中按动作类型应用状态;`EZWindowManager` 在计算窗口位置前先更新动作类型,避免窗口高度变化导致 + 顶部位置偏移。 +- 折叠状态下用户直接输入或粘贴时自动展开,避免输入内容不可见;此时动作类型已变为输入查询, + 不会覆盖已保存的划词折叠偏好。 +- 修复:新窗口带入选中文本时,`updateInputText:` 按文本高度上报行高,导致折叠的输入区块被撑高且 + 按钮错位;改为上报 `heightOfQueryView`,并在折叠时不再固定滚动视图顶部,避免与按钮栏约束冲突。 +- 修复(Review 反馈):OCR 加载动画是 `EZQueryView` 的子视图并锚定在文本视图上,折叠时不会随 + 滚动视图隐藏,其计时器还会反复将其设为可见,导致点状动画覆盖底部按钮或溢出到下一单元格。 + `startOCRImage:actionType:autoQuery:` 在开始加载前先按动作类型展开输入框;`EZQueryView` 记录 + 加载状态,折叠时停止动画,加载仍在进行时展开则恢复动画。 +- 调整(用户反馈):OCR 不再为显示加载动画而展开折叠的输入框。`EZQueryView` 为加载视图维护两组 + 约束:展开时锚定在文本视图左上角,折叠时位于按钮栏中清除按钮左侧并与箭头按钮垂直居中, + `setCollapsed:` 切换约束;不使用 `mas_remakeConstraints`,以免移除加载视图内部的宽度约束。 + OCR 完成后仍由 `updateQueryTextAndParagraphStyle:actionType:` 按动作类型应用折叠状态。 +- 调整(用户反馈):OCR 完成后也保持折叠。新增 `isOCRActionType:`(`EZActionTypeOCRQuery`、 + `EZActionTypeScreenshotOCR`、`EZActionTypePasteboardOCR`),`updateInputFieldCollapsedWithActionType:` + 对 OCR 动作不改变当前折叠状态(既不展开也不读取偏好),手动切换仍只在当前会话生效。 +- 调整(用户反馈):折叠时加载视图改为位于按钮栏左侧按钮之后:默认紧跟复制按钮,语言检测按钮 + 可见时(`showAutoLanguage` 或检测语言非 auto)通过单独约束排在其右侧;`updateDetectButton` + 更新检测语言后同步该约束。 +- 修复(Review 反馈):结果区右键 “Query in App” 命中当前窗口或已固定窗口时直接调用 + `startQueryText:actionType:`,不经过折叠状态同步,`EZActionTypeInvokeQuery` 不会恢复已保存的 + 折叠状态;互换翻译语言以 `EZActionTypeInputQuery` 重新查询时,折叠的输入框也不会展开。 + 新增 `switchActionType:`,在 `startQueryText:actionType:` 与 `toggleTranslationLanguages` 中使用, + 仅在动作类型变化时应用折叠状态,同类型重新查询(重试、切换语言、点击结果中的单词)保留当前会话 + 中手动切换的状态。 +- 修复(合并 `dev` 后的 Review 反馈):`dev` 中 #1327 移除了 `updateQueryCellWithAnimation:`, + 行高改为立即提交、仅窗口动画;折叠切换的两处调用改为 `updateQueryCell`。 + +### 设计意图 + +沿用结果区块的箭头交互和既有窗口高度更新链路,只在输入视图与查询控制器之间增加折叠状态, +不改变窗口配置开关的语义。按窗口类型保存与其他窗口级布局偏好保持一致。 + +### 验证 + +- `xcodebuild build -workspace Easydict.xcworkspace -scheme Easydict -derivedDataPath `: + 默认配置在 actool 的 `.icon` 图标导出阶段因本机环境失败(`Icon export exited with status 255`), + 与本次改动无关;附加 `'EXCLUDED_SOURCE_FILE_NAMES=*.icon' ASSETCATALOG_COMPILER_APPICON_NAME=AppIcon` + 后构建成功,改动文件无新增警告。 +- `git diff --check`:通过。 +- 手动检查:用户在本地构建中发现新窗口折叠区块被撑高的问题,已修复并重新构建成功,待用户复测。 +- OCR 加载动画修复:同一构建命令(附加上述图标参数)构建成功;`git diff --check` 通过; + 未进行手动 UI 验证。 +- OCR 加载视图移至折叠按钮栏:同一构建命令(附加上述图标参数)构建成功,改动文件无新增警告; + `git diff --check` 通过;未进行手动 UI 验证。 +- OCR 保持折叠状态:同一构建命令(附加上述图标参数)构建成功,改动文件无新增警告; + `git diff --check` 通过;未进行手动 UI 验证。 +- 加载视图移至左侧按钮之后:同一构建命令(附加上述图标参数)构建成功,改动文件无新增警告; + `git diff --check` 通过;未进行手动 UI 验证。 +- Query in App / 互换语言修复:同一构建命令(附加上述图标参数)构建成功;`git diff --check` 通过; + 未进行手动 UI 验证。 +- 合并 `dev` 后替换已移除的 `updateQueryCellWithAnimation:`:默认构建命令构建成功(无需附加图标参数); + `git diff --check` 通过;未进行手动 UI 验证。 + +### 受影响文件 + +- `Easydict/Swift/Feature/Configuration/Configuration+Extension.swift` +- `Easydict/Swift/Feature/Configuration/Defaults.Keys+Extension.swift` +- `Easydict/objc/ViewController/View/QueryView/EZQueryView.h` +- `Easydict/objc/ViewController/View/QueryView/EZQueryView.m` +- `Easydict/objc/ViewController/Window/BaseQueryWindow/EZBaseQueryViewController.m` +- `Easydict/objc/ViewController/Window/WindowManager/EZWindowManager.m` + +### 后续事项 + +- None