diff --git a/samples/Objective-C++/EventHandlerMac.mm b/samples/Objective-C++/EventHandlerMac.mm new file mode 100644 index 00000000..f6028c0b --- /dev/null +++ b/samples/Objective-C++/EventHandlerMac.mm @@ -0,0 +1,778 @@ +// grabbed from https://raw.github.com/AOKP/external_webkit/61b2fb934bdd3a5fea253e2de0bcf8a47a552333/Source/WebCore/page/mac/EventHandlerMac.mm + +/* + * Copyright (C) 2006, 2007, 2008, 2009 Apple Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" +#include "EventHandler.h" + +#include "AXObjectCache.h" +#include "BlockExceptions.h" +#include "Chrome.h" +#include "ChromeClient.h" +#include "ClipboardMac.h" +#include "DragController.h" +#include "EventNames.h" +#include "FocusController.h" +#include "Frame.h" +#include "FrameLoader.h" +#include "FrameView.h" +#include "KeyboardEvent.h" +#include "MouseEventWithHitTestResults.h" +#include "NotImplemented.h" +#include "Page.h" +#include "PlatformKeyboardEvent.h" +#include "PlatformWheelEvent.h" +#include "RenderWidget.h" +#include "RuntimeApplicationChecks.h" +#include "Scrollbar.h" +#include "Settings.h" +#include "WebCoreSystemInterface.h" +#include +#include + +#if !(defined(OBJC_API_VERSION) && OBJC_API_VERSION > 0) +static inline IMP method_setImplementation(Method m, IMP i) +{ + IMP oi = m->method_imp; + m->method_imp = i; + return oi; +} +#endif + +namespace WebCore { + +#if ENABLE(DRAG_SUPPORT) +const double EventHandler::TextDragDelay = 0.15; +#endif + +static RetainPtr& currentNSEventSlot() +{ + DEFINE_STATIC_LOCAL(RetainPtr, event, ()); + return event; +} + +NSEvent *EventHandler::currentNSEvent() +{ + return currentNSEventSlot().get(); +} + +class CurrentEventScope { + WTF_MAKE_NONCOPYABLE(CurrentEventScope); +public: + CurrentEventScope(NSEvent *); + ~CurrentEventScope(); + +private: + RetainPtr m_savedCurrentEvent; +#ifndef NDEBUG + RetainPtr m_event; +#endif +}; + +inline CurrentEventScope::CurrentEventScope(NSEvent *event) + : m_savedCurrentEvent(currentNSEventSlot()) +#ifndef NDEBUG + , m_event(event) +#endif +{ + currentNSEventSlot() = event; +} + +inline CurrentEventScope::~CurrentEventScope() +{ + ASSERT(currentNSEventSlot() == m_event); + currentNSEventSlot() = m_savedCurrentEvent; +} + +bool EventHandler::wheelEvent(NSEvent *event) +{ + Page* page = m_frame->page(); + if (!page) + return false; + + CurrentEventScope scope(event); + + PlatformWheelEvent wheelEvent(event, page->chrome()->platformPageClient()); + handleWheelEvent(wheelEvent); + + return wheelEvent.isAccepted(); +} + +PassRefPtr EventHandler::currentKeyboardEvent() const +{ + NSEvent *event = [NSApp currentEvent]; + if (!event) + return 0; + switch ([event type]) { + case NSKeyDown: { + PlatformKeyboardEvent platformEvent(event); + platformEvent.disambiguateKeyDownEvent(PlatformKeyboardEvent::RawKeyDown); + return KeyboardEvent::create(platformEvent, m_frame->document()->defaultView()); + } + case NSKeyUp: + return KeyboardEvent::create(event, m_frame->document()->defaultView()); + default: + return 0; + } +} + +bool EventHandler::keyEvent(NSEvent *event) +{ + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + ASSERT([event type] == NSKeyDown || [event type] == NSKeyUp); + + CurrentEventScope scope(event); + return keyEvent(PlatformKeyboardEvent(event)); + + END_BLOCK_OBJC_EXCEPTIONS; + + return false; +} + +void EventHandler::focusDocumentView() +{ + Page* page = m_frame->page(); + if (!page) + return; + + if (FrameView* frameView = m_frame->view()) { + if (NSView *documentView = frameView->documentView()) + page->chrome()->focusNSView(documentView); + } + + page->focusController()->setFocusedFrame(m_frame); +} + +bool EventHandler::passWidgetMouseDownEventToWidget(const MouseEventWithHitTestResults& event) +{ + // Figure out which view to send the event to. + RenderObject* target = targetNode(event) ? targetNode(event)->renderer() : 0; + if (!target || !target->isWidget()) + return false; + + // Double-click events don't exist in Cocoa. Since passWidgetMouseDownEventToWidget will + // just pass currentEvent down to the widget, we don't want to call it for events that + // don't correspond to Cocoa events. The mousedown/ups will have already been passed on as + // part of the pressed/released handling. + return passMouseDownEventToWidget(toRenderWidget(target)->widget()); +} + +bool EventHandler::passWidgetMouseDownEventToWidget(RenderWidget* renderWidget) +{ + return passMouseDownEventToWidget(renderWidget->widget()); +} + +static bool lastEventIsMouseUp() +{ + // Many AppKit widgets run their own event loops and consume events while the mouse is down. + // When they finish, currentEvent is the mouseUp that they exited on. We need to update + // the WebCore state with this mouseUp, which we never saw. This method lets us detect + // that state. Handling this was critical when we used AppKit widgets for form elements. + // It's not clear in what cases this is helpful now -- it's possible it can be removed. + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + NSEvent *currentEventAfterHandlingMouseDown = [NSApp currentEvent]; + return EventHandler::currentNSEvent() != currentEventAfterHandlingMouseDown + && [currentEventAfterHandlingMouseDown type] == NSLeftMouseUp + && [currentEventAfterHandlingMouseDown timestamp] >= [EventHandler::currentNSEvent() timestamp]; + END_BLOCK_OBJC_EXCEPTIONS; + + return false; +} + +bool EventHandler::passMouseDownEventToWidget(Widget* pWidget) +{ + // FIXME: This function always returns true. It should be changed either to return + // false in some cases or the return value should be removed. + + RefPtr widget = pWidget; + + if (!widget) { + LOG_ERROR("hit a RenderWidget without a corresponding Widget, means a frame is half-constructed"); + return true; + } + + // In WebKit2 we will never have an NSView. Just return early and let the regular event handler machinery take care of + // dispatching the event. + if (!widget->platformWidget()) + return false; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + NSView *nodeView = widget->platformWidget(); + ASSERT([nodeView superview]); + NSView *view = [nodeView hitTest:[[nodeView superview] convertPoint:[currentNSEvent() locationInWindow] fromView:nil]]; + if (!view) { + // We probably hit the border of a RenderWidget + return true; + } + + Page* page = m_frame->page(); + if (!page) + return true; + + if (page->chrome()->client()->firstResponder() != view) { + // Normally [NSWindow sendEvent:] handles setting the first responder. + // But in our case, the event was sent to the view representing the entire web page. + if ([currentNSEvent() clickCount] <= 1 && [view acceptsFirstResponder] && [view needsPanelToBecomeKey]) + page->chrome()->client()->makeFirstResponder(view); + } + + // We need to "defer loading" while tracking the mouse, because tearing down the + // page while an AppKit control is tracking the mouse can cause a crash. + + // FIXME: In theory, WebCore now tolerates tear-down while tracking the + // mouse. We should confirm that, and then remove the deferrsLoading + // hack entirely. + + bool wasDeferringLoading = page->defersLoading(); + if (!wasDeferringLoading) + page->setDefersLoading(true); + + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + NSView *outerView = widget->getOuterView(); + widget->beforeMouseDown(outerView, widget.get()); + [view mouseDown:currentNSEvent()]; + widget->afterMouseDown(outerView, widget.get()); + m_sendingEventToSubview = false; + + if (!wasDeferringLoading) + page->setDefersLoading(false); + + // Remember which view we sent the event to, so we can direct the release event properly. + m_mouseDownView = view; + m_mouseDownWasInSubframe = false; + + // Many AppKit widgets run their own event loops and consume events while the mouse is down. + // When they finish, currentEvent is the mouseUp that they exited on. We need to update + // the EventHandler state with this mouseUp, which we never saw. + // If this event isn't a mouseUp, we assume that the mouseUp will be coming later. There + // is a hole here if the widget consumes both the mouseUp and subsequent events. + if (lastEventIsMouseUp()) + m_mousePressed = false; + + END_BLOCK_OBJC_EXCEPTIONS; + + return true; +} + +// Note that this does the same kind of check as [target isDescendantOf:superview]. +// There are two differences: This is a lot slower because it has to walk the whole +// tree, and this works in cases where the target has already been deallocated. +static bool findViewInSubviews(NSView *superview, NSView *target) +{ + BEGIN_BLOCK_OBJC_EXCEPTIONS; + NSEnumerator *e = [[superview subviews] objectEnumerator]; + NSView *subview; + while ((subview = [e nextObject])) { + if (subview == target || findViewInSubviews(subview, target)) { + return true; + } + } + END_BLOCK_OBJC_EXCEPTIONS; + + return false; +} + +NSView *EventHandler::mouseDownViewIfStillGood() +{ + // Since we have no way of tracking the lifetime of m_mouseDownView, we have to assume that + // it could be deallocated already. We search for it in our subview tree; if we don't find + // it, we set it to nil. + NSView *mouseDownView = m_mouseDownView; + if (!mouseDownView) { + return nil; + } + FrameView* topFrameView = m_frame->view(); + NSView *topView = topFrameView ? topFrameView->platformWidget() : nil; + if (!topView || !findViewInSubviews(topView, mouseDownView)) { + m_mouseDownView = nil; + return nil; + } + return mouseDownView; +} + +#if ENABLE(DRAG_SUPPORT) +bool EventHandler::eventLoopHandleMouseDragged(const MouseEventWithHitTestResults&) +{ + NSView *view = mouseDownViewIfStillGood(); + + if (!view) + return false; + + if (!m_mouseDownWasInSubframe) { + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + BEGIN_BLOCK_OBJC_EXCEPTIONS; + [view mouseDragged:currentNSEvent()]; + END_BLOCK_OBJC_EXCEPTIONS; + m_sendingEventToSubview = false; + } + + return true; +} +#endif // ENABLE(DRAG_SUPPORT) + +bool EventHandler::eventLoopHandleMouseUp(const MouseEventWithHitTestResults&) +{ + NSView *view = mouseDownViewIfStillGood(); + if (!view) + return false; + + if (!m_mouseDownWasInSubframe) { + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + BEGIN_BLOCK_OBJC_EXCEPTIONS; + [view mouseUp:currentNSEvent()]; + END_BLOCK_OBJC_EXCEPTIONS; + m_sendingEventToSubview = false; + } + + return true; +} + +bool EventHandler::passSubframeEventToSubframe(MouseEventWithHitTestResults& event, Frame* subframe, HitTestResult* hoveredNode) +{ + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + switch ([currentNSEvent() type]) { + case NSLeftMouseDragged: + case NSOtherMouseDragged: + case NSRightMouseDragged: + // This check is bogus and results in , but removing it breaks a number of + // layout tests. + if (!m_mouseDownWasInSubframe) + return false; +#if ENABLE(DRAG_SUPPORT) + if (subframe->page()->dragController()->didInitiateDrag()) + return false; +#endif + case NSMouseMoved: + // Since we're passing in currentNSEvent() here, we can call + // handleMouseMoveEvent() directly, since the save/restore of + // currentNSEvent() that mouseMoved() does would have no effect. + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + subframe->eventHandler()->handleMouseMoveEvent(currentPlatformMouseEvent(), hoveredNode); + m_sendingEventToSubview = false; + return true; + + case NSLeftMouseDown: { + Node* node = targetNode(event); + if (!node) + return false; + RenderObject* renderer = node->renderer(); + if (!renderer || !renderer->isWidget()) + return false; + Widget* widget = toRenderWidget(renderer)->widget(); + if (!widget || !widget->isFrameView()) + return false; + if (!passWidgetMouseDownEventToWidget(toRenderWidget(renderer))) + return false; + m_mouseDownWasInSubframe = true; + return true; + } + case NSLeftMouseUp: { + if (!m_mouseDownWasInSubframe) + return false; + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + subframe->eventHandler()->handleMouseReleaseEvent(currentPlatformMouseEvent()); + m_sendingEventToSubview = false; + return true; + } + default: + return false; + } + END_BLOCK_OBJC_EXCEPTIONS; + + return false; +} + +static IMP originalNSScrollViewScrollWheel; +static bool _nsScrollViewScrollWheelShouldRetainSelf; +static void selfRetainingNSScrollViewScrollWheel(NSScrollView *, SEL, NSEvent *); + +static bool nsScrollViewScrollWheelShouldRetainSelf() +{ + ASSERT(isMainThread()); + + return _nsScrollViewScrollWheelShouldRetainSelf; +} + +static void setNSScrollViewScrollWheelShouldRetainSelf(bool shouldRetain) +{ + ASSERT(isMainThread()); + + if (!originalNSScrollViewScrollWheel) { + Method method = class_getInstanceMethod(objc_getRequiredClass("NSScrollView"), @selector(scrollWheel:)); + originalNSScrollViewScrollWheel = method_setImplementation(method, reinterpret_cast(selfRetainingNSScrollViewScrollWheel)); + } + + _nsScrollViewScrollWheelShouldRetainSelf = shouldRetain; +} + +static void selfRetainingNSScrollViewScrollWheel(NSScrollView *self, SEL selector, NSEvent *event) +{ + bool shouldRetainSelf = isMainThread() && nsScrollViewScrollWheelShouldRetainSelf(); + + if (shouldRetainSelf) + [self retain]; + originalNSScrollViewScrollWheel(self, selector, event); + if (shouldRetainSelf) + [self release]; +} + +bool EventHandler::passWheelEventToWidget(PlatformWheelEvent& wheelEvent, Widget* widget) +{ + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + if (!widget) + return false; + + NSView* nodeView = widget->platformWidget(); + if (!nodeView) { + // WebKit2 code path. + if (!widget->isFrameView()) + return false; + + return static_cast(widget)->frame()->eventHandler()->handleWheelEvent(wheelEvent); + } + + if ([currentNSEvent() type] != NSScrollWheel || m_sendingEventToSubview) + return false; + + ASSERT(nodeView); + ASSERT([nodeView superview]); + NSView *view = [nodeView hitTest:[[nodeView superview] convertPoint:[currentNSEvent() locationInWindow] fromView:nil]]; + if (!view) + // We probably hit the border of a RenderWidget + return false; + + ASSERT(!m_sendingEventToSubview); + m_sendingEventToSubview = true; + // Work around which can cause -[NSScrollView scrollWheel:] to + // crash if the NSScrollView is released during timer or network callback dispatch + // in the nested tracking runloop that -[NSScrollView scrollWheel:] runs. + setNSScrollViewScrollWheelShouldRetainSelf(true); + [view scrollWheel:currentNSEvent()]; + setNSScrollViewScrollWheelShouldRetainSelf(false); + m_sendingEventToSubview = false; + return true; + + END_BLOCK_OBJC_EXCEPTIONS; + return false; +} + +void EventHandler::mouseDown(NSEvent *event) +{ + FrameView* v = m_frame->view(); + if (!v || m_sendingEventToSubview) + return; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + m_frame->loader()->resetMultipleFormSubmissionProtection(); + + m_mouseDownView = nil; + + CurrentEventScope scope(event); + + handleMousePressEvent(currentPlatformMouseEvent()); + + END_BLOCK_OBJC_EXCEPTIONS; +} + +void EventHandler::mouseDragged(NSEvent *event) +{ + FrameView* v = m_frame->view(); + if (!v || m_sendingEventToSubview) + return; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + CurrentEventScope scope(event); + handleMouseMoveEvent(currentPlatformMouseEvent()); + + END_BLOCK_OBJC_EXCEPTIONS; +} + +void EventHandler::mouseUp(NSEvent *event) +{ + FrameView* v = m_frame->view(); + if (!v || m_sendingEventToSubview) + return; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + CurrentEventScope scope(event); + + // Our behavior here is a little different that Qt. Qt always sends + // a mouse release event, even for a double click. To correct problems + // in khtml's DOM click event handling we do not send a release here + // for a double click. Instead we send that event from FrameView's + // handleMouseDoubleClickEvent. Note also that the third click of + // a triple click is treated as a single click, but the fourth is then + // treated as another double click. Hence the "% 2" below. + int clickCount = [event clickCount]; + if (clickCount > 0 && clickCount % 2 == 0) + handleMouseDoubleClickEvent(currentPlatformMouseEvent()); + else + handleMouseReleaseEvent(currentPlatformMouseEvent()); + + m_mouseDownView = nil; + + END_BLOCK_OBJC_EXCEPTIONS; +} + +/* + A hack for the benefit of AK's PopUpButton, which uses the Carbon menu manager, which thus + eats all subsequent events after it is starts its modal tracking loop. After the interaction + is done, this routine is used to fix things up. When a mouse down started us tracking in + the widget, we post a fake mouse up to balance the mouse down we started with. When a + key down started us tracking in the widget, we post a fake key up to balance things out. + In addition, we post a fake mouseMoved to get the cursor in sync with whatever we happen to + be over after the tracking is done. + */ +void EventHandler::sendFakeEventsAfterWidgetTracking(NSEvent *initiatingEvent) +{ + FrameView* view = m_frame->view(); + if (!view) + return; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + + m_sendingEventToSubview = false; + int eventType = [initiatingEvent type]; + if (eventType == NSLeftMouseDown || eventType == NSKeyDown) { + NSEvent *fakeEvent = nil; + if (eventType == NSLeftMouseDown) { + fakeEvent = [NSEvent mouseEventWithType:NSLeftMouseUp + location:[initiatingEvent locationInWindow] + modifierFlags:[initiatingEvent modifierFlags] + timestamp:[initiatingEvent timestamp] + windowNumber:[initiatingEvent windowNumber] + context:[initiatingEvent context] + eventNumber:[initiatingEvent eventNumber] + clickCount:[initiatingEvent clickCount] + pressure:[initiatingEvent pressure]]; + + [NSApp postEvent:fakeEvent atStart:YES]; + } else { // eventType == NSKeyDown + fakeEvent = [NSEvent keyEventWithType:NSKeyUp + location:[initiatingEvent locationInWindow] + modifierFlags:[initiatingEvent modifierFlags] + timestamp:[initiatingEvent timestamp] + windowNumber:[initiatingEvent windowNumber] + context:[initiatingEvent context] + characters:[initiatingEvent characters] + charactersIgnoringModifiers:[initiatingEvent charactersIgnoringModifiers] + isARepeat:[initiatingEvent isARepeat] + keyCode:[initiatingEvent keyCode]]; + [NSApp postEvent:fakeEvent atStart:YES]; + } + // FIXME: We should really get the current modifierFlags here, but there's no way to poll + // them in Cocoa, and because the event stream was stolen by the Carbon menu code we have + // no up-to-date cache of them anywhere. + fakeEvent = [NSEvent mouseEventWithType:NSMouseMoved + location:[[view->platformWidget() window] convertScreenToBase:[NSEvent mouseLocation]] + modifierFlags:[initiatingEvent modifierFlags] + timestamp:[initiatingEvent timestamp] + windowNumber:[initiatingEvent windowNumber] + context:[initiatingEvent context] + eventNumber:0 + clickCount:0 + pressure:0]; + [NSApp postEvent:fakeEvent atStart:YES]; + } + + END_BLOCK_OBJC_EXCEPTIONS; +} + +void EventHandler::mouseMoved(NSEvent *event) +{ + // Reject a mouse moved if the button is down - screws up tracking during autoscroll + // These happen because WebKit sometimes has to fake up moved events. + if (!m_frame->view() || m_mousePressed || m_sendingEventToSubview) + return; + + BEGIN_BLOCK_OBJC_EXCEPTIONS; + CurrentEventScope scope(event); + mouseMoved(currentPlatformMouseEvent()); + END_BLOCK_OBJC_EXCEPTIONS; +} + +static bool frameHasPlatformWidget(Frame* frame) +{ + if (FrameView* frameView = frame->view()) { + if (frameView->platformWidget()) + return true; + } + + return false; +} + +bool EventHandler::passMousePressEventToSubframe(MouseEventWithHitTestResults& mev, Frame* subframe) +{ + // WebKit1 code path. + if (frameHasPlatformWidget(m_frame)) + return passSubframeEventToSubframe(mev, subframe); + + // WebKit2 code path. + subframe->eventHandler()->handleMousePressEvent(mev.event()); + return true; +} + +bool EventHandler::passMouseMoveEventToSubframe(MouseEventWithHitTestResults& mev, Frame* subframe, HitTestResult* hoveredNode) +{ + // WebKit1 code path. + if (frameHasPlatformWidget(m_frame)) + return passSubframeEventToSubframe(mev, subframe, hoveredNode); + + // WebKit2 code path. + if (m_mouseDownMayStartDrag && !m_mouseDownWasInSubframe) + return false; + subframe->eventHandler()->handleMouseMoveEvent(mev.event(), hoveredNode); + return true; +} + +bool EventHandler::passMouseReleaseEventToSubframe(MouseEventWithHitTestResults& mev, Frame* subframe) +{ + // WebKit1 code path. + if (frameHasPlatformWidget(m_frame)) + return passSubframeEventToSubframe(mev, subframe); + + // WebKit2 code path. + subframe->eventHandler()->handleMouseReleaseEvent(mev.event()); + return true; +} + +PlatformMouseEvent EventHandler::currentPlatformMouseEvent() const +{ + NSView *windowView = nil; + if (Page* page = m_frame->page()) + windowView = page->chrome()->platformPageClient(); + return PlatformMouseEvent(currentNSEvent(), windowView); +} + +#if ENABLE(CONTEXT_MENUS) +bool EventHandler::sendContextMenuEvent(NSEvent *event) +{ + Page* page = m_frame->page(); + if (!page) + return false; + return sendContextMenuEvent(PlatformMouseEvent(event, page->chrome()->platformPageClient())); +} +#endif // ENABLE(CONTEXT_MENUS) + +#if ENABLE(DRAG_SUPPORT) +bool EventHandler::eventMayStartDrag(NSEvent *event) +{ + Page* page = m_frame->page(); + if (!page) + return false; + return eventMayStartDrag(PlatformMouseEvent(event, page->chrome()->platformPageClient())); +} +#endif // ENABLE(DRAG_SUPPORT) + +bool EventHandler::eventActivatedView(const PlatformMouseEvent& event) const +{ + return m_activationEventNumber == event.eventNumber(); +} + +#if ENABLE(DRAG_SUPPORT) + +PassRefPtr EventHandler::createDraggingClipboard() const +{ + NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard]; + // Must be done before ondragstart adds types and data to the pboard, + // also done for security, as it erases data from the last drag + [pasteboard declareTypes:[NSArray array] owner:nil]; + return ClipboardMac::create(Clipboard::DragAndDrop, pasteboard, ClipboardWritable, m_frame); +} + +#endif + +bool EventHandler::tabsToAllFormControls(KeyboardEvent* event) const +{ + Page* page = m_frame->page(); + if (!page) + return false; + + KeyboardUIMode keyboardUIMode = page->chrome()->client()->keyboardUIMode(); + bool handlingOptionTab = isKeyboardOptionTab(event); + + // If tab-to-links is off, option-tab always highlights all controls + if ((keyboardUIMode & KeyboardAccessTabsToLinks) == 0 && handlingOptionTab) + return true; + + // If system preferences say to include all controls, we always include all controls + if (keyboardUIMode & KeyboardAccessFull) + return true; + + // Otherwise tab-to-links includes all controls, unless the sense is flipped via option-tab. + if (keyboardUIMode & KeyboardAccessTabsToLinks) + return !handlingOptionTab; + + return handlingOptionTab; +} + +bool EventHandler::needsKeyboardEventDisambiguationQuirks() const +{ + Document* document = m_frame->document(); + + // RSS view needs arrow key keypress events. + if (applicationIsSafari() && (document->url().protocolIs("feed") || document->url().protocolIs("feeds"))) + return true; + Settings* settings = m_frame->settings(); + if (!settings) + return false; + +#if ENABLE(DASHBOARD_SUPPORT) + if (settings->usesDashboardBackwardCompatibilityMode()) + return true; +#endif + + if (settings->needsKeyboardEventDisambiguationQuirks()) + return true; + + return false; +} + +unsigned EventHandler::accessKeyModifiers() +{ + // Control+Option key combinations are usually unused on Mac OS X, but not when VoiceOver is enabled. + // So, we use Control in this case, even though it conflicts with Emacs-style key bindings. + // See for more detail. + if (AXObjectCache::accessibilityEnhancedUserInterfaceEnabled()) + return PlatformKeyboardEvent::CtrlKey; + + return PlatformKeyboardEvent::CtrlKey | PlatformKeyboardEvent::AltKey; +} + +} diff --git a/samples/Objective-C++/objsql.mm b/samples/Objective-C++/objsql.mm new file mode 100644 index 00000000..f71ca8eb --- /dev/null +++ b/samples/Objective-C++/objsql.mm @@ -0,0 +1,1372 @@ +/* + * objsql.m - implementaion simple persistence layer using objcpp.h + * ======== + * + * Created by John Holdsworth on 01/04/2009. + * Copyright 2009 John Holdsworth. + * + * $Id: //depot/4.4/ObjCpp/objsql.mm#11 $ + * $DateTime: 2012/09/05 00:20:47 $ + * + * C++ classes to wrap up XCode classes for operator overload of + * useful operations such as access to NSArrays and NSDictionary + * by subscript or NSString operators such as + for concatenation. + * + * This works as the Apple Objective-C compiler supports source + * which mixes C++ with objective C. To enable this: for each + * source file which will include/import this header file, select + * it in Xcode and open it's "Info". To enable mixed compilation, + * for the file's "File Type" select: "sourcecode.cpp.objcpp". + * + * For bugs or ommisions please email objcpp@johnholdsworth.com + * + * Home page for updates and docs: http://objcpp.johnholdsworth.com + * + * You may make commercial use of this source in applications without + * charge but not sell it as source nor can you remove this notice from + * this source if you redistribute. You can make any changes you like + * to this code before redistribution but please annotate them below. + * + * If you find it useful please send a donation via paypal to account + * objcpp@johnholdsworth.com. Thanks. + * + * THIS CODE IS PROVIDED “AS IS” WITHOUT WARRANTY OF ANY KIND EITHER + * EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. + * + * IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + * WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + * THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING + * ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT + * OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED + * TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED + * BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH + * ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * + */ + +#import +#import + +#import "objsql.h" + +#if 0 +#ifdef OODEBUG +#define OODEBUG_SQL 1 +#endif +#endif + +OOOODatabase OODB; + +static NSString *kOOObject = @"__OOOBJECT__", *kOOInsert = @"__ISINSERT__", *kOOUpdate = @"__ISUPDATE__", *kOOExecSQL = @"__OOEXEC__"; + +#pragma mark OORecord abstract superclass for records + +@implementation OORecord + ++ (id)record OO_AUTORETURNS { + return OO_AUTORELEASE( [[self alloc] init] ); +} + ++ (id)insert OO_AUTORETURNS { + OORecord *record = [self record]; + [record insert]; + return record; +} + ++ (id)insertWithParent:(id)parent { + return [[OODatabase sharedInstance] copyJoinKeysFrom:parent to:[self insert]]; +} + +- (id)insert { [[OODatabase sharedInstance] insert:self]; return self; } +- (id)delete { [[OODatabase sharedInstance] delete:self]; return self; } + +- (void)update { [[OODatabase sharedInstance] update:self]; } +- (void)indate { [[OODatabase sharedInstance] indate:self]; } +- (void)upsert { [[OODatabase sharedInstance] upsert:self]; } + +- (int)commit { return [[OODatabase sharedInstance] commit]; } +- (int)rollback { return [[OODatabase sharedInstance] rollback]; } + +// to handle null values for ints, floats etc. +- (void)setNilValueForKey:(NSString *)key { + static OOReference zeroForNull; + if ( !zeroForNull ) + zeroForNull = [NSNumber numberWithInt:0]; + [self setValue:zeroForNull forKey:key]; +} + ++ (OOArray)select { + return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:nil]; +} + ++ (OOArray)select:(cOOString)sql { + return [[OODatabase sharedInstance] select:sql intoClass:self joinFrom:nil]; +} + ++ (OOArray)selectRecordsRelatedTo:(id)parent { + return [[OODatabase sharedInstance] select:nil intoClass:self joinFrom:parent]; +} + +- (OOArray)select { + return [[OODatabase sharedInstance] select:nil intoClass:[self class] joinFrom:self]; +} + +/** + import a flat file with column values separated by the delimiter specified into + the table associated with this class. + */ + ++ (int)importFrom:(OOFile &)file delimiter:(cOOString)delim { + OOArray rows = [OOMetaData import:file.string() intoClass:self delimiter:delim]; + [[OODatabase sharedInstance] insertArray:rows]; + return [OODatabase commit]; +} + +/** + Export a flat file with all rows in the table associated with the record subclass. + */ + ++ (BOOL)exportTo:(OOFile &)file delimiter:(cOOString)delim { + return file.save( [OOMetaData export:[self select] delimiter:delim] ); +} + +/** + populate a view with the string values taken from the ivars of the record + */ + +- (void)bindToView:(OOView *)view delegate:(id)delegate { + [OOMetaData bindRecord:self toView:view delegate:delegate]; +} + +/** + When the delegate method os sent use this method to update the record's values + */ + +- (void)updateFromView:(OOView *)view { + [OOMetaData updateRecord:self fromView:view]; +} + +/** + Default description is dictionary containing values of all ivars + */ + +- (NSString *)description { + OOMetaData *metaData = [OOMetaData metaDataForClass:[self class]]; + // hack required where record contains a field "description" to avoid recursion + OOStringArray ivars; ivars <<= *metaData->ivars; ivars -= "description"; + return [*[metaData encode:[self dictionaryWithValuesForKeys:ivars]] description]; +} + +@end + +#pragma mark OOAdaptor - all methods required by objsql to access a database + +/** + An internal class representing the interface to a particular database, in this case sqlite3. + */ + +@interface OOAdaptor : NSObject { + sqlite3 *db; + sqlite3_stmt *stmt; + struct _str_link { + struct _str_link *next; char str[1]; + } *strs; + OO_UNSAFE OODatabase *owner; +} + +- initPath:(cOOString)path database:(OODatabase *)database; +- (BOOL)prepare:(cOOString)sql; + +- (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls; +- (OOArray)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData; +- (sqlite_int64)lastInsertRowID; + +@end + +@interface NSData(OOExtras) +- initWithDescription:(NSString *)description; +@end + +#pragma mark OODatabase is the low level interface to a particular database + +@implementation OODatabase + +static OOReference sharedInstance; + +/** + By default database file is "objsql.db" in the user/application's "Documents" directory + and a single shared OODatabase instance used for all db operations. + */ + ++ (OODatabase *)sharedInstance { + if ( !sharedInstance ) + [self sharedInstanceForPath:OODocument("objsql.db").path()]; + return sharedInstance; +} + +/** + Shared instance can be switched between any file paths. + */ + + + (OODatabase *)sharedInstanceForPath:(cOOString)path { + if ( !!sharedInstance ) + sharedInstance = OONil; + if ( !!path ) + OO_RELEASE( sharedInstance = [[OODatabase alloc] initPath:path] ); + return sharedInstance; +} + ++ (BOOL)exec:(cOOString)fmt, ... { + va_list argp; va_start(argp, fmt); + NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp]; + va_end( argp ); + return [[self sharedInstance] exec:OO_AUTORELEASE( sql )]; +} + ++ (OOArray)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent { + return [[self sharedInstance] select:select intoClass:recordClass joinFrom:parent]; +} ++ (OOArray)select:(cOOString)select intoClass:(Class)recordClass { + return [[self sharedInstance] select:select intoClass:recordClass joinFrom:nil]; +} ++ (OOArray)select:(cOOString)select { + return [[self sharedInstance] select:select intoClass:nil joinFrom:nil]; +} + ++ (int)insertArray:(const OOArray &)objects { return [[self sharedInstance] insertArray:objects]; } ++ (int)deleteArray:(const OOArray &)objects { return [[self sharedInstance] deleteArray:objects]; } + ++ (int)insert:(id)object { return [[self sharedInstance] insert:object]; } ++ (int)delete:(id)object { return [[self sharedInstance] delete:object]; } ++ (int)update:(id)object { return [[self sharedInstance] update:object]; } + ++ (int)indate:(id)object { return [[self sharedInstance] indate:object]; } ++ (int)upsert:(id)object { return [[self sharedInstance] upsert:object]; } + ++ (int)commit { return [[self sharedInstance] commit]; } ++ (int)rollback { return [[self sharedInstance] rollback]; } ++ (int)commitTransaction { return [[OODatabase sharedInstance] commitTransaction]; } + +/** + Designated initialiser for OODatabase instances. Generally only the shared instance is used + and the OODatabase class object is messaged instead. + */ + +- initPath:(cOOString)path { + if ( self = [super init] ) + OO_RELEASE( adaptor = [[OOAdaptor alloc] initPath:path database:self] ); + return self; +} + +/** + Automatically register all classes which are subclasses of a record abstract superclass (e.g. OORecord). + */ + +- (OOStringArray)registerSubclassesOf:(Class)recordSuperClass { + int numClasses = objc_getClassList( NULL, 0 ); + Class *classes = (Class *)malloc( sizeof *classes * numClasses ); + OOArray viewClasses; + OOStringArray classNames; + + // scan all registered classes for relevant subclasses + numClasses = objc_getClassList( classes, numClasses ); + for ( int c=0 ; cresults. + */ + +- (BOOL)exec:(cOOString)fmt, ... { + va_list argp; va_start(argp, fmt); + NSString *sql = [[NSString alloc] initWithFormat:fmt arguments:argp]; + va_end( argp ); + results = [self select:sql intoClass:NULL joinFrom:nil]; + OO_RELEASE( sql ); + return !errcode; +} + +/** + Return a single value from row 1, column one from sql sent to the database as a string. + */ + +- (OOString)stringForSql:(cOOString)fmt, ... { + va_list argp; va_start(argp, fmt); + NSString *sql = OO_AUTORELEASE( [[NSString alloc] initWithFormat:fmt arguments:argp] ); + va_end( argp ); + if( [self exec:"%@", sql] && results > 0 ) { + NSString *aColumnName = [[**results[0] allKeys] objectAtIndex:0]; + return [(NSNumber *)(*results[0])[aColumnName] stringValue]; + } + else + return nil; +} + +/** + Used to initialise new child records automatically from parent in relation. + */ + +- (id)copyJoinKeysFrom:(id)parent to:(id)newChild { + OOMetaData *parentMetaData = [self tableMetaDataForClass:[parent class]], + *childMetaData = [self tableMetaDataForClass:[newChild class]]; + OOStringArray commonColumns = [parentMetaData naturalJoinTo:childMetaData->columns]; //// + OOValueDictionary keyValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:commonColumns]]; + [newChild setValuesForKeysWithDictionary:[childMetaData decode:keyValues]]; + return newChild; +} + +/** + Build a where clause for the columns specified. + */ + +- (OOString)whereClauseFor:(cOOStringArray)columns values:(cOOValueDictionary)values qualifyNulls:(BOOL)qualifyNulls { + OOString out; + for ( int i=0 ; ijoinableColumns]; + joinValues = [parentMetaData encode:[parent dictionaryWithValuesForKeys:sharedColumns]]; + + sql += [self whereClauseFor:sharedColumns values:joinValues qualifyNulls:NO]; + } + + if ( [metaData->recordClass respondsToSelector:@selector(ooOrderBy)] ) + sql += OOFormat( @"\norder by %@", [metaData->recordClass ooOrderBy] ); + +#ifdef OODEBUG_SQL + NSLog( @"-[OOMetaData prepareSql:] %@\n%@", *sql, *joinValues ); +#endif + + if ( ![*adaptor prepare:sql] ) + return NO; + + return !parent || [*adaptor bindCols:sharedColumns values:joinValues startingAt:1 bindNulls:NO]; +} + +/** + Determine a list of the tables which have a natural join to the record passed in. + If the record is a specific instance of from a table this should determine if + there are any record which exist using the join. + */ + +- (OOArray)tablesRelatedByNaturalJoinFrom:(id)record { + OOMetaData *metaData = [record class] == [OOMetaData class] ? + record : [self tableMetaDataForClass:[record class]]; + + OOStringArray tablesWithNaturalJoin; + tablesWithNaturalJoin <<= metaData->tablesWithNaturalJoin; + + if ( record && record != metaData ) + for ( int i=0 ; i > tmpResults = [*adaptor bindResultsIntoInstancesOfClass:NULL metaData:nil]; + + if ( ![(*tmpResults[0])["result"] intValue] ) + ~tablesWithNaturalJoin[i--]; + } + + return tableMetaDataByClassName[+tablesWithNaturalJoin]; +} + +/** + Perform a select from a table on the database using either the sql specified + orselect all columns from the table associated with the record class passed in. + If a parent is passed in make a natural join from that record. + */ + +- (OOArray)select:(cOOString)select intoClass:(Class)recordClass joinFrom:(id)parent { + OOMetaData *metaData = [self tableMetaDataForClass:recordClass ? recordClass : [parent class]]; + OOString sql = !select ? + OOFormat( @"select %@\nfrom %@", *(metaData->outcols/", "), *metaData->tableName ) : *select; + + if ( ![self prepareSql:sql joinFrom:parent toTable:metaData] ) + return nil; + + return [*adaptor bindResultsIntoInstancesOfClass:recordClass metaData:metaData]; +} + +- (OOArray)select:(cOOString)select intoClass:(Class)recordClass { + return [self select:select intoClass:recordClass joinFrom:nil]; +} + +- (OOArray)select:(cOOString)select { + return [self select:select intoClass:nil joinFrom:nil]; +} + +/** + Returns sqlite3 row identifier for a record instance. + */ + +- (long long)rowIDForRecord:(id)record { + OOMetaData *metaData = [self tableMetaDataForClass:[record class]]; + OOString sql = OOFormat( @"select ROWID from %@", *metaData->tableName ); + OOArray > idResults = [self select:sql intoClass:nil joinFrom:record]; + return [*(*idResults[0])[@"rowid"] longLongValue]; +} + +/** + Returns sqlite3 row identifier for last inserted record. + */ + +- (long long)lastInsertRowID { + return [*adaptor lastInsertRowID]; +} + +/** + Insert an array of record objects into the database. This needs to be commited to take effect. + */ + +- (int)insertArray:(const OOArray &)objects { + int count = 0; + for ( id object in *objects ) + count = [self insert:object]; + return count; +} + +/** + Delete an array of record objects from the database. This needs to be commited to take effect. + */ + +- (int)deleteArray:(const OOArray &)objects { + int count = 0; + for ( id object in *objects ) + if ( ![object respondsToSelector:@selector(delete)] ) + count = [self delete:object]; + else { + [object delete]; + count++; + } + return count; +} + +/** + Insert the values of the record class instance at the time this method was called into the db. + (must be commited to take effect). Returns the total number of outstanding inserts/updates/deletes. + */ + +- (int)insert:(id)record { + return transaction += OOValueDictionary( kOOObject, record, kOOInsert, kCFNull, nil ); +} + +/** + Use the values of the record instance at the time this method is called in a where clause to + delete from the database when commit is called. Returns the total number of outstanding + inserts/updates/deletes. + */ + +- (int)delete:(id)record { + return transaction += OOValueDictionary( kOOObject, record, nil ); +} + +/** + Call this method if you intend to make changes to the record object and save them to the database. + This takes a snapshot of the previous values to use as a key for the update operation when "commit" + is called. Returns the total number of outstanding inserts/updates/deletes. + */ + +- (int)update:(id)record { + OOMetaData *metaData = [self tableMetaDataForClass:[record class]]; + OOValueDictionary oldValues = [metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]]; + for ( NSString *key in *metaData->tocopy ) + OO_RELEASE( oldValues[key] = [oldValues[key] copy] ); + oldValues[kOOUpdate] = OONull; + oldValues[kOOObject] = record; + return transaction += oldValues; +} + +/** + Inserts a record into the database the deletes any previous record with the same key. + This ensures the record's rowid changes if this is used by child records. + */ + +- (int)indate:(id)record { + OOMetaData *metaData = [self tableMetaDataForClass:[record class]]; + OOString sql = OOFormat( @"select rowid from %@", *metaData->tableName ); + OOArray existing = [self select:sql intoClass:nil joinFrom:record]; + int count = [self insert:record]; + for ( NSDictionary *exist in *existing ) { + OOString sql = OOFormat( @"delete from %@ where rowid = %ld", *metaData->tableName, + (long)[[exist objectForKey:@"rowid"] longLongValue] ); + transaction += OOValueDictionary( kOOExecSQL, *sql, nil ); + } + return count; +} + +/** + Inserts a record into the database unless another record with the same key column values + exists in which case it will do an update of the previous record (preserving the ROWID.) + */ + +- (int)upsert:(id)record { + OOArray existing = [self select:nil intoClass:[record class] joinFrom:record]; + if ( existing > 1 ) + OOWarn( @"-[ODatabase upsert:] Duplicate record for upsert: %@", record ); + if ( existing > 0 ) { + [self update:existing[0]]; + (*transaction[-1])[kOOObject] = record; + return transaction; + } + else + return [self insert:record]; +} + +/** + Commit all pending inserts, updates and deletes to the database. Use commitTransaction to perform + this inside a database transaction. + */ + +- (int)commit { + int commited = 0; + + for ( int i=0 ; i object = *values[kOOObject]; values -= kOOObject; + BOOL isInsert = !!~values[kOOInsert], isUpdate = !!~values[kOOUpdate]; + + OOMetaData *metaData = [self tableMetaDataForClass:[object class]]; + OOValueDictionary newValues = [metaData encode:[object dictionaryWithValuesForKeys:metaData->columns]]; + OOStringArray changedCols; + + if ( isUpdate ) { + for ( NSString *name in *metaData->columns ) + if ( ![*newValues[name] isEqual:values[name]] ) + changedCols += name; + } + else + values = newValues; + + OOString sql = isInsert ? + OOFormat( @"insert into %@ (%@) values (", *metaData->tableName, *(metaData->columns/", ") ) : + OOFormat( isUpdate ? @"update %@ set" : @"delete from %@", *metaData->tableName ); + + int nchanged = changedCols; + if ( isUpdate && nchanged == 0 ) { + OOWarn( @"%s %@ (%@)", errmsg = (char *)"-[ODatabase commit:] Update of unchanged record", *object, *(lastSQL = sql) ); + continue; + } + + for ( int i=0 ; icolumns ; i++ ) + sql += i==0 ? quote : commaQuote; + sql += ")"; + } + else + sql += [self whereClauseFor:metaData->columns values:values qualifyNulls:YES]; + +#ifdef OODEBUG_SQL + NSLog( @"-[OODatabase commit]: %@ %@", *sql, *values ); +#endif + + if ( ![*adaptor prepare:sql] ) + continue; + + if ( isUpdate ) + [*adaptor bindCols:changedCols values:newValues startingAt:1 bindNulls:YES]; + [*adaptor bindCols:metaData->columns values:values startingAt:1+nchanged bindNulls:isInsert]; + + [*adaptor bindResultsIntoInstancesOfClass:nil metaData:metaData]; + commited += updateCount; + } + + transaction = nil; + return commited; +} + +/** + Commit all pending inserts, updates, deletes to the database inside a transaction. + */ + +- (int)commitTransaction { + [self exec:"BEGIN TRANSACTION"]; + int updated = [self commit]; + return [self exec:"COMMIT"] ? updated : 0; +} + +/** + Rollback any outstanding inserts, updates, or deletes. Please note updated values + are also rolled back inside the actual record in the application as well. + */ + +- (int)rollback { + for ( NSMutableDictionary *d in *transaction ) { + OODictionary values = d; + + if ( !!~values[kOOUpdate] ) { + OORef record = ~values[kOOObject]; + OOMetaData *metaData = [self tableMetaDataForClass:[*record class]]; + +#ifndef OO_ARC + for ( NSString *name in *metaData->boxed ) + OO_RELEASE( (id)[[*record valueForKey:name] pointerValue] ); +#endif + + [*record setValuesForKeysWithDictionary:[metaData decode:values]]; + } + } + return (int)[*~transaction count]; +} + +/** + Find/create an instance of the OOMetaData class which describes a record class and its associated table. + If the table does not exist it will be created along with indexes for columns/ivars which have + upper case names for use in joins. Details of the tables parameters can be controlled by using + methods in the OOTableCustomisation protool. If it does not exist a meta table class which + prepresents OOMetaData records themselves is also created from which the list of all registered + tables can be selected. + */ + +- (OOMetaData *)tableMetaDataForClass:(Class)recordClass { + if ( !recordClass || recordClass == [OOMetaData class] ) + return [OOMetaData metaDataForClass:[OOMetaData class]]; + + OOString className = class_getName( recordClass ); + OOMetaData *metaData = tableMetaDataByClassName[className]; + + if ( !metaData ) { + metaData = [OOMetaData metaDataForClass:recordClass]; + +#ifdef OODEBUG_SQL + NSLog(@"\n%@", *metaData->createTableSQL); +#endif + + if ( metaData->tableName[0] != '_' && + [self stringForSql:"select count(*) from sqlite_master where name = '%@'", + *metaData->tableName] == "0" ) + if ( [self exec:"%@", *metaData->createTableSQL] ) + for ( NSString *idx in *metaData->indexes ) + if ( ![self exec:idx] ) + OOWarn( @"-[OOMetaData tableMetaDataForClass:] Error creating index: %@", idx ); + + tableMetaDataByClassName[className] = metaData; + } + + return metaData; +} + +@end + +#pragma mark OOAdaptor - implements all access to a particular database + +@implementation OOAdaptor + +/** + Connect to/create sqlite3 database + */ + +- (OOAdaptor *)initPath:(cOOString)path database:(OODatabase *)database { + if ( self = [super init] ) { + owner = database; + OOFile( OOFile( path ).directory() ).mkdir(); + if ( (owner->errcode = sqlite3_open( path, &db )) != SQLITE_OK ) { + OOWarn( @"-[OOAdaptor initPath:database:] Error opening database at path: %@", *path ); + return nil; + } + } + return self; +} + +/** + Prepare a sql statement after which values can be bound and results returned. + */ + +- (BOOL)prepare:(cOOString)sql { + if ( (owner->errcode = sqlite3_prepare_v2( db, owner->lastSQL = sql, -1, &stmt, 0 )) != SQLITE_OK ) + OOWarn(@"-[OOAdaptor prepare:] Could not prepare sql: \"%@\" - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) ); + return owner->errcode == SQLITE_OK; +} + +- (int)bindValue:(id)value asParameter:(int)pno { +#ifdef OODEBUG_BIND + NSLog( @"-[OOAdaptor bindValue:bindValue:] bind parameter #%d as: %@", pno, value ); +#endif + if ( !value || value == OONull ) + return sqlite3_bind_null( stmt, pno ); +#if OOSQL_THREAD_SAFE_BUT_USES_MORE_MEMORY + else if ( [value isKindOfClass:[NSString class]] ) + return sqlite3_bind_text( stmt, pno, [value UTF8String], -1, SQLITE_STATIC ); +#else + else if ( [value isKindOfClass:[NSString class]] ) { + int len = (int)[value lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + struct _str_link *str = (struct _str_link *)malloc( sizeof *str + len ); + str->next = strs; + strs = str; + [value getCString:str->str maxLength:len+1 encoding:NSUTF8StringEncoding]; + return sqlite3_bind_text( stmt, pno, str->str, len, SQLITE_STATIC ); + } +#endif + else if ( [value isKindOfClass:[NSData class]] ) + return sqlite3_bind_blob( stmt, pno, [value bytes], (int)[value length], SQLITE_STATIC ); + + const char *type = [value objCType]; + if ( type ) + switch ( type[0] ) { + case 'c': case 's': case 'i': case 'l': + case 'C': case 'S': case 'I': case 'L': + return sqlite3_bind_int( stmt, pno, [value intValue] ); + case 'q': case 'Q': + return sqlite3_bind_int64( stmt, pno, [value longLongValue] ); + case 'f': case 'd': + return sqlite3_bind_double( stmt, pno, [value doubleValue] ); + } + + OOWarn( @"-[OOAdaptor bindValue:bindValue:] Undefined type in bind of parameter #%d: %s, value: %@", pno, type, value ); + return -1; +} + +/** + Bind parameters from a prepared SQL statement. The "objCType" method is used to determine the type to bind. + */ + +- (BOOL)bindCols:(cOOStringArray)columns values:(cOOValueDictionary)values startingAt:(int)pno bindNulls:(BOOL)bindNulls { + int errcode; + for ( NSString *name in *columns ) + if ( bindNulls || *values[name] != OONull ) + if ( (errcode = [self bindValue:values[name] asParameter:pno++]) != SQLITE_OK ) + OOWarn( @"-[OOAdaptor bindCols:...] Bind failed column: %@ - %s (%d)", name, owner->errmsg = (char *)sqlite3_errmsg( db ), owner->errcode = errcode ); + return owner->errcode == SQLITE_OK; +} + +/** + Return a dictionary containing the values for a row returned by the database from a select. + These values need to be decoded using a classes metadata to set the ivar values later. + */ + +- (OOValueDictionary)valuesForNextRow { + int ncols = sqlite3_column_count( stmt ); + OOValueDictionary values; + + for ( int i=0 ; i)bindResultsIntoInstancesOfClass:(Class)recordClass metaData:(OOMetaData *)metaData { + OOArray out; + BOOL awakeFromDB = [recordClass instancesRespondToSelector:@selector(awakeFromDB)]; + + while( (owner->errcode = sqlite3_step( stmt )) == SQLITE_ROW ) { + OOValueDictionary values = [self valuesForNextRow]; + if ( recordClass ) { + id record = [[recordClass alloc] init]; + [record setValuesForKeysWithDictionary:[metaData decode:values]]; + + if ( awakeFromDB ) + [record awakeFromDB]; + + out += record; + OO_RELEASE( record ); + } + else + out += values; + } + + if ( owner->errcode != SQLITE_DONE ) + OOWarn(@"-[OOAdaptor bindResultsIntoInstancesOfClass:metaData:] Not done (bind) stmt: %@ - %s", *owner->lastSQL, owner->errmsg = (char *)sqlite3_errmsg( db ) ); + else { + owner->errcode = SQLITE_OK; + out.alloc(); + } + + while ( strs != NULL ) { + struct _str_link *next = strs->next; + free( strs ); + strs = next; + } + owner->updateCount = sqlite3_changes( db ); + sqlite3_finalize( stmt ); + return out; +} + +- (sqlite_int64)lastInsertRowID { + return sqlite3_last_insert_rowid( db ); +} + +- (void) dealloc { + sqlite3_close( db ); + OO_DEALLOC( super ); +} + +@end + +#pragma mark OOMetaData instances represent a table in the database and it's record class + +@implementation OOMetaData + +static OODictionary metaDataByClass; +static OOMetaData *tableOfTables; + ++ (NSString *)ooTableTitle { return @"Table MetaData"; } + ++ (OOMetaData *)metaDataForClass:(Class)recordClass OO_RETURNS { + if ( !tableOfTables ) + OO_RELEASE( tableOfTables = [[OOMetaData alloc] initClass:[OOMetaData class]] ); + OOMetaData *metaData = metaDataByClass[recordClass]; + if ( !metaData ) + OO_RELEASE( metaData = [[OOMetaData alloc] initClass:recordClass] ); + return metaData; +} + ++ (OOArray)selectRecordsRelatedTo:(id)record { + return [[OODatabase sharedInstance] tablesRelatedByNaturalJoinFrom:record]; +} + +- initClass:(Class)aClass { + if ( !(self = [super init]) ) + return self; + recordClass = aClass; + metaDataByClass[recordClass] = self; + recordClassName = class_getName( recordClass ); + tableTitle = [recordClass respondsToSelector:@selector(ooTableTitle)] ? + [recordClass ooTableTitle] : *recordClassName; + tableName = [recordClass respondsToSelector:@selector(ooTableName)] ? + [recordClass ooTableName] : *recordClassName; + + if ( aClass == [OOMetaData class] ) { + ivars = columns = outcols = boxed = unbox = + "tableTitle tableName recordClassName keyColumns ivars columns outcols"; + return self; + } + + createTableSQL = OOFormat( @"create table %@ (", *tableName ); + + OOArray hierarchy; + do + hierarchy += aClass; + while ( (aClass = [aClass superclass]) && aClass != [NSObject class] ); + + for ( int h=(int)hierarchy-1 ; h>=0 ; h-- ) { + aClass = (Class)hierarchy[h]; /// + Ivar *ivarInfo = class_copyIvarList( aClass, NULL ); + if ( !ivarInfo ) + continue; + + for ( int in=0 ; ivarInfo[in] ; in++ ) { + OOString columnName = ivar_getName( ivarInfo[in] ); + ivars += columnName; + + OOString type = types[columnName] = ivar_getTypeEncoding( ivarInfo[in] ), dbtype = ""; + + SEL columnSel = sel_getUid(columnName); + switch ( type[0] ) { + case 'c': case 's': case 'i': case 'l': + case 'C': case 'S': case 'I': case 'L': + case 'q': case 'Q': + dbtype = @"int"; + break; + case 'f': case 'd': + dbtype = @"real"; + break; + case '{': + static OOPattern isOORef( "=\"ref\"@\"NS" ); + if( !(type & isOORef) ) + OOWarn( @"-[OOMetaData initClass:] Invalid structure type for ivar %@ in class %@: %@", *columnName, *recordClassName, *type ); + boxed += columnName; + if ( ![recordClass instancesRespondToSelector:columnSel] ) { + unbox += columnName; + if ( [[recordClass superclass] instancesRespondToSelector:columnSel] ) + OOWarn( @"-[OOMetaData initClass:] Superclass of class %@ is providing method for column: %@", *recordClassName, *columnName ); + } + case '@': + static OOPattern isNSString( "NS(Mutable)?String\"" ), + isNSDate( "\"NSDate\"" ), isNSData( "NS(Mutable)?Data\"" ); + if ( type & isNSString ) + dbtype = @"text"; + else if ( type & isNSDate ) { + dbtype = @"real"; + dates += columnName; + } + else { + if ( !(type & isNSData) ) + archived += columnName; + blobs += columnName; + dbtype = @"blob"; + } + break; + default: + OOWarn( @"-[OOMetaData initClass:] Unknown data type '%@' in class %@", *type, *tableName ); + archived += columnName; + blobs += columnName; + dbtype = @"blob"; + break; + } + + if ( dbtype == @"text" ) + tocopy += columnName; + + if ( columnName == @"rowid" || columnName == @"ROWID" || + columnName == @"OID" || columnName == @"_ROWID_" ) { + outcols += columnName; + continue; + } + + createTableSQL += OOFormat(@"%s\n\t%@ %@ /* %@ */", + !columns?"":",", *columnName, *dbtype, *type ); + + if ( iswupper( columnName[columnName[0] != '_' ? 0 : 1] ) ) + indexes += OOFormat(@"create index %@_%@ on %@ (%@)\n", + *tableName, *columnName, + *tableName, *columnName); + + if ( class_getName( [aClass superclass] )[0] != '_' ) { + columns += columnName; + outcols += columnName; + joinableColumns += columnName; + } + } + + free( ivarInfo ); + } + + if ( [recordClass respondsToSelector:@selector(ooTableKey)] ) + createTableSQL += OOFormat( @",\n\tprimary key (%@)", + *(keyColumns = [recordClass ooTableKey]) ); + + if ( [recordClass respondsToSelector:@selector(ooConstraints)] ) + createTableSQL += OOFormat( @",\n\t%@", [recordClass ooConstraints] ); + + createTableSQL += "\n)\n"; + + if ( [recordClass respondsToSelector:@selector(ooTableSql)] ) { + createTableSQL = [recordClass ooTableSql]; + indexes = nil; + } + + tableOfTables->tablesWithNaturalJoin += recordClassName; + tablesWithNaturalJoin += recordClassName; + + for( Class other in [*metaDataByClass allKeys] ) { + OOMetaData *otherMetaData = metaDataByClass[other]; + if ( other == recordClass || otherMetaData == tableOfTables ) + continue; + + if ( [self naturalJoinTo:otherMetaData->joinableColumns] > 0 ) + tablesWithNaturalJoin += otherMetaData->recordClassName; + if ( [otherMetaData naturalJoinTo:joinableColumns] > 0 ) + otherMetaData->tablesWithNaturalJoin += recordClassName; + } + + return self; +} + +/** + Find the columns shared between two classes and that have upper case names (are indexed). + */ + +- (OOStringArray)naturalJoinTo:(cOOStringArray)to { + //NSLog( @"%@ -- %@", *columns, *to ); + OOStringArray commonColumns = columns & to; + for ( int i=0 ; i)import:(const OOArray > &)nodes intoClass:(Class)recordClass { + OOMetaData *metaData = [self metaDataForClass:recordClass]; + OOArray out; + + for ( NSMutableDictionary *dict in *nodes ) { + OOStringDictionary node = dict, values; + for ( NSString *ivar in *metaData->columns ) + values[ivar] = node[ivar]; + + id record = [[recordClass alloc] init]; + [record setValuesForKeysWithDictionary:[metaData decode:values]]; + out += record; + OO_RELEASE( record ); + } + + return out; +} + +/** + Convert a string taken from a flat file into record instances which can be inserted into the database. + */ + ++ (OOArray)import:(cOOString)string intoClass:(Class)recordClass delimiter:(cOOString)delim { + OOMetaData *metaData = [self metaDataForClass:recordClass]; + // remove escaped newlines then split by newline + OOStringArray lines = (string - @"\\\\\n") / @"\n"; + lines--; // pop last empty line + + OOArray out; + for ( int l=0 ; l values; + values[metaData->columns] = *lines[l] / delim; + + // empty columns are taken as null values + for ( NSString *key in *metaData->columns ) + if ( [*values[key] isEqualToString:@""] ) + values[key] = OONull; + + // convert description strings to NSData + for ( NSString *key in *metaData->blobs ) + OO_RELEASE( values[key] = (NSString *)[[NSData alloc] initWithDescription:values[key]] ); + + id record = [[recordClass alloc] init]; + [record setValuesForKeysWithDictionary:[metaData decode:values]]; + out += record; + OO_RELEASE( record ); + } + + return out; +} + +/** + Convert a set of records selected from the database into a string which can be saved to disk. + */ + ++ (OOString)export:(const OOArray &)array delimiter:(cOOString)delim { + OOMetaData *metaData = nil; + OOString out; + + for ( id record in *array ) { + if ( !metaData ) + metaData = [record isKindOfClass:[NSDictionary class]] ? + OONull : [self metaDataForClass:[record class]]; + + OODictionary values = metaData == OONull ? record : + *[metaData encode:[record dictionaryWithValuesForKeys:metaData->columns]]; + + OOStringArray line; + NSString *blank = @""; + for ( NSString *key in *metaData->columns ) + line += *values[key] != OONull ? [values[key] stringValue] : blank; + + out += line/delim+"\n"; + } + + return out; +} + +/** + Bind a record to a view containing elements which are to display values from the record. + The ivar number is selected by the subview's tag value and it's ".text" property if set to + the value returned record value "stringValue" for the ivar. Supports images stored as + NSData objects, UISwitches bound to boolean valuea and UITextField for alll other values. + */ + ++ (void)bindRecord:(id)record toView:(OOView *)view delegate:(id)delegate { +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + OOMetaData *metaData = [self metaDataForClass:[record class]]; + OOValueDictionary values = [metaData encode:[record dictionaryWithValuesForKeys:metaData->ivars]]; + + for ( int i=0 ; iivars ; i++ ) { + UILabel *label = (UILabel *)[view viewWithTag:1+i]; + id value = values[metaData->ivars[i]]; + + if ( [label isKindOfClass:[UIImageView class]] ) + ((UIImageView *)label).image = value != OONull ? [UIImage imageWithData:(NSData *)value] : nil; + else if ( [label isKindOfClass:[UISwitch class]] ) { + UISwitch *uiSwitch = (UISwitch *)label; + uiSwitch.on = value != OONull ? [value charValue] : 0; + if ( delegate ) + [uiSwitch addTarget:delegate action:@selector(valueChanged:) forControlEvents:UIControlEventValueChanged]; + } + else if ( [label isKindOfClass:[UIWebView class]] ) + [(UIWebView *)label loadHTMLString:value != OONull ? value : @"" baseURL:nil]; + else if ( label ) { + label.text = value != OONull ? [value stringValue] : @""; + if ( [label isKindOfClass:[UITextView class]] ) { + [(UITextView *)label setContentOffset:CGPointMake(0,0) animated:NO]; + [(UITextView *)label scrollRangeToVisible:NSMakeRange(0,0)]; + } + } + + if ( [label respondsToSelector:@selector(delegate)] ) + ((UITextField *)label).delegate = delegate; + label.hidden = NO; + + if ( (label = (UILabel *)[view viewWithTag:-1-i]) ) { + label.text = **metaData->ivars[i]; + label.hidden = NO; + } + } + + OOView *subView; + for ( int i=metaData->ivars ; (subView = [view viewWithTag:1+i]) ; i++ ) { + subView.hidden = YES; + if ( (subView = [view viewWithTag:-1-i]) ) + subView.hidden =YES; + } +#endif +} + +/** + When the delegate method fires this method should be called to update + the record with the modified value before updating the database. + */ + ++ (void)updateRecord:(id)record fromView:(OOView *)view { +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + if ( view.tag > 0 && [view respondsToSelector:@selector(text)] ) { + OOMetaData *metaData = [self metaDataForClass:[record class]]; + NSString *name = *metaData->ivars[view.tag-1]; + OOString type = metaData->types[name]; + id value = OO_RETAIN(((UITextField *)view).text ); + + if ( type[0] == '{' ) { +#ifdef OO_ARC + ooArcRetain( value ); +#endif + value = [[NSValue alloc] initWithBytes:&value objCType:@encode(id)]; +#ifndef OO_ARC + OO_RELEASE( (id)[[record valueForKey:name] pointerValue] ); +#endif + } + + [record setValue:value forKey:name]; + OO_RELEASE( value ); + } + for ( OOView *subview in [view subviews] ) + [self updateRecord:record fromView:subview]; +#endif +} + +@end + +@implementation OOView(OOExtras) + +- copyView { + NSData *archived = [NSKeyedArchiver archivedDataWithRootObject:self]; + OOView *copy = [NSKeyedUnarchiver unarchiveObjectWithData:archived]; +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED + copy.frame = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height); +#else + copy.frame = NSMakeRect(0.0, 0.0, self.frame.size.width, self.frame.size.height); +#endif + return copy; +} + +@end + +@implementation NSData(OOExtras) + +static int unhex ( unsigned char ch ) { + return ch >= 'a' ? 10 + ch - 'a' : ch >= 'A' ? 10 + ch - 'A' : ch - '0'; +} + +- initWithDescription:(NSString *)description { + NSInteger len = [description length]/2, lin = [description lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; + char *bytes = (char *)malloc( len ), *optr = bytes, *hex = (char *)malloc( lin+1 ); + [description getCString:hex maxLength:lin+1 encoding:NSUTF8StringEncoding]; + + for ( char *iptr = hex ; *iptr ; iptr+=2 ) { + if ( *iptr == '<' || *iptr == ' ' || *iptr == '>' ) + iptr--; + else + *optr++ = unhex( *iptr )*16 + unhex( *(iptr+1) ); + } + + free( hex ); + return [self initWithBytesNoCopy:bytes length:optr-bytes freeWhenDone:YES]; +} + +- (NSString *)stringValue { return [self description]; } + +@end + +@interface NSString(OOExtras) +@end +@implementation NSString(OOExtras) +- (char)charValue { return [self intValue]; } +- (char)shortValue { return [self intValue]; } +- (NSString *)stringValue { return self; } +@end + +@interface NSArray(OOExtras) +@end +@implementation NSArray(OOExtras) +- (NSString *)stringValue { + static OOReplace reformat( "/(\\s)\\s+|^\\(|\\)$|\"/$1/" ); + return &([self description] | reformat); +} +@end + +@interface NSDictionary(OOExtras) +@end +@implementation NSDictionary(OOExtras) +- (NSString *)stringValue { + static OOReplace reformat( "/(\\s)\\s+|^\\{|\\}$|\"/$1/" ); + return &([self description] | reformat); +} +@end + +#ifdef __IPHONE_OS_VERSION_MIN_REQUIRED +@interface UISwitch(OOExtras) +@end +@implementation UISwitch(OOExtras) +- (NSString *)text { return self.on ? @"1" : @"0"; } +@end +#endif +