From eff35450f6fdecf147e44e3b44bc1780e23255b6 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Sun, 22 Mar 2026 00:32:48 +0800 Subject: [PATCH] feat: folder button bulk move --- VoidLink/Base.lproj/iPad.storyboard | 47 +++++++- VoidLink/Base.lproj/iPhone.storyboard | 48 +++++++- VoidLink/Input/CustomOSC/OSCProfilesManager.m | 1 + .../Input/CustomOSC/OnScreenButtonState.h | 1 + .../Input/CustomOSC/OnScreenButtonState.m | 2 + VoidLink/Input/OnScreenWidgetView.swift | 18 +++ VoidLink/Input/StreamView.m | 1 + .../LayoutOnScreenControlsViewController.h | 4 + .../LayoutOnScreenControlsViewController.m | 15 +++ VoidLink/mul.lproj/iPad.xcstrings | 108 +++++++++++++++++- VoidLink/mul.lproj/iPhone.xcstrings | 102 +++++++++++++++++ 11 files changed, 333 insertions(+), 14 deletions(-) diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index ba59c702..b27b0c1c 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -2626,7 +2626,7 @@ - + + + + + + + + + + + + + + + + + + @@ -3100,6 +3133,8 @@ + + @@ -3279,22 +3314,22 @@ - + - + - + - + - + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index 95055c18..71075aa3 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -315,7 +315,7 @@ - + + + + + + + + + + + + + + + + + + + + + @@ -868,6 +904,8 @@ + + @@ -3134,16 +3172,16 @@ - + - + - + - + diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index dab5a2b4..895ac83e 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -450,6 +450,7 @@ static CGRect layoutViewBounds; buttonState.parentSequence = widgetView.parentSequence; buttonState.folded = widgetView.folded; buttonState.revealMode = widgetView.revealMode; + buttonState.bulkMoveEnabled = widgetView.bulkMoveEnabled; buttonState.widthFactor = [self normalizeSizeWidthFactorWith:widgetView]; buttonState.heightFactor = [self normalizeSizeHeightFactorWith:widgetView]; buttonState.componentSizeFactor = [self normalizeComponentSizeFactorWith:widgetView]; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.h b/VoidLink/Input/CustomOSC/OnScreenButtonState.h index d6d3cf56..5e54b723 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.h +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.h @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN @property NSString *alias; @property (nonatomic, assign) BOOL folded; @property (nonatomic, assign) uint8_t revealMode; +@property (nonatomic, assign) BOOL bulkMoveEnabled; @property (nonatomic, assign) int16_t sequence; @property (nonatomic, assign) int16_t parentSequence; @property NSSet *sequenceSet; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m index fed5a00b..f3f12b85 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m @@ -34,6 +34,7 @@ [encoder encodeObject:self.name forKey:@"name"]; [encoder encodeBool:self.folded forKey:@"folded"]; [encoder encodeInt32:self.revealMode forKey:@"revealMode"]; + [encoder encodeBool:self.bulkMoveEnabled forKey:@"bulkMoveEnabled"]; [encoder encodeInt32:self.sequence forKey:@"sequence"]; [encoder encodeInt32:self.parentSequence forKey:@"parentSequence"]; [encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"]; @@ -75,6 +76,7 @@ self.name = [decoder decodeObjectForKey:@"name"]; self.folded = [decoder containsValueForKey:@"folded"] ? [decoder decodeBoolForKey:@"folded"] : false; self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : coexist; + self.bulkMoveEnabled = [decoder containsValueForKey:@"bulkMoveEnabled"] ? [decoder decodeBoolForKey:@"bulkMoveEnabled"] : true; self.sequence = [decoder containsValueForKey:@"sequence"] ? [decoder decodeInt32ForKey:@"sequence"] : -1; self.parentSequence = [decoder containsValueForKey:@"parentSequence"] ? [decoder decodeInt32ForKey:@"parentSequence"] : -1; self.sequenceSet = [decoder containsValueForKey:@"sequenceSet"] ? [decoder decodeObjectForKey:@"sequenceSet"] : [NSSet set]; diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 407c9517..cca1164b 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -291,6 +291,7 @@ import SVGKit @objc public var folded: Bool = false @objc public var persistedFolded: Bool = false @objc public var revealMode: RevealMode = .coexist + @objc public var bulkMoveEnabled: Bool = false @objc public var sequenceSet: Set = Set() @objc public var parentSequence: Int16 = -1 private weak var capturer: OnScreenWidgetView? @@ -1973,6 +1974,11 @@ import SVGKit relocatedDuringStreaming = true // center = currentLocation; //NSLog("x coord: %f, y coord: %f", self.frame.origin.x, self.frame.origin.y) + + if isFolder, bulkMoveEnabled { + self.moveSubWidgetsInBatch(by: CGVector(dx: offsetX, dy: offsetY)) + } + if OnScreenWidgetView.editMode { guidelineDelegate?.updateGuidelinesForOnScreenWidget(self) } @@ -2877,6 +2883,18 @@ import SVGKit return sequence+1 } + @objc func moveSubWidgetsInBatch(by vector:CGVector) { + guard isFolder else {return} + for sequence in self.sequenceSet { + guard let widget = OnScreenWidgetView.mapping[sequence] else {return} + widget.storedCenter = CGPoint(x: widget.storedCenter.x+vector.dx, y: widget.storedCenter.y+vector.dy) + if !widget.isHidden { + widget.center = CGPoint(x: widget.center.x+vector.dx, y: widget.center.y+vector.dy) + } + widget.relocatedDuringStreaming = true + } + } + @objc static var enableFolderAnimation:Bool = true private static func setCollection(hidden:Bool, for folder:OnScreenWidgetView, exception:OnScreenWidgetView? = nil, recursive:Bool = false) { guard folder.isFolder else {return} diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 003ef639..920a8da8 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -640,6 +640,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; widgetView.folded = buttonState.folded; widgetView.persistedFolded = buttonState.folded; widgetView.revealMode = buttonState.revealMode; + widgetView.bulkMoveEnabled = buttonState.bulkMoveEnabled; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place widgetView.widthFactor = buttonState.widthFactor; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h index 12edc7b8..fb4212fc 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.h @@ -133,6 +133,10 @@ NS_ASSUME_NONNULL_BEGIN @property (weak, nonatomic) IBOutlet UISegmentedControl *collectedWidgetsSelector; @property (weak, nonatomic) IBOutlet UISegmentedControl *revealModeSelector; +@property (weak, nonatomic) IBOutlet UIStackView *bulkMoveStack; +@property (weak, nonatomic) IBOutlet UISegmentedControl *bulkMoveSelector; + + @property (weak, nonatomic) IBOutlet UIStackView *widgetPanelStack; diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index 5f9b8340..20c3e7c3 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -171,6 +171,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { widgetView.parentSequence = buttonState.parentSequence; widgetView.folded = buttonState.folded; widgetView.revealMode = buttonState.revealMode; + widgetView.bulkMoveEnabled = buttonState.bulkMoveEnabled; widgetView.guidelineDelegate = (id)self; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place @@ -816,6 +817,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { OnScreenWidgetView* newWidget = [[OnScreenWidgetView alloc] initWithCmdString:widgetInitParams[@"cmdString"] buttonLabel:widgetInitParams[@"buttonLabel"] shape:widgetInitParams[@"shape"] profile:profile]; //reconstruct widgetView newWidget.sequence = widget.sequence; newWidget.revealMode = widget.revealMode; + newWidget.bulkMoveEnabled = widget.bulkMoveEnabled; newWidget.guidelineDelegate = (id)self; newWidget.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place newWidget.widthFactor = widget.widthFactor; @@ -1136,6 +1138,9 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.collectedWidgetsSelector setSelectedSegmentIndex:selectedWidgetView.folded ? 1 :0]; [self.revealModeSelector setSelectedSegmentIndex:selectedWidgetView.revealMode]; [OnScreenWidgetView setWithFolded:selectedWidgetView.folded for:selectedWidgetView]; + + self.bulkMoveStack.hidden = !selectedWidgetView.isFolder; + [self.bulkMoveSelector setSelectedSegmentIndex:selectedWidgetView.bulkMoveEnabled]; [self autoFitStack:self.widgetPanelStack]; @@ -1305,6 +1310,12 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { } } +- (void)bulkMoveChanged:(UISegmentedControl* )sender{ + if(self->selectedWidgetView != nil && self->widgetViewSelected){ + selectedWidgetView.bulkMoveEnabled = sender.selectedSegmentIndex == 1; + } +} + - (void)revealModeChanged:(UISegmentedControl* )sender{ if(self->selectedWidgetView != nil && self->widgetViewSelected){ selectedWidgetView.revealMode = sender.selectedSegmentIndex; @@ -1627,6 +1638,10 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.revealModeSelector addTarget:self action:@selector(revealModeChanged:) forControlEvents:(UIControlEventValueChanged)]; [self.revealModeSelector setTitleTextAttributes:whiteFontAttributes forState:UIControlStateNormal]; self.collectedWidgetsStack.hidden = YES; + + [self.bulkMoveSelector addTarget:self action:@selector(bulkMoveChanged:) forControlEvents:(UIControlEventValueChanged)]; + [self.bulkMoveSelector setTitleTextAttributes:whiteFontAttributes forState:UIControlStateNormal]; + self.bulkMoveStack.hidden = YES; if([self isIPhone]){ [self.vibrationStyleSelector addTarget:self action:@selector(vibrationStyleChanged:) forControlEvents:(UIControlEventValueChanged)]; diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index 0d3d34ad..121f086b 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -397,7 +397,7 @@ }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "非線性因子" } } @@ -1597,6 +1597,66 @@ } } }, + "bNW-R2-UuH.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; bNW-R2-UuH.segmentTitles[0] = \"disabled\"; ObjectID = \"bNW-R2-UuH\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "disabled" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + } + } + }, + "bNW-R2-UuH.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; bNW-R2-UuH.segmentTitles[1] = \"enabled\"; ObjectID = \"bNW-R2-UuH\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "enabled" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用" + } + } + } + }, "BQH-nQ-IyM.text" : { "comment" : "Class = \"UILabel\"; text = \"Reverse Hold Button\"; ObjectID = \"BQH-nQ-IyM\";", "extractionState" : "extracted_with_value", @@ -1669,7 +1729,7 @@ }, "zh-Hans" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "指定软键盘高度" } }, @@ -3524,6 +3584,36 @@ } } }, + "Jeb-fE-sCm.text" : { + "comment" : "Class = \"UILabel\"; text = \" Bulk move \"; ObjectID = \"Jeb-fE-sCm\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " Bulk move " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批量移动" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批次移動" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批次移動" + } + } + } + }, "JTl-un-QJJ.text" : { "comment" : "Class = \"UILabel\"; text = \"Emulated Controller Type\"; ObjectID = \"JTl-un-QJJ\";", "extractionState" : "extracted_with_value", @@ -3878,7 +3968,7 @@ }, "zh-HK" : { "stringUnit" : { - "state" : "needs_review", + "state" : "translated", "value" : "串流位元速率" } } @@ -5878,6 +5968,18 @@ } } }, + "wrg-n4-F5w.text" : { + "comment" : "Class = \"UILabel\"; text = \" \"; ObjectID = \"wrg-n4-F5w\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " " + } + } + } + }, "WWl-77-twB.text" : { "comment" : "Class = \"UILabel\"; text = \"SDR Performance Workaround\"; ObjectID = \"WWl-77-twB\";", "extractionState" : "extracted_with_value", diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index 8c90eed4..682b66a1 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -2299,6 +2299,66 @@ } } }, + "ful-j0-kgc.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; ful-j0-kgc.segmentTitles[0] = \"disabled\"; ObjectID = \"ful-j0-kgc\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "disabled" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "关闭" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "關閉" + } + } + } + }, + "ful-j0-kgc.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; ful-j0-kgc.segmentTitles[1] = \"enabled\"; ObjectID = \"ful-j0-kgc\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "enabled" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "啟用" + } + } + } + }, "GbY-52-itv.text" : { "comment" : "Class = \"UILabel\"; text = \"Local Volume\"; ObjectID = \"GbY-52-itv\";", "extractionState" : "extracted_with_value", @@ -4057,6 +4117,36 @@ } } }, + "P9b-Th-Ybc.text" : { + "comment" : "Class = \"UILabel\"; text = \" Bulk move \"; ObjectID = \"P9b-Th-Ybc\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " Bulk move " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批量移动" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批次移動" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "needs_review", + "value" : " 批次移動" + } + } + } + }, "PaG-9w-YxB.text" : { "comment" : "Class = \"UILabel\"; text = \"Mute in Background\"; ObjectID = \"PaG-9w-YxB\";", "extractionState" : "extracted_with_value", @@ -4735,6 +4825,18 @@ } } }, + "QtA-AD-aJw.text" : { + "comment" : "Class = \"UILabel\"; text = \" \"; ObjectID = \"QtA-AD-aJw\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " " + } + } + } + }, "r8X-jt-JfH.text" : { "comment" : "Class = \"UILabel\"; text = \"Frames to Buffer\"; ObjectID = \"r8X-jt-JfH\";", "extractionState" : "extracted_with_value",