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