#!/usr/bin/env python3 """Drag reorder both dashboard lists; preserve icons and Settings.""" from pathlib import Path from datetime import datetime import shutil, re root=Path.cwd() controller=root/'OrionToolbar/OrionVMController.swift' menu=root/'OrionToolbar/OrionMenuView.swift' for p in (controller,menu): if not p.is_file(): raise SystemExit(f'Missing {p}; run from OrionToolbar root') c=controller.read_text(); m=menu.read_text() if 'orionVMOrder.v1' in c: raise SystemExit('This patch has already been applied.') def replace_once(src, old, new, label): if src.count(old)!=1: raise SystemExit(f'Cannot uniquely locate {label} (found {src.count(old)}). No changes made.') return src.replace(old,new,1) # Maintain a stable, persisted order separate from server definitions. anchor=' private let defaultsKey = "serverDefinitions.v2"' c=replace_once(c,anchor,anchor+''' private let vmOrderKey = "orionVMOrder.v1" private var vmOrder: [String] { get { UserDefaults.standard.stringArray(forKey: vmOrderKey) ?? [] } set { UserDefaults.standard.set(newValue, forKey: vmOrderKey) } } func reorderVM(draggedID: String, onto targetID: String) { guard draggedID != targetID, discoveredVMs.contains(where: { $0.id == draggedID }), discoveredVMs.contains(where: { $0.id == targetID }) else { return } var order = discoveredVMs.map(\\.id) guard let from = order.firstIndex(of: draggedID), let to = order.firstIndex(of: targetID) else { return } order.remove(at: from) order.insert(draggedID, at: to) vmOrder = order let ranks = Dictionary(uniqueKeysWithValues: order.enumerated().map { ($0.element, $0.offset) }) discoveredVMs.sort { (ranks[$0.id] ?? Int.max) < (ranks[$1.id] ?? Int.max) } } ''','controller ordering properties') # Discovery sorts alphabetically initially; then apply persisted order, preserving newly discovered VMs. anchor=''' // Resolve Parallels VM bundles without requiring the guest to run.''' c=replace_once(c,anchor,''' // Keep the user's VM order through every 15-second discovery refresh. // New VMs are appended in discovery order. let savedOrder = vmOrder let ranks = Dictionary(uniqueKeysWithValues: savedOrder.enumerated().map { ($0.element, $0.offset) }) discoveredVMs.sort { left, right in let a = ranks[left.id] ?? Int.max let b = ranks[right.id] ?? Int.max if a != b { return a < b } return left.name.localizedStandardCompare(right.name) == .orderedAscending } '''+anchor,'discovery sort persistence') # Use NSItemProvider for consistent macOS drag and drop. Drag handle only: buttons remain clickable. # The drag provider is explicitly NSString, and callback converts from NSItemProviderReading. vm_name=''' Text(machine.name).font(.system(size: 12, weight: .semibold)).lineLimit(1).frame(width: 230, alignment: .leading)''' vm_name_new=''' HStack(spacing: 6) { Image(systemName: "line.3.horizontal") .foregroundStyle(OrionStyle.muted) Text(machine.name).lineLimit(1) } .font(.system(size: 12, weight: .semibold)) .frame(width: 230, alignment: .leading) .contentShape(Rectangle()) .onDrag { NSItemProvider(object: ("vm:" + machine.id) as NSString) }''' m=replace_once(m,vm_name,vm_name_new,'All Virtual Machines drag handle') # Target only machineRow by isolating function boundaries. start=m.index(' private func machineRow(') end=m.index(' private func integratedRow(',start) block=m[start:end] background=''' .background(OrionStyle.card.opacity(0.75), in: RoundedRectangle(cornerRadius: 10))''' vm_drop=''' .onDrop(of: [.plainText], isTargeted: nil) { providers in guard let provider = providers.first else { return false } _ = provider.loadObject(ofClass: NSString.self) { item, _ in guard let string = item as? NSString else { return } let value = string as String guard value.hasPrefix("vm:") else { return } Task { @MainActor in vm.reorderVM(draggedID: String(value.dropFirst(3)), onto: machine.id) } } return true }''' block=replace_once(block,background,background+'\n'+vm_drop,'All Virtual Machines drop target') m=m[:start]+block+m[end:] # Integrated server grip (may already be .onDrag or .draggable). start=m.index(' private func integratedRow(') end=m.index(' private func action(',start) block=m[start:end] block=block.replace('.draggable(server.id.uuidString)', '.onDrag { NSItemProvider(object: ("server:" + server.id.uuidString) as NSString) }') # Prior patch may have installed .onDrag without a namespace; update its payload. block=block.replace('NSItemProvider(object: server.id.uuidString as NSString)', 'NSItemProvider(object: ("server:" + server.id.uuidString) as NSString)') # Replace only integratedRow drop handler, whether original .dropDestination or last .onDrop. new_drop=''' .onDrop(of: [.plainText], isTargeted: nil) { providers in guard let provider = providers.first else { return false } _ = provider.loadObject(ofClass: NSString.self) { item, _ in guard let string = item as? NSString else { return } let value = string as String guard value.hasPrefix("server:"), let dragged = UUID(uuidString: String(value.dropFirst(7))) else { return } Task { @MainActor in vm.reorderServer(draggedID: dragged, onto: server.id) } } return true }''' block,n=re.subn(r' \.onDrop\(of: \[\.plainText\], isTargeted: nil\) \{ providers in\n.*?\n \}',lambda _:new_drop,block,count=1,flags=re.S) if n==0: block,n=re.subn(r' \.dropDestination\(for: String\.self\) \{ items, _ in\n.*?\n \}',lambda _:new_drop,block,count=1,flags=re.S) if n!=1: raise SystemExit('Cannot locate integrated server drop handler; no changes made') if '.onDrag' not in block: raise SystemExit('Cannot locate integrated server drag handle; no changes made') m=m[:start]+block+m[end:] # Fix previous build error if it remains elsewhere, without modifying other behavior. m=m.replace('guard let text = value as String?, let dragged = UUID(uuidString: text) else { return }', 'guard let text = value as? NSString, let dragged = UUID(uuidString: text as String) else { return }') # Avoid duplicate right-hand chevron if prior patch was not applied. chevron=''' Menu { Button("Open in Parallels") { vm.openParallels() } if machine.status.lowercased() == "running" { Button("Restart") { vm.operate(machine, action: "restart") } Button("Force Stop", role: .destructive) { pendingForceStop = machine } } } label: { Image(systemName: "chevron.right").foregroundStyle(OrionStyle.muted) } .menuStyle(.borderlessButton).frame(width: 22)''' m=m.replace(chevron,'') backup=root/'backups'/datetime.now().strftime('%Y%m%d-%H%M%S-%f') for path,source in ((controller,c),(menu,m)): target=backup/path.relative_to(root) target.parent.mkdir(parents=True,exist_ok=True) shutil.copy2(path,target) for path,source in ((controller,c),(menu,m)): path.write_text(source) print('Updated OrionVMController.swift and OrionMenuView.swift') print('Backups:',backup) print('Icons and SettingsView.swift unchanged.')