diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index 61f07fc..8164b1f 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -94,7 +94,6 @@ 35DA4DF6123BF4F30043CA87 /* sparkle_dsa_pub.pem in Resources */ = {isa = PBXBuildFile; fileRef = 35DA4DF5123BF4F30043CA87 /* sparkle_dsa_pub.pem */; }; 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 */; }; @@ -128,6 +127,7 @@ AA00001C000000000000AAAA /* HostsTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */; }; AA00001E000000000000AAAA /* CombinedHostsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */; }; AA000020000000000000AAAA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001F000000000000AAAA /* ContentView.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 */; }; @@ -303,6 +303,7 @@ AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsTextViewRepresentable.swift; path = "Source/Swift/HostsTextViewRepresentable.swift"; sourceTree = ""; }; 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 = ""; }; + 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 = ""; }; @@ -338,8 +339,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 = ""; }; @@ -715,6 +714,7 @@ AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */, AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */, AA00001F000000000000AAAA /* ContentView.swift */, + AA000023000000000000AAAA /* ErrorPopoverView.swift */, BB000020000000000000BBBB /* Preferences */, ); name = Swift; @@ -775,8 +775,6 @@ 35F414F3152E1B7800B99583 /* VDKQueue.m */, 35FBCA391223172300860FDA /* RegexKitLite.h */, 35FBCA3A1223172300860FDA /* RegexKitLite.m */, - 35E900881147F42900851A25 /* MAAttachedWindow.h */, - 35E900891147F42900851A25 /* MAAttachedWindow.m */, ); name = "3rd Party"; sourceTree = ""; @@ -1027,6 +1025,7 @@ AA00001C000000000000AAAA /* HostsTextViewRepresentable.swift in Sources */, AA00001E000000000000AAAA /* CombinedHostsPickerView.swift in Sources */, AA000020000000000000AAAA /* ContentView.swift in Sources */, + AA000024000000000000AAAA /* ErrorPopoverView.swift in Sources */, BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */, BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */, CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */, @@ -1036,7 +1035,6 @@ BB00000C000000000000BBBB /* PreferencesPresenter.swift in Sources */, FF000002000000000000FF01 /* AboutBoxView.swift in Sources */, FF000004000000000000FF01 /* AboutBoxPresenter.swift in Sources */, - 35E9008A1147F42900851A25 /* MAAttachedWindow.m in Sources */, 35E32EDC12145CF900B2C631 /* Logger.m in Sources */, 35E33075121490B100B2C631 /* ExtendedNSThread.m in Sources */, 35FBCA3B1223172300860FDA /* RegexKitLite.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/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 { diff --git a/screenshots/popover-download-failed.png b/screenshots/popover-download-failed.png new file mode 100644 index 0000000..e2033ee Binary files /dev/null and b/screenshots/popover-download-failed.png differ diff --git a/screenshots/popover-offline.png b/screenshots/popover-offline.png new file mode 100644 index 0000000..71d8def Binary files /dev/null and b/screenshots/popover-offline.png differ diff --git a/screenshots/popover-server-not-found.png b/screenshots/popover-server-not-found.png new file mode 100644 index 0000000..d9505af Binary files /dev/null and b/screenshots/popover-server-not-found.png differ