Fix notch ears. Fix Terminal Detach. Add icons. Fix mouse hover behaviour during shrink.
@@ -7,6 +7,7 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0F4A88A33D93B6E100A1C002 /* Assets.xcassets */; };
|
||||||
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6359CF9DDF89413440300D /* NotchSettings.swift */; };
|
2213F430F3D8A88033607CD2 /* NotchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6359CF9DDF89413440300D /* NotchSettings.swift */; };
|
||||||
247C6F84E7ADE7AED43381E2 /* DowntermApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B671125208055E5334CB85E /* DowntermApp.swift */; };
|
247C6F84E7ADE7AED43381E2 /* DowntermApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B671125208055E5334CB85E /* DowntermApp.swift */; };
|
||||||
26A767A10DDA77A690CC3C37 /* NotchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589421631401C819FE1A7BA9 /* NotchViewModel.swift */; };
|
26A767A10DDA77A690CC3C37 /* NotchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 589421631401C819FE1A7BA9 /* NotchViewModel.swift */; };
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
|
02FEFF9074A85F02C43D9408 /* NotchWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchWindow.swift; sourceTree = "<group>"; };
|
||||||
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
0A973877BCE6084D0EBBBDBD /* SettingsWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsWindowController.swift; sourceTree = "<group>"; };
|
||||||
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
|
0B567F3B5D006D2B35630CFF /* LaunchAtLoginHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginHelper.swift; sourceTree = "<group>"; };
|
||||||
|
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
15A290D4D21D6C01A583A372 /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
|
15A290D4D21D6C01A583A372 /* ScreenManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenManager.swift; sourceTree = "<group>"; };
|
||||||
1E47000112562615C7E59489 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
|
1E47000112562615C7E59489 /* SwiftTermView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftTermView.swift; sourceTree = "<group>"; };
|
||||||
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
1FC09C538CBE7C2D072008B2 /* NotchShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotchShape.swift; sourceTree = "<group>"; };
|
||||||
@@ -72,6 +74,7 @@
|
|||||||
0EF94ED46B4860C241540F0A /* Resources */ = {
|
0EF94ED46B4860C241540F0A /* Resources */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0F4A88A33D93B6E100A1C002 /* Assets.xcassets */,
|
||||||
9547A79F60E46F4521A70674 /* Downterm.entitlements */,
|
9547A79F60E46F4521A70674 /* Downterm.entitlements */,
|
||||||
);
|
);
|
||||||
path = Resources;
|
path = Resources;
|
||||||
@@ -171,6 +174,7 @@
|
|||||||
buildPhases = (
|
buildPhases = (
|
||||||
F3C6D5CD1247D246A3F6F7AB /* Sources */,
|
F3C6D5CD1247D246A3F6F7AB /* Sources */,
|
||||||
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
|
6085DF2BDFFB2A99C4ABD514 /* Frameworks */,
|
||||||
|
0F4A88A33D93B6E100A1C003 /* Resources */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -214,6 +218,17 @@
|
|||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
0F4A88A33D93B6E100A1C003 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
0F4A88A33D93B6E100A1C001 /* Assets.xcassets in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
|
F3C6D5CD1247D246A3F6F7AB /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
@@ -250,17 +265,20 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
||||||
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
|
MARKETING_VERSION = 0.0.3;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||||
PRODUCT_NAME = Downterm;
|
PRODUCT_NAME = Downterm;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
};
|
};
|
||||||
@@ -334,17 +352,20 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
|
||||||
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
CLANG_USE_OPTIMIZATION_PROFILE = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Downterm/Resources/Downterm.entitlements;
|
||||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
"DEVELOPMENT_TEAM[sdk=macosx*]" = G698BP272N;
|
||||||
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
INFOPLIST_FILE = Downterm/Resources/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = CommandNotch;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.downterm.app;
|
MARKETING_VERSION = 0.0.3;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.commandnotch.app;
|
||||||
PRODUCT_NAME = Downterm;
|
PRODUCT_NAME = Downterm;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import SwiftUI
|
|||||||
/// between the compact closed state and the expanded open state.
|
/// between the compact closed state and the expanded open state.
|
||||||
///
|
///
|
||||||
/// The shape uses quadratic Bezier curves to produce the distinctive
|
/// The shape uses quadratic Bezier curves to produce the distinctive
|
||||||
/// "ear" ramps on each side when closed, and a clean rounded-bottom
|
/// top-edge cut-ins of the closed notch, and a clean rounded-bottom
|
||||||
/// rectangle when open (topCornerRadius approaches 0).
|
/// rectangle when open (topCornerRadius approaches 0).
|
||||||
struct NotchShape: Shape {
|
struct NotchShape: Shape {
|
||||||
|
|
||||||
/// Radius applied to the top-left and top-right outer corners (the "ears").
|
/// Radius applied to the top-left and top-right transitions where the notch
|
||||||
/// When close to 0, the top corners become sharp and the shape is a
|
/// curves away from the screen edge. When close to 0, the top corners become
|
||||||
/// rectangle with rounded bottom corners — no visible ear ramps.
|
/// sharp and the shape is a rectangle with rounded bottom corners.
|
||||||
var topCornerRadius: CGFloat
|
var topCornerRadius: CGFloat
|
||||||
|
|
||||||
/// Radius applied to the bottom-left and bottom-right inner corners.
|
/// Radius applied to the bottom-left and bottom-right inner corners.
|
||||||
@@ -46,10 +46,10 @@ struct NotchShape: Shape {
|
|||||||
path.move(to: CGPoint(x: minX, y: minY))
|
path.move(to: CGPoint(x: minX, y: minY))
|
||||||
|
|
||||||
if topR > 0.5 {
|
if topR > 0.5 {
|
||||||
// Top-left ear: curve down from the top edge
|
// Leave the screen edge horizontally, then turn into the side wall.
|
||||||
path.addQuadCurve(
|
path.addQuadCurve(
|
||||||
to: CGPoint(x: minX + topR, y: minY + topR),
|
to: CGPoint(x: minX + topR, y: minY + topR),
|
||||||
control: CGPoint(x: minX, y: minY + topR)
|
control: CGPoint(x: minX + topR, y: minY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
path.addLine(to: CGPoint(x: minX, y: minY))
|
path.addLine(to: CGPoint(x: minX, y: minY))
|
||||||
@@ -73,14 +73,14 @@ struct NotchShape: Shape {
|
|||||||
control: CGPoint(x: maxX - topR, y: maxY)
|
control: CGPoint(x: maxX - topR, y: maxY)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Right edge up to top-right ear area
|
// Right edge up to the top-right transition
|
||||||
path.addLine(to: CGPoint(x: maxX - topR, y: minY + topR))
|
path.addLine(to: CGPoint(x: maxX - topR, y: minY + topR))
|
||||||
|
|
||||||
if topR > 0.5 {
|
if topR > 0.5 {
|
||||||
// Top-right ear: curve back up to the top edge
|
// Mirror the top-left transition.
|
||||||
path.addQuadCurve(
|
path.addQuadCurve(
|
||||||
to: CGPoint(x: maxX, y: minY),
|
to: CGPoint(x: maxX, y: minY),
|
||||||
control: CGPoint(x: maxX, y: minY + topR)
|
control: CGPoint(x: maxX - topR, y: minY)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
path.addLine(to: CGPoint(x: maxX, y: minY))
|
path.addLine(to: CGPoint(x: maxX, y: minY))
|
||||||
@@ -100,8 +100,8 @@ extension NotchShape {
|
|||||||
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
NotchShape(topCornerRadius: 6, bottomCornerRadius: 14)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Open-state shape: no ear ramps, just rounded bottom corners.
|
/// Open-state shape: no top-edge cut-ins, just rounded bottom corners.
|
||||||
/// topCornerRadius is near-zero so the ears disappear and the panel
|
/// topCornerRadius is near-zero so the top becomes effectively flat and the panel
|
||||||
/// extends flush to the top edge of the screen.
|
/// extends flush to the top edge of the screen.
|
||||||
static var opened: NotchShape {
|
static var opened: NotchShape {
|
||||||
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)
|
NotchShape(topCornerRadius: 0, bottomCornerRadius: 24)
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ struct ContentView: View {
|
|||||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.width)
|
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.width)
|
||||||
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.height)
|
.animation(vm.notchState == .open ? vm.openAnimation : vm.closeAnimation, value: vm.notchSize.height)
|
||||||
.onHover { handleHover($0) }
|
.onHover { handleHover($0) }
|
||||||
|
.onChange(of: vm.isCloseTransitionActive) { _, isClosing in
|
||||||
|
if isClosing {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
} else {
|
||||||
|
scheduleHoverOpenIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
.edgesIgnoringSafeArea(.all)
|
.edgesIgnoringSafeArea(.all)
|
||||||
}
|
}
|
||||||
@@ -146,20 +156,30 @@ struct ContentView: View {
|
|||||||
private func handleHover(_ hovering: Bool) {
|
private func handleHover(_ hovering: Bool) {
|
||||||
if hovering {
|
if hovering {
|
||||||
withAnimation(hoverAnimation) { vm.isHovering = true }
|
withAnimation(hoverAnimation) { vm.isHovering = true }
|
||||||
guard openNotchOnHover, vm.notchState == .closed else { return }
|
scheduleHoverOpenIfNeeded()
|
||||||
|
|
||||||
hoverTask?.cancel()
|
|
||||||
hoverTask = Task { @MainActor in
|
|
||||||
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
|
||||||
guard !Task.isCancelled, vm.isHovering else { return }
|
|
||||||
vm.requestOpen?()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
hoverTask?.cancel()
|
hoverTask?.cancel()
|
||||||
withAnimation(hoverAnimation) { vm.isHovering = false }
|
withAnimation(hoverAnimation) { vm.isHovering = false }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func scheduleHoverOpenIfNeeded() {
|
||||||
|
hoverTask?.cancel()
|
||||||
|
guard openNotchOnHover,
|
||||||
|
vm.notchState == .closed,
|
||||||
|
!vm.isCloseTransitionActive,
|
||||||
|
vm.isHovering else { return }
|
||||||
|
|
||||||
|
hoverTask = Task { @MainActor in
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(minimumHoverDuration * 1_000_000_000))
|
||||||
|
guard !Task.isCancelled,
|
||||||
|
vm.isHovering,
|
||||||
|
vm.notchState == .closed,
|
||||||
|
!vm.isCloseTransitionActive else { return }
|
||||||
|
vm.requestOpen?()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func abbreviate(_ title: String) -> String {
|
private func abbreviate(_ title: String) -> String {
|
||||||
title.count <= 30 ? title : String(title.prefix(28)) + "…"
|
title.count <= 30 ? title : String(title.prefix(28)) + "…"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import AppKit
|
import AppKit
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftTerm
|
import Combine
|
||||||
|
|
||||||
/// Manages standalone pop-out terminal windows for detached tabs.
|
/// Manages standalone pop-out terminal windows for detached tabs.
|
||||||
/// Each detached tab gets its own resizable window with the terminal view.
|
/// Each detached tab gets its own resizable window with the terminal view.
|
||||||
@@ -12,6 +12,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
/// Tracks open pop-out windows so they aren't released prematurely.
|
/// Tracks open pop-out windows so they aren't released prematurely.
|
||||||
private var windows: [UUID: NSWindow] = [:]
|
private var windows: [UUID: NSWindow] = [:]
|
||||||
private var sessions: [UUID: TerminalSession] = [:]
|
private var sessions: [UUID: TerminalSession] = [:]
|
||||||
|
private var titleObservers: [UUID: AnyCancellable] = [:]
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@@ -21,6 +22,12 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
func popout(session: TerminalSession) {
|
func popout(session: TerminalSession) {
|
||||||
let windowID = session.id
|
let windowID = session.id
|
||||||
|
|
||||||
|
if let existingWindow = windows[windowID] {
|
||||||
|
existingWindow.makeKeyAndOrderFront(nil)
|
||||||
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let win = NSWindow(
|
let win = NSWindow(
|
||||||
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
|
contentRect: NSRect(x: 0, y: 0, width: 720, height: 480),
|
||||||
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
styleMask: [.titled, .closable, .resizable, .miniaturizable],
|
||||||
@@ -33,12 +40,14 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
win.delegate = self
|
win.delegate = self
|
||||||
win.isReleasedWhenClosed = false
|
win.isReleasedWhenClosed = false
|
||||||
|
|
||||||
// Embed the terminal view directly
|
let hostingView = NSHostingView(
|
||||||
let tv = session.terminalView
|
rootView: SwiftTermView(session: session)
|
||||||
tv.removeFromSuperview()
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
tv.frame = NSRect(origin: .zero, size: win.contentView!.bounds.size)
|
.background(Color.black)
|
||||||
tv.autoresizingMask = [.width, .height]
|
.preferredColorScheme(.dark)
|
||||||
win.contentView?.addSubview(tv)
|
)
|
||||||
|
hostingView.frame = NSRect(origin: .zero, size: win.contentRect(forFrameRect: win.frame).size)
|
||||||
|
win.contentView = hostingView
|
||||||
|
|
||||||
win.center()
|
win.center()
|
||||||
win.makeKeyAndOrderFront(nil)
|
win.makeKeyAndOrderFront(nil)
|
||||||
@@ -48,16 +57,22 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
sessions[windowID] = session
|
sessions[windowID] = session
|
||||||
|
|
||||||
// Update window title when the terminal title changes
|
// Update window title when the terminal title changes
|
||||||
session.$title
|
titleObservers[windowID] = session.$title
|
||||||
.receive(on: RunLoop.main)
|
.receive(on: RunLoop.main)
|
||||||
.sink { [weak win] title in win?.title = title }
|
.sink { [weak win] title in win?.title = title }
|
||||||
.store(in: &popoutCancellables)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var popoutCancellables = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
// MARK: - NSWindowDelegate
|
// MARK: - NSWindowDelegate
|
||||||
|
|
||||||
|
func windowDidBecomeKey(_ notification: Notification) {
|
||||||
|
guard let window = notification.object as? NSWindow,
|
||||||
|
let entry = windows.first(where: { $0.value === window }),
|
||||||
|
let terminalView = sessions[entry.key]?.terminalView,
|
||||||
|
terminalView.window === window else { return }
|
||||||
|
|
||||||
|
window.makeFirstResponder(terminalView)
|
||||||
|
}
|
||||||
|
|
||||||
func windowWillClose(_ notification: Notification) {
|
func windowWillClose(_ notification: Notification) {
|
||||||
guard let closingWindow = notification.object as? NSWindow else { return }
|
guard let closingWindow = notification.object as? NSWindow else { return }
|
||||||
|
|
||||||
@@ -66,8 +81,7 @@ class PopoutWindowController: NSObject, NSWindowDelegate {
|
|||||||
sessions[entry.key]?.terminate()
|
sessions[entry.key]?.terminate()
|
||||||
sessions.removeValue(forKey: entry.key)
|
sessions.removeValue(forKey: entry.key)
|
||||||
windows.removeValue(forKey: entry.key)
|
windows.removeValue(forKey: entry.key)
|
||||||
|
titleObservers.removeValue(forKey: entry.key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
import Combine
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Combine
|
|||||||
class ScreenManager: ObservableObject {
|
class ScreenManager: ObservableObject {
|
||||||
|
|
||||||
static let shared = ScreenManager()
|
static let shared = ScreenManager()
|
||||||
|
private let focusRetryDelay: TimeInterval = 0.01
|
||||||
|
|
||||||
private(set) var windows: [String: NotchWindow] = [:]
|
private(set) var windows: [String: NotchWindow] = [:]
|
||||||
private(set) var viewModels: [String: NotchViewModel] = [:]
|
private(set) var viewModels: [String: NotchViewModel] = [:]
|
||||||
@@ -91,6 +92,8 @@ class ScreenManager: ObservableObject {
|
|||||||
guard let vm = viewModels[screenUUID],
|
guard let vm = viewModels[screenUUID],
|
||||||
let window = windows[screenUUID] else { return }
|
let window = windows[screenUUID] else { return }
|
||||||
|
|
||||||
|
vm.cancelCloseTransition()
|
||||||
|
|
||||||
withAnimation(vm.openAnimation) {
|
withAnimation(vm.openAnimation) {
|
||||||
vm.open()
|
vm.open()
|
||||||
}
|
}
|
||||||
@@ -102,18 +105,15 @@ class ScreenManager: ObservableObject {
|
|||||||
NSApp.activate(ignoringOtherApps: true)
|
NSApp.activate(ignoringOtherApps: true)
|
||||||
window.makeKeyAndOrderFront(nil)
|
window.makeKeyAndOrderFront(nil)
|
||||||
|
|
||||||
// After layout settles, push keyboard focus to the terminal view
|
focusActiveTerminal(in: screenUUID)
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
|
||||||
if let tv = TerminalManager.shared.activeTab?.terminalView {
|
|
||||||
window.makeFirstResponder(tv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeNotch(screenUUID: String) {
|
func closeNotch(screenUUID: String) {
|
||||||
guard let vm = viewModels[screenUUID],
|
guard let vm = viewModels[screenUUID],
|
||||||
let window = windows[screenUUID] else { return }
|
let window = windows[screenUUID] else { return }
|
||||||
|
|
||||||
|
vm.beginCloseTransition()
|
||||||
|
|
||||||
withAnimation(vm.closeAnimation) {
|
withAnimation(vm.closeAnimation) {
|
||||||
vm.close()
|
vm.close()
|
||||||
}
|
}
|
||||||
@@ -124,7 +124,9 @@ class ScreenManager: ObservableObject {
|
|||||||
|
|
||||||
private func detachActiveTab() {
|
private func detachActiveTab() {
|
||||||
if let session = TerminalManager.shared.detachActiveTab() {
|
if let session = TerminalManager.shared.detachActiveTab() {
|
||||||
PopoutWindowController.shared.popout(session: session)
|
DispatchQueue.main.async {
|
||||||
|
PopoutWindowController.shared.popout(session: session)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,4 +249,20 @@ class ScreenManager: ObservableObject {
|
|||||||
repositionWindows()
|
repositionWindows()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func focusActiveTerminal(in screenUUID: String, attemptsRemaining: Int = 12) {
|
||||||
|
guard let window = windows[screenUUID],
|
||||||
|
let terminalView = TerminalManager.shared.activeTab?.terminalView else { return }
|
||||||
|
|
||||||
|
if terminalView.window === window {
|
||||||
|
window.makeFirstResponder(terminalView)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard attemptsRemaining > 0 else { return }
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + focusRetryDelay) { [weak self] in
|
||||||
|
self?.focusActiveTerminal(in: screenUUID, attemptsRemaining: attemptsRemaining - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class NotchViewModel: ObservableObject {
|
|||||||
@Published var notchSize: CGSize
|
@Published var notchSize: CGSize
|
||||||
@Published var closedNotchSize: CGSize
|
@Published var closedNotchSize: CGSize
|
||||||
@Published var isHovering: Bool = false
|
@Published var isHovering: Bool = false
|
||||||
|
@Published var isCloseTransitionActive: Bool = false
|
||||||
|
|
||||||
let terminalManager = TerminalManager.shared
|
let terminalManager = TerminalManager.shared
|
||||||
|
|
||||||
@@ -29,6 +30,8 @@ class NotchViewModel: ObservableObject {
|
|||||||
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
|
@AppStorage(NotchSettings.Keys.closeSpringResponse) private var closeSpringResponse = NotchSettings.Defaults.closeSpringResponse
|
||||||
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
|
@AppStorage(NotchSettings.Keys.closeSpringDamping) private var closeSpringDamping = NotchSettings.Defaults.closeSpringDamping
|
||||||
|
|
||||||
|
private var closeTransitionTask: Task<Void, Never>?
|
||||||
|
|
||||||
var openAnimation: Animation {
|
var openAnimation: Animation {
|
||||||
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
|
.spring(response: openSpringResponse, dampingFraction: openSpringDamping)
|
||||||
}
|
}
|
||||||
@@ -63,4 +66,31 @@ class NotchViewModel: ObservableObject {
|
|||||||
var openNotchSize: CGSize {
|
var openNotchSize: CGSize {
|
||||||
CGSize(width: openWidth, height: openHeight)
|
CGSize(width: openWidth, height: openHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var closeInteractionLockDuration: TimeInterval {
|
||||||
|
max(closeSpringResponse + 0.2, 0.35)
|
||||||
|
}
|
||||||
|
|
||||||
|
func beginCloseTransition() {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
isCloseTransitionActive = true
|
||||||
|
|
||||||
|
let delay = closeInteractionLockDuration
|
||||||
|
closeTransitionTask = Task { @MainActor [weak self] in
|
||||||
|
try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||||
|
guard let self, !Task.isCancelled else { return }
|
||||||
|
self.isCloseTransitionActive = false
|
||||||
|
self.closeTransitionTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelCloseTransition() {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
closeTransitionTask = nil
|
||||||
|
isCloseTransitionActive = false
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
closeTransitionTask?.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "icon_16x16.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_16x16@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "16x16"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_32x32.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_32x32@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "32x32"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_128x128.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_128x128@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "128x128"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_256x256.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_256x256@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_512x512.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "1x",
|
||||||
|
"size" : "512x512"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "icon_512x512@2x.png",
|
||||||
|
"idiom" : "mac",
|
||||||
|
"scale" : "2x",
|
||||||
|
"size" : "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 945 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 735 B |
|
After Width: | Height: | Size: 290 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 958 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,15 +5,15 @@
|
|||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.downterm.app</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Downterm</string>
|
<string>CommandNotch</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|||||||
BIN
icons/.DS_Store
vendored
Normal file
BIN
icons/Downterm-icon-128.png
Normal file
|
After Width: | Height: | Size: 945 B |
BIN
icons/Downterm-icon-256.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
BIN
icons/Downterm-icon-32.png
Normal file
|
After Width: | Height: | Size: 290 B |
BIN
icons/Downterm-icon-512.png
Normal file
|
After Width: | Height: | Size: 4.3 KiB |
BIN
icons/Downterm-icon-64.png
Normal file
|
After Width: | Height: | Size: 512 B |
197
icons/Icon.svg
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
inkscape:export-batch-name="Downterm-icon"
|
||||||
|
inkscape:export-batch-path="/Users/harvmaster/Projects/Workflow/downterm/icons"
|
||||||
|
inkscape:export-filename="Downterm-icon-32.png"
|
||||||
|
inkscape:export-xdpi="6"
|
||||||
|
inkscape:export-ydpi="6"
|
||||||
|
inkscape:version="1.4.3 (0d15f75, 2025-12-25)"
|
||||||
|
sodipodi:docname="Icon.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
showguides="true"
|
||||||
|
inkscape:zoom="1.6884766"
|
||||||
|
inkscape:cx="175.00983"
|
||||||
|
inkscape:cy="258.81319"
|
||||||
|
inkscape:window-width="2260"
|
||||||
|
inkscape:window-height="1251"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="30"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer1" /><defs
|
||||||
|
id="defs1"><linearGradient
|
||||||
|
id="swatch428"
|
||||||
|
inkscape:swatch="solid"><stop
|
||||||
|
style="stop-color:#a4e0d3;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop429" /></linearGradient><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect9"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ IF,0,0,1,0,16,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect8"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ IF,0,0,1,0,16,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect7"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,16,0,1 @ F,0,0,1,0,16,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect6"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect5"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /><inkscape:path-effect
|
||||||
|
effect="fillet_chamfer"
|
||||||
|
id="path-effect4"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1"
|
||||||
|
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
|
||||||
|
radius="0"
|
||||||
|
unit="px"
|
||||||
|
method="auto"
|
||||||
|
mode="F"
|
||||||
|
chamfer_steps="1"
|
||||||
|
flexible="false"
|
||||||
|
use_knot_distance="true"
|
||||||
|
apply_no_radius="true"
|
||||||
|
apply_with_radius="true"
|
||||||
|
only_selected="false"
|
||||||
|
hide_knots="false" /></defs><g
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1"><rect
|
||||||
|
style="fill:#ddf4f7;fill-opacity:1;stroke:#ffffff;stroke-width:0;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="rect9"
|
||||||
|
width="512"
|
||||||
|
height="288"
|
||||||
|
x="0"
|
||||||
|
y="224" /><rect
|
||||||
|
style="fill:#000000;stroke-width:0.935414"
|
||||||
|
id="rect1"
|
||||||
|
width="512"
|
||||||
|
height="224"
|
||||||
|
x="0"
|
||||||
|
y="0" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect4"
|
||||||
|
width="128"
|
||||||
|
height="64"
|
||||||
|
x="191.59363"
|
||||||
|
y="255.87695"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
inkscape:path-effect="#path-effect7"
|
||||||
|
d="m 191.59363,255.87695 h 128 v 48 a 16,16 135 0 1 -16,16 h -96 a 16,16 45 0 1 -16,-16 z"
|
||||||
|
transform="matrix(1.5,0,0,1,-127.39044,-31.876953)" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect7"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
x="163.53511"
|
||||||
|
y="255.87695"
|
||||||
|
inkscape:path-effect="#path-effect8"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 163.53511,255.87695 h 16 v 16 a 16,16 45 0 0 -16,-16 z"
|
||||||
|
transform="translate(-19.53511,-31.876953)" /><path
|
||||||
|
style="fill:#000000"
|
||||||
|
id="rect8"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
x="319.96298"
|
||||||
|
y="255.85194"
|
||||||
|
inkscape:path-effect="#path-effect9"
|
||||||
|
sodipodi:type="rect"
|
||||||
|
d="m 319.96298,255.85194 h 16 a 16,16 135 0 0 -16,16 z"
|
||||||
|
transform="translate(32.037018,-31.851944)" /><g
|
||||||
|
style="fill:none;stroke:#ffffff;stroke-opacity:1"
|
||||||
|
id="g9"
|
||||||
|
transform="matrix(1.9934648,0,0,1.999414,232.01775,232.00703)"><path
|
||||||
|
id="Vector"
|
||||||
|
d="M 17,15 H 12 M 7,10 10,12.5 7,15 m -4,0.8002 v -7.6 C 3,7.08009 3,6.51962 3.21799,6.0918 3.40973,5.71547 3.71547,5.40973 4.0918,5.21799 4.51962,5 5.08009,5 6.2002,5 h 11.6 c 1.1201,0 1.6794,0 2.1072,0.21799 0.3763,0.19174 0.6831,0.49748 0.8748,0.87381 C 21,6.5192 21,7.07899 21,8.19691 v 7.60619 c 0,1.1179 0,1.6769 -0.2178,2.1043 -0.1917,0.3763 -0.4985,0.6831 -0.8748,0.8748 C 19.48,19 18.921,19 17.8031,19 H 6.19691 C 5.07899,19 4.5192,19 4.0918,18.7822 3.71547,18.5905 3.40973,18.2837 3.21799,17.9074 3,17.4796 3,16.9203 3,15.8002 Z"
|
||||||
|
stroke="#000000"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
style="stroke:#ffffff;stroke-opacity:1" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 6.7 KiB |