diff options
Diffstat (limited to 'cocoa/ScintillaCocoa.mm')
-rw-r--r-- | cocoa/ScintillaCocoa.mm | 1430 |
1 files changed, 1430 insertions, 0 deletions
diff --git a/cocoa/ScintillaCocoa.mm b/cocoa/ScintillaCocoa.mm new file mode 100644 index 000000000..b0456a53d --- /dev/null +++ b/cocoa/ScintillaCocoa.mm @@ -0,0 +1,1430 @@ + +/** + * Scintilla source code edit control + * ScintillaCocoa.mm - Cocoa subclass of ScintillaBase + * + * Mike Lischke <mlischke@sun.com> + * + * Loosely based on ScintillaMacOSX.cxx. + * Copyright 2003 by Evan Jones <ejones@uwaterloo.ca> + * Based on ScintillaGTK.cxx Copyright 1998-2002 by Neil Hodgson <neilh@scintilla.org> + * The License.txt file describes the conditions under which this software may be distributed. + * + * Copyright 2009 Sun Microsystems, Inc. All rights reserved. + * This file is dual licensed under LGPL v2.1 and the Scintilla license (http://www.scintilla.org/License.txt). + */ + +#include "ScintillaCocoa.h" +#include "ScintillaView.h" + +#include <Carbon/Carbon.h> // Temporary + +using namespace Scintilla; + +#ifndef WM_UNICHAR +#define WM_UNICHAR 0x0109 +#endif + +NSString* ScintillaRecPboardType = @"com.scintilla.utf16-plain-text.rectangular"; + +//-------------------------------------------------------------------------------------------------- + +// Define keyboard shortcuts (equivalents) the Mac way. +#define SCI_CMD ( SCI_ALT | SCI_CTRL) +#define SCI_SCMD ( SCI_CMD | SCI_SHIFT) + +static const KeyToCommand macMapDefault[] = +{ + {SCK_DOWN, SCI_CMD, SCI_DOCUMENTEND}, + {SCK_UP, SCI_CMD, SCI_DOCUMENTSTART}, + {SCK_LEFT, SCI_CMD, SCI_VCHOME}, + {SCK_LEFT, SCI_SCMD, SCI_VCHOMEEXTEND}, + {SCK_RIGHT, SCI_CMD, SCI_LINEEND}, + {SCK_RIGHT, SCI_SCMD, SCI_LINEENDEXTEND}, + {SCK_DOWN, SCI_NORM, SCI_LINEDOWN}, + {SCK_DOWN, SCI_SHIFT, SCI_LINEDOWNEXTEND}, + {SCK_DOWN, SCI_CTRL, SCI_LINESCROLLDOWN}, + {SCK_DOWN, SCI_ASHIFT, SCI_LINEDOWNRECTEXTEND}, + {SCK_UP, SCI_NORM, SCI_LINEUP}, + {SCK_UP, SCI_SHIFT, SCI_LINEUPEXTEND}, + {SCK_UP, SCI_CTRL, SCI_LINESCROLLUP}, + {SCK_UP, SCI_ASHIFT, SCI_LINEUPRECTEXTEND}, + {'[', SCI_CTRL, SCI_PARAUP}, + {'[', SCI_CSHIFT, SCI_PARAUPEXTEND}, + {']', SCI_CTRL, SCI_PARADOWN}, + {']', SCI_CSHIFT, SCI_PARADOWNEXTEND}, + {SCK_LEFT, SCI_NORM, SCI_CHARLEFT}, + {SCK_LEFT, SCI_SHIFT, SCI_CHARLEFTEXTEND}, + {SCK_LEFT, SCI_ALT, SCI_WORDLEFT}, + {SCK_LEFT, SCI_CSHIFT, SCI_WORDLEFTEXTEND}, + {SCK_LEFT, SCI_ASHIFT, SCI_CHARLEFTRECTEXTEND}, + {SCK_RIGHT, SCI_NORM, SCI_CHARRIGHT}, + {SCK_RIGHT, SCI_SHIFT, SCI_CHARRIGHTEXTEND}, + {SCK_RIGHT, SCI_ALT, SCI_WORDRIGHT}, + {SCK_RIGHT, SCI_CSHIFT, SCI_WORDRIGHTEXTEND}, + {SCK_RIGHT, SCI_ASHIFT, SCI_CHARRIGHTRECTEXTEND}, + {'/', SCI_CTRL, SCI_WORDPARTLEFT}, + {'/', SCI_CSHIFT, SCI_WORDPARTLEFTEXTEND}, + {'\\', SCI_CTRL, SCI_WORDPARTRIGHT}, + {'\\', SCI_CSHIFT, SCI_WORDPARTRIGHTEXTEND}, + {SCK_HOME, SCI_NORM, SCI_VCHOME}, + {SCK_HOME, SCI_SHIFT, SCI_VCHOMEEXTEND}, + {SCK_HOME, SCI_CTRL, SCI_DOCUMENTSTART}, + {SCK_HOME, SCI_CSHIFT, SCI_DOCUMENTSTARTEXTEND}, + {SCK_HOME, SCI_ALT, SCI_HOMEDISPLAY}, + {SCK_HOME, SCI_ASHIFT, SCI_VCHOMERECTEXTEND}, + {SCK_END, SCI_NORM, SCI_LINEEND}, + {SCK_END, SCI_SHIFT, SCI_LINEENDEXTEND}, + {SCK_END, SCI_CTRL, SCI_DOCUMENTEND}, + {SCK_END, SCI_CSHIFT, SCI_DOCUMENTENDEXTEND}, + {SCK_END, SCI_ALT, SCI_LINEENDDISPLAY}, + {SCK_END, SCI_ASHIFT, SCI_LINEENDRECTEXTEND}, + {SCK_PRIOR, SCI_NORM, SCI_PAGEUP}, + {SCK_PRIOR, SCI_SHIFT, SCI_PAGEUPEXTEND}, + {SCK_PRIOR, SCI_ASHIFT, SCI_PAGEUPRECTEXTEND}, + {SCK_NEXT, SCI_NORM, SCI_PAGEDOWN}, + {SCK_NEXT, SCI_SHIFT, SCI_PAGEDOWNEXTEND}, + {SCK_NEXT, SCI_ASHIFT, SCI_PAGEDOWNRECTEXTEND}, + {SCK_DELETE, SCI_NORM, SCI_CLEAR}, + {SCK_DELETE, SCI_SHIFT, SCI_CUT}, + {SCK_DELETE, SCI_CTRL, SCI_DELWORDRIGHT}, + {SCK_DELETE, SCI_CSHIFT, SCI_DELLINERIGHT}, + {SCK_INSERT, SCI_NORM, SCI_EDITTOGGLEOVERTYPE}, + {SCK_INSERT, SCI_SHIFT, SCI_PASTE}, + {SCK_INSERT, SCI_CTRL, SCI_COPY}, + {SCK_ESCAPE, SCI_NORM, SCI_CANCEL}, + {SCK_BACK, SCI_NORM, SCI_DELETEBACK}, + {SCK_BACK, SCI_SHIFT, SCI_DELETEBACK}, + {SCK_BACK, SCI_CTRL, SCI_DELWORDLEFT}, + {SCK_BACK, SCI_ALT, SCI_UNDO}, + {SCK_BACK, SCI_CSHIFT, SCI_DELLINELEFT}, + {'z', SCI_CMD, SCI_UNDO}, + {'y', SCI_CMD, SCI_REDO}, + {'x', SCI_CMD, SCI_CUT}, + {'c', SCI_CMD, SCI_COPY}, + {'v', SCI_CMD, SCI_PASTE}, + {'a', SCI_CMD, SCI_SELECTALL}, + {SCK_TAB, SCI_NORM, SCI_TAB}, + {SCK_TAB, SCI_SHIFT, SCI_BACKTAB}, + {SCK_RETURN, SCI_NORM, SCI_NEWLINE}, + {SCK_RETURN, SCI_SHIFT, SCI_NEWLINE}, + {SCK_ADD, SCI_CMD, SCI_ZOOMIN}, + {SCK_SUBTRACT, SCI_CMD, SCI_ZOOMOUT}, + {SCK_DIVIDE, SCI_CMD, SCI_SETZOOM}, + {'l', SCI_CMD, SCI_LINECUT}, + {'l', SCI_CSHIFT, SCI_LINEDELETE}, + {'t', SCI_CSHIFT, SCI_LINECOPY}, + {'t', SCI_CTRL, SCI_LINETRANSPOSE}, + {'d', SCI_CTRL, SCI_SELECTIONDUPLICATE}, + {'u', SCI_CTRL, SCI_LOWERCASE}, + {'u', SCI_CSHIFT, SCI_UPPERCASE}, + {0, 0, 0}, +}; + +//-------------------------------------------------------------------------------------------------- + +@implementation TimerTarget + +- (id) init: (void*) target +{ + [super init]; + if (self != nil) + { + mTarget = target; + + // Get the default notification queue for the thread which created the instance (usually the + // main thread). We need that later for idle event processing. + NSNotificationCenter* center = [NSNotificationCenter defaultCenter]; + notificationQueue = [[NSNotificationQueue alloc] initWithNotificationCenter: center]; + [center addObserver: self selector: @selector(idleTriggered:) name: @"Idle" object: nil]; + } + return self; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Method called by a timer installed by ScintillaCocoa. This two step approach is needed because + * a native Obj-C class is required as target for the timer. + */ +- (void) timerFired: (NSTimer*) timer +{ + reinterpret_cast<ScintillaCocoa*>(mTarget)->TimerFired(timer); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Another timer callback for the idle timer. + */ +- (void) idleTimerFired: (NSTimer*) timer +{ + // Idle timer event. + // Post a new idle notification, which gets executed when the run loop is idle. + // Since we are coalescing on name and sender there will always be only one actual notification + // even for multiple requests. + NSNotification *notification = [NSNotification notificationWithName: @"Idle" object: self]; + [notificationQueue enqueueNotification: notification + postingStyle: NSPostWhenIdle + coalesceMask: (NSNotificationCoalescingOnName | NSNotificationCoalescingOnSender) + forModes: nil]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Another step for idle events. The timer (for idle events) simply requests a notification on + * idle time. Only when this notification is send we actually call back the editor. + */ +- (void) idleTriggered: (NSNotification*) notification +{ + reinterpret_cast<ScintillaCocoa*>(mTarget)->IdleTimerFired(); +} + +@end + +//----------------- ScintillaCocoa ----------------------------------------------------------------- + +ScintillaCocoa::ScintillaCocoa(NSView* view) +{ + wMain= [view retain]; + timerTarget = [[[TimerTarget alloc] init: this] retain]; + Initialise(); +} + +//-------------------------------------------------------------------------------------------------- + +ScintillaCocoa::~ScintillaCocoa() +{ + SetTicking(false); + [timerTarget release]; + NSView* container = ContentView(); + [container release]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Core initialization of the control. Everything that needs to be set up happens here. + */ +void ScintillaCocoa::Initialise() +{ + notifyObj = NULL; + notifyProc = NULL; + + capturedMouse = false; + + // Tell Scintilla not to buffer: Quartz buffers drawing for us. + WndProc(SCI_SETBUFFEREDDRAW, 0, 0); + + // We are working with Unicode exclusively. + WndProc(SCI_SETCODEPAGE, SC_CP_UTF8, 0); + + // Add Mac specific key bindings. + for (int i = 0; macMapDefault[i].key; i++) + kmap.AssignCmdKey(macMapDefault[i].key, macMapDefault[i].modifiers, macMapDefault[i].msg); + +} + +//-------------------------------------------------------------------------------------------------- + +/** + * We need some clean up. Do it here. + */ +void ScintillaCocoa::Finalise() +{ + SetTicking(false); + ScintillaBase::Finalise(); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Helper function to get the outer container which represents the Scintilla editor on application side. + */ +ScintillaView* ScintillaCocoa::TopContainer() +{ + NSView* container = static_cast<NSView*>(wMain.GetID()); + return static_cast<ScintillaView*>([container superview]); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Helper function to get the inner container which represents the actual "canvas" we work with. + */ +NSView* ScintillaCocoa::ContentView() +{ + return static_cast<NSView*>(wMain.GetID()); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Instead of returning the size of the inner view we have to return the visible part of it + * in order to make scrolling working properly. + */ +PRectangle ScintillaCocoa::GetClientRectangle() +{ + NSView* host = ContentView(); + NSSize size = [host frame].size; + return PRectangle(0, 0, size.width, size.height); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Converts the given point from base coordinates to local coordinates and at the same time into + * a native Point structure. Base coordinates are used for the top window used in the view hierarchy. + */ +Scintilla::Point ScintillaCocoa::ConvertPoint(NSPoint point) +{ + NSView* container = ContentView(); + NSPoint result = [container convertPoint: point fromView: nil]; + + return Point(result.x, result.y); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * A function to directly execute code that would usually go the long way via window messages. + * However this is a Windows metapher and not used here, hence we just call our fake + * window proc. The given parameters directly reflect the message parameters used on Windows. + * + * @param sciThis The target which is to be called. + * @param iMessage A code that indicates which message was sent. + * @param wParam One of the two free parameters for the message. Traditionally a word sized parameter + * (hence the w prefix). + * @param lParam The other of the two free parameters. A signed long. + */ +sptr_t ScintillaCocoa::DirectFunction(ScintillaCocoa *sciThis, unsigned int iMessage, uptr_t wParam, + sptr_t lParam) +{ + return sciThis->WndProc(iMessage, wParam, lParam); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * This method is very similar to DirectFunction. On Windows it sends a message (not in the Obj-C sense) + * to the target window. Here we simply call our fake window proc. + */ +sptr_t scintilla_send_message(void* sci, unsigned int iMessage, uptr_t wParam, sptr_t lParam) +{ + ScintillaView *control = reinterpret_cast<ScintillaView*>(sci); + ScintillaCocoa* scintilla = [control backend]; + return scintilla->WndProc(iMessage, wParam, lParam); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * That's our fake window procedure. On Windows each window has a dedicated procedure to handle + * commands (also used to synchronize UI and background threads), which is not the case in Cocoa. + * + * Messages handled here are almost solely for special commands of the backend. Everything which + * would be sytem messages on Windows (e.g. for key down, mouse move etc.) are handled by + * directly calling appropriate handlers. + */ +sptr_t ScintillaCocoa::WndProc(unsigned int iMessage, uptr_t wParam, sptr_t lParam) +{ + switch (iMessage) + { + case SCI_GETDIRECTFUNCTION: + return reinterpret_cast<sptr_t>(DirectFunction); + + case SCI_GETDIRECTPOINTER: + return reinterpret_cast<sptr_t>(this); + + case SCI_GRABFOCUS: + // TODO: implement it + break; + + case WM_UNICHAR: + // Special case not used normally. Characters passed in this way will be inserted + // regardless of their value or modifier states. That means no command interpretation is + // performed. + if (IsUnicodeMode()) + { + NSString* input = [[NSString stringWithCharacters: (const unichar*) &wParam length: 1] autorelease]; + const char* utf8 = [input UTF8String]; + AddCharUTF((char*) utf8, strlen(utf8), false); + return 1; + } + return 0; + + default: + unsigned int r = ScintillaBase::WndProc(iMessage, wParam, lParam); + + return r; + } + return 0l; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * In Windows lingo this is the handler which handles anything that wasn't handled in the normal + * window proc which would usually send the message back to generic window proc that Windows uses. + */ +sptr_t ScintillaCocoa::DefWndProc(unsigned int, uptr_t, sptr_t) +{ + return 0; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Enables or disables a timer that can trigger background processing at a regular interval, like + * drag scrolling or caret blinking. + */ +void ScintillaCocoa::SetTicking(bool on) +{ + if (timer.ticking != on) + { + timer.ticking = on; + if (timer.ticking) + { + // Scintilla ticks = milliseconds + // Using userInfo as flag to distinct between tick and idle timer. + NSTimer* tickTimer = [NSTimer scheduledTimerWithTimeInterval: timer.tickSize / 1000.0 + target: timerTarget + selector: @selector(timerFired:) + userInfo: nil + repeats: YES]; + timer.tickerID = reinterpret_cast<TickerID>(tickTimer); + } + else + if (timer.tickerID != NULL) + { + [reinterpret_cast<NSTimer*>(timer.tickerID) invalidate]; + timer.tickerID = 0; + } + } + timer.ticksToWait = caret.period; +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::SetIdle(bool on) +{ + if (idler.state != on) + { + idler.state = on; + if (idler.state) + { + // Scintilla ticks = milliseconds + NSTimer* idleTimer = [NSTimer scheduledTimerWithTimeInterval: timer.tickSize / 1000.0 + target: timerTarget + selector: @selector(idleTimerFired:) + userInfo: nil + repeats: YES]; + idler.idlerID = reinterpret_cast<IdlerID>(idleTimer); + } + else + if (idler.idlerID != NULL) + { + [reinterpret_cast<NSTimer*>(idler.idlerID) invalidate]; + idler.idlerID = 0; + } + } + return true; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::CopyToClipboard(const SelectionText &selectedText) +{ + SetPasteboardData([NSPasteboard generalPasteboard], selectedText); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::Copy() +{ + if (currentPos != anchor) + { + SelectionText selectedText; + CopySelectionRange(&selectedText); + CopyToClipboard(selectedText); + } +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::CanPaste() +{ + if (!Editor::CanPaste()) + return false; + + bool ok = GetPasteboardData([NSPasteboard generalPasteboard], NULL); + return ok; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::Paste() +{ + Paste(false); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Pastes data from the paste board into the editor. + */ +void ScintillaCocoa::Paste(bool forceRectangular) +{ + SelectionText selectedText; + bool ok = GetPasteboardData([NSPasteboard generalPasteboard], &selectedText); + if (forceRectangular) + selectedText.rectangular = forceRectangular; + + if (!ok || !selectedText.s) + // No data or no flavor we support. + return; + + pdoc->BeginUndoAction(); + ClearSelection(); + if (selectedText.rectangular) + { + int selStart = SelectionStart(); + PasteRectangular(selStart, selectedText.s, selectedText.len); + } + else + if (pdoc->InsertString(currentPos, selectedText.s, selectedText.len)) + SetEmptySelection(currentPos + selectedText.len); + + pdoc->EndUndoAction(); + + Redraw(); + EnsureCaretVisible(); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::CreateCallTipWindow(PRectangle rc) +{ +#if 0 + // create a calltip window + if (!ct.wCallTip.Created()) { + WindowClass windowClass = kHelpWindowClass; + WindowAttributes attributes = kWindowNoAttributes; + Rect contentBounds; + WindowRef outWindow; + + // convert PRectangle to Rect + // this adjustment gets the calltip window placed in the correct location relative + // to our editor window + Rect bounds; + OSStatus err; + err = GetWindowBounds( this->GetOwner(), kWindowGlobalPortRgn, &bounds ); + assert( err == noErr ); + contentBounds.top = rc.top + bounds.top; + contentBounds.bottom = rc.bottom + bounds.top; + contentBounds.right = rc.right + bounds.left; + contentBounds.left = rc.left + bounds.left; + + // create our calltip hiview + HIViewRef ctw = scintilla_calltip_new(); + CallTip* objectPtr = &ct; + ScintillaCocoa* sciThis = this; + SetControlProperty( ctw, scintillaMacOSType, 0, sizeof( this ), &sciThis ); + SetControlProperty( ctw, scintillaCallTipType, 0, sizeof( objectPtr ), &objectPtr ); + + CreateNewWindow(windowClass, attributes, &contentBounds, &outWindow); + ControlRef root; + CreateRootControl(outWindow, &root); + + HIViewRef hiroot = HIViewGetRoot (outWindow); + HIViewAddSubview(hiroot, ctw); + + HIRect boundsRect; + HIViewGetFrame(hiroot, &boundsRect); + HIViewSetFrame( ctw, &boundsRect ); + + // bind the size of the calltip to the size of it's container window + HILayoutInfo layout = { + kHILayoutInfoVersionZero, + { + { NULL, kHILayoutBindTop, 0 }, + { NULL, kHILayoutBindLeft, 0 }, + { NULL, kHILayoutBindBottom, 0 }, + { NULL, kHILayoutBindRight, 0 } + }, + { + { NULL, kHILayoutScaleAbsolute, 0 }, + { NULL, kHILayoutScaleAbsolute, 0 } + + }, + { + { NULL, kHILayoutPositionTop, 0 }, + { NULL, kHILayoutPositionLeft, 0 } + } + }; + HIViewSetLayoutInfo(ctw, &layout); + + ct.wCallTip = root; + ct.wDraw = ctw; + ct.wCallTip.SetWindow(outWindow); + HIViewSetVisible(ctw,true); + + } +#endif +} + + +void ScintillaCocoa::AddToPopUp(const char *label, int cmd, bool enabled) +{ + // Translate stuff into menu item attributes + MenuItemAttributes attributes = 0; + if (label[0] == '\0') + attributes |= kMenuItemAttrSeparator; + if (!enabled) + attributes |= kMenuItemAttrDisabled; + + // Translate Scintilla commands into Mac OS commands + // TODO: If I create an AEDesc, OS X may insert these standard + // text editing commands into the menu for me. + MenuCommand macCommand; + switch (cmd) + { + case idcmdUndo: + macCommand = kHICommandUndo; + break; + case idcmdRedo: + macCommand = kHICommandRedo; + break; + case idcmdCut: + macCommand = kHICommandCut; + break; + case idcmdCopy: + macCommand = kHICommandCopy; + break; + case idcmdPaste: + macCommand = kHICommandPaste; + break; + case idcmdDelete: + macCommand = kHICommandClear; + break; + case idcmdSelectAll: + macCommand = kHICommandSelectAll; + break; + case 0: + macCommand = 0; + break; + default: + assert( false ); + return; + } + +// NSMenu *menu= reinterpret_cast<NSMenu*>(popup.GetID()); + + //XXX TODO:XXX +// [menu addItemWithTitle:[NSString stringWithUTF8String:label] +// action:@selector() +// keyEquivalent:@""]; +} + +// ------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::ClaimSelection() +{ + // Mac OS X does not have a primary selection. +} + +// ------------------------------------------------------------------------------------------------- + +#pragma segment Drag + +/** + * Triggered by the tick timer on a regular basis to scroll the content during a drag operation. + */ +void ScintillaCocoa::DragScroll() +{ + if (posDrag == invalidPosition) + { + scrollSpeed = 1; + scrollTicks = 2000; + return; + } + + // TODO: does not work for wrapped lines, fix it. + int line = pdoc->LineFromPosition(posDrag); + int currentVisibleLine = cs.DisplayFromDoc(line); + int lastVisibleLine = Platform::Minimum(topLine + LinesOnScreen(), cs.LinesDisplayed()) - 2; + + if (currentVisibleLine <= topLine && topLine > 0) + ScrollTo(topLine - scrollSpeed); + else + if (currentVisibleLine >= lastVisibleLine) + ScrollTo(topLine + scrollSpeed); + else + { + scrollSpeed = 1; + scrollTicks = 2000; + return; + } + + // TODO: also handle horizontal scrolling. + + if (scrollSpeed == 1) + { + scrollTicks -= timer.tickSize; + if (scrollTicks <= 0) + { + scrollSpeed = 5; + scrollTicks = 2000; + } + } + +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Called when a drag operation was initiated from within Scintilla. + */ +void ScintillaCocoa::StartDrag() +{ + if (currentPos == anchor) + return; + + // Put the data to be dragged on the drag pasteboard. + SelectionText selectedText; + NSPasteboard* pasteboard = [NSPasteboard pasteboardWithName: NSDragPboard]; + CopySelectionRange(&selectedText); + SetPasteboardData(pasteboard, selectedText); + + // Prepare drag image. + PRectangle localRectangle = RectangleFromRange(SelectionStart(), SelectionEnd()); + NSRect selectionRectangle = PRectangleToNSRect(localRectangle); + + NSView* content = ContentView(); + +#if 0 // TODO: fix initialization of the drag image with CGImageRef. + // To get a bitmap of the text we're dragging, we just use Paint on a pixmap surface. + SurfaceImpl *sw = new SurfaceImpl(); + SurfaceImpl *pixmap = NULL; + + bool lastHideSelection = hideSelection; + hideSelection = true; + if (sw) + { + pixmap = new SurfaceImpl(); + if (pixmap) + { + PRectangle client = GetClientRectangle(); + PRectangle imageRect = NSRectToPRectangle(selectionRectangle); + paintState = painting; + //sw->InitPixMap(client.Width(), client.Height(), NULL, NULL); + sw->InitPixMap(imageRect.Width(), imageRect.Height(), NULL, NULL); + paintingAllText = true; + Paint(sw, imageRect); + paintState = notPainting; + + pixmap->InitPixMap(imageRect.Width(), imageRect.Height(), NULL, NULL); + + CGContextRef gc = pixmap->GetContext(); + + // To make Paint() work on a bitmap, we have to flip our coordinates and translate the origin + CGContextTranslateCTM(gc, 0, imageRect.Height()); + CGContextScaleCTM(gc, 1.0, -1.0); + + pixmap->CopyImageRectangle(*sw, imageRect, PRectangle(0, 0, imageRect.Width(), imageRect.Height())); + // XXX TODO: overwrite any part of the image that is not part of the + // selection to make it transparent. right now we just use + // the full rectangle which may include non-selected text. + } + sw->Release(); + delete sw; + } + hideSelection = lastHideSelection; + + NSBitmapImageRep* bitmap = NULL; + if (pixmap) + { + bitmap = [[[NSBitmapImageRep alloc] initWithCGImage: pixmap->GetImage()] autorelease]; + pixmap->Release(); + delete pixmap; + } +#else + + // Poor man's drag image: take a snapshot of the content view. + [content lockFocus]; + NSBitmapImageRep* bitmap = [[[NSBitmapImageRep alloc] initWithFocusedViewRect: selectionRectangle] autorelease]; + [bitmap setColorSpaceName: NSDeviceRGBColorSpace]; + [content unlockFocus]; + +#endif + + NSImage* image = [[[NSImage alloc] initWithSize: selectionRectangle.size] autorelease]; + [image addRepresentation: bitmap]; + + NSImage* dragImage = [[[NSImage alloc] initWithSize: selectionRectangle.size] autorelease]; + [dragImage setBackgroundColor: [NSColor clearColor]]; + [dragImage lockFocus]; + [image dissolveToPoint: NSMakePoint(0.0, 0.0) fraction: 0.5]; + [dragImage unlockFocus]; + + NSPoint startPoint; + startPoint.x = selectionRectangle.origin.x; + startPoint.y = selectionRectangle.origin.y + selectionRectangle.size.height; + [content dragImage: dragImage + at: startPoint + offset: NSZeroSize + event: lastMouseEvent // Set in MouseMove. + pasteboard: pasteboard + source: content + slideBack: YES]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Called when a drag operation reaches the control which was initiated outside. + */ +NSDragOperation ScintillaCocoa::DraggingEntered(id <NSDraggingInfo> info) +{ + inDragDrop = ddDragging; + return DraggingUpdated(info); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Called frequently during a drag operation if we are the target. Keep telling the caller + * what drag operation we accept and update the drop caret position to indicate the + * potential insertion point of the dragged data. + */ +NSDragOperation ScintillaCocoa::DraggingUpdated(id <NSDraggingInfo> info) +{ + // Convert the drag location from window coordinates to view coordinates and + // from there to a text position to finally set the drag position. + Point location = ConvertPoint([info draggingLocation]); + SetDragPosition(PositionFromLocation(location)); + + NSDragOperation sourceDragMask = [info draggingSourceOperationMask]; + if (sourceDragMask == NSDragOperationNone) + return sourceDragMask; + + NSPasteboard* pasteboard = [info draggingPasteboard]; + + // Return what type of operation we will perform. Prefer move over copy. + if ([[pasteboard types] containsObject: NSStringPboardType] || + [[pasteboard types] containsObject: ScintillaRecPboardType]) + return (sourceDragMask & NSDragOperationMove) ? NSDragOperationMove : NSDragOperationCopy; + + if ([[pasteboard types] containsObject: NSFilenamesPboardType]) + return (sourceDragMask & NSDragOperationGeneric); + return NSDragOperationNone; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Resets the current drag position as we are no longer the drag target. + */ +void ScintillaCocoa::DraggingExited(id <NSDraggingInfo> info) +{ + SetDragPosition(invalidPosition); + inDragDrop = ddNone; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Here is where the real work is done. Insert the text from the pasteboard. + */ +bool ScintillaCocoa::PerformDragOperation(id <NSDraggingInfo> info) +{ + NSPasteboard* pasteboard = [info draggingPasteboard]; + + if ([[pasteboard types] containsObject: NSFilenamesPboardType]) + { + NSArray* files = [pasteboard propertyListForType: NSFilenamesPboardType]; + for (NSString* uri in files) + NotifyURIDropped([uri UTF8String]); + } + else + { + SelectionText text; + GetPasteboardData(pasteboard, &text); + + if (text.len > 0) + { + NSDragOperation operation = [info draggingSourceOperationMask]; + bool moving = (operation & NSDragOperationMove) != 0; + + DropAt(posDrag, text.s, moving, text.rectangular); + }; + } + + return true; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::SetPasteboardData(NSPasteboard* board, const SelectionText &selectedText) +{ + if (selectedText.len == 0) + return; + + NSString *string; + string = [NSString stringWithUTF8String: selectedText.s]; + + [board declareTypes:[NSArray arrayWithObjects: + NSStringPboardType, + selectedText.rectangular ? ScintillaRecPboardType : nil, + nil] owner:nil]; + + if (selectedText.rectangular) + { + // This is specific to scintilla, allows us to drag rectangular selections around the document. + [board setString: string forType: ScintillaRecPboardType]; + } + + [board setString: string forType: NSStringPboardType]; + +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Helper method to retrieve the best fitting alternative from the general pasteboard. + */ +bool ScintillaCocoa::GetPasteboardData(NSPasteboard* board, SelectionText* selectedText) +{ + NSArray* supportedTypes = [NSArray arrayWithObjects: ScintillaRecPboardType, + NSStringPboardType, + nil]; + NSString *bestType = [board availableTypeFromArray: supportedTypes]; + NSString* data = [board stringForType: bestType]; + + if (data != nil) + { + if (selectedText != nil) + { + char* text = (char*) [data UTF8String]; + bool rectangular = bestType == ScintillaRecPboardType; + selectedText->Copy(text, strlen(text) + 1, 0, 0, rectangular, false); + } + return true; + } + + return false; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::SetMouseCapture(bool on) +{ + capturedMouse = on; + /* + if (mouseDownCaptures) + { + if (capturedMouse) + WndProc(SCI_SETCURSOR, Window::cursorArrow, 0); + else + // Reset to normal. Actual image will be set on mouse move. + WndProc(SCI_SETCURSOR, (unsigned int) SC_CURSORNORMAL, 0); + } + */ +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::HaveMouseCapture() +{ + return capturedMouse; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Synchronously paint a rectangle of the window. + */ +void ScintillaCocoa::SyncPaint(void* gc, PRectangle rc) +{ + paintState = painting; + rcPaint = rc; + PRectangle rcText = GetTextRectangle(); + paintingAllText = rcPaint.Contains(rcText); + Surface *sw = Surface::Allocate(); + if (sw) + { + sw->Init(gc, wMain.GetID()); + Paint(sw, rc); + if (paintState == paintAbandoned) + { + // Do a full paint. + rcPaint = GetClientRectangle(); + paintState = painting; + paintingAllText = true; + Paint(sw, rcPaint); + } + sw->Release(); + delete sw; + } + paintState = notPainting; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Modfies the vertical scroll position to make the current top line show up as such. + */ +void ScintillaCocoa::SetVerticalScrollPos() +{ + ScintillaView* topContainer = TopContainer(); + + // Convert absolute coordinate into the range [0..1]. Keep in mind that the visibile area + // does *not* belong to the scroll range. + float relativePosition = (float) topLine / MaxScrollPos(); + [topContainer setVerticalScrollPosition: relativePosition]; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::SetHorizontalScrollPos() +{ + ScintillaView* topContainer = TopContainer(); + PRectangle textRect = GetTextRectangle(); + + // Convert absolute coordinate into the range [0..1]. Keep in mind that the visibile area + // does *not* belong to the scroll range. + float relativePosition = (float) xOffset / (scrollWidth - textRect.Width()); + [topContainer setHorizontalScrollPosition: relativePosition]; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Used to adjust both scrollers to reflect the current scroll range and position in the editor. + * + * @param nMax Number of lines in the editor. + * @param nPage Number of lines per scroll page. + * @return True if there was a change, otherwise false. + */ +bool ScintillaCocoa::ModifyScrollBars(int nMax, int nPage) +{ + // Input values are given in lines, not pixels, so we have to convert. + int lineHeight = WndProc(SCI_TEXTHEIGHT, 0, 0); + NSView* container = ContentView(); + NSRect bounds = [container frame]; + ScintillaView* topContainer = TopContainer(); + + int scrollRange = lineHeight * (nMax + 1); // +1 because the caller subtracted one. + int pageSize = bounds.size.height; + bool verticalChange = [topContainer setVerticalScrollRange: scrollRange page: pageSize]; + + scrollRange = scrollWidth; + pageSize = bounds.size.width; + bool horizontalChange = [topContainer setHorizontalScrollRange: scrollRange page: pageSize]; + + return verticalChange || horizontalChange; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::Resize() +{ + ChangeSize(); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Called by the frontend control when the user manipulates one of the scrollers. + * + * @param position The relative position of the scroller in the range of [0..1]. + * @param part Specifies which part was clicked on by the user, so we can handle thumb tracking + * as well as page and line scrolling. + * @param horizontal True if the horizontal scroller was hit, otherwise false. + */ +void ScintillaCocoa::DoScroll(float position, NSScrollerPart part, bool horizontal) +{ + // If the given scroller part is not the knob (or knob slot) then the given position is not yet + // current and we have to update it. + if (horizontal) + { + // Horizontal offset is given in pixels. + PRectangle textRect = GetTextRectangle(); + int offset = (int) (position * (scrollWidth - textRect.Width())); + int smallChange = (int) (textRect.Width() / 30); + if (smallChange < 5) + smallChange = 5; + switch (part) + { + case NSScrollerDecrementLine: + offset -= smallChange; + break; + case NSScrollerDecrementPage: + offset -= textRect.Width(); + break; + case NSScrollerIncrementLine: + offset += smallChange; + break; + case NSScrollerIncrementPage: + offset += textRect.Width(); + break; + }; + HorizontalScrollTo(offset); + } + else + { + // VerticalScrolling is by line. + int topLine = (int) (position * MaxScrollPos()); + int page = LinesOnScreen(); + switch (part) + { + case NSScrollerDecrementLine: + topLine--; + break; + case NSScrollerDecrementPage: + topLine -= page; + break; + case NSScrollerIncrementLine: + topLine++; + break; + case NSScrollerIncrementPage: + topLine += page; + break; + }; + ScrollTo(topLine, true); + } +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Used to register a callback function for a given window. This is used to emulate the way + * Windows notfies other controls (mainly up in the view hierarchy) about certain events. + * + * @param windowid A handle to a window. That value is generic and can be anything. It is passed + * through to the callback. + * @param callback The callback function to be used for future notifications. If NULL then no + * notifications will be sent anymore. + */ +void ScintillaCocoa::RegisterNotifyCallback(intptr_t windowid, SciNotifyFunc callback) +{ + notifyObj = windowid; + notifyProc = callback; +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::NotifyChange() +{ + if (notifyProc != NULL) + notifyProc(notifyObj, WM_COMMAND, (uintptr_t) (SCEN_CHANGE << 16), (uintptr_t) this); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::NotifyFocus(bool focus) +{ + if (notifyProc != NULL) + notifyProc(notifyObj, WM_COMMAND, (uintptr_t) ((focus ? SCEN_SETFOCUS : SCEN_KILLFOCUS) << 16), (uintptr_t) this); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Used to send a notification (as WM_NOTIFY call) to the procedure, which has been set by the call + * to RegisterNotifyCallback (so it is not necessarily the parent window). + * + * @param scn The notification to send. + */ +void ScintillaCocoa::NotifyParent(SCNotification scn) +{ + if (notifyProc != NULL) + { + scn.nmhdr.hwndFrom = (void*) this; + scn.nmhdr.idFrom = (unsigned int) wMain.GetID(); + notifyProc(notifyObj, WM_NOTIFY, (uintptr_t) 0, (uintptr_t) &scn); + } +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::NotifyURIDropped(const char *uri) +{ + SCNotification scn; + scn.nmhdr.code = SCN_URIDROPPED; + scn.text = uri; + + NotifyParent(scn); +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::HasSelection() +{ + return (SelectionEnd() - SelectionStart()) > 0; +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::CanUndo() +{ + return pdoc->CanUndo(); +} + +//-------------------------------------------------------------------------------------------------- + +bool ScintillaCocoa::CanRedo() +{ + return pdoc->CanRedo(); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::TimerFired(NSTimer* timer) +{ + Tick(); + DragScroll(); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::IdleTimerFired() +{ + bool more = Idle(); + if (!more) + SetIdle(false); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Main entry point for drawing the control. + * + * @param rect The area to paint, given in the sender's coordinate. + * @param gc The context we can use to paint. + */ +void ScintillaCocoa::Draw(NSRect rect, CGContextRef gc) +{ + SyncPaint(gc, NSRectToPRectangle(rect)); +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Helper function to translate OS X key codes to Scintilla key codes. + */ +static inline UniChar KeyTranslate(UniChar unicodeChar) +{ + switch (unicodeChar) + { + case NSDownArrowFunctionKey: + return SCK_DOWN; + case NSUpArrowFunctionKey: + return SCK_UP; + case NSLeftArrowFunctionKey: + return SCK_LEFT; + case NSRightArrowFunctionKey: + return SCK_RIGHT; + case NSHomeFunctionKey: + return SCK_HOME; + case NSEndFunctionKey: + return SCK_END; + case NSPageUpFunctionKey: + return SCK_PRIOR; + case NSPageDownFunctionKey: + return SCK_NEXT; + case NSDeleteFunctionKey: + return SCK_DELETE; + case NSInsertFunctionKey: + return SCK_INSERT; + case '\n': + case 3: + return SCK_RETURN; + case 27: + return SCK_ESCAPE; + case 127: + return SCK_BACK; + case '\t': + case 25: // Shift tab, return to unmodified tab and handle that via modifiers. + return SCK_TAB; + default: + return unicodeChar; + } +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Main keyboard input handling method. It is called for any key down event, including function keys, + * numeric keypad input and whatnot. + * + * @param event The event instance associated with the key down event. + * @return True if the input was handled, false otherwise. + */ +bool ScintillaCocoa::KeyboardInput(NSEvent* event) +{ + // For now filter out function keys. + NSUInteger modifiers = [event modifierFlags]; + + NSString* input = [event characters]; + + bool control = (modifiers & NSControlKeyMask) != 0; + bool shift = (modifiers & NSShiftKeyMask) != 0; + bool command = (modifiers & NSCommandKeyMask) != 0; + bool alt = (modifiers & NSAlternateKeyMask) != 0; + + bool handled = false; + + // Handle each entry individually. Usually we only have one entry anway. + for (int i = 0; i < input.length; i++) + { + const UniChar originalKey = [input characterAtIndex: i]; + UniChar key = KeyTranslate(originalKey); + + bool consumed = false; // Consumed as command? + + // Signal command as control + alt. This leaves us without command + control and command + alt + // but that's what we get when we have a modifier key more than other platforms. + if (KeyDown(key, shift, control || command, alt || command, &consumed)) + handled = true; + if (consumed) + handled = true; + } + + return handled; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Used to insert already processed text provided by the Cocoa text input system. + */ +int ScintillaCocoa::InsertText(NSString* input) +{ + const char* utf8 = [input UTF8String]; + AddCharUTF((char*) utf8, strlen(utf8), false); + return true; +} + +//-------------------------------------------------------------------------------------------------- + +/** + * Called by the owning view when the mouse pointer enters the control. + */ +void ScintillaCocoa::MouseEntered(NSEvent* event) +{ + if (!HaveMouseCapture()) + { + WndProc(SCI_SETCURSOR, (long int)SC_CURSORNORMAL, 0); + + // Mouse location is given in screen coordinates and might also be outside of our bounds. + Point location = ConvertPoint([event locationInWindow]); + ButtonMove(location); + } +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::MouseExited(NSEvent* event) +{ + // Nothing to do here. +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::MouseDown(NSEvent* event) +{ + Point location = ConvertPoint([event locationInWindow]); + NSTimeInterval time = [event timestamp]; + bool command = ([event modifierFlags] & NSCommandKeyMask) != 0; + bool shift = ([event modifierFlags] & NSShiftKeyMask) != 0; + bool control = ([event modifierFlags] & NSControlKeyMask) != 0; + + ButtonDown(Point(location.x, location.y), (int) (time * 1000), shift, control, command); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::MouseMove(NSEvent* event) +{ + lastMouseEvent = event; + ButtonMove(ConvertPoint([event locationInWindow])); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::MouseUp(NSEvent* event) +{ + NSTimeInterval time = [event timestamp]; + bool control = ([event modifierFlags] & NSControlKeyMask) != 0; + + ButtonUp(ConvertPoint([event locationInWindow]), (int) (time * 1000), control); +} + +//-------------------------------------------------------------------------------------------------- + +void ScintillaCocoa::MouseWheel(NSEvent* event) +{ + bool command = ([event modifierFlags] & NSCommandKeyMask) != 0; + bool shift = ([event modifierFlags] & NSShiftKeyMask) != 0; + int lineHeight = WndProc(SCI_TEXTHEIGHT, 0, 0); + int delta; + if (shift) + delta = 10 * [event deltaX]; // Arbitrary scale factor. + else + { + // In order to make scrolling with larger offset smoother we scroll less line the larger the + // delta value is. + if ([event deltaY] < 0) + delta = -(int) sqrt(-10.0 * [event deltaY]); + else + delta = (int) sqrt(10.0 * [event deltaY]); + } + + if (command) + { + // Zoom! We play with the font sizes in the styles. + // Number of steps/line is ignored, we just care if sizing up or down. + if (delta > 0) + KeyCommand(SCI_ZOOMIN); + else + KeyCommand(SCI_ZOOMOUT); + } + else + if (shift) + HorizontalScrollTo(xOffset - delta); + else + ScrollTo(topLine - delta, true); +} + +//-------------------------------------------------------------------------------------------------- + +//OSStatus ScintillaCocoa::ContextualMenuClick( HIPoint& location ) +//{ +// // convert screen coords to window relative +// Rect bounds; +// OSStatus err; +// err = GetWindowBounds( this->GetOwner(), kWindowContentRgn, &bounds ); +// assert( err == noErr ); +// location.x += bounds.left; +// location.y += bounds.top; +// ContextMenu( Scintilla::Point( static_cast<int>( location.x ), static_cast<int>( location.y ) ) ); +// return noErr; +//} +// +//OSStatus ScintillaCocoa::ActiveStateChanged() +//{ +// // If the window is being deactivated, lose the focus and turn off the ticking +// if ( ! this->IsActive() ) { +// DropCaret(); +// //SetFocusState( false ); +// SetTicking( false ); +// } else { +// ShowCaretAtCurrentPosition(); +// } +// return noErr; +//} +// + +//-------------------------------------------------------------------------------------------------- |