Native Mac OS X OtaClock replica
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MainWindow.m 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. //
  2. // MainWindow.m
  3. // OtaClock
  4. //
  5. // Created by Thomas Buck on 16.08.15.
  6. // Copyright (c) 2015 xythobuz. All rights reserved.
  7. //
  8. #import "MainView.h"
  9. #import "MainWindow.h"
  10. #define RESIZE_START 1
  11. #define RESIZE_END 10
  12. #define RESIZE_STEP 1
  13. #define CONFIG_START_SCALE @"window_scale"
  14. #define CONFIG_KEEP_POSITION @"keep_position"
  15. #define CONFIG_KEEP_ON_TOP @"keep_on_top"
  16. #define CONFIG_MILITARY_TIME @"military_time"
  17. #define CONFIG_SHOW_DATE @"show_date"
  18. #define CONFIG_ALARM_TIME @"alarm_time"
  19. #define CONFIG_ALARM_MODE @"alarm_mode"
  20. #define CONFIG_DROP_SHADOW @"drop_shadow"
  21. #define MOUSE_CENTER_X 67
  22. #define MOUSE_CENTER_Y 47
  23. #define EYE_BLINK 0
  24. #define EYE_TOP_RIGHT 4
  25. #define EYE_TOP_LEFT 2
  26. #define EYE_BOTTOM_RIGHT 3
  27. #define EYE_BOTTOM_LEFT 1
  28. #define MAX_BLINK_DELAY 5.0
  29. #define UNBLINK_DELAY 0.1
  30. #define TIME_KEEPING_DELAY 0.25
  31. // The shadow is not properly recomputed if we don't do it multiple times
  32. #define ANIMATION_SHADOW_HACK 3
  33. #define ALARM_ANIMATION_DELAY (0.25 / ANIMATION_SHADOW_HACK)
  34. #define ALARM_ANIMATION_DELAY_END 3.0
  35. @interface MainWindow ()
  36. @property (assign) NSSize defaultSize;
  37. @property (assign) NSInteger lastEyeState;
  38. @property (assign) BOOL currentlyBlinking, showDate;
  39. @property (weak) IBOutlet MainView *mainView;
  40. @property (weak) IBOutlet NSDatePicker *alarmDatePicker;
  41. @property (weak) IBOutlet NSMenuItem *lockPositionItem;
  42. @property (weak) IBOutlet NSMenuItem *keepOnTopItem;
  43. @property (weak) IBOutlet NSMenuItem *setAlarmItem;
  44. @property (weak) IBOutlet NSMenuItem *showDateItem;
  45. @property (weak) IBOutlet NSMenuItem *alarmModeItem;
  46. @property (weak) IBOutlet NSMenuItem *alarmTextItem;
  47. @property (weak) IBOutlet NSMenuItem *militaryTimeItem;
  48. @property (weak) IBOutlet NSMenuItem *dropShadowItem;
  49. @property (weak) IBOutlet NSMenuItem *changeSize1;
  50. @property (weak) IBOutlet NSMenuItem *changeSize2;
  51. @property (weak) IBOutlet NSMenuItem *changeSize3;
  52. @property (weak) IBOutlet NSMenuItem *changeSize4;
  53. @property (weak) IBOutlet NSMenuItem *changeSize5;
  54. @property (weak) IBOutlet NSMenuItem *changeSize6;
  55. @property (weak) IBOutlet NSMenuItem *changeSize7;
  56. @property (weak) IBOutlet NSMenuItem *changeSize8;
  57. @property (weak) IBOutlet NSMenuItem *changeSize9;
  58. @property (weak) IBOutlet NSMenuItem *changeSize10;
  59. @end
  60. @implementation MainWindow
  61. @synthesize dragStart;
  62. @synthesize keepPosition;
  63. @synthesize defaultSize;
  64. @synthesize startScale;
  65. @synthesize lastEyeState;
  66. @synthesize currentlyBlinking, showDate;
  67. - (id)initWithContentRect:(NSRect)contentRect
  68. styleMask:(NSUInteger)aStyle
  69. backing:(NSBackingStoreType)bufferingType
  70. defer:(BOOL)flag {
  71. self = [super initWithContentRect:contentRect styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
  72. if (self != nil) {
  73. [self setAlphaValue:1.0];
  74. [self setOpaque:NO];
  75. lastEyeState = EYE_TOP_LEFT;
  76. currentlyBlinking = NO;
  77. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  78. // load and see if window should be kept on top
  79. if ([defaults objectForKey:CONFIG_KEEP_ON_TOP] != nil) {
  80. if ([defaults boolForKey:CONFIG_KEEP_ON_TOP]) {
  81. [self setLevel:NSFloatingWindowLevel];
  82. }
  83. }
  84. // load and see if window pos should be fixed
  85. if ([defaults objectForKey:CONFIG_KEEP_POSITION] == nil) {
  86. keepPosition = NO;
  87. } else {
  88. keepPosition = [defaults boolForKey:CONFIG_KEEP_POSITION];
  89. }
  90. // load window scale factor
  91. if ([defaults objectForKey:CONFIG_START_SCALE] == nil) {
  92. startScale = 1;
  93. } else {
  94. startScale = [defaults integerForKey:CONFIG_START_SCALE];
  95. }
  96. }
  97. [self setAcceptsMouseMovedEvents:YES];
  98. [NSEvent addGlobalMonitorForEventsMatchingMask:NSMouseMovedMask handler:^(NSEvent *mouseMovedEvent) { [self mouseMoved:mouseMovedEvent]; }];
  99. return self;
  100. }
  101. - (void)setDefaultBackgroundSize:(NSSize)size {
  102. defaultSize = size;
  103. NSRect frame = [self frame];
  104. frame.size = defaultSize;
  105. frame.size.width *= startScale;
  106. frame.size.height *= startScale;
  107. // We need to do all initialization of view state in here, because they are not ready in init
  108. if (keepPosition) [self.lockPositionItem setState:NSOnState];
  109. if ([self level] == NSFloatingWindowLevel) [self.keepOnTopItem setState:NSOnState];
  110. if (startScale == 1) [self.changeSize1 setState:NSOnState];
  111. if (startScale == 2) [self.changeSize2 setState:NSOnState];
  112. if (startScale == 3) [self.changeSize3 setState:NSOnState];
  113. if (startScale == 4) [self.changeSize4 setState:NSOnState];
  114. if (startScale == 5) [self.changeSize5 setState:NSOnState];
  115. if (startScale == 6) [self.changeSize6 setState:NSOnState];
  116. if (startScale == 7) [self.changeSize7 setState:NSOnState];
  117. if (startScale == 8) [self.changeSize8 setState:NSOnState];
  118. if (startScale == 9) [self.changeSize9 setState:NSOnState];
  119. if (startScale == 10) [self.changeSize10 setState:NSOnState];
  120. [[self.mainView render] drawWithEye:lastEyeState]; // Initialize render image
  121. [self unblink]; // Schedule next blinking
  122. // Start time keeping
  123. [self updateTime:nil];
  124. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  125. // load alarm date
  126. if ([defaults objectForKey:CONFIG_ALARM_TIME] == nil) {
  127. self.alarmDatePicker.dateValue = [NSDate date];
  128. } else {
  129. self.alarmDatePicker.dateValue = [defaults objectForKey:CONFIG_ALARM_TIME];
  130. }
  131. self.setAlarmItem.view = self.alarmDatePicker;
  132. NSDateComponents *components = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit | NSMinuteCalendarUnit) fromDate:self.alarmDatePicker.dateValue];
  133. [self.alarmTextItem setTitle:[NSString stringWithFormat:@"%02ld:%02ld", (long)[components hour], (long)[components minute]]];
  134. // load show date state
  135. if ([defaults objectForKey:CONFIG_SHOW_DATE] == nil) {
  136. showDate = YES;
  137. } else {
  138. showDate = [defaults boolForKey:CONFIG_SHOW_DATE];
  139. }
  140. if (showDate == YES) {
  141. self.showDateItem.state = NSOnState;
  142. } else {
  143. self.showDateItem.state = NSOffState;
  144. }
  145. // load alarm mode state
  146. if ([defaults objectForKey:CONFIG_ALARM_MODE] != nil) {
  147. if ([defaults boolForKey:CONFIG_ALARM_MODE] == YES) {
  148. [self.alarmModeItem setState:NSOnState];
  149. [[self.mainView render] drawAlarmDate:self.alarmDatePicker.dateValue];
  150. }
  151. }
  152. // load military time state
  153. if ([defaults objectForKey:CONFIG_MILITARY_TIME] == nil) {
  154. [self.militaryTimeItem setState:NSOnState];
  155. [[self.mainView render] drawMilitaryTime:YES];
  156. } else {
  157. if ([defaults boolForKey:CONFIG_MILITARY_TIME]) {
  158. [self.militaryTimeItem setState:NSOnState];
  159. [[self.mainView render] drawMilitaryTime:YES];
  160. } else {
  161. [[self.mainView render] drawMilitaryTime:NO];
  162. }
  163. }
  164. // load drop shadow state
  165. if ([defaults objectForKey:CONFIG_DROP_SHADOW] != nil) {
  166. if ([defaults boolForKey:CONFIG_DROP_SHADOW]) {
  167. [[self.mainView render] drawDropShadow:YES];
  168. [self.dropShadowItem setState:NSOnState];
  169. // increase window size
  170. frame.size.width += DROP_SHADOW_OFFSET * startScale;
  171. frame.size.height += DROP_SHADOW_OFFSET * startScale;
  172. frame.origin.y -= DROP_SHADOW_OFFSET * startScale;
  173. }
  174. }
  175. [[self.mainView render] drawDate:showDate];
  176. [self setFrame:frame display:YES];
  177. }
  178. - (IBAction)toggleDropShadow:(id)sender {
  179. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  180. if (self.dropShadowItem.state == NSOnState) {
  181. // Turn off drop shadow
  182. [self.dropShadowItem setState:NSOffState];
  183. [[self.mainView render] drawDropShadow:NO];
  184. [defaults setBool:NO forKey:CONFIG_DROP_SHADOW];
  185. // decrease window size
  186. NSRect rect = [self frame];
  187. rect.size = defaultSize;
  188. rect.size.width *= startScale;
  189. rect.size.height *= startScale;
  190. rect.origin.y += DROP_SHADOW_OFFSET * startScale;
  191. [self setFrame:rect display:YES];
  192. } else {
  193. // Turn on drop shadow
  194. [self.dropShadowItem setState:NSOnState];
  195. [[self.mainView render] drawDropShadow:YES];
  196. [defaults setBool:YES forKey:CONFIG_DROP_SHADOW];
  197. // increase window size
  198. NSRect rect = [self frame];
  199. rect.size.width += DROP_SHADOW_OFFSET * startScale;
  200. rect.size.height += DROP_SHADOW_OFFSET * startScale;
  201. rect.origin.y -= DROP_SHADOW_OFFSET * startScale;
  202. [self setFrame:rect display:YES];
  203. }
  204. [defaults synchronize];
  205. self.mainView.needsDisplay = YES;
  206. }
  207. - (IBAction)toggleMilitaryTime:(id)sender {
  208. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  209. if (self.militaryTimeItem.state == NSOnState) {
  210. // Turn off military time
  211. [self.militaryTimeItem setState:NSOffState];
  212. [[self.mainView render] drawMilitaryTime:NO];
  213. [defaults setBool:NO forKey:CONFIG_MILITARY_TIME];
  214. } else {
  215. // Turn on military time
  216. [self.militaryTimeItem setState:NSOnState];
  217. [[self.mainView render] drawMilitaryTime:YES];
  218. [defaults setBool:YES forKey:CONFIG_MILITARY_TIME];
  219. }
  220. [defaults synchronize];
  221. self.mainView.needsDisplay = YES;
  222. }
  223. - (IBAction)toggleAlarm:(id)sender {
  224. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  225. if (self.alarmModeItem.state == NSOnState) {
  226. // Turn off alarm
  227. [self.alarmModeItem setState:NSOffState];
  228. [[self.mainView render] drawAlarmDate:nil];
  229. [defaults setBool:NO forKey:CONFIG_ALARM_MODE];
  230. } else {
  231. // Turn on alarm
  232. [self.alarmModeItem setState:NSOnState];
  233. [[self.mainView render] drawAlarmDate:self.alarmDatePicker.dateValue];
  234. [defaults setBool:YES forKey:CONFIG_ALARM_MODE];
  235. }
  236. [defaults synchronize];
  237. self.mainView.needsDisplay = YES;
  238. }
  239. - (void)drawAnimation:(NSNumber *)anim {
  240. if ([anim integerValue] > (3 * ANIMATION_SHADOW_HACK)) {
  241. [[self.mainView render] drawAnimation:0]; // reset
  242. } else {
  243. [[self.mainView render] drawAnimation:([anim integerValue] / ANIMATION_SHADOW_HACK)];
  244. }
  245. if ([anim integerValue] < (3 * ANIMATION_SHADOW_HACK)) {
  246. [self performSelector:@selector(drawAnimation:) withObject:[NSNumber numberWithInteger:([anim integerValue] + 1)] afterDelay:ALARM_ANIMATION_DELAY];
  247. } else if ([anim integerValue] < (4 * ANIMATION_SHADOW_HACK)) {
  248. [self performSelector:@selector(drawAnimation:) withObject:[NSNumber numberWithInteger:([anim integerValue] + 1)] afterDelay:ALARM_ANIMATION_DELAY_END];
  249. }
  250. [self invalidateShadow];
  251. self.mainView.needsDisplay = YES;
  252. }
  253. - (void)updateTime:(id)sender {
  254. [self performSelector:@selector(updateTime:) withObject:nil afterDelay:TIME_KEEPING_DELAY];
  255. // Check if a second went by
  256. NSDate *now = [NSDate date];
  257. NSDateComponents *components = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit) fromDate:now];
  258. static NSInteger lastSec = -1;
  259. if (lastSec == [components second]) {
  260. return;
  261. }
  262. lastSec = [components second];
  263. [[self.mainView render] drawWithDate:now];
  264. [[self.mainView render] blinkDots];
  265. if (self.alarmModeItem.state == NSOnState) {
  266. NSDateComponents *alarmComponents = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit | NSMinuteCalendarUnit) fromDate:self.alarmDatePicker.dateValue];
  267. if (([components minute] == [alarmComponents minute]) && ([components hour] == [alarmComponents hour]) && ([components second] == 0)) {
  268. NSLog(@"Alarm time reached! (%02ld:%02ld)", (long)[alarmComponents hour], (long)[alarmComponents minute]);
  269. [self drawAnimation:[NSNumber numberWithInteger:1]];
  270. }
  271. }
  272. self.mainView.needsDisplay = YES;
  273. }
  274. - (void)blink {
  275. if (currentlyBlinking == NO) {
  276. currentlyBlinking = YES;
  277. [[self.mainView render] drawWithEye:EYE_BLINK];
  278. self.mainView.needsDisplay = YES;
  279. }
  280. [self performSelector:@selector(unblink) withObject:nil afterDelay:UNBLINK_DELAY];
  281. }
  282. - (void)unblink {
  283. if (currentlyBlinking == YES) {
  284. currentlyBlinking = NO;
  285. [[self.mainView render] drawWithEye:lastEyeState];
  286. self.mainView.needsDisplay = YES;
  287. }
  288. [self performSelector:@selector(blink) withObject:nil afterDelay:(((float)rand() / RAND_MAX) * MAX_BLINK_DELAY)];
  289. }
  290. - (IBAction)alarmDateSelected:(id)sender {
  291. // Update text representation
  292. NSDateComponents *components = [[NSCalendar currentCalendar] components:(NSHourCalendarUnit | NSMinuteCalendarUnit) fromDate:self.alarmDatePicker.dateValue];
  293. [self.alarmTextItem setTitle:[NSString stringWithFormat:@"%02ld:%02ld", (long)[components hour], (long)[components minute]]];
  294. if (self.alarmModeItem.state == NSOnState) {
  295. [[self.mainView render] drawAlarmDate:self.alarmDatePicker.dateValue];
  296. self.mainView.needsDisplay = YES;
  297. }
  298. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  299. [defaults setObject:self.alarmDatePicker.dateValue forKey:CONFIG_ALARM_TIME];
  300. [defaults synchronize];
  301. }
  302. - (IBAction)showDate:(id)sender {
  303. if (showDate == YES) {
  304. showDate = NO;
  305. self.showDateItem.state = NSOffState;
  306. } else {
  307. showDate = YES;
  308. self.showDateItem.state = NSOnState;
  309. }
  310. [[self.mainView render] drawDate:showDate];
  311. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  312. [defaults setBool:showDate forKey:CONFIG_SHOW_DATE];
  313. [defaults synchronize];
  314. }
  315. - (IBAction)changeSize:(NSMenuItem *)sender {
  316. NSRect frame = [self frame];
  317. [self.changeSize1 setState:NSOffState];
  318. [self.changeSize2 setState:NSOffState];
  319. [self.changeSize3 setState:NSOffState];
  320. [self.changeSize4 setState:NSOffState];
  321. [self.changeSize5 setState:NSOffState];
  322. [self.changeSize6 setState:NSOffState];
  323. [self.changeSize7 setState:NSOffState];
  324. [self.changeSize8 setState:NSOffState];
  325. [self.changeSize9 setState:NSOffState];
  326. [self.changeSize10 setState:NSOffState];
  327. BOOL found = NO;
  328. for (int i = RESIZE_START; i <= RESIZE_END; i += RESIZE_STEP) {
  329. NSString *title = [NSString stringWithFormat:@"x%d", i];
  330. if ([[sender title] isEqualToString:title]) {
  331. [sender setState:NSOnState];
  332. NSSize newSize = defaultSize;
  333. if (self.dropShadowItem.state == NSOnState) {
  334. newSize.width += DROP_SHADOW_OFFSET;
  335. newSize.height += DROP_SHADOW_OFFSET;
  336. }
  337. newSize.height *= i;
  338. newSize.width *= i;
  339. frame.size = newSize;
  340. found = YES;
  341. startScale = i;
  342. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  343. [defaults setInteger:i forKey:CONFIG_START_SCALE];
  344. [defaults synchronize];
  345. break;
  346. }
  347. }
  348. if (found == NO) {
  349. NSLog(@"Unknown changeSize sender: %@", sender);
  350. } else {
  351. [self setFrame:frame display:YES];
  352. }
  353. }
  354. - (IBAction)lockPosition:(NSMenuItem *)sender {
  355. BOOL state = [sender state];
  356. if (state == NSOffState) {
  357. // Lock position
  358. state = NSOnState;
  359. [sender setState:state];
  360. self.keepPosition = YES;
  361. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  362. [defaults setBool:YES forKey:CONFIG_KEEP_POSITION];
  363. [defaults synchronize];
  364. } else {
  365. // Unlock position
  366. state = NSOffState;
  367. [sender setState:state];
  368. self.keepPosition = NO;
  369. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  370. [defaults setBool:NO forKey:CONFIG_KEEP_POSITION];
  371. [defaults synchronize];
  372. }
  373. }
  374. - (IBAction)keepOnTop:(NSMenuItem *)sender {
  375. BOOL state = [sender state];
  376. if (state == NSOffState) {
  377. // Keep window on top
  378. state = NSOnState;
  379. [sender setState:state];
  380. [self setLevel:NSFloatingWindowLevel];
  381. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  382. [defaults setBool:YES forKey:CONFIG_KEEP_ON_TOP];
  383. [defaults synchronize];
  384. } else {
  385. // Don't keep window on top
  386. state = NSOffState;
  387. [sender setState:state];
  388. [self setLevel:NSNormalWindowLevel];
  389. NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  390. [defaults setBool:NO forKey:CONFIG_KEEP_ON_TOP];
  391. [defaults synchronize];
  392. }
  393. }
  394. - (BOOL)canBecomeKeyWindow {
  395. return YES;
  396. }
  397. - (void)mouseDown:(NSEvent *)theEvent {
  398. dragStart = [theEvent locationInWindow];
  399. }
  400. - (void)mouseDragged:(NSEvent *)theEvent {
  401. NSRect screenVisibleFrame = [[NSScreen mainScreen] visibleFrame];
  402. NSRect windowFrame = [self frame];
  403. NSPoint newOrigin = windowFrame.origin;
  404. if (self.keepPosition == NO) {
  405. NSPoint currentLocation = [theEvent locationInWindow];
  406. newOrigin.x += (currentLocation.x - dragStart.x);
  407. newOrigin.y += (currentLocation.y - dragStart.y);
  408. if ((newOrigin.y + windowFrame.size.height) > (screenVisibleFrame.origin.y + screenVisibleFrame.size.height)) {
  409. newOrigin.y = screenVisibleFrame.origin.y + (screenVisibleFrame.size.height - windowFrame.size.height);
  410. }
  411. [self setFrameOrigin:newOrigin];
  412. }
  413. }
  414. - (void)mouseMoved:(NSEvent *)theEvent {
  415. NSPoint mousePoint = [NSEvent mouseLocation];
  416. mousePoint.x -= [self frame].origin.x;
  417. mousePoint.y -= [self frame].origin.y;
  418. BOOL top = (mousePoint.y > (MOUSE_CENTER_Y * startScale));
  419. BOOL right = (mousePoint.x > (MOUSE_CENTER_X * startScale));
  420. NSInteger eyeState = EYE_BOTTOM_LEFT;
  421. if (top && right) {
  422. eyeState = EYE_TOP_RIGHT;
  423. } else if (top && (!right)) {
  424. eyeState = EYE_TOP_LEFT;
  425. } else if ((!top) && right) {
  426. eyeState = EYE_BOTTOM_RIGHT;
  427. }
  428. if (eyeState != lastEyeState) {
  429. lastEyeState = eyeState;
  430. if (currentlyBlinking == NO) {
  431. [[self.mainView render] drawWithEye:lastEyeState];
  432. self.mainView.needsDisplay = YES;
  433. }
  434. }
  435. }
  436. @end