Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions DYImageView.m
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,12 @@ - (float)zoomForFit {
}

- (void)drawRect:(NSRect)rect {
if (!image) return; //don't draw if nil

// Always paint a black canvas first
[NSColor.blackColor set];
NSRectFill(self.bounds);

if (!image) return;

NSRect srcRect, destinationRect;
float zoom = zoomF;
NSRect boundsRect = [self convertRect:self.bounds toView:nil];
Expand Down Expand Up @@ -121,6 +125,7 @@ - (void)drawRect:(NSRect)rect {
destinationRect = [self convertRect:destinationRect fromView:nil];
[NSBezierPath fillRect:destinationRect];
}
destinationRect = NSIntegralRect(destinationRect);
NSGraphicsContext *cg = NSGraphicsContext.currentContext;
NSImageInterpolation oldInterp = cg.imageInterpolation;
cg.imageInterpolation = zoom >= 4 ? NSImageInterpolationNone : NSImageInterpolationHigh;
Expand Down Expand Up @@ -158,6 +163,10 @@ - (void)drawRect:(NSRect)rect {
}
}

- (BOOL)isOpaque {
return YES; // matches full-bounds black fill
}

- (void)animateGIF:(NSTimer *)t {
gifTimer = nil;
if (image != t.userInfo) return; // stop if image is changed
Expand Down
185 changes: 144 additions & 41 deletions SlideshowWindow.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ static BOOL UsingMagicMouse(NSEvent *e) {

@interface SlideshowWindow () <DYFileWatcherDelegate>
@property (nonatomic, copy) NSComparator comparator;
@property (nonatomic, strong) NSLayoutConstraint *imgL;
@property (nonatomic, strong) NSLayoutConstraint *imgR;
@property (nonatomic, strong) NSLayoutConstraint *imgT;
@property (nonatomic, strong) NSLayoutConstraint *imgB;

- (void)jump:(NSInteger)n;
- (void)jumpTo:(NSUInteger)n;
Expand Down Expand Up @@ -95,21 +99,47 @@ - (instancetype)initWithContentRect:(NSRect)r styleMask:(NSWindowStyleMask)m bac
_upcomingQueue = [[NSOperationQueue alloc] init];
_fileWatcher = [[DYFileWatcher alloc] initWithDelegate:self];

self.backgroundColor = NSColor.blackColor;
self.opaque = NO;
self.backgroundColor = NSColor.blackColor;
self.opaque = YES;
_fullscreenMode = YES; // set this to prevent autosaving the frame from the nib
self.hasShadow = NO;
self.titlebarAppearsTransparent = YES;
self.titleVisibility = NSWindowTitleHidden;
self.collectionBehavior = NSWindowCollectionBehaviorParticipatesInCycle|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorMoveToActiveSpace;
// *** Unfortunately the menubar doesn't seem to show up on the second screen... Eventually we'll want to switch to use NSView's enterFullScreenMode:withOptions:
currentIndex = NSNotFound;
}
}
return self;
}

- (void)awakeFromNib {
[super awakeFromNib];

imgView = [[DYImageView alloc] initWithFrame:NSZeroRect];
[self.contentView addSubview:imgView];
imgView.frame = self.contentView.frame;
imgView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;

if (_fullscreenMode) {
// Fullscreen: constraint-driven
self.contentView.wantsLayer = YES;
self.contentView.layer.backgroundColor = NSColor.blackColor.CGColor;

imgView.translatesAutoresizingMaskIntoConstraints = NO;
NSLayoutGuide *clg = self.contentLayoutGuide;

self.imgL = [imgView.leadingAnchor constraintEqualToAnchor:clg.leadingAnchor];
self.imgR = [imgView.trailingAnchor constraintEqualToAnchor:clg.trailingAnchor];
self.imgT = [imgView.topAnchor constraintEqualToAnchor:clg.topAnchor];
self.imgB = [imgView.bottomAnchor constraintEqualToAnchor:clg.bottomAnchor];
[NSLayoutConstraint activateConstraints:@[self.imgL, self.imgR, self.imgT, self.imgB]];
// Notch avoidance (fullscreen) via constraints
[self applyNotchTopInsetForScreen:(self.visible ? self.screen : NSScreen.mainScreen)];
} else {
// Windowed: autoresizing-driven
self.contentView.wantsLayer = NO; // window background supplies black
imgView.translatesAutoresizingMaskIntoConstraints = YES;
imgView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
imgView.frame = self.contentView.bounds; // initial size only
}

infoFld = [[NSTextField alloc] initWithFrame:NSMakeRect(0,0,360,20)];
[imgView addSubview:infoFld];
Expand Down Expand Up @@ -158,17 +188,94 @@ - (void)awakeFromNib {
}
}

/*
Developer note: Fullscreen vs windowed layout (macOS 26 grey-border fix)

- Fullscreen mode:
- Use Auto Layout constraints pinned to contentLayoutGuide on all edges.
- Make contentView layer-backed and paint black; window is opaque and shadowless.
- This avoids translucent composition that produced thin grey borders on macOS 26.
- Notch avoidance is applied via applyNotchTopInsetForScreen(screen), which offsets
the top constraint by safeAreaInsets.top when not ignored.

- Windowed mode:
- Disable constraints for imgView and rely on springs/struts (autoresizingMask).
- Keep contentView non-layer-backed; the window background provides black.
- Do not mix constraints and autoresizing; mixing caused subtle frame churn
and grey artifacts on macOS 26.

If you refactor this, keep the two systems mutually exclusive or move both modes
to constraints consistently. Always test fullscreen on notched displays to ensure
no grey edge reappears.
*/
- (void)setFullscreenMode:(BOOL)b {
_fullscreenMode = b;
if (b) {
self.styleMask = NSWindowStyleMaskBorderless;
self.collectionBehavior = NSWindowCollectionBehaviorParticipatesInCycle|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorMoveToActiveSpace;
} else {
self.styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
self.collectionBehavior = NSWindowCollectionBehaviorParticipatesInCycle|NSWindowCollectionBehaviorFullScreenNone|NSWindowCollectionBehaviorMoveToActiveSpace;
_fullscreenMode = b;

if (b) {
// Fullscreen window config
self.styleMask = NSWindowStyleMaskBorderless;
self.backgroundColor = NSColor.blackColor; // solid canvas at the window level
self.opaque = YES; // avoid translucent composition
self.hasShadow = NO;
self.titlebarAppearsTransparent = YES;
self.titleVisibility = NSWindowTitleHidden;

// Layout system: constraints ON
self.contentView.wantsLayer = YES;
self.contentView.layer.backgroundColor = NSColor.blackColor.CGColor;

imgView.translatesAutoresizingMaskIntoConstraints = NO;
if (!self.imgL) {
NSLayoutGuide *clg = self.contentLayoutGuide;
self.imgL = [imgView.leadingAnchor constraintEqualToAnchor:clg.leadingAnchor];
self.imgR = [imgView.trailingAnchor constraintEqualToAnchor:clg.trailingAnchor];
self.imgT = [imgView.topAnchor constraintEqualToAnchor:clg.topAnchor];
self.imgB = [imgView.bottomAnchor constraintEqualToAnchor:clg.bottomAnchor];
}
[NSLayoutConstraint activateConstraints:@[self.imgL, self.imgR, self.imgT, self.imgB]];
// Notch avoidance (fullscreen) via constraints
[self applyNotchTopInsetForScreen:(self.visible ? self.screen : NSScreen.mainScreen)];
imgView.autoresizingMask = NSViewNotSizable;
} else {
// Windowed (titled) window config
self.styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable;
self.backgroundColor = NSColor.blackColor; // black canvas from window
self.opaque = YES; // prevent Tahoe’s translucent treatment
self.hasShadow = YES;
self.titlebarAppearsTransparent = NO;
self.titleVisibility = NSWindowTitleVisible;

// Layout system: constraints OFF, autoresizing ON
if (self.imgL) {
[NSLayoutConstraint deactivateConstraints:@[self.imgL, self.imgR, self.imgT, self.imgB]];
self.imgL = self.imgR = self.imgT = self.imgB = nil;
}
self.contentView.wantsLayer = NO;
imgView.translatesAutoresizingMaskIntoConstraints = YES;
imgView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
imgView.frame = self.contentView.bounds; // reset initial size; autoresizing manages thereafter
}

self.collectionBehavior = NSWindowCollectionBehaviorParticipatesInCycle |
NSWindowCollectionBehaviorFullScreenNone |
NSWindowCollectionBehaviorMoveToActiveSpace;

[self.contentView layoutSubtreeIfNeeded]; // apply layout immediately
if (self.visible) [self configureScreen];
}

#pragma mark Notch avoidance
// Centralized notch handling: offset top constraint by screen.safeAreaInsets.top when “pretendNotchIsntThere” is false.
// This avoids placing content under the camera housing on notched displays in fullscreen.
- (void)applyNotchTopInsetForScreen:(NSScreen *)screen {
if (!self.imgT) return;
CGFloat inset = 0.0;
if (@available(macOS 12.0, *)) {
if (![NSUserDefaults.standardUserDefaults boolForKey:@"pretendNotchIsntThere"]) {
inset = screen.safeAreaInsets.top;
}
}
if (self.visible)
[self configureScreen];
self.imgT.constant = inset;
}

- (void)setAutoRotate:(BOOL)b {
Expand Down Expand Up @@ -242,33 +349,29 @@ - (NSString *)currentShortFilename {
return s.length <= basePath.length ? s : [s substringFromIndex:basePath.length];
}

- (void)configureScreen // or rather, configure the window *for* the screen
{
NSScreen *myScreen = self.visible ? self.screen : NSScreen.mainScreen;
NSRect screenRect = myScreen.frame;
if (_fullscreenMode) {
NSRect boundingRect = screenRect;
if (@available(macOS 12.0, *))
if (![NSUserDefaults.standardUserDefaults boolForKey:@"pretendNotchIsntThere"])
boundingRect.size.height -= myScreen.safeAreaInsets.top;
[self setFrame:screenRect display:NO];
boundingRect.origin = imgView.frame.origin;
imgView.frame = boundingRect;
} else {
NSString *v = [NSUserDefaults.standardUserDefaults objectForKey:@"DYSlideshowWindowFrame"];
NSRect r;
if (v) {
r = NSRectFromString(v);
} else {
// if no saved frame, put it in the top left of the screen
r = screenRect;
r.size.width = r.size.width/2;
r.size.height = r.size.height/2;
r.origin.y = screenRect.size.height;
}
[self setFrame:r display:NO];
imgView.frame = self.contentLayoutRect;
}
- (void)configureScreen {
NSScreen *myScreen = self.visible ? self.screen : NSScreen.mainScreen;
NSRect screenRect = myScreen.frame;

if (_fullscreenMode) {
[self setFrame:screenRect display:NO];
// Notch avoidance (fullscreen) via constraints
[self applyNotchTopInsetForScreen:myScreen];
// Do NOT set imgView.frame here (constraints own it)
} else {
NSString *v = [NSUserDefaults.standardUserDefaults objectForKey:@"DYSlideshowWindowFrame"];
NSRect r;
if (v) {
r = NSRectFromString(v);
} else {
r = screenRect;
r.size.width = r.size.width/2;
r.size.height = r.size.height/2;
r.origin.y = screenRect.size.height;
}
[self setFrame:r display:NO];
// Do NOT set imgView.frame here; autoresizing owns it
}
}

- (void)configureBacking {
Expand Down