diff --git a/VoidLink/Input/CommandManager.swift b/VoidLink/Input/CommandManager.swift index 93162e1a..04def505 100644 --- a/VoidLink/Input/CommandManager.swift +++ b/VoidLink/Input/CommandManager.swift @@ -171,6 +171,7 @@ import UIKit "SOFTKEYBOARD", "ABSTCHDRAG", "FOLDER", + "LABELEDFOLDER", "PRESSURECURVE", "PENCILHOVER", "BRUSH", @@ -200,6 +201,7 @@ import UIKit "BRUSH": 0xFF, "ERASER": 0xFF, "FOLDER": 0xFF, + "LABELEDFOLDER": 0xFF, ] @objc public static let keyboardButtonMappings: [String: Int16] = [ diff --git a/VoidLink/Input/OnScreenWidgetView.swift b/VoidLink/Input/OnScreenWidgetView.swift index 3eac6aca..f531a525 100644 --- a/VoidLink/Input/OnScreenWidgetView.swift +++ b/VoidLink/Input/OnScreenWidgetView.swift @@ -92,9 +92,17 @@ import ObjectiveC.runtime @objc static public var editMode: Bool = false @objc static public var buttonVisualFeedbackEnabled: Bool = true + @objc public var widgetLabel: String private var nonEditableWidgetLabel: String = "" @objc public var cmdString: String + @objc public var showCmdStringLabel: Bool { + if let parent = OnScreenWidgetView.mapping[self.parentSequence] { + return parent.cmdString.contains("LABELEDFOLDER") + } + return false + } + @objc public var sequence: Int16 = -1 private var buttonString: String = "" @objc var functionalButtonString: String = "" @@ -268,7 +276,7 @@ import ObjectiveC.runtime @objc public var isMagnifier: Bool = false @objc public var animatesTransition: Bool = true - + // for all stick pads @objc public var minStickOffset: CGFloat = 0 public let stickMaxOffset: CGFloat = 0x7FFE @@ -389,7 +397,9 @@ import ObjectiveC.runtime // key / button label private let label: UILabel - + private let cmdLabel: UILabel + private let labelStackView: UIStackView + // first touch location within the button or pad view (self) @objc public var touchBeganLocation: CGPoint = .zero @@ -446,7 +456,11 @@ import ObjectiveC.runtime @objc public var revealMode: RevealMode = .coexist @objc public var bulkMoveEnabled: Bool = false @objc public var sequenceSet: Set = Set() - @objc public var parentSequence: Int16 = -1 + @objc public var parentSequence: Int16 = -1 { + didSet { + setupAtrributedText() + } + } @objc public var standardFoldingInterval: TimeInterval = 0.05 static weak var capturer: OnScreenWidgetView? @objc static weak var deepestButton: OnScreenWidgetView? @@ -497,6 +511,8 @@ import ObjectiveC.runtime self.widgetLabel = buttonLabel self.shape = shape self.label = UILabel() + self.cmdLabel = UILabel() + self.labelStackView = UIStackView() // self.originalBackgroundColor = UIColor(white: 0.2, alpha: 0.7) // self.widthFactor = 1.0 // self.heightFactor = 1.0 @@ -549,7 +565,7 @@ import ObjectiveC.runtime if self.cmdString.contains("ERASER"){ self.functionalButtonString = "ERASER" } - if self.cmdString.contains("FOLDER") { + if self.cmdString.contains("FOLDER") || self.cmdString.contains("LABELEDFOLDER") { self.functionalButtonString = "FOLDER" } if self.isFunctionalButton { @@ -680,7 +696,7 @@ import ObjectiveC.runtime self.mouseButtonActionDelay = self.cmdString.contains("ABSMOUSEPAD") ? 0.005 : 0 - self.standardFoldingInterval = widgetType == .touchPad ? 0.05 : 0.085; + self.standardFoldingInterval = widgetType == .touchPad ? 0.05 : 0.065; self.isMagnifier = self.cmdString.contains("MAGNIFIER") @@ -985,17 +1001,17 @@ import ObjectiveC.runtime } NSLayoutConstraint.activate(self.shape == "round" ? [ - label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 13), // set up label size contrain within UIView - label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -13), - label.centerXAnchor.constraint(equalTo: centerXAnchor), - label.centerYAnchor.constraint(equalTo: centerYAnchor),] + labelStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 13), // set up label size contrain within UIView + labelStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -13), + labelStackView.centerXAnchor.constraint(equalTo: centerXAnchor), + labelStackView.centerYAnchor.constraint(equalTo: centerYAnchor),] : [ - label.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), // set up label size contrain within UIView - label.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), - label.topAnchor.constraint(equalTo: self.topAnchor, constant: 8), // set up label size contrain within UIView - label.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: -8), - label.centerXAnchor.constraint(equalTo: centerXAnchor), - label.centerYAnchor.constraint(equalTo: centerYAnchor),]) + labelStackView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 10), // set up label size contrain within UIView + labelStackView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: -10), + labelStackView.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor, constant: 8), // set up label size contrain within UIView + labelStackView.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor, constant: -8), + labelStackView.centerXAnchor.constraint(equalTo: centerXAnchor), + labelStackView.centerYAnchor.constraint(equalTo: centerYAnchor),]) if self.shape != "round"{ self.setSquareWidgetCornerRadius() @@ -1036,6 +1052,38 @@ import ObjectiveC.runtime } return false } + + private func cleanedCmdStringLabelText() -> String { + let trimmedCmdString = cmdString.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedCmdString.isEmpty else { return "" } + + guard trimmedCmdString.contains("-") else { + let cmdText = trimmedCmdString + return cmdText.hasSuffix("+") ? String(cmdText.dropLast()) : cmdText + } + + let parts = trimmedCmdString + .split(separator: "-") + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + .compactMap { part -> String? in + guard !part.isEmpty else { return nil } + + let uppercasedPart = part.uppercased() + if part.contains("+") || uppercasedPart.hasSuffix("MS") { + return nil + } + + let cleanedPart = part.replacingOccurrences( + of: #"\.\d+"#, + with: "", + options: .regularExpression + ) + return cleanedPart.isEmpty ? nil : cleanedPart + } + + let cmdText = parts.joined(separator: "-") + return cmdText.hasSuffix("+") ? String(cmdText.dropLast()) : cmdText + } @objc func reverseColorPhase(reversed: Bool){ backgroundAlpha = reversed ? (originalBackgroundAlpha.sign == .minus ? 0.5 : -0.5) : originalBackgroundAlpha @@ -1045,6 +1093,8 @@ import ObjectiveC.runtime } @objc func setupAtrributedText(){ + setupLabelFonts() + var text = self.widgetLabel.contains("#") ? "\(self.widgetLabel.split(separator: "#").first ?? "")" : LocalizationHelper.localizedString(forKey: self.widgetLabel) if !OnScreenWidgetView.editMode, self.widgetType == .touchPad, !self.hasTemporaryLabel { @@ -1093,14 +1143,54 @@ import ObjectiveC.runtime label.attributedText = attr label.textAlignment = .center label.baselineAdjustment = .alignCenters + setupCmdLabel() } - private func setupView() { - // label.text = self.widgetLabel - // label.font = UIFont.boldSystemFont(ofSize: 19) - // label.font = UIFont.systemFont(ofSize: 19, weight: .medium, design: .rounded) + @objc func setupCmdLabel() { + setupLabelFonts() + guard canShowCmdLabel else { + cmdLabel.isHidden = true + return + } + guard showCmdStringLabel else { + cmdLabel.isHidden = true + return + } + let cmdText = cleanedCmdStringLabelText() + let labelText = label.text ?? label.attributedText?.string ?? "" + let duplicatesLabelText = cmdText.uppercased() == labelText.uppercased() - let baseFont = UIFont.boldSystemFont(ofSize: self.shape == "round" ? 22 : 19) + let cmdAttr = NSAttributedString( + string: cmdText, + attributes: [ + .foregroundColor: UIColor(white:labelAlpha>0 ? 1.0 : 0, alpha: abs(labelAlpha)), + .strokeColor: (labelAlpha>0 ? UIColor.black : UIColor.white).withAlphaComponent(abs(labelAlpha)*0.43), + .strokeWidth: widgetType == .touchPad ? 7 : (containsNonLatin(cmdText) ? -1 : -4) + ] + ) + cmdLabel.attributedText = cmdAttr + cmdLabel.textAlignment = .center + cmdLabel.baselineAdjustment = .alignCenters + cmdLabel.isHidden = cmdText.isEmpty || duplicatesLabelText + } + + private var canShowCmdLabel: Bool { + !self.hasNonEditableLabel + && self.widgetType == .button + && self.functionalButtonString.isEmpty + && !self.isFolder + } + + private func setupLabelFonts() { + let baseFontSize: CGFloat + let usesCompactCmdLabelLayout = PublicUtils.isIPhone && showCmdStringLabel && canShowCmdLabel + if usesCompactCmdLabelLayout { + baseFontSize = self.shape == "round" ? 17 : 15 + } else { + baseFontSize = self.shape == "round" ? 22 : 19 + } + + let baseFont = UIFont.boldSystemFont(ofSize: baseFontSize) if #available(iOS 13.0, *) { if let desc = baseFont.fontDescriptor.withDesign(.rounded) { label.font = UIFont(descriptor: desc, size: 0) @@ -1110,12 +1200,44 @@ import ObjectiveC.runtime } else { label.font = baseFont } + + let cmdLabelFontSize: CGFloat = PublicUtils.isIPhone ? 8 : 12 + if #available(iOS 13.0, tvOS 13.0, *) { + cmdLabel.font = UIFont.monospacedSystemFont(ofSize: cmdLabelFontSize, weight: .medium) + } else { + cmdLabel.font = UIFont.systemFont(ofSize: cmdLabelFontSize, weight: .medium) + } + + label.minimumScaleFactor = usesCompactCmdLabelLayout ? 0.62 : 0.1 + cmdLabel.minimumScaleFactor = PublicUtils.isIPhone ? 0.6 : 0.6 + } + + private func setupView() { + // label.text = self.widgetLabel + // label.font = UIFont.boldSystemFont(ofSize: 19) + // label.font = UIFont.systemFont(ofSize: 19, weight: .medium, design: .rounded) + + setupLabelFonts() label.translatesAutoresizingMaskIntoConstraints = false label.adjustsFontSizeToFitWidth = true label.minimumScaleFactor = 0.1 // Adjust the scale factor as needed label.textAlignment = .center + cmdLabel.translatesAutoresizingMaskIntoConstraints = false + cmdLabel.adjustsFontSizeToFitWidth = true + cmdLabel.textAlignment = .center + cmdLabel.numberOfLines = 1 + + labelStackView.translatesAutoresizingMaskIntoConstraints = false + labelStackView.axis = .vertical + labelStackView.alignment = .fill + labelStackView.distribution = .fill + labelStackView.spacing = 1 + labelStackView.isUserInteractionEnabled = false + labelStackView.addArrangedSubview(label) + labelStackView.addArrangedSubview(cmdLabel) + // label.textColor = UIColor(white: 1.0, alpha: labelAlpha) // label.shadowColor = .black // label.shadowOffset = CGSize(width: 0, height: 0) @@ -1172,7 +1294,7 @@ import ObjectiveC.runtime // self.layer.shadowRadius = 8 // self.layer.shadowOpacity = 0.5 - addSubview(label) + addSubview(labelStackView) _ = installCustomContentIfNeeded() if(OnScreenWidgetView.editMode) {self.changeAndActivateContraints()} @@ -3870,6 +3992,7 @@ import ObjectiveC.runtime widget.capturedTouches.removeAllObjects() widget.center = folder.storedCenter widget.isHidden = false + widget.setupCmdLabel() if (widget.widgetType == .touchPad || abs(widget.backgroundAlpha) < 0.1 || widget.hasTemporaryLabel){ diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 4238b01c..0ec8350c 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -801,8 +801,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; widgetView.componentSizeFactor = buttonState.componentSizeFactor; widgetView.touchPointAnchored = buttonState.touchPointAnchored; widgetView.stickIndicatorOffset = buttonState.stickIndicatorOffset; - - [widgetView setupAtrributedText]; if(widgetView.isFolder && widgetView.parentSequence<0 && widgetView.autoDockIdleDuration>0) { [widgetView setAutoDockWithEnabled:true]; @@ -819,6 +817,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; } [widgetView accessWidgetAttributes]; + + [widgetView setupAtrributedText]; // rely on "labeledFolder cmd" } else if(buttonState.widgetType == LegacyOnScreenControls) hasLegacyWidget = true; } diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 19d2147b..4f26a02f 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -773,6 +773,35 @@ } } }, + "=brush" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Brush" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "笔刷" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "筆刷" + } + } + } + }, "=brushShortcut" : { "extractionState" : "manual", "localizations" : { @@ -1034,6 +1063,35 @@ } } }, + "=deselect" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Deselect" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消选择" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消選取" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消選取" + } + } + } + }, "=del" : { "extractionState" : "manual", "localizations" : { @@ -1440,6 +1498,35 @@ } } }, + "=eraser" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Eraser" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "橡皮擦" + } + } + } + }, "=eraserShortcut" : { "extractionState" : "manual", "localizations" : { @@ -1585,6 +1672,35 @@ } } }, + "=labeledFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Labeled Folder" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "带标签的控件夹" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "帶標籤的控制項夾" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "帶標籤的控制項夾" + } + } + } + }, "=functions" : { "extractionState" : "manual", "localizations" : { @@ -1614,6 +1730,35 @@ } } }, + "=groupLayers" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Group Layers" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "组合图层" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組圖層" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "群組圖層" + } + } + } + }, "=GamepadOverlayOff" : { "extractionState" : "manual", "localizations" : { @@ -2136,6 +2281,35 @@ } } }, + "=lasso" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Lasso" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "套索" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "套索" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "套索" + } + } + } + }, "=lock" : { "extractionState" : "manual", "localizations" : { @@ -2252,6 +2426,35 @@ } } }, + "=mergeLayers" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Merge Layers" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "合并图层" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "合併圖層" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "合併圖層" + } + } + } + }, "=mic" : { "extractionState" : "manual", "localizations" : { @@ -2310,6 +2513,35 @@ } } }, + "=move" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "移動" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "移動" + } + } + } + }, "=mouseM" : { "extractionState" : "manual", "localizations" : { @@ -2629,6 +2861,35 @@ } } }, + "=paintBucket" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Paint Bucket" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "油漆桶" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "油漆桶" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "油漆桶" + } + } + } + }, "=paste" : { "extractionState" : "manual", "localizations" : { @@ -2687,6 +2948,35 @@ } } }, + "=pen" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Pen" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "钢笔" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "鋼筆" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "鋼筆" + } + } + } + }, "=pgdn" : { "extractionState" : "manual", "localizations" : { @@ -4345,6 +4635,29 @@ } } }, + "A folder whose collected buttons show command labels." : { + "extractionState" : "manual", + "localizations" : { + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "收纳其中的按键会显示命令标签。" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "收納其中的按鍵會顯示命令標籤。" + } + }, + "zh-HK" : { + "stringUnit" : { + "state" : "translated", + "value" : "收納其中的按鍵會顯示命令標籤。" + } + } + } + }, "A touchpad with double-tap button triggering will be created" : { "extractionState" : "manual", "localizations" : { @@ -19388,4 +19701,4 @@ } }, "version" : "1.1" -} \ No newline at end of file +} diff --git a/VoidLink/PresetConfigs/widgetTemplates.bin b/VoidLink/PresetConfigs/widgetTemplates.bin index 481d3d62..a53998e1 100644 Binary files a/VoidLink/PresetConfigs/widgetTemplates.bin and b/VoidLink/PresetConfigs/widgetTemplates.bin differ diff --git a/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin b/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin index a22b2f45..1bde6e44 100644 Binary files a/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin and b/VoidLink/PresetConfigs/widgetTemplatesIPhone.bin differ diff --git a/VoidLinkPreview/WidgetPickerView.swift b/VoidLinkPreview/WidgetPickerView.swift index 7389182b..c2c290f0 100644 --- a/VoidLinkPreview/WidgetPickerView.swift +++ b/VoidLinkPreview/WidgetPickerView.swift @@ -623,6 +623,16 @@ struct WidgetPickerView: View { allowsShortcutCombo: true, forcedComboMode: nil ), + FunctionalButtonOption( + localizationKey: "=labeledFolder", + cmd: "LABELEDFOLDER", + tip: LocalizationHelper.localizedString(forKey: "A folder whose collected buttons show command labels."), + allowsKeyboardCombination: true, + allowsGamepadCombination: true, + allowsSkillCombo: true, + allowsShortcutCombo: true, + forcedComboMode: nil + ), FunctionalButtonOption( localizationKey: "=toolbox", cmd: "TOOLBOX",