1 // -*- objc -*-
2 // --------------------------------------------------------------------
3 // IpePresenter with Cocoa UI
4 // --------------------------------------------------------------------
5 /*
6 
7     This file is part of the extensible drawing editor Ipe.
8     Copyright (c) 1993-2020 Otfried Cheong
9 
10     Ipe is free software; you can redistribute it and/or modify it
11     under the terms of the GNU General Public License as published by
12     the Free Software Foundation; either version 3 of the License, or
13     (at your option) any later version.
14 
15     As a special exception, you have permission to link Ipe with the
16     CGAL library and distribute executables, as long as you follow the
17     requirements of the Gnu General Public License in regard to all of
18     the software in the executable aside from CGAL.
19 
20     Ipe is distributed in the hope that it will be useful, but WITHOUT
21     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
22     or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
23     License for more details.
24 
25     You should have received a copy of the GNU General Public License
26     along with Ipe; if not, you can find it at
27     "http://www.gnu.org/copyleft/gpl.html", or write to the Free
28     Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
29 
30 */
31 
32 #include "ipepresenter.h"
33 #include "ipepdfview_cocoa.h"
34 #include "ipeselector_cocoa.h"
35 
36 #import <Cocoa/Cocoa.h>
37 
38 using ipe::String;
39 
N2I(NSString * aStr)40 inline String N2I(NSString *aStr) { return String(aStr.UTF8String); }
I2N(String s)41 inline NSString *I2N(String s) {return [NSString stringWithUTF8String:s.z()];}
42 
43 // --------------------------------------------------------------------
44 
45 @interface AppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate, NSSplitViewDelegate>
46 @end
47 
48 @interface IpePdfSelectorProvider : IpeSelectorProvider
49 
50 @property ipe::PdfFile *pdf;
51 @property ipe::PdfThumbnail *pdfThumb;
52 @property NSMutableArray<NSString *> *pdfLabels;
53 
54 - (int) count;
55 - (NSString *) title:(int) index;
56 - (ipe::Buffer) renderImage:(int) index;
57 @end
58 
59 // --------------------------------------------------------------------
60 
61 @implementation IpePdfSelectorProvider
62 
63 - (int) count
64 {
65   return self.pdf->countPages();
66 }
67 
68 - (NSString *) title:(int) index
69 {
70   return self.pdfLabels[index];
71 }
72 
73 - (ipe::Buffer) renderImage:(int) index
74 {
75   return self.pdfThumb->render(self.pdf->page(index));
76 }
77 @end
78 
79 // --------------------------------------------------------------------
80 
81 class AppUi : public Presenter {
82 public:
83   AppUi();
84   bool load(NSString *aFname);
85   void setPdf();
86   void setView();
87   void fitBoxAll();
88   void setTime();
89   void timerElapsed();
90   void selectPage();
91   virtual void showType3Warning(const char *s) override;
92   virtual void browseLaunch(bool launch, String dest) override;
93 
94 public:
95   int iTime;
96   bool iCountDown;
97   bool iCountTime;
98 
99   NSWindow *iWindow;
100   NSWindow *iScreenWindow;
101 
102   NSSplitView *iContent;
103   NSSplitView *iRightSide;
104   NSStackView *iTopRight;
105 
106   NSTextField *iClock;
107   NSTextView *iNotesView;
108   NSScrollView *iNotes;
109 
110   IpePdfView *iCurrent;
111   IpePdfView *iNext;
112   IpePdfView *iScreen;
113 
114   IpePdfSelectorProvider *iProvider;
115 };
116 
AppUi()117 AppUi::AppUi() : iTime{0}, iCountDown{false}, iCountTime{false},
118   iCurrent{nil}, iNext{nil}, iScreen{nil}, iProvider{nil}
119 {
120   NSRect contentRect = NSMakeRect(335., 390., 800., 600.);
121   NSRect mainRect = NSMakeRect(0., 0., 200., 100.);
122   NSRect subRect = NSMakeRect(0., 0., 100., 100.);
123   NSRect clockRect = NSMakeRect(0., 0., 100., 30.);
124 
125   iWindow = [[NSWindow alloc]
126 	       initWithContentRect:contentRect
127 			 styleMask:(NSTitledWindowMask|
128 				    NSClosableWindowMask|
129 				    NSResizableWindowMask|
130 				    NSMiniaturizableWindowMask)
131 			   backing:NSBackingStoreBuffered
132 			     defer:YES];
133 
134   iContent = [[NSSplitView alloc] initWithFrame:subRect];
135   iContent.vertical = YES;
136 
137   iRightSide = [[NSSplitView alloc] initWithFrame:subRect];
138   iRightSide.vertical = NO;
139 
140   iCurrent = [[IpePdfView alloc] initWithFrame:mainRect];
141   iNext = [[IpePdfView alloc] initWithFrame:subRect];
142 
143   iClock = [[NSTextField alloc] initWithFrame:clockRect];
144   iClock.bordered= NO;
145   iClock.drawsBackground = NO;
146   iClock.editable = NO;
147   iClock.font = [NSFont labelFontOfSize:24.0];
148   iClock.alignment = NSCenterTextAlignment;
149   iClock.usesSingleLineMode = YES;
150 
151   iNotes = [[NSScrollView alloc] initWithFrame:subRect];
152   iNotesView = [[NSTextView alloc] initWithFrame:subRect];
153   iNotesView.editable = NO;
154   iNotesView.richText = NO;
155   [iNotesView setAutoresizingMask:NSViewWidthSizable|NSViewHeightSizable];
156   [iNotes setDocumentView:iNotesView];
157   iNotes.hasVerticalScroller = YES;
158 
159   iTopRight = [NSStackView stackViewWithViews:@[iClock, iNotes]];
160   iTopRight.orientation = NSUserInterfaceLayoutOrientationVertical;
161 
162   [iContent addSubview:iCurrent];
163   [iContent addSubview:iRightSide];
164   [iRightSide addSubview:iTopRight];
165   [iRightSide addSubview:iNext];
166   [iContent adjustSubviews];
167   [iRightSide adjustSubviews];
168   double split = (0.6 * [iRightSide minPossiblePositionOfDividerAtIndex:0] +
169 		  0.4 * [iRightSide maxPossiblePositionOfDividerAtIndex:0]);
170   [iRightSide setPosition:split ofDividerAtIndex:0];
171 
172   [iWindow setContentView:iContent];
173 
174   iScreenWindow = [[NSWindow alloc]
175 		    initWithContentRect:contentRect
176 			      styleMask:(NSTitledWindowMask|
177 					 NSResizableWindowMask|
178 					 NSMiniaturizableWindowMask)
179 				backing:NSBackingStoreBuffered
180 				  defer:YES];
181   iScreen = [[IpePdfView alloc] initWithFrame:subRect];
182   iScreen.pdfView->setBackground(ipe::Color(0, 0, 0));
183   [iScreenWindow setContentView:iScreen];
184 }
185 
load(NSString * aFname)186 bool AppUi::load(NSString *aFname)
187 {
188   bool result = Presenter::load(N2I(aFname).z());
189   if (result) {
190     setPdf();
191     setView();
192     fitBoxAll();
193     iProvider = nil;
194   }
195   return result;
196 }
197 
setPdf()198 void AppUi::setPdf()
199 {
200   iCurrent.pdfView->setPdf(iPdf.get(), iFonts.get());
201   iNext.pdfView->setPdf(iPdf.get(), iFonts.get());
202   iScreen.pdfView->setPdf(iPdf.get(), iFonts.get());
203 }
204 
setView()205 void AppUi::setView()
206 {
207   setViewPage(iScreen.pdfView, iPdfPageNo);
208   setViewPage(iCurrent.pdfView, iPdfPageNo);
209   setViewPage(iNext.pdfView, iPdfPageNo < iPdf->countPages() - 1 ? iPdfPageNo + 1 : iPdfPageNo);
210 
211   [iWindow setTitle:I2N(currentLabel())];
212   NSAttributedString *n = [[NSAttributedString alloc]
213 			    initWithString:I2N(iAnnotations[iPdfPageNo])];
214   [[iNotesView textStorage] setAttributedString:n];
215   iNotesView.textColor = [NSColor textColor];
216   iNotesView.font = [NSFont labelFontOfSize:14.0];
217   setTime();
218 }
219 
fitBoxAll()220 void AppUi::fitBoxAll()
221 {
222   if (!iPdf)
223     return;
224   fitBox(mediaBox(-1), iCurrent.pdfView);
225   fitBox(mediaBox(-2), iNext.pdfView);
226   fitBox(mediaBox(-1), iScreen.pdfView);
227 }
228 
setTime()229 void AppUi::setTime()
230 {
231   [iClock setStringValue:[NSString stringWithFormat:@"%d:%02d:%02d",
232 				   iTime / 3600, (iTime / 60) % 60, iTime % 60]];
233   [iRightSide adjustSubviews];
234 }
235 
timerElapsed()236 void AppUi::timerElapsed()
237 {
238   if (iCountTime) {
239     if (iCountDown) {
240       if (iTime > 0)
241 	--iTime;
242     } else
243       ++iTime;
244     setTime();
245   }
246 }
247 
selectPage()248 void AppUi::selectPage()
249 {
250   constexpr int iconWidth = 250;
251 
252   if (!iProvider) {
253     iProvider = [IpePdfSelectorProvider new];
254     iProvider.pdf = iPdf.get();
255     iProvider.pdfThumb = new ipe::PdfThumbnail(iPdf.get(), iconWidth);
256 
257     iProvider.pdfLabels = [NSMutableArray new];
258     for (int i = 0; i < iPdf->countPages(); ++i)
259       [iProvider.pdfLabels addObject:I2N(pageLabel(i))];
260 
261     NSSize tnSize;
262     tnSize.width = iProvider.pdfThumb->width() / 2.0;
263     tnSize.height = iProvider.pdfThumb->height() / 2.0;
264     iProvider.tnSize = tnSize;
265   }
266 
267   const char *title = "IpePresenter: Select page";
268   int width = 800;
269   int height = 600;
270 
271   int sel = showPageSelectDialog(width, height, title, iProvider, iPdfPageNo);
272   if (sel >= 0) {
273     iPdfPageNo = sel;
274     setView();
275   }
276 }
277 
showType3Warning(const char * s)278 void AppUi::showType3Warning(const char *s)
279 {
280   NSAlert *alert = [[NSAlert alloc] init];
281   alert.messageText = [NSString stringWithUTF8String:s];
282   [alert addButtonWithTitle:@"Ok"];
283   [alert runModal];
284 }
285 
browseLaunch(bool launch,String dest)286 void AppUi::browseLaunch(bool launch, String dest)
287 {
288   NSString *urls = I2N(dest);
289   if (launch) {
290     NSURL *url = [NSURL fileURLWithPath:urls isDirectory:NO];
291     [[NSWorkspace sharedWorkspace] openURL:url];
292   } else {
293     NSURL *url = [NSURL URLWithString:urls];
294     [[NSWorkspace sharedWorkspace] openURL:url];
295   }
296 }
297 
298 // --------------------------------------------------------------------
299 
setItemShortcut(NSMenu * menu,int index,unichar code)300 static void setItemShortcut(NSMenu *menu, int index, unichar code)
301 {
302   NSMenuItem *item = [menu itemAtIndex:index];
303   item.keyEquivalent = [NSString stringWithFormat:@"%C", code];
304   item.keyEquivalentModifierMask = 0;
305 }
306 
307 // --------------------------------------------------------------------
308 
309 static const char * const about_text =
310   "IpePresenter %d.%d.%d\n\n"
311   "Copyright (c) 2020 Otfried Cheong\n\n"
312   "A presentation tool for giving PDF presentations "
313   "created in Ipe or using beamer.\n"
314   "Originally invented by Dmitriy Morozov, "
315   "IpePresenter is now developed together with Ipe and released under the GNU Public License.\n"
316   "See http://ipepresenter.otfried.org for details.\n\n"
317   "You can \"like\" IpePresenter and follow IpePresenter announcements on Facebook "
318   "(http://www.facebook.com/drawing.editor.Ipe7).\n\n"
319   "If you are an IpePresenter fan and want to show others, have a look at the "
320   "Ipe T-shirts (www.shirtee.com/en/store/ipe).\n\n"
321   "Platinum and gold sponsors\n\n"
322   " * Hee-Kap Ahn\n"
323   " * Günter Rote\n"
324   " * SCALGO\n"
325   " * Martin Ziegler\n\n"
326   "If you enjoy IpePresenter, feel free to treat the author on a cup of coffee at https://ko-fi.com/ipe7author.\n\n"
327   "You can also become a member of the exclusive community of "
328   "Ipe patrons (http://patreon.com/otfried). "
329   "For the price of a cup of coffee per month you can make a meaningful contribution "
330   "to the continuing development of IpePresenter and Ipe.";
331 
332 // --------------------------------------------------------------------
333 
334 @implementation AppDelegate  {
335   AppUi *ui;
336 }
337 
338 - (instancetype) init {
339   self = [super init];
340   if (self)
341     ui = new AppUi();
342   return self;
343 }
344 
345 - (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *) app {
346   return YES;
347 }
348 
349 - (void) applicationWillFinishLaunching:(NSNotification *) notification {
350   [ui->iWindow setDelegate:(id<NSWindowDelegate>) self];
351   [ui->iScreenWindow setDelegate:(id<NSWindowDelegate>) self];
352   [ui->iContent setDelegate:(id<NSSplitViewDelegate>) self];
353   [ui->iRightSide setDelegate:(id<NSSplitViewDelegate>) self];
354 }
355 
356 - (void) applicationDidFinishLaunching:(NSNotification *) aNotification {
357   NSMenu *menu = [NSApp menu];
358   int i = [menu indexOfItemWithTag:13];
359   NSMenu *navi = [menu itemAtIndex:i].submenu;
360   setItemShortcut(navi, 0, NSRightArrowFunctionKey);
361   setItemShortcut(navi, 1, NSDownArrowFunctionKey);
362   setItemShortcut(navi, 2, NSLeftArrowFunctionKey);
363   setItemShortcut(navi, 3, NSUpArrowFunctionKey);
364 
365   [ui->iWindow makeKeyAndOrderFront:self];
366   ui->fitBoxAll();
367 
368   [NSTimer scheduledTimerWithTimeInterval:1.0
369 				   target:self
370 				 selector:@selector(timerFired:)
371 				 userInfo:nil
372 				  repeats:YES];
373 
374   if (ui->iFileName.empty())
375     [self openDocument:self];
376 }
377 
378 - (BOOL) application:(NSApplication *) app openFile:(NSString *) filename
379 {
380   return ui->load(filename);
381 }
382 
383 - (void) windowDidEndLiveResize:(NSNotification *) notification
384 {
385   ui->fitBoxAll();
386 }
387 
388 - (void) splitViewDidResizeSubviews:(NSNotification *) notification
389 {
390   ui->fitBoxAll();
391 }
392 
393 - (void) windowDidExitFullScreen:(NSNotification *) notification
394 {
395   ui->fitBoxAll();
396 }
397 
398 - (BOOL) validateMenuItem:(NSMenuItem *) item
399 {
400   if (item.action == @selector(countDown:)) {
401     item.state = ui->iCountDown ? NSOnState : NSOffState;
402   } else if (item.action == @selector(countTime:)) {
403     item.state = ui->iCountTime ? NSOnState : NSOffState;
404   } else if (item.action == @selector(blackout:)) {
405     item.state = ui->iScreen.pdfView->blackout() ? NSOnState : NSOffState;
406   }
407   return YES;
408 }
409 
410 - (void) pdfViewMouseButton:(NSEvent *) event atLocation:(NSArray<NSNumber *> *) pos
411 {
412   ipe::Vector p([pos[0] doubleValue], [pos[1] doubleValue]);
413   const ipe::PdfDict *action = ui->findLink(p);
414   if (action) {
415     ui->interpretAction(action);
416     ui->setView();
417   } else {
418     int button = event.buttonNumber;
419     if (button == 0)
420       [self nextView:event.window];
421     else if (button == 1)
422       [self previousView:event.window];
423   }
424 }
425 
426 - (BOOL) windowShouldClose:(id) sender
427 {
428   if (sender == ui->iWindow)
429     [ui->iScreenWindow close]; // also close presentation window
430   return true;
431 }
432 
433 - (void) timerFired:(NSTimer *) timer
434 {
435   ui->timerElapsed();
436 }
437 
438 // --------------------------------------------------------------------
439 
440 - (void) openDocument:(id) sender {
441   NSOpenPanel *panel = [NSOpenPanel openPanel];
442   [panel beginSheetModalForWindow:ui->iWindow completionHandler:
443     ^(NSInteger result) {
444       if (result == NSFileHandlingPanelOKButton) {
445 	NSURL *url = [[panel URLs] objectAtIndex:0];
446 	if ([url isFileURL])
447 	  ui->load([url path]);
448       }
449       // canceled or failed to load and we didn't have a document before
450       if (ui->iFileName.empty())
451 	  [NSApp terminate:self];
452     }
453    ];
454 }
455 
456 - (void) showPresentation:(id) sender
457 {
458   [ui->iScreenWindow setIsVisible:true];
459 }
460 
461 - (void) blackout:(id) sender
462 {
463   ui->iScreen.pdfView->setBlackout(!ui->iScreen.pdfView->blackout());
464   ui->iScreen.pdfView->updatePdf();
465 }
466 
467 - (void) setTime:(id) sender
468 {
469   NSString *input = [self input:@"Enter time in minutes:" defaultValue:@""];
470   if (input) {
471     ipe::Lex lex(N2I(input));
472     int minutes = lex.getInt();
473     ui->iTime = 60 * minutes;
474     ui->setTime();
475   }
476 }
477 
478 - (void) resetTime:(id) sender
479 {
480   ui->iTime = 0;
481   ui->setTime();
482 }
483 
484 - (void) countDown:(id) sender
485 {
486   ui->iCountDown = !ui->iCountDown;
487 }
488 
489 - (void) countTime:(id) sender
490 {
491   ui->iCountTime = !ui->iCountTime;
492 }
493 
494 - (void) nextView:(id) sender
495 {
496   ui->nextView(+1);
497   ui->setView();
498 }
499 
500 - (void) previousView:(id) sender
501 {
502   ui->nextView(-1);
503   ui->setView();
504 }
505 
506 - (void) nextPage:(id) sender
507 {
508   ui->nextPage(+1);
509   ui->setView();
510 }
511 
512 - (void) previousPage:(id) sender
513 {
514   ui->nextPage(-1);
515   ui->setView();
516 }
517 
518 - (void) jumpTo:(id) sender
519 {
520   NSString *input = [self input:@"Enter page label:" defaultValue:@""];
521   if (input) {
522     ui->jumpToPage(N2I(input));
523     ui->setView();
524   }
525 }
526 
527 - (void) selectPage:(id) sender
528 {
529   ui->selectPage();
530 }
531 
532 - (NSString *) input: (NSString *) prompt defaultValue: (NSString *)defaultValue
533 {
534   NSAlert *alert = [[NSAlert alloc] init];
535   alert.messageText = prompt;
536   [alert addButtonWithTitle:@"Ok"];
537   [alert addButtonWithTitle:@"Cancel"];
538 
539   NSTextField *input = [[NSTextField alloc] initWithFrame:NSMakeRect(0, 0, 200, 24)];
540   input.stringValue = defaultValue;
541   alert.accessoryView = input;
542 
543   NSInteger button = [alert runModal];
544   if (button == NSAlertFirstButtonReturn) {
545     [input validateEditing];
546     return [input stringValue];
547   } else
548     return nil;
549 }
550 
551 - (void) aboutIpePresenter:(id) sender
552 {
553   NSString *info = [NSString stringWithFormat:@(about_text),
554 					  ipe::IPELIB_VERSION / 10000,
555 					 (ipe::IPELIB_VERSION / 100) % 100,
556 					  ipe::IPELIB_VERSION % 100];
557 
558   NSAlert *alert = [[NSAlert alloc] init];
559   [alert setMessageText:@"About IpePresenter"];
560   [alert setInformativeText:info];
561   [alert setAlertStyle:NSInformationalAlertStyle];
562   [alert runModal];
563 }
564 
565 @end
566 
567 // --------------------------------------------------------------------
568 
main(int argc,const char * argv[])569 int main(int argc, const char * argv[])
570 {
571   ipe::Platform::initLib(ipe::IPELIB_VERSION);
572   return NSApplicationMain(argc, argv);
573 }
574 
575 // --------------------------------------------------------------------
576