diff options
author | Paul Gardiner <paul@pauls-mac-mini.site> | 2013-09-17 15:04:13 +0100 |
---|---|---|
committer | Paul Gardiner <paul@pauls-mac-mini.site> | 2013-09-17 15:08:40 +0100 |
commit | 43bcd8a7516bfbd455d81de7e00d5e139abce438 (patch) | |
tree | 71efd1fe71fb8dfd6dd4399547f9e10fa2b24d5d /platform/ios/Classes | |
parent | 0f6711fd4db36602034734b063cdaf30a396126c (diff) | |
download | mupdf-43bcd8a7516bfbd455d81de7e00d5e139abce438.tar.xz |
iOS: split the classes into separate files
Diffstat (limited to 'platform/ios/Classes')
-rw-r--r-- | platform/ios/Classes/MuAppDelegate.h | 18 | ||||
-rw-r--r-- | platform/ios/Classes/MuAppDelegate.m | 98 | ||||
-rw-r--r-- | platform/ios/Classes/MuDocumentController.h | 49 | ||||
-rw-r--r-- | platform/ios/Classes/MuDocumentController.m | 575 | ||||
-rw-r--r-- | platform/ios/Classes/MuHitView.h | 28 | ||||
-rw-r--r-- | platform/ios/Classes/MuHitView.m | 94 | ||||
-rw-r--r-- | platform/ios/Classes/MuLibraryController.h | 29 | ||||
-rw-r--r-- | platform/ios/Classes/MuLibraryController.m | 225 | ||||
-rw-r--r-- | platform/ios/Classes/MuOutlineController.h | 17 | ||||
-rw-r--r-- | platform/ios/Classes/MuOutlineController.m | 76 | ||||
-rw-r--r-- | platform/ios/Classes/MuPageView.h | 45 | ||||
-rw-r--r-- | platform/ios/Classes/MuPageView.m | 462 |
12 files changed, 1716 insertions, 0 deletions
diff --git a/platform/ios/Classes/MuAppDelegate.h b/platform/ios/Classes/MuAppDelegate.h new file mode 100644 index 00000000..6131a328 --- /dev/null +++ b/platform/ios/Classes/MuAppDelegate.h @@ -0,0 +1,18 @@ +// +// MuAppDelegate.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +#import "MuLibraryController.h" + +@interface MuAppDelegate : NSObject <UIApplicationDelegate, UINavigationControllerDelegate> +{ + UIWindow *window; + UINavigationController *navigator; + MuLibraryController *library; +} +@end diff --git a/platform/ios/Classes/MuAppDelegate.m b/platform/ios/Classes/MuAppDelegate.m new file mode 100644 index 00000000..aaf0cff9 --- /dev/null +++ b/platform/ios/Classes/MuAppDelegate.m @@ -0,0 +1,98 @@ +// +// MuAppDelegate.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#include "common.h" + +#import "MuAppDelegate.h" + +@implementation MuAppDelegate + +- (BOOL) application: (UIApplication*)application didFinishLaunchingWithOptions: (NSDictionary*)launchOptions +{ + NSString *filename; + + queue = dispatch_queue_create("com.artifex.mupdf.queue", NULL); + + // use at most 128M for resource cache + ctx = fz_new_context(NULL, NULL, 128<<20); + + screenScale = [[UIScreen mainScreen] scale]; + + library = [[MuLibraryController alloc] initWithStyle: UITableViewStylePlain]; + + navigator = [[UINavigationController alloc] initWithRootViewController: library]; + [[navigator navigationBar] setTranslucent: YES]; + [[navigator toolbar] setTranslucent: YES]; + [navigator setDelegate: self]; + + window = [[UIWindow alloc] initWithFrame: [[UIScreen mainScreen] bounds]]; + [window setBackgroundColor: [UIColor scrollViewTexturedBackgroundColor]]; + [window setRootViewController: navigator]; + [window makeKeyAndVisible]; + + filename = [[NSUserDefaults standardUserDefaults] objectForKey: @"OpenDocumentKey"]; + if (filename) + [library openDocument: filename]; + + filename = [launchOptions objectForKey: UIApplicationLaunchOptionsURLKey]; + NSLog(@"urlkey = %@\n", filename); + + return YES; +} + +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation +{ + NSLog(@"openURL: %@\n", url); + if ([url isFileURL]) { + NSString *path = [url path]; + NSString *dir = [NSString stringWithFormat: @"%@/Documents/", NSHomeDirectory()]; + path = [path stringByReplacingOccurrencesOfString:@"/private" withString:@""]; + path = [path stringByReplacingOccurrencesOfString:dir withString:@""]; + NSLog(@"file relative path: %@\n", path); + [library openDocument:path]; + return YES; + } + return NO; +} + +- (void)applicationDidEnterBackground:(UIApplication *)application +{ + printf("applicationDidEnterBackground!\n"); + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)applicationWillEnterForeground:(UIApplication *)application +{ + printf("applicationWillEnterForeground!\n"); +} + +- (void)applicationDidBecomeActive:(UIApplication *)application +{ + printf("applicationDidBecomeActive!\n"); +} + +- (void)applicationWillTerminate:(UIApplication *)application +{ + printf("applicationWillTerminate!\n"); + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application +{ + printf("applicationDidReceiveMemoryWarning\n"); +} + +- (void) dealloc +{ + dispatch_release(queue); + [library release]; + [navigator release]; + [window release]; + [super dealloc]; +} + +@end diff --git a/platform/ios/Classes/MuDocumentController.h b/platform/ios/Classes/MuDocumentController.h new file mode 100644 index 00000000..f5d6115a --- /dev/null +++ b/platform/ios/Classes/MuDocumentController.h @@ -0,0 +1,49 @@ +// +// MuDocumentController.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +#undef ABS +#undef MIN +#undef MAX + +#include "mupdf/fitz.h" + +#import "MuOutlineController.h" + +@interface MuDocumentController : UIViewController <UIScrollViewDelegate, UISearchBarDelegate> +{ + fz_document *doc; + NSString *key; + MuOutlineController *outline; + UIScrollView *canvas; + UILabel *indicator; + UISlider *slider; + UISearchBar *searchBar; + UIBarButtonItem *nextButton, *prevButton, *cancelButton, *searchButton, *outlineButton, *linkButton; + UIBarButtonItem *sliderWrapper; + 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 +} +- (id) initWithFilename: (NSString*)nsfilename document: (fz_document *)aDoc; +- (void) createPageView: (int)number; +- (void) gotoPage: (int)number animated: (BOOL)animated; +- (void) onShowOutline: (id)sender; +- (void) onShowSearch: (id)sender; +- (void) onCancelSearch: (id)sender; +- (void) resetSearch; +- (void) showSearchResults: (int)count forPage: (int)number; +- (void) onSlide: (id)sender; +- (void) onTap: (UITapGestureRecognizer*)sender; +- (void) showNavigationBar; +- (void) hideNavigationBar; +@end diff --git a/platform/ios/Classes/MuDocumentController.m b/platform/ios/Classes/MuDocumentController.m new file mode 100644 index 00000000..89ee0da8 --- /dev/null +++ b/platform/ios/Classes/MuDocumentController.m @@ -0,0 +1,575 @@ +// +// MuDocumentController.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#include "common.h" +#import "MuPageView.h" + +#import "MuDocumentController.h" + +#define GAP 20 +#define INDICATOR_Y -44-24 +#define SLIDER_W (width - GAP - 24) +#define SEARCH_W (width - GAP - 170) + +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) + { + if (outline->dest.kind == FZ_LINK_GOTO) + { + int page = outline->dest.ld.gotor.page; + if (page >= 0 && outline->title) + { + NSString *title = [NSString stringWithUTF8String: outline->title]; + [titles addObject: [NSString stringWithFormat: @"%s%@", indent, title]]; + [pages addObject: [NSNumber numberWithInt: page]]; + } + } + flattenOutline(titles, pages, outline->down, level + 1); + outline = outline->next; + } +} + +@implementation MuDocumentController + +- (id) initWithFilename: (NSString*)filename document: (fz_document *)aDoc +{ + self = [super init]; + if (!self) + return nil; + + key = [filename retain]; + doc = aDoc; + + dispatch_sync(queue, ^{}); + + fz_outline *root = fz_load_outline(doc); + if (root) { + NSMutableArray *titles = [[NSMutableArray alloc] init]; + NSMutableArray *pages = [[NSMutableArray alloc] init]; + flattenOutline(titles, pages, root, 0); + if ([titles count]) + outline = [[MuOutlineController alloc] initWithTarget: self titles: titles pages: pages]; + [titles release]; + [pages release]; + fz_free_outline(ctx, root); + } + + return self; +} + +- (void) loadView +{ + [[NSUserDefaults standardUserDefaults] setObject: key forKey: @"OpenDocumentKey"]; + + current = [[NSUserDefaults standardUserDefaults] integerForKey: key]; + if (current < 0 || current >= fz_count_pages(doc)) + current = 0; + + UIView *view = [[UIView alloc] initWithFrame: CGRectZero]; + [view setAutoresizingMask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight]; + [view setAutoresizesSubviews: YES]; + + canvas = [[UIScrollView alloc] initWithFrame: CGRectMake(0,0,GAP,0)]; + [canvas setAutoresizingMask: UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight]; + [canvas setPagingEnabled: YES]; + [canvas setShowsHorizontalScrollIndicator: NO]; + [canvas setShowsVerticalScrollIndicator: NO]; + [canvas setDelegate: self]; + + [canvas addGestureRecognizer: [[[UITapGestureRecognizer alloc] initWithTarget: self action: @selector(onTap:)] autorelease]]; + + scroll_animating = NO; + + indicator = [[UILabel alloc] initWithFrame: CGRectZero]; + [indicator setAutoresizingMask: UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin]; + [indicator setText: @"0000 of 9999"]; + [indicator sizeToFit]; + [indicator setCenter: CGPointMake(0, INDICATOR_Y)]; + [indicator setTextAlignment: UITextAlignmentCenter]; + [indicator setBackgroundColor: [[UIColor blackColor] colorWithAlphaComponent: 0.5]]; + [indicator setTextColor: [UIColor whiteColor]]; + + [view addSubview: canvas]; + [view addSubview: indicator]; + + slider = [[UISlider alloc] initWithFrame: CGRectZero]; + [slider setMinimumValue: 0]; + [slider setMaximumValue: fz_count_pages(doc) - 1]; + [slider addTarget: self action: @selector(onSlide:) forControlEvents: UIControlEventValueChanged]; + + sliderWrapper = [[UIBarButtonItem alloc] initWithCustomView: slider]; + + [self setToolbarItems: [NSArray arrayWithObjects: sliderWrapper, nil]]; + + // Set up the buttons on the navigation and search bar + + if (outline) { + outlineButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem: UIBarButtonSystemItemBookmarks + target:self action:@selector(onShowOutline:)]; + } + linkButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem: UIBarButtonSystemItemAction + target:self action:@selector(onToggleLinks:)]; + cancelButton = [[UIBarButtonItem alloc] + initWithTitle: @"Cancel" style: UIBarButtonItemStyleBordered + target:self action:@selector(onCancelSearch:)]; + searchButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem: UIBarButtonSystemItemSearch + target:self action:@selector(onShowSearch:)]; + prevButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem: UIBarButtonSystemItemRewind + target:self action:@selector(onSearchPrev:)]; + nextButton = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem: UIBarButtonSystemItemFastForward + target:self action:@selector(onSearchNext:)]; + + searchBar = [[UISearchBar alloc] initWithFrame: CGRectMake(0,0,50,32)]; + [searchBar setPlaceholder: @"Search"]; + [searchBar setDelegate: self]; + // HACK to make transparent background + [[searchBar.subviews objectAtIndex:0] removeFromSuperview]; + + [prevButton setEnabled: NO]; + [nextButton setEnabled: NO]; + + [[self navigationItem] setRightBarButtonItems: + [NSArray arrayWithObjects: searchButton, linkButton, outlineButton, nil]]; + + // TODO: add activityindicator to search bar + + [self setView: view]; + [view release]; +} + +- (void) dealloc +{ + if (doc) { + fz_document *self_doc = doc; // don't auto-retain self here! + dispatch_async(queue, ^{ + printf("close document\n"); + fz_close_document(self_doc); + }); + } + + [indicator release]; indicator = nil; + [slider release]; slider = nil; + [sliderWrapper release]; sliderWrapper = nil; + [searchBar release]; searchBar = nil; + [outlineButton release]; outlineButton = nil; + [searchButton release]; searchButton = nil; + [cancelButton release]; cancelButton = nil; + [prevButton release]; prevButton = nil; + [nextButton release]; nextButton = nil; + [canvas release]; canvas = nil; + + [outline release]; + [key release]; + [super dealloc]; +} + +- (void) viewWillAppear: (BOOL)animated +{ + [self setTitle: [key lastPathComponent]]; + + [slider setValue: current]; + + [indicator setText: [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(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 setContentInset: UIEdgeInsetsZero]; + [canvas setContentSize: CGSizeMake(fz_count_pages(doc) * width, height)]; + [canvas setContentOffset: CGPointMake(current * width, 0)]; + + [sliderWrapper setWidth: SLIDER_W]; + [searchBar setFrame: CGRectMake(0,0,SEARCH_W,32)]; + + [[[self navigationController] toolbar] setNeedsLayout]; // force layout! + + // use max_width so we don't clamp the content offset too early during animation + [canvas setContentSize: CGSizeMake(fz_count_pages(doc) * max_width, height)]; + [canvas setContentOffset: CGPointMake(current * width, 0)]; + + for (MuPageView *view in [canvas subviews]) { + if ([view number] == current) { + [view setFrame: CGRectMake([view number] * width, 0, width-GAP, height)]; + [view willRotate]; + } + } + for (MuPageView *view in [canvas subviews]) { + if ([view number] != current) { + [view setFrame: CGRectMake([view number] * width, 0, width-GAP, height)]; + [view willRotate]; + } + } +} + +- (void) viewDidAppear: (BOOL)animated +{ + [self scrollViewDidScroll: canvas]; +} + +- (void) viewWillDisappear: (BOOL)animated +{ + [self setTitle: @"Resume"]; + [[NSUserDefaults standardUserDefaults] removeObjectForKey: @"OpenDocumentKey"]; + [[self navigationController] setToolbarHidden: YES animated: animated]; +} + +- (void) showNavigationBar +{ + if ([[self navigationController] isNavigationBarHidden]) { + [[self navigationController] setNavigationBarHidden: NO]; + [[self navigationController] setToolbarHidden: NO]; + [indicator setHidden: NO]; + + [UIView beginAnimations: @"MuNavBar" context: NULL]; + + [[[self navigationController] navigationBar] setAlpha: 1]; + [[[self navigationController] toolbar] setAlpha: 1]; + [indicator setAlpha: 1]; + + [UIView commitAnimations]; + } +} + +- (void) hideNavigationBar +{ + if (![[self navigationController] isNavigationBarHidden]) { + [searchBar resignFirstResponder]; + + [UIView beginAnimations: @"MuNavBar" context: NULL]; + [UIView setAnimationDelegate: self]; + [UIView setAnimationDidStopSelector: @selector(onHideNavigationBarFinished)]; + + [[[self navigationController] navigationBar] setAlpha: 0]; + [[[self navigationController] toolbar] setAlpha: 0]; + [indicator setAlpha: 0]; + + [UIView commitAnimations]; + } +} + +- (void) onHideNavigationBarFinished +{ + [[self navigationController] setNavigationBarHidden: YES]; + [[self navigationController] setToolbarHidden: YES]; + [indicator setHidden: YES]; +} + +- (void) onShowOutline: (id)sender +{ + [[self navigationController] pushViewController: outline animated: YES]; +} + +- (void) onToggleLinks: (id)sender +{ + showLinks = !showLinks; + for (MuPageView *view in [canvas subviews]) + { + if (showLinks) + [view showLinks]; + else + [view hideLinks]; + } +} + +- (void) onShowSearch: (id)sender +{ + [[self navigationItem] setTitleView: searchBar]; + [[self navigationItem] setRightBarButtonItems: + [NSArray arrayWithObjects: nextButton, prevButton, nil]]; + [[self navigationItem] setLeftBarButtonItem: cancelButton]; + [searchBar becomeFirstResponder]; +} + +- (void) onCancelSearch: (id)sender +{ + cancelSearch = YES; + [searchBar resignFirstResponder]; + [[self navigationItem] setTitleView: nil]; + [[self navigationItem] setRightBarButtonItems: + [NSArray arrayWithObjects: searchButton, linkButton, outlineButton, nil]]; + [[self navigationItem] setLeftBarButtonItem: nil]; + [self resetSearch]; +} + +- (void) resetSearch +{ + searchPage = -1; + for (MuPageView *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 (MuPageView *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(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: [NSString stringWithUTF8String: 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 isTracking]) + [indicator setText: [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(doc)]]; + else + [self gotoPage: number animated: NO]; +} + +- (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; + 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] isNavigationBarHidden]) + [self showNavigationBar]; + else + [self hideNavigationBar]; + } +} + +- (void) scrollViewWillBeginDragging: (UIScrollView *)scrollView +{ + [self hideNavigationBar]; +} + +- (void) scrollViewDidScroll: (UIScrollView*)scrollview +{ + 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 setText: [NSString stringWithFormat: @" %d of %d ", current+1, fz_count_pages(doc)]]; + [slider setValue: current]; + + // swap the distant page views out + + NSMutableSet *invisiblePages = [[NSMutableSet alloc] init]; + for (MuPageView *view in [canvas subviews]) { + if ([view number] != current) + [view resetZoomAnimated: YES]; + if ([view number] < current - 2 || [view number] > current + 2) + [invisiblePages addObject: view]; + } + for (MuPageView *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(doc)) + return; + int found = 0; + for (MuPageView *view in [canvas subviews]) + if ([view number] == number) + found = 1; + if (!found) { + MuPageView *view = [[MuPageView alloc] initWithFrame: CGRectMake(number * width, 0, width-GAP, height) document: doc page: number]; + [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(doc)) + number = fz_count_pages(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 (MuPageView *view in [canvas subviews]) + [view resetZoomAnimated: NO]; + + [canvas setContentOffset: CGPointMake(number * width, 0)]; + [slider setValue: number]; + [indicator setText: [NSString stringWithFormat: @" %d of %d ", number+1, fz_count_pages(doc)]]; + + [UIView commitAnimations]; + } else { + for (MuPageView *view in [canvas subviews]) + [view resetZoomAnimated: NO]; + [canvas setContentOffset: CGPointMake(number * width, 0)]; + } + current = number; +} + +- (void) onGotoPageFinished +{ + scroll_animating = NO; + [self scrollViewDidScroll: canvas]; +} + +- (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} + +- (void) didRotateFromInterfaceOrientation: (UIInterfaceOrientation)o +{ + [canvas setContentSize: CGSizeMake(fz_count_pages(doc) * width, height)]; + [canvas setContentOffset: CGPointMake(current * width, 0)]; +} + +@end diff --git a/platform/ios/Classes/MuHitView.h b/platform/ios/Classes/MuHitView.h new file mode 100644 index 00000000..1be5dcb2 --- /dev/null +++ b/platform/ios/Classes/MuHitView.h @@ -0,0 +1,28 @@ +// +// MuHitView.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +#undef ABS +#undef MIN +#undef MAX + +#include "mupdf/fitz.h" + +@interface MuHitView : UIView +{ + CGSize pageSize; + int hitCount; + CGRect hitRects[500]; + int linkPage[500]; + char *linkUrl[500]; + UIColor *color; +} +- (id) initWithSearchResults: (int)n forDocument: (fz_document *)doc; +- (id) initWithLinks: (fz_link*)links forDocument: (fz_document *)doc; +- (void) setPageSize: (CGSize)s; +@end diff --git a/platform/ios/Classes/MuHitView.m b/platform/ios/Classes/MuHitView.m new file mode 100644 index 00000000..be67601a --- /dev/null +++ b/platform/ios/Classes/MuHitView.m @@ -0,0 +1,94 @@ +// +// MuHitView.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import "common.h" +#import "MuHitView.h" + +@implementation MuHitView + +- (id) initWithSearchResults: (int)n forDocument: (fz_document *)doc +{ + self = [super initWithFrame: CGRectMake(0,0,100,100)]; + if (self) { + [self setOpaque: NO]; + + color = [[UIColor colorWithRed: 0x25/255.0 green: 0x72/255.0 blue: 0xAC/255.0 alpha: 0.5] retain]; + + pageSize = CGSizeMake(100,100); + + for (int i = 0; i < n && i < nelem(hitRects); i++) { + fz_rect bbox = search_result_bbox(doc, i); // this is thread-safe enough + hitRects[i].origin.x = bbox.x0; + hitRects[i].origin.y = bbox.y0; + hitRects[i].size.width = bbox.x1 - bbox.x0; + hitRects[i].size.height = bbox.y1 - bbox.y0; + } + hitCount = n; + } + return self; +} + +- (id) initWithLinks: (fz_link*)link forDocument: (fz_document *)doc +{ + self = [super initWithFrame: CGRectMake(0,0,100,100)]; + if (self) { + [self setOpaque: NO]; + + color = [[UIColor colorWithRed: 0xAC/255.0 green: 0x72/255.0 blue: 0x25/255.0 alpha: 0.5] retain]; + + pageSize = CGSizeMake(100,100); + + while (link && hitCount < nelem(hitRects)) { + if (link->dest.kind == FZ_LINK_GOTO || link->dest.kind == FZ_LINK_URI) { + fz_rect bbox = link->rect; + hitRects[hitCount].origin.x = bbox.x0; + hitRects[hitCount].origin.y = bbox.y0; + hitRects[hitCount].size.width = bbox.x1 - bbox.x0; + hitRects[hitCount].size.height = bbox.y1 - bbox.y0; + linkPage[hitCount] = link->dest.kind == FZ_LINK_GOTO ? link->dest.ld.gotor.page : -1; + linkUrl[hitCount] = link->dest.kind == FZ_LINK_URI ? strdup(link->dest.ld.uri.uri) : nil; + hitCount++; + } + link = link->next; + } + } + return self; +} + +- (void) setPageSize: (CGSize)s +{ + pageSize = s; + // if page takes a long time to load we may have drawn at the initial (wrong) size + [self setNeedsDisplay]; +} + +- (void) drawRect: (CGRect)r +{ + CGSize scale = fitPageToScreen(pageSize, self.bounds.size); + + [color set]; + + for (int i = 0; i < hitCount; i++) { + CGRect rect = hitRects[i]; + rect.origin.x *= scale.width; + rect.origin.y *= scale.height; + rect.size.width *= scale.width; + rect.size.height *= scale.height; + UIRectFill(rect); + } +} + +- (void) dealloc +{ + int i; + [color release]; + for (i = 0; i < hitCount; i++) + free(linkUrl[i]); + [super dealloc]; +} + +@end diff --git a/platform/ios/Classes/MuLibraryController.h b/platform/ios/Classes/MuLibraryController.h new file mode 100644 index 00000000..2bb7ff71 --- /dev/null +++ b/platform/ios/Classes/MuLibraryController.h @@ -0,0 +1,29 @@ +// +// MuLibraryController.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +#undef ABS +#undef MIN +#undef MAX + +#include "mupdf/fitz.h" + +@interface MuLibraryController : UITableViewController <UIActionSheetDelegate> +{ + NSArray *files; + NSTimer *timer; + fz_document *_doc; // temporaries for juggling password dialog + NSString *_filename; +} +- (void) openDocument: (NSString*)filename; +- (void) askForPassword: (NSString*)prompt; +- (void) onPasswordOkay; +- (void) onPasswordCancel; +- (void) reload; +@end + diff --git a/platform/ios/Classes/MuLibraryController.m b/platform/ios/Classes/MuLibraryController.m new file mode 100644 index 00000000..cc5a1ddd --- /dev/null +++ b/platform/ios/Classes/MuLibraryController.m @@ -0,0 +1,225 @@ +// +// MuLibraryController.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#include "common.h" +#import "MuDocumentController.h" +#import "MuLibraryController.h" + +static void showAlert(NSString *msg, NSString *filename) +{ + UIAlertView *alert = [[UIAlertView alloc] + initWithTitle: msg + message: filename + delegate: nil + cancelButtonTitle: @"Okay" + otherButtonTitles: nil]; + [alert show]; + [alert release]; +} + +@implementation MuLibraryController + +- (void) viewWillAppear: (BOOL)animated +{ + [self setTitle: @"PDF, XPS and CBZ Documents"]; + [self reload]; + printf("library viewWillAppear (starting reload timer)\n"); + timer = [NSTimer timerWithTimeInterval: 3 + target: self selector: @selector(reload) userInfo: nil + repeats: YES]; + [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode]; +} + +- (void) viewWillDisappear: (BOOL)animated +{ + printf("library viewWillDisappear (stopping reload timer)\n"); + [timer invalidate]; + timer = nil; +} + +- (void) reload +{ + if (files) { + [files release]; + files = nil; + } + + NSFileManager *fileman = [NSFileManager defaultManager]; + NSString *docdir = [NSString stringWithFormat: @"%@/Documents", NSHomeDirectory()]; + NSMutableArray *outfiles = [[NSMutableArray alloc] init]; + NSDirectoryEnumerator *direnum = [fileman enumeratorAtPath:docdir]; + NSString *file; + BOOL isdir; + while (file = [direnum nextObject]) { + NSString *filepath = [docdir stringByAppendingPathComponent:file]; + NSLog(@"file %@\n", file); + if ([fileman fileExistsAtPath:filepath isDirectory:&isdir] && !isdir) { + [outfiles addObject:file]; + } + } + + files = outfiles; + + [[self tableView] reloadData]; +} + +- (void) dealloc +{ + [files release]; + [super dealloc]; +} + +- (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} + +- (NSInteger) numberOfSectionsInTableView: (UITableView*)tableView +{ + return 1; +} + +- (NSInteger) tableView: (UITableView*)tableView numberOfRowsInSection: (NSInteger)section +{ + return [files count]; +} + +- (void) actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex +{ + if (buttonIndex == [actionSheet destructiveButtonIndex]) + { + char filename[PATH_MAX]; + int row = [actionSheet tag]; + + dispatch_sync(queue, ^{}); + + strcpy(filename, [NSHomeDirectory() UTF8String]); + strcat(filename, "/Documents/"); + strcat(filename, [[files objectAtIndex: row - 1] UTF8String]); + + printf("delete document '%s'\n", filename); + + unlink(filename); + + [self reload]; + } +} + +- (void) onTapDelete: (UIControl*)sender +{ + int row = [sender tag]; + NSString *title = [NSString stringWithFormat: @"Delete %@?", [files objectAtIndex: row - 1]]; + UIActionSheet *sheet = [[UIActionSheet alloc] + initWithTitle: title + delegate: self + cancelButtonTitle: @"Cancel" + destructiveButtonTitle: @"Delete" + otherButtonTitles: nil]; + [sheet setTag: row]; + [sheet showInView: [self tableView]]; + [sheet release]; +} + +- (UITableViewCell*) tableView: (UITableView*)tableView cellForRowAtIndexPath: (NSIndexPath*)indexPath +{ + static NSString *cellid = @"MuCellIdent"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellid]; + if (!cell) + cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleDefault reuseIdentifier: cellid] autorelease]; + int row = [indexPath row]; + [[cell textLabel] setText: [files objectAtIndex: row]]; + [[cell textLabel] setFont: [UIFont systemFontOfSize: 20]]; + + UIButton *deleteButton = [UIButton buttonWithType:UIButtonTypeCustom]; + [deleteButton setImage: [UIImage imageNamed: @"x_alt_blue.png"] forState: UIControlStateNormal]; + [deleteButton setFrame: CGRectMake(0, 0, 35, 35)]; + [deleteButton addTarget: self action: @selector(onTapDelete:) forControlEvents: UIControlEventTouchUpInside]; + [deleteButton setTag: row]; + [cell setAccessoryView: deleteButton]; + + return cell; +} + +- (void) tableView: (UITableView*)tableView didSelectRowAtIndexPath: (NSIndexPath*)indexPath +{ + int row = [indexPath row]; + [self openDocument: [files objectAtIndex: row]]; +} + +- (void) openDocument: (NSString*)nsfilename +{ + char filename[PATH_MAX]; + + dispatch_sync(queue, ^{}); + + strcpy(filename, [NSHomeDirectory() UTF8String]); + strcat(filename, "/Documents/"); + strcat(filename, [nsfilename UTF8String]); + + printf("open document '%s'\n", filename); + + _filename = [nsfilename retain]; + _doc = fz_open_document(ctx, filename); + if (!_doc) { + showAlert(@"Cannot open document", nsfilename); + return; + } + + if (fz_needs_password(_doc)) + [self askForPassword: @"'%@' needs a password:"]; + else + [self onPasswordOkay]; +} + +- (void) askForPassword: (NSString*)prompt +{ + UIAlertView *passwordAlertView = [[UIAlertView alloc] + initWithTitle: @"Password Protected" + message: [NSString stringWithFormat: prompt, [_filename lastPathComponent]] + delegate: self + cancelButtonTitle: @"Cancel" + otherButtonTitles: @"Done", nil]; + [passwordAlertView setAlertViewStyle: UIAlertViewStyleSecureTextInput]; + [passwordAlertView show]; + [passwordAlertView release]; +} + +- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex +{ + char *password = (char*) [[[alertView textFieldAtIndex: 0] text] UTF8String]; + [alertView dismissWithClickedButtonIndex: buttonIndex animated: TRUE]; + if (buttonIndex == 1) { + if (fz_authenticate_password(_doc, password)) + [self onPasswordOkay]; + else + [self askForPassword: @"Wrong password for '%@'. Try again:"]; + } else { + [self onPasswordCancel]; + } +} + +- (void) onPasswordOkay +{ + MuDocumentController *document = [[MuDocumentController alloc] initWithFilename: _filename document: _doc]; + if (document) { + [self setTitle: @"Library"]; + [[self navigationController] pushViewController: document animated: YES]; + [document release]; + } + [_filename release]; + _doc = NULL; +} + +- (void) onPasswordCancel +{ + [_filename release]; + printf("close document (password cancel)\n"); + fz_close_document(_doc); + _doc = NULL; +} + +@end diff --git a/platform/ios/Classes/MuOutlineController.h b/platform/ios/Classes/MuOutlineController.h new file mode 100644 index 00000000..3af69848 --- /dev/null +++ b/platform/ios/Classes/MuOutlineController.h @@ -0,0 +1,17 @@ +// +// MuOutlineController.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +@interface MuOutlineController : UITableViewController +{ + id target; + NSMutableArray *titles; + NSMutableArray *pages; +} +- (id) initWithTarget: (id)aTarget titles: (NSMutableArray*)aTitles pages: (NSMutableArray*)aPages; +@end diff --git a/platform/ios/Classes/MuOutlineController.m b/platform/ios/Classes/MuOutlineController.m new file mode 100644 index 00000000..694fcef3 --- /dev/null +++ b/platform/ios/Classes/MuOutlineController.m @@ -0,0 +1,76 @@ +// +// MuOutlineController.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import "MuOutlineController.h" + +@implementation MuOutlineController + +- (id) initWithTarget: (id)aTarget titles: (NSMutableArray*)aTitles pages: (NSMutableArray*)aPages +{ + self = [super initWithStyle: UITableViewStylePlain]; + if (self) { + [self setTitle: @"Table of Contents"]; + target = aTarget; // only keep a weak reference, to avoid retain cycles + titles = [aTitles retain]; + pages = [aPages retain]; + [[self tableView] setSeparatorStyle: UITableViewCellSeparatorStyleNone]; + } + return self; +} + +- (void) dealloc +{ + [titles release]; + [pages release]; + [super dealloc]; +} + +- (BOOL) shouldAutorotateToInterfaceOrientation: (UIInterfaceOrientation)o +{ + return YES; +} + +- (NSInteger) numberOfSectionsInTableView: (UITableView*)tableView +{ + return 1; +} + +- (NSInteger) tableView: (UITableView*)tableView numberOfRowsInSection: (NSInteger)section +{ + return [titles count]; +} + +- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath +{ + return 28; +} + +- (UITableViewCell*) tableView: (UITableView*)tableView cellForRowAtIndexPath: (NSIndexPath*)indexPath +{ + static NSString *cellid = @"MuCellIdent"; + UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellid]; + if (!cell) + { + cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleValue1 reuseIdentifier: cellid] autorelease]; + [[cell textLabel] setFont: [UIFont systemFontOfSize: 16]]; + [[cell detailTextLabel] setFont: [UIFont systemFontOfSize: 16]]; + } + NSString *title = [titles objectAtIndex: [indexPath row]]; + NSString *page = [pages objectAtIndex: [indexPath row]]; + [[cell textLabel] setText: title]; + [[cell detailTextLabel] setText: [NSString stringWithFormat: @"%d", [page intValue]+1]]; + return cell; +} + +- (void) tableView: (UITableView*)tableView didSelectRowAtIndexPath: (NSIndexPath*)indexPath +{ + NSNumber *page = [pages objectAtIndex: [indexPath row]]; + [target gotoPage: [page intValue] animated: NO]; + [[self navigationController] popViewControllerAnimated: YES]; +} + +@end diff --git a/platform/ios/Classes/MuPageView.h b/platform/ios/Classes/MuPageView.h new file mode 100644 index 00000000..ab141ea8 --- /dev/null +++ b/platform/ios/Classes/MuPageView.h @@ -0,0 +1,45 @@ +// +// MuPageView.h +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#import <UIKit/UIKit.h> + +#undef ABS +#undef MIN +#undef MAX + +#include "mupdf/fitz.h" + +#include "MuHitView.h" + +@interface MuPageView : UIScrollView <UIScrollViewDelegate> +{ + fz_document *doc; + fz_page *page; + int number; + UIActivityIndicatorView *loadingView; + UIImageView *imageView; + UIImageView *tileView; + MuHitView *hitView; + MuHitView *linkView; + CGSize pageSize; + CGRect tileFrame; + float tileScale; + BOOL cancel; +} +- (id) initWithFrame: (CGRect)frame document: (fz_document*)aDoc page: (int)aNumber; +- (void) displayImage: (UIImage*)image; +- (void) resizeImage; +- (void) loadPage; +- (void) loadTile; +- (void) willRotate; +- (void) resetZoomAnimated: (BOOL)animated; +- (void) showSearchResults: (int)count; +- (void) clearSearchResults; +- (void) showLinks; +- (void) hideLinks; +- (int) number; +@end diff --git a/platform/ios/Classes/MuPageView.m b/platform/ios/Classes/MuPageView.m new file mode 100644 index 00000000..53fe79b9 --- /dev/null +++ b/platform/ios/Classes/MuPageView.m @@ -0,0 +1,462 @@ +// +// MuPageView.m +// MuPDF +// +// Copyright (c) 2013 Artifex Software, Inc. All rights reserved. +// + +#include "common.h" + +static CGSize measurePage(fz_document *doc, fz_page *page) +{ + CGSize pageSize; + fz_rect bounds; + fz_bound_page(doc, page, &bounds); + pageSize.width = bounds.x1 - bounds.x0; + pageSize.height = bounds.y1 - bounds.y0; + return pageSize; +} + +static void releasePixmap(void *info, const void *data, size_t size) +{ + if (queue) + dispatch_async(queue, ^{ + fz_drop_pixmap(ctx, info); + }); + else + fz_drop_pixmap(ctx, info); +} + +static UIImage *newImageWithPixmap(fz_pixmap *pix) +{ + unsigned char *samples = fz_pixmap_samples(ctx, pix); + int w = fz_pixmap_width(ctx, pix); + int h = fz_pixmap_height(ctx, pix); + CGDataProviderRef cgdata = CGDataProviderCreateWithData(pix, samples, w * 4 * h, releasePixmap); + CGColorSpaceRef cgcolor = CGColorSpaceCreateDeviceRGB(); + CGImageRef cgimage = CGImageCreate(w, h, 8, 32, 4 * w, + cgcolor, kCGBitmapByteOrderDefault, + cgdata, NULL, NO, kCGRenderingIntentDefault); + UIImage *image = [[UIImage alloc] + initWithCGImage: cgimage + scale: screenScale + orientation: UIImageOrientationUp]; + CGDataProviderRelease(cgdata); + CGColorSpaceRelease(cgcolor); + CGImageRelease(cgimage); + return image; +} + +static UIImage *renderPage(fz_document *doc, fz_page *page, CGSize screenSize) +{ + CGSize pageSize; + fz_irect bbox; + fz_matrix ctm; + fz_device *dev; + fz_pixmap *pix; + CGSize scale; + + screenSize.width *= screenScale; + screenSize.height *= screenScale; + + pageSize = measurePage(doc, page); + scale = fitPageToScreen(pageSize, screenSize); + fz_scale(&ctm, scale.width, scale.height); + bbox = (fz_irect){0, 0, pageSize.width * scale.width, pageSize.height * scale.height}; + + pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), &bbox); + fz_clear_pixmap_with_value(ctx, pix, 255); + + dev = fz_new_draw_device(ctx, pix); + fz_run_page(doc, page, dev, &ctm, NULL); + fz_free_device(dev); + + return newImageWithPixmap(pix); +} + +static UIImage *renderTile(fz_document *doc, fz_page *page, CGSize screenSize, CGRect tileRect, float zoom) +{ + CGSize pageSize; + fz_irect bbox; + fz_matrix ctm; + fz_device *dev; + fz_pixmap *pix; + CGSize scale; + + screenSize.width *= screenScale; + screenSize.height *= screenScale; + tileRect.origin.x *= screenScale; + tileRect.origin.y *= screenScale; + tileRect.size.width *= screenScale; + tileRect.size.height *= screenScale; + + pageSize = measurePage(doc, page); + scale = fitPageToScreen(pageSize, screenSize); + fz_scale(&ctm, scale.width * zoom, scale.height * zoom); + + bbox.x0 = tileRect.origin.x; + bbox.y0 = tileRect.origin.y; + bbox.x1 = tileRect.origin.x + tileRect.size.width; + bbox.y1 = tileRect.origin.y + tileRect.size.height; + + pix = fz_new_pixmap_with_bbox(ctx, fz_device_rgb(ctx), &bbox); + fz_clear_pixmap_with_value(ctx, pix, 255); + + dev = fz_new_draw_device(ctx, pix); + fz_run_page(doc, page, dev, &ctm, NULL); + fz_free_device(dev); + + return newImageWithPixmap(pix); +} + +#import "MuPageView.h" + +@implementation MuPageView + +- (id) initWithFrame: (CGRect)frame document: (fz_document*)aDoc page: (int)aNumber +{ + self = [super initWithFrame: frame]; + if (self) { + doc = aDoc; + number = aNumber; + cancel = NO; + + [self setShowsVerticalScrollIndicator: NO]; + [self setShowsHorizontalScrollIndicator: NO]; + [self setDecelerationRate: UIScrollViewDecelerationRateFast]; + [self setDelegate: self]; + + // zoomDidFinish/Begin events fire before bounce animation completes, + // making a mess when we rearrange views during the animation. + [self setBouncesZoom: NO]; + + [self resetZoomAnimated: NO]; + + // TODO: use a one shot timer to delay the display of this? + loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + [loadingView startAnimating]; + [self addSubview: loadingView]; + + [self loadPage]; + } + return self; +} + +- (void) dealloc +{ + // dealloc can trigger in background thread when the queued block is + // our last owner, and releases us on completion. + // Send the dealloc back to the main thread so we don't mess up UIKit. + if (dispatch_get_current_queue() != dispatch_get_main_queue()) { + __block id block_self = self; // don't auto-retain self! + dispatch_async(dispatch_get_main_queue(), ^{ [block_self dealloc]; }); + } else { + __block fz_page *block_page = page; + __block fz_document *block_doc = doc; + dispatch_async(queue, ^{ + if (block_page) + fz_free_page(block_doc, block_page); + block_page = nil; + }); + [linkView release]; + [hitView release]; + [tileView release]; + [loadingView release]; + [imageView release]; + [super dealloc]; + } +} + +- (int) number +{ + return number; +} + +- (void) showLinks +{ + if (!linkView) { + dispatch_async(queue, ^{ + if (!page) + page = fz_load_page(doc, number); + fz_link *links = fz_load_links(doc, page); + dispatch_async(dispatch_get_main_queue(), ^{ + linkView = [[MuHitView alloc] initWithLinks: links forDocument: doc]; + dispatch_async(queue, ^{ + fz_drop_link(ctx, links); + }); + if (imageView) { + [linkView setFrame: [imageView frame]]; + [linkView setPageSize: pageSize]; + } + [self addSubview: linkView]; + }); + }); + } +} + +- (void) hideLinks +{ + [linkView removeFromSuperview]; + [linkView release]; + linkView = nil; +} + +- (void) showSearchResults: (int)count +{ + if (hitView) { + [hitView removeFromSuperview]; + [hitView release]; + hitView = nil; + } + hitView = [[MuHitView alloc] initWithSearchResults: count forDocument: doc]; + if (imageView) { + [hitView setFrame: [imageView frame]]; + [hitView setPageSize: pageSize]; + } + [self addSubview: hitView]; +} + +- (void) clearSearchResults +{ + if (hitView) { + [hitView removeFromSuperview]; + [hitView release]; + hitView = nil; + } +} + +- (void) resetZoomAnimated: (BOOL)animated +{ + // discard tile and any pending tile jobs + tileFrame = CGRectZero; + tileScale = 1; + if (tileView) { + [tileView removeFromSuperview]; + [tileView release]; + tileView = nil; + } + + [self setMinimumZoomScale: 1]; + [self setMaximumZoomScale: 5]; + [self setZoomScale: 1 animated: animated]; +} + +- (void) removeFromSuperview +{ + cancel = YES; + [super removeFromSuperview]; +} + +- (void) loadPage +{ + if (number < 0 || number >= fz_count_pages(doc)) + return; + dispatch_async(queue, ^{ + if (!cancel) { + printf("render page %d\n", number); + if (!page) + page = fz_load_page(doc, number); + CGSize size = measurePage(doc, page); + UIImage *image = renderPage(doc, page, self.bounds.size); + dispatch_async(dispatch_get_main_queue(), ^{ + pageSize = size; + [self displayImage: image]; + [image release]; + }); + } else { + printf("cancel page %d\n", number); + } + }); +} + +- (void) displayImage: (UIImage*)image +{ + if (loadingView) { + [loadingView removeFromSuperview]; + [loadingView release]; + loadingView = nil; + } + + if (hitView) + [hitView setPageSize: pageSize]; + + if (!imageView) { + imageView = [[UIImageView alloc] initWithImage: image]; + imageView.opaque = YES; + [self addSubview: imageView]; + if (hitView) + [self bringSubviewToFront: hitView]; + } else { + [imageView setImage: image]; + } + + [self resizeImage]; +} + +- (void) resizeImage +{ + if (imageView) { + CGSize imageSize = imageView.image.size; + CGSize scale = fitPageToScreen(imageSize, self.bounds.size); + if (fabs(scale.width - 1) > 0.1) { + CGRect frame = [imageView frame]; + frame.size.width = imageSize.width * scale.width; + frame.size.height = imageSize.height * scale.height; + [imageView setFrame: frame]; + + printf("resized view; queuing up a reload (%d)\n", number); + dispatch_async(queue, ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + CGSize scale = fitPageToScreen(imageView.image.size, self.bounds.size); + if (fabs(scale.width - 1) > 0.01) + [self loadPage]; + }); + }); + } else { + [imageView sizeToFit]; + } + + [self setContentSize: imageView.frame.size]; + + [self layoutIfNeeded]; + } + +} + +- (void) willRotate +{ + if (imageView) { + [self resetZoomAnimated: NO]; + [self resizeImage]; + } +} + +- (void) layoutSubviews +{ + [super layoutSubviews]; + + // center the image as it becomes smaller than the size of the screen + + CGSize boundsSize = self.bounds.size; + CGRect frameToCenter = loadingView ? loadingView.frame : imageView.frame; + + // center horizontally + if (frameToCenter.size.width < boundsSize.width) + frameToCenter.origin.x = floor((boundsSize.width - frameToCenter.size.width) / 2); + else + frameToCenter.origin.x = 0; + + // center vertically + if (frameToCenter.size.height < boundsSize.height) + frameToCenter.origin.y = floor((boundsSize.height - frameToCenter.size.height) / 2); + else + frameToCenter.origin.y = 0; + + if (loadingView) + loadingView.frame = frameToCenter; + else + imageView.frame = frameToCenter; + + if (hitView && imageView) + [hitView setFrame: [imageView frame]]; +} + +- (UIView*) viewForZoomingInScrollView: (UIScrollView*)scrollView +{ + return imageView; +} + +- (void) loadTile +{ + CGSize screenSize = self.bounds.size; + + tileFrame.origin = self.contentOffset; + tileFrame.size = self.bounds.size; + tileFrame = CGRectIntersection(tileFrame, imageView.frame); + tileScale = self.zoomScale; + + CGRect frame = tileFrame; + float scale = tileScale; + + CGRect viewFrame = frame; + if (self.contentOffset.x < imageView.frame.origin.x) + viewFrame.origin.x = 0; + if (self.contentOffset.y < imageView.frame.origin.y) + viewFrame.origin.y = 0; + + if (scale < 1.01) + return; + + dispatch_async(queue, ^{ + __block BOOL isValid; + dispatch_sync(dispatch_get_main_queue(), ^{ + isValid = CGRectEqualToRect(frame, tileFrame) && scale == tileScale; + }); + if (!isValid) { + printf("cancel tile\n"); + return; + } + + if (!page) + page = fz_load_page(doc, number); + + printf("render tile\n"); + UIImage *image = renderTile(doc, page, screenSize, viewFrame, scale); + + dispatch_async(dispatch_get_main_queue(), ^{ + isValid = CGRectEqualToRect(frame, tileFrame) && scale == tileScale; + if (isValid) { + tileFrame = CGRectZero; + tileScale = 1; + if (tileView) { + [tileView removeFromSuperview]; + [tileView release]; + tileView = nil; + } + + tileView = [[UIImageView alloc] initWithFrame: frame]; + [tileView setImage: image]; + [self addSubview: tileView]; + if (hitView) + [self bringSubviewToFront: hitView]; + } else { + printf("discard tile\n"); + } + [image release]; + }); + }); +} + +- (void) scrollViewDidScrollToTop:(UIScrollView *)scrollView { [self loadTile]; } +- (void) scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { [self loadTile]; } +- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self loadTile]; } +- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate +{ + if (!decelerate) + [self loadTile]; +} + +- (void) scrollViewWillBeginZooming: (UIScrollView*)scrollView withView: (UIView*)view +{ + // discard tile and any pending tile jobs + tileFrame = CGRectZero; + tileScale = 1; + if (tileView) { + [tileView removeFromSuperview]; + [tileView release]; + tileView = nil; + } +} + +- (void) scrollViewDidEndZooming: (UIScrollView*)scrollView withView: (UIView*)view atScale: (float)scale +{ + [self loadTile]; +} + +- (void) scrollViewDidZoom: (UIScrollView*)scrollView +{ + if (hitView && imageView) + [hitView setFrame: [imageView frame]]; +} + +@end |