From f81a0c4d051e5afad9b8bc1cdd7f5032dfae702f Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Sun, 1 Mar 2026 09:30:38 +0200 Subject: [PATCH 1/2] feat: restore info bubble as SwiftUI popover on badge icons (#237) Restore the error/offline info bubble that was accidentally removed in PR #208 when InfoBubble was deleted. Replace the legacy MAAttachedWindow (2007 3rd-party ObjC library) with native SwiftUI .popover() modifiers on badge icons in HostsRowView. - Add ErrorPopoverView showing error title, description, and optional "Open in Browser" button - Make error and offline badge icons tappable with popover details - Remove dead MAAttachedWindow and NSToolbarPoofAnimator files - Clean up dead code in ListController.m left behind by PR #208 (showEditError:, locationOfHosts:, handleHostsFileRemoval:, etc.) --- Gas Mask.xcodeproj/project.pbxproj | 12 +- Source/3rd Party/MAAttachedWindow.h | 184 ------ Source/3rd Party/MAAttachedWindow.m | 951 ---------------------------- Source/ListController.m | 87 --- Source/NSToolbarPoofAnimator.h | 27 - Source/Swift/ErrorPopoverView.swift | 30 + Source/Swift/HostsRowView.swift | 70 +- 7 files changed, 92 insertions(+), 1269 deletions(-) delete mode 100644 Source/3rd Party/MAAttachedWindow.h delete mode 100644 Source/3rd Party/MAAttachedWindow.m delete mode 100644 Source/NSToolbarPoofAnimator.h create mode 100644 Source/Swift/ErrorPopoverView.swift diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index 51ddacc..631b423 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -114,7 +114,6 @@ 35E32D841211D52F00B2C631 /* OfflineBadge.m in Sources */ = {isa = PBXBuildFile; fileRef = 35E32D831211D52F00B2C631 /* OfflineBadge.m */; }; 35E32EDC12145CF900B2C631 /* Logger.m in Sources */ = {isa = PBXBuildFile; fileRef = 35E32EDB12145CF900B2C631 /* Logger.m */; }; 35E33075121490B100B2C631 /* ExtendedNSThread.m in Sources */ = {isa = PBXBuildFile; fileRef = 35E33074121490B100B2C631 /* ExtendedNSThread.m */; }; - 35E9008A1147F42900851A25 /* MAAttachedWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 35E900891147F42900851A25 /* MAAttachedWindow.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 35F414F4152E1B7800B99583 /* VDKQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 35F414F3152E1B7800B99583 /* VDKQueue.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 35FBCA3B1223172300860FDA /* RegexKitLite.m in Sources */ = {isa = PBXBuildFile; fileRef = 35FBCA3A1223172300860FDA /* RegexKitLite.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 35FBCA511223181000860FDA /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 35FBCA501223181000860FDA /* libicucore.dylib */; }; @@ -145,6 +144,7 @@ AA00001E000000000000AAAA /* CombinedHostsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */; }; AA000020000000000000AAAA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001F000000000000AAAA /* ContentView.swift */; }; AA000022000000000000AAAA /* ContentInstaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000021000000000000AAAA /* ContentInstaller.swift */; }; + AA000024000000000000AAAA /* ErrorPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000023000000000000AAAA /* ErrorPopoverView.swift */; }; BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000001000000000000BBBB /* RemoteIntervalMapper.swift */; }; BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000003000000000000BBBB /* ShortcutRecorderView.swift */; }; CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000CC01 /* GlobalShortcuts.swift */; }; @@ -278,7 +278,6 @@ 354C0BC410E782A5005B9A33 /* hosts.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; name = hosts.icns; path = Resources/hosts.icns; sourceTree = ""; }; 354CCF7F1117285300EB6948 /* HostsListViewMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HostsListViewMenu.h; path = Source/HostsListViewMenu.h; sourceTree = ""; }; 354CCF801117285300EB6948 /* HostsListViewMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HostsListViewMenu.m; path = Source/HostsListViewMenu.m; sourceTree = ""; }; - 354DDCED114EAC5000DB76D7 /* NSToolbarPoofAnimator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NSToolbarPoofAnimator.h; path = Source/NSToolbarPoofAnimator.h; sourceTree = ""; }; 354E7E3E10AEB09D00FC4757 /* HostsMainController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HostsMainController.h; path = Source/HostsMainController.h; sourceTree = ""; }; 354E7E3F10AEB09D00FC4757 /* HostsMainController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HostsMainController.m; path = Source/HostsMainController.m; sourceTree = ""; }; 354E7E7210AEB25100FC4757 /* Hosts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Hosts.h; path = Source/Hosts.h; sourceTree = ""; }; @@ -347,6 +346,7 @@ AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombinedHostsPickerView.swift; path = "Source/Swift/CombinedHostsPickerView.swift"; sourceTree = ""; }; AA00001F000000000000AAAA /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = "Source/Swift/ContentView.swift"; sourceTree = ""; }; AA000021000000000000AAAA /* ContentInstaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContentInstaller.swift; path = "Source/Swift/ContentInstaller.swift"; sourceTree = ""; }; + AA000023000000000000AAAA /* ErrorPopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ErrorPopoverView.swift; path = "Source/Swift/ErrorPopoverView.swift"; sourceTree = ""; }; 35A183A71A0ACF37002D6289 /* menuIcon@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "menuIcon@2x.tiff"; path = "Resources/Images/menuIcon@2x.tiff"; sourceTree = ""; }; BB000001000000000000BBBB /* RemoteIntervalMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteIntervalMapper.swift; path = "Source/Swift/RemoteIntervalMapper.swift"; sourceTree = ""; }; BB000003000000000000BBBB /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShortcutRecorderView.swift; path = "Source/Swift/ShortcutRecorderView.swift"; sourceTree = ""; }; @@ -385,8 +385,6 @@ 35E32EDB12145CF900B2C631 /* Logger.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Logger.m; path = Source/Logger.m; sourceTree = ""; }; 35E33073121490B100B2C631 /* ExtendedNSThread.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ExtendedNSThread.h; path = Source/ExtendedNSThread.h; sourceTree = ""; }; 35E33074121490B100B2C631 /* ExtendedNSThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ExtendedNSThread.m; path = Source/ExtendedNSThread.m; sourceTree = ""; }; - 35E900881147F42900851A25 /* MAAttachedWindow.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = MAAttachedWindow.h; path = "Source/3rd Party/MAAttachedWindow.h"; sourceTree = ""; }; - 35E900891147F42900851A25 /* MAAttachedWindow.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = MAAttachedWindow.m; path = "Source/3rd Party/MAAttachedWindow.m"; sourceTree = ""; }; 35F414F2152E1B7800B99583 /* VDKQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = VDKQueue.h; path = "Source/3rd Party/VDKQueue.h"; sourceTree = ""; }; 35F414F3152E1B7800B99583 /* VDKQueue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = VDKQueue.m; path = "Source/3rd Party/VDKQueue.m"; sourceTree = ""; }; 35FBCA391223172300860FDA /* RegexKitLite.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RegexKitLite.h; path = "Source/3rd Party/RegexKitLite.h"; sourceTree = ""; }; @@ -794,6 +792,7 @@ AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */, AA00001F000000000000AAAA /* ContentView.swift */, AA000021000000000000AAAA /* ContentInstaller.swift */, + AA000023000000000000AAAA /* ErrorPopoverView.swift */, BB000020000000000000BBBB /* Preferences */, ); name = Swift; @@ -830,7 +829,6 @@ 35D4C98210E64D8800B9F63A /* Extended Next Step */ = { isa = PBXGroup; children = ( - 354DDCED114EAC5000DB76D7 /* NSToolbarPoofAnimator.h */, 35D4CBF910E6934700B9F63A /* ExtendedNSTextView.h */, 35D4CBFA10E6934700B9F63A /* ExtendedNSTextView.m */, 353A80C810B01F320005CAD1 /* ExtendedNSString.h */, @@ -865,8 +863,6 @@ 35F414F3152E1B7800B99583 /* VDKQueue.m */, 35FBCA391223172300860FDA /* RegexKitLite.h */, 35FBCA3A1223172300860FDA /* RegexKitLite.m */, - 35E900881147F42900851A25 /* MAAttachedWindow.h */, - 35E900891147F42900851A25 /* MAAttachedWindow.m */, ); name = "3rd Party"; sourceTree = ""; @@ -1126,6 +1122,7 @@ AA00001E000000000000AAAA /* CombinedHostsPickerView.swift in Sources */, AA000020000000000000AAAA /* ContentView.swift in Sources */, AA000022000000000000AAAA /* ContentInstaller.swift in Sources */, + AA000024000000000000AAAA /* ErrorPopoverView.swift in Sources */, BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */, BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */, CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */, @@ -1134,7 +1131,6 @@ BB00000A000000000000BBBB /* PreferencesView.swift in Sources */, BB00000C000000000000BBBB /* PreferencesPresenter.swift in Sources */, 356DB76A1824EAFD0020CEA0 /* ExtendedNSSplitView.m in Sources */, - 35E9008A1147F42900851A25 /* MAAttachedWindow.m in Sources */, 350E7D3E121093E400D2F5F5 /* AlertBadge.m in Sources */, 350E7D7E12111B2300D2F5F5 /* Badge.m in Sources */, 350E7D941211A7AE00D2F5F5 /* BadgeManager.m in Sources */, diff --git a/Source/3rd Party/MAAttachedWindow.h b/Source/3rd Party/MAAttachedWindow.h deleted file mode 100644 index cb18001..0000000 --- a/Source/3rd Party/MAAttachedWindow.h +++ /dev/null @@ -1,184 +0,0 @@ -// -// MAAttachedWindow.h -// -// Created by Matt Gemmell on 27/09/2007. -// Copyright 2007 Magic Aubergine. -// - -#import - -/* - Below are the positions the attached window can be displayed at. - - Note that these positions are relative to the point passed to the constructor, - e.g. MAPositionBottomRight will put the window below the point and towards the right, - MAPositionTop will horizontally center the window above the point, - MAPositionRightTop will put the window to the right and above the point, - and so on. - - You can also pass MAPositionAutomatic (or use an initializer which omits the 'onSide:' - argument) and the attached window will try to position itself sensibly, based on - available screen-space. - - Notes regarding automatically-positioned attached windows: - - (a) The window prefers to position itself horizontally centered below the specified point. - This gives a certain enhanced visual sense of an attachment/relationship. - - (b) The window will try to align itself with its parent window (if any); i.e. it will - attempt to stay within its parent window's frame if it can. - - (c) The algorithm isn't perfect. :) If in doubt, do your own calculations and then - explicitly request that the window attach itself to a particular side. - */ - -typedef enum _MAWindowPosition { - // The four primary sides are compatible with the preferredEdge of NSDrawer. - MAPositionLeft = NSMinXEdge, // 0 - MAPositionRight = NSMaxXEdge, // 2 - MAPositionTop = NSMaxYEdge, // 3 - MAPositionBottom = NSMinYEdge, // 1 - MAPositionLeftTop = 4, - MAPositionLeftBottom = 5, - MAPositionRightTop = 6, - MAPositionRightBottom = 7, - MAPositionTopLeft = 8, - MAPositionTopRight = 9, - MAPositionBottomLeft = 10, - MAPositionBottomRight = 11, - MAPositionAutomatic = 12 -} MAWindowPosition; - -@interface MAAttachedWindow : NSWindow { - NSColor *borderColor; - float borderWidth; - float viewMargin; - float arrowBaseWidth; - float arrowHeight; - BOOL hasArrow; - float cornerRadius; - BOOL drawsRoundCornerBesideArrow; - - @private - NSColor *_MABackgroundColor; - NSView *_view; - NSWindow *_window; - NSPoint _point; - MAWindowPosition _side; - float _distance; - NSRect _viewFrame; - BOOL _resizing; -} - -/* - Initialization methods - - Parameters: - - view The view to display in the attached window. Must not be nil. - - point The point to which the attached window should be attached. If you - are also specifying a parent window, the point should be in the - coordinate system of that parent window. If you are not specifying - a window, the point should be in the screen's coordinate space. - This value is required. - - window The parent window to attach this one to. Note that no actual - relationship is created (particularly, this window is not made - a childWindow of the parent window). - Default: nil. - - side The side of the specified point on which to attach this window. - Default: MAPositionAutomatic. - - distance How far from the specified point this window should be. - Default: 0. - */ - -- (MAAttachedWindow *)initWithView:(NSView *)view // designated initializer - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window - onSide:(MAWindowPosition)side - atDistance:(float)distance; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window - atDistance:(float)distance; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - onSide:(MAWindowPosition)side - atDistance:(float)distance; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - atDistance:(float)distance; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - onSide:(MAWindowPosition)side; -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point; - -// Accessor methods -- (void)setPoint:(NSPoint)point side:(MAWindowPosition)side; -- (NSColor *)borderColor; -- (void)setBorderColor:(NSColor *)value; -- (float)borderWidth; -- (void)setBorderWidth:(float)value; // See note 1 below. -- (float)viewMargin; -- (void)setViewMargin:(float)value; // See note 2 below. -- (float)arrowBaseWidth; -- (void)setArrowBaseWidth:(float)value; // See note 2 below. -- (float)arrowHeight; -- (void)setArrowHeight:(float)value; // See note 2 below. -- (float)hasArrow; -- (void)setHasArrow:(float)value; -- (float)cornerRadius; -- (void)setCornerRadius:(float)value; // See note 2 below. -- (float)drawsRoundCornerBesideArrow; // See note 3 below. -- (void)setDrawsRoundCornerBesideArrow:(float)value; // See note 2 below. -- (void)setBackgroundImage:(NSImage *)value; -- (NSColor *)windowBackgroundColor; // See note 4 below. -- (void)setBackgroundColor:(NSColor *)value; - -/* - Notes regarding accessor methods: - - 1. The border is drawn inside the viewMargin area, expanding inwards; it does not - increase the width/height of the window. You can use the -setBorderWidth: and - -setViewMargin: methods together to achieve the exact look/geometry you want. - (viewMargin is the distance between the edge of the view and the window edge.) - - 2. The specified setter methods are primarily intended to be used _before_ the window - is first shown. If you use them while the window is already visible, be aware - that they may cause the window to move and/or resize, in order to stay anchored - to the point specified in the initializer. They may also cause the view to move - within the window, in order to remain centered there. - - Note that the -setHasArrow: method can safely be used at any time, and will not - cause moving/resizing of the window. This is for convenience, in case you want - to add or remove the arrow in response to user interaction. For example, you - could make the attached window movable by its background, and if the user dragged - it away from its initial point, the arrow could be removed. This would duplicate - how Aperture's attached windows behave. - - 3. drawsRoundCornerBesideArrow takes effect when the arrow is being drawn at a corner, - i.e. when it's not at one of the four primary compass directions. In this situation, - if drawsRoundCornerBesideArrow is YES (the default), then that corner of the window - will be rounded just like the other three corners, thus the arrow will be inset - slightly from the edge of the window to allow room for the rounded corner. If this - value is NO, the corner beside the arrow will be a square corner, and the other - three corners will be rounded. - - This is useful when you want to attach a window very near the edge of another window, - and don't want the attached window's edge to be visually outside the frame of the - parent window. - - 4. Note that to retrieve the background color of the window, you should use the - -windowBackgroundColor method, instead of -backgroundColor. This is because we draw - the entire background of the window (rounded path, arrow, etc) in an NSColor pattern - image, and set it as the backgroundColor of the window. - */ - -@end diff --git a/Source/3rd Party/MAAttachedWindow.m b/Source/3rd Party/MAAttachedWindow.m deleted file mode 100644 index 8cef109..0000000 --- a/Source/3rd Party/MAAttachedWindow.m +++ /dev/null @@ -1,951 +0,0 @@ -// -// MAAttachedWindow.m -// -// Created by Matt Gemmell on 27/09/2007. -// Copyright 2007 Magic Aubergine. -// - -#import "MAAttachedWindow.h" - -#define MAATTACHEDWINDOW_DEFAULT_BACKGROUND_COLOR [NSColor colorWithCalibratedWhite:0.1 alpha:0.75] -#define MAATTACHEDWINDOW_DEFAULT_BORDER_COLOR [NSColor whiteColor] -#define MAATTACHEDWINDOW_SCALE_FACTOR [[NSScreen mainScreen] backingScaleFactor] - -@interface MAAttachedWindow (MAPrivateMethods) - -// Geometry -- (void)_updateGeometry; -- (MAWindowPosition)_bestSideForAutomaticPosition; -- (float)_arrowInset; - -// Drawing -- (void)_updateBackground; -- (NSColor *)_backgroundColorPatternImage; -- (NSBezierPath *)_backgroundPath; -- (void)_appendArrowToPath:(NSBezierPath *)path; -- (void)_redisplay; - -@end - -@implementation MAAttachedWindow - - -#pragma mark Initializers - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window - onSide:(MAWindowPosition)side - atDistance:(float)distance -{ - // Insist on having a valid view. - if (!view) { - return nil; - } - - // Create dummy initial contentRect for window. - NSRect contentRect = NSZeroRect; - contentRect.size = [view frame].size; - - if ((self = [super initWithContentRect:contentRect - styleMask:NSBorderlessWindowMask - backing:NSBackingStoreBuffered - defer:NO])) { - _view = view; - _window = window; - _point = point; - _side = side; - _distance = distance; - - // Configure window characteristics. - [super setBackgroundColor:[NSColor clearColor]]; - [self setMovableByWindowBackground:NO]; - [self setExcludedFromWindowsMenu:YES]; - [self setAlphaValue:1.0]; - [self setOpaque:NO]; - [self setHasShadow:YES]; - [self useOptimizedDrawing:YES]; - - // Set up some sensible defaults for display. - _MABackgroundColor = [MAATTACHEDWINDOW_DEFAULT_BACKGROUND_COLOR copy]; - borderColor = [MAATTACHEDWINDOW_DEFAULT_BORDER_COLOR copy]; - borderWidth = 2.0; - viewMargin = 2.0; - arrowBaseWidth = 20.0; - arrowHeight = 16.0; - hasArrow = YES; - cornerRadius = 8.0; - drawsRoundCornerBesideArrow = YES; - _resizing = NO; - - // Work out what side to put the window on if it's "automatic". - if (_side == MAPositionAutomatic) { - _side = [self _bestSideForAutomaticPosition]; - } - - // Configure our initial geometry. - [self _updateGeometry]; - - // Update the background. - [self _updateBackground]; - - // Add view as subview of our contentView. - [[self contentView] addSubview:_view]; - - // Subscribe to notifications for when we change size. - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(windowDidResize:) - name:NSWindowDidResizeNotification - object:self]; - } - return self; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window - atDistance:(float)distance -{ - return [self initWithView:view attachedToPoint:point - inWindow:window onSide:MAPositionAutomatic - atDistance:distance]; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - onSide:(MAWindowPosition)side - atDistance:(float)distance -{ - return [self initWithView:view attachedToPoint:point - inWindow:nil onSide:side - atDistance:distance]; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - atDistance:(float)distance -{ - return [self initWithView:view attachedToPoint:point - inWindow:nil onSide:MAPositionAutomatic - atDistance:distance]; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - inWindow:(NSWindow *)window -{ - return [self initWithView:view attachedToPoint:point - inWindow:window onSide:MAPositionAutomatic - atDistance:0]; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point - onSide:(MAWindowPosition)side -{ - return [self initWithView:view attachedToPoint:point - inWindow:nil onSide:side - atDistance:0]; -} - - -- (MAAttachedWindow *)initWithView:(NSView *)view - attachedToPoint:(NSPoint)point -{ - return [self initWithView:view attachedToPoint:point - inWindow:nil onSide:MAPositionAutomatic - atDistance:0]; -} - - -- (void)dealloc -{ - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [borderColor release]; - [_MABackgroundColor release]; - - [super dealloc]; -} - - -#pragma mark Geometry - - -- (void)_updateGeometry -{ - NSRect contentRect = NSZeroRect; - contentRect.size = [_view frame].size; - - // Account for viewMargin. - _viewFrame = NSMakeRect(viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR, - viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR, - [_view frame].size.width, [_view frame].size.height); - contentRect = NSInsetRect(contentRect, - -viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR, - -viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR); - - // Account for arrowHeight in new window frame. - // Note: we always leave room for the arrow, even if it currently set to - // not be shown. This is so it can easily be toggled whilst the window - // is visible, without altering the window's frame origin point. - float scaledArrowHeight = arrowHeight * MAATTACHEDWINDOW_SCALE_FACTOR; - switch (_side) { - case MAPositionLeft: - case MAPositionLeftTop: - case MAPositionLeftBottom: - contentRect.size.width += scaledArrowHeight; - break; - case MAPositionRight: - case MAPositionRightTop: - case MAPositionRightBottom: - _viewFrame.origin.x += scaledArrowHeight; - contentRect.size.width += scaledArrowHeight; - break; - case MAPositionTop: - case MAPositionTopLeft: - case MAPositionTopRight: - _viewFrame.origin.y += scaledArrowHeight; - contentRect.size.height += scaledArrowHeight; - break; - case MAPositionBottom: - case MAPositionBottomLeft: - case MAPositionBottomRight: - contentRect.size.height += scaledArrowHeight; - break; - default: - break; // won't happen, but this satisfies gcc with -Wall - } - - // Position frame origin appropriately for _side, accounting for arrow-inset. - contentRect.origin = (_window) ? [_window convertBaseToScreen:_point] : _point; - float arrowInset = [self _arrowInset]; - float halfWidth = contentRect.size.width / 2.0; - float halfHeight = contentRect.size.height / 2.0; - switch (_side) { - case MAPositionTopLeft: - contentRect.origin.x -= contentRect.size.width - arrowInset; - break; - case MAPositionTop: - contentRect.origin.x -= halfWidth; - break; - case MAPositionTopRight: - contentRect.origin.x -= arrowInset; - break; - case MAPositionBottomLeft: - contentRect.origin.y -= contentRect.size.height; - contentRect.origin.x -= contentRect.size.width - arrowInset; - break; - case MAPositionBottom: - contentRect.origin.y -= contentRect.size.height; - contentRect.origin.x -= halfWidth; - break; - case MAPositionBottomRight: - contentRect.origin.x -= arrowInset; - contentRect.origin.y -= contentRect.size.height; - break; - case MAPositionLeftTop: - contentRect.origin.x -= contentRect.size.width; - contentRect.origin.y -= arrowInset; - break; - case MAPositionLeft: - contentRect.origin.x -= contentRect.size.width; - contentRect.origin.y -= halfHeight; - break; - case MAPositionLeftBottom: - contentRect.origin.x -= contentRect.size.width; - contentRect.origin.y -= contentRect.size.height - arrowInset; - break; - case MAPositionRightTop: - contentRect.origin.y -= arrowInset; - break; - case MAPositionRight: - contentRect.origin.y -= halfHeight; - break; - case MAPositionRightBottom: - contentRect.origin.y -= contentRect.size.height - arrowInset; - break; - default: - break; // won't happen, but this satisfies gcc with -Wall - } - - // Account for _distance in new window frame. - switch (_side) { - case MAPositionLeft: - case MAPositionLeftTop: - case MAPositionLeftBottom: - contentRect.origin.x -= _distance; - break; - case MAPositionRight: - case MAPositionRightTop: - case MAPositionRightBottom: - contentRect.origin.x += _distance; - break; - case MAPositionTop: - case MAPositionTopLeft: - case MAPositionTopRight: - contentRect.origin.y += _distance; - break; - case MAPositionBottom: - case MAPositionBottomLeft: - case MAPositionBottomRight: - contentRect.origin.y -= _distance; - break; - default: - break; // won't happen, but this satisfies gcc with -Wall - } - - // Reconfigure window and view frames appropriately. - [self setFrame:contentRect display:NO]; - [_view setFrame:_viewFrame]; -} - - -- (MAWindowPosition)_bestSideForAutomaticPosition -{ - // Get all relevant geometry in screen coordinates. - NSRect screenFrame; - if (_window && [_window screen]) { - screenFrame = [[_window screen] visibleFrame]; - } else { - screenFrame = [[NSScreen mainScreen] visibleFrame]; - } - NSPoint pointOnScreen = (_window) ? [_window convertBaseToScreen:_point] : _point; - NSSize viewSize = [_view frame].size; - viewSize.width += (viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR) * 2.0; - viewSize.height += (viewMargin * MAATTACHEDWINDOW_SCALE_FACTOR) * 2.0; - MAWindowPosition side = MAPositionBottom; // By default, position us centered below. - float scaledArrowHeight = (arrowHeight * MAATTACHEDWINDOW_SCALE_FACTOR) + _distance; - - // We'd like to display directly below the specified point, since this gives a - // sense of a relationship between the point and this window. Check there's room. - if (pointOnScreen.y - viewSize.height - scaledArrowHeight < NSMinY(screenFrame)) { - // We'd go off the bottom of the screen. Try the right. - if (pointOnScreen.x + viewSize.width + scaledArrowHeight >= NSMaxX(screenFrame)) { - // We'd go off the right of the screen. Try the left. - if (pointOnScreen.x - viewSize.width - scaledArrowHeight < NSMinX(screenFrame)) { - // We'd go off the left of the screen. Try the top. - if (pointOnScreen.y + viewSize.height + scaledArrowHeight < NSMaxY(screenFrame)) { - side = MAPositionTop; - } - } else { - side = MAPositionLeft; - } - } else { - side = MAPositionRight; - } - } - - float halfWidth = viewSize.width / 2.0; - float halfHeight = viewSize.height / 2.0; - - NSRect parentFrame = (_window) ? [_window frame] : screenFrame; - float arrowInset = [self _arrowInset]; - - // We're currently at a primary side. - // Try to avoid going outwith the parent area in the secondary dimension, - // by checking to see if an appropriate corner side would be better. - switch (side) { - case MAPositionBottom: - case MAPositionTop: - // Check to see if we go beyond the left edge of the parent area. - if (pointOnScreen.x - halfWidth < NSMinX(parentFrame)) { - // We go beyond the left edge. Try using right position. - if (pointOnScreen.x + viewSize.width - arrowInset < NSMaxX(screenFrame)) { - // We'd still be on-screen using right, so use it. - if (side == MAPositionBottom) { - side = MAPositionBottomRight; - } else { - side = MAPositionTopRight; - } - } - } else if (pointOnScreen.x + halfWidth >= NSMaxX(parentFrame)) { - // We go beyond the right edge. Try using left position. - if (pointOnScreen.x - viewSize.width + arrowInset >= NSMinX(screenFrame)) { - // We'd still be on-screen using left, so use it. - if (side == MAPositionBottom) { - side = MAPositionBottomLeft; - } else { - side = MAPositionTopLeft; - } - } - } - break; - case MAPositionRight: - case MAPositionLeft: - // Check to see if we go beyond the bottom edge of the parent area. - if (pointOnScreen.y - halfHeight < NSMinY(parentFrame)) { - // We go beyond the bottom edge. Try using top position. - if (pointOnScreen.y + viewSize.height - arrowInset < NSMaxY(screenFrame)) { - // We'd still be on-screen using top, so use it. - if (side == MAPositionRight) { - side = MAPositionRightTop; - } else { - side = MAPositionLeftTop; - } - } - } else if (pointOnScreen.y + halfHeight >= NSMaxY(parentFrame)) { - // We go beyond the top edge. Try using bottom position. - if (pointOnScreen.y - viewSize.height + arrowInset >= NSMinY(screenFrame)) { - // We'd still be on-screen using bottom, so use it. - if (side == MAPositionRight) { - side = MAPositionRightBottom; - } else { - side = MAPositionLeftBottom; - } - } - } - break; - default: - break; // won't happen, but this satisfies gcc with -Wall - } - - return side; -} - - -- (float)_arrowInset -{ - float cornerInset = (drawsRoundCornerBesideArrow) ? cornerRadius : 0; - return (cornerInset + (arrowBaseWidth / 2.0)) * MAATTACHEDWINDOW_SCALE_FACTOR; -} - - -#pragma mark Drawing - - -- (void)_updateBackground -{ - // Call NSWindow's implementation of -setBackgroundColor: because we override - // it in this class to let us set the entire background image of the window - // as an NSColor patternImage. - NSDisableScreenUpdates(); - [super setBackgroundColor:[self _backgroundColorPatternImage]]; - if ([self isVisible]) { - [self display]; - [self invalidateShadow]; - } - NSEnableScreenUpdates(); -} - - -- (NSColor *)_backgroundColorPatternImage -{ - NSImage *bg = [[NSImage alloc] initWithSize:[self frame].size]; - NSRect bgRect = NSZeroRect; - bgRect.size = [bg size]; - - [bg lockFocus]; - NSBezierPath *bgPath = [self _backgroundPath]; - [NSGraphicsContext saveGraphicsState]; - [bgPath addClip]; - - // Draw background. - [_MABackgroundColor set]; - [bgPath fill]; - - // Draw border if appropriate. - if (borderWidth > 0) { - // Double the borderWidth since we're drawing inside the path. - [bgPath setLineWidth:(borderWidth * 2.0) * MAATTACHEDWINDOW_SCALE_FACTOR]; - [borderColor set]; - [bgPath stroke]; - } - - [NSGraphicsContext restoreGraphicsState]; - [bg unlockFocus]; - - return [NSColor colorWithPatternImage:[bg autorelease]]; -} - - -- (NSBezierPath *)_backgroundPath -{ - /* - Construct path for window background, taking account of: - 1. hasArrow - 2. _side - 3. drawsRoundCornerBesideArrow - 4. arrowBaseWidth - 5. arrowHeight - 6. cornerRadius - */ - - float scaleFactor = MAATTACHEDWINDOW_SCALE_FACTOR; - float scaledRadius = cornerRadius * scaleFactor; - float scaledArrowWidth = arrowBaseWidth * scaleFactor; - float halfArrowWidth = scaledArrowWidth / 2.0; - NSRect contentArea = NSInsetRect(_viewFrame, - -viewMargin * scaleFactor, - -viewMargin * scaleFactor); - float minX = ceilf(NSMinX(contentArea) * scaleFactor + 0.5f); - float midX = NSMidX(contentArea) * scaleFactor; - float maxX = floorf(NSMaxX(contentArea) * scaleFactor - 0.5f); - float minY = ceilf(NSMinY(contentArea) * scaleFactor + 0.5f); - float midY = NSMidY(contentArea) * scaleFactor; - float maxY = floorf(NSMaxY(contentArea) * scaleFactor - 0.5f); - - NSBezierPath *path = [NSBezierPath bezierPath]; - [path setLineJoinStyle:NSRoundLineJoinStyle]; - - // Begin at top-left. This will be either after the rounded corner, or - // at the top-left point if cornerRadius is zero and/or we're drawing - // the arrow at the top-left or left-top without a rounded corner. - NSPoint currPt = NSMakePoint(minX, maxY); - if (scaledRadius > 0 && - (drawsRoundCornerBesideArrow || - (_side != MAPositionBottomRight && _side != MAPositionRightBottom)) - ) { - currPt.x += scaledRadius; - } - - NSPoint endOfLine = NSMakePoint(maxX, maxY); - BOOL shouldDrawNextCorner = NO; - if (scaledRadius > 0 && - (drawsRoundCornerBesideArrow || - (_side != MAPositionBottomLeft && _side != MAPositionLeftBottom)) - ) { - endOfLine.x -= scaledRadius; - shouldDrawNextCorner = YES; - } - - [path moveToPoint:currPt]; - - // If arrow should be drawn at top-left point, draw it. - if (_side == MAPositionBottomRight) { - [self _appendArrowToPath:path]; - } else if (_side == MAPositionBottom) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(midX - halfArrowWidth, maxY)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } else if (_side == MAPositionBottomLeft) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(endOfLine.x - scaledArrowWidth, maxY)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } - - // Line to end of this side. - [path lineToPoint:endOfLine]; - - // Rounded corner on top-right. - if (shouldDrawNextCorner) { - [path appendBezierPathWithArcFromPoint:NSMakePoint(maxX, maxY) - toPoint:NSMakePoint(maxX, maxY - scaledRadius) - radius:scaledRadius]; - } - - - // Draw the right side, beginning at the top-right. - endOfLine = NSMakePoint(maxX, minY); - shouldDrawNextCorner = NO; - if (scaledRadius > 0 && - (drawsRoundCornerBesideArrow || - (_side != MAPositionTopLeft && _side != MAPositionLeftTop)) - ) { - endOfLine.y += scaledRadius; - shouldDrawNextCorner = YES; - } - - // If arrow should be drawn at right-top point, draw it. - if (_side == MAPositionLeftBottom) { - [self _appendArrowToPath:path]; - } else if (_side == MAPositionLeft) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(maxX, midY + halfArrowWidth)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } else if (_side == MAPositionLeftTop) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(maxX, endOfLine.y + scaledArrowWidth)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } - - // Line to end of this side. - [path lineToPoint:endOfLine]; - - // Rounded corner on bottom-right. - if (shouldDrawNextCorner) { - [path appendBezierPathWithArcFromPoint:NSMakePoint(maxX, minY) - toPoint:NSMakePoint(maxX - scaledRadius, minY) - radius:scaledRadius]; - } - - - // Draw the bottom side, beginning at the bottom-right. - endOfLine = NSMakePoint(minX, minY); - shouldDrawNextCorner = NO; - if (scaledRadius > 0 && - (drawsRoundCornerBesideArrow || - (_side != MAPositionTopRight && _side != MAPositionRightTop)) - ) { - endOfLine.x += scaledRadius; - shouldDrawNextCorner = YES; - } - - // If arrow should be drawn at bottom-right point, draw it. - if (_side == MAPositionTopLeft) { - [self _appendArrowToPath:path]; - } else if (_side == MAPositionTop) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(midX + halfArrowWidth, minY)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } else if (_side == MAPositionTopRight) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(endOfLine.x + scaledArrowWidth, minY)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } - - // Line to end of this side. - [path lineToPoint:endOfLine]; - - // Rounded corner on bottom-left. - if (shouldDrawNextCorner) { - [path appendBezierPathWithArcFromPoint:NSMakePoint(minX, minY) - toPoint:NSMakePoint(minX, minY + scaledRadius) - radius:scaledRadius]; - } - - - // Draw the left side, beginning at the bottom-left. - endOfLine = NSMakePoint(minX, maxY); - shouldDrawNextCorner = NO; - if (scaledRadius > 0 && - (drawsRoundCornerBesideArrow || - (_side != MAPositionRightBottom && _side != MAPositionBottomRight)) - ) { - endOfLine.y -= scaledRadius; - shouldDrawNextCorner = YES; - } - - // If arrow should be drawn at left-bottom point, draw it. - if (_side == MAPositionRightTop) { - [self _appendArrowToPath:path]; - } else if (_side == MAPositionRight) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(minX, midY - halfArrowWidth)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } else if (_side == MAPositionRightBottom) { - // Line to relevant point before arrow. - [path lineToPoint:NSMakePoint(minX, endOfLine.y - scaledArrowWidth)]; - // Draw arrow. - [self _appendArrowToPath:path]; - } - - // Line to end of this side. - [path lineToPoint:endOfLine]; - - // Rounded corner on top-left. - if (shouldDrawNextCorner) { - [path appendBezierPathWithArcFromPoint:NSMakePoint(minX, maxY) - toPoint:NSMakePoint(minX + scaledRadius, maxY) - radius:scaledRadius]; - } - - [path closePath]; - return path; -} - - -- (void)_appendArrowToPath:(NSBezierPath *)path -{ - if (!hasArrow) { - return; - } - - float scaleFactor = MAATTACHEDWINDOW_SCALE_FACTOR; - float scaledArrowWidth = arrowBaseWidth * scaleFactor; - float halfArrowWidth = scaledArrowWidth / 2.0; - float scaledArrowHeight = arrowHeight * scaleFactor; - NSPoint currPt = [path currentPoint]; - NSPoint tipPt = currPt; - NSPoint endPt = currPt; - - // Note: we always build the arrow path in a clockwise direction. - switch (_side) { - case MAPositionLeft: - case MAPositionLeftTop: - case MAPositionLeftBottom: - // Arrow points towards right. We're starting from the top. - tipPt.x += scaledArrowHeight; - tipPt.y -= halfArrowWidth; - endPt.y -= scaledArrowWidth; - break; - case MAPositionRight: - case MAPositionRightTop: - case MAPositionRightBottom: - // Arrow points towards left. We're starting from the bottom. - tipPt.x -= scaledArrowHeight; - tipPt.y += halfArrowWidth; - endPt.y += scaledArrowWidth; - break; - case MAPositionTop: - case MAPositionTopLeft: - case MAPositionTopRight: - // Arrow points towards bottom. We're starting from the right. - tipPt.y -= scaledArrowHeight; - tipPt.x -= halfArrowWidth; - endPt.x -= scaledArrowWidth; - break; - case MAPositionBottom: - case MAPositionBottomLeft: - case MAPositionBottomRight: - // Arrow points towards top. We're starting from the left. - tipPt.y += scaledArrowHeight; - tipPt.x += halfArrowWidth; - endPt.x += scaledArrowWidth; - break; - default: - break; // won't happen, but this satisfies gcc with -Wall - } - - [path lineToPoint:tipPt]; - [path lineToPoint:endPt]; -} - - -- (void)_redisplay -{ - if (_resizing) { - return; - } - - _resizing = YES; - NSDisableScreenUpdates(); - [self _updateGeometry]; - [self _updateBackground]; - NSEnableScreenUpdates(); - _resizing = NO; -} - - -# pragma mark Window Behaviour - - -- (BOOL)canBecomeMainWindow -{ - return NO; -} - - -- (BOOL)canBecomeKeyWindow -{ - return YES; -} - - -- (BOOL)isExcludedFromWindowsMenu -{ - return YES; -} - - -- (BOOL)validateMenuItem:(NSMenuItem *)item -{ - if (_window) { - return [_window validateMenuItem:item]; - } - return [super validateMenuItem:item]; -} - - -- (IBAction)performClose:(id)sender -{ - if (_window) { - [_window performClose:sender]; - } else { - [super performClose:sender]; - } -} - - -# pragma mark Notification handlers - - -- (void)windowDidResize:(NSNotification *)note -{ - [self _redisplay]; -} - - -#pragma mark Accessors - - -- (void)setPoint:(NSPoint)point side:(MAWindowPosition)side -{ - // Thanks to Martin Redington. - _point = point; - _side = side; - NSDisableScreenUpdates(); - [self _updateGeometry]; - [self _updateBackground]; - NSEnableScreenUpdates(); -} - - -- (NSColor *)windowBackgroundColor { - return [[_MABackgroundColor retain] autorelease]; -} - - -- (void)setBackgroundColor:(NSColor *)value { - if (_MABackgroundColor != value) { - [_MABackgroundColor release]; - _MABackgroundColor = [value copy]; - - [self _updateBackground]; - } -} - - -- (NSColor *)borderColor { - return [[borderColor retain] autorelease]; -} - - -- (void)setBorderColor:(NSColor *)value { - if (borderColor != value) { - [borderColor release]; - borderColor = [value copy]; - - [self _updateBackground]; - } -} - - -- (float)borderWidth { - return borderWidth; -} - - -- (void)setBorderWidth:(float)value { - if (borderWidth != value) { - float maxBorderWidth = viewMargin; - if (value <= maxBorderWidth) { - borderWidth = value; - } else { - borderWidth = maxBorderWidth; - } - - [self _updateBackground]; - } -} - - -- (float)viewMargin { - return viewMargin; -} - - -- (void)setViewMargin:(float)value { - if (viewMargin != value) { - viewMargin = MAX(value, 0.0); - - // Adjust cornerRadius appropriately (which will also adjust arrowBaseWidth). - [self setCornerRadius:cornerRadius]; - } -} - - -- (float)arrowBaseWidth { - return arrowBaseWidth; -} - - -- (void)setArrowBaseWidth:(float)value { - float maxWidth = (MIN(_viewFrame.size.width, _viewFrame.size.height) + - (viewMargin * 2.0)) - cornerRadius; - if (drawsRoundCornerBesideArrow) { - maxWidth -= cornerRadius; - } - if (value <= maxWidth) { - arrowBaseWidth = value; - } else { - arrowBaseWidth = maxWidth; - } - - [self _redisplay]; -} - - -- (float)arrowHeight { - return arrowHeight; -} - - -- (void)setArrowHeight:(float)value { - if (arrowHeight != value) { - arrowHeight = value; - - [self _redisplay]; - } -} - - -- (float)hasArrow { - return hasArrow; -} - - -- (void)setHasArrow:(float)value { - if (hasArrow != value) { - hasArrow = value; - - [self _updateBackground]; - } -} - - -- (float)cornerRadius { - return cornerRadius; -} - - -- (void)setCornerRadius:(float)value { - float maxRadius = ((MIN(_viewFrame.size.width, _viewFrame.size.height) + - (viewMargin * 2.0)) - arrowBaseWidth) / 2.0; - if (value <= maxRadius) { - cornerRadius = value; - } else { - cornerRadius = maxRadius; - } - cornerRadius = MAX(cornerRadius, 0.0); - - // Adjust arrowBaseWidth appropriately. - [self setArrowBaseWidth:arrowBaseWidth]; -} - - -- (float)drawsRoundCornerBesideArrow { - return drawsRoundCornerBesideArrow; -} - - -- (void)setDrawsRoundCornerBesideArrow:(float)value { - if (drawsRoundCornerBesideArrow != value) { - drawsRoundCornerBesideArrow = value; - - [self _redisplay]; - } -} - - -- (void)setBackgroundImage:(NSImage *)value -{ - if (value) { - [self setBackgroundColor:[NSColor colorWithPatternImage:value]]; - } -} - - -@end diff --git a/Source/ListController.m b/Source/ListController.m index f1b6b22..82d6405 100644 --- a/Source/ListController.m +++ b/Source/ListController.m @@ -18,8 +18,6 @@ * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ -#import "NSToolbarPoofAnimator.h" - #import "ListController.h" #import "HostsListView.h" #import "Node.h" @@ -39,13 +37,9 @@ @interface ListController (Private) - (void)hostsFilesLoaded:(NSNotification *)notification; - (void)selectActiveHostsFile; - (void)expandAllItems; -- (void)showEditError:(NSString*)message; - (NSString*)urlFromPasteBoard:(NSPasteboard*)pasteboard; - (BOOL)allowToDropTo:(Hosts*)target; - (int)indexOfHosts:(Hosts*)hosts; -- (NSPoint)locationOfHosts:(Hosts*)hosts; -- (NSPoint)rightCenterLocationOfHosts:(Hosts*)hosts; -- (NSPoint)centerLocationOfHostsOnScreen:(Hosts*)hosts; @end @implementation ListController @@ -70,7 +64,6 @@ - (id)init [nc addObserver:self selector:@selector(renameHostsFile:) name:HostsFileShouldBeRenamedNotification object:nil]; [nc addObserver:self selector:@selector(selectHostsFile:) name:HostsFileShouldBeSelectedNotification object:nil]; [nc addObserver:self selector:@selector(deleteDraggedHostsFile:) name:DraggedFileShouldBeRemovedNotification object:nil]; - [nc addObserver:self selector:@selector(handleHostsFileRemoval:) name:HostsFileWillBeRemovedNotification object:nil]; [nc addObserver:self selector:@selector(hostsFilesLoaded:) name:AllHostsFilesLoadedFromDiskNotification object:nil]; sharedInstance = self; @@ -146,16 +139,6 @@ - (void)deleteDraggedHostsFile:(NSNotification *)notification } } -- (void)handleHostsFileRemoval:(NSNotification *)notification -{ - [list removeBadgesFromGroups]; - - // Let's have some fun :) - NSPoint point = [self centerLocationOfHostsOnScreen:[notification object]]; - [NSToolbarPoofAnimator runPoofAtPoint:point]; -} - - - (void)expandAllItems { logDebug(@"Expanding all items"); @@ -364,74 +347,4 @@ - (int)indexOfHosts:(Hosts*)hosts return -1; } -- (NSPoint)locationOfHosts:(Hosts*)hosts -{ - NSRect frame = [list rectOfRow:[self indexOfHosts:hosts]]; - - NSPoint widgetOrigin = frame.origin; - NSPoint point = [list convertPoint:widgetOrigin toView:nil]; - - return point; -} - -- (NSPoint)rightCenterLocationOfHosts:(Hosts*)hosts -{ - NSPoint point = [self locationOfHosts:hosts]; - NSRect frame = [list rectOfRow:[list selectedRow]]; - - point.x += frame.size.width; - point.y -= frame.size.height / 2; - - return point; -} - -- (NSPoint)centerLocationOfHostsOnScreen:(Hosts*)hosts -{ - NSPoint hostsPoint = [self locationOfHosts:hosts]; - - NSRect frame = [list rectOfRow:[list selectedRow]]; - - hostsPoint.x += frame.size.width / 2; - hostsPoint.y -= frame.size.height / 2; - - NSPoint point = [[NSApp mainWindow] frame].origin; - point.x += hostsPoint.x; - point.y += hostsPoint.y; - - return point; -} - -#pragma mark - -#pragma mark NSControlTextEditingDelegate - -- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor -{ - Hosts *selectedHosts = [self selectedHosts]; - - // Nothing changed - if ([[selectedHosts name] isEqualToString:[fieldEditor string]]) { - return YES; - } - - NSRange range = [[fieldEditor string] rangeOfString:@"/"]; - if (range.location != NSNotFound) { - [self showEditError:@"File Name Can Not Contain Forward Slash."]; - [fieldEditor setString:[selectedHosts name]]; - return YES; - } - - BOOL renamed = [hostsController rename:selectedHosts to:[fieldEditor string]]; - if (renamed) { - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc postNotificationName:HostsFileRenamedNotification object:selectedHosts]; - } - else { - [self showEditError:@"File With Specified Name Already Exists."]; - [fieldEditor setString:[selectedHosts name]]; - return YES; - } - - return YES; -} - @end diff --git a/Source/NSToolbarPoofAnimator.h b/Source/NSToolbarPoofAnimator.h deleted file mode 100644 index 9063fb3..0000000 --- a/Source/NSToolbarPoofAnimator.h +++ /dev/null @@ -1,27 +0,0 @@ -/*************************************************************************** - * Copyright (C) 2009-2010 by Clockwise * - * copyright@clockwise.ee * - * * - * This program is free software; you can redistribute it and/or modify * - * it under the terms of the GNU General Public License as published by * - * the Free Software Foundation; either version 2 of the License, or * - * (at your option) any later version. * - * * - * This program is distributed in the hope that it will be useful, * - * but WITHOUT ANY WARRANTY; without even the implied warranty of * - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * - * GNU General Public License for more details. * - * * - * You should have received a copy of the GNU General Public License * - * along with this program; if not, write to the * - * Free Software Foundation, Inc., * - * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * - ***************************************************************************/ - -#import - -@interface NSToolbarPoofAnimator:NSObject - -+ (void)runPoofAtPoint:(NSPoint)location; - -@end \ No newline at end of file diff --git a/Source/Swift/ErrorPopoverView.swift b/Source/Swift/ErrorPopoverView.swift new file mode 100644 index 0000000..4657f1a --- /dev/null +++ b/Source/Swift/ErrorPopoverView.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct ErrorPopoverView: View { + let title: String + var description: String? + var url: URL? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + Text(title) + .font(.system(size: NSFont.smallSystemFontSize, weight: .semibold)) + + if let description, !description.isEmpty { + Text(description) + .font(.system(size: NSFont.smallSystemFontSize)) + .foregroundStyle(.secondary) + } + + if let url { + Button("Open in Browser") { + NSWorkspace.shared.open(url) + } + .buttonStyle(.link) + .font(.system(size: NSFont.smallSystemFontSize)) + } + } + .padding() + .frame(maxWidth: 240) + } +} diff --git a/Source/Swift/HostsRowView.swift b/Source/Swift/HostsRowView.swift index e7dfd86..c086ca5 100644 --- a/Source/Swift/HostsRowView.swift +++ b/Source/Swift/HostsRowView.swift @@ -4,6 +4,9 @@ struct HostsRowView: View { let hosts: Hosts let isGroup: Bool + @State private var showingErrorPopover = false + @State private var showingOfflinePopover = false + var body: some View { if isGroup { groupRow @@ -27,10 +30,22 @@ struct HostsRowView: View { private var groupBadges: some View { if let group = hosts as? HostsGroup { if !group.online() { - Image(systemName: "wifi.slash") - .font(.system(size: 10)) - .foregroundStyle(.secondary) - .help("Offline") + Button { + showingOfflinePopover = true + } label: { + Image(systemName: "wifi.slash") + .font(.system(size: 10)) + .foregroundStyle(.secondary) + } + .buttonStyle(.plain) + .help("Offline") + .accessibilityLabel("Show offline details") + .popover(isPresented: $showingOfflinePopover) { + ErrorPopoverView( + title: "No Internet Connection", + description: "Can't update hosts files because you are not connected to the Internet." + ) + } } if group.synchronizing() { ProgressView() @@ -39,10 +54,7 @@ struct HostsRowView: View { } } if let error = hosts.error() { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 10)) - .foregroundStyle(.yellow) - .help(error.description ?? "Error") + errorBadgeButton(for: error) } } @@ -90,10 +102,7 @@ struct HostsRowView: View { @ViewBuilder private var trailingBadges: some View { if let error = hosts.error() { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 10)) - .foregroundStyle(.yellow) - .help(error.description ?? "Error") + errorBadgeButton(for: error) } if !hosts.saved() { Circle() @@ -103,6 +112,43 @@ struct HostsRowView: View { } } + // MARK: - Error Badge + + // Note: `Error` here is the ObjC Error class from Error.h, not Swift.Error + private func errorBadgeButton(for error: Error) -> some View { + Button { + showingErrorPopover = true + } label: { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 10)) + .foregroundStyle(.yellow) + } + .buttonStyle(.plain) + .help(error.description ?? "Error") + .accessibilityLabel("Show error details") + .popover(isPresented: $showingErrorPopover) { + ErrorPopoverView( + title: errorTitle(for: error.type), + description: error.description, + url: error.url + ) + } + } + + // MARK: - Helpers + + private func errorTitle(for type: UInt) -> String { + switch type { + case UInt(NetworkOffline): return "No Internet Connection" + case UInt(ServerNotFound): return "Server Not Found" + case UInt(FileNotFound): return "Hosts File Not Found" + case UInt(FailedToDownload): return "Download Failed" + case UInt(BadContentType): return "Bad Content" + case UInt(InvalidMobileMeAccount): return "Invalid Account" + default: return "Error" + } + } + // MARK: - Accessibility private var accessibilityDescription: String { From b91d49d8f4a95435f74496e6e8f276914e081fa9 Mon Sep 17 00:00:00 2001 From: Siim Raud Date: Sun, 1 Mar 2026 09:47:46 +0200 Subject: [PATCH 2/2] docs: add popover screenshots for PR review --- screenshots/popover-download-failed.png | Bin 0 -> 18803 bytes screenshots/popover-offline.png | Bin 0 -> 20846 bytes screenshots/popover-server-not-found.png | Bin 0 -> 20114 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 screenshots/popover-download-failed.png create mode 100644 screenshots/popover-offline.png create mode 100644 screenshots/popover-server-not-found.png diff --git a/screenshots/popover-download-failed.png b/screenshots/popover-download-failed.png new file mode 100644 index 0000000000000000000000000000000000000000..e2033eed9177a0a15e35c9d581b445bca4c3b349 GIT binary patch literal 18803 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 alCgwvT5oWa2KvW~6l7Ip%AP)d`@aCmQ6X&r literal 0 HcmV?d00001 diff --git a/screenshots/popover-offline.png b/screenshots/popover-offline.png new file mode 100644 index 0000000000000000000000000000000000000000..71d8defe0ae75bca5be2f6f78281e8dd427eab24 GIT binary patch literal 20846 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 literal 0 HcmV?d00001 diff --git a/screenshots/popover-server-not-found.png b/screenshots/popover-server-not-found.png new file mode 100644 index 0000000000000000000000000000000000000000..d9505afa03a882fc57b489d64546424065ac16fe GIT binary patch literal 20114 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

?d*k*>>Yh9U$nX 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