From ac6c688054d12ca30ed236b0cab583912d6cede6 Mon Sep 17 00:00:00 2001 From: True Zhuanjia Date: Sat, 31 Jan 2026 20:59:24 +0800 Subject: [PATCH] feat: "folder" onscreen button --- VoidLink/Base.lproj/iPad.storyboard | 4 +- VoidLink/Base.lproj/iPhone.storyboard | 64 ++++- VoidLink/Input/CustomOSC/OSCProfile.h | 1 + VoidLink/Input/CustomOSC/OSCProfile.m | 3 + VoidLink/Input/CustomOSC/OSCProfilesManager.m | 10 +- .../Input/CustomOSC/OnScreenButtonState.h | 3 +- .../Input/CustomOSC/OnScreenButtonState.m | 4 +- VoidLink/Input/OnScreenWidgetView.swift | 227 ++++++++++++++---- VoidLink/Input/StreamView.m | 23 +- .../LayoutOnScreenControlsViewController.m | 46 +++- VoidLink/mul.lproj/iPad.xcstrings | 116 ++++++++- VoidLink/mul.lproj/iPhone.xcstrings | 180 ++++++++++++++ 12 files changed, 607 insertions(+), 74 deletions(-) diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index 1bad48d1..cd6cae05 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -2888,8 +2888,8 @@ - - + + diff --git a/VoidLink/Base.lproj/iPhone.storyboard b/VoidLink/Base.lproj/iPhone.storyboard index 612c6bc6..c5440cf5 100644 --- a/VoidLink/Base.lproj/iPhone.storyboard +++ b/VoidLink/Base.lproj/iPhone.storyboard @@ -1,9 +1,9 @@ - + - + @@ -156,7 +156,7 @@ - + @@ -315,7 +315,7 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -813,7 +862,7 @@ - + @@ -823,6 +872,8 @@ + + @@ -838,6 +889,7 @@ + @@ -883,7 +935,7 @@ - + diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index 3edc2b26..e8b567fe 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -30,6 +30,7 @@ typedef NS_ENUM(NSInteger, MapGyroTo) { @property NSString *name; @property NSMutableArray *buttonStatesEncoded; +@property (nonatomic, assign) int16_t unfoldedExclusiveFolderSequence; @property BOOL isSelected; @property (nonatomic, assign) MapGyroTo mapGyroTo; @property (nonatomic, assign) bool yawPitchToRightStick; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.m b/VoidLink/Input/CustomOSC/OSCProfile.m index f402ab14..38ef717f 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -31,6 +31,7 @@ - (void) encodeWithCoder:(NSCoder*)encoder { [encoder encodeObject:self.name forKey:@"name"]; [encoder encodeObject:self.buttonStatesEncoded forKey:@"buttonStates"]; + [encoder encodeInt32:self.unfoldedExclusiveFolderSequence forKey:@"unfoldedExclusiveFolderSequence"]; [encoder encodeBool:self.isSelected forKey:@"isSelected"]; [encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"]; [encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"]; @@ -73,6 +74,7 @@ [NSData class], nil] forKey:@"buttonStates"]; + self.unfoldedExclusiveFolderSequence = [decoder containsValueForKey:@"unfoldedExclusiveFolderSequence"] ? [decoder decodeInt32ForKey:@"unfoldedExclusiveFolderSequence"] : -1; self.isSelected = [decoder decodeBoolForKey:@"isSelected"]; self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse; self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true; @@ -131,6 +133,7 @@ OSCProfile *copy = [[[self class] allocWithZone:zone] init]; copy.name = [self.name mutableCopy]; // NSString → NSMutableString copy.buttonStatesEncoded = [[NSMutableArray alloc] initWithArray:self.buttonStatesEncoded copyItems:YES]; + copy.unfoldedExclusiveFolderSequence = self.unfoldedExclusiveFolderSequence; copy.isSelected = self.isSelected; copy.mapGyroTo = self.mapGyroTo; copy.yawPitchToRightStick = self.yawPitchToRightStick; diff --git a/VoidLink/Input/CustomOSC/OSCProfilesManager.m b/VoidLink/Input/CustomOSC/OSCProfilesManager.m index 0c68ea27..a55af653 100644 --- a/VoidLink/Input/CustomOSC/OSCProfilesManager.m +++ b/VoidLink/Input/CustomOSC/OSCProfilesManager.m @@ -291,6 +291,7 @@ static CGRect layoutViewBounds; */ OSCProfile *selectedProfile = [self getSelectedProfile]; selectedProfile.buttonStatesEncoded = buttonStatesEncoded; + selectedProfile.unfoldedExclusiveFolderSequence = OnScreenWidgetView.unfoldedExclusiveFolderSequence; [self replaceSelectedProfileWith:selectedProfile overwriteDefault:NO]; return true; } @@ -389,10 +390,14 @@ static CGRect layoutViewBounds; // save on-screen widget views (keyboard & mouse command) as buttonstate: _widgetSizeTransition = keepWidgetSize; for(OnScreenWidgetView* widgetView in OnScreenWidgetViews){ - CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.center]; + CGPoint normalizedPosition = [self normalizeWidgetPosition:widgetView.storedCenter]; OnScreenButtonState *buttonState = [[OnScreenButtonState alloc] initWithButtonName:widgetView.cmdString buttonType:CustomOnScreenWidget andPosition:normalizedPosition]; buttonState.alias = widgetView.widgetLabel; buttonState.sequence = widgetView.sequence; + buttonState.sequenceSet = widgetView.sequenceSet; + buttonState.parentSequence = widgetView.parentSequence; + buttonState.folded = widgetView.folded; + buttonState.revealMode = widgetView.revealMode; buttonState.widthFactor = [self normalizeSizeWidthFactorWith:widgetView]; buttonState.heightFactor = [self normalizeSizeHeightFactorWith:widgetView]; buttonState.componentSizeFactor = [self normalizeComponentSizeFactorWith:widgetView]; @@ -421,7 +426,6 @@ static CGRect layoutViewBounds; NSData *buttonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; [buttonStatesEncoded addObject: buttonStateEncoded]; - } return buttonStatesEncoded; } @@ -477,7 +481,7 @@ static CGRect layoutViewBounds; - (OnScreenButtonState *)unarchiveButtonStateEncoded:(NSData *)data { OnScreenButtonState* buttonState; - buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], nil] + buttonState = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObjects:[NSString class], [OnScreenButtonState class], [NSSet class], nil] fromData:data error:nil]; return buttonState; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.h b/VoidLink/Input/CustomOSC/OnScreenButtonState.h index e2c2a3c0..d6d3cf56 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.h +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.h @@ -27,7 +27,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL folded; @property (nonatomic, assign) uint8_t revealMode; @property (nonatomic, assign) int16_t sequence; -@property NSMutableSet *sequenceSet; +@property (nonatomic, assign) int16_t parentSequence; +@property NSSet *sequenceSet; @property CGPoint position; @property (nonatomic, assign) BOOL isHidden; @property (nonatomic, assign) uint8_t buttonMode; diff --git a/VoidLink/Input/CustomOSC/OnScreenButtonState.m b/VoidLink/Input/CustomOSC/OnScreenButtonState.m index 080d3762..fed5a00b 100644 --- a/VoidLink/Input/CustomOSC/OnScreenButtonState.m +++ b/VoidLink/Input/CustomOSC/OnScreenButtonState.m @@ -35,6 +35,7 @@ [encoder encodeBool:self.folded forKey:@"folded"]; [encoder encodeInt32:self.revealMode forKey:@"revealMode"]; [encoder encodeInt32:self.sequence forKey:@"sequence"]; + [encoder encodeInt32:self.parentSequence forKey:@"parentSequence"]; [encoder encodeObject:self.sequenceSet forKey:@"sequenceSet"]; [encoder encodeObject:self.alias forKey:@"alias"]; [encoder encodeInt:self.widgetType forKey:@"buttonType"]; // keep original key @@ -73,8 +74,9 @@ if (self = [super init]) { self.name = [decoder decodeObjectForKey:@"name"]; self.folded = [decoder containsValueForKey:@"folded"] ? [decoder decodeBoolForKey:@"folded"] : false; - self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : exclusive; + self.revealMode = [decoder containsValueForKey:@"revealMode"] ? [decoder decodeInt32ForKey:@"revealMode"] : coexist; 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]; self.alias = [decoder decodeObjectForKey:@"alias"]; self.widgetType = [decoder decodeIntForKey:@"buttonType"]; diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 08f43606..07b8bd72 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -10,7 +10,18 @@ import UIKit import SVGKit @objc class OnScreenWidgetView: UIView { - + @objc public static var mapping: [Int16:OnScreenWidgetView] = [:] + @objc public static func set(widget:OnScreenWidgetView, for key:Int16) { + mapping[key] = widget + } + @objc public static func removeWidgetFromMappings(key:Int16) { + mapping.removeValue(forKey: key) + } + @objc public static func clearMappings() { + mapping.removeAll() + } + @objc public static var unfoldedExclusiveFolderSequence:Int16 = -1 + @objc public weak var guidelineDelegate: OnScreenWidgetGuidelineUpdateDelegate? @objc protocol OnScreenWidgetGuidelineUpdateDelegate: AnyObject { func updateGuidelinesForOnScreenWidget(_ sender: Any) @@ -263,8 +274,9 @@ import SVGKit private var tickFlag: UInt8 = 0 @objc public var folded: Bool = false - @objc public var revealMode: RevealMode = .exclusive + @objc public var revealMode: RevealMode = .coexist @objc public var sequenceSet: Set = Set() + @objc public var parentSequence: Int16 = -1 private weak var capturer: OnScreenWidgetView? @objc init(cmdString: String, buttonLabel: String, shape:String, profile:OSCProfile) { @@ -1415,17 +1427,21 @@ import SVGKit } } - private func handleFingerUpOrSlideout() { + + private func handleFingerUpOrSlideout(leaveFunctionalButtonAlone: Bool = false, event: UIEvent? = nil) { + print("handleFingerUpOrSlideout triggered, \(self.widgetLabel) \(CACurrentMediaTime())") if autoTapInterval < OnScreenWidgetView.MinAutotapInterval { - handleButtonUp() + handleButtonUp(leaveFunctionalButtonAlone:leaveFunctionalButtonAlone, event:event) } else{ self.autoTapTimer?.pause() - self.handleButtonUp() + self.handleButtonUp(leaveFunctionalButtonAlone: leaveFunctionalButtonAlone) } } private func handleButtonDown() { + if !self.isUserInteractionEnabled {return} + if !OnScreenWidgetView.editMode, !self.functionalButtonString.isEmpty {self.handleFunctionalButtonDown()} if !OnScreenWidgetView.editMode {self.sendComboButtonsDownEvent(comboStrings: self.comboButtonStrings)} @@ -1460,7 +1476,8 @@ import SVGKit } } - private func handleButtonUp() { + private func handleButtonUp(leaveFunctionalButtonAlone:Bool = false, event: UIEvent? = nil) { + if !self.isUserInteractionEnabled {return} if !OnScreenWidgetView.editMode {self.sendComboButtonsUpEvent(comboStrings: self.comboButtonStrings)} if !OnScreenWidgetView.editMode && !self.motionControlButtonString.isEmpty{ @@ -1470,7 +1487,7 @@ import SVGKit self.buttonUpVisualEffect() if !OnScreenWidgetView.editMode && !self.functionalButtonString.isEmpty{ - self.handleFunctionalButtonUp() + if !leaveFunctionalButtonAlone {self.handleFunctionalButtonUp(event:event)} } // legacy keyboard button combo connected by "+" @@ -1817,9 +1834,9 @@ import SVGKit if(self.tapToToggleFlag) {self.handleTapDownOrSlidein()} else {self.handleFingerUpOrSlideout()} self.tapToToggleFlag = !self.tapToToggleFlag - case .slideToToggle: + case .slideToToggle where !self.isFolder: self.handleButtonSliding(touches: touches) - case .slideAndHold: + case .slideAndHold where !self.isFolder: self.handleButtonSliding(touches: touches) default: self.handleTapDownOrSlidein() @@ -1843,11 +1860,11 @@ import SVGKit capturer = nil currentLocation = touch.location(in: superview) forEachWidget { (widget) in + guard widget != self else {return} if widget.isFolder { - if self.frame.intersects(widget.frame), !self.sequenceSet.contains(widget.sequence) { + if isLocation(currentLocation, in: widget), !self.sequenceSet.contains(widget.sequence) { // self.layer.borderColor = UIColor.systemYellow.cgColor widget.highlightBorder(highlighted: true) - if widget == self {return} capturer = widget return } @@ -1856,7 +1873,13 @@ import SVGKit } } else { - if self.buttonMode == .movable {currentLocation = touch.location(in: superview)} + if self.buttonMode == .movable { + currentLocation = touch.location(in: superview) + for sequence in self.sequenceSet { + let widget = OnScreenWidgetView.mapping[sequence] + if widget?.isHidden == true {widget?.center = currentLocation} + } + } else {return} } @@ -1872,7 +1895,10 @@ import SVGKit let outOfBoundsX = center.x+offsetX >= (self.superview?.bounds.width)! || center.x+offsetX < 0 let outOfBoundsY = center.y+offsetY >= (self.superview?.bounds.height)! || center.y+offsetY < 0 - if firstTouchMoved {center = CGPoint(x: outOfBoundsX ? center.x : center.x+offsetX, y: outOfBoundsY ? center.y : center.y+offsetY)} + if firstTouchMoved { + center = CGPoint(x: outOfBoundsX ? center.x : center.x+offsetX, y: outOfBoundsY ? center.y : center.y+offsetY) + storedCenter = center + } latestTouchLocation = currentLocation @@ -1919,14 +1945,17 @@ import SVGKit } } - private func handleFingerUpAfterSliding(touches: Set) { + private func handleFingerUpAfterSliding(touches: Set, event: UIEvent? = nil) { + // a special touchUp handling for .slideToToggle for touch in touches { self.forEachWidget(){ widget in setLock.lock() let captured = widget.capturedTouches.contains(touch) setLock.unlock() if !captured || widget.buttonMode == .regular {return} - widget.handleFingerUpOrSlideout() + let needReleaseButton = (isLocation(touch.location(in: superview), in: widget) + || widget.buttonMode == .slideAndHold) + if needReleaseButton {widget.handleFingerUpOrSlideout(event: event)} setLock.lock() widget.capturedTouches.remove(touch) setLock.unlock() @@ -1953,7 +1982,10 @@ import SVGKit let locationInSuperView = touch.location(in: self.superview) self.forEachWidget{ widget in if widget.widgetType != WidgetTypeEnum.button {return} - let isSlidableButton = widget.buttonMode == .slideToToggle || widget.buttonMode == .slideAndHold + let isSlidableButton = (widget.buttonMode == .slideToToggle + || widget.buttonMode == .slideAndHold + || (widget.buttonMode == .movable && widget != self) + ) if isLocation(locationInSuperView, in: widget){ setLock.lock() let captured = widget.capturedTouches.contains(touch) @@ -1972,8 +2004,8 @@ import SVGKit setLock.unlock() if !captured || !isSlidableButton {return} // print("UIButton: \(widget.buttonLabel) out test, \(widget.touchPadString), \(CACurrentMediaTime())") - if(widget.buttonMode == .slideToToggle){ - widget.handleFingerUpOrSlideout() + if(widget.buttonMode == .slideToToggle || widget.buttonMode == .movable){ + widget.handleFingerUpOrSlideout(leaveFunctionalButtonAlone: widget.isFuncationalButton) setLock.lock() widget.capturedTouches.remove(touch) setLock.unlock() @@ -2271,6 +2303,10 @@ import SVGKit private func handleFunctionalButtonDown(){ switch self.functionalButtonString { + case "FOLDER": + if self.buttonMode != .slideAndHold {break} + self.folded = false + OnScreenWidgetView.set(folded: false, for: self) case "ABSTCHDRAG": let mouseButton = CommandManager.mouseButtonMappings[Set(self.comboButtonStrings).intersection(CommandManager.mouseButtonMappings.keys).first ?? "MLEFT"] ?? BUTTON_LEFT print("mouseButton \(mouseButton)"); @@ -2292,10 +2328,16 @@ import SVGKit private var singleTouchEnabled:Bool = true - private func handleFunctionalButtonUp(){ + private func handleFunctionalButtonUp(event: UIEvent? = nil){ + print("handleFunctionalButtonUp \(CACurrentMediaTime())") if buttonMode == .movable { - if moveableButtonLongPressed() {return} + if moveableButtonLongPressed() && !UITouchUtil.touches(in: self, from: event).isEmpty {return} + print("handleFunctionalButtonUp movable, \(moveableButtonLongPressed()) \(CACurrentMediaTime())") + switch self.functionalButtonString { + // case "FOLDER": + // self.folded = !self.folded + // OnScreenWidgetView.set(folded: self.folded, for: self) case "NOSINGLETOUCH": if !self.isPencilProEnabled() {break} singleTouchEnabled = !singleTouchEnabled @@ -2306,6 +2348,9 @@ import SVGKit } } switch self.functionalButtonString { + case "FOLDER": + self.folded = !self.folded + OnScreenWidgetView.set(folded: self.folded, for: self) case "SETTINGS": self.functionalButtonDelegate?.expandSettingsView() case "TOOLBOX": @@ -2512,8 +2557,9 @@ import SVGKit } if !OnScreenWidgetView.editMode && !self.cmdString.contains("+") && !self.comboButtonStrings.isEmpty { // if the command(keystring contains "+", it's a legacy multi-key command + // print("self.comboButtonStrings \(self.comboButtonStrings),\(CACurrentMediaTime())") if self.buttonMode == .slideToToggle || self.buttonMode == .slideAndHold { - self.handleFingerUpAfterSliding(touches: touches) + self.handleFingerUpAfterSliding(touches: touches, event: event) // .slideAndHold has to be released at touchesEnded, don't deal with it here. setLock.lock() self.capturedTouches.minus(touches) setLock.unlock() @@ -2532,11 +2578,10 @@ import SVGKit if firstTouchMoved {undoRelocation()} if let capturer = capturer { capturer.highlightBorder(highlighted: false) - guard !self.sequenceSet.contains(capturer.sequence) else { return } - OnScreenWidgetView.put(widget: self, into: capturer) - self.isHidden = capturer.folded + OnScreenWidgetView.putWidget(self, into: capturer) } } + capturer = nil guard let superview = superview else { return } @@ -2570,7 +2615,10 @@ import SVGKit } } - if self.buttonMode != .tapToToggle {self.handleFingerUpOrSlideout()} + if (self.buttonMode != .tapToToggle + && self.buttonMode != .slideToToggle + && self.buttonMode != .slideAndHold + && self == touches.first?.view) {self.handleFingerUpOrSlideout(event:event)} } @objc public func setAutoTapIntervalByText(str: String){ @@ -2685,44 +2733,137 @@ import SVGKit return sequence+1 } - @objc static func setCollection(hidden:Bool, for folder:OnScreenWidgetView) { + private static func setCollection(hidden:Bool, for folder:OnScreenWidgetView, exception:OnScreenWidgetView? = nil, recursive:Bool = false) { guard folder.isFolder else {return} + // guard folder.folded != hidden else { return } folder.folded = hidden folder.setupAtrributedText() if hidden { folder.forEachWidget{ widget in - if widget == folder {return} - if folder.sequenceSet.contains(widget.sequence){ - DispatchQueue.main.async {widget.isHidden = hidden} - if widget.isFolder { - // OnScreenWidgetView.setCollection(hidden: hidden, for: widget) + guard widget != folder else {return} + if folder.sequenceSet.contains(widget.sequence), widget != exception{ + DispatchQueue.main.async { + widget.isUserInteractionEnabled = false + UIView.animate(withDuration: folder.buttonMode == .slideAndHold ? 0.05 : 0.15, animations: { + widget.center = folder.center + },completion: { finished in + widget.isUserInteractionEnabled = !folder.folded + widget.center = folder.folded ? folder.storedCenter : widget.storedCenter + widget.isHidden = folder.folded + }) } } } } else{ folder.forEachWidget{ widget in - if widget == folder {return} - if folder.sequenceSet.contains(widget.sequence){ - DispatchQueue.main.async {widget.isHidden = hidden} + guard widget != folder else {return} + if folder.sequenceSet.contains(widget.sequence), widget != exception{ + DispatchQueue.main.async { + widget.center = folder.storedCenter + widget.isUserInteractionEnabled = false + widget.isHidden = false + UIView.animate(withDuration: folder.buttonMode == .slideAndHold ? 0.05 : 0.15, animations: { + widget.center = widget.storedCenter + },completion: { finished in + widget.isUserInteractionEnabled = !folder.folded + widget.center = folder.folded ? folder.storedCenter : widget.storedCenter + widget.isHidden = folder.folded + }) + } } } - if folder.revealMode == .exclusive { - folder.forEachWidget{ widget in - if widget == folder {return} - if widget.isFolder, !widget.sequenceSet.contains(folder.sequence) { - OnScreenWidgetView.setCollection(hidden: true, for: widget) - } + } + if recursive { + folder.forEachWidget{ widget in + guard widget != folder else {return} + guard widget.isFolder, widget != exception else {return} + if folder.sequenceSet.contains(widget.sequence){ + OnScreenWidgetView.setCollection(hidden: hidden, for: widget, exception: exception, recursive: true) } } } } - private static func put(widget:OnScreenWidgetView, into folder:OnScreenWidgetView){ - widget.forEachWidget{ otherWidget in - otherWidget.sequenceSet.remove(widget.sequence) + @objc static func set(folded:Bool, for folder:OnScreenWidgetView) { // folder综合逻辑 + guard folder.isFolder else {return} + setCollection(hidden: folded, for: folder) + if !folded, folder.revealMode == .exclusive { + OnScreenWidgetView.unfoldedExclusiveFolderSequence = folder.sequence + let currentRootFolder = OnScreenWidgetView.getRootFolder(of: folder) ?? folder + var offshootRootFolders:Set = Set() + folder.forEachWidget{ widget in + guard folder != widget else {return} + let rootFolder = getRootFolder(of: widget) + guard let rootFolder = rootFolder else {return} + offshootRootFolders.insert(rootFolder) + } + offshootRootFolders.remove(currentRootFolder) + + guard !folder.sequenceSet.isEmpty else {return} + for folder in offshootRootFolders { + setCollection(hidden: true, for: folder, recursive: true) + } + + guard currentRootFolder != folder else {return} + setCollection(hidden: true, for:currentRootFolder, exception: folder, recursive: true) } + } + + private static func getRootFolder(of widget:OnScreenWidgetView) -> OnScreenWidgetView?{ + var widgetRef:OnScreenWidgetView? = widget + var parentFolder:OnScreenWidgetView? = OnScreenWidgetView.mapping[widgetRef?.parentSequence ?? -1] + repeat { + widgetRef = parentFolder + parentFolder = OnScreenWidgetView.mapping[widgetRef?.parentSequence ?? -1] + } while parentFolder != nil + return widgetRef + } + + private static func getParentFolders(of widget: OnScreenWidgetView) -> Set { + var parents = Set() + var current = widget + while let parent = OnScreenWidgetView.mapping[current.parentSequence] { + parents.insert(parent) + current = parent + } + return parents + } + + private static func putWidget(_ widget:OnScreenWidgetView, into folder:OnScreenWidgetView){ + guard !OnScreenWidgetView.getParentFolders(of: folder).contains(widget) else { return } + let parentFolder = OnScreenWidgetView.mapping[widget.parentSequence] + if parentFolder != nil { + parentFolder?.sequenceSet.remove(widget.sequence) + } + widget.parentSequence = folder.sequence folder.sequenceSet.insert(widget.sequence) + widget.isHidden = folder.folded + if folder.buttonMode == .slideAndHold, widget.isFuncationalButton { + widget.buttonMode = .slideToToggle + } + } + + @objc static func setFree(widget:OnScreenWidgetView){ + let parentFolder = OnScreenWidgetView.mapping[widget.parentSequence] + if parentFolder != nil { + parentFolder?.sequenceSet.remove(widget.sequence) + } + widget.parentSequence = -1 + } + + @objc static func restoreFoldedStates(){ + var unfoldedExclusiveFolder: OnScreenWidgetView? + if unfoldedExclusiveFolderSequence != -1 { + unfoldedExclusiveFolder = OnScreenWidgetView.mapping[unfoldedExclusiveFolderSequence] + if(unfoldedExclusiveFolder != nil){ + OnScreenWidgetView.set(folded: false, for: unfoldedExclusiveFolder!) + } + } + + for widget in OnScreenWidgetView.mapping.values { + OnScreenWidgetView.setCollection(hidden: widget.folded, for: widget, exception: unfoldedExclusiveFolder) + } } override func didMoveToSuperview() { diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index bb897c18..5e047c6c 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -467,6 +467,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; - (void) clearOnScreenWidgets{ OnScreenWidgetView.isTweakingHighlight = false; + [OnScreenWidgetView clearMappings]; for (UIView *subview in self->_streamFrameTopLayerView.subviews) { // 检查子视图是否是特定类型的实例 if ([subview isKindOfClass:[OnScreenWidgetView class]]) { @@ -503,7 +504,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } } - NSLog(@"relocatedWidgetDict.allKeys.count %d, %f", relocatedWidgetDict.allKeys.count, CACurrentMediaTime()); + // NSLog(@"relocatedWidgetDict.allKeys.count %d, %f", relocatedWidgetDict.allKeys.count, CACurrentMediaTime()); if(relocatedWidgetDict.allKeys.count == 0) return; @@ -515,7 +516,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; OnScreenButtonState *newButtonState = [oscProfileMan unarchiveButtonStateEncoded:buttonStateEncoded]; if([relocatedWidgetDict.allKeys containsObject:@(newButtonState.sequence)]){ OnScreenWidgetView* widget = [relocatedWidgetDict objectForKey:@(newButtonState.sequence)]; - newButtonState.position = [oscProfileMan normalizeWidgetPosition:widget.center]; + newButtonState.position = [oscProfileMan normalizeWidgetPosition:widget.storedCenter]; NSData *newButtonStateEncoded = [NSKeyedArchiver archivedDataWithRootObject:newButtonState requiringSecureCoding:YES error:nil]; newProfile.buttonStatesEncoded[i] = newButtonStateEncoded; } @@ -560,11 +561,11 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; widgetView.sequence = buttonState.sequence == -1 ? i : buttonState.sequence; if(!sequenceGenerated) sequenceGenerated = buttonState.sequence == -1; if(!hasMovableWidget) hasMovableWidget = buttonState.buttonMode == movable; - if(sequenceGenerated){ - NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime()); - buttonState.sequence = widgetView.sequence; - oscProfile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; - } + [OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence]; + widgetView.sequenceSet = buttonState.sequenceSet; + widgetView.parentSequence = buttonState.parentSequence; + widgetView.folded = buttonState.folded; + widgetView.revealMode = buttonState.revealMode; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place widgetView.widthFactor = buttonState.widthFactor; @@ -602,6 +603,12 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; [widgetView tweakHighlightAlphaWithAlpha:buttonState.highlightAlpha]; [widgetView setupAutoTapTimer]; [widgetView setupInertialScrollerWithFps:self->settings.framerate.intValue]; + + if(sequenceGenerated){ + // NSLog(@"widgetView.sequence %d %f", widgetView.sequence, CACurrentMediaTime()); + buttonState.sequence = widgetView.sequence; + oscProfile.buttonStatesEncoded[i] = [NSKeyedArchiver archivedDataWithRootObject:buttonState requiringSecureCoding:YES error:nil]; + } } else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true; } @@ -636,6 +643,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } } + [OnScreenWidgetView restoreFoldedStates]; + NSLog(@"hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime()); // legacy widgets if(!OnScreenWidgetView.editMode && (hasLegacyWidget || !reload)){ diff --git a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m index f1acb078..3847630f 100644 --- a/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m +++ b/VoidLink/ViewControllers/CustomOSCViewControl/LayoutOnScreenControlsViewController.m @@ -128,6 +128,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { } - (void)clearOnScreenWidgets{ + [OnScreenWidgetView clearMappings]; for (UIView *subview in self.view.subviews) { if ([subview isKindOfClass:[OnScreenWidgetView class]]) { [subview removeFromSuperview]; @@ -156,7 +157,14 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { NSLog(@"reloadOnScreenWidgets name %@", buttonState.name); if(buttonState.widgetType == CustomOnScreenWidget){ OnScreenWidgetView* widgetView = [[OnScreenWidgetView alloc] initWithCmdString:buttonState.name buttonLabel:buttonState.alias shape:buttonState.widgetShape profile:oscProfile]; //reconstruct widgetView + widgetView.sequence = buttonState.sequence == -1 ? ++sequence : buttonState.sequence; + [OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence]; + widgetView.sequenceSet = buttonState.sequenceSet; + widgetView.parentSequence = buttonState.parentSequence; + widgetView.folded = buttonState.folded; + widgetView.revealMode = buttonState.revealMode; + widgetView.guidelineDelegate = (id)self; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place widgetView.widthFactor = buttonState.widthFactor; @@ -194,6 +202,13 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { } else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true; } + + for(OnScreenWidgetView* widget in self.onScreenWidgetViews){ + [widget accessWidgetAttributes]; + // if(widget.isFolder) [OnScreenWidgetView setWithFolded:widget.folded for:widget]; + } + + [OnScreenWidgetView restoreFoldedStates]; NSLog(@"reloadOnScreenWidgets hasLegacyWidget %d %f", hasLegacyWidget, CACurrentMediaTime()); if(hasLegacyWidget) [self reloadLegacyOnScreenControls:oscProfile]; else{ @@ -789,6 +804,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { OSCProfile* profile = [[OSCProfilesManager sharedManager:CGRectZero] getSelectedProfile]; 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.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; @@ -824,7 +840,12 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [newWidget setLocationWithPosition:CGPointMake(90, 130)]; newWidget.sequence = [newWidget getAvailableSequence]; } - else [newWidget setLocationWithPosition:widget.center]; + else{ + [newWidget setLocationWithPosition:widget.center]; + newWidget.sequenceSet = [widget.sequenceSet mutableCopy]; + newWidget.parentSequence = widget.parentSequence; + } + [OnScreenWidgetView setWithWidget:newWidget for:newWidget.sequence]; newWidget.sizeReference = widget.sizeReference; [newWidget resizeWidgetView]; // resize must be called after relocation [newWidget adjustTransparencyWithAlpha:widget.backgroundAlpha tweakBorderAlpha:NO]; @@ -849,6 +870,7 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.view insertSubview:widgetView belowSubview:self.widgetPanelStack]; widgetView.hidden = true; widgetView.sequence = [widgetView getAvailableSequence]; + [OnScreenWidgetView setWithWidget:widgetView for:widgetView.sequence]; widgetView.guidelineDelegate = (id)self; widgetView.translatesAutoresizingMaskIntoConstraints = NO; // weird but this is mandatory, or you will find no key views added to the right place [self.onScreenWidgetViews addObject:widgetView]; @@ -1088,17 +1110,21 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { self.mouseButtonDownSelector.selectedSegmentIndex = selectedWidgetView.mouseButtonAction; self.buttonModeStack.hidden = selectedWidgetView.widgetType != WidgetTypeEnumButton; - [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton forSegmentAtIndex:0]; - [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton forSegmentAtIndex:1]; + // bool slideToToggleEnabled = !selectedWidgetView.isFuncationalButton || selectedWidgetView.isFolder; + // [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton forSegmentAtIndex:0]; + [self.buttonModeSelector setEnabled:true forSegmentAtIndex:slideToToggle]; + bool slideAndHoldEnabled = !selectedWidgetView.isFuncationalButton || selectedWidgetView.isFolder; + [self.buttonModeSelector setEnabled:slideAndHoldEnabled forSegmentAtIndex:slideAndHold]; + [self.buttonModeSelector setEnabled:!selectedWidgetView.isFolder forSegmentAtIndex:regular]; [self.buttonModeSelector setEnabled:!selectedWidgetView.isFuncationalButton || [selectedWidgetView.functionalButtonString containsString:@"ABSTCHDRAG"] - forSegmentAtIndex:3]; + forSegmentAtIndex:tapToToggle]; [self.buttonModeSelector setSelectedSegmentIndex:selectedWidgetView.buttonMode]; self.collectedWidgetsStack.hidden = !selectedWidgetView.isFolder; [self.collectedWidgetsSelector setSelectedSegmentIndex:selectedWidgetView.folded ? 1 :0]; [self.revealModeSelector setSelectedSegmentIndex:selectedWidgetView.revealMode]; - [OnScreenWidgetView setCollectionWithHidden:selectedWidgetView.folded for:selectedWidgetView]; + [OnScreenWidgetView setWithFolded:selectedWidgetView.folded for:selectedWidgetView]; [self autoFitStack:self.widgetPanelStack]; @@ -1264,14 +1290,14 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { - (void)collectionHiddenChanged:(UISegmentedControl* )sender{ if(self->selectedWidgetView != nil && self->widgetViewSelected){ - [OnScreenWidgetView setCollectionWithHidden:sender.selectedSegmentIndex==1 for:selectedWidgetView]; + [OnScreenWidgetView setWithFolded:sender.selectedSegmentIndex==1 for:selectedWidgetView]; } } - (void)revealModeChanged:(UISegmentedControl* )sender{ if(self->selectedWidgetView != nil && self->widgetViewSelected){ selectedWidgetView.revealMode = sender.selectedSegmentIndex; - [OnScreenWidgetView setCollectionWithHidden:selectedWidgetView.folded for:selectedWidgetView]; + [OnScreenWidgetView setWithFolded:selectedWidgetView.folded for:selectedWidgetView]; } } @@ -1772,6 +1798,10 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { [self.layoutOSC updateGuidelinesForOnScreenWidget:widget]; [self.view bringSubviewToFront:widget]; trashCanButton.tintColor = trashCanButton.titleLabel.textColor = [self layerIsOverlappingWithTrashcanButton:widget.layer] ? [UIColor redColor] : trashCanStoryBoardColor; + + if(trashCanButton.tintColor == [UIColor redColor] && selectedWidgetView != nil){ + [OnScreenWidgetView setFreeWithWidget:selectedWidgetView]; + } self.undoButton.alpha = 1.0; } @@ -1861,6 +1891,8 @@ typedef NS_ENUM(NSUInteger, DecelerationRateSliderMode) { if(!isToolbarHidden && self->selectedWidgetView != nil && [self layerIsOverlappingWithTrashcanButton:selectedWidgetView.layer]){ + [OnScreenWidgetView setFreeWithWidget:selectedWidgetView]; + [OnScreenWidgetView removeWidgetFromMappingsWithKey:selectedWidgetView.sequence]; [self->selectedWidgetView removeFromSuperview]; [self.onScreenWidgetViews removeObject:self->selectedWidgetView]; } diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index c55917b0..03887591 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -4048,6 +4048,24 @@ "state" : "new", "value" : " Reveal mode " } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展开模式" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展開模式" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展開模式" + } } } }, @@ -4756,6 +4774,24 @@ "state" : "new", "value" : "coexist" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } } } }, @@ -4768,6 +4804,24 @@ "state" : "new", "value" : "exclusive" } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } } } }, @@ -5048,25 +5102,61 @@ } }, "sch-0e-nfa.segmentTitles[0]" : { - "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[0] = \"display\"; ObjectID = \"sch-0e-nfa\";", + "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[0] = \"unfold\"; ObjectID = \"sch-0e-nfa\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "display" + "value" : "unfold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展开" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開" } } } }, "sch-0e-nfa.segmentTitles[1]" : { - "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[1] = \"hide\"; ObjectID = \"sch-0e-nfa\";", + "comment" : "Class = \"UISegmentedControl\"; sch-0e-nfa.segmentTitles[1] = \"fold\"; ObjectID = \"sch-0e-nfa\";", "extractionState" : "extracted_with_value", "localizations" : { "en" : { "stringUnit" : { "state" : "new", - "value" : "hide" + "value" : "fold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" } } } @@ -5518,6 +5608,24 @@ "state" : "new", "value" : "Collected widgets " } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收纳控件 " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收納小工具 " + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收納小工具 " + } } } }, diff --git a/VoidLink/mul.lproj/iPhone.xcstrings b/VoidLink/mul.lproj/iPhone.xcstrings index db88b6ee..3d9291b6 100644 --- a/VoidLink/mul.lproj/iPhone.xcstrings +++ b/VoidLink/mul.lproj/iPhone.xcstrings @@ -2815,6 +2815,36 @@ } } }, + "kek-77-Kmg.text" : { + "comment" : "Class = \"UILabel\"; text = \"Collected Widgets \"; ObjectID = \"kek-77-Kmg\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Collected Widgets " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收纳控件 " + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收納小工具 " + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "已收納小工具 " + } + } + } + }, "kva-gQ-CZh.text" : { "comment" : "Class = \"UILabel\"; text = \"Size\"; ObjectID = \"kva-gQ-CZh\";", "extractionState" : "extracted_with_value", @@ -3205,6 +3235,66 @@ } } }, + "myu-Th-CTZ.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; myu-Th-CTZ.segmentTitles[0] = \"coexist\"; ObjectID = \"myu-Th-CTZ\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "coexist" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "共存" + } + } + } + }, + "myu-Th-CTZ.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; myu-Th-CTZ.segmentTitles[1] = \"exclusive\"; ObjectID = \"myu-Th-CTZ\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "exclusive" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "互斥" + } + } + } + }, "n6Z-cT-3a1.text" : { "comment" : "Class = \"UILabel\"; text = \"Mic Volume\"; ObjectID = \"n6Z-cT-3a1\";", "extractionState" : "extracted_with_value", @@ -3775,6 +3865,66 @@ } } }, + "Oo7-lI-LXT.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; Oo7-lI-LXT.segmentTitles[0] = \"unfold\"; ObjectID = \"Oo7-lI-LXT\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "unfold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "展开" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "展開" + } + } + } + }, + "Oo7-lI-LXT.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; Oo7-lI-LXT.segmentTitles[1] = \"fold\"; ObjectID = \"Oo7-lI-LXT\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "fold" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "收起" + } + } + } + }, "ozU-7C-qKE.text" : { "comment" : "Class = \"UILabel\"; text = \"Button down\"; ObjectID = \"ozU-7C-qKE\";", "extractionState" : "extracted_with_value", @@ -5527,6 +5677,36 @@ } } }, + "YLN-X6-oj9.text" : { + "comment" : "Class = \"UILabel\"; text = \" Reveal mode \"; ObjectID = \"YLN-X6-oj9\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : " Reveal mode " + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展开模式" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展開模式" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : " 展開模式" + } + } + } + }, "Z9M-yq-an2.title" : { "comment" : "Class = \"UIBarButtonItem\"; title = \"Import\"; ObjectID = \"Z9M-yq-an2\";", "extractionState" : "extracted_with_value",