#include "common.h" #import "MuPageViewNormal.h" #import "MuPageViewReflow.h" #import "MuDocumentController.h" #import "MuTextFieldController.h" #import "MuChoiceFieldController.h" #import "MuPrintPageRenderer.h" #define GAP 20 #define INDICATOR_Y -44-24 #define SLIDER_W (width - GAP - 24) #define SEARCH_W (width - GAP - 170) #define MIN_SCALE (1.0) #define MAX_SCALE (5.0) static NSString *const AlertTitle = @"Save Document?"; // Correct functioning of the app relies on CloseAlertMessage and ShareAlertMessage differing static NSString *const CloseAlertMessage = @"Changes have been made to the document that will be lost if not saved"; static NSString *const ShareAlertMessage = @"Your changes will not be shared unless the document is first saved"; static void flattenOutline(NSMutableArray *titles, NSMutableArray *pages, fz_outline *outline, int level) { char indent[8*4+1]; if (level > 8) level = 8; memset(indent, ' ', level * 4); indent[level * 4] = 0; while (outline) { int page = outline->page; if (page >= 0 && outline->title) { NSString *title = @(outline->title); [titles addObject: [NSString stringWithFormat: @"%s%@", indent, title]]; [pages addObject: @(page)]; } flattenOutline(titles, pages, outline->down, level + 1); outline = outline->next; } } static char *tmp_path(const char *path) { int f; char *buf = malloc(strlen(path) + 6 + 1); if (!buf) return NULL; strcpy(buf, path); strcat(buf, "XXXXXX"); f = mkstemp(buf); if (f >= 0) { close(f); return buf; } else { free(buf); return NULL; } } static void saveDoc(const char *current_path, fz_document *doc) { char *tmp; pdf_document *idoc = pdf_specifics(ctx, doc); pdf_write_options opts = { 0 }; opts.do_incremental = 1; if (!idoc) return; tmp = tmp_path(current_path); if (tmp) { int written = 0; fz_var(written); fz_try(ctx) { FILE *fin = fopen(current_path, "rb"); FILE *fout = fopen(tmp, "wb"); char buf[256]; size_t n; int err = 1; if (fin && fout) { while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) fwrite(buf, 1, n, fout); err = (ferror(fin) || ferror(fout)); } if (fin) fclose(fin); if (fout) fclose(fout); if (!err) { pdf_save_document(ctx, idoc, tmp, &opts); written = 1; } } fz_catch(ctx) { written = 0; } if (written) { rename(tmp, current_path); } free(tmp); } } @implementation MuDocumentController { fz_document *doc; MuDocRef *docRef; NSString *key; NSString *_filePath; BOOL reflowMode; MuOutlineController *outline; UIScrollView *canvas; UILabel *indicator; UISlider *slider; UISearchBar *searchBar; UIBarButtonItem *nextButton, *prevButton, *cancelButton, *searchButton, *outlineButton, *linkButton; UIBarButtonItem *moreButton; UIBarButtonItem *shareButton, *printButton, *annotButton; UIBarButtonItem *highlightButton, *underlineButton, *strikeoutButton; UIBarButtonItem *inkButton; UIBarButtonItem *tickButton; UIBarButtonItem *deleteButton; UIBarButtonItem *reflowButton; UIBarButtonItem *backButton; UIBarButtonItem *sliderWrapper; int barmode; int searchPage; int cancelSearch; int showLinks; int width; // current screen size int height; int current; // currently visible page int scroll_animating; // stop view updates during scrolling animations float scale; // scale applied to views (only used in reflow mode) BOOL _isRotating; } - (instancetype) initWithFilename: (NSString*)filename path:(NSString *)cstr document: (MuDocRef *)aDoc { self = [super initWithNibName:nil bundle:nil]; if (!self) return nil; #if __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 if ([self respondsToSelector:@selector(automaticallyAdjustsScrollViewInsets)]) self.automaticallyAdjustsScrollViewInsets = NO; #endif key = [filename copy]; docRef = [aDoc retain]; doc = docRef->doc; _filePath = [cstr copy]; // this will be created right before the outline is shown outline = nil; dispatch_sync(queue, ^{}); return self; } - (UIBarButtonItem *) newResourceBasedButton:(NSString *)resource withAction:(SEL)selector { if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { return [[UIBarButtonItem alloc] initWithImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:resource ofType:@"png"]] style:UIBarButtonItemStylePlain target:self action:selector]; } else { UIView *buttonView; BOOL iOS7Style = (([UIDevice currentDevice].systemVersion).floatValue >= 7.0f); UIButton *button = [UIButton buttonWithType:iOS7Style ? UIButtonTypeSystem : UIButtonTypeCustom]; [button setImage:[UIImage imageNamed:resource] forState:UIControlStateNormal]; [button addTarget:self action:selector forControlEvents:UIControlEventTouchUpInside]; [button sizeToFit]; buttonView = button; return [[UIBarButtonItem alloc] initWithCustomView:buttonView]; } } - (void) addMainMenuButtons { NSMutableArray *array = [NSMutableArray arrayWithCapacity:3]; [array addObject:moreButton]; [array addObject:searchButton]; if (outlineButton) [array addObject:outlineButton]; [array addObject:reflowButton]; [array addObject:linkButton]; self.navigationItem.rightBarButtonItems = array ; self.navigationItem.leftBarButtonItem = backButton; } - (void) loadView { [[NSUserDefaults standardUserDefaults] setObject: key forKey: @"OpenDocumentKey"]; current = (int)[[NSUserDefaults standardUserDefaults] integerForKey: key]; if (current < 0 || current >= fz_count_pages(ctx, doc)) current = 0; UIView *view = [[UIView alloc] initWithFrame: CGRectZero]; view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [view setAutoresizesSubviews: YES]; view.backgroundColor = [UIColor grayColor]; canvas = [[UIScrollView alloc] initWithFrame: CGRectMake(0,0,GAP,0)]; canvas.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [canvas setPagingEnabled: YES]; [canvas setShowsHorizontalScrollIndicator: NO]; [canvas setShowsVerticalScrollIndicator: NO]; canvas.delegate = self; UITapGestureRecognizer *tapRecog = [[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(onTap:)]; tapRecog.delegate = self; [canvas addGestureRecognizer: tapRecog]; [tapRecog release]; // In reflow mode, we need to track pinch gestures on the canvas and pass // the scale changes to the subviews. UIPinchGestureRecognizer *pinchRecog = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(onPinch:)]; pinchRecog.delegate = self; [canvas addGestureRecognizer:pinchRecog]; [pinchRecog release]; scale = 1.0; scroll_animating = NO; indicator = [[UILabel alloc] initWithFrame: CGRectZero]; indicator.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin; indicator.text = @"0000 of 9999"; [indicator sizeToFit]; indicator.center = CGPointMake(0, INDICATOR_Y); indicator.textAlignment = NSTextAlignmentCenter; indicator.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent: 0.5]; indicator.textColor = [UIColor whiteColor]; [view addSubview: canvas]; [view addSubview: indicator]; slider = [[UISlider alloc] initWithFrame: CGRectZero]; slider.minimumValue = 0; slider.maximumValue = fz_count_pages(ctx, doc) - 1; [slider addTarget: self action: @selector(onSlide:) forControlEvents: UIControlEventValueChanged]; if ([UIDevice currentDevice].systemVersion.floatValue < 7.0) { sliderWrapper = [[UIBarButtonItem alloc] initWithCustomView: slider]; self.toolbarItems = @[sliderWrapper]; } // Set up the buttons on the navigation and search bar fz_outline *outlineRoot; fz_try(ctx) outlineRoot = fz_load_outline(ctx, doc); fz_catch(ctx) outlineRoot = NULL; if (outlineRoot) { // only show the outline button if there is an outline outlineButton = [self newResourceBasedButton:@"ic_list" withAction:@selector(onShowOutline:)]; fz_drop_outline(ctx, outlineRoot); } linkButton = [self newResourceBasedButton:@"ic_link" withAction:@selector(onToggleLinks:)]; cancelButton = [self newResourceBasedButton:@"ic_cancel" withAction:@selector(onCancel:)]; searchButton = [self newResourceBasedButton:@"ic_magnifying_glass" withAction:@selector(onShowSearch:)]; prevButton = [self newResourceBasedButton:@"ic_arrow_left" withAction:@selector(onSearchPrev:)]; nextButton = [self newResourceBasedButton:@"ic_arrow_right" withAction:@selector(onSearchNext:)]; reflowButton = [self newResourceBasedButton:@"ic_reflow" withAction:@selector(onToggleReflow:)]; moreButton = [self newResourceBasedButton:@"ic_more" withAction:@selector(onMore:)]; annotButton = [self newResourceBasedButton:@"ic_annotation" withAction:@selector(onAnnot:)]; shareButton = [self newResourceBasedButton:@"ic_share" withAction:@selector(onShare:)]; printButton = [self newResourceBasedButton:@"ic_print" withAction:@selector(onPrint:)]; highlightButton = [self newResourceBasedButton:@"ic_highlight" withAction:@selector(onHighlight:)]; underlineButton = [self newResourceBasedButton:@"ic_underline" withAction:@selector(onUnderline:)]; strikeoutButton = [self newResourceBasedButton:@"ic_strike" withAction:@selector(onStrikeout:)]; inkButton = [self newResourceBasedButton:@"ic_pen" withAction:@selector(onInk:)]; tickButton = [self newResourceBasedButton:@"ic_check" withAction:@selector(onTick:)]; deleteButton = [self newResourceBasedButton:@"ic_trash" withAction:@selector(onDelete:)]; searchBar = [[UISearchBar alloc] initWithFrame: CGRectMake(0,0,50,32)]; backButton = [self newResourceBasedButton:@"ic_arrow_left" withAction:@selector(onBack:)]; searchBar.placeholder = @"Search"; searchBar.delegate = self; [prevButton setEnabled: NO]; [nextButton setEnabled: NO]; [self addMainMenuButtons]; // TODO: add activityindicator to search bar self.view = view; [view release]; } - (void) dealloc { [docRef release]; docRef = nil; doc = NULL; [indicator release]; indicator = nil; [slider release]; slider = nil; [sliderWrapper release]; sliderWrapper = nil; [reflowButton release]; reflowButton = nil; [backButton release]; backButton = nil; [moreButton release]; moreButton = nil; [searchBar release]; searchBar = nil; [outlineButton release]; outlineButton = nil; [linkButton release]; linkButton = nil; [searchButton release]; searchButton = nil; [cancelButton release]; cancelButton = nil; [prevButton release]; prevButton = nil; [nextButton release]; nextButton = nil; [shareButton release]; shareButton = nil; [printButton release]; printButton = nil; [annotButton release]; annotButton = nil; [highlightButton release]; highlightButton = nil; [underlineButton release]; underlineButton = nil; [strikeoutButton release]; strikeoutButton = nil; [inkButton release]; inkButton = nil; [tickButton release]; tickButton = nil; [deleteButton release]; deleteButton = nil; [canvas release]; canvas = nil; [_filePath release]; _filePath = NULL; [outline release]; [key release]; [super dealloc]; } - (void) viewWillAppear: (BOOL)animated { [super viewWillAppear:animated]; self.title = key.lastPathComponent; slider.value = current; if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0) [self.navigationController.toolbar addSubview:slider]; indicator.text = [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(ctx, doc)]; [self.navigationController setToolbarHidden: NO animated: animated]; } - (void) viewWillLayoutSubviews { CGSize size = canvas.frame.size; int max_width = fz_max(width, size.width); width = size.width; height = size.height; canvas.contentInset = UIEdgeInsetsZero; canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * width, height); canvas.contentOffset = CGPointMake(current * width, 0); [sliderWrapper setWidth: SLIDER_W]; searchBar.frame = CGRectMake(0,0,SEARCH_W,32); if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0) { CGRect r = self.navigationController.toolbar.frame; r.origin.x = 0; r.origin.y = 0; slider.frame = r; } [self.navigationController.toolbar setNeedsLayout]; // force layout! // use max_width so we don't clamp the content offset too early during animation canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * max_width, height); canvas.contentOffset = CGPointMake(current * width, 0); for (UIView *view in canvas.subviews) { if (view.number == current) { view.frame = CGRectMake(view.number * width, 0, width-GAP, height); [view willRotate]; } } for (UIView *view in canvas.subviews) { if (view.number != current) { view.frame = CGRectMake(view.number * width, 0, width-GAP, height); [view willRotate]; } } } - (void) viewDidAppear: (BOOL)animated { [super viewDidAppear:animated]; [self scrollViewDidScroll: canvas]; } - (void) viewWillDisappear: (BOOL)animated { [super viewWillDisappear:animated]; if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0) [slider removeFromSuperview]; self.title = @"Resume"; [[NSUserDefaults standardUserDefaults] removeObjectForKey: @"OpenDocumentKey"]; [self.navigationController setToolbarHidden: YES animated: animated]; } - (void) showNavigationBar { if (self.navigationController.navigationBarHidden) { [sliderWrapper setWidth: SLIDER_W]; if ([UIDevice currentDevice].systemVersion.floatValue >= 7.0) { CGRect r = self.navigationController.toolbar.frame; r.origin.x = 0; r.origin.y = 0; slider.frame = r; } [self.navigationController setNavigationBarHidden: NO]; [self.navigationController setToolbarHidden: NO]; [indicator setHidden: NO]; [UIView beginAnimations: @"MuNavBar" context: NULL]; self.navigationController.navigationBar.alpha = 1; self.navigationController.toolbar.alpha = 1; indicator.alpha = 1; [UIView commitAnimations]; } } - (void) hideNavigationBar { if (!self.navigationController.navigationBarHidden) { [searchBar resignFirstResponder]; [UIView beginAnimations: @"MuNavBar" context: NULL]; [UIView setAnimationDelegate: self]; [UIView setAnimationDidStopSelector: @selector(onHideNavigationBarFinished)]; self.navigationController.navigationBar.alpha = 0; self.navigationController.toolbar.alpha = 0; indicator.alpha = 0; [UIView commitAnimations]; } } - (void) onHideNavigationBarFinished { [self.navigationController setNavigationBarHidden: YES]; [self.navigationController setToolbarHidden: YES]; [indicator setHidden: YES]; } - (void) onShowOutline: (id)sender { // rebuild the outline in case the layout has changed fz_outline *root; fz_try(ctx) root = fz_load_outline(ctx, doc); fz_catch(ctx) root = NULL; if (root) { NSMutableArray *titles = [[NSMutableArray alloc] init]; NSMutableArray *pages = [[NSMutableArray alloc] init]; flattenOutline(titles, pages, root, 0); [outline release]; if (titles.count) outline = [[MuOutlineController alloc] initWithTarget: self titles: titles pages: pages]; [titles release]; [pages release]; fz_drop_outline(ctx, root); } // now show it [self.navigationController pushViewController: outline animated: YES]; } - (void) onToggleLinks: (id)sender { showLinks = !showLinks; for (UIView *view in canvas.subviews) { if (showLinks) [view showLinks]; else [view hideLinks]; } } - (void) onToggleReflow: (id)sender { reflowMode = !reflowMode; annotButton.enabled = !reflowMode; searchButton.enabled = !reflowMode; linkButton.enabled = !reflowMode; moreButton.enabled = !reflowMode; [canvas.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; [self scrollViewDidScroll:canvas]; } - (void) showMoreMenu { NSMutableArray *rightbuttons = [NSMutableArray arrayWithObjects:printButton, shareButton, nil]; if (docRef->interactive) [rightbuttons insertObject:annotButton atIndex:0]; self.navigationItem.rightBarButtonItems = rightbuttons; self.navigationItem.leftBarButtonItem = cancelButton; barmode = BARMODE_MORE; } - (void) showAnnotationMenu { self.navigationItem.rightBarButtonItems = @[inkButton, strikeoutButton, underlineButton, highlightButton]; self.navigationItem.leftBarButtonItem = cancelButton; for (UIView *view in canvas.subviews) { if (view.number == current) [view deselectAnnotation]; } barmode = BARMODE_ANNOTATION; } - (void) update { for (UIView *view in canvas.subviews) [view update]; } - (void) onMore: (id)sender { [self showMoreMenu]; } - (void) onAnnot: (id)sender { [self showAnnotationMenu]; } - (void) onPrint: (id)sender { UIPrintInteractionController *pic = [UIPrintInteractionController sharedPrintController]; if (pic) { UIPrintInfo *printInfo = [UIPrintInfo printInfo]; printInfo.outputType = UIPrintInfoOutputGeneral; printInfo.jobName = key; printInfo.duplex = UIPrintInfoDuplexLongEdge; pic.printInfo = printInfo; pic.showsPageRange = YES; pic.printPageRenderer = [[[MuPrintPageRenderer alloc] initWithDocRef:docRef] autorelease]; void (^completionHandler)(UIPrintInteractionController *, BOOL, NSError *) = ^(UIPrintInteractionController *pic, BOOL completed, NSError *error) { if (!completed && error) NSLog(@"FAILED! due to error in domain %@ with error code %u", error.domain, (unsigned int)error.code); }; if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { [pic presentFromBarButtonItem:printButton animated:YES completionHandler:completionHandler]; } else { [pic presentAnimated:YES completionHandler:completionHandler]; } } } - (void) shareDocument { NSURL *url = [NSURL fileURLWithPath:_filePath]; UIActivityViewController *cont = [[UIActivityViewController alloc] initWithActivityItems:@[url] applicationActivities:nil]; cont.popoverPresentationController.barButtonItem = shareButton; [self presentViewController:cont animated:YES completion:nil]; [cont release]; } - (void) onShare: (id)sender { pdf_document *idoc = pdf_specifics(ctx, doc); if (idoc && pdf_has_unsaved_changes(ctx, idoc)) { UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:AlertTitle message:ShareAlertMessage delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Save and Share", nil]; [alertView show]; [alertView release]; } else { [self shareDocument]; } } - (void) textSelectModeOn { self.navigationItem.rightBarButtonItems = @[tickButton]; for (UIView *view in canvas.subviews) { if (view.number == current) [view textSelectModeOn]; } } - (void) textSelectModeOff { for (UIView *view in canvas.subviews) { [view textSelectModeOff]; } } - (void) inkModeOn { self.navigationItem.rightBarButtonItems = @[tickButton]; for (UIView *view in canvas.subviews) { if (view.number == current) [view inkModeOn]; } } - (void) deleteModeOn { self.navigationItem.rightBarButtonItems = @[deleteButton]; barmode = BARMODE_DELETE; } - (void) inkModeOff { for (UIView *view in canvas.subviews) { [view inkModeOff]; } } - (void) onHighlight: (id)sender { barmode = BARMODE_HIGHLIGHT; [self textSelectModeOn]; } - (void) onUnderline: (id)sender { barmode = BARMODE_UNDERLINE; [self textSelectModeOn]; } - (void) onStrikeout: (id)sender { barmode = BARMODE_STRIKE; [self textSelectModeOn]; } - (void) onInk: (id)sender { barmode = BARMODE_INK; [self inkModeOn]; } - (void) onShowSearch: (id)sender { self.navigationItem.rightBarButtonItems = @[nextButton, prevButton]; self.navigationItem.leftBarButtonItem = cancelButton; self.navigationItem.titleView = searchBar; [searchBar becomeFirstResponder]; barmode = BARMODE_SEARCH; } - (void) onTick: (id)sender { for (UIView *view in canvas.subviews) { if (view.number == current) { switch (barmode) { case BARMODE_HIGHLIGHT: [view saveSelectionAsMarkup:PDF_ANNOT_HIGHLIGHT]; break; case BARMODE_UNDERLINE: [view saveSelectionAsMarkup:PDF_ANNOT_UNDERLINE]; break; case BARMODE_STRIKE: [view saveSelectionAsMarkup:PDF_ANNOT_STRIKE_OUT]; break; case BARMODE_INK: [view saveInk]; } } } [self showAnnotationMenu]; } - (void) onDelete: (id)sender { for (UIView *view in canvas.subviews) { if (view.number == current) [view deleteSelectedAnnotation]; } [self showAnnotationMenu]; } - (void) onCancel: (id)sender { switch (barmode) { case BARMODE_SEARCH: cancelSearch = YES; [searchBar resignFirstResponder]; [self resetSearch]; /* fallthrough */ case BARMODE_ANNOTATION: case BARMODE_MORE: [self.navigationItem setTitleView: nil]; [self addMainMenuButtons]; barmode = BARMODE_MAIN; break; case BARMODE_HIGHLIGHT: case BARMODE_UNDERLINE: case BARMODE_STRIKE: case BARMODE_DELETE: [self showAnnotationMenu]; [self textSelectModeOff]; break; case BARMODE_INK: [self showAnnotationMenu]; [self inkModeOff]; break; } } - (void) onBack: (id)sender { pdf_document *idoc = pdf_specifics(ctx, doc); if (idoc && pdf_has_unsaved_changes(ctx, idoc)) { UIAlertView *saveAlert = [[UIAlertView alloc] initWithTitle:AlertTitle message:CloseAlertMessage delegate:self cancelButtonTitle:@"Discard" otherButtonTitles:@"Save", nil]; [saveAlert show]; [saveAlert release]; } else { [self.navigationController popViewControllerAnimated:YES]; } } - (void) alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex { if ([CloseAlertMessage isEqualToString:alertView.message]) { if (buttonIndex == 1) saveDoc(_filePath.UTF8String, doc); [alertView dismissWithClickedButtonIndex:buttonIndex animated:YES]; [self.navigationController popViewControllerAnimated:YES]; } if ([ShareAlertMessage isEqualToString:alertView.message]) { [alertView dismissWithClickedButtonIndex:buttonIndex animated:NO]; if (buttonIndex == 1) { saveDoc(_filePath.UTF8String, doc); [self shareDocument]; } } } - (void) resetSearch { searchPage = -1; for (UIView *view in canvas.subviews) [view clearSearchResults]; } - (void) showSearchResults: (int)count forPage: (int)number { printf("search found match on page %d\n", number); searchPage = number; [self gotoPage: number animated: NO]; for (UIView *view in canvas.subviews) if (view.number == number) [view showSearchResults: count]; else [view clearSearchResults]; } - (void) searchInDirection: (int)dir { UITextField *searchField; char *needle; int start; [searchBar resignFirstResponder]; if (searchPage == current) start = current + dir; else start = current; needle = strdup(searchBar.text.UTF8String); searchField = nil; for (id view in searchBar.subviews) if ([view isKindOfClass: [UITextField class]]) searchField = view; [prevButton setEnabled: NO]; [nextButton setEnabled: NO]; [searchField setEnabled: NO]; cancelSearch = NO; dispatch_async(queue, ^{ for (int i = start; i >= 0 && i < fz_count_pages(ctx, doc); i += dir) { int n = search_page(doc, i, needle, NULL); if (n) { dispatch_async(dispatch_get_main_queue(), ^{ [prevButton setEnabled: YES]; [nextButton setEnabled: YES]; [searchField setEnabled: YES]; [self showSearchResults: n forPage: i]; free(needle); }); return; } if (cancelSearch) { dispatch_async(dispatch_get_main_queue(), ^{ [prevButton setEnabled: YES]; [nextButton setEnabled: YES]; [searchField setEnabled: YES]; free(needle); }); return; } } dispatch_async(dispatch_get_main_queue(), ^{ printf("no search results found\n"); [prevButton setEnabled: YES]; [nextButton setEnabled: YES]; [searchField setEnabled: YES]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"No matches found for:" message: @(needle) delegate: nil cancelButtonTitle: @"Close" otherButtonTitles: nil]; [alert show]; [alert release]; free(needle); }); }); } - (void) onSearchPrev: (id)sender { [self searchInDirection: -1]; } - (void) onSearchNext: (id)sender { [self searchInDirection: 1]; } - (void) searchBarSearchButtonClicked: (UISearchBar*)sender { [self onSearchNext: sender]; } - (void) searchBar: (UISearchBar*)sender textDidChange: (NSString*)searchText { [self resetSearch]; if (searchBar.text.length > 0) { [prevButton setEnabled: YES]; [nextButton setEnabled: YES]; } else { [prevButton setEnabled: NO]; [nextButton setEnabled: NO]; } } - (void) onSlide: (id)sender { int number = slider.value; if (slider.tracking) indicator.text = [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(ctx, doc)]; else [self gotoPage: number animated: NO]; } - (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { // For reflow mode, we load UIWebViews into the canvas. Returning YES // here prevents them stealing our tap and pinch events. return YES; } - (void) onTap: (UITapGestureRecognizer*)sender { CGPoint p = [sender locationInView: canvas]; CGPoint ofs = canvas.contentOffset; float x0 = (width - GAP) / 5; float x1 = (width - GAP) - x0; p.x -= ofs.x; p.y -= ofs.y; __block BOOL tapHandled = NO; for (UIView *view in canvas.subviews) { CGPoint pp = [sender locationInView:view]; if (CGRectContainsPoint(view.bounds, pp)) { MuTapResult *result = [view handleTap:pp]; __block BOOL hitAnnot = NO; [result switchCaseInternal:^(MuTapResultInternalLink *link) { [self gotoPage:link.pageNumber animated:NO]; tapHandled = YES; } caseExternal:^(MuTapResultExternalLink *link) { // Not currently supported } caseRemote:^(MuTapResultRemoteLink *link) { // Not currently supported } caseWidget:^(MuTapResultWidget *widget) { tapHandled = YES; } caseAnnotation:^(MuTapResultAnnotation *annot) { hitAnnot = YES; }]; switch (barmode) { case BARMODE_ANNOTATION: if (hitAnnot) [self deleteModeOn]; tapHandled = YES; break; case BARMODE_DELETE: if (!hitAnnot) [self showAnnotationMenu]; tapHandled = YES; break; default: if (hitAnnot) { // Annotation will have been selected, which is wanted // only in annotation-editing mode [view deselectAnnotation]; } break; } if (tapHandled) break; } } if (tapHandled) { // Do nothing further } else if (p.x < x0) { [self gotoPage: current-1 animated: YES]; } else if (p.x > x1) { [self gotoPage: current+1 animated: YES]; } else { if (self.navigationController.navigationBarHidden) [self showNavigationBar]; else if (barmode == BARMODE_MAIN) [self hideNavigationBar]; } } - (void) onPinch:(UIPinchGestureRecognizer*)sender { if (sender.state == UIGestureRecognizerStateBegan) sender.scale = scale; if (sender.scale < MIN_SCALE) sender.scale = MIN_SCALE; if (sender.scale > MAX_SCALE) sender.scale = MAX_SCALE; if (sender.state == UIGestureRecognizerStateEnded) scale = sender.scale; for (UIView *view in canvas.subviews) { // Zoom only the visible page until end of gesture if (view.number == current || sender.state == UIGestureRecognizerStateEnded) [view setScale:sender.scale]; } } - (void) scrollViewWillBeginDragging: (UIScrollView *)scrollView { if (barmode == BARMODE_MAIN) [self hideNavigationBar]; } - (void) scrollViewDidScroll: (UIScrollView*)scrollview { // scrollViewDidScroll seems to get called part way through a screen rotation. // (This is possibly a UIScrollView bug - see // http://stackoverflow.com/questions/4123991/uiscrollview-disable-scrolling-while-rotating-on-iphone-ipad/8141423#8141423 ). // This ends up corrupting the current page number, because the calculation // 'current = x / width' is using the new value of 'width' before the // pages have been resized/repositioned. To avoid this problem, we filter out // calls to scrollViewDidScroll during rotation. if (_isRotating) return; if (width == 0) return; // not visible yet if (scroll_animating) return; // don't mess with layout during animations float x = canvas.contentOffset.x + width * 0.5f; current = x / width; [[NSUserDefaults standardUserDefaults] setInteger: current forKey: key]; indicator.text = [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(ctx, doc)]; slider.value = current; // swap the distant page views out NSMutableSet *invisiblePages = [[NSMutableSet alloc] init]; for (UIView *view in canvas.subviews) { if (view.number != current) [view resetZoomAnimated: YES]; if (view.number < current - 2 || view.number > current + 2) [invisiblePages addObject: view]; } for (UIView *view in invisiblePages) [view removeFromSuperview]; [invisiblePages release]; // don't bother recycling them... [self createPageView: current]; [self createPageView: current - 1]; [self createPageView: current + 1]; // reset search results when page has flipped if (current != searchPage) [self resetSearch]; } - (void) createPageView: (int)number { if (number < 0 || number >= fz_count_pages(ctx, doc)) return; int found = 0; for (UIView *view in canvas.subviews) if (view.number == number) found = 1; if (!found) { UIView *view = reflowMode ? [[MuPageViewReflow alloc] initWithFrame:CGRectMake(number * width, 0, width-GAP, height) document:docRef page:number] : [[MuPageViewNormal alloc] initWithFrame:CGRectMake(number * width, 0, width-GAP, height) dialogCreator:self updater:self document:docRef page:number]; [view setScale:scale]; [canvas addSubview: view]; if (showLinks) [view showLinks]; [view release]; } } - (void) gotoPage: (int)number animated: (BOOL)animated { if (number < 0) number = 0; if (number >= fz_count_pages(ctx, doc)) number = fz_count_pages(ctx, doc) - 1; if (current == number) return; if (animated) { // setContentOffset:animated: does not use the normal animation // framework. It also doesn't play nice with the tap gesture // recognizer. So we do our own page flipping animation here. // We must set the scroll_animating flag so that we don't create // or remove subviews until after the animation, or they'll // swoop in from origo during the animation. scroll_animating = YES; [UIView beginAnimations: @"MuScroll" context: NULL]; [UIView setAnimationDuration: 0.4]; [UIView setAnimationBeginsFromCurrentState: YES]; [UIView setAnimationDelegate: self]; [UIView setAnimationDidStopSelector: @selector(onGotoPageFinished)]; for (UIView *view in canvas.subviews) [view resetZoomAnimated: NO]; canvas.contentOffset = CGPointMake(number * width, 0); slider.value = number; indicator.text = [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(ctx, doc)]; [UIView commitAnimations]; } else { for (UIView *view in canvas.subviews) [view resetZoomAnimated: NO]; canvas.contentOffset = CGPointMake(number * width, 0); } current = number; } - (void) invokeTextDialog:(NSString *)aString okayAction:(void (^)(NSString *))block { MuTextFieldController *tf = [[MuTextFieldController alloc] initWithText:aString okayAction:block]; tf.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:tf animated:YES completion:nil]; [tf release]; } - (void) invokeChoiceDialog:(NSArray *)anArray okayAction:(void (^)(NSArray *))block { MuChoiceFieldController *cf = [[MuChoiceFieldController alloc] initWithChoices:anArray okayAction:block]; cf.modalPresentationStyle = UIModalPresentationFormSheet; [self presentViewController:cf animated:YES completion:nil]; [cf release]; } - (void) onGotoPageFinished { scroll_animating = NO; [self scrollViewDidScroll: canvas]; } - (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o { return YES; } - (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { _isRotating = YES; } - (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation)o { _isRotating = NO; // We need to set these here, because during the animation we may use a wider // size (the maximum of the landscape/portrait widths), to avoid clipping during // the rotation. canvas.contentSize = CGSizeMake(fz_count_pages(ctx, doc) * width, height); canvas.contentOffset = CGPointMake(current * width, 0); } @end