/* * Copyright 2011 Sven Weidauer * * This file is part of NetSurf, http://www.netsurf-browser.org/ * * NetSurf is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * NetSurf is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #import "utils/nsoption.h" #import "utils/messages.h" #import "utils/nsurl.h" #import "utils/utils.h" #import "netsurf/browser_window.h" #import "netsurf/plotters.h" #import "netsurf/content.h" #import "netsurf/keypress.h" #import "cocoa/gui.h" #import "cocoa/BrowserView.h" #import "cocoa/HistoryView.h" #import "cocoa/font.h" #import "cocoa/coordinates.h" #import "cocoa/plotter.h" #import "cocoa/LocalHistoryController.h" #import "cocoa/BrowserWindowController.h" @interface BrowserView () @property (readwrite, copy, nonatomic) NSString *markedText; @property (readwrite, nonatomic) NSRect caretRect; - (void)scrollHorizontal:(CGFloat)amount; - (void)scrollVertical:(CGFloat)amount; - (CGFloat)pageScroll; - (void)popUpContextMenuForEvent:(NSEvent *)event; - (IBAction)cmOpenURLInTab:(id)sender; - (IBAction)cmOpenURLInWindow:(id)sender; - (IBAction)cmDownloadURL:(id)sender; - (IBAction)cmLinkCopy:(id)sender; - (IBAction)cmImageCopy:(id)sender; @end @implementation BrowserView @synthesize browser; @synthesize caretTimer; @synthesize markedText; static const CGFloat CaretWidth = 1.0; static const NSTimeInterval CaretBlinkTime = 0.8; - (instancetype)initWithFrame:(NSRect)frame { if ((self = [super initWithFrame:frame]) == nil) { return nil; } [self registerForDraggedTypes:[NSArray arrayWithObjects:NSURLPboardType, @"public.url", nil]]; return self; } - (void)dealloc { [self setCaretTimer:nil]; [self setMarkedText:nil]; } - (void)setCaretTimer:(NSTimer *)newTimer { if (newTimer != caretTimer) { [caretTimer invalidate]; caretTimer = newTimer; } } - (void)updateHistory { [history redraw]; } - (void)removeCaret { hasCaret = NO; [self setNeedsDisplayInRect: self.caretRect]; [self setCaretTimer:nil]; } - (void)addCaretAtX: (int)caretX Y: (int)caretY height: (int)caretHeight { if (hasCaret) { [self setNeedsDisplayInRect:self.caretRect]; } self.caretRect = cocoa_rect_wh(caretX, caretY, CaretWidth, caretHeight); hasCaret = YES; caretVisible = YES; if (nil == caretTimer) { [self setCaretTimer:[NSTimer scheduledTimerWithTimeInterval:CaretBlinkTime target:self selector:@selector(caretBlink:) userInfo:nil repeats:YES]]; } else { [caretTimer setFireDate:[NSDate dateWithTimeIntervalSinceNow:CaretBlinkTime]]; } [self setNeedsDisplayInRect:self.caretRect]; } - (void)caretBlink:(NSTimer *)timer { if (hasCaret) { caretVisible = !caretVisible; [self setNeedsDisplayInRect: self.caretRect]; } } - (void)drawRect:(NSRect)dirtyRect { @autoreleasepool { struct redraw_context ctx = { .interactive = true, .background_images = true, .plot = &cocoa_plotters }; const NSRect *rects = NULL; NSInteger count = 0; [self getRectsBeingDrawn:&rects count:&count]; for (NSInteger i = 0; i < count; i++) { const struct rect clip = { .x0 = cocoa_pt_to_px(NSMinX(rects[i])), .y0 = cocoa_pt_to_px(NSMinY(rects[i])), .x1 = cocoa_pt_to_px(NSMaxX(rects[i])), .y1 = cocoa_pt_to_px(NSMaxY(rects[i])) }; browser_window_redraw(browser, 0, 0, &clip, &ctx); } if (hasCaret && caretVisible && [self needsToDrawRect:self.caretRect]) { [[NSColor blackColor] set]; [NSBezierPath fillRect: self.caretRect]; } } } - (BOOL)isFlipped { return YES; } - (void)viewDidMoveToWindow { NSTrackingArea *area = [[NSTrackingArea alloc] initWithRect:[self visibleRect] options:NSTrackingMouseMoved | NSTrackingMouseEnteredAndExited | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect owner:self userInfo:nil]; [self addTrackingArea:area]; } static browser_mouse_state cocoa_mouse_flags_for_event(NSEvent *evt) { browser_mouse_state result = 0; NSUInteger flags = [evt modifierFlags]; if (flags & NSEventModifierFlagShift) result |= BROWSER_MOUSE_MOD_1; if (flags & NSEventModifierFlagOption) result |= BROWSER_MOUSE_MOD_2; return result; } - (NSPoint)convertMousePoint:(NSEvent *)event { NSPoint location = [self convertPoint:[event locationInWindow] fromView:nil]; float bscale = browser_window_get_scale(browser); location.x /= bscale; location.y /= bscale; location.x = cocoa_pt_to_px(location.x); location.y = cocoa_pt_to_px(location.y); return location; } - (void)mouseDown:(NSEvent *)theEvent { if ([theEvent modifierFlags] & NSEventModifierFlagControl) { [self popUpContextMenuForEvent:theEvent]; return; } dragStart = [self convertMousePoint:theEvent]; browser_window_mouse_click(browser, BROWSER_MOUSE_PRESS_1 | cocoa_mouse_flags_for_event(theEvent), dragStart.x, dragStart.y); } - (void)rightMouseDown:(NSEvent *)theEvent { [self popUpContextMenuForEvent:theEvent]; } - (void)mouseUp:(NSEvent *)theEvent { if (historyVisible) { [self setHistoryVisible:NO]; return; } NSPoint location = [self convertMousePoint:theEvent]; browser_mouse_state modifierFlags = cocoa_mouse_flags_for_event(theEvent); if (isDragging) { isDragging = NO; browser_window_mouse_track(browser, (browser_mouse_state)0, location.x, location.y); } else { modifierFlags |= BROWSER_MOUSE_CLICK_1; if ([theEvent clickCount] == 2) modifierFlags |= BROWSER_MOUSE_DOUBLE_CLICK; browser_window_mouse_click(browser, modifierFlags, location.x, location.y); } } #define squared(x) ((x) * (x)) #define MinDragDistance (5.0) - (void)mouseDragged:(NSEvent *)theEvent { NSPoint location = [self convertMousePoint:theEvent]; browser_mouse_state modifierFlags = cocoa_mouse_flags_for_event(theEvent); if (!isDragging) { const CGFloat distance = squared(dragStart.x - location.x) + squared(dragStart.y - location.y); if (distance >= squared(MinDragDistance)) { isDragging = YES; browser_window_mouse_click(browser, BROWSER_MOUSE_DRAG_1 | modifierFlags, dragStart.x, dragStart.y); } } if (isDragging) { browser_window_mouse_track(browser, BROWSER_MOUSE_HOLDING_1 | BROWSER_MOUSE_DRAG_ON | modifierFlags, location.x, location.y); } } - (void)mouseMoved:(NSEvent *)theEvent { if (historyVisible) return; NSPoint location = [self convertMousePoint:theEvent]; browser_window_mouse_track(browser, cocoa_mouse_flags_for_event(theEvent), location.x, location.y); } - (void)mouseExited:(NSEvent *)theEvent { [[NSCursor arrowCursor] set]; } - (void)keyDown:(NSEvent *)theEvent { if (!historyVisible) { [self interpretKeyEvents:[NSArray arrayWithObject:theEvent]]; } else { [history keyDown:theEvent]; } } - (void)insertText:(id)string { for (NSUInteger i = 0, length = [string length]; i < length; i++) { unichar ch = [string characterAtIndex:i]; if (!browser_window_key_press(browser, ch)) { if (ch == ' ') [self scrollPageDown:self]; break; } } [self setMarkedText:nil]; } - (void)moveLeft:(id)sender { if (browser_window_key_press(browser, NS_KEY_LEFT)) return; [self scrollHorizontal:-[[self enclosingScrollView] horizontalLineScroll]]; } - (void)moveRight:(id)sender { if (browser_window_key_press(browser, NS_KEY_RIGHT)) return; [self scrollHorizontal:[[self enclosingScrollView] horizontalLineScroll]]; } - (void)moveUp:(id)sender { if (browser_window_key_press(browser, NS_KEY_UP)) return; [self scrollVertical:-[[self enclosingScrollView] lineScroll]]; } - (void)moveDown:(id)sender { if (browser_window_key_press(browser, NS_KEY_DOWN)) return; [self scrollVertical:[[self enclosingScrollView] lineScroll]]; } - (void)deleteBackward:(id)sender { if (!browser_window_key_press(browser, NS_KEY_DELETE_LEFT)) { [NSApp sendAction:@selector(goBack:) to:nil from:self]; } } - (void)deleteForward:(id)sender { browser_window_key_press(browser, NS_KEY_DELETE_RIGHT); } - (void)cancelOperation:(id)sender { browser_window_key_press(browser, NS_KEY_ESCAPE); } - (void)scrollPageUp:(id)sender { if (browser_window_key_press(browser, NS_KEY_PAGE_UP)) { return; } [self scrollVertical:-[self pageScroll]]; } - (void)scrollPageDown:(id)sender { if (browser_window_key_press(browser, NS_KEY_PAGE_DOWN)) { return; } [self scrollVertical:[self pageScroll]]; } - (void)insertTab:(id)sender { browser_window_key_press(browser, NS_KEY_TAB); } - (void)insertBacktab:(id)sender { browser_window_key_press(browser, NS_KEY_SHIFT_TAB); } - (void)moveToBeginningOfLine:(id)sender { browser_window_key_press(browser, NS_KEY_LINE_START); } - (void)moveToEndOfLine:(id)sender { browser_window_key_press(browser, NS_KEY_LINE_END); } - (void)moveToBeginningOfDocument:(id)sender { if (browser_window_key_press(browser, NS_KEY_TEXT_START)) return; } - (void)scrollToBeginningOfDocument:(id)sender { NSPoint origin = [self visibleRect].origin; origin.y = 0; [self scrollPoint:origin]; } - (void)moveToEndOfDocument:(id)sender { browser_window_key_press(browser, NS_KEY_TEXT_END); } - (void)scrollToEndOfDocument:(id)sender { NSPoint origin = [self visibleRect].origin; origin.y = NSHeight([self frame]); [self scrollPoint:origin]; } - (void)insertNewline:(id)sender { browser_window_key_press(browser, NS_KEY_NL); } - (void)selectAll:(id)sender { browser_window_key_press(browser, NS_KEY_SELECT_ALL); } - (void)copy:(id)sender { browser_window_key_press(browser, NS_KEY_COPY_SELECTION); } - (void)cut:(id)sender { browser_window_key_press(browser, NS_KEY_CUT_SELECTION); } - (void)paste:(id)sender { browser_window_key_press(browser, NS_KEY_PASTE); } - (BOOL)acceptsFirstResponder { return YES; } - (void)adjustFrame { browser_window_schedule_reformat(browser); [super adjustFrame]; } - (BOOL)isHistoryVisible { return historyVisible; } - (void)setHistoryVisible:(BOOL)newVisible { if (newVisible == historyVisible) return; historyVisible = newVisible; if (historyVisible) { if (nil == history) { history = [[LocalHistoryController alloc] initWithBrowser:self]; } [history attachToView:[(BrowserWindowController *)[[self window] windowController] historyButton]]; } else { [history detach]; } } - (void)scrollHorizontal:(CGFloat)amount { NSPoint currentPoint = [self visibleRect].origin; currentPoint.x += amount; [self scrollPoint:currentPoint]; } - (void)scrollVertical:(CGFloat)amount { NSPoint currentPoint = [self visibleRect].origin; currentPoint.y += amount; [self scrollPoint:currentPoint]; } - (CGFloat)pageScroll { return NSHeight([[self superview] frame]) - [[self enclosingScrollView] pageScroll]; } - (void)popUpContextMenuForEvent:(NSEvent *)event { NSMenu *popupMenu = [[NSMenu alloc] initWithTitle:@""]; NSPoint point = [self convertMousePoint:event]; struct browser_window_features cont; browser_window_get_features(browser, point.x, point.y, &cont); if (cont.object != NULL) { NSString *imageURL = [NSString stringWithUTF8String:nsurl_access(hlcache_handle_get_url(cont.object))]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Open image in new tab", @"Context menu") action:@selector(cmOpenURLInTab:) keyEquivalent:@""] setRepresentedObject:imageURL]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Open image in new window", @"Context menu") action:@selector(cmOpenURLInWindow:) keyEquivalent:@""] setRepresentedObject:imageURL]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Save image as", @"Context menu") action:@selector(cmDownloadURL:) keyEquivalent:@""] setRepresentedObject:imageURL]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Copy image", @"Context menu") action:@selector(cmImageCopy:) keyEquivalent:@""] setRepresentedObject:(__bridge id)content_get_bitmap(cont.object)]; [popupMenu addItem:[NSMenuItem separatorItem]]; } if (cont.link != NULL) { NSString *target = [NSString stringWithUTF8String:nsurl_access(cont.link)]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Open link in new tab", @"Context menu") action:@selector(cmOpenURLInTab:) keyEquivalent:@""] setRepresentedObject:target]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Open link in new window", @"Context menu") action:@selector(cmOpenURLInWindow:) keyEquivalent:@""] setRepresentedObject:target]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Save link target", @"Context menu") action:@selector(cmDownloadURL:) keyEquivalent:@""] setRepresentedObject:target]; [[popupMenu addItemWithTitle:NSLocalizedString(@"Copy link", @"Context menu") action:@selector(cmLinkCopy:) keyEquivalent:@""] setRepresentedObject:target]; [popupMenu addItem:[NSMenuItem separatorItem]]; } [popupMenu addItemWithTitle:NSLocalizedString(@"Back", @"Context menu") action:@selector(goBack:) keyEquivalent:@""]; [popupMenu addItemWithTitle:NSLocalizedString(@"Reload", @"Context menu") action:@selector(reloadPage:) keyEquivalent:@""]; [popupMenu addItemWithTitle:NSLocalizedString(@"Forward", @"Context menu") action:@selector(goForward:) keyEquivalent:@""]; [popupMenu addItemWithTitle:NSLocalizedString(@"View Source", @"Context menu") action:@selector(viewSource:) keyEquivalent:@""]; [NSMenu popUpContextMenu:popupMenu withEvent:event forView:self]; } - (IBAction)cmOpenURLInTab:(id)sender { nsurl *url; nserror error; error = nsurl_create([[sender representedObject] UTF8String], &url); if (error == NSERROR_OK) { error = browser_window_create(BW_CREATE_HISTORY | BW_CREATE_TAB | BW_CREATE_CLONE, url, NULL, browser, NULL); nsurl_unref(url); } if (error != NSERROR_OK) { cocoa_warning(messages_get_errorcode(error), 0); } } - (IBAction)cmOpenURLInWindow:(id)sender { nsurl *url; nserror error; error = nsurl_create([[sender representedObject] UTF8String], &url); if (error == NSERROR_OK) { error = browser_window_create(BW_CREATE_HISTORY | BW_CREATE_CLONE, url, NULL, browser, NULL); nsurl_unref(url); } if (error != NSERROR_OK) { cocoa_warning(messages_get_errorcode(error), 0); } } - (IBAction)cmDownloadURL:(id)sender { nsurl *url; if (nsurl_create([[sender representedObject] UTF8String], &url) == NSERROR_OK) { browser_window_navigate(browser, url, NULL, BW_NAVIGATE_DOWNLOAD, NULL, NULL, NULL); nsurl_unref(url); } } - (IBAction)cmImageCopy:(id)sender { NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb declareTypes:[NSArray arrayWithObject:NSTIFFPboardType] owner:nil]; [pb setData:[[sender representedObject] TIFFRepresentation] forType:NSTIFFPboardType]; } - (IBAction)cmLinkCopy:(id)sender { NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:nil]; [pb setString:[sender representedObject] forType:NSStringPboardType]; } // MARK: - // MARK: Accepting dragged URLs - (NSDragOperation)draggingEntered:(id)sender { if ((NSDragOperationCopy | NSDragOperationGeneric) & [sender draggingSourceOperationMask]) { return NSDragOperationCopy; } return NSDragOperationNone; } - (BOOL)prepareForDragOperation:(id)sender { return YES; } - (BOOL)performDragOperation:(id)sender { nsurl *url; nserror error; NSPasteboard *pb = [sender draggingPasteboard]; NSString *type = [pb availableTypeFromArray:[NSArray arrayWithObjects:@"public.url", NSURLPboardType, nil]]; NSString *urlstr = nil; if ([type isEqualToString:NSURLPboardType]) { urlstr = [[NSURL URLFromPasteboard:pb] absoluteString]; } else { urlstr = [pb stringForType:type]; } error = nsurl_create([urlstr UTF8String], &url); if (error != NSERROR_OK) { cocoa_warning(messages_get_errorcode(error), 0); } else { browser_window_navigate(browser, url, NULL, BW_NAVIGATE_DOWNLOAD, NULL, NULL, NULL); nsurl_unref(url); } return YES; } // MARK: - // MARK: NSTextInput protocol implementation - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange { markedText = [aString isEqualToString:@""] ? nil : [aString copy]; } - (void)unmarkText { [self setMarkedText:nil]; } - (BOOL)hasMarkedText { return markedText != nil; } - (NSInteger)conversationIdentifier { return (NSInteger)self; } - (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange { return [[NSAttributedString alloc] initWithString:@""]; } - (NSRange)markedRange { return NSMakeRange(NSNotFound, 0); } - (NSRange)selectedRange { return NSMakeRange(NSNotFound, 0); } - (NSRect)firstRectForCharacterRange:(NSRange)theRange { return NSZeroRect; } - (NSUInteger)characterIndexForPoint:(NSPoint)thePoint { return 0; } - (NSArray *)validAttributesForMarkedText { return [NSArray array]; } - (void)doCommandBySelector:(SEL)sel { [super doCommandBySelector:sel]; } @end