1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
|
/* xdaliclock - a melting digital clock
* Copyright © 1991-2023 Jamie Zawinski <jwz@jwz.org>
*
* Permission to use, copy, modify, distribute, and sell this software and its
* documentation for any purpose is hereby granted without fee, provided that
* the above copyright notice appear in all copies and that both that
* copyright notice and this permission notice appear in supporting
* documentation. No representations are made about the suitability of this
* software for any purpose. It is provided "as is" without express or
* implied warranty.
*/
#import "AppController.h"
#import "DaliClockWindow.h"
char *progname = "Dali Clock"; // digital.c wants this for error messages.
@interface AppController (ForwardDeclarations)
- (void)createDaliClockWindow;
@end
@implementation AppController
+ (void)initialize;
{
static BOOL initialized_p = NO;
if (initialized_p)
return;
initialized_p = YES;
NSUserDefaultsController *controller =
[NSUserDefaultsController sharedUserDefaultsController];
NSUserDefaults *defs = [controller defaults];
/* Tell the DaliClockView class to initialize its default preferences.
*/
[DaliClockView registerDefaults:defs];
/* Now that the DaliClockView class has initialized its preferences,
set the defaults for those preferences handled by AppController
rather than by DaliClockView.
*/
NSDictionary *extras = [NSDictionary dictionaryWithObjectsAndKeys:
@"NO", @"windowTitlebarIsHidden",
@"NO", @"dockIconIsHidden",
@"0", @"windowLevel", // default to "normal window"
nil];
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:100];
[dict addEntriesFromDictionary:[defs dictionaryRepresentation]];
[dict addEntriesFromDictionary:extras];
[defs registerDefaults:dict];
[DaliClockView setUserDefaultsController: controller];
}
/* 2018: As of macOS 10.11 or so, the "On top of screen saver too" option
no longer works. Normal applications can't open windows in spaces other
than their own (full-screen-mode being a kind of "space"). Since
Dali Clock is not a member of (for example) the special iTunes
full-screen space, it can't map itself inside that space. The same
thing is true of the lock screen, but moreso.
"Background" applications (the kind that do not have a dock icon or a
menubar) can open their windows in whatever the current space is, but
apps can't switch back and forth between the two without an edit to the
Info.plist file, so that's out too.
*/
// Convert preferences radio button value to real window level.
static long
real_window_level(long pref) {
return (pref == 0 ? NSNormalWindowLevel : // normal
pref == 1 ? NSNormalWindowLevel-1 : // below all
pref == 2 ? NSFloatingWindowLevel : // above all
pref == 3 ? NSScreenSaverWindowLevel+1 : // above saver
// pref == 3 ? CGShieldingWindowLevel() : // above saver
NSNormalWindowLevel); // unknown
}
- (void)createDaliClockWindow;
{
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
BOOL title_p = ![prefs boolForKey:@"windowTitlebarIsHidden"];
BOOL was_fullscreen_p = (fullscreen_window_level != 0);
long level = real_window_level ([prefs integerForKey:@"windowLevel"]);
/* CGDisplayCaptureWithOptions (kCGDirectMainDisplay, kCGCaptureNoFill)
is supposed to grab the screen without blanking it, but it blanks anyway.
As of 10.7, the new way to do full-screen is
[app setPresentationOptions:NSApplicationPresentationFullScreen]
but that doesn't seem to do anything. E.g., it doesn't grab the mouse.
So let's just not do anything.
*/
# if 0
NSApplication *app = [NSApplication sharedApplication];
if (was_fullscreen_p) {
if ([app respondsToSelector:@selector(setPresentationOptions:)]) { // 10.7
[app setPresentationOptions: NSApplicationPresentationDefault];
} else {
if (CGDisplayRelease (kCGDirectMainDisplay) != kCGErrorSuccess)
NSLog ( @"Couldn't release the display!");
}
}
if (fullscreen_p) {
if ([app respondsToSelector:@selector(setPresentationOptions:)]) { // 10.7
[app setPresentationOptions:(NSApplicationPresentationHideDock|
NSApplicationPresentationHideMenuBar|
NSApplicationPresentationFullScreen)];
} else {
if (CGDisplayCaptureWithOptions (kCGDirectMainDisplay, kCGCaptureNoFill)
!= kCGErrorSuccess) {
NSLog(@"Couldn't capture the main display!");
fullscreen_p = NO;
}
}
}
# endif // 0
if (fullscreen_p) {
level = CGShieldingWindowLevel();
fullscreen_window_level = level;
title_p = NO;
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition (false); // locks mouse position
} else {
[NSCursor unhide];
CGAssociateMouseAndMouseCursorPosition (true); // normal mouse
}
NSScreen *screen = [NSScreen mainScreen];
// Set default size/position of the window if there is no autosave.
//
NSRect rect;
rect.size.width = 625;
rect.size.height = 128;
rect.origin.x = ([screen frame].size.width - rect.size.width) / 2;
rect.origin.y = ([screen frame].size.height - rect.size.height) / 2;
NSRect default_rect = rect;
// Nuke the old window if we're re-creating it.
//
BOOL keep_old_size_p = NO;
if (window) {
keep_old_size_p = !was_fullscreen_p;
rect = [window contentRectForFrameRect:[window frame]];
[window close];
[window setFrameAutosaveName:@""]; // two windows can't have the same one
[window autorelease];
window = 0;
}
window = [[DaliClockWindow alloc]
initWithContentRect:rect
styleMask:(title_p
? (NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable)
: (NSWindowStyleMaskBorderless))
backing:NSBackingStoreBuffered
defer:YES
screen:screen];
[window setTitle:@"Dali Clock"];
[window setOpaque:NO];
[window setHasShadow:NO]; // windows with shadows flicker
[window setLevel:level];
[window setMovableByWindowBackground:!fullscreen_p];
[window setReleasedWhenClosed:NO];
[window setDelegate:self]; // for the "cancel" method
if (fullscreen_p) {
// don't save window sizes for fullscreen windows.
[window setFrameAutosaveName:@""];
} else {
[window setFrameAutosaveName:@"clockWindow"];
// need "force" for size to be updated when borderless.
if (! [window setFrameUsingName:[window frameAutosaveName] force:YES]) {
// If we weren't able to set the frame, then that means that no size
// was yet saved; so instead of leaving it at the "full screen" size,
// use the default size and position instead.
[window setFrame:[window frameRectForContentRect:default_rect]
display:NO];
}
}
if (fullscreen_p)
[window setFrame:[screen frame] display:NO];
else if (keep_old_size_p)
[window setFrame:[window frameRectForContentRect:rect] display:NO];
// if we're re-creating the window, just move the existing view
// from the previous window.
if (! view)
view = [[DaliClockView alloc] initWithFrame:[window frame]];
// Without this, toggling the title bar kills transparency until restart.
[window setBackgroundColor:[NSColor clearColor]];
[window setContentView:view];
}
/* When the window closes, exit (even if prefs still open.)
*/
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)a
{
return YES;
}
/* Connect the various window/app-related preferences to accessor methods.
Note that DaliClockView has its own set of preferences it deals with.
*/
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[self createDaliClockWindow];
if (!view || !window) abort();
NSUserDefaultsController *userDefaultsController =
[NSUserDefaultsController sharedUserDefaultsController];
[userDefaultsController addObserver:self
forKeyPath:@"values.windowLevel"
options:0
context:@selector(windowLevelDidChange:)];
[userDefaultsController addObserver:self
forKeyPath:@"values.windowTitlebarIsHidden"
options:0
context:@selector(windowTitlebarIsHiddenDidChange:)];
[window makeKeyAndOrderFront:self];
#if 0
/* For this to work, AppInfo.plist must have LSBackgroundOnly=1.
(LSUIElement=1 does not work). When that is set, the app comes
up with no icon by default, and we are able to turn it on (but
cannot turn it off).
But, then there's no way out, because the window will not take
focus. It has no menubar, and typing Command-, does not bring
up preferences!
If LSUIElement=1, then Command-, does bring up preferences...
but TransformProcessType() returns error -50 ("error in user
parameter list").
So, as far as I can see, there's no way to make this preference
not be completely horrible.
*/
if (![[NSUserDefaults standardUserDefaults]
boolForKey:@"dockIconIsHidden"]) {
// Dock icon is hidden by default; turn it on if it should be on.
ProcessSerialNumber psn = { 0, kCurrentProcess };
OSStatus status =
TransformProcessType (&psn, kProcessTransformToForegroundApplication);
if (status)
NSLog (@"Could not turn on the dock icon: error %d", status);
}
[NSMenu setMenuBarVisible:YES]; // this doesn't work.
#endif
}
- (void)windowLevelDidChange:(NSDictionary *)change
{
NSUserDefaults *prefs = [NSUserDefaults standardUserDefaults];
long level = real_window_level ([prefs integerForKey:@"windowLevel"]);
[window setLevel:level];
}
- (void)windowTitlebarIsHiddenDidChange:(NSDictionary *)change
{
// re-create the window when the "Title Bar" preference changes.
[self createDaliClockWindow];
[window makeKeyAndOrderFront:self];
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
SEL dispatchSelector = (SEL)context;
if (dispatchSelector != NULL) {
[self performSelector:dispatchSelector withObject:change];
} else {
[super observeValueForKeyPath:keyPath
ofObject:object
change:change
context:context];
}
}
/* Toggle, called by the "Window / Full Screen" menu item.
This doesn't go through the preferences because it should not be saved.
*/
- (void)performFullscreen:(id)sender
{
fullscreen_p = !fullscreen_p;
[self createDaliClockWindow];
[window makeKeyAndOrderFront:self];
}
/* "Window / Minimize".
Normally this happens in NSWindow via FirstResponder, but we bind this to
the menu directly so that it works when the window is borderless or full
screen. (To minimize a full-screen window, we have to take it out of
full-screen mode first, which is why this can't be in DaliClockWindow
like performClose is).
*/
- (void)performMiniaturize:(id)sender
{
if (fullscreen_p)
// turn off fullscreen mode before miniaturizing.
// (it does not automatically turn back on when unminiaturizing.)
[self performFullscreen:sender];
//[window performMiniaturize:sender]; // beeps when borderless
[window miniaturize:sender];
}
/* This is sent by ESC and Cmd-.
Take us out of full-screen if it is active.
*/
- (void)cancel:(id)sender
{
if (fullscreen_p) {
[self performFullscreen:sender];
}
}
/* The About button in the menu bar.
*/
- (IBAction)aboutClick:(id)sender
{
[view aboutClick:sender];
}
@end
|