File: AppController.m

package info (click to toggle)
xdaliclock 2.48-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 17,328 kB
  • sloc: ansic: 8,377; sh: 3,522; objc: 2,029; makefile: 1,220; java: 1,044; javascript: 802; asm: 419; xml: 337; perl: 269; ruby: 5
file content (353 lines) | stat: -rw-r--r-- 12,082 bytes parent folder | download
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