diff --git a/AppKit/AppKit.j b/AppKit/AppKit.j index 4cd54116c..89669053f 100644 --- a/AppKit/AppKit.j +++ b/AppKit/AppKit.j @@ -84,6 +84,8 @@ @import "CPRadio.j" @import "CPResponder.j" @import "CPRuleEditor.j" +@import "CPRulerMarker.j" +@import "CPRulerView.j" @import "CPScroller.j" @import "CPScrollView.j" @import "CPSearchField.j" @@ -99,7 +101,9 @@ @import "CPTableView.j" @import "CPTabView.j" @import "CPText.j" +@import "CPTextAttachment.j" @import "CPTextField.j" +@import "CPTextList.j" @import "CPTextView.j" @import "CPTokenField.j" @import "CPToolbar.j" diff --git a/AppKit/CPAlert.j b/AppKit/CPAlert.j index e95225562..60588fe04 100644 --- a/AppKit/CPAlert.j +++ b/AppKit/CPAlert.j @@ -137,7 +137,8 @@ var bottomHeight = 71; @delegate -(void)alertDidEnd:(CPAlert)theAlert returnCode:(int)returnCode; Called when the user dismisses the alert by clicking one of the buttons. @param theAlert the alert panel that the user dismissed - @param returnCode the index of the button that the user clicked (starting from 0, + @param returnCode the tag of the button that the user clicked. Unless a button's + tag has been changed, it is the button's index (starting from 0, representing the first button added to the alert which appears on the right, 1 representing the next button to the left and so on) */ diff --git a/AppKit/CPApplication.j b/AppKit/CPApplication.j index 656351f7e..baee91219 100644 --- a/AppKit/CPApplication.j +++ b/AppKit/CPApplication.j @@ -37,6 +37,8 @@ @import "CPWindowController.j" @import "_CPPopoverWindow.j" +@global CPWindowObjectList + @typedef CPModalSession var CPMainCibFile = @"CPMainCibFile", @@ -121,6 +123,9 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0, CPPanel _aboutPanel; + CPMenu _windowsMenu; + CPMenuItem _windowsMenuSeparator; + CPThemeBlend _themeBlend @accessors(property=themeBlend); // OS behavior @@ -248,9 +253,10 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0, // At this point we clear the window.status to eliminate Safari's "Cancelled" error message // The message shouldn't be displayed, because only an XHR is cancelled, but it is a usability issue. // We do it here so that applications can change it in willFinish or didFinishLaunching -#if PLATFORM(DOM) - window.status = " "; -#endif + if (CPDOMAvailable) + { + window.status = " "; + } // We also want to set the default cursor on the body, so that buttons and things don't have an iBeam [[CPCursor arrowCursor] set]; @@ -280,9 +286,10 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0, var needsUntitled = !!_documentController, URLStrings = nil; -#if PLATFORM(DOM) - URLStrings = window.cpOpeningURLStrings && window.cpOpeningURLStrings(); -#endif + if (CPDOMAvailable) + { + URLStrings = window.cpOpeningURLStrings && window.cpOpeningURLStrings(); + } var index = 0, count = [URLStrings count]; @@ -375,7 +382,7 @@ var CPApplicationDelegate_applicationShouldTerminate_ = 1 << 0, to the following respective keys in your info.plist file.
-    CPBundleName
+    CPBundleDisplayName, or CPBundleName when there is no display name
     CPApplicationIcon (through a call to -applicationIconImage, see documentation for that method for more details)
     CPBundleVersion
     CPBundleShortVersionString
@@ -389,7 +396,7 @@ var CPApplicationDelegate_applicationShouldTerminate_           = 1 << 0,
     if (!_aboutPanel)
     {
         var mainInfo = [[CPBundle mainBundle] infoDictionary],
-            applicationTitle = [options objectForKey:"ApplicationName"] || [mainInfo objectForKey:@"CPBundleName"],
+            applicationTitle = [options objectForKey:"ApplicationName"] || [self _applicationName],
             applicationIcon = [options objectForKey:@"ApplicationIcon"] || [self applicationIconImage],
             version = [options objectForKey:@"Version"] || [mainInfo objectForKey:@"CPBundleVersion"],
             applicationVersion = [options objectForKey:@"ApplicationVersion"] || [mainInfo objectForKey:@"CPBundleShortVersionString"],
@@ -734,11 +741,10 @@ var CPApplicationDelegate_applicationShouldTerminate_           = 1 << 0,
 */
 - (CPArray)orderedWindows
 {
-#if PLATFORM(DOM)
-    return CPWindowObjectList();
-#else
+    if (CPDOMAvailable)
+        return CPWindowObjectList();
+
     return [];
-#endif
 }
 
 - (void)hide:(id)aSender
@@ -775,11 +781,238 @@ var CPApplicationDelegate_applicationShouldTerminate_           = 1 << 0,
 
         if ([CPPlatform supportsNativeMainMenu])
             window.cpSetMainMenu([self menu]);
+
+        // The new main menu may contain a different Windows menu, so find it again and list
+        // the windows that were ordered in before the main menu was set.
+        _windowsMenu = nil;
+        _windowsMenuSeparator = nil;
+
+        for (var i = 0, count = [_windows count]; i < count; i++)
+        {
+            var aWindow = _windows[i];
+
+            // _windows is indexed by window number and holds a placeholder at index 0.
+            if ([aWindow isKindOfClass:[CPWindow class]])
+                [aWindow _updateWindowsMenuItem];
+        }
     }
     else
         [aMenu _setMenuName:@"CPMainMenu"];
 }
 
+/*
+    @ignore
+    Returns the application's name as users see it: CPBundleDisplayName from the main bundle's
+    Info.plist, or CPBundleName when there is no display name.
+*/
+- (CPString)_applicationName
+{
+    var mainBundle = [CPBundle mainBundle];
+
+    return [mainBundle objectForInfoDictionaryKey:@"CPBundleDisplayName"] || [mainBundle objectForInfoDictionaryKey:@"CPBundleName"];
+}
+
+// MARK: Managing the Windows Menu
+
+/*!
+    Returns the application's Windows menu, which lists the application's windows.
+
+    Unless a menu has been set with \c -setWindowsMenu:, this is the submenu of the main menu
+    named \c _CPWindowsMenu. nib2cib gives that name to the menu marked as the Windows menu in
+    Interface Builder.
+    @return the Windows menu, or \c nil if the application has none
+*/
+- (CPMenu)windowsMenu
+{
+    if (!_windowsMenu)
+        _windowsMenu = [[self mainMenu] _menuWithName:@"_CPWindowsMenu"];
+
+    return _windowsMenu;
+}
+
+/*!
+    Makes \c aMenu the application's Windows menu, and moves the items for the listed windows
+    from the previous Windows menu to it.
+    @param aMenu the new Windows menu
+*/
+- (void)setWindowsMenu:(CPMenu)aMenu
+{
+    var previousMenu = [self windowsMenu];
+
+    if (previousMenu === aMenu)
+        return;
+
+    var listedWindows = [self _windowsInWindowsMenu];
+
+    [listedWindows makeObjectsPerformSelector:@selector(_removeFromWindowsMenu)];
+
+    _windowsMenu = aMenu;
+    _windowsMenuSeparator = nil;
+
+    [listedWindows makeObjectsPerformSelector:@selector(_updateWindowsMenuItem)];
+}
+
+/*!
+    Adds an item for \c aWindow to the Windows menu. The item makes the window key and orders it
+    front when chosen. Does nothing if the menu already has an item for \c aWindow.
+    @param aWindow the window to list
+    @param aString the item's title
+    @param isFilename if \c YES, \c aString is a file path, shown as the file name followed by its
+    directory
+*/
+- (void)addWindowsItem:(CPWindow)aWindow title:(CPString)aString filename:(BOOL)isFilename
+{
+    var menu = [self windowsMenu];
+
+    if (!menu || [self _windowsMenuItemForWindow:aWindow])
+        return;
+
+    // Separate the window list from the menu's own items.
+    if (!_windowsMenuSeparator && [menu numberOfItems] > 0)
+    {
+        _windowsMenuSeparator = [CPMenuItem separatorItem];
+        [menu addItem:_windowsMenuSeparator];
+    }
+
+    var item = [[CPMenuItem alloc] initWithTitle:[self _windowsMenuTitle:aString filename:isFilename]
+                                          action:@selector(makeKeyAndOrderFront:)
+                                   keyEquivalent:@""];
+
+    [item setTarget:aWindow];
+    [menu addItem:item];
+
+    [self updateWindowsItem:aWindow];
+}
+
+/*!
+    Changes the title of the Windows menu item for \c aWindow, adding the item if the menu has
+    none for it.
+    @param aWindow the listed window
+    @param aString the item's new title
+    @param isFilename if \c YES, \c aString is a file path, shown as the file name followed by its
+    directory
+*/
+- (void)changeWindowsItem:(CPWindow)aWindow title:(CPString)aString filename:(BOOL)isFilename
+{
+    var item = [self _windowsMenuItemForWindow:aWindow];
+
+    if (item)
+        [item setTitle:[self _windowsMenuTitle:aString filename:isFilename]];
+    else
+        [self addWindowsItem:aWindow title:aString filename:isFilename];
+}
+
+/*!
+    Removes the Windows menu item for \c aWindow, and removes the separator above the window
+    list when the last window item is removed.
+    @param aWindow the window to remove from the list
+*/
+- (void)removeWindowsItem:(CPWindow)aWindow
+{
+    var item = [self _windowsMenuItemForWindow:aWindow];
+
+    if (!item)
+        return;
+
+    var menu = [self windowsMenu];
+
+    [menu removeItem:item];
+
+    if (_windowsMenuSeparator && ![[self _windowsInWindowsMenu] count])
+    {
+        [menu removeItem:_windowsMenuSeparator];
+        _windowsMenuSeparator = nil;
+    }
+}
+
+/*!
+    Updates the state of the Windows menu item for \c aWindow: a check mark for the main window,
+    the mixed state for a miniaturized window, and no mark otherwise.
+    @param aWindow the listed window
+*/
+- (void)updateWindowsItem:(CPWindow)aWindow
+{
+    var item = [self _windowsMenuItemForWindow:aWindow];
+
+    if (!item)
+        return;
+
+    if ([aWindow isMiniaturized])
+        [item setState:CPMixedState];
+    else
+        [item setState:aWindow === [self mainWindow] ? CPOnState : CPOffState];
+}
+
+/*!
+    Orders front every window listed in the Windows menu that is not miniaturized, keeping their
+    order relative to each other.
+    @param aSender the object requesting this
+*/
+- (void)arrangeInFront:(id)aSender
+{
+    var listedWindows = [self _windowsInWindowsMenu],
+        orderedWindows = [self orderedWindows],
+        index = [orderedWindows count];
+
+    // orderedWindows lists front to back, so order front from the back.
+    while (index--)
+    {
+        var aWindow = orderedWindows[index];
+
+        if ([listedWindows containsObjectIdenticalTo:aWindow] && ![aWindow isMiniaturized])
+            [aWindow orderFront:aSender];
+    }
+}
+
+/* @ignore */
+- (CPMenuItem)_windowsMenuItemForWindow:(CPWindow)aWindow
+{
+    var items = [[self windowsMenu] itemArray],
+        count = [items count];
+
+    for (var i = 0; i < count; i++)
+    {
+        var item = items[i];
+
+        if ([item target] === aWindow && [item action] === @selector(makeKeyAndOrderFront:))
+            return item;
+    }
+
+    return nil;
+}
+
+/* @ignore */
+- (CPArray)_windowsInWindowsMenu
+{
+    var items = [[self windowsMenu] itemArray],
+        count = [items count],
+        listedWindows = [];
+
+    for (var i = 0; i < count; i++)
+    {
+        var item = items[i];
+
+        if ([item action] === @selector(makeKeyAndOrderFront:) && [[item target] isKindOfClass:[CPWindow class]])
+            listedWindows.push([item target]);
+    }
+
+    return listedWindows;
+}
+
+/* @ignore */
+- (CPString)_windowsMenuTitle:(CPString)aString filename:(BOOL)isFilename
+{
+    if (!isFilename)
+        return aString;
+
+    var directory = [aString stringByDeletingLastPathComponent];
+
+    if (![directory length])
+        return [aString lastPathComponent];
+
+    return [aString lastPathComponent] + @" — " + directory;
+}
+
 /*!
     Opens the shared color panel.
     @param aSender
@@ -1349,22 +1582,22 @@ _CPRunModalLoop = function(anEvent)
 
 function CPApplicationMain(args, namedArgs)
 {
-
-#if PLATFORM(DOM)
-    // hook to allow recorder, etc to manipulate things before starting AppKit
-    if (window.parent !== window && typeof window.parent._childAppIsStarting === "function")
+    if (CPDOMAvailable)
     {
-        try
+        // hook to allow recorder, etc to manipulate things before starting AppKit
+        if (window.parent !== window && typeof window.parent._childAppIsStarting === "function")
         {
-            window.parent._childAppIsStarting(window);
-        }
-        catch(err)
-        {
-            // This could happen if we're in an iframe without access to the parent frame.
-            CPLog.warn("Failed to call parent frame's _childAppIsStarting().");
+            try
+            {
+                window.parent._childAppIsStarting(window);
+            }
+            catch(err)
+            {
+                // This could happen if we're in an iframe without access to the parent frame.
+                CPLog.warn("Failed to call parent frame's _childAppIsStarting().");
+            }
         }
     }
-#endif
 
     var mainBundle = [CPBundle mainBundle],
         principalClass = [mainBundle principalClass];
diff --git a/AppKit/CPColor.j b/AppKit/CPColor.j
index 1d1199350..a3408560d 100644
--- a/AppKit/CPColor.j
+++ b/AppKit/CPColor.j
@@ -563,9 +563,10 @@ var cachedBlackColor,
 {
     // Fix to avoid a problem when compiling a theme (missing the alpha component as theme compiler doesn't have CSS rgba capability)
     var hasAlpha = YES;
-#if PLATFORM(DOM)
-    hasAlpha = CPFeatureIsCompatible(CPCSSRGBAFeature) && _components[3] != 1.0;
-#endif
+    if (CPDOMAvailable)
+    {
+        hasAlpha = CPFeatureIsCompatible(CPCSSRGBAFeature) && _components[3] != 1.0;
+    }
 
     _cssString = (hasAlpha ? "rgba(" : "rgb(") +
         parseInt(_components[0] * 255.0) + ", " +
@@ -1001,19 +1002,16 @@ url("data:image/png;base64,BASE64ENCODEDDATA")  // if there is a pattern image
 
 + (void)restorePreviousCSSState:(CPArrayRef)aPreviousStateRef forDOMElement:(DOMElement)aDOMElement
 {
-#if PLATFORM(DOM)
     var aPreviousState = @deref(aPreviousStateRef);
 
     for (var i = 0, count = aPreviousState.length; i < count; i++)
         aDOMElement.style[aPreviousState[i][0]] = aPreviousState[i][1];
 
     @deref(aPreviousStateRef) = @[];
-#endif
 }
 
 - (DOMElement)applyCSSColorForView:(CPView)aView onDOMElement:(DOMElement)aDOMElement styleNode:(DOMElement)aStyleNode previousState:(CPArrayRef)aPreviousStateRef
 {
-#if PLATFORM(DOM)
     var aPreviousState = @deref(aPreviousStateRef);
 
     [_cssDictionary enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop)
@@ -1086,7 +1084,6 @@ url("data:image/png;base64,BASE64ENCODEDDATA")  // if there is a pattern image
     @deref(aPreviousStateRef) = aPreviousState;
 
     return aStyleNode;
-#endif
 }
 
 @end
@@ -1494,7 +1491,7 @@ var patternColorsFromPattern = function(pattern, attributes, imageFactory)
         else
         {
             numParts = 3;
-            isVertical = orientation === PatternIsVertical;
+            isVertical = orientation === CPColorPatternIsVertical;
 
             if (isVertical)
             {
diff --git a/AppKit/CPColorPicker.j b/AppKit/CPColorPicker.j
index 6ee6ed8cc..81e9db00b 100644
--- a/AppKit/CPColorPicker.j
+++ b/AppKit/CPColorPicker.j
@@ -23,6 +23,7 @@
 @import 
 
 @import "CPView.j"
+@import "CPCompatibility.j"
 
 @class CPSlider
 @class CPColorPanel
@@ -235,24 +236,25 @@
 {
     if (self = [super initWithFrame:aFrame])
     {
-#if PLATFORM(DOM)
-        var path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel.png"];
+        if (CPDOMAvailable)
+        {
+            var path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel.png"];
 
-        _wheelImage = new Image();
-        _wheelImage.src = path;
-        _wheelImage.style.position = "absolute";
+            _wheelImage = new Image();
+            _wheelImage.src = path;
+            _wheelImage.style.position = "absolute";
 
-        path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel_black.png"];
+            path = [[CPBundle bundleForClass:CPColorPicker] pathForResource:@"wheel_black.png"];
 
-        _blackWheelImage = new Image();
-        _blackWheelImage.src = path;
-        _blackWheelImage.style.opacity = "0";
-        _blackWheelImage.style.filter = "alpha(opacity=0)";
-        _blackWheelImage.style.position = "absolute";
+            _blackWheelImage = new Image();
+            _blackWheelImage.src = path;
+            _blackWheelImage.style.opacity = "0";
+            _blackWheelImage.style.filter = "alpha(opacity=0)";
+            _blackWheelImage.style.position = "absolute";
 
-        _DOMElement.appendChild(_wheelImage);
-        _DOMElement.appendChild(_blackWheelImage);
-#endif
+            _DOMElement.appendChild(_wheelImage);
+            _DOMElement.appendChild(_blackWheelImage);
+        }
 
         [self setWheelSize:aFrame.size];
 
@@ -272,10 +274,10 @@
 
 - (void)setWheelBrightness:(float)brightness
 {
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
+
     _blackWheelImage.style.opacity = 1.0 - brightness;
     _blackWheelImage.style.filter = "alpha(opacity=" + (1.0 - brightness) * 100 + ")"
-#endif
 }
 
 - (void)setFrameSize:(CGSize)aSize
@@ -288,21 +290,22 @@
 {
     var min = MIN(aSize.width, aSize.height);
 
-#if PLATFORM(DOM)
-    _blackWheelImage.style.width = min;
-    _blackWheelImage.style.height = min;
-    _blackWheelImage.width = min;
-    _blackWheelImage.height = min;
-    _blackWheelImage.style.top = (aSize.height - min) / 2.0 + "px";
-    _blackWheelImage.style.left = (aSize.width - min) / 2.0 + "px";
+    if (CPDOMAvailable)
+    {
+        _blackWheelImage.style.width = min;
+        _blackWheelImage.style.height = min;
+        _blackWheelImage.width = min;
+        _blackWheelImage.height = min;
+        _blackWheelImage.style.top = (aSize.height - min) / 2.0 + "px";
+        _blackWheelImage.style.left = (aSize.width - min) / 2.0 + "px";
 
-    _wheelImage.style.width = min;
-    _wheelImage.style.height = min;
-    _wheelImage.width = min;
-    _wheelImage.height = min;
-    _wheelImage.style.top = (aSize.height - min) / 2.0 + "px";
-    _wheelImage.style.left = (aSize.width - min) / 2.0 + "px";
-#endif
+        _wheelImage.style.width = min;
+        _wheelImage.style.height = min;
+        _wheelImage.width = min;
+        _wheelImage.height = min;
+        _wheelImage.style.top = (aSize.height - min) / 2.0 + "px";
+        _wheelImage.style.left = (aSize.width - min) / 2.0 + "px";
+    }
 
     _radius = min / 2.0;
 
diff --git a/AppKit/CPComboBox.j b/AppKit/CPComboBox.j
index 2c31d7cc9..abdfc38e2 100644
--- a/AppKit/CPComboBox.j
+++ b/AppKit/CPComboBox.j
@@ -846,7 +846,6 @@ var CPComboBoxTextSubview = @"text",
 
     if (!shouldResign)
     {
-#if PLATFORM(DOM)
         // In FireFox this needs to be done in setTimeout, otherwise there is no caret
         // We have to save the input element now, when we lose focus it will change.
         var element = [self _inputElement];
@@ -861,7 +860,6 @@ var CPComboBoxTextSubview = @"text",
             [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
 
         } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
-#endif
 
         return NO;
     }
diff --git a/AppKit/CPCompatibility.j b/AppKit/CPCompatibility.j
index 405626ca8..f4c185ef6 100644
--- a/AppKit/CPCompatibility.j
+++ b/AppKit/CPCompatibility.j
@@ -24,6 +24,29 @@
 @import "CPPlatform.j"
 
 
+/*
+    CPDOMAvailable is the authoritative runtime predicate for DOM capability.
+
+    It replaces the former `#if PLATFORM(DOM)` preprocessor conditional, which
+    selected code at build time between the browser (DOM) and CommonJS (Node)
+    targets. It is a CAPABILITY test: YES only when a
+    functioning document object with createElement exists.
+
+    The predicate subsumes all direct local probing for the presence of a DOM.
+
+        YES  - browsers (the only deployment target)
+        YES  - any environment providing a working DOM (e.g. jsdom), where DOM
+               operations genuinely function
+        NO   - CommonJS/Node without a DOM (legacy tooling: theme blending, nib2cib)
+
+    Rules for consumers:
+        - Platform-conditional code MUST test CPDOMAvailable.
+        - Do NOT reference `document` (or test for it) anywhere else in AppKit;
+          this declaration is the single environment peek.
+        - The preprocessor form must not return: there is no preprocessor.
+*/
+CPDOMAvailable = (typeof document !== "undefined") && document && (typeof document.createElement === "function");
+
 // Browser Engines
 CPUnknownBrowserEngine                  = 0;
 CPGeckoBrowserEngine                    = 1 << 0;
@@ -55,8 +78,6 @@ CPJavaScriptClipboardAccessFeature      = 12;
 CPJavaScriptCanvasDrawFeature           = 13;
 CPJavaScriptCanvasTransformFeature      = 14;
 
-CPVMLFeature                            = 15;
-
 CPJavaScriptRemedialKeySupport          = 16;
 CPJavaScriptShadowFeature               = 20;
 
@@ -133,7 +154,6 @@ else if (typeof window !== "undefined" && (window.attachEvent || (!(window.Activ
     PLATFORM_ENGINE |= CPInternetExplorerBrowserEngine;
 
     // Features we can only be sure of with IE (no known independent tests)
-    PLATFORM_FEATURES[CPVMLFeature] = YES;
     PLATFORM_FEATURES[CPJavaScriptRemedialKeySupport] = YES;
     PLATFORM_FEATURES[CPJavaScriptShadowFeature] = YES;
 
@@ -388,7 +408,6 @@ function CPBrowserStyleProperty(aProperty)
     {
         var r = nil;
 
-#if PLATFORM(DOM)
         var testElement = document.createElement('div');
 
         switch (lowerProperty)
@@ -455,7 +474,6 @@ function CPBrowserStyleProperty(aProperty)
 
                 break;
         }
-#endif
 
         PLATFORM_STYLE_JS_PROPERTIES[lowerProperty] = r;
     }
diff --git a/AppKit/CPControl.j b/AppKit/CPControl.j
index 1dec17504..8cec6d209 100644
--- a/AppKit/CPControl.j
+++ b/AppKit/CPControl.j
@@ -1043,7 +1043,8 @@ var CPControlBlackColor = [CPColor blackColor];
     _baseWritingDirection = writingDirection;
     [self didChangeValueForKey:@"baseWritingDirection"];
 
-#if PLATFORM(DOM)
+    if (CPDOMAvailable)
+    {
 
     var style;
 
@@ -1066,7 +1067,7 @@ var CPControlBlackColor = [CPColor blackColor];
     }
 
     _DOMElement.style.direction = style;
-#endif
+    }
 }
 
 @end
diff --git a/AppKit/CPCookie.j b/AppKit/CPCookie.j
index 1b05644c1..c63901f44 100644
--- a/AppKit/CPCookie.j
+++ b/AppKit/CPCookie.j
@@ -22,6 +22,7 @@
 
 @import 
 @import 
+@import "CPCompatibility.j"
 
 
 /*!
@@ -95,32 +96,37 @@
     else
         domain = "";
 
-#if PLATFORM(DOM)
-    document.cookie = _cookieName + "=" + value + expires + "; path=/" + domain;
-#else
-    _cookieValue = value;
-    _expires = expires;
-#endif
+    if (CPDOMAvailable)
+    {
+        document.cookie = _cookieName + "=" + value + expires + "; path=/" + domain;
+    }
+    else
+    {
+        _cookieValue = value;
+        _expires = expires;
+    }
 }
 
 /* @ignore */
 - (CPString)_readCookieValue
 {
-#if PLATFORM(DOM)
-    var nameEQ = _cookieName + "=",
-        ca = document.cookie.split(';');
-
-    for (var i = 0; i < ca.length; i++)
+    if (CPDOMAvailable)
     {
-        var c = ca[i];
+        var nameEQ = _cookieName + "=",
+            ca = document.cookie.split(';');
 
-        while (c.charAt(0) == ' ')
-            c = c.substring(1, c.length);
+        for (var i = 0; i < ca.length; i++)
+        {
+            var c = ca[i];
 
-        if (c.indexOf(nameEQ) == 0)
-            return c.substring(nameEQ.length, c.length);
+            while (c.charAt(0) == ' ')
+                c = c.substring(1, c.length);
+
+            if (c.indexOf(nameEQ) == 0)
+                return c.substring(nameEQ.length, c.length);
+        }
     }
-#endif
+
     return "";
 }
 
diff --git a/AppKit/CPCursor.j b/AppKit/CPCursor.j
index 7a3907f26..f7595a106 100644
--- a/AppKit/CPCursor.j
+++ b/AppKit/CPCursor.j
@@ -135,9 +135,7 @@ CPCursorPlatformBoth    = 3;
     
     currentCursor = self;
 
-#if PLATFORM(DOM)
     [[self class] _setCursorCSS:_cssString];
-#endif
 }
 
 - (void)mouseEntered:(CPEvent)event
@@ -155,12 +153,10 @@ CPCursorPlatformBoth    = 3;
 
 + (void)_setCursorCSS:(CPString)aString
 {
-#if PLATFORM(DOM)
     var platformWindows = [[CPPlatformWindow visiblePlatformWindows] allObjects];
 
     for (var i = 0, count = [platformWindows count]; i < count; i++)
         platformWindows[i]._DOMBodyElement.style.cursor = aString;
-#endif
 }
 
 // Internal method that is used to return the system cursors.  Caches the system cursors for performance.
@@ -217,7 +213,6 @@ CPCursorPlatformBoth    = 3;
 
 + (BOOL)_nativeCursorExists:(CPString)cursorCSSName
 {
-#if PLATFORM(DOM)
 
     // FIXME: Trick until FF/Win & Chrome/Win correctly implement context-menu cursor
     // They will answer that they implement it but they actually don't
@@ -240,7 +235,6 @@ CPCursorPlatformBoth    = 3;
 
         return doesExist;
     }
-#endif
 
     return NO;
 }
diff --git a/AppKit/CPDatePicker/CPDatePicker.j b/AppKit/CPDatePicker/CPDatePicker.j
index 1cf4ca6cf..1bee274aa 100644
--- a/AppKit/CPDatePicker/CPDatePicker.j
+++ b/AppKit/CPDatePicker/CPDatePicker.j
@@ -30,6 +30,7 @@
 @import 
 @import 
 @import 
+@import "CPCompatibility.j"
 
 @class CPStepper
 @class CPApp
@@ -258,10 +259,11 @@ CPEraDatePickerElementFlag              = 0x0100;
     [_datePickerComponent setDatePickerElements:_datePickerElements];
 
     // FIXME: Don't know why but next line will cause theme compilation to fail...
-    // Workaround: added "if PLATFORM(DOM)"
-#if PLATFORM(DOM)
-    [_datePickerComponent setEnabled:[self isEnabled]];
-#endif
+    // Workaround: guarded with CPDOMAvailable (was: "if PLATFORM(DOM)")
+    if (CPDOMAvailable)
+    {
+        [_datePickerComponent setEnabled:[self isEnabled]];
+    }
 
     if (_isTextual)
         // We need to transmit text color to the text field version (Cocoa doesn't permit adapting the calendar view text color)
diff --git a/AppKit/CPDatePicker/_CPDatePickerClock.j b/AppKit/CPDatePicker/_CPDatePickerClock.j
index a756209e9..419b99ee8 100644
--- a/AppKit/CPDatePicker/_CPDatePickerClock.j
+++ b/AppKit/CPDatePicker/_CPDatePickerClock.j
@@ -24,11 +24,15 @@
 @import 
 @import "CPView.j"
 @import "CPTextField.j"
-@import "CPImageView.j"
 @import "CPImage.j"
+@import "CALayer.j"
+@import "CPCompatibility.j"
 
 @class _CPCibCustomResource
 @class CPDatePicker
+@class HandImageLayer
+@class HoursLayer
+@class HandLayer
 
 @global CPHourMinuteSecondDatePickerElementFlag
 @global CPTextFieldAndStepperDatePickerStyle
@@ -44,19 +48,15 @@ _CPDatePickerClockSeconds = 3;
 @implementation _CPDatePickerClock : CPControl
 {
     BOOL                    _isEnabled;
-    
-    // Pure DOM Views
-    CPImageView             _hourHandView;
-    CPImageView             _minuteHandView;
-    CPImageView             _secondHandView;
-    CPImageView             _middleHandView;
-    
-    CPArray                 _hourLabels;
+    HoursLayer              _rootLayer;
+    HandLayer               _hourHandLayer;
+    HandLayer               _minuteHandLayer;
+    HandLayer               _secondHandLayer;
+    CALayer                 _middleHandLayer;
+    CPDatePicker            _datePicker;
     CPTextField             _PMAMTextField;
 
-    CPDatePicker            _datePicker;
-
-    CPView                  _currentHandView;
+    CALayer                 _currentHandLayer;
     _CPDatePickerClockHand  _currentHand;
     CPInteger               _currentRepresentedValue;
     float                   _currentValueShift;
@@ -68,14 +68,10 @@ _CPDatePickerClockSeconds = 3;
     CPInteger               _representedSeconds;
     BOOL                    _representedHourIsPM;
 
-    // Angles for Hit-Testing
-    float                   _hourAngle;
-    float                   _minuteAngle;
-    float                   _secondAngle;
-
-    CPInteger               _datePickerElements @accessors(getter=datePickerElements);
+    CPInteger                _datePickerElements         @accessors(getter=datePickerElements);
 }
 
+
 // MARK: -
 // MARK: Init methods
 
@@ -86,53 +82,69 @@ _CPDatePickerClockSeconds = 3;
         _datePicker         = aDatePicker;
         _datePickerElements = [_datePicker datePickerElements];
         _trackingHand       = NO;
-        _isEnabled          = YES;
 
-        // 1. Initialize AM/PM Label
         _PMAMTextField = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
-        [_PMAMTextField setAlignment:CPCenterTextAlignment];
-        [_PMAMTextField setVerticalAlignment:CPCenterVerticalTextAlignment];
+
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-font" inState:CPThemeStateNormal] forThemeAttribute:@"font" inState:CPThemeStateNormal];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-color" inState:CPThemeStateNormal] forThemeAttribute:@"text-color" inState:CPThemeStateNormal];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-shadow-color" inState:CPThemeStateNormal] forThemeAttribute:@"text-shadow-color" inState:CPThemeStateNormal];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-shadow-offset" inState:CPThemeStateNormal] forThemeAttribute:@"text-shadow-offset" inState:CPThemeStateNormal];
+
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-font" inState:CPThemeStateDisabled] forThemeAttribute:@"font" inState:CPThemeStateDisabled];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-color" inState:CPThemeStateDisabled] forThemeAttribute:@"text-color" inState:CPThemeStateDisabled];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-shadow-color" inState:CPThemeStateDisabled] forThemeAttribute:@"text-shadow-color" inState:CPThemeStateDisabled];
+        [_PMAMTextField setValue:[_datePicker valueForThemeAttribute:@"clock-text-shadow-offset" inState:CPThemeStateDisabled] forThemeAttribute:@"text-shadow-offset" inState:CPThemeStateDisabled];
+
         [self addSubview:_PMAMTextField];
 
-        // 2. Initialize Number Labels (1 to 12)
-        _hourLabels = [CPArray array];
+        var middleHandSize = [_datePicker valueForThemeAttribute:@"middle-hand-size"],
+            minuteHandSize = [_datePicker valueForThemeAttribute:@"minute-hand-size"],
+            hourHandSize   = [_datePicker valueForThemeAttribute:@"hour-hand-size"],
+            secondHandSize = [_datePicker valueForThemeAttribute:@"second-hand-size"];
 
-        for (var i = 1; i <= 12; i++)
-        {
-            var label = [[CPTextField alloc] initWithFrame:CGRectMakeZero()];
-            [label setStringValue:String(i)];
-            [label setAlignment:CPCenterTextAlignment];
-            [label setVerticalAlignment:CPCenterVerticalTextAlignment];
-            [self addSubview:label];
-            [_hourLabels addObject:label];
-        }
+        // We use layer to make the rotation possible
+        _hourHandLayer = [[HandLayer alloc] initWithSize:hourHandSize];
+        [_hourHandLayer setBounds:CGRectMake(0, 0, aFrame.size.width, aFrame.size.height)];
+        [_hourHandLayer setAnchorPoint:CGPointMakeZero()];
+        [_hourHandLayer setPosition:CGPointMake(0.0, 0.0)];
 
-        // 3. Initialize Hand Views
-        _hourHandView   = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
-        _minuteHandView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
-        _secondHandView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
-        _middleHandView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()];
+        _minuteHandLayer = [[HandLayer alloc] initWithSize:minuteHandSize];
+        [_minuteHandLayer setBounds:CGRectMake(0, 0, aFrame.size.width, aFrame.size.height)];
+        [_minuteHandLayer setAnchorPoint:CGPointMakeZero()];
+        [_minuteHandLayer setPosition:CGPointMake(0.0, 0.0)];
 
-        // Ensure images stretch accurately across the bounds of the image view
-        [_hourHandView setImageScaling:CPImageScaleAxesIndependently];
-        [_minuteHandView setImageScaling:CPImageScaleAxesIndependently];
-        [_secondHandView setImageScaling:CPImageScaleAxesIndependently];
-        [_middleHandView setImageScaling:CPImageScaleAxesIndependently];
+        _secondHandLayer = [[HandLayer alloc] initWithSize:secondHandSize];
+        [_secondHandLayer setBounds:CGRectMake(0, 0, aFrame.size.width, aFrame.size.height)];
+        [_secondHandLayer setAnchorPoint:CGPointMakeZero()];
+        [_secondHandLayer setPosition:CGPointMake(0.0, 0.0)];
 
-        // 4. Add subviews in correct Z-Order
-        [self addSubview:_hourHandView];
-        [self addSubview:_minuteHandView];
+        _middleHandLayer = [[HandLayer alloc] initWithSize:middleHandSize];
+        [_middleHandLayer setBounds:CGRectMake(0, 0, aFrame.size.width, aFrame.size.height)];
+        [_middleHandLayer setAnchorPoint:CGPointMakeZero()];
+        [_middleHandLayer setPosition:CGPointMake(0.0, 0.0)];
+
+        _rootLayer = [[HoursLayer alloc] init];
+        [self setWantsLayer:YES];
+        [self setLayer:_rootLayer];
+
+        [self _initHands];
+
+        [_rootLayer addSublayer:_hourHandLayer];
+        [_rootLayer addSublayer:_minuteHandLayer];
 
         if ([_datePicker valueForThemeAttribute:@"clock-second-hand-over"])
         {
-            [self addSubview:_middleHandView];
-            [self addSubview:_secondHandView];
+            [_rootLayer addSublayer:_middleHandLayer];
+            [_rootLayer addSublayer:_secondHandLayer];
         }
         else
         {
-            [self addSubview:_secondHandView];
-            [self addSubview:_middleHandView];
+            [_rootLayer addSublayer:_secondHandLayer];
+            [_rootLayer addSublayer:_middleHandLayer];
         }
+
+        [_rootLayer setDrawsHours:[_datePicker valueForThemeAttribute:@"clock-draws-hours"]];
+        [_rootLayer setNeedsDisplay];
     }
 
     return self;
@@ -140,58 +152,60 @@ _CPDatePickerClockSeconds = 3;
 
 - (void)_initHands
 {
-    // FIX: Using 'duplicate' prevents CPImageViews from stealing the DOM element from each other!
-    [_middleHandView setImage:[[_datePicker currentValueForThemeAttribute:@"middle-hand-image"] duplicate]];
-    [_hourHandView   setImage:[[_datePicker currentValueForThemeAttribute:@"hour-hand-image"] duplicate]];
-    [_minuteHandView setImage:[[_datePicker currentValueForThemeAttribute:@"minute-hand-image"] duplicate]];
-    [_secondHandView setImage:[[_datePicker currentValueForThemeAttribute:@"second-hand-image"] duplicate]];
+    var middleHandImage = [_datePicker currentValueForThemeAttribute:@"middle-hand-image"],
+        hourHandImage   = [_datePicker currentValueForThemeAttribute:@"hour-hand-image"],
+        minuteHandImage = [_datePicker currentValueForThemeAttribute:@"minute-hand-image"],
+        secondHandImage = [_datePicker currentValueForThemeAttribute:@"second-hand-image"];
 
-    var font       = [_datePicker currentValueForThemeAttribute:@"clock-font"],
-        textColor  = [_datePicker currentValueForThemeAttribute:@"clock-text-color"],
-        shadowCol  = [_datePicker currentValueForThemeAttribute:@"clock-text-shadow-color"],
-        shadowOff  = [_datePicker currentValueForThemeAttribute:@"clock-text-shadow-offset"];
+    // If hand images are true CPImage, we have to duplicate them to avoid
+    // the multiple delegates bug when multiple clocks are displayed
 
-    if (font)
-        [_PMAMTextField setFont:font];
+    if ([middleHandImage isKindOfClass:[CPImage class]])
+        middleHandImage = [middleHandImage duplicate];
 
-    if (textColor)
-        [_PMAMTextField setTextColor:textColor];
+    if ([hourHandImage isKindOfClass:[CPImage class]])
+        hourHandImage = [hourHandImage duplicate];
 
-    if (shadowCol)
-        [_PMAMTextField setTextShadowColor:shadowCol];
+    if ([minuteHandImage isKindOfClass:[CPImage class]])
+        minuteHandImage = [minuteHandImage duplicate];
 
-    if (shadowOff)
-        [_PMAMTextField setTextShadowOffset:shadowOff];
+    if ([secondHandImage isKindOfClass:[CPImage class]])
+        secondHandImage = [secondHandImage duplicate];
 
-    var hoursFont  = [_datePicker currentValueForThemeAttribute:@"clock-hours-font"],
-        hoursColor = [_datePicker currentValueForThemeAttribute:@"clock-hours-text-color"],
-        drawsHours = [_datePicker currentValueForThemeAttribute:@"clock-draws-hours"];
+    [_middleHandLayer setImage:middleHandImage];
+    [_hourHandLayer   setImage:hourHandImage];
+    [_minuteHandLayer setImage:minuteHandImage];
+    [_secondHandLayer setImage:secondHandImage];
 
-    for (var i = 0; i < 12; i++)
-    {
-        var label = _hourLabels[i];
-        [label setHidden:!drawsHours];
-        if (drawsHours) {
-            if (hoursFont)  [label setFont:hoursFont];
-            if (hoursColor) [label setTextColor:hoursColor];
-            [label sizeToFit];
-        }
-    }
+    [_hourHandLayer   setNeedsDisplay];
+    [_middleHandLayer setNeedsDisplay];
+    [_secondHandLayer setNeedsDisplay];
+    [_minuteHandLayer setNeedsDisplay];
+
+    [_rootLayer setFont:[_datePicker currentValueForThemeAttribute:@"clock-hours-font"]];
+    [_rootLayer setTextColor:[_datePicker currentValueForThemeAttribute:@"clock-hours-text-color"]];
+    [_rootLayer setRadius:[_datePicker currentValueForThemeAttribute:@"clock-hours-radius"]];
+
+    [_rootLayer setNeedsDisplay];
 }
 
 - (void)setDatePickerElements:(CPInteger)aDatePickerElements
 {
     _datePickerElements = aDatePickerElements;
 
-    [_secondHandView setHidden:!((_datePickerElements & CPHourMinuteSecondDatePickerElementFlag) == CPHourMinuteSecondDatePickerElementFlag)];
+    // Check if we have to display the hand second
+    // FIXME: Don't know why but next line will cause theme compilation to fail...
+    // Workaround: guarded with CPDOMAvailable (was: "if PLATFORM(DOM)")
+    if (!CPDOMAvailable) return;
+
+    [_secondHandLayer setHidden:!((_datePickerElements & CPHourMinuteSecondDatePickerElementFlag) == CPHourMinuteSecondDatePickerElementFlag)];
 }
 
 // MARK: Layout methods
 
 - (void)layoutSubviews
 {
-    [self _initHands];
-
+    // While tracking a hand, we don't want the whole thing to be relayouted at each mouse movement
     if (_trackingHand)
         return;
 
@@ -206,65 +220,31 @@ _CPDatePickerClockSeconds = 3;
     _representedSeconds  = dateValue.getSeconds();
     _representedHourIsPM = (_representedHours > 11);
 
+    // Hours are expressed in 24 hours format, we need 12 hours format
     _representedHours -= (_representedHourIsPM ? 12 : 0);
 
-    var bounds  = [self bounds],
-        centerX = bounds.size.width / 2.0,
-        centerY = bounds.size.height / 2.0;
-
-    [_PMAMTextField setStringValue:_representedHourIsPM ? @"PM" : @"AM"];
-    [_PMAMTextField sizeToFit];
-    [_PMAMTextField setFrameOrigin:CGPointMake(centerX - [_PMAMTextField frameSize].width / 2.0, centerY + 15.0)];
-
-    if ([_datePicker currentValueForThemeAttribute:@"clock-draws-hours"])
-    {
-        var radius = [_datePicker currentValueForThemeAttribute:@"clock-hours-radius"] || 50.0;
-        for (var i = 0, angle = 60.0; i < 12; i++, angle -= 30.0)
-        {
-            var label = _hourLabels[i],
-                size  = [label frameSize],
-                x     = centerX + radius * COS(angle * RADIANS) - size.width / 2.0,
-                y     = centerY - radius * SIN(angle * RADIANS) - size.height / 2.0;
-
-            [label setFrameOrigin:CGPointMake(x, y)];
-        }
-    }
-
-    var centerView = function(view, size) {[view setFrame:CGRectMake(centerX - size.width / 2.0, centerY - size.height / 2.0, size.width, size.height)];
-    };
-
-    var hSize   = [_datePicker currentValueForThemeAttribute:@"hour-hand-size"]   || CGSizeMake(4, 64),
-        mSize   = [_datePicker currentValueForThemeAttribute:@"minute-hand-size"] || CGSizeMake(4, 96),
-        sSize   = [_datePicker currentValueForThemeAttribute:@"second-hand-size"] || CGSizeMake(4, 96),
-        midSize = [_datePicker currentValueForThemeAttribute:@"middle-hand-size"] || CGSizeMake(8, 8);
-
-    centerView(_hourHandView, hSize);
-    centerView(_minuteHandView, mSize);
-    centerView(_secondHandView, sSize);
-    centerView(_middleHandView, midSize);
-
     [self _updateHands];
-}
 
-// Applies Pure CSS Transforms to rotate the elements natively in the browser
-- (void)_rotateView:(CPView)view byAngle:(float)radians
-{
-#if PLATFORM(DOM)
-    var style = view._DOMElement.style;
-    style[CPBrowserStyleProperty("transformOrigin")] = "50% 50%";
-    style[CPBrowserStyleProperty("transform")] = "rotate(" + radians + "rad)";
-#endif
+    // FIXME: Workaround. Seems that CALayer doesn't redraw without an event
+    [CALayer runLoopUpdateLayers];
 }
 
 - (void)_updateHands
 {
-    _hourAngle   = (360.0 * (_representedHours   + _representedMinutes / 60.0) / 12.0) * RADIANS;
-    _minuteAngle = (360.0 * (_representedMinutes + _representedSeconds / 60.0) / 60.0) * RADIANS;
-    _secondAngle = (360.0 * _representedSeconds / 60.0) * RADIANS;
+    var bounds = [self bounds];
 
-    [self _rotateView:_hourHandView byAngle:_hourAngle];
-    [self _rotateView:_minuteHandView byAngle:_minuteAngle];
-    [self _rotateView:_secondHandView byAngle:_secondAngle];
+    [_PMAMTextField setStringValue:_representedHourIsPM ? @"PM" : @"AM"];
+    [_PMAMTextField sizeToFit];
+    [_PMAMTextField setFrameOrigin:CGPointMake(bounds.size.width / 2 - [_PMAMTextField frameSize].width / 2, bounds.size.height / 2 + 15)];
+
+    [_hourHandLayer   setRotationRadians:(360 * (_representedHours   + _representedMinutes / 60) / 12) * RADIANS];
+    [_minuteHandLayer setRotationRadians:(360 * (_representedMinutes + _representedSeconds / 60) / 60) * RADIANS];
+    [_secondHandLayer setRotationRadians:(360 * _representedSeconds / 60) * RADIANS];
+
+    [_hourHandLayer   setNeedsDisplay];
+    [_minuteHandLayer setNeedsDisplay];
+    [_secondHandLayer setNeedsDisplay];
+//    [_middleHandLayer setNeedsDisplay];
 }
 
 // MARK: Accessors
@@ -278,40 +258,12 @@ _CPDatePickerClockSeconds = 3;
 
     _isEnabled = shouldEnable;
 
+    [self _initHands];
     [self setNeedsLayout];
 }
 
 // MARK: Mouse actions
 
-// Since we rotate using Pure CSS, Cappuccino's `convertPoint:` doesn't know about it.
-// So we use standard Trigonometry to perfectly hit-test the rotated hands!
-- (BOOL)_hitTestHandWithSize:(CGSize)size angle:(float)radians atPoint:(CGPoint)aPoint
-{
-    var bounds  = [self bounds],
-        centerX = bounds.size.width / 2.0,
-        centerY = bounds.size.height / 2.0;
-
-    // 1. Move point to center
-    var tx = aPoint.x - centerX,
-        ty = aPoint.y - centerY;
-
-    // 2. Rotate point backwards by the angle of the hand
-    var cosA = COS(-radians),
-        sinA = SIN(-radians),
-        rx   = tx * cosA - ty * sinA,
-        ry   = tx * sinA + ty * cosA;
-
-    // 3. Test if point is within the unrotated hand's rectangle
-    // The visual needle is in the top half of the hand's box (y from -h/2 to 0)
-    var w2 = size.width / 2.0,
-        h2 = size.height / 2.0;
-
-    if (rx >= -w2 && rx <= w2 && ry >= -h2 && ry <= 0)
-        return YES;
-
-    return NO;
-}
-
 - (void)mouseDown:(CPEvent)anEvent
 {
     if (!_isEnabled)
@@ -319,29 +271,25 @@ _CPDatePickerClockSeconds = 3;
 
     var currentLocation = [self convertPoint:[anEvent locationInWindow] fromView:nil];
 
-    var sSize = [_datePicker currentValueForThemeAttribute:@"second-hand-size"] || CGSizeMake(4, 96),
-        mSize = [_datePicker currentValueForThemeAttribute:@"minute-hand-size"] || CGSizeMake(4, 96),
-        hSize = [_datePicker currentValueForThemeAttribute:@"hour-hand-size"]   || CGSizeMake(4, 64);
-
-    if (![_secondHandView isHidden] && [self _hitTestHandWithSize:sSize angle:_secondAngle atPoint:currentLocation])
+    if ([_secondHandLayer handIsHitAtPoint:currentLocation])
     {
-        _currentHandView         = _secondHandView;
+        _currentHandLayer        = _secondHandLayer;
         _currentHand             = _CPDatePickerClockSeconds;
         _currentRepresentedValue = _representedSeconds;
         _currentValueShift       = 0;
         _numberOfUnits           = 60;
     }
-    else if (![_minuteHandView isHidden] && [self _hitTestHandWithSize:mSize angle:_minuteAngle atPoint:currentLocation])
+    else if ([_minuteHandLayer handIsHitAtPoint:currentLocation])
     {
-        _currentHandView         = _minuteHandView;
+        _currentHandLayer        = _minuteHandLayer;
         _currentHand             = _CPDatePickerClockMinutes;
         _currentRepresentedValue = _representedMinutes;
         _currentValueShift       = _representedSeconds / 60;
         _numberOfUnits           = 60;
     }
-    else if (![_hourHandView isHidden] && [self _hitTestHandWithSize:hSize angle:_hourAngle atPoint:currentLocation])
+    else if ([_hourHandLayer handIsHitAtPoint:currentLocation])
     {
-        _currentHandView         = _hourHandView;
+        _currentHandLayer        = _hourHandLayer;
         _currentHand             = _CPDatePickerClockHours;
         _currentRepresentedValue = _representedHours;
         _currentValueShift       = _representedMinutes / 60;
@@ -349,14 +297,14 @@ _CPDatePickerClockSeconds = 3;
     }
     else
     {
-        _currentHandView         = nil;
+        _currentHandLayer        = nil;
         _currentHand             = CPNotFound;
         _currentRepresentedValue = CPNotFound;
         _currentValueShift       = CPNotFound;
         _numberOfUnits           = CPNotFound;
     }
 
-    if (_currentHandView)
+    if (_currentHandLayer)
         [self trackMouse:anEvent];
 }
 
@@ -368,12 +316,13 @@ _CPDatePickerClockSeconds = 3;
 - (BOOL)startTrackingAt:(CGPoint)aPoint
 {
     _trackingHand = YES;
+
     return YES;
 }
 
 - (BOOL)continueTracking:(CGPoint)lastPoint at:(CGPoint)aPoint
 {
-    var dx    = aPoint.x - _bounds.size.width / 2,
+    var dx    = aPoint.x -_bounds.size.width / 2,
         dy    = _bounds.size.height / 2 - aPoint.y,
         angle = (PI_2 - ATAN2(dy,dx) + PI2) % PI2,
         value = ROUND(angle * _numberOfUnits / PI2 - _currentValueShift) % _numberOfUnits;
@@ -395,10 +344,14 @@ _CPDatePickerClockSeconds = 3;
                     _representedHourIsPM = !_representedHourIsPM;
 
                     if (movedForward && !_representedHourIsPM)
+                        // Day++
                         dateValue.setDate(dateValue.getDate() + 1);
+
                     else if (movedBackward && _representedHourIsPM)
+                        // Day--
                         dateValue.setDate(dateValue.getDate() - 1);
                 }
+
                 dateValue.setHours(value + (_representedHourIsPM ? 12 : 0));
                 break;
 
@@ -429,29 +382,34 @@ _CPDatePickerClockSeconds = 3;
                 break;
         }
 
-#if PLATFORM(DOM)
-        _datePicker._invokedByUserEvent = YES;
-#endif
-
-    [_datePicker _setDateValue:dateValue timeInterval:[_datePicker timeInterval]];
-
-#if PLATFORM(DOM)
-        _datePicker._invokedByUserEvent = NO;
-#endif
+        if (CPDOMAvailable)
+        {
+            _datePicker._invokedByUserEvent = YES;
+        }
+        [_datePicker _setDateValue:dateValue timeInterval:[_datePicker timeInterval]];
+        if (CPDOMAvailable)
+        {
+            _datePicker._invokedByUserEvent = NO;
+        }
 
+        // We have to adapt represented values
         _representedHours    = dateValue.getHours();
         _representedMinutes  = dateValue.getMinutes();
         _representedSeconds  = dateValue.getSeconds();
         _representedHourIsPM = (_representedHours > 11);
+
+        // Hours are expressed in 24 hours format, we need 12 hours format
         _representedHours -= (_representedHourIsPM ? 12 : 0);
 
         switch (_currentHand) {
             case _CPDatePickerClockHours:
                 _currentRepresentedValue = _representedHours;
                 break;
+
             case _CPDatePickerClockMinutes:
                 _currentRepresentedValue = _representedMinutes;
                 break;
+
             case _CPDatePickerClockSeconds:
                 _currentRepresentedValue = _representedSeconds;
                 break;
@@ -469,3 +427,188 @@ _CPDatePickerClockSeconds = 3;
 }
 
 @end
+
+// MARK: -
+
+@implementation HandLayer : CALayer
+{
+    CPImage         _image;
+    HandImageLayer  _imageLayer;
+    float           _rotationRadians;
+}
+
+// MARK: Init methods
+
+- (id)initWithSize:(CGSize)aSize
+{
+    if (self = [super init])
+    {
+        _imageLayer = [HandImageLayer layer];
+        _rotationRadians = 0;
+
+        [_imageLayer setDelegate:self];
+        [_imageLayer setBounds:CGRectMake(0.0, 0.0, aSize.width, aSize.height)];
+
+        [self addSublayer:_imageLayer];
+    }
+
+    return self;
+}
+
+
+// MARK: Setter Getter methods
+
+/*!
+    Set the bounds of the layer. The imageLayer will be at the center of this bounds.
+*/
+- (void)setBounds:(CGRect)aRect
+{
+    [super setBounds:aRect];
+
+    [_imageLayer setPosition:CGPointMake(CGRectGetMidX(aRect), CGRectGetMidY(aRect))];
+}
+
+- (void)setImage:(CPImage)anImage
+{
+    if (_image === anImage)
+        return;
+
+    if ([anImage isKindOfClass:[_CPCibCustomResource class]])
+        _image = [anImage imageFromCoder:nil];
+    else
+        _image = anImage;
+
+    [_imageLayer setNeedsDisplay];
+}
+
+- (void)setRotationRadians:(float)radians
+{
+    if (_rotationRadians === radians)
+        return;
+
+    _rotationRadians = radians;
+
+    [_imageLayer setAffineTransform:CGAffineTransformScale(
+        CGAffineTransformMakeRotation(_rotationRadians),
+        1.0, 1.0)];
+}
+
+- (void)imageDidLoad:(CPImage)anImage
+{
+    [_imageLayer setNeedsDisplay];
+}
+
+- (void)drawLayer:(CALayer)aLayer inContext:(CGContext)aContext
+{
+    if ([_image loadStatus] != CPImageLoadStatusCompleted)
+        [_image setDelegate:self];
+    else
+        CGContextDrawImage(aContext, [aLayer bounds], _image);
+}
+
+- (BOOL)handIsHitAtPoint:(CGPoint)aPoint
+{
+    return (!_isHidden && [_imageLayer hitTest:aPoint] === _imageLayer);
+}
+
+- (void)setNeedsDisplay
+{
+    [super       setNeedsDisplay];
+    [_imageLayer setNeedsDisplay];
+}
+
+@end
+
+// MARK: -
+
+@implementation HandImageLayer : CALayer
+{
+    CGRect  _handBounds;
+}
+
+// We have to adapt hitTest so it only takes the hand into account (that's the top half of the image layer)
+// We are also sure there's no sublayers
+- (CALayer)hitTest:(CGPoint)aPoint
+{
+    if (_isHidden)
+        return nil;
+
+    var point = CGPointApplyAffineTransform(aPoint, _transformToLayer);
+
+    return CGRectContainsPoint(_handBounds, point) ? self : nil;
+}
+
+- (void)setBounds:(CGRect)aBounds
+{
+    if (CGRectEqualToRect(_bounds, aBounds))
+        return;
+
+    _handBounds = CGRectMakeCopy(aBounds);
+
+    _handBounds.size.height = _handBounds.size.height / 2;
+
+    [super setBounds:aBounds];
+}
+
+
+@end
+
+// MARK: -
+
+@implementation HoursLayer : CALayer
+{
+    BOOL    _drawsHours;
+
+    CPFont  _font           @accessors(property=font);
+    CPColor _textColor      @accessors(property=textColor);
+    float   _radius         @accessors(property=radius);
+}
+
+
+- (id)init
+{
+    if (self = [super init])
+    {
+        _drawsHours = NO;
+    }
+
+    return self;
+}
+
+- (void)setDrawsHours:(BOOL)shouldDrawHours
+{
+    shouldDrawHours = !!shouldDrawHours;
+
+    if (_drawsHours === shouldDrawHours)
+        return;
+
+    _drawsHours = shouldDrawHours;
+    [self setNeedsDisplay];
+}
+
+- (void)drawInContext:(CGContext)aContext
+{
+    [super drawInContext:aContext];
+
+    if (_drawsHours)
+    {
+        var bounds = [self bounds],
+            centerX = bounds.size.width / 2,
+            centerY = bounds.size.height / 2;
+
+        CGContextSelectFont(aContext, _font);
+        CGContextSetFillColor(aContext, _textColor);
+        aContext.textBaseline = @"middle";
+        aContext.textAlign = @"center";
+
+        for (var i = 1, angle = 60.0, x, y; i < 13; i++, angle -= 30.0)
+        {
+            x = centerX + _radius * COS(angle * RADIANS);
+            y = centerY - _radius * SIN(angle * RADIANS);
+
+            aContext.fillText(i, x, y);
+        }
+    }
+}
+
+@end
diff --git a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
index 49bacb985..dbdc595ac 100644
--- a/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
+++ b/AppKit/CPDatePicker/_CPDatePickerElementTextField.j
@@ -20,6 +20,7 @@
 */
 
 @import "CPTextField.j"
+@import "CPCompatibility.j"
 
 @class CPDatePicker
 @class _CPDatePickerElementView
@@ -471,13 +472,15 @@ CPAMPMDateType = 6;
         newDateValue.setSeconds(newDateValue.getSeconds() + secondsFromGMT - secondsFromGMTTimeZone);
     }
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = YES;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = YES;
+    }
     [_datePicker _setDateValue:newDateValue timeInterval:[_datePicker timeInterval]];
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = NO;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = NO;
+    }
 }
 
 
diff --git a/AppKit/CPDatePicker/_CPDatePickerElementView.j b/AppKit/CPDatePicker/_CPDatePickerElementView.j
index d6339d43f..fd581532a 100644
--- a/AppKit/CPDatePicker/_CPDatePickerElementView.j
+++ b/AppKit/CPDatePicker/_CPDatePickerElementView.j
@@ -21,6 +21,7 @@
 
 @import "CPControl.j"
 @import "_CPDatePickerElementTextField.j"
+@import "CPCompatibility.j"
 
 @class CPDatePicker
 
@@ -403,13 +404,15 @@
             dateValue.setHours(dateValue.getHours() - 12);
     }
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = YES;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = YES;
+    }
     [_datePicker _setDateValue:dateValue timeInterval:[_datePicker timeInterval]];
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = NO;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = NO;
+    }
 }
 
 
diff --git a/AppKit/CPDatePicker/_CPDatePickerMonthView.j b/AppKit/CPDatePicker/_CPDatePickerMonthView.j
index f1d4470e4..2a065b637 100644
--- a/AppKit/CPDatePicker/_CPDatePickerMonthView.j
+++ b/AppKit/CPDatePicker/_CPDatePickerMonthView.j
@@ -23,6 +23,7 @@
 
 @import "CPControl.j"
 @import "_CPDatePickerDayView.j"
+@import "CPCompatibility.j"
 
 @class CPDatePicker
 
@@ -457,9 +458,10 @@
     _indexDayTile = -1;
     _eventDragged = nil;
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = YES;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = YES;
+    }
 
     // Check if we have to change or not the month of the component
     if ([dayTile date].getMonth() == _date.getMonth())
@@ -514,9 +516,10 @@
             [_delegate _displayNextMonth];
     }
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = NO;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = NO;
+    }
 }
 
 /*! Mouse dragged event
@@ -534,9 +537,10 @@
     _indexDayTile = [self indexOfTileForEvent:anEvent];
     _eventDragged = anEvent;
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = YES;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = YES;
+    }
 
     if ([_datePicker datePickerMode] == CPSingleDateMode)
     {
@@ -580,9 +584,10 @@
         }
     }
 
-#if PLATFORM(DOM)
-    _datePicker._invokedByUserEvent = NO;
-#endif
+    if (CPDOMAvailable)
+    {
+        _datePicker._invokedByUserEvent = NO;
+    }
 }
 
 - (void)mouseUp:(CPEvent)anEvent
diff --git a/AppKit/CPDatePicker/_CPDatePickerTextField.j b/AppKit/CPDatePicker/_CPDatePickerTextField.j
index cbeda2865..8dd7b12aa 100644
--- a/AppKit/CPDatePicker/_CPDatePickerTextField.j
+++ b/AppKit/CPDatePicker/_CPDatePickerTextField.j
@@ -34,6 +34,7 @@
 @class _CPDatePickerElementView
 
 @import "_CPDatePickerElementView.j"
+@import "CPCompatibility.j"
 
 @global CPSingleDateMode
 @global CPRangeDateMode
@@ -89,17 +90,18 @@
     [self setNeedsLayout];
     [self setNeedsDisplay:YES];
 
-#if PLATFORM(DOM)
     if ([_datePicker currentValueForThemeAttribute:@"uses-focus-ring"])
     {
         // As with overflow:hidden, views are clipping their content, in order
         // to show a focus ring (which is external to a view), we need to let
         // content extend outside the view.
-        _datePicker._DOMElement.style.overflow            = "visible";
-        _DOMElement.style.overflow                        = "visible";
-        _datePickerElementView._DOMElement.style.overflow = "visible";
+        if (CPDOMAvailable)
+        {
+            _datePicker._DOMElement.style.overflow            = "visible";
+            _DOMElement.style.overflow                        = "visible";
+            _datePickerElementView._DOMElement.style.overflow = "visible";
+        }
     }
-#endif
 }
 
 
diff --git a/AppKit/CPDragServer.j b/AppKit/CPDragServer.j
index bd2b09a74..5190defd3 100644
--- a/AppKit/CPDragServer.j
+++ b/AppKit/CPDragServer.j
@@ -33,7 +33,10 @@
 
 @global CPApp
 
-#define DRAGGING_WINDOW(anObject) ([anObject isKindOfClass:[CPWindow class]] ? anObject : [anObject window])
+function DRAGGING_WINDOW(anObject)
+{
+    return ([anObject isKindOfClass:[CPWindow class]] ? anObject : [anObject window]);
+}
 
 var CPDragServerPreviousEvent = nil,
     CPDragServerPeriodicUpdateInterval = 0.05;
diff --git a/AppKit/CPEvent.j b/AppKit/CPEvent.j
index 5cc439e08..304413cb1 100644
--- a/AppKit/CPEvent.j
+++ b/AppKit/CPEvent.j
@@ -82,10 +82,8 @@ var _CPEventPeriodicEventPeriod         = 0,
     float               _scrollingDeltaY;
     BOOL                _hasPreciseScrollingDeltas;
 
-#if PLATFORM(DOM)
     BOOL                _suppressCappuccinoCut;
     BOOL                _suppressCappuccinoPaste;
-#endif
 
     CPTrackingArea      _trackingArea;
 }
@@ -609,11 +607,7 @@ var _CPEventPeriodicEventPeriod         = 0,
 */
 - (BOOL)_platformIsEffectingCutOrPaste
 {
-#if PLATFORM(DOM)
     return _suppressCappuccinoCut || _suppressCappuccinoPaste;
-#else
-    return NO;
-#endif
 }
 
 
diff --git a/AppKit/CPFont.j b/AppKit/CPFont.j
index 1a02d33fb..3304a697a 100644
--- a/AppKit/CPFont.j
+++ b/AppKit/CPFont.j
@@ -51,14 +51,40 @@ var _CPFontCache                   = {},
     _CPFontSystemFontFaceSpecified = NO;
 
 
-#define _CPRealFontSize(aSize)  (aSize <= 0 ? _CPFontSystemFontSize : aSize)
-#define _CPFontNormalizedNames(aName)  _CPFontNormalizedNameArray(aName).join(", ")
-#define _CPCachedFont(aName, aSize, isBold, isItalic)  _CPFontCache[_CPFontCreateCSSString(_CPFontNormalizedNames(aName), aSize, isBold, isItalic)]
-#define _CPUserFont(aName, aSize, isBold, isItalic)  _CPCachedFont(aName, aSize, isBold, isItalic) || [[CPFont alloc] _initWithName:aName size:aSize bold:isBold italic:isItalic system:NO]
+function _CPRealFontSize(aSize)
+{
+    return (aSize <= 0 ? _CPFontSystemFontSize : aSize);
+}
 
-#define _CPSystemFontCacheKey(aSize, isBold)  (String(aSize) + (isBold ? "b" : ""))
-#define _CPCachedSystemFont(aSize, isBold)  _CPSystemFontCache[_CPSystemFontCacheKey(aSize, isBold)]
-#define _CPSystemFont(aSize, isBold)  (_CPCachedSystemFont(aSize, isBold) || [[CPFont alloc] _initWithName:_CPFontSystemFacePlaceholder size:aSize bold:isBold italic:NO system:YES])
+function _CPFontNormalizedNames(aName)
+{
+    return _CPFontNormalizedNameArray(aName).join(", ");
+}
+
+function _CPCachedFont(aName, aSize, isBold, isItalic)
+{
+    return _CPFontCache[_CPFontCreateCSSString(_CPFontNormalizedNames(aName), aSize, isBold, isItalic)];
+}
+
+function _CPUserFont(aName, aSize, isBold, isItalic)
+{
+    return _CPCachedFont(aName, aSize, isBold, isItalic) || [[CPFont alloc] _initWithName:aName size:aSize bold:isBold italic:isItalic system:NO];
+}
+
+function _CPSystemFontCacheKey(aSize, isBold)
+{
+    return (String(aSize) + (isBold ? "b" : ""));
+}
+
+function _CPCachedSystemFont(aSize, isBold)
+{
+    return _CPSystemFontCache[_CPSystemFontCacheKey(aSize, isBold)];
+}
+
+function _CPSystemFont(aSize, isBold)
+{
+    return (_CPCachedSystemFont(aSize, isBold) || [[CPFont alloc] _initWithName:_CPFontSystemFacePlaceholder size:aSize bold:isBold italic:NO system:YES]);
+}
 
 /*!
 @ingroup appkit
diff --git a/AppKit/CPFontManager.j b/AppKit/CPFontManager.j
index d10a27e44..6429590ac 100644
--- a/AppKit/CPFontManager.j
+++ b/AppKit/CPFontManager.j
@@ -25,6 +25,7 @@
 @import "CPControl.j"
 @import "CPFont.j"
 @import "CPFontDescriptor.j"
+@import "CPCompatibility.j"
 
 @global CPApp
 @class CPFontPanel
@@ -132,28 +133,31 @@ CPRemoveTraitFontAction = 7;
     {
         _availableFonts = [];
 
-#if PLATFORM(DOM)
-        _CPFontDetectSpan = document.createElement("span");
-        _CPFontDetectSpan.fontSize = "24px";
-        _CPFontDetectSpan.appendChild(document.createTextNode("mmmmmmmmmml"));
-        var div = document.createElement("div");
-        div.style.position = "absolute";
-        div.style.top = "-1000px";
-        div.appendChild(_CPFontDetectSpan);
-        document.getElementsByTagName("body")[0].appendChild(div);
-
-        _CPFontDetectReferenceFonts = _CPFontDetectPickTwoDifferentFonts(["monospace", "serif", "sans-serif", "cursive"]);
-
-        for (var i = 0; i < _CPFontDetectAllFonts.length; i++)
+        if (CPDOMAvailable)
         {
-            var available = _CPFontDetectFontAvailable(_CPFontDetectAllFonts[i]);
-            if (available)
-                _availableFonts.push(_CPFontDetectAllFonts[i]);
+            _CPFontDetectSpan = document.createElement("span");
+            _CPFontDetectSpan.fontSize = "24px";
+            _CPFontDetectSpan.appendChild(document.createTextNode("mmmmmmmmmml"));
+            var div = document.createElement("div");
+            div.style.position = "absolute";
+            div.style.top = "-1000px";
+            div.appendChild(_CPFontDetectSpan);
+            document.getElementsByTagName("body")[0].appendChild(div);
+
+            _CPFontDetectReferenceFonts = _CPFontDetectPickTwoDifferentFonts(["monospace", "serif", "sans-serif", "cursive"]);
+
+            for (var i = 0; i < _CPFontDetectAllFonts.length; i++)
+            {
+                var available = _CPFontDetectFontAvailable(_CPFontDetectAllFonts[i]);
+                if (available)
+                    _availableFonts.push(_CPFontDetectAllFonts[i]);
+            }
+        }
+        else
+        {
+            // If there's no font detection, just assume all fonts are available.
+            _availableFonts = _CPFontDetectAllFonts;
         }
-#else
-        // If there's no font detection, just assume all fonts are available.
-        _availableFonts = _CPFontDetectAllFonts;
-#endif
     }
     return _availableFonts;
 }
diff --git a/AppKit/CPImage.j b/AppKit/CPImage.j
index 3ee6bfeaa..955beeacb 100644
--- a/AppKit/CPImage.j
+++ b/AppKit/CPImage.j
@@ -239,7 +239,8 @@ function CPAppKitImage(aFilename, aSize)
 */
 - (CPData)data
 {
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
+
     var dataURL;
 
     if ([_filename hasPrefix:@"data:image"])
@@ -261,7 +262,6 @@ function CPAppKitImage(aFilename, aSize)
 
     var base64 = dataURL.replace(/^data:image\/png;base64,/, "");
     return [CPData dataWithBase64:base64];
-#endif
 }
 
 /*!
@@ -384,7 +384,8 @@ function CPAppKitImage(aFilename, aSize)
 
     _loadStatus = CPImageLoadStatusLoading;
 
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
+
     _image = new Image();
 
     var isSynchronous = YES;
@@ -431,7 +432,6 @@ function CPAppKitImage(aFilename, aSize)
     // onload and friends may fire after this point but BEFORE the end of the run loop,
     // crazy, I know. So don't set isSynchronous here, rather wait a bit longer.
     window.setTimeout(function() { isSynchronous = NO; }, 0);
-#endif
 }
 
 - (BOOL)isSingleImage
@@ -641,7 +641,8 @@ function CPAppKitImage(aFilename, aSize)
 
 - (DOMElement)applyCSSImageForView:(CPView)aView onDOMElement:(DOMElement)aDOMElement styleNode:(DOMElement)aStyleNode previousState:(CPArrayRef)aPreviousStateRef
 {
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
+
     // First, restore previous CSS styling before applying the new one
 
     var aPreviousState = @deref(aPreviousStateRef);
@@ -724,7 +725,6 @@ function CPAppKitImage(aFilename, aSize)
     @deref(aPreviousStateRef) = aPreviousState;
 
     return aStyleNode;
-#endif
 }
 
 - (BOOL)_shouldBeResized
diff --git a/AppKit/CPKeyBinding.j b/AppKit/CPKeyBinding.j
index 51c234dc4..1af77a7cc 100644
--- a/AppKit/CPKeyBinding.j
+++ b/AppKit/CPKeyBinding.j
@@ -88,6 +88,7 @@ CPStandardKeyBindings[@"~" + CPDeleteFunctionKey] = @"deleteWordForward:";
 CPStandardKeyBindings[CPTabCharacter] = @"insertTab:";
 CPStandardKeyBindings[@"~" + CPTabCharacter] = @"insertTabIgnoringFieldEditor:";
 CPStandardKeyBindings[@"^" + CPTabCharacter] = @"selectNextKeyView:";
+CPStandardKeyBindings[@"$" + CPTabCharacter] = @"insertBacktab:";
 CPStandardKeyBindings[CPBackTabCharacter] = @"insertBacktab:";
 CPStandardKeyBindings[@"^" + CPBackTabCharacter] = @"selectPreviousKeyView:";
 
diff --git a/AppKit/CPMenu/CPMenu.j b/AppKit/CPMenu/CPMenu.j
index 9700956a2..a737ded12 100644
--- a/AppKit/CPMenu/CPMenu.j
+++ b/AppKit/CPMenu/CPMenu.j
@@ -28,6 +28,7 @@
 @import "CPKeyValueBinding.j"
 @import "CPMenuItem.j"
 @import "CALayer.j"
+@import "CPCompatibility.j"
 
 @global CPApp
 
@@ -51,7 +52,8 @@ CPMenuDidEndTrackingNotification    = @"CPMenuDidEndTrackingNotification";
 var _CPMenuBarVisible               = NO,
     _CPMenuBarTitle                 = @"",
     _CPMenuBarAttributes            = nil,
-    _CPMenuBarSharedWindow          = nil;
+    _CPMenuBarSharedWindow          = nil,
+    _CPMenuBarImage                 = nil;
 
 /*!
     @ingroup appkit
@@ -136,9 +138,10 @@ var _CPMenuBarVisible               = NO,
         [_CPMenuBarSharedWindow orderOut:self];
 
 // FIXME: There must be a better way to do this.
-#if PLATFORM(DOM)
-    [[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
-#endif
+    if (CPDOMAvailable)
+    {
+        [[CPPlatformWindow primaryPlatformWindow] resizeEvent:nil];
+    }
 }
 
 + (void)setMenuBarTitle:(CPString)aTitle
@@ -1119,14 +1122,14 @@ var _CPMenuBarVisible               = NO,
                 anEvent._isKeyEquivalent = YES; // prevent the menu keystroke from beeing inserted into textview
                 [self performActionForItemAtIndex:index];
 
-#if PLATFORM(DOM)
                 // we are done with this event in cappuccino space. do not let the browser do something weird additionally (e.g. command-o).
                 // but we must not stop copy/paste events as these can only be handled by the browser at this time even if they are in our menu
                 // (until we move to CPTextView as the fieleditor)
 
-                if (characters != "c" && characters != "x" && characters != "v")
+                if (CPDOMAvailable && characters != "c" && characters != "x" && characters != "v")
+                {
                     _CPDOMEventStop(anEvent._DOMEvent);
-#endif
+                }
             }
             else
             {
@@ -1433,3 +1436,5 @@ var CPMenuTitleKey              = @"CPMenuTitleKey",
 
 @import "_CPMenuBarWindow.j"
 @import "_CPMenuWindow.j"
+
+@global _CPDOMEventStop
diff --git a/AppKit/CPMenu/_CPMenuBarWindow.j b/AppKit/CPMenu/_CPMenuBarWindow.j
index c39596622..4a40d0a75 100644
--- a/AppKit/CPMenu/_CPMenuBarWindow.j
+++ b/AppKit/CPMenu/_CPMenuBarWindow.j
@@ -22,6 +22,7 @@
 
 @import "CPPanel.j"
 @import "_CPMenuManager.j"
+@import "CPCompatibility.j"
 
 @class _CPMenuView
 @class CPMenu
@@ -100,16 +101,17 @@
 
 - (void)setTitle:(CPString)aTitle
 {
-#if PLATFORM(DOM)
-    var bundleName = [[CPBundle mainBundle] objectForInfoDictionaryKey:@"CPBundleName"];
+    if (CPDOMAvailable)
+    {
+        var bundleName = [CPApp _applicationName];
 
-    if (![bundleName length])
-        document.title = aTitle;
-    else if ([aTitle length])
-        document.title = aTitle + @" - " + bundleName;
-    else
-        document.title = bundleName;
-#endif
+        if (![bundleName length])
+            document.title = aTitle;
+        else if ([aTitle length])
+            document.title = aTitle + @" - " + bundleName;
+        else
+            document.title = bundleName;
+    }
 
     [_titleField setStringValue:aTitle];
     [_titleField sizeToFit];
diff --git a/AppKit/CPMenuItem/CPMenuItem.j b/AppKit/CPMenuItem/CPMenuItem.j
index 1a25128bd..a92e1ca77 100644
--- a/AppKit/CPMenuItem/CPMenuItem.j
+++ b/AppKit/CPMenuItem/CPMenuItem.j
@@ -895,6 +895,17 @@ CPControlKeyMask
     return [super description] + @" target: " + [self target] + @" action: " + CPStringFromSelector([self action]);
 }
 
+- (id)_decodeValueForKey:(CPString)aKey defaultValue:(id)aDefaultValue coder:(CPCoder)aCoder
+{
+    return [aCoder containsValueForKey:aKey] ? [aCoder decodeObjectForKey:aKey] : aDefaultValue;
+}
+
+- (void)_encodeValue:(id)aValue forKey:(CPString)aKey ifNotEqualTo:(id)aDefaultValue coder:(CPCoder)aCoder
+{
+    if (aValue !== aDefaultValue)
+        [aCoder encodeObject:aValue forKey:aKey];
+}
+
 @end
 
 // MARK: -
@@ -940,9 +951,6 @@ var CPMenuItemIsSeparatorKey                = @"CPMenuItemIsSeparatorKey",
     CPMenuItemRepresentedObjectKey          = @"CPMenuItemRepresentedObjectKey",
     CPMenuItemViewKey                       = @"CPMenuItemViewKey";
 
-#define DEFAULT_VALUE(aKey, aDefaultValue) [aCoder containsValueForKey:(aKey)] ? [aCoder decodeObjectForKey:(aKey)] : (aDefaultValue)
-#define ENCODE_IFNOT(aKey, aValue, aDefaultValue) if ((aValue) !== (aDefaultValue)) [aCoder encodeObject:(aValue) forKey:(aKey)];
-
 @implementation CPMenuItem (CPCoding)
 /*!
     Initializes the menu item from a coder.
@@ -965,7 +973,7 @@ var CPMenuItemIsSeparatorKey                = @"CPMenuItemIsSeparatorKey",
         _target = [aCoder decodeObjectForKey:CPMenuItemTargetKey];
         _action = [aCoder decodeObjectForKey:CPMenuItemActionKey];
 
-        _isEnabled = DEFAULT_VALUE(CPMenuItemIsEnabledKey, YES);
+        _isEnabled = [self _decodeValueForKey:CPMenuItemIsEnabledKey defaultValue:YES coder:aCoder];
         _isHidden = [aCoder decodeBoolForKey:CPMenuItemIsHiddenKey];
         _tag = [aCoder decodeIntForKey:CPMenuItemTagKey];
         _state = [aCoder decodeIntForKey:CPMenuItemStateKey];
@@ -1012,20 +1020,20 @@ var CPMenuItemIsSeparatorKey                = @"CPMenuItemIsSeparatorKey",
     [aCoder encodeObject:_target forKey:CPMenuItemTargetKey];
     [aCoder encodeObject:_action forKey:CPMenuItemActionKey];
 
-    ENCODE_IFNOT(CPMenuItemIsEnabledKey, _isEnabled, YES);
-    ENCODE_IFNOT(CPMenuItemIsHiddenKey, _isHidden, NO);
+    [self _encodeValue:_isEnabled forKey:CPMenuItemIsEnabledKey ifNotEqualTo:YES coder:aCoder]
+    [self _encodeValue:_isHidden forKey:CPMenuItemIsHiddenKey ifNotEqualTo:NO coder:aCoder]
 
-    ENCODE_IFNOT(CPMenuItemTagKey, _tag, 0);
-    ENCODE_IFNOT(CPMenuItemStateKey, _state, CPOffState);
+    [self _encodeValue:_tag forKey:CPMenuItemTagKey ifNotEqualTo:0 coder:aCoder]
+    [self _encodeValue:_state forKey:CPMenuItemStateKey ifNotEqualTo:CPOffState coder:aCoder]
 
-    ENCODE_IFNOT(CPMenuItemImageKey, _image, nil);
-    ENCODE_IFNOT(CPMenuItemAlternateImageKey, _alternateImage, nil);
-    ENCODE_IFNOT(CPMenuItemOnStateImageKey, _onStateImage, nil);
-    ENCODE_IFNOT(CPMenuItemOffStateImageKey, _offStateImage, nil);
-    ENCODE_IFNOT(CPMenuItemMixedStateImageKey, _mixedStateImage, nil);
+    [self _encodeValue:_image forKey:CPMenuItemImageKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_alternateImage forKey:CPMenuItemAlternateImageKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_onStateImage forKey:CPMenuItemOnStateImageKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_offStateImage forKey:CPMenuItemOffStateImageKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_mixedStateImage forKey:CPMenuItemMixedStateImageKey ifNotEqualTo:nil coder:aCoder]
 
-    ENCODE_IFNOT(CPMenuItemSubmenuKey, _submenu, nil);
-    ENCODE_IFNOT(CPMenuItemMenuKey, _menu, nil);
+    [self _encodeValue:_submenu forKey:CPMenuItemSubmenuKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_menu forKey:CPMenuItemMenuKey ifNotEqualTo:nil coder:aCoder]
 
     if (_keyEquivalent && _keyEquivalent.length)
         [aCoder encodeObject:_keyEquivalent forKey:CPMenuItemKeyEquivalentKey];
@@ -1036,8 +1044,9 @@ var CPMenuItemIsSeparatorKey                = @"CPMenuItemIsSeparatorKey",
     if (_indentationLevel > 0)
         [aCoder encodeInt:_indentationLevel forKey:CPMenuItemIndentationLevelKey];
 
-    ENCODE_IFNOT(CPMenuItemRepresentedObjectKey, _representedObject, nil);
-    ENCODE_IFNOT(CPMenuItemViewKey, _view, nil);
+    [self _encodeValue:_representedObject forKey:CPMenuItemRepresentedObjectKey ifNotEqualTo:nil coder:aCoder]
+    [self _encodeValue:_view forKey:CPMenuItemViewKey ifNotEqualTo:nil coder:aCoder]
 }
 
+
 @end
diff --git a/AppKit/CPOutlineView.j b/AppKit/CPOutlineView.j
index 237b08038..3312bea2b 100644
--- a/AppKit/CPOutlineView.j
+++ b/AppKit/CPOutlineView.j
@@ -87,9 +87,15 @@ var CPOutlineViewCoalesceSelectionNotificationStateOff  = 0,
     CPOutlineViewCoalesceSelectionNotificationStateOn   = 1,
     CPOutlineViewCoalesceSelectionNotificationStateDid  = 2;
 
-#define SELECTION_SHOULD_CHANGE(anOutlineView) (!((anOutlineView)._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_selectionShouldChangeInOutlineView_) || [(anOutlineView)._outlineViewDelegate selectionShouldChangeInOutlineView:(anOutlineView)])
+function SELECTION_SHOULD_CHANGE(anOutlineView)
+{
+    return (!((anOutlineView)._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_selectionShouldChangeInOutlineView_) || [(anOutlineView)._outlineViewDelegate selectionShouldChangeInOutlineView:(anOutlineView)]);
+}
 
-#define SHOULD_SELECT_ITEM(anOutlineView, anItem) (!((anOutlineView)._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectItem_) || [(anOutlineView)._outlineViewDelegate outlineView:(anOutlineView) shouldSelectItem:(anItem)])
+function SHOULD_SELECT_ITEM(anOutlineView, anItem)
+{
+    return (!((anOutlineView)._implementedOutlineViewDelegateMethods & CPOutlineViewDelegate_outlineView_shouldSelectItem_) || [(anOutlineView)._outlineViewDelegate outlineView:(anOutlineView) shouldSelectItem:(anItem)]);
+}
 
 
 @protocol CPOutlineViewDelegate 
diff --git a/AppKit/CPPasteboard.j b/AppKit/CPPasteboard.j
index 384936ba7..dc4be9839 100644
--- a/AppKit/CPPasteboard.j
+++ b/AppKit/CPPasteboard.j
@@ -45,6 +45,7 @@ CPURLPboardType         = @"CPURLPboardType";
 CPImagesPboardType      = @"CPImagesPboardType";
 CPVideosPboardType      = @"CPVideosPboardType";
 CPRTFPboardType         = @"CPRTFPboardType";
+CPRulerPboardType       = @"CPRulerPboardType";
 _CPSmartPboardType      = @"_CPSmartPboardType";
 _CPASPboardType         = @"_CPASPboardType";
 
@@ -323,8 +324,6 @@ var CPPasteboards = nil;
 
 @end
 
-#if PLATFORM(DOM)
-
 var DOMDataTransferPasteboard = nil;
 
 @implementation _CPDOMDataTransferPasteboard : CPPasteboard
@@ -388,4 +387,3 @@ var DOMDataTransferPasteboard = nil;
 
 @end
 
-#endif
diff --git a/AppKit/CPResponder.j b/AppKit/CPResponder.j
index 0b7467dd7..26b542c5a 100644
--- a/AppKit/CPResponder.j
+++ b/AppKit/CPResponder.j
@@ -290,6 +290,16 @@ CPDeleteForwardKeyCode  = 46;
 {
 }
 
+/*
+    @ignore
+    Returns YES if the receiver handles the Tab key itself, as an editable text view does by
+    inserting a tab character. Otherwise the window uses Tab to move to the next key view.
+*/
+- (BOOL)_wantsTabKey
+{
+    return NO;
+}
+
 - (void)insertBackTab:(id)sender
 {
 }
diff --git a/AppKit/CPRuleEditor/CPRuleEditor.j b/AppKit/CPRuleEditor/CPRuleEditor.j
index 38cb4e7e4..da926d47c 100644
--- a/AppKit/CPRuleEditor/CPRuleEditor.j
+++ b/AppKit/CPRuleEditor/CPRuleEditor.j
@@ -33,6 +33,7 @@
 
 @import "_CPRuleEditorViewSliceRow.j"
 @import "_CPRuleEditorLocalizer.j"
+@import "CPCompatibility.j"
 
 @class CPCompoundPredicate
 @class CPComparisonPredicate
@@ -2260,10 +2261,11 @@ TODO: implement
         firstSlice = [_slices objectAtIndex:firstIndex],
         dragview = [[CPView alloc] initWithFrame:[firstSlice frame]];
 
-#if PLATFORM(DOM)
-    var html = firstSlice._DOMElement.innerHTML;
-    dragview._DOMElement.innerHTML = [html copy];
-#endif
+    if (CPDOMAvailable)
+    {
+        var html = firstSlice._DOMElement.innerHTML;
+        dragview._DOMElement.innerHTML = [html copy];
+    }
     [dragview setBackgroundColor:[firstSlice backgroundColor]];
     [dragview setAlphaValue:0.7];
 
diff --git a/AppKit/CPScreen.j b/AppKit/CPScreen.j
index e4a763088..fceb68276 100644
--- a/AppKit/CPScreen.j
+++ b/AppKit/CPScreen.j
@@ -42,11 +42,10 @@
 */
 - (CGRect)visibleFrame
 {
-#if PLATFORM(DOM)
-    return CGRectMake(window.screen.availLeft, window.screen.availTop, window.screen.availWidth, window.screen.availHeight);
-#else
+    if (CPDOMAvailable)
+        return CGRectMake(window.screen.availLeft, window.screen.availTop, window.screen.availWidth, window.screen.availHeight);
+
     return CGRectMakeZero();
-#endif
 }
 
 @end
diff --git a/AppKit/CPScrollView.j b/AppKit/CPScrollView.j
index 7795419cd..13626216d 100644
--- a/AppKit/CPScrollView.j
+++ b/AppKit/CPScrollView.j
@@ -30,11 +30,11 @@
 @import "CPScroller.j"
 @import "CPView.j"
 @import "CPRulerView.j"
+@import "CPCompatibility.j"
 
 @class CPTableView
 @class CPRulerView
 
-#define SHOULD_SHOW_CORNER_VIEW() (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden])
 
 
 @protocol CPScrollViewDelegate 
@@ -49,7 +49,8 @@
 /*! @ignore */
 var _isBrowserUsingOverlayScrollers = function()
 {
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return NO;
+
     /*
         Even if the system supports overlay (Lion) scrollers,
         the browser (e.g. FireFox *cough*) may not.
@@ -85,9 +86,6 @@ var _isBrowserUsingOverlayScrollers = function()
     document.body.removeChild(outer);
 
     return usingOverlayScrollers;
-#else
-    return NO;
-#endif
 };
 
 var TIMER_INTERVAL                              = 0.2,
@@ -223,7 +221,7 @@ var _CPScrollViews;
         bounds.size.height -= [horizontalScrollerClass scrollerWidthInStyle:scrollerStyle];
 
     if (verticalScrollerClass)
-        bounds.size.width -= [verticalScrollerClass scrollerWidthForControlSize:scrollerStyle];
+        bounds.size.width -= [verticalScrollerClass scrollerWidthInStyle:scrollerStyle];
 
     return bounds.size;
 }
@@ -252,11 +250,11 @@ var _CPScrollViews;
         heightInset = contentSize.height - bounds.size.height,
         frameSize = CGSizeMake(contentSize.width + widthInset, contentSize.height + heightInset);
 
-    if (hFlag)
+    if (horizontalScrollerClass)
         frameSize.height += [horizontalScrollerClass scrollerWidthInStyle:scrollerStyle];
 
-    if (vFlag)
-        frameSize.width += [verticalScrollerClass scrollerWidthForControlSize:scrollerStyle];
+    if (verticalScrollerClass)
+        frameSize.width += [verticalScrollerClass scrollerWidthInStyle:scrollerStyle];
 
     return frameSize;
 }
@@ -917,7 +915,10 @@ Notifies the delegate when the scroll view has finished scrolling.
     _horizontalRuler = aRulerView;
 
     if (_horizontalRuler)
+    {
+        [_horizontalRuler setScrollView:self];
         [self addSubview:_horizontalRuler];
+    }
 
     [self tile];
 }
@@ -936,7 +937,10 @@ Notifies the delegate when the scroll view has finished scrolling.
     _verticalRuler = aRulerView;
 
     if (_verticalRuler)
+    {
+        [_verticalRuler setScrollView:self];
         [self addSubview:_verticalRuler];
+    }
 
     [self tile];
 }
@@ -1037,7 +1041,7 @@ Notifies the delegate when the scroll view has finished scrolling.
 
         if (_cornerView)
         {
-            [_cornerView setHidden:!SHOULD_SHOW_CORNER_VIEW()];
+            [_cornerView setHidden:![self _shouldShowCornerView]];
             [self addSubview:_cornerView];
         }
     }
@@ -1080,7 +1084,7 @@ Notifies the delegate when the scroll view has finished scrolling.
 
     frame.size.height = CGRectGetHeight([headerView frame]);
 
-    if (SHOULD_SHOW_CORNER_VIEW())
+    if ([self _shouldShowCornerView])
         frame.size.width -= CGRectGetWidth([self _cornerViewFrame]);
 
     return frame;
@@ -1319,8 +1323,8 @@ Notifies the delegate when the scroll view has finished scrolling.
     var showHorizontalRuler = _rulersVisible && _hasHorizontalRuler && _horizontalRuler,
         showVerticalRuler = _rulersVisible && _hasVerticalRuler && _verticalRuler;
 
-    var horizRulerThickness = showHorizontalRuler ? ([_horizontalRuler respondsToSelector:@selector(ruleThickness)] ? [_horizontalRuler ruleThickness] : 16.0) : 0.0,
-        vertRulerThickness = showVerticalRuler ? ([_verticalRuler respondsToSelector:@selector(ruleThickness)] ? [_verticalRuler ruleThickness] : 24.0) : 0.0;
+    var horizRulerThickness = showHorizontalRuler ? [_horizontalRuler requiredThickness] : 0.0,
+        vertRulerThickness = showVerticalRuler ? [_verticalRuler requiredThickness] : 0.0;
 
     if (showHorizontalRuler)
     {
@@ -1443,7 +1447,7 @@ Notifies the delegate when the scroll view has finished scrolling.
     [_headerClipView setFrame:[self _headerClipViewFrame]];
     [[_headerClipView documentView] setNeedsDisplay:YES];
     
-    if (SHOULD_SHOW_CORNER_VIEW())
+    if ([self _shouldShowCornerView])
     {
         [_cornerView setFrame:[self _cornerViewFrame]];
         [_cornerView setHidden:NO];
@@ -1469,10 +1473,7 @@ Notifies the delegate when the scroll view has finished scrolling.
             horizRulerThickness
         )];
         
-        if ([_horizontalRuler respondsToSelector:@selector(updateRuler)])
-            [_horizontalRuler updateRuler];
-        else
-            [_horizontalRuler setNeedsDisplay:YES];
+        [_horizontalRuler setNeedsDisplay:YES];
     }
 
     if (showVerticalRuler)
@@ -1484,10 +1485,7 @@ Notifies the delegate when the scroll view has finished scrolling.
             CGRectGetHeight(contentFrame)
         )];
         
-        if ([_verticalRuler respondsToSelector:@selector(updateRuler)])
-            [_verticalRuler updateRuler];
-        else
-            [_verticalRuler setNeedsDisplay:YES];
+        [_verticalRuler setNeedsDisplay:YES];
     }
 
     --_recursionCount;
@@ -1763,6 +1761,10 @@ Notifies the delegate when the scroll view has finished scrolling.
     [_headerClipView scrollToPoint:CGPointMake(contentBounds.origin.x, 0)];
 }
 
+- (BOOL)_shouldShowCornerView
+{
+    return (_scrollerStyle === CPScrollerStyleLegacy && _verticalScroller && ![_verticalScroller isHidden]);
+}
 @end
 
 // MARK: -
@@ -1907,6 +1909,10 @@ var CPScrollViewContentViewKey          = @"CPScrollViewContentView",
         _verticalRuler          = [aCoder decodeObjectForKey:CPScrollViewVRulerKey];
         _horizontalRuler        = [aCoder decodeObjectForKey:CPScrollViewHRulerKey];
 
+        // A ruler does not archive its scroll view, which owns it.
+        [_verticalRuler setScrollView:self];
+        [_horizontalRuler setScrollView:self];
+
         _delegate = nil;
         _scrollTimer = nil;
         _implementedDelegateMethods = 0;
@@ -1967,4 +1973,5 @@ var CPScrollViewContentViewKey          = @"CPScrollViewContentView",
     [aCoder encodeObject:_horizontalRuler       forKey:CPScrollViewHRulerKey];
 }
 
+
 @end
diff --git a/AppKit/CPSlider.j b/AppKit/CPSlider.j
index c72f49c1d..06fdf69bc 100644
--- a/AppKit/CPSlider.j
+++ b/AppKit/CPSlider.j
@@ -1002,15 +1002,13 @@ var AFFINITY = 5;
        }
     }
 
-#if PLATFORM(DOM)
     // Don't do this in nib2cib !..
-    if (_allowsTickMarkValuesOnly)
+    if (CPDOMAvailable && _allowsTickMarkValuesOnly)
     {
         [self setObjectValue:[self closestTickMarkValueToValue:[self doubleValue]]];
         _currentTickMarkSegment = _closestTickMarkIndex;
         [self sendAction:[self action] to:[self target]];
     }
-#endif
 }
 
 /*! @ignore */
diff --git a/AppKit/CPSplitView.j b/AppKit/CPSplitView.j
index 6d9b4ff9c..363bb0029 100644
--- a/AppKit/CPSplitView.j
+++ b/AppKit/CPSplitView.j
@@ -62,28 +62,6 @@ var CPSplitViewDelegate_splitView_canCollapseSubview_
     CPSplitViewDelegate_splitViewDidResizeSubviews_                                     = 1 << 9,
     CPSplitViewDelegate_splitViewWillResizeSubviews_                                    = 1 << 10;
 
-#define SPLIT_VIEW_MAYBE_POST_WILL_RESIZE() \
-    if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0) \
-    { \
-        [self _postNotificationWillResize]; \
-        _suppressResizeNotificationsMask |= DidPostWillResizeNotification; \
-    }
-
-#define SPLIT_VIEW_MAYBE_POST_DID_RESIZE() \
-    if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0) \
-        _suppressResizeNotificationsMask |= DidSuppressResizeNotification; \
-    else \
-        [self _postNotificationDidResize];
-
-#define SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION() \
-    ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0)
-
-#define SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(shouldSuppress) \
-    if (shouldSuppress) \
-        _suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications; \
-    else \
-        _suppressResizeNotificationsMask = 0;
-
 CPSplitViewDidResizeSubviewsNotification = @"CPSplitViewDidResizeSubviewsNotification";
 CPSplitViewWillResizeSubviewsNotification = @"CPSplitViewWillResizeSubviewsNotification";
 
@@ -733,7 +711,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
  */
 - (void)adjustSubviews
 {
-    SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
+    [self _maybePostWillResizeNotification];
     [self _postNotificationWillResize];
 
     // 2 possibilities : we have, or not, enough flexible space to accomodate fixed size subviews
@@ -815,7 +793,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
             [self _distribute:remainingSpace amoung:fixedCount onFlexible:NO fromIndex:0 toIndex:nbSubviews-1];
     }
 
-    SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
+    [self _maybePostDidResizeNotification];
 
     [self layoutSubviews];
 }
@@ -1217,7 +1195,7 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
     // autosave.
     _shouldRestoreFromAutosaveUnlessFrameSize = nil;
 
-    SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(YES);
+    [self _setResizeNotificationsSuppressed:YES];
 
     var realPosition = [self _realPositionForPosition:position ofDividerAtIndex:dividerIndex],
         viewA = _arrangedSubviews[dividerIndex],
@@ -1234,9 +1212,9 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
 
     if (preSize !== frameA.size[_sizeComponent])
     {
-        SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
+        [self _maybePostWillResizeNotification];
         [_arrangedSubviews[dividerIndex] setFrame:frameA];
-        SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
+        [self _maybePostDidResizeNotification];
     }
 
     preSize = frameB.size[_sizeComponent];
@@ -1251,9 +1229,9 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
 
     if (preSize !== frameB.size[_sizeComponent] || preOrigin !== frameB.origin[_originComponent])
     {
-        SPLIT_VIEW_MAYBE_POST_WILL_RESIZE();
+        [self _maybePostWillResizeNotification];
         [_arrangedSubviews[dividerIndex + 1] setFrame:frameB];
-        SPLIT_VIEW_MAYBE_POST_DID_RESIZE();
+        [self _maybePostDidResizeNotification];
     }
 
     if (preCollapsePosition)
@@ -1266,10 +1244,10 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
 
     [self _updateRatios];
 
-    if (SPLIT_VIEW_DID_SUPPRESS_RESIZE_NOTIFICATION())
+    if ([self _didSuppressResizeNotifications])
         [self _postNotificationDidResize];
 
-    SPLIT_VIEW_SUPPRESS_RESIZE_NOTIFICATIONS(NO);
+    [self _setResizeNotificationsSuppressed:NO];
 }
 
 /*!
@@ -1291,8 +1269,11 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
 - (void)setButtonBar:(CPButtonBar)aButtonBar forDividerAtIndex:(CPUInteger)dividerIndex
 {
     // For compatibility with previous behavior
-    [aButtonBar setAutomaticResizeControl:YES];
-    [self attachButtonBar:aButtonBar];
+    if ([aButtonBar respondsToSelector:@selector(setAutomaticResizeControl:)])
+    {
+        [aButtonBar setAutomaticResizeControl:YES];
+        [self attachButtonBar:aButtonBar];
+    }
 }
 
 - (void)attachButtonBar:(CPButtonBar)aButtonBar
@@ -1489,6 +1470,36 @@ var CPThemeStatesForSplitViewDivider = @[@"dummy one as CPSplitViewDividerStyle
     return @"CPSplitView Subview Precollapse Positions " + theAutosaveName;
 }
 
+- (void)_maybePostWillResizeNotification
+{
+    if ((_suppressResizeNotificationsMask & DidPostWillResizeNotification) === 0)
+    {
+        [self _postNotificationWillResize];
+        _suppressResizeNotificationsMask |= DidPostWillResizeNotification;
+    }
+}
+
+- (void)_maybePostDidResizeNotification
+{
+    if ((_suppressResizeNotificationsMask & ShouldSuppressResizeNotifications) !== 0)
+        _suppressResizeNotificationsMask |= DidSuppressResizeNotification;
+    else
+        [self _postNotificationDidResize];
+}
+
+- (BOOL)_didSuppressResizeNotifications
+{
+    return ((_suppressResizeNotificationsMask & DidSuppressResizeNotification) !== 0);
+}
+
+- (void)_setResizeNotificationsSuppressed:(BOOL)shouldSuppress
+{
+    if (shouldSuppress)
+        _suppressResizeNotificationsMask |= ShouldSuppressResizeNotifications;
+    else
+        _suppressResizeNotificationsMask = 0;
+}
+
 @end
 
 // MARK: -
@@ -1902,5 +1913,5 @@ var CPSplitViewDelegateKey            = @"CPSplitViewDelegateKey",
     [self setDividerStyle:(shouldBePaneSplitter ? CPSplitViewDividerStylePaneSplitter : CPSplitViewDividerStyleThin)];
 }
 
-@end
 
+@end
diff --git a/AppKit/CPStringDrawing.j b/AppKit/CPStringDrawing.j
index 81cdca94d..ebf903ed9 100644
--- a/AppKit/CPStringDrawing.j
+++ b/AppKit/CPStringDrawing.j
@@ -62,7 +62,8 @@ CPCanvasStringSizingIsFunctional = NO;
     if ([self class] != [CPString class])
         return;
 
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
+
     if (CPFeatureIsCompatible(CPHTMLCanvasFeature))
     {
         if (!CPStringSizeMeasuringContext)
@@ -73,46 +74,47 @@ CPCanvasStringSizingIsFunctional = NO;
 
         CPCanvasStringSizingIsFunctional = NO;
     }
-#endif
 }
 
 - (CGSize)sizeWithFont:(CPFont)aFont inWidth:(float)aWidth
 {
     var size;
 
-#if PLATFORM(DOM)
-    var sizeCacheForFont = CPStringSizeWithFontInWidthCache[self];
-
-    if (sizeCacheForFont === undefined)
-        sizeCacheForFont = CPStringSizeWithFontInWidthCache[self] = [];
-
-    var cssString = [aFont cssString],
-        cacheKey = cssString + '_' + (aWidth ? aWidth : '0');
-
-    size = sizeCacheForFont[cacheKey];
-
-    if (size !== undefined && sizeCacheForFont.hasOwnProperty(cacheKey))
-        return CGSizeMakeCopy(size);
-
-    if (!CPCanvasStringSizingIsFunctional || aWidth)
-        size = [CPPlatformString sizeOfString:self withFont:aFont forWidth:aWidth];
-    else
+    if (CPDOMAvailable)
     {
-        if (CPPlatformHasBug(CPTextSizingAlwaysNeedsSetFontBug) || CPStringSizeMeasuringContext.font !== cssString)
-            CPStringSizeMeasuringContext.font = cssString;
+        var sizeCacheForFont = CPStringSizeWithFontInWidthCache[self];
 
-        var fontHeight = CPStringSizeWithFontHeightCache[cssString];
+        if (sizeCacheForFont === undefined)
+            sizeCacheForFont = CPStringSizeWithFontInWidthCache[self] = [];
 
-        if (fontHeight === undefined)
-            fontHeight = CPStringSizeWithFontHeightCache[cssString] = [aFont defaultLineHeightForFont];
+        var cssString = [aFont cssString],
+            cacheKey = cssString + '_' + (aWidth ? aWidth : '0');
 
-        size = CGSizeMake(CPStringSizeMeasuringContext.measureText(self).width, fontHeight);
+        size = sizeCacheForFont[cacheKey];
+
+        if (size !== undefined && sizeCacheForFont.hasOwnProperty(cacheKey))
+            return CGSizeMakeCopy(size);
+
+        if (!CPCanvasStringSizingIsFunctional || aWidth)
+            size = [CPPlatformString sizeOfString:self withFont:aFont forWidth:aWidth];
+        else
+        {
+            if (CPPlatformHasBug(CPTextSizingAlwaysNeedsSetFontBug) || CPStringSizeMeasuringContext.font !== cssString)
+                CPStringSizeMeasuringContext.font = cssString;
+
+            var fontHeight = CPStringSizeWithFontHeightCache[cssString];
+
+            if (fontHeight === undefined)
+                fontHeight = CPStringSizeWithFontHeightCache[cssString] = [aFont defaultLineHeightForFont];
+
+            size = CGSizeMake(CPStringSizeMeasuringContext.measureText(self).width, fontHeight);
+        }
+
+        sizeCacheForFont[cacheKey] = size;
     }
+    else
+        size = CGSizeMake(0, 0);
 
-    sizeCacheForFont[cacheKey] = size;
-#else
-    size = CGSizeMake(0, 0);
-#endif
     return CGSizeMakeCopy(size);
 }
 
diff --git a/AppKit/CPTabView.j b/AppKit/CPTabView.j
index 54444de55..fa52c7a63 100644
--- a/AppKit/CPTabView.j
+++ b/AppKit/CPTabView.j
@@ -982,8 +982,6 @@ var CPTabViewItemsKey               = "CPTabViewItemsKey",
 
 @end
 
-# pragma mark -
-
 // This subclass of CPSegmentedControl implements specific behaviour needed by CPTabView, which
 // differs in some ways from normal CPSegmentedControl habits :
 //
diff --git a/AppKit/CPTextField.j b/AppKit/CPTextField.j
index 9a2710160..6981ef4c8 100644
--- a/AppKit/CPTextField.j
+++ b/AppKit/CPTextField.j
@@ -279,9 +279,10 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 
 // MARK: -
 
-#if PLATFORM(DOM)
 - (DOMElement)_inputElement
 {
+    if (!CPDOMAvailable) return nil;
+
     if (!CPTextFieldDOMTextAreaElement)
     {
         CPTextFieldDOMTextAreaElement                  = document.createElement("textarea");
@@ -384,7 +385,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 
     return CPTextFieldDOMCurrentElement;
 }
-#endif
 
 - (id)initWithFrame:(CGRect)aFrame
 {
@@ -675,123 +675,122 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     _stringValue = [self stringValue];
 
 
-#if PLATFORM(DOM)
-
-    [self _setCSSStyleForInputElement];
-
-    var element = [self _inputElement];
-    element.value = _stringValue;
-
-    if (element.parentNode !== _DOMElement)
+    if (CPDOMAvailable)
     {
-        var previousOwner = CPTextFieldInputOwner;
+        [self _setCSSStyleForInputElement];
 
-        // Otherwise Chrome will fire the blur during appendChild (NotFoundError).
-        _CPTextFieldDetachInputElement(element);
+        var element = [self _inputElement];
+        element.value = _stringValue;
 
-        try
+        if (element.parentNode !== _DOMElement)
         {
-            _DOMElement.appendChild(element);
-        }
-        catch (e)
-        {
-            // Race with the Blur-Handler: retry once and yield silently.
+            var previousOwner = CPTextFieldInputOwner;
+
+            // Otherwise Chrome will fire the blur during appendChild (NotFoundError).
             _CPTextFieldDetachInputElement(element);
 
             try
             {
                 _DOMElement.appendChild(element);
             }
-            catch (e2)
+            catch (e)
             {
-                [self unsetThemeState:CPThemeStateEditing];
-                [self _updatePlaceholderState];
-                [self setNeedsLayout];
-                return NO;
+                // Race with the Blur-Handler: retry once and yield silently.
+                _CPTextFieldDetachInputElement(element);
+
+                try
+                {
+                    _DOMElement.appendChild(element);
+                }
+                catch (e2)
+                {
+                    [self unsetThemeState:CPThemeStateEditing];
+                    [self _updatePlaceholderState];
+                    [self setNeedsLayout];
+                    return NO;
+                }
+            }
+
+            if (previousOwner && previousOwner !== self)
+            {
+                [previousOwner unsetThemeState:CPThemeStateEditing];
+                [previousOwner setNeedsLayout];
             }
         }
 
-        if (previousOwner && previousOwner !== self)
+        CPTextFieldInputIsActive = YES;
+
+        if (document.attachEvent)
         {
-            [previousOwner unsetThemeState:CPThemeStateEditing];
-            [previousOwner setNeedsLayout];
+            CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
+            CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
+
+            [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
+            [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
         }
-    }
 
-    CPTextFieldInputIsActive = YES;
+        CPTextFieldInputOwner = self;
 
-    if (document.attachEvent)
-    {
-        CPTextFieldCachedSelectStartFunction = [[self window] platformWindow]._DOMBodyElement.onselectstart;
-        CPTextFieldCachedDragFunction = [[self window] platformWindow]._DOMBodyElement.ondrag;
-
-        [[self window] platformWindow]._DOMBodyElement.ondrag = function () {};
-        [[self window] platformWindow]._DOMBodyElement.onselectstart = function () {};
-    }
-
-    CPTextFieldInputOwner = self;
-
-    [[CPRunLoop mainRunLoop] performBlock:function()
-    {
-        if (CPTextFieldInputOwner !== self)
-            return;
-
-        // This will prevent to jump to the focused element
-        var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
-
-        element.focus();
-
-        [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
-
-        // Select the text if the textfield became first responder through keyboard interaction
-        if (!_willBecomeFirstResponderByClick)
+        [[CPRunLoop mainRunLoop] performBlock:function()
         {
-            [self _selectText:self immediately:YES];
-        }
-        else
-        {
-            var x            = [self convertPointFromBase:[[CPApp currentEvent] locationInWindow]].x,
-                contentInset = [self currentValueForThemeAttribute:@"content-inset"],
-                text         = [self stringValue],
-                font         = [self font];
+            if (CPTextFieldInputOwner !== self)
+                return;
 
-            switch ([self alignment]) {
-                case CPCenterTextAlignment:
+            // This will prevent to jump to the focused element
+            var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
 
-                    var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
-                        textWidth    = [text sizeWithFont:font].width;
+            element.focus();
 
-                    x -= (contentWidth - textWidth) / 2 + contentInset.left;
+            [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
 
-                    break;
+            // Select the text if the textfield became first responder through keyboard interaction
+            if (!_willBecomeFirstResponderByClick)
+            {
+                [self _selectText:self immediately:YES];
+            }
+            else
+            {
+                var x            = [self convertPointFromBase:[[CPApp currentEvent] locationInWindow]].x,
+                    contentInset = [self currentValueForThemeAttribute:@"content-inset"],
+                    text         = [self stringValue],
+                    font         = [self font];
 
-                case CPRightTextAlignment:
+                switch ([self alignment]) {
+                    case CPCenterTextAlignment:
 
-                    var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
-                        textWidth    = [text sizeWithFont:font].width;
+                        var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
+                            textWidth    = [text sizeWithFont:font].width;
 
-                    x -= (contentWidth - textWidth) + contentInset.left;
+                        x -= (contentWidth - textWidth) / 2 + contentInset.left;
 
-                    break;
+                        break;
 
-                default: // CPLeftTextAlignment, CPJustifiedTextAlignment, CPNaturalTextAlignment
+                    case CPRightTextAlignment:
 
-                    x -= contentInset.left;
+                        var contentWidth = [self bounds].size.width - contentInset.left - contentInset.right,
+                            textWidth    = [text sizeWithFont:font].width;
 
-                    break;
+                        x -= (contentWidth - textWidth) + contentInset.left;
+
+                        break;
+
+                    default: // CPLeftTextAlignment, CPJustifiedTextAlignment, CPNaturalTextAlignment
+
+                        x -= contentInset.left;
+
+                        break;
+                }
+
+                var position = [CPPlatformString charPositionOfString:text withFont:font forPoint:CGPointMake(x, 0)];
+
+                [self setSelectedRange:CPMakeRange(position, 0)];
             }
 
-            var position = [CPPlatformString charPositionOfString:text withFont:font forPoint:CGPointMake(x, 0)];
+            _willBecomeFirstResponderByClick = NO;
 
-            [self setSelectedRange:CPMakeRange(position, 0)];
-        }
-
-        _willBecomeFirstResponderByClick = NO;
-
-        [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
-    } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
-
-#endif
+            [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]];
+        } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
+    }
 
     return YES;
 }
@@ -802,13 +801,14 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 */
 - (void)_setEnableCSSSelection:(BOOL)shouldEnable
 {
-#if PLATFORM (DOM)
+    if (!CPDOMAvailable)
+        return;
+
     if (CPTexFieldCurrentCSSSelectableField)
         CPTexFieldCurrentCSSSelectableField._DOMElement.style[CPBrowserStyleProperty(@"user-select")] = @"none";
 
     CPTexFieldCurrentCSSSelectableField = self;
     _DOMElement.style[CPBrowserStyleProperty(@"user-select")] = shouldEnable ? @"text" : @"none";
-#endif
 }
 
 /*!
@@ -817,7 +817,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 */
 - (void)_setCSSStyleForInputElement
 {
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable)
+        return;
 
     var element       = [self _inputElement],
         font          = [self font],
@@ -880,41 +881,40 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 
     if (isTextArea)
         element.style.whiteSpace = _wraps ? "pre" : "nowrap";
-
-#endif
 }
 
 /*! @ignore */
 - (BOOL)resignFirstResponder
 {
-#if PLATFORM(DOM)
-    // We might have been the first responder without actually editing.
-    if (_isEditing && CPTextFieldInputOwner === self)
+    if (CPDOMAvailable)
     {
-        var element = [self _inputElement],
-            newValue = element.value,
-            error = @"";
-
-        if (newValue !== _stringValue)
+        // We might have been the first responder without actually editing.
+        if (_isEditing && CPTextFieldInputOwner === self)
         {
-            [self _setStringValue:newValue];
-        }
+            var element = [self _inputElement],
+                newValue = element.value,
+                error = @"";
 
-        // If there is a formatter, always give it a chance to reject the resignation,
-        // even if the value has not changed.
-        if ([self _valueIsValid:newValue] === NO)
-        {
-            // This will prevent to jump to the focused element
-            var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
+            if (newValue !== _stringValue)
+            {
+                [self _setStringValue:newValue];
+            }
 
-            element.focus();
+            // If there is a formatter, always give it a chance to reject the resignation,
+            // even if the value has not changed.
+            if ([self _valueIsValid:newValue] === NO)
+            {
+                // This will prevent to jump to the focused element
+                var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin];
 
-            [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
+                element.focus();
 
-            return NO;
+                [self _restorePreviousScrollingOrigin:previousScrollingOrigin];
+
+                return NO;
+            }
         }
     }
-#endif
 
     // When we are no longer the first responder we don't worry about the key status of our window anymore.
     [self _setObserveWindowKeyNotifications:NO];
@@ -949,7 +949,7 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     [self _updatePlaceholderState];
     [self setNeedsLayout];
 
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable) return;
 
     var element = [self _inputElement];
 
@@ -992,8 +992,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
         CPTextFieldCachedSelectStartFunction = nil;
         CPTextFieldCachedDragFunction = nil;
     }
-
-#endif
 }
 
 - (void)_setObserveWindowKeyNotifications:(BOOL)shouldObserve
@@ -1050,42 +1048,41 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
 
 - (BOOL)_valueIsValid:(CPString)aValue
 {
-#if PLATFORM(DOM)
-
-    var error = @"";
-
-    if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
+    if (CPDOMAvailable)
     {
-        var acceptInvalidValue = NO;
+        var error = @"";
 
-        if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
+        if ([self _setStringValue:aValue isNewValue:NO errorDescription:@ref(error)] === NO)
         {
-            var currentEvent = [CPApp currentEvent];
+            var acceptInvalidValue = NO;
 
-            if (currentEvent &&
-                CPTextFieldLastValidationFailureField === self &&
-                CPTextFieldLastValidationFailureString === aValue &&
-                CPTextFieldLastValidationFailureEvent === currentEvent)
+            if (_implementedDelegateMethods & CPTextFieldDelegate_control_didFailToFormatString_errorDescription_)
             {
-                acceptInvalidValue = CPTextFieldLastValidationFailureResult;
-            }
-            else
-            {
-                acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
+                var currentEvent = [CPApp currentEvent];
 
-                CPTextFieldLastValidationFailureField = self;
-                CPTextFieldLastValidationFailureString = aValue;
-                CPTextFieldLastValidationFailureEvent = currentEvent;
-                CPTextFieldLastValidationFailureResult = acceptInvalidValue;
+                if (currentEvent &&
+                    CPTextFieldLastValidationFailureField === self &&
+                    CPTextFieldLastValidationFailureString === aValue &&
+                    CPTextFieldLastValidationFailureEvent === currentEvent)
+                {
+                    acceptInvalidValue = CPTextFieldLastValidationFailureResult;
+                }
+                else
+                {
+                    acceptInvalidValue = [_delegate control:self didFailToFormatString:aValue errorDescription:error];
+
+                    CPTextFieldLastValidationFailureField = self;
+                    CPTextFieldLastValidationFailureString = aValue;
+                    CPTextFieldLastValidationFailureEvent = currentEvent;
+                    CPTextFieldLastValidationFailureResult = acceptInvalidValue;
+                }
             }
+
+            if (acceptInvalidValue === NO)
+                return NO;
         }
-
-        if (acceptInvalidValue === NO)
-            return NO;
     }
 
-#endif
-
     return YES;
 }
 
@@ -1193,17 +1190,18 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     if (!([self isEnabled] && [self isEditable]))
         return;
 
-#if PLATFORM(DOM)
-    var newValue = [self _inputElement].value;
-
-    if (newValue !== _stringValue)
+    if (CPDOMAvailable)
     {
-        [self _setStringValue:newValue];
-        [self _didEdit];
-    }
+        var newValue = [self _inputElement].value;
 
-    [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
-#endif
+        if (newValue !== _stringValue)
+        {
+            [self _setStringValue:newValue];
+            [self _didEdit];
+        }
+
+        [[[self window] platformWindow] _propagateCurrentDOMEvent:YES];
+    }
 }
 
 - (void)keyDown:(CPEvent)anEvent
@@ -1300,7 +1298,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     if (!([self isEnabled] && [self isEditable]))
         return;
 
-#if PLATFORM(DOM)
+    if (!CPDOMAvailable)
+        return;
 
     var oldValue = _stringValue,
         range = [self selectedRange],
@@ -1318,8 +1317,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     {
         [self _didEdit];
     }
-
-#endif
 }
 
 - (void)textDidBlur:(CPNotification)note
@@ -1482,13 +1479,9 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     else
         _stringValue = [self stringValue];
 
-#if PLATFORM(DOM)
-
-    if ((CPTextFieldInputOwner === self || [[self window] firstResponder] === self) && [[self window] isKeyWindow])
+    if (CPDOMAvailable && (CPTextFieldInputOwner === self || [[self window] firstResponder] === self) && [[self window] isKeyWindow])
         [self _inputElement].value = _stringValue;
 
-#endif
-
     [self _updatePlaceholderState];
 }
 
@@ -1618,31 +1611,34 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     {
         var wind = [self window];
 
-#if PLATFORM(DOM)
-        if ([self isEditable])
+        if (CPDOMAvailable)
         {
-            var element = [self _inputElement];
-
-            if ([wind firstResponder] === self)
+            if ([self isEditable])
             {
-                if (immediately)
-                    element.select();
-                else
-                    [[CPRunLoop mainRunLoop] performBlock:function(){ element.select(); } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
+                var element = [self _inputElement];
+
+                if ([wind firstResponder] === self)
+                {
+                    if (immediately)
+                        element.select();
+                    else
+                        [[CPRunLoop mainRunLoop] performBlock:function(){ element.select(); } argument:nil order:0 modes:[CPDefaultRunLoopMode]];
+                }
+                else if (wind != nil && [wind makeFirstResponder:self])
+                    [self _selectText:sender immediately:immediately];
+            }
+            else
+            {
+                [self setSelectedRange:CPMakeRange(0, _stringValue.length)];
             }
-            else if (wind != nil && [wind makeFirstResponder:self])
-                [self _selectText:sender immediately:immediately];
         }
         else
         {
-            [self setSelectedRange:CPMakeRange(0, _stringValue.length)];
+            // Even if we can't actually select the text we need to preserve the first
+            // responder side effect.
+            if (wind != nil && [wind firstResponder] !== self)
+                [wind makeFirstResponder:self];
         }
-#else
-        // Even if we can't actually select the text we need to preserve the first
-        // responder side effect.
-        if (wind != nil && [wind firstResponder] !== self)
-            [wind makeFirstResponder:self];
-#endif
     }
 
 }
@@ -1665,9 +1661,10 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder");
     {
         // selectedRange won't work if we're displaying our text using a 
. Instead we have to ask the browser // what's selected and hope it's right in a Cappuccino context as well. -#if PLATFORM(DOM) - stringToCopy = [[[self window] platformWindow] _selectedText]; -#endif + if (CPDOMAvailable) + { + stringToCopy = [[[self window] platformWindow] _selectedText]; + } } var pasteboard = [CPPasteboard generalPasteboard]; @@ -1747,35 +1744,34 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); if ([[self window] firstResponder] !== self) return CPMakeRange(0, 0); -#if PLATFORM(DOM) - - // we wrap this in try catch because firefox will throw an exception in certain instances - try + if (CPDOMAvailable) { - var inputElement = [self _inputElement], - selectionStart = inputElement.selectionStart, - selectionEnd = inputElement.selectionEnd; - - if ([selectionStart isKindOfClass:CPNumber]) - return CPMakeRange(selectionStart, selectionEnd - selectionStart); - - // browsers which don't support selectionStart/selectionEnd (aka IE). - var theDocument = inputElement.ownerDocument || inputElement.document, - selectionRange = theDocument.selection.createRange(), - range = inputElement.createTextRange(); - - if (range.inRange(selectionRange)) + // we wrap this in try catch because firefox will throw an exception in certain instances + try { - range.setEndPoint('EndToStart', selectionRange); - return CPMakeRange(range.text.length, selectionRange.text.length); + var inputElement = [self _inputElement], + selectionStart = inputElement.selectionStart, + selectionEnd = inputElement.selectionEnd; + + if ([selectionStart isKindOfClass:CPNumber]) + return CPMakeRange(selectionStart, selectionEnd - selectionStart); + + // browsers which don't support selectionStart/selectionEnd (aka IE). + var theDocument = inputElement.ownerDocument || inputElement.document, + selectionRange = theDocument.selection.createRange(), + range = inputElement.createTextRange(); + + if (range.inRange(selectionRange)) + { + range.setEndPoint('EndToStart', selectionRange); + return CPMakeRange(range.text.length, selectionRange.text.length); + } + } + catch (e) + { + // fall through to the return } } - catch (e) - { - // fall through to the return - } - -#endif return CPMakeRange(0, 0); } @@ -1785,7 +1781,8 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); if (![[self window] firstResponder] === self) return; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; if (![self isEditable]) { @@ -1829,7 +1826,6 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); { } } -#endif } - (void)selectAll:(id)sender @@ -1903,10 +1899,11 @@ CPTextFieldStatePlaceholder = CPThemeState("placeholder"); [self _setObjectValue:newValue useFormatter:NO]; [self setSelectedRange:CPMakeRange(range.location, 0)]; -#if PLATFORM(DOM) - // Since we just performed the deletion manually, we don't need the browser to do anything else. - [[[self window] platformWindow] _propagateCurrentDOMEvent:NO]; -#endif + if (CPDOMAvailable) + { + // Since we just performed the deletion manually, we don't need the browser to do anything else. + [[[self window] platformWindow] _propagateCurrentDOMEvent:NO]; + } } [self _didEdit]; diff --git a/AppKit/CPTextView/CPLayoutManager.j b/AppKit/CPTextView/CPLayoutManager.j index e31cbafd3..8d3572a35 100644 --- a/AppKit/CPTextView/CPLayoutManager.j +++ b/AppKit/CPTextView/CPLayoutManager.j @@ -25,10 +25,10 @@ */ @import "CPText.j" -@import "CPTextContainer.j" @import "CGContext.j" @import "CPTypesetter.j" @import "CPFont.j" +@import "CPCompatibility.j" @global _MakeRangeFromAbs @global document @@ -298,9 +298,10 @@ _oncontextmenuhandler = function () { return false; }; [_typesetter layoutGlyphsInLayoutManager:self startingAtGlyphIndex:startIndex maxNumberOfLineFragments:-1 nextGlyphIndex:nil]; -#if PLATFORM(DOM) - [self _cleanUpDOM]; -#endif + if (CPDOMAvailable) + { + [self _cleanUpDOM]; + } _isValidatingLayoutAndGlyphs = NO; } @@ -1124,6 +1125,7 @@ var _objectsInRange = function(aList, aRange) BOOL _isInvalid; BOOL _isLast; + float _wordSpacing; CGRect _fragmentRect; CGRect _usedRect; CGPoint _location; @@ -1146,7 +1148,8 @@ var _objectsInRange = function(aList, aRange) if (!aString || aString.length === 0) return nil; -#if PLATFORM(DOM) + if (!CPDOMAvailable) return nil; + var style, span = document.createElement("span"); @@ -1160,6 +1163,10 @@ var _objectsInRange = function(aList, aRange) style.whiteSpace = "pre"; style.backgroundColor = "transparent"; style.font = [aFont cssString]; + + // A justified line widens its spaces; the typesetter has added the same width to their advancements. + if (_wordSpacing) + style.wordSpacing = _wordSpacing + "px"; if (aUnderline) { @@ -1197,9 +1204,6 @@ var _objectsInRange = function(aList, aRange) span.textContent = aString; return span; -#else - return nil; -#endif } - (id)initWithRange:(CPRange)aRange textContainer:(CPTextContainer)aContainer textStorage:(CPTextStorage)textStorage @@ -1215,6 +1219,7 @@ var _objectsInRange = function(aList, aRange) _range = CPMakeRangeCopy(aRange); _textContainer = aContainer; _isInvalid = NO; + _wordSpacing = 0.0; _runs = []; _glyphsFrames = []; _glyphsOffsets = []; @@ -1495,6 +1500,9 @@ var _objectsInRange = function(aList, aRange) if (!oldFragmentRuns || !newFragmentRuns || oldFragmentRuns.length !== newFragmentRuns.length) return NO; + if (newLineFragment._wordSpacing !== _wordSpacing) + return NO; + var l = oldFragmentRuns.length; for (var i = 0; i < l; i++) diff --git a/AppKit/CPTextView/CPParagraphStyle.j b/AppKit/CPTextView/CPParagraphStyle.j index 0b8bdae7f..32f68446e 100644 --- a/AppKit/CPTextView/CPParagraphStyle.j +++ b/AppKit/CPTextView/CPParagraphStyle.j @@ -106,6 +106,24 @@ var kDefaultTabInterval = 28.0; // MARK: - CPParagraphStyle Implementation +/* + Returns YES if both arrays hold the same text lists, in the same order. A text list is + identified by the object itself, so the lists are compared by identity. +*/ +var _CPParagraphStyleTextListsAreIdentical = function(someLists, otherLists) +{ + var count = someLists ? someLists.length : 0; + + if (count !== (otherLists ? otherLists.length : 0)) + return NO; + + for (var i = 0; i < count; i++) + if (someLists[i] !== otherLists[i]) + return NO; + + return YES; +}; + var _sharedDefaultParagraphStyle = nil; @implementation CPParagraphStyle : CPObject @@ -124,6 +142,7 @@ var _sharedDefaultParagraphStyle = nil; float _paragraphSpacingBefore @accessors(readonly, property=paragraphSpacingBefore); float _defaultTabInterval @accessors(readonly, property=defaultTabInterval); CPArray _tabStops @accessors(readonly, property=tabStops); + CPArray _textLists @accessors(readonly, property=textLists); } + (CPParagraphStyle)defaultParagraphStyle @@ -168,6 +187,8 @@ var _sharedDefaultParagraphStyle = nil; [_tabStops addObject:[[CPTextTab alloc] initWithType:CPLeftTextAlignment location:i * kDefaultTabInterval]]; } + + _textLists = []; } return self; } @@ -190,6 +211,7 @@ var _sharedDefaultParagraphStyle = nil; _paragraphSpacingBefore = [other paragraphSpacingBefore]; _defaultTabInterval = [other defaultTabInterval]; _tabStops = [[other tabStops] copy]; + _textLists = [[other textLists] copy] || []; } return self; } @@ -229,6 +251,7 @@ var _sharedDefaultParagraphStyle = nil; _paragraphSpacingBefore = [aCoder decodeFloatForKey:@"CPParagraphStyleParagraphSpacingBefore"]; _defaultTabInterval = [aCoder decodeFloatForKey:@"CPParagraphStyleDefaultTabInterval"]; _tabStops = [aCoder decodeObjectForKey:@"CPParagraphStyleTabStops"]; + _textLists = [aCoder decodeObjectForKey:@"CPParagraphStyleTextLists"] || []; } return self; } @@ -249,6 +272,9 @@ var _sharedDefaultParagraphStyle = nil; [aCoder encodeFloat:_paragraphSpacingBefore forKey:@"CPParagraphStyleParagraphSpacingBefore"]; [aCoder encodeFloat:_defaultTabInterval forKey:@"CPParagraphStyleDefaultTabInterval"]; [aCoder encodeObject:_tabStops forKey:@"CPParagraphStyleTabStops"]; + + if ([_textLists count]) + [aCoder encodeObject:_textLists forKey:@"CPParagraphStyleTextLists"]; } // MARK: - Equality @@ -265,7 +291,8 @@ var _sharedDefaultParagraphStyle = nil; _tailIndent === [other tailIndent] && _firstLineHeadIndent === [other firstLineHeadIndent] && _lineBreakMode === [other lineBreakMode] && - [_tabStops isEqualToArray:[other tabStops]]; + [_tabStops isEqualToArray:[other tabStops]] && + _CPParagraphStyleTextListsAreIdentical(_textLists, [other textLists]); } @end @@ -377,6 +404,14 @@ var _sharedDefaultParagraphStyle = nil; _tabStops = [newTabStops mutableCopy]; } +/*! + Sets the text lists that the paragraph belongs to, outermost first. +*/ +- (void)setTextLists:(CPArray)someTextLists +{ + _textLists = [someTextLists copy] || []; +} + - (id)copyWithZone:(CPZone)aZone { // Return an immutable copy diff --git a/AppKit/CPTextView/CPRulerMarker.j b/AppKit/CPTextView/CPRulerMarker.j new file mode 100644 index 000000000..69d45ad59 --- /dev/null +++ b/AppKit/CPTextView/CPRulerMarker.j @@ -0,0 +1,504 @@ +/* + * CPRulerMarker.j + * AppKit + * + * Created by Daniel Boehringer on 11/01/2014 + * Copyright Daniel Boehringer 2014. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import + +@import "CGGeometry.j" +@import "CPEvent.j" +@import "CPGraphicsContext.j" +@import "CPImage.j" + +@class CPRulerView + +@global CPApp +@global CPHorizontalRuler + + +/*! + Distance, in points, that the pointer must move beyond the ruler before a removable + marker is removed when it is released. +*/ +var CPRulerMarkerRemovalDistance = 0.0; + +/*! + @ingroup appkit + @class CPRulerMarker + + A CPRulerMarker shows a location on a CPRulerView, such as a tab stop or a paragraph + indent. The marker is drawn by its ruler view as an image, positioned so that the + image's \c imageOrigin lies at the marker's location on the ruler's baseline. The + marker's location is expressed in the coordinate system of the ruler's client view. + + The user can drag a movable marker along the ruler, and drag a removable marker off the + ruler to remove it. The ruler's client view is consulted through the CPRulerView client + methods before and after each change. +*/ +@implementation CPRulerMarker : CPObject +{ + CPRulerView _ruler; + float _markerLocation; + CPImage _image; + CGPoint _imageOrigin; + BOOL _isMovable; + BOOL _isRemovable; + BOOL _isDragging; + BOOL _isOffRuler; + id _representedObject; + + float _trackingStartLocation; + float _trackingStartPoint; + float _trackingStartCrossPoint; + float _trackingCrossOffset; + BOOL _trackingIsAdding; + BOOL _trackingMoved; +} + +/*! + Initializes a marker for \c aRulerView at \c aLocation, drawn with \c anImage. The + marker is associated with the ruler view, but not added to it. A new marker is movable + and not removable. + @param aRulerView the ruler view the marker belongs to + @param aLocation the marker's location, in the client view's coordinate system + @param anImage the image drawn for the marker + @param anImageOrigin the point in the image, measured from its top left corner, placed + at the marker's location on the ruler's baseline + @return the initialized marker +*/ +- (id)initWithRulerView:(CPRulerView)aRulerView markerLocation:(float)aLocation image:(CPImage)anImage imageOrigin:(CGPoint)anImageOrigin +{ + self = [super init]; + + if (self) + { + _ruler = aRulerView; + _markerLocation = aLocation; + _image = anImage; + _imageOrigin = CGPointMakeCopy(anImageOrigin); + _isMovable = YES; + _isRemovable = NO; + _isDragging = NO; + _isOffRuler = NO; + } + + return self; +} + +// MARK: Getting the Ruler View + +/*! + Returns the ruler view the marker belongs to. +*/ +- (CPRulerView)ruler +{ + return _ruler; +} + +// MARK: Setting the Image + +/*! + Returns the image drawn for the marker. +*/ +- (CPImage)image +{ + return _image; +} + +/*! + Sets the image drawn for the marker. + @param anImage the new image +*/ +- (void)setImage:(CPImage)anImage +{ + if (_image === anImage) + return; + + _image = anImage; + [_ruler setNeedsDisplay:YES]; +} + +/*! + Returns the point in the image, measured from its top left corner, that is placed at + the marker's location on the ruler's baseline. +*/ +- (CGPoint)imageOrigin +{ + return _imageOrigin; +} + +/*! + Sets the point in the image, measured from its top left corner, that is placed at the + marker's location on the ruler's baseline. + @param aPoint the new image origin +*/ +- (void)setImageOrigin:(CGPoint)aPoint +{ + _imageOrigin = CGPointMakeCopy(aPoint); + [_ruler setNeedsDisplay:YES]; +} + +/*! + Returns the rectangle that the marker's image occupies, in the ruler view's coordinate + system. +*/ +- (CGRect)imageRectInRuler +{ + var imageSize = [_image size] || CGSizeMakeZero(), + point = [_ruler _rulerPointForLocation:_markerLocation], + baseline = [_ruler baselineLocation]; + + // Whole-point positions keep every marker image sharp and identical, wherever it lies on the ruler. + // While the user drags the marker outward, it follows the pointer across the ruler. + var crossPosition = baseline + (_isDragging ? _trackingCrossOffset : 0.0); + + if ([_ruler orientation] === CPHorizontalRuler) + return CGRectMake(ROUND(point - _imageOrigin.x), ROUND(crossPosition - _imageOrigin.y), imageSize.width, imageSize.height); + + return CGRectMake(ROUND(crossPosition - _imageOrigin.x), ROUND(point - _imageOrigin.y), imageSize.width, imageSize.height); +} + +/*! + Returns the extent of the marker's image on the marker side of the ruler's baseline: + above the baseline on a horizontal ruler, and to its left on a vertical ruler. +*/ +- (float)thicknessRequiredInRuler +{ + return [_ruler orientation] === CPHorizontalRuler ? _imageOrigin.y : _imageOrigin.x; +} + +// MARK: Setting Movability + +/*! + Returns \c YES if the user can drag the marker along the ruler. +*/ +- (BOOL)isMovable +{ + return _isMovable; +} + +/*! + Sets whether the user can drag the marker along the ruler. + @param shouldBeMovable \c YES to let the user move the marker +*/ +- (void)setMovable:(BOOL)shouldBeMovable +{ + _isMovable = shouldBeMovable; +} + +/*! + Returns \c YES if the user can remove the marker by dragging it off the ruler. +*/ +- (BOOL)isRemovable +{ + return _isRemovable; +} + +/*! + Sets whether the user can remove the marker by dragging it off the ruler. + @param shouldBeRemovable \c YES to let the user remove the marker +*/ +- (void)setRemovable:(BOOL)shouldBeRemovable +{ + _isRemovable = shouldBeRemovable; +} + +// MARK: Setting the Location + +/*! + Returns the marker's location, in the client view's coordinate system. +*/ +- (float)markerLocation +{ + return _markerLocation; +} + +/*! + Sets the marker's location, in the client view's coordinate system. + @param aLocation the new location +*/ +- (void)setMarkerLocation:(float)aLocation +{ + if (_markerLocation === aLocation) + return; + + _markerLocation = aLocation; + [_ruler setNeedsDisplay:YES]; +} + +// MARK: Setting the Represented Object + +/*! + Returns the object the marker represents, such as a CPTextTab. +*/ +- (id)representedObject +{ + return _representedObject; +} + +/*! + Sets the object the marker represents. + @param anObject the represented object +*/ +- (void)setRepresentedObject:(id)anObject +{ + _representedObject = anObject; +} + +// MARK: Drawing and Event Handling + +/*! + Returns \c YES while the user is dragging the marker. +*/ +- (BOOL)isDragging +{ + return _isDragging; +} + +/*! + Draws the marker's image if it lies within \c aRect, in the ruler view's coordinate + system. While a removable marker is dragged off the ruler, it is drawn faded. + @param aRect the rectangle being drawn +*/ +- (void)drawRect:(CGRect)aRect +{ + if (!_image || [_image loadStatus] !== CPImageLoadStatusCompleted) + return; + + var imageRect = [self imageRectInRuler]; + + if (!CGRectIntersectsRect(imageRect, aRect)) + return; + + var context = [[CPGraphicsContext currentContext] graphicsPort]; + + CGContextSaveGState(context); + + // Off the ruler's outer edge the marker follows the pointer and is clipped by the ruler. + // Off its inner edge it stays on its line, so it is drawn faded instead. + if (_isOffRuler && _trackingCrossOffset === 0.0) + CGContextSetAlpha(context, 0.4); + + CGContextDrawImage(context, imageRect, _image); + CGContextRestoreGState(context); +} + +/*! + Starts tracking the user's manipulation of the marker, from the mouse-down event + \c anEvent. Tracking continues until the mouse button is released. + + If \c isAdding is \c YES, the marker is not yet in the ruler: it is added when the user + releases it over the ruler, if the client view agrees. Otherwise the marker is moved, + and removed if it is removable and released away from the ruler. + @param anEvent the mouse-down event that started the manipulation + @param isAdding \c YES if the marker is being added to the ruler + @return \c YES if tracking started, or \c NO if the marker cannot be moved or removed, + or the client view refused +*/ +- (BOOL)trackMouse:(CPEvent)anEvent adding:(BOOL)isAdding +{ + if (!isAdding && !_isMovable && !_isRemovable) + return NO; + + if (isAdding && ![_ruler _clientShouldAddMarker:self]) + return NO; + + if (!isAdding && _isMovable && ![_ruler _clientShouldMoveMarker:self]) + return NO; + + _trackingIsAdding = isAdding; + _trackingMoved = NO; + _trackingStartLocation = _markerLocation; + _trackingStartPoint = [self _axisPointForEvent:anEvent]; + _trackingStartCrossPoint = [self _crossPointForEvent:anEvent]; + _trackingCrossOffset = 0.0; + _isDragging = YES; + _isOffRuler = NO; + + if (isAdding) + { + _markerLocation = [_ruler _clientWillAddMarker:self atLocation:_markerLocation]; + [_ruler _setTrackedMarker:self]; + } + + [_ruler setNeedsDisplay:YES]; + + [CPApp setTarget:self + selector:@selector(_trackMouseWithEvent:) + forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask + untilDate:nil + inMode:nil + dequeue:YES]; + + return YES; +} + +/* @ignore */ +- (void)_trackMouseWithEvent:(CPEvent)anEvent +{ + var type = [anEvent type]; + + if (type === CPLeftMouseDragged) + { + _trackingMoved = YES; + _isOffRuler = [self _isEventOffRuler:anEvent] && (_trackingIsAdding || _isRemovable); + + // As in Cocoa, the marker stays on its line while the pointer is on the ruler, and follows + // the pointer out over the ruler's outer edge, where the ruler clips it. + _trackingCrossOffset = (_trackingIsAdding || _isRemovable) ? MIN(0.0, [self _crossPointForEvent:anEvent] - _trackingStartCrossPoint) : 0.0; + + if (_trackingIsAdding || _isMovable) + { + var proposedLocation = _trackingStartLocation + [self _axisPointForEvent:anEvent] - _trackingStartPoint; + + _markerLocation = _trackingIsAdding ? [_ruler _clientWillAddMarker:self atLocation:proposedLocation] + : [_ruler _clientWillMoveMarker:self toLocation:proposedLocation]; + } + + [_ruler setNeedsDisplay:YES]; + + [CPApp setTarget:self + selector:@selector(_trackMouseWithEvent:) + forNextEventMatchingMask:CPLeftMouseDraggedMask | CPLeftMouseUpMask + untilDate:nil + inMode:nil + dequeue:YES]; + + return; + } + + if (type !== CPLeftMouseUp) + return; + + var wasOffRuler = _isOffRuler; + + _trackingCrossOffset = 0.0; + + _isDragging = NO; + _isOffRuler = NO; + + if (_trackingIsAdding) + { + [_ruler _setTrackedMarker:nil]; + + if (!wasOffRuler) + { + [_ruler addMarker:self]; + [_ruler _clientDidAddMarker:self]; + } + } + else if (wasOffRuler && [_ruler _clientShouldRemoveMarker:self]) + { + [_ruler removeMarker:self]; + [_ruler _clientDidRemoveMarker:self]; + } + else if (_trackingMoved) + { + if (wasOffRuler) + _markerLocation = _trackingStartLocation; + + if (_markerLocation !== _trackingStartLocation) + [_ruler _clientDidMoveMarker:self]; + } + + [_ruler setNeedsDisplay:YES]; +} + +/* @ignore */ +- (float)_crossPointForEvent:(CPEvent)anEvent +{ + var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil]; + + return [_ruler orientation] === CPHorizontalRuler ? point.y : point.x; +} + +/* @ignore */ +- (float)_axisPointForEvent:(CPEvent)anEvent +{ + var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil]; + + return [_ruler orientation] === CPHorizontalRuler ? point.x : point.y; +} + +/* @ignore */ +- (BOOL)_isEventOffRuler:(CPEvent)anEvent +{ + // Only distance across the ruler counts. Dragging past either end of the ruler moves the marker. + var point = [_ruler convertPoint:[anEvent locationInWindow] fromView:nil], + bounds = [_ruler bounds]; + + if ([_ruler orientation] === CPHorizontalRuler) + return point.y < CGRectGetMinY(bounds) - CPRulerMarkerRemovalDistance || point.y > CGRectGetMaxY(bounds) + CPRulerMarkerRemovalDistance; + + return point.x < CGRectGetMinX(bounds) - CPRulerMarkerRemovalDistance || point.x > CGRectGetMaxX(bounds) + CPRulerMarkerRemovalDistance; +} + +@end + + +var CPRulerMarkerRulerKey = @"CPRulerMarkerRulerKey", + CPRulerMarkerLocationKey = @"CPRulerMarkerLocationKey", + CPRulerMarkerImageKey = @"CPRulerMarkerImageKey", + CPRulerMarkerImageOriginKey = @"CPRulerMarkerImageOriginKey", + CPRulerMarkerMovableKey = @"CPRulerMarkerMovableKey", + CPRulerMarkerRemovableKey = @"CPRulerMarkerRemovableKey", + CPRulerMarkerRepresentedObjectKey = @"CPRulerMarkerRepresentedObjectKey"; + +@implementation CPRulerMarker (CPCoding) + +/*! + Initializes a marker from an archive. +*/ +- (id)initWithCoder:(CPCoder)aCoder +{ + self = [super init]; + + if (self) + { + _ruler = [aCoder decodeObjectForKey:CPRulerMarkerRulerKey]; + _markerLocation = [aCoder decodeFloatForKey:CPRulerMarkerLocationKey]; + _image = [aCoder decodeObjectForKey:CPRulerMarkerImageKey]; + _imageOrigin = [aCoder decodePointForKey:CPRulerMarkerImageOriginKey]; + _isMovable = [aCoder decodeBoolForKey:CPRulerMarkerMovableKey]; + _isRemovable = [aCoder decodeBoolForKey:CPRulerMarkerRemovableKey]; + _representedObject = [aCoder decodeObjectForKey:CPRulerMarkerRepresentedObjectKey]; + _isDragging = NO; + _isOffRuler = NO; + _trackingCrossOffset = 0.0; + } + + return self; +} + +/*! + Archives the marker. The ruler is archived conditionally. +*/ +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeConditionalObject:_ruler forKey:CPRulerMarkerRulerKey]; + [aCoder encodeFloat:_markerLocation forKey:CPRulerMarkerLocationKey]; + [aCoder encodeObject:_image forKey:CPRulerMarkerImageKey]; + [aCoder encodePoint:_imageOrigin forKey:CPRulerMarkerImageOriginKey]; + [aCoder encodeBool:_isMovable forKey:CPRulerMarkerMovableKey]; + [aCoder encodeBool:_isRemovable forKey:CPRulerMarkerRemovableKey]; + [aCoder encodeObject:_representedObject forKey:CPRulerMarkerRepresentedObjectKey]; +} + +@end diff --git a/AppKit/CPTextView/CPTextAttachment.j b/AppKit/CPTextView/CPTextAttachment.j new file mode 100644 index 000000000..76d4a41b0 --- /dev/null +++ b/AppKit/CPTextView/CPTextAttachment.j @@ -0,0 +1,367 @@ +/* + * CPTextAttachment.j + * AppKit + * + * Created by David Richardson on 25/10/2026. + * Copyright David Richardson 2026. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import +@import +@import + +@import "CPImage.j" +@import "CPImageView.j" + +/* + Posted when an attachment's displayed size changes, for example when its image has loaded. + A text view that contains the attachment lays its text out again. +*/ +_CPTextAttachmentDidChangeSizeNotification = @"_CPTextAttachmentDidChangeSizeNotification"; + +/*! + @ingroup appkit + @class CPTextAttachment + + A CPTextAttachment holds an image in rich text, as NSTextAttachment does in Cocoa. The text + holds the attachment as one attachment character (CPAttachmentCharacter), whose + CPAttachmentAttributeName attribute is the attachment. The attachment is edited, copied and + undone as that one character. + + The attachment keeps the image's data and type, and shows the image in a view. As in TextKit + 2, an attachment is drawn by a view, not by a cell. The text system lays the view out as the + character, at the view's size, with its bottom on the baseline. That size is the attachment's + bounds, or the image's natural size, scaled down to fit the width of the text, as in TextEdit. +*/ +@implementation CPTextAttachment : CPObject +{ + CPData _contents; + CPString _fileType; + CPImage _image; + CGRect _bounds; + + CPImageView _imageView; + float _maximumWidth; +} + +/*! + Initialises an attachment with image data. + @param contentData the image's data + @param aType the image's type, as a MIME type such as \c @"image/png", or a uniform type + identifier such as \c @"public.png" +*/ +- (id)initWithData:(CPData)contentData ofType:(CPString)aType +{ + if (self = [super init]) + { + _contents = contentData; + _fileType = aType; + _bounds = CGRectMakeZero(); + _maximumWidth = 0.0; + } + + return self; +} + +- (id)init +{ + return [self initWithData:nil ofType:nil]; +} + +/* @ignore + Returns an attachment made from a data URL, such as the browser's FileReader produces. +*/ ++ (CPTextAttachment)_attachmentWithDataURL:(CPString)aDataURL +{ + var match = /^data:([^;,]+)(;base64)?,(.*)$/.exec(aDataURL || ""); + + if (!match) + return nil; + + var data = match[2] ? [CPData dataWithBase64:match[3]] : [CPData dataWithRawString:decodeURIComponent(match[3])]; + + return [[self alloc] initWithData:data ofType:match[1]]; +} + +/*! + Returns the image's data. +*/ +- (CPData)contents +{ + return _contents; +} + +/*! + Sets the image's data. The image is made again from the data. +*/ +- (void)setContents:(CPData)contentData +{ + _contents = contentData; + [self _setImage:nil]; +} + +/*! + Returns the image's type. +*/ +- (CPString)fileType +{ + return _fileType; +} + +/*! + Sets the image's type. +*/ +- (void)setFileType:(CPString)aType +{ + _fileType = aType; +} + +/*! + Returns the image. It is made from the attachment's data the first time it is needed. +*/ +- (CPImage)image +{ + if (!_image && _contents) + [self _setImage:[[CPImage alloc] initWithContentsOfFile:[self _dataURL]]]; + + return _image; +} + +/*! + Sets the image. An image set this way has no data of its own unless it was made from a data + URL, so an attachment that must be archived or copied should be made with + -initWithData:ofType:. +*/ +- (void)setImage:(CPImage)anImage +{ + [self _setImage:anImage]; +} + +/*! + Returns the attachment's bounds in the text. With a zero size, the attachment takes the + image's natural size. +*/ +- (CGRect)bounds +{ + return CGRectMakeCopy(_bounds); +} + +/*! + Sets the attachment's bounds in the text. +*/ +- (void)setBounds:(CGRect)aRect +{ + _bounds = CGRectMakeCopy(aRect); + [self _updateViewSize]; +} + +// MARK: Display + +/* @ignore + Returns the view that shows the attachment in the text. The text system lays it out as the + attachment character. +*/ +- (CPView)_attachmentView +{ + if (!_imageView) + { + _imageView = [[CPImageView alloc] initWithFrame:CGRectMakeZero()]; + [_imageView setImageScaling:CPImageScaleAxesIndependently]; + [_imageView setImage:[self image]]; + [self _updateViewSize]; + } + + return _imageView; +} + +/* @ignore + Sets the widest the attachment may be shown, which is the width of the text. Returns YES if + the displayed size changes. +*/ +- (BOOL)_setMaximumWidth:(float)aWidth +{ + if (_maximumWidth === aWidth) + return NO; + + _maximumWidth = aWidth; + + return [self _updateViewSize]; +} + +/* @ignore + Returns the size at which the attachment is shown: its bounds, or the image's natural size, + scaled down in proportion to fit the maximum width. The size is zero until the image has + loaded. +*/ +- (CGSize)_displaySize +{ + var size = CGSizeMakeCopy(_bounds.size); + + if (!size.width || !size.height) + { + var image = [self image]; + + if (!image || [image loadStatus] !== CPImageLoadStatusCompleted) + return CGSizeMakeZero(); + + size = CGSizeMakeCopy([image size]); + } + + if (_maximumWidth > 0.0 && size.width > _maximumWidth) + { + size.height = ROUND(size.height * _maximumWidth / size.width); + size.width = _maximumWidth; + } + + return size; +} + +/* @ignore + Gives the view the displayed size. Returns YES if the size changes. +*/ +- (BOOL)_updateViewSize +{ + if (!_imageView) + return NO; + + var size = [self _displaySize]; + + if (CGSizeEqualToSize(size, [_imageView frameSize])) + return NO; + + [_imageView setFrameSize:size]; + + return YES; +} + +/* @ignore + Sets the image, and waits for it to load when it has not yet loaded. +*/ +- (void)_setImage:(CPImage)anImage +{ + if (_image) + [[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image]; + + _image = anImage; + + if (_image && [_image loadStatus] !== CPImageLoadStatusCompleted) + { + [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_imageDidLoad:) name:CPImageDidLoadNotification object:_image]; + [_image load]; + } + + if (_imageView) + { + [_imageView setImage:_image]; + + if ([self _updateViewSize]) + [[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self]; + } +} + +/* @ignore + The image's natural size is known once it has loaded, so the view takes its size, and the + text that contains the attachment is laid out again. +*/ +- (void)_imageDidLoad:(CPNotification)aNotification +{ + [[CPNotificationCenter defaultCenter] removeObserver:self name:CPImageDidLoadNotification object:_image]; + + if (_imageView) + [_imageView setImage:_image]; + + if ([self _updateViewSize]) + [[CPNotificationCenter defaultCenter] postNotificationName:_CPTextAttachmentDidChangeSizeNotification object:self]; +} + +/* @ignore + Returns the attachment's data as a data URL, which the browser loads as an image. +*/ +- (CPString)_dataURL +{ + return "data:" + [self _MIMEType] + ";base64," + [_contents base64]; +} + +/* @ignore + Returns the image's MIME type. A uniform type identifier is mapped to its MIME type. +*/ +- (CPString)_MIMEType +{ + var type = _fileType || @"image/png"; + + if (type.indexOf("/") !== -1) + return type; + + switch (type) + { + case @"public.jpeg": + return @"image/jpeg"; + + case @"com.compuserve.gif": + return @"image/gif"; + + case @"public.svg-image": + return @"image/svg+xml"; + + case @"org.webmproject.webp": + return @"image/webp"; + + default: + return @"image/png"; + } +} + +@end + +var CPTextAttachmentContentsKey = @"CPTextAttachmentContents", + CPTextAttachmentFileTypeKey = @"CPTextAttachmentFileType", + CPTextAttachmentBoundsKey = @"CPTextAttachmentBounds"; + +@implementation CPTextAttachment (CPCoding) + +/*! + Decodes the attachment. The image is made again from the archived data. +*/ +- (id)initWithCoder:(CPCoder)aCoder +{ + var contentsString = [aCoder decodeObjectForKey:CPTextAttachmentContentsKey]; + + self = [self initWithData:contentsString ? [CPData dataWithBase64:contentsString] : nil + ofType:[aCoder decodeObjectForKey:CPTextAttachmentFileTypeKey]]; + + if (self && [aCoder containsValueForKey:CPTextAttachmentBoundsKey]) + _bounds = [aCoder decodeRectForKey:CPTextAttachmentBoundsKey]; + + return self; +} + +/*! + Encodes the attachment's data, type and bounds. The view is not encoded. +*/ +- (void)encodeWithCoder:(CPCoder)aCoder +{ + if (_contents) + [aCoder encodeObject:[_contents base64] forKey:CPTextAttachmentContentsKey]; + + if (_fileType) + [aCoder encodeObject:_fileType forKey:CPTextAttachmentFileTypeKey]; + + if (!CGRectIsEmpty(_bounds)) + [aCoder encodeRect:_bounds forKey:CPTextAttachmentBoundsKey]; +} + +@end diff --git a/AppKit/CPTextView/CPTextContainer.j b/AppKit/CPTextView/CPTextContainer.j index d343b397a..be031bc81 100644 --- a/AppKit/CPTextView/CPTextContainer.j +++ b/AppKit/CPTextView/CPTextContainer.j @@ -112,7 +112,8 @@ CPLineMovesUp = 4; - (void)_init { - _lineFragmentPadding = 0.0; + // Cocoa's default: room at each end of a line fragment, so that text does not touch the container's edges. + _lineFragmentPadding = 5.0; _layoutManager = [[CPLayoutManager alloc] init]; [_layoutManager addTextContainer:self]; @@ -129,6 +130,9 @@ CPLineMovesUp = 4; if (oldSize.width != _size.width) { + // Image attachments fit the new width before the text is laid out again. + [_textView _setTextAttachmentsMaximumWidth:MAX(0.0, _size.width - 2.0 * _lineFragmentPadding)]; + _inResizing = YES; [_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(0, [[_layoutManager textStorage] length]) isSoft:NO @@ -193,13 +197,16 @@ CPLineMovesUp = 4; - (void)textViewFrameChanged:(CPNotification)aNotification { - var newSize = CGSizeMake(_size.width, _size.height); + // As in Cocoa, a tracking container fills the text view inside its text container inset on both sides. + var newSize = CGSizeMake(_size.width, _size.height), + frameSize = [_textView frame].size, + inset = [_textView textContainerInset]; if (_widthTracksTextView) - newSize.width = [_textView frame].size.width; + newSize.width = MAX(0.0, frameSize.width - 2.0 * inset.width); if (_heightTracksTextView) - newSize.height = [_textView frame].size.height; + newSize.height = MAX(0.0, frameSize.height - 2.0 * inset.height); [self setContainerSize:newSize]; } @@ -271,7 +278,8 @@ CPLineMovesUp = 4; var CPTextContainerSizeKey = @"CPTextContainerSizeKey", CPTextContainerLayoutManagerKey = @"CPTextContainerLayoutManagerKey", CPTextContainerWidthTracksTextViewKey = @"CPTextContainerWidthTracksTextViewKey", - CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey"; + CPTextContainerHeightTracksTextViewKey = @"CPTextContainerHeightTracksTextViewKey", + CPTextContainerLineFragmentPaddingKey = @"CPTextContainerLineFragmentPaddingKey"; @implementation CPTextContainer (CPCoding) @@ -290,6 +298,10 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey", _widthTracksTextView = [aCoder decodeBoolForKey:CPTextContainerWidthTracksTextViewKey]; _heightTracksTextView = [aCoder decodeBoolForKey:CPTextContainerHeightTracksTextViewKey]; + + // Archives made before the padding was archived keep the default from _init. + if ([aCoder containsValueForKey:CPTextContainerLineFragmentPaddingKey]) + _lineFragmentPadding = [aCoder decodeFloatForKey:CPTextContainerLineFragmentPaddingKey]; } return self; @@ -301,6 +313,7 @@ var CPTextContainerSizeKey = @"CPTextContainerSizeKey", [aCoder encodeObject:_layoutManager forKey:CPTextContainerLayoutManagerKey]; [aCoder encodeBool:_widthTracksTextView forKey:CPTextContainerWidthTracksTextViewKey]; [aCoder encodeBool:_heightTracksTextView forKey:CPTextContainerHeightTracksTextViewKey]; + [aCoder encodeFloat:_lineFragmentPadding forKey:CPTextContainerLineFragmentPaddingKey]; } @end diff --git a/AppKit/CPTextView/CPTextList.j b/AppKit/CPTextView/CPTextList.j new file mode 100644 index 000000000..8886c3c62 --- /dev/null +++ b/AppKit/CPTextView/CPTextList.j @@ -0,0 +1,403 @@ +/* + * CPTextList.j + * AppKit + * + * Created by David Richardson on 24/10/2026. + * Copyright David Richardson 2026. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +@import +@import + +@import "CPParagraphStyle.j" + +/* + CPTextListOptions +*/ +CPTextListPrependEnclosingMarker = 1 << 0; + +/* + CPTextListMarkerFormat +*/ +CPTextListMarkerBox = @"{box}"; +CPTextListMarkerCheck = @"{check}"; +CPTextListMarkerCircle = @"{circle}"; +CPTextListMarkerDiamond = @"{diamond}"; +CPTextListMarkerDisc = @"{disc}"; +CPTextListMarkerHyphen = @"{hyphen}"; +CPTextListMarkerSquare = @"{square}"; +CPTextListMarkerLowercaseHexadecimal = @"{lower-hexadecimal}"; +CPTextListMarkerUppercaseHexadecimal = @"{upper-hexadecimal}"; +CPTextListMarkerOctal = @"{octal}"; +CPTextListMarkerLowercaseAlpha = @"{lower-alpha}"; +CPTextListMarkerUppercaseAlpha = @"{upper-alpha}"; +CPTextListMarkerLowercaseLatin = @"{lower-latin}"; +CPTextListMarkerUppercaseLatin = @"{upper-latin}"; +CPTextListMarkerLowercaseRoman = @"{lower-roman}"; +CPTextListMarkerUppercaseRoman = @"{upper-roman}"; +CPTextListMarkerDecimal = @"{decimal}"; + +/* + The characters that the unordered marker formats stand for. +*/ +var CPTextListBulletCharacters = { + "box": "□", + "check": "✓", + "circle": "◦", + "diamond": "◆", + "disc": "•", + "hyphen": "⁃", + "square": "▪" + }, + CPTextListPlaceholderPattern = /\{([a-z-]+)\}/; + +/* + Returns anItemNumber as a letter sequence: 1 is "a", 26 is "z", 27 is "aa". +*/ +var _CPTextListAlphaString = function(anItemNumber) +{ + var result = ""; + + while (anItemNumber > 0) + { + anItemNumber--; + result = String.fromCharCode(97 + anItemNumber % 26) + result; + anItemNumber = Math.floor(anItemNumber / 26); + } + + return result; +}; + +/* + Returns anItemNumber as a lowercase Roman numeral. +*/ +var _CPTextListRomanString = function(anItemNumber) +{ + var values = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1], + numerals = ["m", "cm", "d", "cd", "c", "xc", "l", "xl", "x", "ix", "v", "iv", "i"], + result = ""; + + for (var i = 0; i < values.length; i++) + { + while (anItemNumber >= values[i]) + { + result += numerals[i]; + anItemNumber -= values[i]; + } + } + + return result; +}; + +/*! + @ingroup appkit + @class CPTextList + + A CPTextList describes one list in rich text: the format of its markers, its options and the + number of its first item. A paragraph belongs to the lists in its paragraph style's + \c textLists, outermost first. As in Cocoa, the markers are ordinary text at the start of + each item: a tab, the marker and another tab, placed by the paragraph style's indents and + tab stops. A list is identified by the object itself, so the paragraphs of one list share + the same CPTextList. +*/ +@implementation CPTextList : CPObject +{ + CPString _markerFormat; + unsigned _listOptions; + int _startingItemNumber; +} + +/*! + Initialises a list. + @param aFormat the marker format: one of the CPTextListMarker constants, optionally with + text around it, such as \c @"({decimal})" + @param aMask the list options, such as CPTextListPrependEnclosingMarker +*/ +- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask +{ + return [self initWithMarkerFormat:aFormat options:aMask startingItemNumber:1]; +} + +/*! + Initialises a list whose first item has the number anItemNumber. +*/ +- (id)initWithMarkerFormat:(CPString)aFormat options:(unsigned)aMask startingItemNumber:(int)anItemNumber +{ + if (self = [super init]) + { + _markerFormat = aFormat || CPTextListMarkerDisc; + _listOptions = aMask; + _startingItemNumber = anItemNumber; + } + + return self; +} + +/*! + Returns the marker format. +*/ +- (CPString)markerFormat +{ + return _markerFormat; +} + +/*! + Returns the list options. +*/ +- (unsigned)listOptions +{ + return _listOptions; +} + +/*! + Returns the number of the list's first item. The default is 1. +*/ +- (int)startingItemNumber +{ + return _startingItemNumber; +} + +/*! + Sets the number of the list's first item. +*/ +- (void)setStartingItemNumber:(int)anItemNumber +{ + _startingItemNumber = anItemNumber; +} + +/*! + Returns \c YES if the markers are numbers or letters, and \c NO if they are bullets. +*/ +- (BOOL)isOrdered +{ + var match = _markerFormat.match(CPTextListPlaceholderPattern); + + return !!match && !CPTextListBulletCharacters.hasOwnProperty(match[1]); +} + +/*! + Returns the marker for the item with the number anItemNumber, without the tabs around it. + The marker format's placeholder is replaced by the bullet or the formatted number, and the + text around the placeholder is kept. +*/ +- (CPString)markerForItemNumber:(int)anItemNumber +{ + var match = _markerFormat.match(CPTextListPlaceholderPattern); + + if (!match) + return _markerFormat; + + var name = match[1], + marker; + + if (CPTextListBulletCharacters.hasOwnProperty(name)) + marker = CPTextListBulletCharacters[name]; + else + { + switch (name) + { + case "lower-hexadecimal": + marker = anItemNumber.toString(16); + break; + + case "upper-hexadecimal": + marker = anItemNumber.toString(16).toUpperCase(); + break; + + case "octal": + marker = anItemNumber.toString(8); + break; + + case "lower-alpha": + case "lower-latin": + marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber) : String(anItemNumber); + break; + + case "upper-alpha": + case "upper-latin": + marker = anItemNumber > 0 ? _CPTextListAlphaString(anItemNumber).toUpperCase() : String(anItemNumber); + break; + + case "lower-roman": + marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber) : String(anItemNumber); + break; + + case "upper-roman": + marker = anItemNumber > 0 ? _CPTextListRomanString(anItemNumber).toUpperCase() : String(anItemNumber); + break; + + default: + marker = String(anItemNumber); + } + } + + return _markerFormat.replace(CPTextListPlaceholderPattern, marker); +} + +- (CPString)description +{ + return [super description] + " markerFormat=" + _markerFormat + " startingItemNumber=" + _startingItemNumber; +} + +@end + +var CPTextListMarkerFormatKey = @"CPTextListMarkerFormat", + CPTextListOptionsKey = @"CPTextListOptions", + CPTextListStartingItemNumberKey = @"CPTextListStartingItemNumber"; + +@implementation CPTextList (CPCoding) + +- (id)initWithCoder:(CPCoder)aCoder +{ + if (self = [super init]) + { + _markerFormat = [aCoder decodeObjectForKey:CPTextListMarkerFormatKey] || CPTextListMarkerDisc; + _listOptions = [aCoder decodeIntForKey:CPTextListOptionsKey]; + _startingItemNumber = [aCoder containsValueForKey:CPTextListStartingItemNumberKey] ? [aCoder decodeIntForKey:CPTextListStartingItemNumberKey] : 1; + } + + return self; +} + +- (void)encodeWithCoder:(CPCoder)aCoder +{ + [aCoder encodeObject:_markerFormat forKey:CPTextListMarkerFormatKey]; + [aCoder encodeInt:_listOptions forKey:CPTextListOptionsKey]; + [aCoder encodeInt:_startingItemNumber forKey:CPTextListStartingItemNumberKey]; +} + +@end + +/* + Returns YES if the character code separates paragraphs. +*/ +var _CPTextListIsParagraphSeparator = function(aCharCode) +{ + return aCharCode === 10 || aCharCode === 13 || aCharCode === 0x2029; +}; + +@implementation CPAttributedString (CPTextListAdditions) + +/* @ignore + Returns the range of the paragraph containing anIndex, including its paragraph separator. +*/ +- (CPRange)_paragraphRangeForIndex:(unsigned)anIndex +{ + var string = [self string], + length = string.length, + start = MIN(anIndex, length), + end = start; + + while (start > 0 && !_CPTextListIsParagraphSeparator(string.charCodeAt(start - 1))) + start--; + + while (end < length && !_CPTextListIsParagraphSeparator(string.charCodeAt(end))) + end++; + + if (end < length) + end++; + + return CPMakeRange(start, end - start); +} + +/* @ignore + Returns the text lists of the paragraph that starts at aParagraphStart, outermost first. +*/ +- (CPArray)_textListsOfParagraphAtIndex:(unsigned)aParagraphStart +{ + if (aParagraphStart >= [self length]) + return []; + + var paragraphStyle = [self attribute:CPParagraphStyleAttributeName atIndex:aParagraphStart effectiveRange:nil]; + + return [paragraphStyle textLists] || []; +} + +/*! + Returns the range of the paragraphs that belong to aList, around anIndex. The paragraphs + are contiguous: the range ends at the first paragraph on either side that does not belong + to the list, at any level. Returns a range with the location CPNotFound if the paragraph + at anIndex does not belong to aList. +*/ +- (CPRange)rangeOfTextList:(CPTextList)aList atIndex:(unsigned)anIndex +{ + var paragraphRange = [self _paragraphRangeForIndex:anIndex]; + + if (![[self _textListsOfParagraphAtIndex:paragraphRange.location] containsObjectIdenticalTo:aList]) + return CPMakeRange(CPNotFound, 0); + + var start = paragraphRange.location, + end = CPMaxRange(paragraphRange), + length = [self length]; + + while (start > 0) + { + var previousRange = [self _paragraphRangeForIndex:start - 1]; + + if (![[self _textListsOfParagraphAtIndex:previousRange.location] containsObjectIdenticalTo:aList]) + break; + + start = previousRange.location; + } + + while (end < length) + { + var nextRange = [self _paragraphRangeForIndex:end]; + + if (![[self _textListsOfParagraphAtIndex:nextRange.location] containsObjectIdenticalTo:aList]) + break; + + end = CPMaxRange(nextRange); + } + + return CPMakeRange(start, end - start); +} + +/*! + Returns the number of the item of aList at anIndex. The items of a list are the paragraphs + whose innermost list it is; paragraphs of lists nested in it do not count. For a paragraph + of a nested list, the result is the number of the enclosing item. Returns 0 if the + paragraph at anIndex does not belong to aList. +*/ +- (int)itemNumberInTextList:(CPTextList)aList atIndex:(unsigned)anIndex +{ + var listRange = [self rangeOfTextList:aList atIndex:anIndex]; + + if (listRange.location === CPNotFound) + return 0; + + var target = [self _paragraphRangeForIndex:anIndex].location, + itemNumber = [aList startingItemNumber] - 1, + location = listRange.location; + + while (location <= target) + { + var paragraphRange = [self _paragraphRangeForIndex:location]; + + if ([[self _textListsOfParagraphAtIndex:location] lastObject] === aList) + itemNumber++; + + if (!paragraphRange.length) + break; + + location = CPMaxRange(paragraphRange); + } + + return MAX(itemNumber, [aList startingItemNumber]); +} + +@end diff --git a/AppKit/CPTextView/CPTextStorage.j b/AppKit/CPTextView/CPTextStorage.j index 18cc8ebcc..b06f2a534 100644 --- a/AppKit/CPTextView/CPTextStorage.j +++ b/AppKit/CPTextView/CPTextStorage.j @@ -26,6 +26,7 @@ @import "CPText.j" @import "CPFont.j" +@import "CPTextAttachment.j" @class CPLayoutManager; @@ -263,16 +264,23 @@ var CPTextStorageDelegate_textStorageWillProcessEditing_ = 1 << 1, } /*! - Returns an instance of CPTextStorage that contains the provided instance of CPView. - This can be used to insert arbitrary views into the text. These views are treated as individual characters during editing. - This works only with views that conform to the CPCoding protocol + Returns an instance of CPTextStorage that contains one attachment character for anAttachment. + The attachment is treated as an individual character during editing. + + anAttachment is either a CPTextAttachment, as in Cocoa, or a view. A CPTextAttachment is + stored as the character's CPAttachmentAttributeName attribute, and its view is laid out as + the character. A view is inserted into the text directly; this works only with views that + conform to the CPCoding protocol. */ -+ (id)attributedStringWithAttachment:(CPView)someView ++ (id)attributedStringWithAttachment:(id)anAttachment { var result = [[self alloc] initWithString:_CPAttachmentCharacterAsString]; - [result setAttributes:@{_CPAttachmentView:someView} range:CPMakeRange(0, 1)]; + if ([anAttachment isKindOfClass:[CPTextAttachment class]]) + [result setAttributes:@{CPAttachmentAttributeName:anAttachment, _CPAttachmentView:[anAttachment _attachmentView]} range:CPMakeRange(0, 1)]; + else + [result setAttributes:@{_CPAttachmentView:anAttachment} range:CPMakeRange(0, 1)]; return result; } diff --git a/AppKit/CPTextView/CPTextView.j b/AppKit/CPTextView/CPTextView.j index 60cba35e1..bd14943f3 100644 --- a/AppKit/CPTextView/CPTextView.j +++ b/AppKit/CPTextView/CPTextView.j @@ -29,14 +29,15 @@ @import "CPColorPanel.j" @import "CPFontManager.j" @import "CPTextStorage.j" -@import "CPTextContainer.j" @import "CPLayoutManager.j" @import "CPParagraphStyle.j" +@import "CPTextList.j" +@import "CPTextContainer.j" +@import "CPRulerView.j" @import "_CPRTFParser.j" @import "_CPRTFProducer.j" - @class CPClipView; @class CPMenu; @class _CPSelectionBox; @@ -58,6 +59,36 @@ @global document +/* + The system clipboard. A key equivalent such as Command-C, Command-X or Command-V lets the + browser send its own clipboard event, which _CPNativeInputManager handles with the + clipboard data of that event. A menu item chosen with the mouse, or a button, has no such + event, so the text view uses the asynchronous Clipboard API instead. That API carries only + plain text in current browsers. Rich text copied in the application stays on the internal + pasteboard, and is pasted from there when the system clipboard still holds the same plain + text. +*/ + +/* + Returns YES if two clipboard strings hold the same text, ignoring the line ending + conventions that the system clipboard may apply. +*/ +var _CPTextViewClipboardStringsMatch = function(aString, anotherString) +{ + if (!aString || !anotherString) + return NO; + + return aString.replace(/\r\n?/g, "\n") === anotherString.replace(/\r\n?/g, "\n"); +}; + +/* + Returns YES if the browser provides the asynchronous Clipboard API. +*/ +var _CPTextViewHasAsyncClipboard = function() +{ + return CPDOMAvailable && typeof navigator !== "undefined" && !!navigator.clipboard && !!navigator.clipboard.readText && !!navigator.clipboard.writeText; +}; + _MakeRangeFromAbs = function(a1, a2) { return (a1 < a2) ? CPMakeRange(a1, a2 - a1) : CPMakeRange(a2, a1 - a2); @@ -122,6 +153,7 @@ var kDelegateRespondsTo_textShouldBeginEditing @implementation CPTextView : CPText { BOOL _allowsUndo @accessors(property=allowsUndo); + BOOL _usesRuler; BOOL _isHorizontallyResizable @accessors(getter=isHorizontallyResizable, setter=setHorizontallyResizable:); BOOL _isVerticallyResizable @accessors(getter=isVerticallyResizable, setter=setVerticallyResizable:); BOOL _usesFontPanel @accessors(property=usesFontPanel); @@ -149,7 +181,6 @@ var kDelegateRespondsTo_textShouldBeginEditing int _startTrackingLocation; _CPCaret _caret; - CPTimer _scrollingTimer; BOOL _scrollingDownward; CPRange _movingSelection; @@ -210,6 +241,9 @@ var kDelegateRespondsTo_textShouldBeginEditing [self _init]; [aContainer setTextView:self]; + // Fit a container that tracks the text view's size inside the text container inset. + [aContainer textViewFrameChanged:nil]; + [self setEditable:YES]; [self setSelectable:YES]; [self setRichText:NO]; @@ -242,11 +276,13 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)_init { -#if PLATFORM(DOM) + if (CPDOMAvailable) + { _DOMElement.style.cursor = "text"; -#endif + } _selectionRange = CPMakeRange(0, 0); + _usesRuler = YES; _textContainerInset = CGSizeMake(2, 0); _textContainerOrigin = CGPointMake(_bounds.origin.x, _bounds.origin.y); @@ -293,6 +329,7 @@ var kDelegateRespondsTo_textShouldBeginEditing [super _removeObservers]; [self _setObserveWindowKeyNotifications:NO]; + [[CPNotificationCenter defaultCenter] removeObserver:self name:_CPTextAttachmentDidChangeSizeNotification object:nil]; [self _removeDelegateObservers]; } @@ -303,6 +340,7 @@ var kDelegateRespondsTo_textShouldBeginEditing [super _addObservers]; [self _setObserveWindowKeyNotifications:YES]; + [[CPNotificationCenter defaultCenter] addObserver:self selector:@selector(_textAttachmentDidChangeSize:) name:_CPTextAttachmentDidChangeSizeNotification object:nil]; [self _startObservingClipView]; [self _addDelegateObservers]; } @@ -447,6 +485,15 @@ var kDelegateRespondsTo_textShouldBeginEditing // MARK: - // MARK: Copy and paste methods +/* @ignore + Returns YES if a clipboard action comes from the browser's clipboard event or from a key + equivalent. The browser then sends its own clipboard event, which carries the clipboard data. +*/ +- (BOOL)_isKeyboardClipboardAction:(id)sender +{ + return sender === _CPNativeInputManager || [sender isKindOfClass:_CPNativeInputManager] || [[CPApp currentEvent] type] === CPKeyDown; +} + - (void)copy:(id)sender { [super copy:sender]; @@ -462,7 +509,7 @@ var kDelegateRespondsTo_textShouldBeginEditing [pasteboard setString:[stringForPasting._string stringByReplacingOccurrencesOfString:_CPAttachmentCharacterAsString withString:''] forType:CPStringPboardType]; [pasteboard setString:richData forType:CPRTFPboardType]; [pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType]; - [pasteboard setString:[[CPKeyedArchiver archivedDataWithRootObject:stringForPasting] rawString] forType:_CPASPboardType]; + [pasteboard setString:[[CPKeyedArchiver archivedDataWithRootObject:[self _attributedStringForArchiving:stringForPasting]] rawString] forType:_CPASPboardType]; } else { @@ -470,6 +517,16 @@ var kDelegateRespondsTo_textShouldBeginEditing [pasteboard setString:stringForPasting._string forType:CPStringPboardType]; [pasteboard setString:_previousSelectionGranularity + '' forType:_CPSmartPboardType]; } + + // Without a browser clipboard event, the plain text goes to the system clipboard through the + // asynchronous Clipboard API. + if (selectedRange.length && ![self _isKeyboardClipboardAction:sender] && _CPTextViewHasAsyncClipboard()) + { + navigator.clipboard.writeText([pasteboard stringForType:CPStringPboardType] || "").then(nil, function(anError) + { + CPLog.warn("CPTextView: the browser refused to write to the clipboard: " + anError); + }); + } } - (void)_pasteString:(id)stringForPasting @@ -513,18 +570,74 @@ var kDelegateRespondsTo_textShouldBeginEditing } - (void)pasteAsPlainText:(id)sender { - if (![sender isKindOfClass:_CPNativeInputManager] && [[CPApp currentEvent] type] != CPAppKitDefined) - return - - [self _pasteString:[self _plainStringForPasting]]; + if (sender === _CPNativeInputManager || [sender isKindOfClass:_CPNativeInputManager]) + [self _pasteString:[self _plainStringForPasting]]; + else if (![self _isKeyboardClipboardAction:sender]) + [self _pasteFromSystemClipboardAsPlainText:YES]; } +/*! + Pastes the contents of the clipboard at the insertion point or over the selection. Rich text + is pasted as rich text in a rich text view. From a key equivalent, the browser's paste event + supplies the clipboard. From a menu item or a button, the text view reads the system + clipboard through the asynchronous Clipboard API. + @param sender the object requesting this +*/ - (void)paste:(id)sender { - if ([[CPApp currentEvent] type] != CPAppKitDefined) - return; + if (![self _isKeyboardClipboardAction:sender]) + [self _pasteFromSystemClipboardAsPlainText:NO]; +} - [self _pasteString:[self _stringForPasting]]; +/* @ignore + Reads the system clipboard asynchronously and pastes it. Without the Clipboard API, the + internal pasteboard is pasted, as before. If the browser refuses access, for example because + the user declines, nothing is pasted. +*/ +- (void)_pasteFromSystemClipboardAsPlainText:(BOOL)asPlainText +{ + if (!_CPTextViewHasAsyncClipboard()) + { + [self _pasteString:asPlainText ? [self _plainStringForPasting] : [self _stringForPasting]]; + return; + } + + var textView = self; + + navigator.clipboard.readText().then(function(aString) + { + [textView _pasteSystemClipboardString:aString asPlainText:asPlainText]; + + // The paste happens outside the run loop, so the run loop is run here to close the + // undo group and to display the change. + [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; + }, + function(anError) + { + CPLog.warn("CPTextView: the browser refused to read the clipboard: " + anError); + }); +} + +/* @ignore + Pastes aString, read from the system clipboard. When it is the plain text of the internal + pasteboard, the clipboard holds what the application copied last, so the internal pasteboard + is pasted, with its rich text. Otherwise the clipboard holds text from elsewhere: it replaces + the internal pasteboard, so that later pastes agree with it, and is pasted as plain text. +*/ +- (void)_pasteSystemClipboardString:(CPString)aString asPlainText:(BOOL)asPlainText +{ + var pasteboard = [CPPasteboard generalPasteboard]; + + if (!_CPTextViewClipboardStringsMatch(aString, [pasteboard stringForType:CPStringPboardType])) + { + if (!aString) + return; + + [pasteboard declareTypes:[CPStringPboardType] owner:nil]; + [pasteboard setString:aString forType:CPStringPboardType]; + } + + [self _pasteString:asPlainText ? [self _plainStringForPasting] : [self _stringForPasting]]; } // MARK: - @@ -689,6 +802,95 @@ var kDelegateRespondsTo_textShouldBeginEditing [[[self window] undoManager] redo]; } +/* + @ignore + Returns the undo manager that records the text view's changes, or nil when allowsUndo is NO, + so that a text view that does not allow undo registers nothing. +*/ +- (CPUndoManager)_registeringUndoManager +{ + return _allowsUndo ? [[self window] undoManager] : nil; +} + +/*! + A text view that does not allow undo does not respond to \c -undo: or \c -redo:, so those + actions continue along the responder chain to the window, as in Cocoa. +*/ +- (BOOL)respondsToSelector:(SEL)aSelector +{ + if (!_allowsUndo && (aSelector === @selector(undo:) || aSelector === @selector(redo:))) + return NO; + + return [super respondsToSelector:aSelector]; +} + +/*! + Enables the Undo and Redo items only when the undo manager has a change to undo or redo. + For \c -toggleRuler:, the item's title becomes "Hide Ruler" or "Show Ruler" to reflect the + ruler's visibility, and the item is enabled only when the text view uses a ruler and lies + in a scroll view. \c -copyRuler: is enabled in rich text, and \c -pasteRuler: in editable + rich text when the ruler pasteboard holds a ruler. \c -changeTextList: is enabled in editable + rich text, and its item is checked when it shows the list style at the selection. + \c -insertImage: is enabled in editable rich text. + \c -copyFont: is enabled in rich text, and + \c -pasteFont: in editable rich text when the font pasteboard holds a style. The superscript, + subscript and baseline actions are enabled in editable rich text. Every other item is enabled. + @param anItem the item to validate + @return \c YES if the item should be enabled +*/ +- (BOOL)validateUserInterfaceItem:(id /**/)anItem +{ + var action = [anItem action]; + + if (action === @selector(undo:)) + return [[[self window] undoManager] canUndo]; + + if (action === @selector(redo:)) + return [[[self window] undoManager] canRedo]; + + // Baseline changes apply only to editable rich text. + if (action === @selector(superscript:) || action === @selector(subscript:) || action === @selector(unscript:) || + action === @selector(raiseBaseline:) || action === @selector(lowerBaseline:)) + return [self isEditable] && [self isRichText]; + + // As in Cocoa, rulers can be copied and pasted only in rich text. + if (action === @selector(copyRuler:)) + return [self isRichText]; + + if (action === @selector(pasteRuler:)) + return [self isRichText] && [self isEditable] && !![[CPPasteboard pasteboardWithName:CPRulerPboard] availableTypeFromArray:[CPRulerPboardType]]; + + // Images can be inserted only in editable rich text. + if (action === @selector(insertImage:)) + return [self isEditable] && [self isRichText]; + + // Lists apply only to editable rich text. The item for the list style at the selection is checked. + if (action === @selector(changeTextList:)) + { + if ([anItem respondsToSelector:@selector(setState:)]) + [anItem setState:[self _textListStyleMatchesItem:anItem] ? CPOnState : CPOffState]; + + return [self isEditable] && [self isRichText]; + } + + // As in Cocoa, styles can be copied and pasted only in rich text. + if (action === @selector(copyFont:)) + return [self isRichText]; + + if (action === @selector(pasteFont:)) + return [self isRichText] && [self isEditable] && !![[CPPasteboard pasteboardWithName:CPFontPboard] availableTypeFromArray:[CPFontPboardType]]; + + if (action === @selector(toggleRuler:)) + { + if ([anItem respondsToSelector:@selector(setTitle:)]) + [anItem setTitle:[self isRulerVisible] ? @"Hide Ruler" : @"Show Ruler"]; + + return _usesRuler && !![self enclosingScrollView]; + } + + return YES; +} + // MARK: - // MARK: Accessors @@ -759,6 +961,9 @@ var kDelegateRespondsTo_textShouldBeginEditing { _textContainerInset = aSize; [self invalidateTextContainerOrigin]; + + // A container that tracks the text view's size must shrink or grow with the new inset. + [_textContainer textViewFrameChanged:nil]; } - (void)invalidateTextContainerOrigin @@ -815,17 +1020,21 @@ var kDelegateRespondsTo_textShouldBeginEditing [self sizeToFit]; [self scrollRangeToVisible:_selectionRange]; [self setNeedsDisplay:YES]; + + // An undone or redone change may alter the paragraph style even when the selection stays the same. + [self updateRuler]; } - (void)_replaceCharactersInRange:(CPRange)aRange withAttributedString:(CPString)aString selectionRange:(CPRange)selectionRange { - [[[[self window] undoManager] prepareWithInvocationTarget:self] + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(aRange.location, [aString length]) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(aRange)] selectionRange:CPMakeRangeCopy(_selectionRange)]; [_textStorage replaceCharactersInRange:aRange withAttributedString:aString]; [self _fixupReplaceForRange:selectionRange]; + [self _updateTextListMarkersAroundRange:CPMakeRange(aRange.location, [aString length])]; } - (void)insertText:(CPString)aString @@ -840,8 +1049,10 @@ var kDelegateRespondsTo_textShouldBeginEditing aString = [[CPAttributedString alloc] initWithString:aString attributes:_typingAttributes]; else if (![self isRichText]) aString = [[CPAttributedString alloc] initWithString:string attributes:_typingAttributes]; + else + [self _prepareTextAttachmentsInAttributedString:aString]; - var undoManager = [[self window] undoManager]; + var undoManager = [self _registeringUndoManager]; [undoManager setActionName:@"Replace/insert text"]; [[undoManager prepareWithInvocationTarget:self] @@ -854,7 +1065,10 @@ var kDelegateRespondsTo_textShouldBeginEditing [self didChangeValueForKey:@"objectValue"]; [self _continuouslyReverseSetBinding]; + var insertionLocation = _selectionRange.location; + [self _setSelectedRange:CPMakeRange(_selectionRange.location + [string length], 0) affinity:0 stillSelecting:NO overwriteTypingAttributes:NO]; + [self _updateTextListMarkersAroundRange:CPMakeRange(insertionLocation, [string length])]; _startTrackingLocation = _selectionRange.location; [self didChangeText]; @@ -887,7 +1101,9 @@ var kDelegateRespondsTo_textShouldBeginEditing - (void)drawRect:(CGRect)aRect { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + var range = [_layoutManager glyphRangeForBoundingRect:aRect inTextContainer:_textContainer]; for (var i = 0; i < [_selectionSpans count]; i++) @@ -924,7 +1140,6 @@ var kDelegateRespondsTo_textShouldBeginEditing } else [_caret setVisibility:NO]; -#endif } @@ -969,6 +1184,33 @@ Sets the selection to a range of characters in response to user action. @param selecting YES to behave appropriately for a continuing selection where the user is still dragging the mouse, NO otherwise. If YES, the receiver doesn’t send notifications or remove the marking from its marked text. If NO, the receiver posts an NSTextViewDidChangeSelectionNotification to the default notification center and removes the marking from marked text if the new selection is greater than the marked region. @param doOverwrite YES to override typing attributes. NO to not override. */ +/* @ignore + Returns the typing attributes for a selection, as in Cocoa: those of the first character of + a selection, or of the character before an insertion point. At the start of a paragraph the + insertion point takes the attributes of the paragraph's first character, so that it keeps + the paragraph's own style. At the end of the text it takes those of the last character. +*/ +- (CPDictionary)_typingAttributesForSelectionRange:(CPRange)aRange +{ + var string = [_textStorage string], + length = string.length, + index = aRange.location; + + if (!length) + return [_typingAttributes copy]; + + if (!aRange.length && index > 0) + { + var previousCharCode = string.charCodeAt(index - 1), + isParagraphStart = previousCharCode === 10 || previousCharCode === 13 || previousCharCode === 0x2029; + + if (!isParagraphStart || index >= length) + index--; + } + + return [_textStorage attributesAtIndex:MIN(index, length - 1) effectiveRange:nil]; +} + - (void)_setSelectedRange:(CPRange)range affinity:(CPSelectionAffinity)affinity stillSelecting:(BOOL)selecting overwriteTypingAttributes:(BOOL)doOverwrite { var maxRange = CPMakeRange(0, [_layoutManager numberOfCharacters]), @@ -1008,7 +1250,11 @@ Sets the selection to a range of characters in response to user action. isNewSelection = !CPEqualRanges(newSelectionRange, _mouseDownOldSelection); if (doOverwrite && _placeholderString == nil && isNewSelection) - [self setTypingAttributes:[_textStorage attributesAtIndex:CPMaxRange(range) effectiveRange:nil]]; + [self setTypingAttributes:[self _typingAttributesForSelectionRange:newSelectionRange]]; + + // The ruler shows the paragraph style at the selection. + if (isNewSelection) + [self updateRuler]; // Update the shared CPColorPanel with the active selection color if ([self _isFirstResponder] && [_textStorage length] > 0) @@ -1036,9 +1282,10 @@ Sets the selection to a range of characters in response to user action. [_CPNativeInputManager focusForClipboardOfTextView:self]; } -#if PLATFORM(DOM) - (CGPoint)_cumulativeOffset { + if (!CPDOMAvailable) return CGPointMake(0, 0); + var top = 0, left = 0, element = self._DOMElement; @@ -1053,7 +1300,6 @@ Sets the selection to a range of characters in response to user action. return CGPointMake(left, top); } -#endif - (CPArray)selectedRanges { @@ -1697,7 +1943,7 @@ Sets the selection to a range of characters in response to user action. changedRange = CPIntersectionRange(CPMakeRange(0, [_layoutManager numberOfCharacters]), changedRange); - [[[_window undoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(changedRange.location, 0) + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] _replaceCharactersInRange:CPMakeRange(changedRange.location, 0) withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(changedRange)] selectionRange:CPMakeRangeCopy(_selectionRange)]; [self willChangeValueForKey:@"objectValue"]; @@ -1706,6 +1952,7 @@ Sets the selection to a range of characters in response to user action. [self _continuouslyReverseSetBinding]; [self setSelectedRange:CPMakeRange(changedRange.location, 0)]; + [self _updateTextListMarkersAroundRange:CPMakeRange(changedRange.location, 0)]; [self didChangeText]; [_layoutManager _validateLayoutAndGlyphs]; [self sizeToFit]; @@ -1739,6 +1986,9 @@ Sets the selection to a range of characters in response to user action. - (void)deleteBackward:(id)sender { + if ([self _deleteBackwardInTextList]) + return; + [self deleteBackward:self handleSmart:[self _shouldUseSmartPasting] && _selectionRange.length > 0]; } @@ -1756,7 +2006,8 @@ Sets the selection to a range of characters in response to user action. - (void)cut:(id)sender { - if ([[CPApp currentEvent] type] != CPAppKitDefined) + // From a key equivalent, the browser's cut event copies and deletes the selection. + if ([self _isKeyboardClipboardAction:sender]) return; var selectedRange = [self selectedRange]; @@ -1777,9 +2028,35 @@ Sets the selection to a range of characters in response to user action. - (void)insertTab:(id)sender { + if ([self _insertTabInTextList]) + return; + [self insertText:@"\t"]; } +/*! + Shift-Tab in a list item returns the item to the enclosing list. Elsewhere it moves to the + previous key view. + @param sender the object requesting this +*/ +- (void)insertBacktab:(id)sender +{ + if ([self _insertBacktabInTextList]) + return; + + [[self window] selectPreviousKeyView:self]; +} + +/* + @ignore + As in Cocoa, an editable text view takes the Tab key and inserts a tab character. + Control-Tab still moves to the next key view. +*/ +- (BOOL)_wantsTabKey +{ + return [self isEditable]; +} + - (void)insertTabIgnoringFieldEditor:(id)sender { [self insertTab:sender]; @@ -1792,6 +2069,12 @@ Sets the selection to a range of characters in response to user action. - (void)insertNewline:(id)sender { + if ([self _insertNewlineInTextList]) + { + [[self window] _temporarilyDisableKeyEquivalentForDefaultButton]; + return; + } + [self insertLineBreak:sender]; } @@ -1839,7 +2122,9 @@ Sets the selection to a range of characters in response to user action. - (CPDictionary)_attributesForFontPanel { - var attributes = [[_textStorage attributesAtIndex:CPMaxRange(_selectionRange) effectiveRange:nil] copy]; + // The font panel shows the font that typing would use: that of the first selected character, + // or of the character before the insertion point. + var attributes = [[self _typingAttributesForSelectionRange:_selectionRange] mutableCopy]; [self _enrichEssentialTypingAttributes:attributes]; @@ -1884,7 +2169,7 @@ Sets the selection to a range of characters in response to user action. var currentAttributes = [_textStorage attributesAtIndex:range.location effectiveRange:nil] || _typingAttributes; - [[[[self window] undoManager] prepareWithInvocationTarget:self] + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] setFont:[currentAttributes objectForKey:CPFontAttributeName] || [self font] range:CPMakeRangeCopy(range)]; @@ -1898,7 +2183,7 @@ Sets the selection to a range of characters in response to user action. oldFont, attributes, scrollRange = CPMakeRange(CPMaxRange(_selectionRange), 0), - undoManager = [[self window] undoManager]; + undoManager = [self _registeringUndoManager]; [undoManager beginUndoGrouping]; @@ -1918,11 +2203,11 @@ Sets the selection to a range of characters in response to user action. } else { - attributes = [_textStorage attributesAtIndex:_selectionRange.location - longestEffectiveRange:_selectionRange - inRange:_selectionRange]; - oldFont = [attributes objectForKey:CPFontAttributeName] || [self font]; + // As in Cocoa, without a selection the change applies to the typing attributes, so + // repeated changes, such as Bigger, build on each other. + oldFont = [_typingAttributes objectForKey:CPFontAttributeName] || [self font]; [_typingAttributes setObject:[sender convertFont:oldFont] forKey:CPFontAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; } } else @@ -1962,7 +2247,7 @@ Sets the selection to a range of characters in response to user action. { var currentAttributes = [_textStorage attributesAtIndex:range.location effectiveRange:nil] || _typingAttributes; - [[[[self window] undoManager] prepareWithInvocationTarget:self] + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] setTextColor:[currentAttributes objectForKey:CPForegroundColorAttributeName] || _textColor range:CPMakeRangeCopy(range)]; @@ -1994,6 +2279,260 @@ Sets the selection to a range of characters in response to user action. [[CPFontManager sharedFontManager] addFontTrait:CPItalicFontMask]; } +// MARK: - +// MARK: Style methods + +/*! + Copies the character attributes at the start of the selection, or the typing attributes + when nothing is selected, to the font pasteboard, so that Paste Style can apply them to + other text. The attributes include the font, colours, underline and baseline. As in Cocoa, + the paragraph style is not included: Copy Ruler copies it. + @param aSender the object requesting this +*/ +- (@action)copyFont:(id)aSender +{ + var attributes = _selectionRange.length ? [_textStorage attributesAtIndex:_selectionRange.location effectiveRange:nil] : _typingAttributes, + pasteboard = [CPPasteboard pasteboardWithName:CPFontPboard]; + + [pasteboard declareTypes:[CPFontPboardType] owner:nil]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self _characterStyleFromAttributes:attributes]] forType:CPFontPboardType]; +} + +/*! + Applies the character attributes on the font pasteboard to the selected text, or to the + text typed next when nothing is selected. They replace the text's own character + attributes. Each paragraph keeps its paragraph style. + @param aSender the object requesting this +*/ +- (@action)pasteFont:(id)aSender +{ + var data = [[CPPasteboard pasteboardWithName:CPFontPboard] dataForType:CPFontPboardType]; + + if (!data) + return; + + var characterStyle = [CPKeyedUnarchiver unarchiveObjectWithData:data], + range = CPMakeRangeCopy(_selectionRange); + + if (!range.length) + { + var typingAttributes = [characterStyle mutableCopy], + paragraphStyle = [_typingAttributes objectForKey:CPParagraphStyleAttributeName]; + + if (paragraphStyle) + [typingAttributes setObject:paragraphStyle forKey:CPParagraphStyleAttributeName]; + + [self setTypingAttributes:typingAttributes]; + return; + } + + if (![self _beginCharacterAttributeChangeInRange:range]) + return; + + // Each run keeps the attributes that are not part of a style: its paragraph style and, for an + // attachment character, the attachment. + var location = range.location, + end = CPMaxRange(range); + + while (location < end) + { + var effectiveRange = CPMakeRange(0, 0), + runAttributes = [_textStorage attributesAtIndex:location effectiveRange:effectiveRange], + runRange = CPIntersectionRange(effectiveRange, range), + newAttributes = [characterStyle mutableCopy], + keptKeys = [CPParagraphStyleAttributeName, CPAttachmentAttributeName, _CPAttachmentView, _CPAttachmentInvisible]; + + if (!runRange.length) + runRange = CPMakeRange(location, end - location); + + for (var i = 0; i < keptKeys.length; i++) + { + var value = [runAttributes objectForKey:keptKeys[i]]; + + if (value) + [newAttributes setObject:value forKey:keptKeys[i]]; + } + + [_textStorage setAttributes:newAttributes range:runRange]; + + location = CPMaxRange(runRange); + } + + [self _endCharacterAttributeChangeInRange:range]; +} + +/* @ignore + Returns a copy of someAttributes without the attributes that are not part of a character + style: the paragraph style and the attachment. +*/ +- (CPDictionary)_characterStyleFromAttributes:(CPDictionary)someAttributes +{ + var characterStyle = [someAttributes mutableCopy] || [CPDictionary dictionary]; + + [characterStyle removeObjectForKey:CPParagraphStyleAttributeName]; + [characterStyle removeObjectForKey:CPAttachmentAttributeName]; + [characterStyle removeObjectForKey:_CPAttachmentView]; + [characterStyle removeObjectForKey:_CPAttachmentInvisible]; + + return characterStyle; +} + +// MARK: - +// MARK: Baseline methods + +/*! + Raises the superscript level of the selected text by one, or of the text typed next when + nothing is selected. Each run of the selection keeps its own level, raised by one. + @param aSender the object requesting this +*/ +- (@action)superscript:(id)aSender +{ + [self _changeCharacterAttribute:CPSuperscriptAttributeName byAmount:1]; +} + +/*! + Lowers the superscript level of the selected text by one, or of the text typed next when + nothing is selected. A negative level is a subscript. + @param aSender the object requesting this +*/ +- (@action)subscript:(id)aSender +{ + [self _changeCharacterAttribute:CPSuperscriptAttributeName byAmount:-1]; +} + +/*! + Removes the superscript level and the baseline offset of the selected text, or of the text + typed next when nothing is selected. + @param aSender the object requesting this +*/ +- (@action)unscript:(id)aSender +{ + [self _removeCharacterAttributes:[CPSuperscriptAttributeName, CPBaselineOffsetAttributeName]]; +} + +/*! + Raises the baseline of the selected text by one point, or of the text typed next when + nothing is selected. + @param aSender the object requesting this +*/ +- (@action)raiseBaseline:(id)aSender +{ + [self _changeCharacterAttribute:CPBaselineOffsetAttributeName byAmount:1.0]; +} + +/*! + Lowers the baseline of the selected text by one point, or of the text typed next when + nothing is selected. + @param aSender the object requesting this +*/ +- (@action)lowerBaseline:(id)aSender +{ + [self _changeCharacterAttribute:CPBaselineOffsetAttributeName byAmount:-1.0]; +} + +/* @ignore + Adds anAmount to a numeric character attribute, run by run across the selection, or in the + typing attributes when nothing is selected. A value that reaches zero removes the attribute. +*/ +- (void)_changeCharacterAttribute:(CPString)anAttributeName byAmount:(float)anAmount +{ + var range = CPMakeRangeCopy(_selectionRange); + + if (!range.length) + { + var newValue = ([_typingAttributes objectForKey:anAttributeName] || 0) + anAmount; + + if (newValue) + [_typingAttributes setObject:newValue forKey:anAttributeName]; + else + [_typingAttributes removeObjectForKey:anAttributeName]; + + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + return; + } + + if (![self _beginCharacterAttributeChangeInRange:range]) + return; + + var location = range.location, + end = CPMaxRange(range); + + while (location < end) + { + var effectiveRange = CPMakeRange(0, 0), + value = [_textStorage attribute:anAttributeName atIndex:location effectiveRange:effectiveRange] || 0, + runRange = CPIntersectionRange(effectiveRange, range), + newValue = value + anAmount; + + if (!runRange.length) + runRange = CPMakeRange(location, end - location); + + if (newValue) + [_textStorage addAttribute:anAttributeName value:newValue range:runRange]; + else + [_textStorage removeAttribute:anAttributeName range:runRange]; + + location = CPMaxRange(runRange); + } + + [self _endCharacterAttributeChangeInRange:range]; +} + +/* @ignore + Removes character attributes from the selection, or from the typing attributes when nothing + is selected. +*/ +- (void)_removeCharacterAttributes:(CPArray)attributeNames +{ + var range = CPMakeRangeCopy(_selectionRange); + + if (!range.length) + { + for (var i = 0; i < attributeNames.length; i++) + [_typingAttributes removeObjectForKey:attributeNames[i]]; + + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + return; + } + + if (![self _beginCharacterAttributeChangeInRange:range]) + return; + + for (var i = 0; i < attributeNames.length; i++) + [_textStorage removeAttribute:attributeNames[i] range:CPMakeRangeCopy(range)]; + + [self _endCharacterAttributeChangeInRange:range]; +} + +/* @ignore + Asks whether the change may happen, and registers undo for the attributes in aRange. +*/ +- (BOOL)_beginCharacterAttributeChangeInRange:(CPRange)aRange +{ + if (![self isEditable] || ![self isRichText] || ![self shouldChangeTextInRange:aRange replacementString:nil]) + return NO; + + // Undo restores the text's previous attributes, and registers the reverse for redo. + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] + _replaceCharactersInRange:CPMakeRangeCopy(aRange) + withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(aRange)] + selectionRange:CPMakeRangeCopy(_selectionRange)]; + + return YES; +} + +/* @ignore + Lays the changed text out again and shows it. +*/ +- (void)_endCharacterAttributeChangeInRange:(CPRange)aRange +{ + [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(aRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(aRange)]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; + [self didChangeText]; +} + - (void)alignLeft:(id)sender { [self _setAlignment:CPLeftTextAlignment]; @@ -2036,7 +2575,7 @@ Sets the selection to a range of characters in response to user action. if (_selectionRange.length > 0) { // Add rudimentary undo support - var undoManager = [[self window] undoManager]; + var undoManager = [self _registeringUndoManager]; if (undoManager) { [[undoManager prepareWithInvocationTarget:self] @@ -2090,10 +2629,6 @@ Sets the selection to a range of characters in response to user action. return 0; } -- (BOOL)isRulerVisible -{ - return NO; -} - (void)replaceCharactersInRange:(CPRange)aRange withString:(CPString)aString { @@ -2129,6 +2664,9 @@ Sets the selection to a range of characters in response to user action. rect = CGRectUnion([_layoutManager boundingRectForGlyphRange:CPMakeRange(0, 1) inTextContainer:_textContainer], [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, [_layoutManager numberOfCharacters] - 2), 1) inTextContainer:_textContainer]); + // The last character can be taller than its line's text, as an image attachment at the end + // of the text is, so the view also fits it. + rect = CGRectUnion(rect, [_layoutManager boundingRectForGlyphRange:CPMakeRange(MAX(0, [_layoutManager numberOfCharacters] - 1), 1) inTextContainer:_textContainer]); if ([_layoutManager extraLineFragmentTextContainer] === _textContainer) rect = CGRectUnion(rect, [_layoutManager extraLineFragmentRect]); @@ -2331,7 +2869,8 @@ Sets the selection to a range of characters in response to user action. if (!numberOfGlyphs) { var font = [_typingAttributes objectForKey:CPFontAttributeName] || [self font]; - return CGRectMake(1, ([font ascender] - [font descender]) * 0.5 + _textContainerOrigin.y, 1, [font size]); + // Like the empty line after text, the caret starts inside the line fragment padding. + return CGRectMake(MAX(1.0, [_textContainer lineFragmentPadding]), ([font ascender] - [font descender]) * 0.5 + _textContainerOrigin.y, 1, [font size]); } // cursor is at a newline character -> jump to next line @@ -2465,366 +3004,654 @@ Sets the selection to a range of characters in response to user action. @end -@implementation CPTextView (CPRulerSupport) +/* + The represented objects of the paragraph indent markers. Tab stop markers represent + their CPTextTab. +*/ +var CPTextViewFirstLineIndentMarker = @"CPTextViewFirstLineIndentMarker", + CPTextViewHeadIndentMarker = @"CPTextViewHeadIndentMarker", + CPTextViewTailIndentMarker = @"CPTextViewTailIndentMarker"; -- (void)updateRuler +/* + The thickness the text view reserves for its markers above the ruler's baseline. +*/ +var CPTextViewRulerMarkerThickness = 9.0; + +/* + The marker images, created on first use: { image, origin } by name. +*/ +var CPTextViewRulerMarkerImages = nil; + +var compareTabStops = function(obj1, obj2, context) { - var scrollView = [self enclosingScrollView]; - if (!scrollView || ![scrollView hasHorizontalRuler] || ![scrollView rulersVisible]) - return; + if ([obj1 location] < [obj2 location]) + return CPOrderedAscending; - var ruler = [scrollView horizontalRulerView]; - if (!ruler) - return; + if ([obj1 location] > [obj2 location]) + return CPOrderedDescending; - // Do not rebuild markers if user is currently dragging one - if ([ruler draggingMarker]) - return; + return CPOrderedSame; +}; - var selectedRange = [self selectedRange], - paragraphStyle = [CPParagraphStyle defaultParagraphStyle], - currentAttributes = _typingAttributes; +/* + Returns an SVG image of the given size. SVG images stay sharp at any display resolution. +*/ +var _CPTextViewRulerSVGImage = function(aWidth, aHeight, aBody) +{ + var svg = '' + aBody + ''; - var textLength = [_textStorage length]; - if (textLength > 0) + return [[CPImage alloc] initWithContentsOfFile:@"data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg) size:CGSizeMake(aWidth, aHeight)]; +}; + +/* + Returns the image and image origin for a marker. Tab stop images are triangles above + the ruler's baseline: a left tab's flat side and a right tab's flat side lie at the + stop, and a centre or decimal tab points down at it. The first line indent marker + points down at the baseline from above, and the head and tail indent markers point up + at it from below. +*/ +var _CPTextViewRulerMarkerImage = function(aName) +{ + if (!CPTextViewRulerMarkerImages) { - var charIndex = selectedRange.location; + // Tab stops keep the triangles that the ruler has always shown: pointing right for a left tab, left for a + // right tab and down for a centre or decimal tab, with the flat side or the point at the stop. + var tab = 'fill="#505050" stroke="#303030" stroke-width="1" stroke-linejoin="round"', + triangle = 'fill="#606060" stroke="#303030" stroke-width="1"'; - if (charIndex >= textLength) - charIndex = textLength - 1; - - if (charIndex < 0) - charIndex = 0; - - currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; + CPTextViewRulerMarkerImages = { + leftTab: { image: _CPTextViewRulerSVGImage(8, 8, ''), origin: CGPointMake(0.0, 8.0) }, + rightTab: { image: _CPTextViewRulerSVGImage(8, 8, ''), origin: CGPointMake(8.0, 8.0) }, + centerTab: { image: _CPTextViewRulerSVGImage(9, 8, ''), origin: CGPointMake(4.0, 8.0) }, + decimalTab: { image: _CPTextViewRulerSVGImage(9, 8, ''), origin: CGPointMake(4.0, 8.0) }, + downIndent: { image: _CPTextViewRulerSVGImage(9, 6, ''), origin: CGPointMake(4.0, 6.0) }, + upIndent: { image: _CPTextViewRulerSVGImage(9, 6, ''), origin: CGPointMake(4.0, 0.0) } + }; } - if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) - paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; + return CPTextViewRulerMarkerImages[aName]; +}; - var markers = [], +/* + Returns the name of the marker image for a tab stop of the given type. +*/ +var _CPTextViewRulerImageNameForTab = function(aTab) +{ + switch ([aTab tabStopType]) + { + case CPRightTabStopType: + return "rightTab"; + + case CPCenterTabStopType: + return "centerTab"; + + case CPDecimalTabStopType: + return "decimalTab"; + + default: + return "leftTab"; + } +}; + +/*! + Ruler support. A text view in a scroll view can show a horizontal ruler with the tab + stops and indents of the paragraph at the selection. The user changes them by dragging + the markers, adds a left tab stop by clicking the ruler, and removes a tab stop by + dragging it off the ruler. Marker locations are in the text view's coordinate system, + and the ruler's zero lines up with the start of the text. +*/ +@implementation CPTextView (CPRulerSupport) + +// MARK: Ruler Control + +/*! + Returns \c YES if the text view uses a ruler. The default is \c YES. +*/ +- (BOOL)usesRuler +{ + return _usesRuler; +} + +/*! + Sets whether the text view uses a ruler. A text view that does not use a ruler hides it. + @param shouldUseRuler \c YES to use a ruler +*/ +- (void)setUsesRuler:(BOOL)shouldUseRuler +{ + if (_usesRuler === shouldUseRuler) + return; + + _usesRuler = shouldUseRuler; + + if (_usesRuler) + [self updateRuler]; + else + [self setRulerVisible:NO]; +} + +/*! + Returns \c YES if the enclosing scroll view shows its horizontal ruler. +*/ +- (BOOL)isRulerVisible +{ + var scrollView = [self enclosingScrollView]; + + return !!scrollView && [scrollView hasHorizontalRuler] && [scrollView rulersVisible]; +} + +/*! + Shows or hides the enclosing scroll view's horizontal ruler. The ruler is shown only if + the text view uses a ruler. + @param shouldBeVisible \c YES to show the ruler +*/ +- (void)setRulerVisible:(BOOL)shouldBeVisible +{ + var scrollView = [self enclosingScrollView]; + + if (!scrollView || (shouldBeVisible && !_usesRuler)) + return; + + if (shouldBeVisible) + [scrollView setHasHorizontalRuler:YES]; + + [scrollView setRulersVisible:shouldBeVisible]; + + if (shouldBeVisible) + [self updateRuler]; +} + +/*! + Shows the ruler if it is hidden, and hides it if it is shown. + @param aSender the object requesting this +*/ +- (@action)toggleRuler:(id)aSender +{ + [self setRulerVisible:![self isRulerVisible]]; +} + +/*! + Copies the paragraph style at the selection, with its tab stops, indents and alignment, + to the ruler pasteboard, so that Paste Ruler can apply it to other paragraphs. + @param aSender the object requesting this +*/ +- (@action)copyRuler:(id)aSender +{ + var pasteboard = [CPPasteboard pasteboardWithName:CPRulerPboard]; + + [pasteboard declareTypes:[CPRulerPboardType] owner:nil]; + [pasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[self _rulerParagraphStyle]] forType:CPRulerPboardType]; +} + +/*! + Applies the paragraph style on the ruler pasteboard to the selected paragraphs, or to + the paragraph at the insertion point. When the style or the paragraphs belong to text + lists, the whole paragraphs change, and their list markers are added, removed and + numbered as for Format > List. + @param aSender the object requesting this +*/ +- (@action)pasteRuler:(id)aSender +{ + var data = [[CPPasteboard pasteboardWithName:CPRulerPboard] dataForType:CPRulerPboardType]; + + if (!data) + return; + + var paragraphStyle = [CPKeyedUnarchiver unarchiveObjectWithData:data]; + + if ([[paragraphStyle textLists] count] || [self _selectionTouchesTextList]) + [self _pasteRulerParagraphStyleWithTextLists:paragraphStyle]; + else + [self _applyRulerParagraphStyle:paragraphStyle]; +} + +/*! + Shows the tab stops and indents of the paragraph at the selection on the ruler, and + lines the ruler's zero up with the start of the text. Does nothing while the user drags + a marker, or when the ruler is hidden. +*/ +- (void)updateRuler +{ + if (!_usesRuler || ![self isRulerVisible]) + return; + + var scrollView = [self enclosingScrollView], + ruler = [scrollView horizontalRulerView]; + + if (!ruler || [self _rulerHasDraggedMarker:ruler]) + return; + + [ruler setClientView:self]; + [ruler setReservedThicknessForMarkers:CPTextViewRulerMarkerThickness]; + [ruler setOriginOffset:[self convertPoint:CGPointMake([self _rulerTextStart], 0.0) toView:[scrollView documentView]].x]; + + var paragraphStyle = [self _rulerParagraphStyle], + markers = [], tabStops = [paragraphStyle tabStops], count = [tabStops count]; for (var i = 0; i < count; i++) - { - var tab = [tabStops objectAtIndex:i], - marker = [[CPRulerMarker alloc] initWithRulerView:ruler - markerLocation:[tab location] - imageValue:[tab location] - representedObject:tab]; - [markers addObject:marker]; - } + markers.push([self _rulerMarkerForTab:tabStops[i] inRuler:ruler]); - var firstLineMarker = [[CPRulerMarker alloc] initWithRulerView:ruler - markerLocation:[paragraphStyle firstLineHeadIndent] - imageValue:[paragraphStyle firstLineHeadIndent] - representedObject:@"CPFirstLineIndent"]; - [markers addObject:firstLineMarker]; - - var headMarker = [[CPRulerMarker alloc] initWithRulerView:ruler - markerLocation:[paragraphStyle headIndent] - imageValue:[paragraphStyle headIndent] - representedObject:@"CPHeadIndent"]; - [markers addObject:headMarker]; + markers.push([self _rulerIndentMarker:CPTextViewFirstLineIndentMarker imageName:"downIndent" location:[self _rulerLocationForIndent:[paragraphStyle firstLineHeadIndent]] inRuler:ruler]); + markers.push([self _rulerIndentMarker:CPTextViewHeadIndentMarker imageName:"upIndent" location:[self _rulerLocationForIndent:[paragraphStyle headIndent]] inRuler:ruler]); + markers.push([self _rulerIndentMarker:CPTextViewTailIndentMarker imageName:"upIndent" location:[self _rulerLocationForTailIndent:[paragraphStyle tailIndent]] inRuler:ruler]); [ruler setMarkers:markers]; } -var compareTabStops = function(obj1, obj2, context) { - if ([obj1 location] < [obj2 location]) return CPOrderedAscending; - if ([obj1 location] > [obj2 location]) return CPOrderedDescending; - return CPOrderedSame; -}; +// MARK: Ruler Client Methods -- (void)rulerView:(CPRulerView)rulerView didAddMarker:(CPRulerMarker)marker +/*! + Adds a left tab stop where the user clicks the ruler, and lets the user drag it into + place before releasing it. +*/ +- (void)rulerView:(CPRulerView)aRulerView handleMouseDown:(CPEvent)anEvent { - var selectedRange = [self selectedRange], - paragraphStyle = [CPParagraphStyle defaultParagraphStyle], - currentAttributes = _typingAttributes; - - var textLength = [_textStorage length], - charIndex = selectedRange.location; - - if (textLength > 0) - { - if (charIndex >= textLength) - charIndex = textLength - 1; - - if (charIndex < 0) - charIndex = 0; - - currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; - } - - if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) - paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; - - var mutableStyle = [paragraphStyle mutableCopy], - newTab = [[CPTextTab alloc] initWithType:CPLeftTextAlignment location:[marker imageValue]], - tabs = [[mutableStyle tabStops] mutableCopy]; - - [tabs addObject:newTab]; - [tabs sortUsingFunction:compareTabStops context:nil]; - - [mutableStyle setTabStops:tabs]; - [marker setRepresentedObject:newTab]; - - var targetRange = selectedRange; - if (targetRange.length === 0 && textLength > 0) - targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; - - if (targetRange.length > 0) - { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; - [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; - } - - [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; - [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; -} - -- (void)rulerView:(CPRulerView)rulerView didMoveMarker:(CPRulerMarker)marker -{ - var selectedRange = [self selectedRange], - paragraphStyle = [CPParagraphStyle defaultParagraphStyle], - currentAttributes = _typingAttributes; - - var textLength = [_textStorage length], - charIndex = selectedRange.location; - - if (textLength > 0) - { - if (charIndex >= textLength) - charIndex = textLength - 1; - - if (charIndex < 0) - charIndex = 0; - - currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; - } - - if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) - paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; - - var mutableStyle = [paragraphStyle mutableCopy], - rep = [marker representedObject]; - - if (!rep) + if (![self isEditable]) return; - // A. Handle standard tab stops - if ([rep isKindOfClass:[CPTextTab class]]) - { - var newTab = [[CPTextTab alloc] initWithType:[rep alignment] location:[marker imageValue]], - tabs = [[mutableStyle tabStops] mutableCopy]; + var point = [aRulerView convertPoint:[anEvent locationInWindow] fromView:nil], + location = [aRulerView _locationForRulerPoint:point.x], + tab = [[CPTextTab alloc] initWithType:CPLeftTabStopType location:location - [self _rulerTextStart]], + marker = [self _rulerMarkerForTab:tab inRuler:aRulerView]; - [tabs removeObject:rep]; - [tabs addObject:newTab]; + [marker setMarkerLocation:location]; + [aRulerView trackMarker:marker withMouseEvent:anEvent]; +} + +- (BOOL)rulerView:(CPRulerView)aRulerView shouldAddMarker:(CPRulerMarker)aMarker +{ + return [self isEditable]; +} + +- (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)aLocation +{ + return [self _constrainedRulerLocation:aLocation forMarker:aMarker]; +} + +- (void)rulerView:(CPRulerView)aRulerView didAddMarker:(CPRulerMarker)aMarker +{ + var paragraphStyle = [[self _rulerParagraphStyle] mutableCopy], + tab = [[CPTextTab alloc] initWithType:[[aMarker representedObject] tabStopType] location:[aMarker markerLocation] - [self _rulerTextStart]], + tabs = [[paragraphStyle tabStops] mutableCopy]; + + [tabs addObject:tab]; + [tabs sortUsingFunction:compareTabStops context:nil]; + [paragraphStyle setTabStops:tabs]; + + [aMarker setRepresentedObject:tab]; + [self _applyRulerParagraphStyle:paragraphStyle]; +} + +- (BOOL)rulerView:(CPRulerView)aRulerView shouldMoveMarker:(CPRulerMarker)aMarker +{ + return [self isEditable]; +} + +- (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)aLocation +{ + return [self _constrainedRulerLocation:aLocation forMarker:aMarker]; +} + +- (void)rulerView:(CPRulerView)aRulerView didMoveMarker:(CPRulerMarker)aMarker +{ + var paragraphStyle = [[self _rulerParagraphStyle] mutableCopy], + representedObject = [aMarker representedObject], + location = [aMarker markerLocation]; + + if ([representedObject isKindOfClass:[CPTextTab class]]) + { + var tab = [[CPTextTab alloc] initWithType:[representedObject tabStopType] location:location - [self _rulerTextStart]], + tabs = [[paragraphStyle tabStops] mutableCopy]; + + [tabs removeObjectAtIndex:[self _indexOfTab:representedObject inTabs:tabs]]; + [tabs addObject:tab]; [tabs sortUsingFunction:compareTabStops context:nil]; - - [mutableStyle setTabStops:tabs]; - [marker setRepresentedObject:newTab]; + [paragraphStyle setTabStops:tabs]; + [aMarker setRepresentedObject:tab]; } + else if (representedObject === CPTextViewFirstLineIndentMarker) + [paragraphStyle setFirstLineHeadIndent:location - [self _rulerTextStart]]; - // B. Handle Indentation Marker drags directly by string literal - else if (rep === @"CPFirstLineIndent") - [mutableStyle setFirstLineHeadIndent:[marker imageValue]]; - else if (rep === @"CPHeadIndent") - [mutableStyle setHeadIndent:[marker imageValue]]; - else if (rep === @"CPTailIndent") - [mutableStyle setTailIndent:[marker imageValue]]; + else if (representedObject === CPTextViewHeadIndentMarker) + [paragraphStyle setHeadIndent:location - [self _rulerTextStart]]; - var targetRange = selectedRange; + else if (representedObject === CPTextViewTailIndentMarker) + [paragraphStyle setTailIndent:[self _tailIndentForRulerLocation:location currentTailIndent:[paragraphStyle tailIndent]]]; - if (targetRange.length === 0 && textLength > 0) - targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; - - if (targetRange.length > 0) - { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; - [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; - } - - [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; - - [_layoutManager _validateLayoutAndGlyphs]; - [self sizeToFit]; - [self setNeedsDisplay:YES]; + [self _applyRulerParagraphStyle:paragraphStyle]; } -- (void)rulerView:(CPRulerView)rulerView didRemoveMarker:(CPRulerMarker)marker +- (BOOL)rulerView:(CPRulerView)aRulerView shouldRemoveMarker:(CPRulerMarker)aMarker { - var selectedRange = [self selectedRange], - paragraphStyle = [CPParagraphStyle defaultParagraphStyle], - currentAttributes = _typingAttributes; - - var textLength = [_textStorage length], - charIndex = selectedRange.location; - - if (textLength > 0) - { - if (charIndex >= textLength) charIndex = textLength - 1; - if (charIndex < 0) charIndex = 0; - currentAttributes = [_textStorage attributesAtIndex:charIndex effectiveRange:nil]; - } - - if ([currentAttributes objectForKey:CPParagraphStyleAttributeName]) - paragraphStyle = [currentAttributes objectForKey:CPParagraphStyleAttributeName]; - - var mutableStyle = [paragraphStyle mutableCopy], - oldTab = [marker representedObject]; - - if (!oldTab) - return; - - var tabs = [[mutableStyle tabStops] mutableCopy]; - [tabs removeObject:oldTab]; - [mutableStyle setTabStops:tabs]; - - var targetRange = selectedRange; - if (targetRange.length === 0 && textLength > 0) - targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; - - if (targetRange.length > 0) - { - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; - [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; - } - - [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; - [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + return [self isEditable] && [[aMarker representedObject] isKindOfClass:[CPTextTab class]]; } -- (void)rulerView:(CPRulerView)rulerView didUpdateMarker:(CPRulerMarker)marker oldTab:(id)oldTab +- (void)rulerView:(CPRulerView)aRulerView didRemoveMarker:(CPRulerMarker)aMarker { - var selectedRange = [self selectedRange], - targetRange = selectedRange; - - if (targetRange.length === 0) - targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; - - if (targetRange.length === 0) - return; - - var paragraphStyle = [[self textStorage] attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:NULL]; - if (!paragraphStyle) - paragraphStyle = [CPParagraphStyle defaultParagraphStyle]; - - var mutableStyle = [paragraphStyle mutableCopy], - newTab = [marker representedObject], - tabs = [[mutableStyle tabStops] mutableCopy]; - - [tabs removeObject:oldTab]; - [tabs addObject:newTab]; - [tabs sortUsingFunction:compareTabStops context:nil]; - [mutableStyle setTabStops:tabs]; - - [_textStorage addAttribute:CPParagraphStyleAttributeName value:mutableStyle range:CPMakeRangeCopy(targetRange)]; - [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; - - if (selectedRange.length === 0) - { - [_typingAttributes setObject:mutableStyle forKey:CPParagraphStyleAttributeName]; - [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; - } - - [_layoutManager _validateLayoutAndGlyphs]; - [self sizeToFit]; - [self setNeedsDisplay:YES]; + [self _removeRulerTab:[aMarker representedObject]]; } -- (float)rulerView:(CPRulerView)aRulerView willMoveMarker:(CPRulerMarker)aMarker toLocation:(float)proposedLocation +/* @ignore + Returns the context menu for a marker: tab stop types and removal for a tab stop, and + a reset for an indent. +*/ +- (CPMenu)_rulerView:(CPRulerView)aRulerView menuForMarker:(CPRulerMarker)aMarker { - var rep = [aMarker representedObject]; + if (![self isEditable]) + return nil; - // Indent markers can be dragged anywhere >= 0 - if (rep === @"CPFirstLineIndent" || rep === @"CPHeadIndent" || rep === @"CPTailIndent") - return Math.max(0.0, proposedLocation); + var menu = [[CPMenu alloc] initWithTitle:@""], + representedObject = [aMarker representedObject]; - var textLength = [_textStorage length]; - if (textLength === 0) - return Math.max(0.0, proposedLocation); - - var selectedRange = [self selectedRange], - targetRange = selectedRange; - - if (targetRange.length === 0) - targetRange = [self selectionRangeForProposedRange:CPMakeRange(targetRange.location, 0) granularity:CPSelectByParagraph]; - - if (targetRange.length === 0) - return Math.max(0.0, proposedLocation); - - [_layoutManager _validateLayoutAndGlyphs]; - - var theString = [_textStorage string], - minLocation = 0.0, - containerWidth = [_textContainer containerSize].width, - maxLocation = containerWidth, - currentParagraphStyle = [_textStorage attribute:CPParagraphStyleAttributeName atIndex:targetRange.location effectiveRange:nil] || [CPParagraphStyle defaultParagraphStyle], - tabStops = [currentParagraphStyle tabStops] || [], - tabCount = [tabStops count]; - - // 1. Right boundary: Tail indent (if configured) - var tailIndent = [currentParagraphStyle tailIndent]; - if (tailIndent > 0.0) - maxLocation = Math.min(maxLocation, tailIndent); - else if (tailIndent < 0.0) - maxLocation = Math.min(maxLocation, containerWidth + tailIndent); - - // 2. Identify active tab stop index - var targetTabIndex = -1; - for (var i = 0; i < tabCount; i++) + if ([representedObject isKindOfClass:[CPTextTab class]]) { - var tab = [tabStops objectAtIndex:i]; - if (tab === rep || ([tab location] === [rep location] && [tab alignment] === [rep alignment])) - { - targetTabIndex = i; - break; - } - } + var types = [[CPLeftTabStopType, @"Left Tab"], [CPCenterTabStopType, @"Center Tab"], [CPRightTabStopType, @"Right Tab"], [CPDecimalTabStopType, @"Decimal Tab"]]; - // 3. Constrain to neighboring tab stops (Left and Right) - var safetySpacer = 2.0; - - if (targetTabIndex !== -1) - { - // Left neighbor constraint (previous tab stop) - if (targetTabIndex > 0) + for (var i = 0; i < types.length; i++) { - var prevTab = [tabStops objectAtIndex:targetTabIndex - 1]; - minLocation = Math.max(minLocation, [prevTab location] + safetySpacer); + var item = [menu addItemWithTitle:types[i][1] action:@selector(_changeRulerTabType:) keyEquivalent:@""]; + + [item setTarget:self]; + [item setTag:types[i][0]]; + [item setRepresentedObject:aMarker]; + [item setState:[representedObject tabStopType] === types[i][0] ? CPOnState : CPOffState]; } - // Right neighbor constraint (next tab stop) - if (targetTabIndex + 1 < tabCount) - { - var nextTab = [tabStops objectAtIndex:targetTabIndex + 1]; - maxLocation = Math.min(maxLocation, [nextTab location] - safetySpacer); - } + [menu addItem:[CPMenuItem separatorItem]]; + + var removeItem = [menu addItemWithTitle:@"Remove Tab" action:@selector(_removeRulerTabFromMenu:) keyEquivalent:@""]; + + [removeItem setTarget:self]; + [removeItem setRepresentedObject:aMarker]; + } + else + { + var resetItem = [menu addItemWithTitle:@"Reset Indent" action:@selector(_resetRulerIndent:) keyEquivalent:@""]; + + [resetItem setTarget:self]; + [resetItem setRepresentedObject:aMarker]; + } + + return menu; +} + +/* @ignore */ +- (void)_changeRulerTabType:(id)aSender +{ + var marker = [aSender representedObject], + oldTab = [marker representedObject], + paragraphStyle = [[self _rulerParagraphStyle] mutableCopy], + tabs = [[paragraphStyle tabStops] mutableCopy], + index = [self _indexOfTab:oldTab inTabs:tabs]; + + if (index === CPNotFound) + return; + + [tabs replaceObjectAtIndex:index withObject:[[CPTextTab alloc] initWithType:[aSender tag] location:[oldTab location]]]; + [paragraphStyle setTabStops:tabs]; + [self _applyRulerParagraphStyle:paragraphStyle]; +} + +/* @ignore */ +- (void)_removeRulerTabFromMenu:(id)aSender +{ + [self _removeRulerTab:[[aSender representedObject] representedObject]]; +} + +/* @ignore */ +- (void)_resetRulerIndent:(id)aSender +{ + var representedObject = [[aSender representedObject] representedObject], + paragraphStyle = [[self _rulerParagraphStyle] mutableCopy]; + + if (representedObject === CPTextViewFirstLineIndentMarker) + [paragraphStyle setFirstLineHeadIndent:0.0]; + + else if (representedObject === CPTextViewHeadIndentMarker) + [paragraphStyle setHeadIndent:0.0]; + + else if (representedObject === CPTextViewTailIndentMarker) + [paragraphStyle setTailIndent:0.0]; + + [self _applyRulerParagraphStyle:paragraphStyle]; +} + +// MARK: Ruler Support + +/* @ignore */ +- (BOOL)_rulerHasDraggedMarker:(CPRulerView)aRulerView +{ + var markers = [aRulerView markers]; + + for (var i = 0; i < markers.length; i++) + if ([markers[i] isDragging]) + return YES; + + return NO; +} + +/* @ignore */ +- (void)_removeRulerTab:(CPTextTab)aTab +{ + var paragraphStyle = [[self _rulerParagraphStyle] mutableCopy], + tabs = [[paragraphStyle tabStops] mutableCopy], + index = [self _indexOfTab:aTab inTabs:tabs]; + + if (index === CPNotFound) + return; + + [tabs removeObjectAtIndex:index]; + [paragraphStyle setTabStops:tabs]; + [self _applyRulerParagraphStyle:paragraphStyle]; +} + +/* @ignore */ +- (CPRulerMarker)_rulerMarkerForTab:(CPTextTab)aTab inRuler:(CPRulerView)aRulerView +{ + var imageInfo = _CPTextViewRulerMarkerImage(_CPTextViewRulerImageNameForTab(aTab)), + marker = [[CPRulerMarker alloc] initWithRulerView:aRulerView + markerLocation:[self _rulerLocationForIndent:[aTab location]] + image:imageInfo.image + imageOrigin:imageInfo.origin]; + + [marker setRemovable:YES]; + [marker setRepresentedObject:aTab]; + + return marker; +} + +/* @ignore */ +- (CPRulerMarker)_rulerIndentMarker:(CPString)aRepresentedObject imageName:(CPString)anImageName location:(float)aLocation inRuler:(CPRulerView)aRulerView +{ + var imageInfo = _CPTextViewRulerMarkerImage(anImageName), + marker = [[CPRulerMarker alloc] initWithRulerView:aRulerView markerLocation:aLocation image:imageInfo.image imageOrigin:imageInfo.origin]; + + [marker setRepresentedObject:aRepresentedObject]; + + return marker; +} + +/* @ignore + Returns the position, in the text view's coordinate system, where lines of text start. +*/ +- (float)_rulerTextStart +{ + return _textContainerOrigin.x + [_textContainer lineFragmentPadding]; +} + +/* @ignore + Returns the width available to lines of text. +*/ +- (float)_rulerTextWidth +{ + return [_textContainer containerSize].width - 2.0 * [_textContainer lineFragmentPadding]; +} + +/* @ignore */ +- (float)_rulerLocationForIndent:(float)anIndent +{ + return [self _rulerTextStart] + anIndent; +} + +/* @ignore + A positive tail indent is measured from the start of the text, and a zero or negative + one from its end. +*/ +- (float)_rulerLocationForTailIndent:(float)aTailIndent +{ + if (aTailIndent > 0.0) + return [self _rulerTextStart] + aTailIndent; + + return [self _rulerTextStart] + [self _rulerTextWidth] + aTailIndent; +} + +/* @ignore + Converts a tail indent marker location back to a tail indent, keeping the way the + current tail indent is measured. +*/ +- (float)_tailIndentForRulerLocation:(float)aLocation currentTailIndent:(float)aTailIndent +{ + var fromStart = aLocation - [self _rulerTextStart]; + + if (aTailIndent > 0.0) + return fromStart; + + return MIN(0.0, fromStart - [self _rulerTextWidth]); +} + +/* @ignore */ +- (CPUInteger)_indexOfTab:(CPTextTab)aTab inTabs:(CPArray)tabs +{ + for (var i = 0; i < tabs.length; i++) + if ([tabs[i] location] === [aTab location] && [tabs[i] tabStopType] === [aTab tabStopType]) + return i; + + return CPNotFound; +} + +/* @ignore + Returns the paragraph style at the selection, or the typing attributes' style in empty text. +*/ +- (CPParagraphStyle)_rulerParagraphStyle +{ + var attributes = _typingAttributes, + textLength = [_textStorage length]; + + if (textLength > 0) + attributes = [_textStorage attributesAtIndex:MAX(0, MIN(_selectionRange.location, textLength - 1)) effectiveRange:nil]; + + return [attributes objectForKey:CPParagraphStyleAttributeName] || [CPParagraphStyle defaultParagraphStyle]; +} + +/* @ignore + Returns the range that ruler changes apply to: the selection, or the paragraph at the + insertion point. +*/ +- (CPRange)_rulerTargetRange +{ + if (_selectionRange.length > 0 || ![_textStorage length]) + return CPMakeRangeCopy(_selectionRange); + + return [self selectionRangeForProposedRange:CPMakeRange(_selectionRange.location, 0) granularity:CPSelectByParagraph]; +} + +/* @ignore + Applies a paragraph style changed through the ruler to the target range and the typing + attributes, lays the text out again and refreshes the ruler. +*/ +- (void)_applyRulerParagraphStyle:(CPParagraphStyle)aParagraphStyle +{ + var targetRange = [self _rulerTargetRange]; + + if (targetRange.length > 0) + { + // Undo restores the paragraphs' previous attributes, and registers the reverse for redo. + [[[self _registeringUndoManager] prepareWithInvocationTarget:self] + _replaceCharactersInRange:CPMakeRangeCopy(targetRange) + withAttributedString:[_textStorage attributedSubstringFromRange:CPMakeRangeCopy(targetRange)] + selectionRange:CPMakeRangeCopy(_selectionRange)]; + + [_textStorage addAttribute:CPParagraphStyleAttributeName value:aParagraphStyle range:CPMakeRangeCopy(targetRange)]; + [_layoutManager textStorage:_textStorage edited:0 range:CPMakeRangeCopy(targetRange) changeInLength:0 invalidatedRange:CPMakeRangeCopy(targetRange)]; + } + + [_typingAttributes setObject:aParagraphStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; + [self updateRuler]; +} + +/* @ignore + Constrains a marker location, in the text view's coordinate system. Indent markers stay + within the text width. Tab stops stay between their neighbouring tab stops, within the + right margin, and after the text that precedes them on the lines they affect. +*/ +- (float)_constrainedRulerLocation:(float)aLocation forMarker:(CPRulerMarker)aMarker +{ + var textStart = [self _rulerTextStart], + textWidth = [self _rulerTextWidth], + proposed = aLocation - textStart, + representedObject = [aMarker representedObject]; + + if (![representedObject isKindOfClass:[CPTextTab class]]) + return textStart + MAX(0.0, MIN(textWidth, proposed)); + + var paragraphStyle = [self _rulerParagraphStyle], + tabStops = [paragraphStyle tabStops] || [], + tabCount = [tabStops count], + isMoving = [[[aMarker ruler] markers] containsObjectIdenticalTo:aMarker], + targetTabIndex = isMoving ? [self _indexOfTab:representedObject inTabs:tabStops] : CPNotFound, + safetySpacer = 2.0, + minLocation = 0.0, + maxLocation = textWidth, + tailIndent = [paragraphStyle tailIndent]; + + // Right boundary: the tail indent. + if (tailIndent > 0.0) + maxLocation = MIN(maxLocation, tailIndent); + else if (tailIndent < 0.0) + maxLocation = MIN(maxLocation, textWidth + tailIndent); + + // Neighbouring tab stops. + if (targetTabIndex !== CPNotFound) + { + if (targetTabIndex > 0) + minLocation = MAX(minLocation, [tabStops[targetTabIndex - 1] location] + safetySpacer); + + if (targetTabIndex + 1 < tabCount) + maxLocation = MIN(maxLocation, [tabStops[targetTabIndex + 1] location] - safetySpacer); } else { - // For new markers being added between existing tab stops for (var i = 0; i < tabCount; i++) { - var tabLoc = [[tabStops objectAtIndex:i] location]; - if (tabLoc < proposedLocation) - minLocation = Math.max(minLocation, tabLoc + safetySpacer); - else if (tabLoc > proposedLocation) - maxLocation = Math.min(maxLocation, tabLoc - safetySpacer); + var tabLocation = [tabStops[i] location]; + + if (tabLocation < proposed) + minLocation = MAX(minLocation, tabLocation + safetySpacer); + else if (tabLocation > proposed) + maxLocation = MIN(maxLocation, tabLocation - safetySpacer); } } - // 4. Constrain to preceding text on active lines - var start = targetRange.location, + // Text before the tab characters that use this tab stop. + var targetRange = [self _rulerTargetRange], + theString = [_textStorage string], end = CPMaxRange(targetRange), tabCountInLine = 0, - lineStart = start; + lineStart = targetRange.location; - for (var i = start; i < end; i++) + if (targetRange.length > 0) + [_layoutManager _validateLayoutAndGlyphs]; + + for (var i = targetRange.location; i < end; i++) { var charCode = theString.charCodeAt(i); + if (charCode === 10 || charCode === 13) { tabCountInLine = 0; @@ -2832,40 +3659,927 @@ var compareTabStops = function(obj1, obj2, context) { continue; } - if (charCode === 9) // '\t' - { - if (tabCountInLine === targetTabIndex || targetTabIndex === -1) - { - var precedingX = 0.0; - if (i === lineStart) - { - var isFirstLine = (lineStart === 0 || theString.charCodeAt(lineStart - 1) === 10 || theString.charCodeAt(lineStart - 1) === 13); - precedingX = isFirstLine ? [currentParagraphStyle firstLineHeadIndent] : [currentParagraphStyle headIndent]; - } - else - { - var glyphRect = [_layoutManager boundingRectForGlyphRange:CPMakeRange(i - 1, 1) inTextContainer:_textContainer]; - precedingX = CGRectGetMaxX(glyphRect); - } + if (charCode !== 9) + continue; - var textLimit = precedingX + safetySpacer; - if (textLimit > minLocation) - minLocation = textLimit; + if (tabCountInLine === targetTabIndex || targetTabIndex === CPNotFound) + { + var precedingX; + + if (i === lineStart) + { + var isFirstLine = lineStart === 0 || theString.charCodeAt(lineStart - 1) === 10 || theString.charCodeAt(lineStart - 1) === 13; + precedingX = isFirstLine ? [paragraphStyle firstLineHeadIndent] : [paragraphStyle headIndent]; } - tabCountInLine++; + else + precedingX = CGRectGetMaxX([_layoutManager boundingRectForGlyphRange:CPMakeRange(i - 1, 1) inTextContainer:_textContainer]); + + minLocation = MAX(minLocation, precedingX + safetySpacer); } + + tabCountInLine++; } if (minLocation > maxLocation) minLocation = maxLocation; - // 5. Clamp proposedLocation within [minLocation, maxLocation] - return Math.min(maxLocation, Math.max(minLocation, proposedLocation)); + return textStart + MIN(maxLocation, MAX(minLocation, proposed)); } -- (float)rulerView:(CPRulerView)aRulerView willAddMarker:(CPRulerMarker)aMarker atLocation:(float)proposedLocation +@end + +/* + The indent of each list level, and the distance from the start of a level to its markers, + in points. As in TextEdit, the first level's markers start 11 points in, and its text starts + 36 points in. +*/ +var CPTextViewTextListLevelIndent = 36.0, + CPTextViewTextListMarkerIndent = 11.0; + +/* + The list styles that -changeTextList: applies, by the tag of the sender. Tag 0, or any tag + not listed, removes the list. +*/ +var CPTextViewTextListStyles = { + 1: {format: @"{disc}", options: 0}, + 2: {format: @"{circle}", options: 0}, + 3: {format: @"{square}", options: 0}, + 4: {format: @"{hyphen}", options: 0}, + 10: {format: @"{decimal}.", options: 0}, + 11: {format: @"{lower-alpha}.", options: 0}, + 12: {format: @"{upper-alpha}.", options: 0}, + 13: {format: @"{lower-roman}.", options: 0}, + 14: {format: @"{upper-roman}.", options: 0}, + 20: {format: @"{decimal}.", options: CPTextListPrependEnclosingMarker} + }; + +/*! + Text list support. As in Cocoa, the markers of a list are ordinary text at the start of each + item: a tab, the marker and another tab. The paragraph style of an item lists its text lists, + outermost first, and sets the indents and tab stops that place the marker and the text. + Return in an item starts the next item, and Return in an empty item ends the list. Tab at the + start of an item makes it an item of a nested list, and Shift-Tab returns it to the enclosing + list. Delete at the start of an item's text removes the item from its list. After every change, + the markers of the lists around the change are numbered again. +*/ +@implementation CPTextView (CPTextListSupport) + +// MARK: Changing Lists + +/*! + Applies the list style that the sender's tag selects to the selected paragraphs, or to the + paragraph at the insertion point. The tags are: 0 for no list; 1 bullet, 2 circle, 3 square + and 4 hyphen; 10 for 1. 2. 3., 11 for a. b. c., 12 for A. B. C., 13 for i. ii. iii. and 14 + for I. II. III.; 20 for 1. with nested items numbered 1.1. A paragraph next to a list of the + same style at the same level joins that list, so that its numbering continues. + @param aSender the menu item or control that requests the change +*/ +- (@action)changeTextList:(id)aSender { - return [self rulerView:aRulerView willMoveMarker:aMarker toLocation:proposedLocation]; + var style = CPTextViewTextListStyles[[aSender tag]]; + + [self setTextList:style ? [self _textListWithMarkerFormat:style.format options:style.options] : nil]; +} + +/* @ignore + Returns the list of the paragraph just before or after the selected paragraphs when it has + the marker format and options at the selection's level, or a new list. +*/ +- (CPTextList)_textListWithMarkerFormat:(CPString)aFormat options:(unsigned)anOptions +{ + var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange], + level = MAX(1, [[_textStorage _textListsOfParagraphAtIndex:paragraphsRange.location] count]), + neighbours = []; + + if (paragraphsRange.location > 0) + neighbours.push([_textStorage _paragraphRangeForIndex:paragraphsRange.location - 1].location); + + if (CPMaxRange(paragraphsRange) < [_textStorage length]) + neighbours.push(CPMaxRange(paragraphsRange)); + + for (var i = 0; i < neighbours.length; i++) + { + var lists = [_textStorage _textListsOfParagraphAtIndex:neighbours[i]], + list = [lists lastObject]; + + if ([lists count] === level && [list markerFormat] === aFormat && [list listOptions] === anOptions) + return list; + } + + return [[CPTextList alloc] initWithMarkerFormat:aFormat options:anOptions]; +} + +/* @ignore + Returns YES if the menu item's tag selects the style of the innermost list at the + selection, or no list when the selection is not in a list. +*/ +- (BOOL)_textListStyleMatchesItem:(id)anItem +{ + var paragraphStart = [_textStorage _paragraphRangeForIndex:_selectionRange.location].location, + list = [[_textStorage _textListsOfParagraphAtIndex:paragraphStart] lastObject], + style = CPTextViewTextListStyles[[anItem tag]]; + + if (!list) + return !style; + + return !!style && [list markerFormat] === style.format && [list listOptions] === style.options; +} + +/*! + Makes the selected paragraphs, or the paragraph at the insertion point, items of aList. A + paragraph that is already an item of a list changes to aList at the same level. With \c nil, + the paragraphs are removed from their lists, at every level. + @param aList the list, or \c nil +*/ +- (void)setTextList:(CPTextList)aList +{ + if (![self isRichText]) + return; + + var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange], + listsForParagraphs = [], + location = paragraphsRange.location; + + do + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location], + lists = [[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] copy]; + + if (!aList) + lists = []; + else if ([lists count]) + lists[[lists count] - 1] = aList; + else + lists = [aList]; + + listsForParagraphs.push(lists); + location = CPMaxRange(paragraphRange); + } + while (location < CPMaxRange(paragraphsRange)); + + [self _setTextLists:listsForParagraphs forParagraphsInRange:paragraphsRange]; +} + +/* @ignore + Makes the item at the insertion point an item of a list nested in its list. The item joins + the nested list of the item before it, when there is one, so that its numbering continues. + Otherwise a new nested list takes the marker format and options of the enclosing list. +*/ +- (void)_indentTextListItemAtIndex:(unsigned)anIndex +{ + var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex], + lists = [_textStorage _textListsOfParagraphAtIndex:paragraphRange.location], + level = [lists count], + nestedList = nil; + + if (paragraphRange.location > 0) + { + var previousRange = [_textStorage _paragraphRangeForIndex:paragraphRange.location - 1], + previousLists = [_textStorage _textListsOfParagraphAtIndex:previousRange.location]; + + if ([previousLists count] > level && [[previousLists subarrayWithRange:CPMakeRange(0, level)] isEqualToArray:lists]) + nestedList = previousLists[level]; + } + + if (!nestedList) + { + var enclosingList = [lists lastObject]; + + nestedList = [[CPTextList alloc] initWithMarkerFormat:[enclosingList markerFormat] options:[enclosingList listOptions]]; + } + + [self _setTextLists:[[lists arrayByAddingObject:nestedList]] forParagraphsInRange:paragraphRange]; +} + +/* @ignore + Returns the item at anIndex to the enclosing list, or removes it from its list when the list + is not nested. +*/ +- (void)_outdentTextListItemAtIndex:(unsigned)anIndex +{ + var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex], + lists = [_textStorage _textListsOfParagraphAtIndex:paragraphRange.location]; + + [self _setTextLists:[[lists subarrayWithRange:CPMakeRange(0, MAX(0, [lists count] - 1))]] forParagraphsInRange:paragraphRange]; +} + +/* @ignore + Gives each paragraph in aParagraphsRange the text lists at the same position in + listsForParagraphs, with a matching paragraph style and marker. The change can be undone. + The insertion point or selection keeps its place in the text after the markers. +*/ +- (void)_setTextLists:(CPArray)listsForParagraphs forParagraphsInRange:(CPRange)aParagraphsRange +{ + [self _setTextLists:listsForParagraphs forParagraphsInRange:aParagraphsRange paragraphStyle:nil]; +} + +/* @ignore + As -_setTextLists:forParagraphsInRange:, but every paragraph takes a copy of aParagraphStyle, + with its text lists, in place of the indents and tab stops that the lists would set. With + \c nil, each paragraph keeps its own style and takes the indents and tab stops of its lists. +*/ +- (void)_setTextLists:(CPArray)listsForParagraphs forParagraphsInRange:(CPRange)aParagraphsRange paragraphStyle:(CPParagraphStyle)aParagraphStyle +{ + if (![self _didBeginEditing] || ![self shouldChangeTextInRange:aParagraphsRange replacementString:nil]) + return; + + var result = [[CPAttributedString alloc] initWithString:@""], + selectionStart = _selectionRange.location, + selectionEnd = CPMaxRange(_selectionRange), + newSelectionStart = selectionStart, + newSelectionEnd = selectionEnd, + location = aParagraphsRange.location, + caretParagraphStyle = nil, + index = 0; + + do + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location], + markerRange = [self _textListMarkerRangeInParagraphRange:paragraphRange], + oldMarkerLength = markerRange.location === CPNotFound ? 0 : markerRange.length, + paragraph = [[CPAttributedString alloc] initWithAttributedString:[_textStorage attributedSubstringFromRange:paragraphRange]], + textStart = paragraphRange.location + oldMarkerLength, + lists = listsForParagraphs[index++], + attributes = [self _textListAttributesForParagraphRange:paragraphRange textStart:textStart], + paragraphStyle = aParagraphStyle ? [aParagraphStyle mutableCopy] : [self _paragraphStyle:[attributes objectForKey:CPParagraphStyleAttributeName] withTextLists:lists], + newMarkerLength = [lists count] ? 2 : 0, + newParagraphStart = aParagraphsRange.location + [result length]; + + if (oldMarkerLength) + [paragraph deleteCharactersInRange:CPMakeRange(0, oldMarkerLength)]; + + if ([paragraph length]) + [paragraph addAttribute:CPParagraphStyleAttributeName value:paragraphStyle range:CPMakeRange(0, [paragraph length])]; + + // The marker is filled in when the list is numbered, after the change. + if (newMarkerLength) + { + var markerAttributes = [attributes mutableCopy]; + + [markerAttributes setObject:paragraphStyle forKey:CPParagraphStyleAttributeName]; + [paragraph insertAttributedString:[[CPAttributedString alloc] initWithString:@"\t\t" attributes:markerAttributes] atIndex:0]; + } + + // Positions in the text keep their place after the marker; positions in the old marker + // move to the start of the text. + if (CPLocationInRange(selectionStart, paragraphRange) || (selectionStart === CPMaxRange(paragraphRange) && selectionStart === CPMaxRange(aParagraphsRange))) + { + newSelectionStart = newParagraphStart + newMarkerLength + MAX(0, selectionStart - textStart); + caretParagraphStyle = paragraphStyle; + } + + if (CPLocationInRange(selectionEnd, paragraphRange) || (selectionEnd === CPMaxRange(paragraphRange) && selectionEnd === CPMaxRange(aParagraphsRange))) + newSelectionEnd = newParagraphStart + newMarkerLength + MAX(0, selectionEnd - textStart); + + [result appendAttributedString:paragraph]; + location = CPMaxRange(paragraphRange); + } + while (location < CPMaxRange(aParagraphsRange)); + + [self _replaceCharactersInRange:CPMakeRangeCopy(aParagraphsRange) + withAttributedString:result + selectionRange:CPMakeRange(newSelectionStart, MAX(0, newSelectionEnd - newSelectionStart))]; + + if (caretParagraphStyle) + { + [_typingAttributes setObject:caretParagraphStyle forKey:CPParagraphStyleAttributeName]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPTextViewDidChangeTypingAttributesNotification object:self]; + } + + [self updateRuler]; + [self didChangeText]; +} + +/* @ignore + Returns YES if any paragraph that the selection touches is a list item. +*/ +- (BOOL)_selectionTouchesTextList +{ + var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange], + location = paragraphsRange.location; + + do + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location]; + + if ([[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count]) + return YES; + + location = CPMaxRange(paragraphRange); + } + while (location < CPMaxRange(paragraphsRange)); + + return NO; +} + +/* @ignore + Applies a pasted paragraph style that has text lists, or replaces the lists of the selected + paragraphs, through the list path, so that markers follow. The pasted lists are new objects, + decoded from the pasteboard. At each level, the list of the paragraph just before or after + the selection takes the place of a pasted list with the same marker format and options, so + that the pasted paragraphs join a neighbouring list and continue its numbering. +*/ +- (void)_pasteRulerParagraphStyleWithTextLists:(CPParagraphStyle)aParagraphStyle +{ + var paragraphsRange = [self _textListParagraphsRangeForRange:_selectionRange], + pastedLists = [aParagraphStyle textLists], + neighbourLists = [], + lists = []; + + if (paragraphsRange.location > 0) + neighbourLists = [_textStorage _textListsOfParagraphAtIndex:[_textStorage _paragraphRangeForIndex:paragraphsRange.location - 1].location]; + + if (![neighbourLists count] && CPMaxRange(paragraphsRange) < [_textStorage length]) + neighbourLists = [_textStorage _textListsOfParagraphAtIndex:CPMaxRange(paragraphsRange)]; + + for (var level = 0; level < [pastedLists count]; level++) + { + var pastedList = pastedLists[level], + neighbourList = level < [neighbourLists count] ? neighbourLists[level] : nil, + enclosingMatches = level === 0 || lists[level - 1] === neighbourLists[level - 1]; + + if (enclosingMatches && neighbourList && [neighbourList markerFormat] === [pastedList markerFormat] && [neighbourList listOptions] === [pastedList listOptions]) + lists.push(neighbourList); + else + lists.push(pastedList); + } + + var paragraphStyle = [aParagraphStyle mutableCopy], + listsForParagraphs = [], + location = paragraphsRange.location; + + [paragraphStyle setTextLists:lists]; + + do + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location]; + + listsForParagraphs.push(lists); + location = CPMaxRange(paragraphRange); + } + while (location < CPMaxRange(paragraphsRange)); + + [self _setTextLists:listsForParagraphs forParagraphsInRange:paragraphsRange paragraphStyle:paragraphStyle]; +} + +// MARK: Editing in Lists + +/* @ignore + Return in a list item starts the next item, with the same paragraph style. Return in an + empty item ends the list there, or returns a nested item to the enclosing list. Returns + \c NO if the insertion point is not in the text of a list item. +*/ +- (BOOL)_insertNewlineInTextList +{ + var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location], + markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil; + + if (!markerRange || markerRange.location === CPNotFound || _selectionRange.location < CPMaxRange(markerRange)) + return NO; + + var string = [_textStorage string], + textEnd = CPMaxRange(paragraphRange); + + if (textEnd > paragraphRange.location && string.charCodeAt(textEnd - 1) === 10) + textEnd--; + + if (!_selectionRange.length && CPMaxRange(markerRange) === textEnd) + { + [self _outdentTextListItemAtIndex:_selectionRange.location]; + return YES; + } + + var attributes = [_typingAttributes mutableCopy]; + + [attributes setObject:[_textStorage attribute:CPParagraphStyleAttributeName atIndex:paragraphRange.location effectiveRange:nil] forKey:CPParagraphStyleAttributeName]; + + // The new item's marker is filled in when the list is numbered, after the insertion. + [self insertText:[[CPAttributedString alloc] initWithString:@"\n\t\t" attributes:attributes]]; + + return YES; +} + +/* @ignore + Tab at the start of a list item makes it an item of a nested list. Returns \c NO if the + insertion point is not at the start of a list item. +*/ +- (BOOL)_insertTabInTextList +{ + var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location], + markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil; + + if (_selectionRange.length || !markerRange || markerRange.location === CPNotFound || _selectionRange.location > CPMaxRange(markerRange)) + return NO; + + [self _indentTextListItemAtIndex:_selectionRange.location]; + return YES; +} + +/* @ignore + Shift-Tab in an item of a nested list returns it to the enclosing list. In an item of a list + that is not nested, it does nothing. Returns \c NO if the insertion point is not in a list item. +*/ +- (BOOL)_insertBacktabInTextList +{ + var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location]; + + if (!paragraphRange) + return NO; + + if ([[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count] > 1) + [self _outdentTextListItemAtIndex:_selectionRange.location]; + + return YES; +} + +/* @ignore + Delete at the start of a list item's text returns a nested item to the enclosing list, or + removes the item from its list, with its marker. Returns \c NO if the insertion point is + not at the start of a list item's text. +*/ +- (BOOL)_deleteBackwardInTextList +{ + var paragraphRange = [self _textListItemRangeAtIndex:_selectionRange.location], + markerRange = paragraphRange ? [self _textListMarkerRangeInParagraphRange:paragraphRange] : nil; + + if (_selectionRange.length || !markerRange || markerRange.location === CPNotFound || _selectionRange.location !== CPMaxRange(markerRange)) + return NO; + + [self _outdentTextListItemAtIndex:_selectionRange.location]; + return YES; +} + +// MARK: Numbering + +/* @ignore + Numbers again the markers of the lists in and next to aRange, which a change has just + affected. Only marker text changes, without undo: undoing the change restores the text, and + the lists are numbered again afterwards. The selection keeps its place in the text. +*/ +- (void)_updateTextListMarkersAroundRange:(CPRange)aRange +{ + var length = [_textStorage length]; + + if (!length || ![self isRichText]) + return; + + // The paragraphs from the one before the change to the one after it. + var scanStart = [_textStorage _paragraphRangeForIndex:MAX(0, aRange.location - 1)].location, + scanEnd = CPMaxRange([_textStorage _paragraphRangeForIndex:MIN(length, CPMaxRange(aRange))]), + listRanges = [], + location = scanStart; + + if (scanEnd < length) + scanEnd = CPMaxRange([_textStorage _paragraphRangeForIndex:scanEnd]); + + while (location < scanEnd) + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location], + lists = [_textStorage _textListsOfParagraphAtIndex:location]; + + if ([lists count]) + { + var listRange = [_textStorage rangeOfTextList:lists[0] atIndex:location]; + + listRanges.push(listRange); + location = MAX(CPMaxRange(listRange), CPMaxRange(paragraphRange)); + } + else + location = CPMaxRange(paragraphRange); + + if (!paragraphRange.length) + break; + } + + if (!listRanges.length) + return; + + var replacements = []; + + for (var i = 0; i < listRanges.length; i++) + [self _collectTextListMarkerReplacements:replacements inRange:listRanges[i]]; + + if (!replacements.length) + return; + + var selectionStart = _selectionRange.location, + selectionEnd = CPMaxRange(_selectionRange); + + // Replace from the end, so that the earlier ranges stay valid. + [_textStorage beginEditing]; + + for (var i = replacements.length - 1; i >= 0; i--) + { + var replacement = replacements[i], + range = replacement.range, + delta = replacement.string.length - range.length; + + [_textStorage replaceCharactersInRange:range + withAttributedString:[[CPAttributedString alloc] initWithString:replacement.string attributes:[_textStorage attributesAtIndex:range.location effectiveRange:nil]]]; + + if (selectionStart >= CPMaxRange(range)) + selectionStart += delta; + else if (selectionStart > range.location) + selectionStart = range.location + replacement.string.length; + + if (selectionEnd >= CPMaxRange(range)) + selectionEnd += delta; + else if (selectionEnd > range.location) + selectionEnd = range.location + replacement.string.length; + } + + [_textStorage endEditing]; + + [self _setSelectedRange:CPMakeRange(selectionStart, selectionEnd - selectionStart) affinity:0 stillSelecting:NO overwriteTypingAttributes:NO]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; +} + +/* @ignore + Adds to someReplacements the marker of each item in aListRange whose text differs from its + number. The items are counted in one pass, with a counter for each list. As in Cocoa, a list + with the CPTextListPrependEnclosingMarker option puts the enclosing item's marker before its + own. +*/ +- (void)_collectTextListMarkerReplacements:(CPArray)someReplacements inRange:(CPRange)aListRange +{ + var string = [_textStorage string], + counters = {}, + location = aListRange.location; + + while (location < CPMaxRange(aListRange)) + { + var paragraphRange = [_textStorage _paragraphRangeForIndex:location], + lists = [_textStorage _textListsOfParagraphAtIndex:location], + count = [lists count], + innermostUID = [lists[count - 1] UID], + marker = @""; + + counters[innermostUID] = (counters.hasOwnProperty(innermostUID) ? counters[innermostUID] : [lists[count - 1] startingItemNumber] - 1) + 1; + + for (var level = 0; level < count; level++) + { + var list = lists[level], + uid = [list UID], + itemNumber = counters.hasOwnProperty(uid) ? counters[uid] : [list startingItemNumber], + levelMarker = [list markerForItemNumber:itemNumber]; + + marker = ([list listOptions] & CPTextListPrependEnclosingMarker) ? marker + levelMarker : levelMarker; + } + + var markerRange = [self _textListMarkerRangeInParagraphRange:paragraphRange], + newMarker = "\t" + marker + "\t"; + + if (markerRange.location !== CPNotFound && string.substr(markerRange.location, markerRange.length) !== newMarker) + someReplacements.push({range: markerRange, string: newMarker}); + + if (!paragraphRange.length) + break; + + location = CPMaxRange(paragraphRange); + } +} + +// MARK: Helpers + +/* @ignore + Returns the range of the list item paragraph at anIndex, or \c nil if the paragraph is not + a list item or the text is not rich. +*/ +- (CPRange)_textListItemRangeAtIndex:(unsigned)anIndex +{ + if (![self isRichText]) + return nil; + + var paragraphRange = [_textStorage _paragraphRangeForIndex:anIndex]; + + // At the end of the text after a paragraph separator, the insertion point is in an empty + // paragraph that has no attributes of its own. + if (![[_textStorage _textListsOfParagraphAtIndex:paragraphRange.location] count]) + return nil; + + return paragraphRange; +} + +/* @ignore + Returns the range of the marker at the start of a list item: a tab, the marker and another + tab. Returns a range with the location CPNotFound if the paragraph is not a list item, or + its text does not start with a marker. +*/ +- (CPRange)_textListMarkerRangeInParagraphRange:(CPRange)aParagraphRange +{ + var string = [_textStorage string], + start = aParagraphRange.location, + end = CPMaxRange(aParagraphRange); + + if (![[_textStorage _textListsOfParagraphAtIndex:start] count] || string.charCodeAt(start) !== 9) + return CPMakeRange(CPNotFound, 0); + + for (var i = start + 1; i < end; i++) + { + var charCode = string.charCodeAt(i); + + if (charCode === 9) + return CPMakeRange(start, i - start + 1); + + if (charCode === 10 || charCode === 13 || charCode === 0x2029) + break; + } + + return CPMakeRange(CPNotFound, 0); +} + +/* @ignore + Returns the range of whole paragraphs that aRange touches. A selection that ends at the + start of a paragraph does not include that paragraph. +*/ +- (CPRange)_textListParagraphsRangeForRange:(CPRange)aRange +{ + var first = [_textStorage _paragraphRangeForIndex:aRange.location], + lastIndex = aRange.length ? CPMaxRange(aRange) - 1 : aRange.location, + last = [_textStorage _paragraphRangeForIndex:MAX(first.location, lastIndex)]; + + return CPMakeRange(first.location, CPMaxRange(last) - first.location); +} + +/* @ignore + Returns the attributes for a list item's marker and paragraph style: those of the first + character of its text, or of the paragraph, or the typing attributes for an empty paragraph. +*/ +- (CPDictionary)_textListAttributesForParagraphRange:(CPRange)aParagraphRange textStart:(unsigned)aTextStart +{ + var length = [_textStorage length]; + + if (aTextStart < CPMaxRange(aParagraphRange) && aTextStart < length && [_textStorage string].charCodeAt(aTextStart) !== 10) + return [_textStorage attributesAtIndex:aTextStart effectiveRange:nil]; + + if (aParagraphRange.location < length) + return [_textStorage attributesAtIndex:aParagraphRange.location effectiveRange:nil]; + + return [_typingAttributes mutableCopy]; +} + +/* @ignore + Returns a copy of aParagraphStyle for the text lists someLists. The indents and tab stops of + a list item place its marker CPTextViewTextListMarkerIndent points into its level, and its + text, including later lines, at the start of the next level. A paragraph without lists gets + no indents and the default tab stops. +*/ +- (CPParagraphStyle)_paragraphStyle:(CPParagraphStyle)aParagraphStyle withTextLists:(CPArray)someLists +{ + var paragraphStyle = [(aParagraphStyle || [CPParagraphStyle defaultParagraphStyle]) mutableCopy], + level = [someLists count]; + + [paragraphStyle setTextLists:someLists]; + + if (level) + { + var levelStart = (level - 1) * CPTextViewTextListLevelIndent, + textStart = level * CPTextViewTextListLevelIndent; + + [paragraphStyle setFirstLineHeadIndent:levelStart]; + [paragraphStyle setHeadIndent:textStart]; + [paragraphStyle setTabStops:[[[CPTextTab alloc] initWithType:CPLeftTabStopType location:levelStart + CPTextViewTextListMarkerIndent], + [[CPTextTab alloc] initWithType:CPLeftTabStopType location:textStart]]]; + } + else + { + [paragraphStyle setFirstLineHeadIndent:0.0]; + [paragraphStyle setHeadIndent:0.0]; + [paragraphStyle setTabStops:[[CPParagraphStyle defaultParagraphStyle] tabStops]]; + } + + return paragraphStyle; +} + +@end + +/*! + Text attachment support. An image in rich text is a CPTextAttachment, held by one attachment + character. Its view is laid out as that character, at most as wide as the text. The user + inserts images with -insertImage:, or by pasting them. +*/ +@implementation CPTextView (CPTextAttachmentSupport) + +// MARK: Inserting Images + +/*! + Lets the user choose image files with the browser's file picker, and inserts them as + attachments at the insertion point, or in place of the selection, in the order chosen. + @param aSender the object requesting this +*/ +- (@action)insertImage:(id)aSender +{ + if (!CPDOMAvailable || ![self isRichText] || ![self isEditable]) + return; + + var textView = self, + input = document.createElement("input"); + + input.type = "file"; + input.accept = "image/*"; + input.multiple = YES; + + input.onchange = function() + { + [textView _insertImageFiles:input.files]; + }; + + input.click(); +} + +/* @ignore + Reads image files from the browser, and inserts them as attachments in their original + order once all of them have been read. Files that are not images are skipped. +*/ +- (void)_insertImageFiles:(FileList)someFiles +{ + var files = []; + + for (var i = 0; i < someFiles.length; i++) + if (someFiles[i].type.indexOf("image/") === 0) + files.push(someFiles[i]); + + if (!files.length) + return; + + var attachments = new Array(files.length); + + for (var i = 0; i < files.length; i++) + [self _readImageFile:files[i] intoAttachments:attachments atIndex:i]; +} + +/* @ignore + Reads one image file as a data URL, and makes an attachment from it. When the last file has + been read, the attachments are inserted. +*/ +- (void)_readImageFile:(File)aFile intoAttachments:(CPArray)someAttachments atIndex:(unsigned)anIndex +{ + var textView = self, + reader = new FileReader(); + + reader.onload = function() + { + someAttachments[anIndex] = [CPTextAttachment _attachmentWithDataURL:reader.result] || [CPNull null]; + [textView _insertAttachmentsIfComplete:someAttachments]; + }; + + reader.onerror = function() + { + CPLog.warn("CPTextView: the image file " + aFile.name + " could not be read."); + someAttachments[anIndex] = [CPNull null]; + [textView _insertAttachmentsIfComplete:someAttachments]; + }; + + reader.readAsDataURL(aFile); +} + +/* @ignore + Inserts the attachments once every file has been read. The files are read outside the run + loop, so the run loop is run afterwards, to close the undo group and display the change. +*/ +- (void)_insertAttachmentsIfComplete:(CPArray)someAttachments +{ + for (var i = 0; i < someAttachments.length; i++) + if (!someAttachments[i]) + return; + + var result = [[CPAttributedString alloc] initWithString:@""]; + + for (var i = 0; i < someAttachments.length; i++) + { + var attachment = someAttachments[i]; + + if (attachment === [CPNull null]) + continue; + + var attachmentString = [CPTextStorage attributedStringWithAttachment:attachment], + attributes = [_typingAttributes mutableCopy]; + + [attributes addEntriesFromDictionary:[attachmentString attributesAtIndex:0 effectiveRange:nil]]; + [attachmentString setAttributes:attributes range:CPMakeRange(0, 1)]; + [result appendAttributedString:attachmentString]; + } + + if ([result length]) + [self insertText:result]; + + [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; +} + +// MARK: Attachment Layout + +/* @ignore + Returns the widest an attachment may be shown: the width of the text container inside its + line fragment padding. +*/ +- (float)_textAttachmentMaximumWidth +{ + return MAX(0.0, [_textContainer containerSize].width - 2.0 * [_textContainer lineFragmentPadding]); +} + +/* @ignore + Gives each CPTextAttachment in anAttributedString its view, which the text system lays out + as the attachment character, and fits it to the width of the text. Text decoded from an + archive, such as the internal pasteboard, holds the attachments without their views. +*/ +- (void)_prepareTextAttachmentsInAttributedString:(CPAttributedString)anAttributedString +{ + var string = [anAttributedString string], + maximumWidth = [self _textAttachmentMaximumWidth], + index = string.indexOf(_CPAttachmentCharacterAsString); + + while (index !== -1) + { + var attachment = [anAttributedString attribute:CPAttachmentAttributeName atIndex:index effectiveRange:nil]; + + if ([attachment isKindOfClass:[CPTextAttachment class]]) + { + [attachment _setMaximumWidth:maximumWidth]; + [anAttributedString addAttribute:_CPAttachmentView value:[attachment _attachmentView] range:CPMakeRange(index, 1)]; + } + + index = string.indexOf(_CPAttachmentCharacterAsString, index + 1); + } +} + +/* @ignore + Fits every CPTextAttachment in the text to aWidth. The text container calls this when its + width changes, before it lays the text out again. +*/ +- (void)_setTextAttachmentsMaximumWidth:(float)aWidth +{ + var string = [_textStorage string], + index = string.indexOf(_CPAttachmentCharacterAsString); + + while (index !== -1) + { + var attachment = [_textStorage attribute:CPAttachmentAttributeName atIndex:index effectiveRange:nil]; + + if ([attachment isKindOfClass:[CPTextAttachment class]]) + [attachment _setMaximumWidth:aWidth]; + + index = string.indexOf(_CPAttachmentCharacterAsString, index + 1); + } +} + +/* @ignore + Returns the index of anAttachment's character in the text, or CPNotFound. +*/ +- (unsigned)_indexOfTextAttachment:(CPTextAttachment)anAttachment +{ + var string = [_textStorage string], + index = string.indexOf(_CPAttachmentCharacterAsString); + + while (index !== -1) + { + if ([_textStorage attribute:CPAttachmentAttributeName atIndex:index effectiveRange:nil] === anAttachment) + return index; + + index = string.indexOf(_CPAttachmentCharacterAsString, index + 1); + } + + return CPNotFound; +} + +/* @ignore + An attachment's displayed size has changed, for example because its image has loaded. If + the attachment is in this text view, the text is laid out again from it. +*/ +- (void)_textAttachmentDidChangeSize:(CPNotification)aNotification +{ + var index = [self _indexOfTextAttachment:[aNotification object]]; + + if (index === CPNotFound) + return; + + [_layoutManager invalidateLayoutForCharacterRange:CPMakeRange(index, 1) isSoft:NO actualCharacterRange:nil]; + [_layoutManager _validateLayoutAndGlyphs]; + [self sizeToFit]; + [self setNeedsDisplay:YES]; +} + +/* @ignore + Returns a copy of anAttributedString for archiving, without the views of its + CPTextAttachments. An attachment archives its image data, and gets a new view when the text + is decoded and inserted. +*/ +- (CPAttributedString)_attributedStringForArchiving:(CPAttributedString)anAttributedString +{ + var result = [[CPAttributedString alloc] initWithAttributedString:anAttributedString], + string = [result string], + index = string.indexOf(_CPAttachmentCharacterAsString); + + while (index !== -1) + { + if ([[result attribute:CPAttachmentAttributeName atIndex:index effectiveRange:nil] isKindOfClass:[CPTextAttachment class]]) + [result removeAttribute:_CPAttachmentView range:CPMakeRange(index, 1)]; + + index = string.indexOf(_CPAttachmentCharacterAsString, index + 1); + } + + return result; } @end @@ -2935,6 +4649,7 @@ var compareTabStops = function(obj1, obj2, context) { var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", CPTextViewUsesFontPanelKey = @"CPTextViewUsesFontPanelKey", + CPTextViewUsesRulerKey = @"CPTextViewUsesRulerKey", CPTextViewContainerKey = @"CPTextViewContainerKey", CPTextViewLayoutManagerKey = @"CPTextViewLayoutManagerKey", CPTextViewTextStorageKey = @"CPTextViewTextStorageKey", @@ -2971,6 +4686,10 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", [self setAllowsUndo:[aCoder decodeBoolForKey:CPTextViewAllowsUndoKey]]; [self setUsesFontPanel:[aCoder decodeBoolForKey:CPTextViewUsesFontPanelKey]]; + // Archives made before usesRuler was archived keep the default from _init. + if ([aCoder containsValueForKey:CPTextViewUsesRulerKey]) + _usesRuler = [aCoder decodeBoolForKey:CPTextViewUsesRulerKey]; + [self setDelegate:[aCoder decodeObjectForKey:CPTextViewDelegateKey]]; var container = [aCoder decodeObjectForKey:CPTextViewContainerKey]; @@ -3009,6 +4728,7 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", [aCoder encodeObject:_selectedTextAttributes forKey:CPTextViewSelectedTextAttributesKey]; [aCoder encodeBool:_allowsUndo forKey:CPTextViewAllowsUndoKey]; [aCoder encodeBool:_usesFontPanel forKey:CPTextViewUsesFontPanelKey]; + [aCoder encodeBool:_usesRuler forKey:CPTextViewUsesRulerKey]; [aCoder encodeBool:_isHorizontallyResizable forKey:CPTextViewHorizontallyResizableKey]; [aCoder encodeBool:_isVerticallyResizable forKey:CPTextViewVerticallyResizableKey]; [aCoder encodeSize:_maxSize forKey:CPMaxSize]; @@ -3047,8 +4767,9 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", - (void)_createSpan { + if (!CPDOMAvailable) + return; -#if PLATFORM(DOM) _selectionBoxDOM = document.createElement("span"); _selectionBoxDOM.style.position = "absolute"; _selectionBoxDOM.style.visibility = "visible"; @@ -3063,8 +4784,6 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", _selectionBoxDOM.style.height = (_rect.size.height) + "px"; _selectionBoxDOM.style.zIndex = -1000; _selectionBoxDOM.oncontextmenu = _selectionBoxDOM.onmousedown = _selectionBoxDOM.onselectstart = function () { return false; }; -#endif - } @end @@ -3084,35 +4803,38 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", { _rect = CGRectCreateCopy(aRect); -#if PLATFORM(DOM) - _caretDOM.style.left = (aRect.origin.x) + "px"; - _caretDOM.style.top = (aRect.origin.y) + "px"; - _caretDOM.style.height = (aRect.size.height) + "px"; -#endif + if (CPDOMAvailable) + { + _caretDOM.style.left = (aRect.origin.x) + "px"; + _caretDOM.style.top = (aRect.origin.y) + "px"; + _caretDOM.style.height = (aRect.size.height) + "px"; + } } - (id)initWithTextView:(CPTextView)aView { if (self = [super init]) { -#if PLATFORM(DOM) - var style; - - if (!_caretDOM) + if (CPDOMAvailable) { - _caretDOM = document.createElement("span"); - style = _caretDOM.style; - style.position = "absolute"; - style.visibility = "visible"; - style.padding = "0px"; - style.margin = "0px"; - style.whiteSpace = "pre"; - _caretDOM.style.width = "1px"; - _caretDOM.style.zIndex = 10001; - _textView = aView; - _textView._DOMElement.appendChild(_caretDOM); + var style; + + if (!_caretDOM) + { + _caretDOM = document.createElement("span"); + style = _caretDOM.style; + style.position = "absolute"; + style.visibility = "visible"; + style.padding = "0px"; + style.margin = "0px"; + style.whiteSpace = "pre"; + style.backgroundColor = "black"; + _caretDOM.style.width = "1px"; + _caretDOM.style.zIndex = 10001; + _textView = aView; + _textView._DOMElement.appendChild(_caretDOM); + } } -#endif } return self; @@ -3120,10 +4842,10 @@ var CPTextViewAllowsUndoKey = @"CPTextViewAllowsUndoKey", - (void)setVisibility:(BOOL)visibilityFlag stop:(BOOL)stopFlag { - -#if PLATFORM(DOM) - _caretDOM.style.visibility = visibilityFlag ? "visible" : "hidden"; -#endif + if (CPDOMAvailable) + { + _caretDOM.style.visibility = visibilityFlag ? "visible" : "hidden"; + } if (!visibilityFlag && stopFlag) [self stopBlinking]; @@ -3188,25 +4910,28 @@ var _CPCopyPlaceholder = '-'; + (void)isDeadKey:(CPEvent)event { -#if PLATFORM(DOM) - return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process'); -#endif + if (CPDOMAvailable) + return event._DOMEvent && (event._DOMEvent.key === 'Dead' || event._DOMEvent.key === 'Process'); + return NO; } + (void)cancelCurrentInputSessionIfNeeded { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + if (_CPNativeInputField) { _CPNativeInputField.innerHTML = ''; } _isComposing = NO; -#endif } + (void)initialize { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + _CPNativeInputField = document.createElement("div"); _CPNativeInputField.contentEditable = YES; @@ -3296,6 +5021,26 @@ var _CPCopyPlaceholder = '-'; e.preventDefault(); var nativeClipboard = (e.originalEvent || e).clipboardData; var currentFirstResponder = [[CPApp keyWindow] firstResponder]; + var pasteboard = [CPPasteboard generalPasteboard]; + var nativePlainText = nativeClipboard.getData('text/plain'); + + // An image on the clipboard, such as a screenshot, is inserted as an attachment when the + // clipboard holds no text. + if (!nativePlainText && nativeClipboard.files && nativeClipboard.files.length && + [currentFirstResponder respondsToSelector:@selector(_insertImageFiles:)] && [currentFirstResponder isRichText]) + { + [currentFirstResponder _insertImageFiles:nativeClipboard.files]; + return; + } + + // The internal pasteboard holds what the application copied last. When the system + // clipboard holds other text, copied elsewhere, the internal pasteboard is out of date: + // it is replaced with that text, so that its rich text is not pasted in its place. + if (nativePlainText && !_CPTextViewClipboardStringsMatch(nativePlainText, [pasteboard stringForType:CPStringPboardType])) + { + [pasteboard declareTypes:[CPStringPboardType] owner:nil]; + [pasteboard setString:nativePlainText forType:CPStringPboardType]; + } // Can we accept richtext? Then this is our preference (fixme: shift key to force plain text paste) if ([currentFirstResponder respondsToSelector:@selector(isRichText)] && [currentFirstResponder isRichText]) @@ -3307,7 +5052,6 @@ var _CPCopyPlaceholder = '-'; richtext = [[_CPRTFParser new] parseRTF:richtext]; else { - var pasteboard = [CPPasteboard generalPasteboard]; // If no RTF is available, try to get the internal represatation of richtext from the pasteboard var richData = [pasteboard stringForType:_CPASPboardType]; @@ -3329,7 +5073,7 @@ var _CPCopyPlaceholder = '-'; // If no richtext is available, fall back to plain text } - var nativeString = nativeClipboard.getData('text/plain'); + var nativeString = nativePlainText; // Use setTimeout to prevent flickering setTimeout(function() @@ -3384,21 +5128,22 @@ var _CPCopyPlaceholder = '-'; [currentFirstResponder deleteBackward:self]; }, 20); }; -#endif } + (void)focusForTextView:(CPTextView)currentFirstResponder { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; if (_CPNativeInputField && document.activeElement !== _CPNativeInputField) _CPNativeInputField.focus(); -#endif } + (void)focusForClipboardOfTextView:(CPTextView)textview { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + var selectedRange = [textview selectedRange]; if (selectedRange.length > 0) { // Put the selected text into the hidden div so the browser can natively copy it. @@ -3419,7 +5164,6 @@ var _CPCopyPlaceholder = '-'; selection.removeAllRanges(); selection.addRange(range); } -#endif } @end diff --git a/AppKit/CPTextView/CPTypesetter.j b/AppKit/CPTextView/CPTypesetter.j index 7100bd051..b9e6ac6d6 100644 --- a/AppKit/CPTextView/CPTypesetter.j +++ b/AppKit/CPTextView/CPTypesetter.j @@ -49,7 +49,23 @@ CPTypesetterParagraphBreakAction = 1 << 4; CPTypesetterContainerBreakAction = 1 << 5; var CPSystemTypesetterFactory, - _sharedSimpleTypesetter; + _sharedSimpleTypesetter, + _CPTypesetterCachedDecimalSeparator = nil; + +/* + Returns the decimal separator of the user's locale, which a decimal tab stop aligns on. +*/ +var _CPTypesetterDecimalSeparator = function() +{ + if (!_CPTypesetterCachedDecimalSeparator) + { + var formatted = (1.5).toLocaleString(); + + _CPTypesetterCachedDecimalSeparator = formatted.length === 3 ? formatted.charAt(1) : @"."; + } + + return _CPTypesetterCachedDecimalSeparator; +}; @implementation CPTypesetter : CPObject @@ -170,10 +186,32 @@ var CPSystemTypesetterFactory, advancements:(CPArray)advancements lineCount:(unsigned)lineCount sameLine:(BOOL)sameLine + wordWrapped:(BOOL)wordWrapped + rightMargin:(float)rightMargin { - var myX = 0, - rect = CGRectMake(lineOrigin.x, lineOrigin.y, _lineWidth, _lineHeight), - containerSize = aContainer._size; + // Line positions are computed inside the line fragment padding. The padding is added here, where + // the line fragment rectangle is stored, and alignment uses the width inside the padding. + var padding = [aContainer lineFragmentPadding], + myX = 0, + rect = CGRectMake(lineOrigin.x + padding, lineOrigin.y, _lineWidth, _lineHeight); + + // As in Cocoa, text is aligned between the indents, and whitespace at the end of the line hangs + // outside the alignment: it does not count in the line's visible width. + var theString = [_textStorage string], + availableWidth = rightMargin - lineOrigin.x, + visibleLength = lineRange.length, + visibleWidth = _lineWidth; + + while (visibleLength > 0) + { + var trailingCharCode = theString.charCodeAt(lineRange.location + visibleLength - 1); + + if (trailingCharCode !== 32 && trailingCharCode !== 10 && trailingCharCode !== 13) + break; + + visibleLength--; + visibleWidth -= advancements[visibleLength].width; + } [_layoutManager _appendNewLineFragmentInTextContainer:_currentTextContainer forGlyphRange:lineRange]; @@ -190,11 +228,41 @@ var CPSystemTypesetterFactory, break; case CPCenterTextAlignment: - myX = (containerSize.width - _lineWidth) / 2; + myX = (availableWidth - visibleWidth) / 2; break; case CPRightTextAlignment: - myX = containerSize.width - _lineWidth; + myX = availableWidth - visibleWidth; + break; + + case CPJustifiedTextAlignment: + // Only a line that wraps is justified. The last line of a paragraph, and the text + // before a tab on the same line, stay left aligned, as in Cocoa. The spare width is + // spread evenly over the spaces inside the visible part of the line. The line fragment + // draws its spans with the same extra word spacing. + if (wordWrapped && !sameLine) + { + var spaceIndexes = []; + + for (var i = 0; i < visibleLength; i++) + { + var charCode = theString.charCodeAt(lineRange.location + i); + + if (charCode === 32 || charCode === 160) + spaceIndexes.push(i); + } + + var spaceCount = spaceIndexes.length, + extraWidth = spaceCount ? (availableWidth - visibleWidth) / spaceCount : 0.0; + + if (extraWidth > 0.0) + { + for (var i = 0; i < spaceCount; i++) + advancements[spaceIndexes[i]].width += extraWidth; + + fragment._wordSpacing = extraWidth; + } + } break; } @@ -237,8 +305,10 @@ var CPSystemTypesetterFactory, _lineBase = 0; _lineWidth = 0; + // containerSizeWidth is the width inside the line fragment padding at both ends. var containerSize = [_currentTextContainer containerSize], - containerSizeWidth = containerSize.width, + lineFragmentPadding = [_currentTextContainer lineFragmentPadding], + containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding, containerSizeHeight = containerSize.height, lineRange = CPMakeRange(glyphIndex, 0), wrapRange = CPMakeRange(0, 0), @@ -274,7 +344,11 @@ var CPSystemTypesetterFactory, rightMargin = containerSizeWidth; if (glyphIndex > 0) + { + // Stored line fragment rectangles include the padding, which the layout below does not. lineOrigin = CGPointCreateCopy([_layoutManager lineFragmentRectForGlyphAtIndex:glyphIndex effectiveRange:nil].origin); + lineOrigin.x -= lineFragmentPadding; + } else if ([_layoutManager extraLineFragmentTextContainer]) lineOrigin = CGPointMake(0, [_layoutManager extraLineFragmentUsedRect].origin.y); else @@ -469,7 +543,16 @@ var CPSystemTypesetterFactory, tabAlignment = [nextTab alignment]; // Mathematically offset the tab character's right boundary - if (tabAlignment === CPCenterTextAlignment) + if ([nextTab tabStopType] === CPDecimalTabStopType) + { + // As in Cocoa, the first decimal separator of the user's locale starts at the stop. + // A segment without one ends at the stop, like a right tab. + var separatorIndex = segmentString.indexOf(_CPTypesetterDecimalSeparator()), + widthBeforeSeparator = separatorIndex === -1 ? nextSegmentWidth : [segmentString.substring(0, separatorIndex) sizeWithFont:currentFont inWidth:NULL].width; + + rangeWidth = (tabLocation - widthBeforeSeparator) - lineOrigin.x; + } + else if (tabAlignment === CPCenterTextAlignment) { rangeWidth = (tabLocation - nextSegmentWidth / 2.0) - lineOrigin.x; } @@ -533,7 +616,7 @@ var CPSystemTypesetterFactory, if (isNewline || isTabStop || isAttachment) { - if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline]) + if ([self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:!isNewline wordWrapped:isWordWrapped rightMargin:rightMargin]) return; if (isTabStop || isAttachment) @@ -556,7 +639,8 @@ var CPSystemTypesetterFactory, { _currentTextContainer = textContainers[++_indexOfCurrentContainer]; containerSize = [_currentTextContainer containerSize]; - containerSizeWidth = containerSize.width; + lineFragmentPadding = [_currentTextContainer lineFragmentPadding]; + containerSizeWidth = containerSize.width - 2.0 * lineFragmentPadding; containerSizeHeight = containerSize.height; } @@ -587,9 +671,10 @@ var CPSystemTypesetterFactory, // Flush remaining characters if (lineRange.length) - [self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO]; + [self _flushRange:lineRange lineOrigin:lineOrigin currentContainer:_currentTextContainer advancements:advancements lineCount:numLines sameLine:NO wordWrapped:NO rightMargin:rightMargin]; - var rect = CGRectMake(1, lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight); + // The empty line after the text starts inside the padding, and at least 1 point in, so the insertion point stays visible. + var rect = CGRectMake(MAX(1.0, lineFragmentPadding), lineOrigin.y - descent, containerSizeWidth, currentFontLineHeight); [_layoutManager setExtraLineFragmentRect:rect usedRect:rect textContainer:_currentTextContainer]; var fragment = [_layoutManager._lineFragments lastObject]; diff --git a/AppKit/CPTokenField.j b/AppKit/CPTokenField.j index 33bd5a21a..30fb5b58a 100644 --- a/AppKit/CPTokenField.j +++ b/AppKit/CPTokenField.j @@ -32,6 +32,7 @@ @import "CPText.j" @import "CPTextField.j" @import "CPWindow_Constants.j" +@import "CPCompatibility.j" @class _CPTokenFieldTokenCloseButton @class _CPTokenFieldTokenDisclosureButton @@ -63,8 +64,6 @@ var CPTokenFieldDelegate_tokenField_hasMenuForRepresentedObject_ CPTokenFieldDelegate_tokenField_representedObjectForEditingString_ = 1 << 6; -#if PLATFORM(DOM) - var CPTokenFieldDOMInputElement = nil, CPTokenFieldDOMPasswordInputElement = nil, CPTokenFieldDOMStandardInputElement = nil, @@ -79,8 +78,6 @@ var CPTokenFieldDOMInputElement = nil, CPTokenFieldBlurHandler = nil; -#endif - var CPScrollDestinationNone = 0, CPScrollDestinationLeft = 1, CPScrollDestinationRight = 2; @@ -394,10 +391,10 @@ CPTokenFieldDeleteButtonType = 1; if (![super becomeFirstResponder]) return NO; -#if PLATFORM(DOM) - if (CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window]) + if (CPDOMAvailable && CPTokenFieldInputOwner && [CPTokenFieldInputOwner window] !== [self window]) + { [[CPTokenFieldInputOwner window] makeFirstResponder:nil]; -#endif + } // As long as we are the first responder we need to monitor the key status of our window. [self _setObserveWindowKeyNotifications:YES]; @@ -423,75 +420,74 @@ CPTokenFieldDeleteButtonType = 1; [self setNeedsLayout]; -#if PLATFORM(DOM) - - var string = [self stringValue], - element = [self _inputElement], - font = [self currentValueForThemeAttribute:@"font"]; - - element.value = nil; - element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString]; - element.style.font = [font cssString]; - element.style.zIndex = 1000; - - switch ([self alignment]) + if (CPDOMAvailable) { - case CPCenterTextAlignment: - element.style.textAlign = "center"; - break; + var string = [self stringValue], + element = [self _inputElement], + font = [self currentValueForThemeAttribute:@"font"]; - case CPRightTextAlignment: - element.style.textAlign = "right"; - break; + element.value = nil; + element.style.color = [[self currentValueForThemeAttribute:@"text-color"] cssString]; + element.style.font = [font cssString]; + element.style.zIndex = 1000; - default: - element.style.textAlign = "left"; - } + switch ([self alignment]) + { + case CPCenterTextAlignment: + element.style.textAlign = "center"; + break; - var contentRect = [self contentRectForBounds:[self bounds]]; + case CPRightTextAlignment: + element.style.textAlign = "right"; + break; - element.style.top = CGRectGetMinY(contentRect) + "px"; - element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // element effectively imposes a 1px left margin - element.style.width = CGRectGetWidth(contentRect) + "px"; - element.style.height = [font defaultLineHeightForFont] + "px"; + default: + element.style.textAlign = "left"; + } - [[CPRunLoop mainRunLoop] performBlock:function() - { - [_tokenScrollView documentView]._DOMElement.appendChild(element); + var contentRect = [self contentRectForBounds:[self bounds]]; - // Removed so CPTokenField doesn't fire the notification the moment it becomes the first responder, but instead defers to the first keystroke, just like Cocoa (see keyDown: in CPTextField). - // [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; + element.style.top = CGRectGetMinY(contentRect) + "px"; + element.style.left = (CGRectGetMinX(contentRect) - 1) + "px"; // element effectively imposes a 1px left margin + element.style.width = CGRectGetWidth(contentRect) + "px"; + element.style.height = [font defaultLineHeightForFont] + "px"; [[CPRunLoop mainRunLoop] performBlock:function() { - // This will prevent to jump to the focused element - var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin]; + [_tokenScrollView documentView]._DOMElement.appendChild(element); - element.focus(); + // Removed so CPTokenField doesn't fire the notification the moment it becomes the first responder, but instead defers to the first keystroke, just like Cocoa (see keyDown: in CPTextField). + // [self textDidBeginEditing:[CPNotification notificationWithName:CPControlTextDidBeginEditingNotification object:self userInfo:nil]]; - [self _restorePreviousScrollingOrigin:previousScrollingOrigin]; + [[CPRunLoop mainRunLoop] performBlock:function() + { + // This will prevent to jump to the focused element + var previousScrollingOrigin = [self _scrollToVisibleRectAndReturnPreviousOrigin]; - CPTokenFieldInputOwner = self; + element.focus(); + + [self _restorePreviousScrollingOrigin:previousScrollingOrigin]; + + CPTokenFieldInputOwner = self; + } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; + + [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]]; } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; - [self textDidFocus:[CPNotification notificationWithName:CPTextFieldDidFocusNotification object:self userInfo:nil]]; - } argument:nil order:0 modes:[CPDefaultRunLoopMode]]; + [[[self window] platformWindow] _propagateCurrentDOMEvent:YES]; - [[[self window] platformWindow] _propagateCurrentDOMEvent:YES]; + CPTokenFieldInputIsActive = YES; - CPTokenFieldInputIsActive = YES; + if (document.attachEvent) + { + CPTokenFieldCachedSelectStartFunction = document.body.onselectstart; + CPTokenFieldCachedDragFunction = document.body.ondrag; - if (document.attachEvent) - { - CPTokenFieldCachedSelectStartFunction = document.body.onselectstart; - CPTokenFieldCachedDragFunction = document.body.ondrag; - - document.body.ondrag = function () {}; - document.body.onselectstart = function () {}; + document.body.ondrag = function () {}; + document.body.onselectstart = function () {}; + } } -#endif - return YES; } @@ -527,7 +523,7 @@ CPTokenFieldDeleteButtonType = 1; [self _updatePlaceholderState]; [self setNeedsLayout]; -#if PLATFORM(DOM) + if (!CPDOMAvailable) return; var element = [self _inputElement]; @@ -561,8 +557,6 @@ CPTokenFieldDeleteButtonType = 1; document.body.ondrag = CPTokenFieldCachedDragFunction; document.body.onselectstart = CPTokenFieldCachedSelectStartFunction; } - -#endif } - (void)mouseDown:(CPEvent)anEvent @@ -630,16 +624,12 @@ CPTokenFieldDeleteButtonType = 1; [objectValue addObject:[token representedObject]]; } -#if PLATFORM(DOM) - - if ([self _editorValue]) + if (CPDOMAvailable && [self _editorValue]) { var token = [self _representedObjectForEditingString:[self _editorValue]]; [objectValue insertObject:token atIndex:_selectedRange.location]; } -#endif - return objectValue; } @@ -755,9 +745,10 @@ CPTokenFieldDeleteButtonType = 1; // ============= // = TEXTFIELD = // ============= -#if PLATFORM(DOM) - (DOMElement)_inputElement { + if (!CPDOMAvailable) return nil; + if (!CPTokenFieldDOMInputElement) { CPTokenFieldDOMInputElement = document.createElement("input"); @@ -798,7 +789,6 @@ CPTokenFieldDeleteButtonType = 1; return CPTokenFieldDOMInputElement; } -#endif - (CPString)_editorValue { @@ -1036,9 +1026,10 @@ CPTokenFieldDeleteButtonType = 1; - (void)keyDown:(CPEvent)anEvent { -#if PLATFORM(DOM) - CPTokenFieldTextDidChangeValue = [self stringValue]; -#endif + if (CPDOMAvailable) + { + CPTokenFieldTextDidChangeValue = [self stringValue]; + } // Has to be enabled, and it also has to be editable or selectable. if (![self isEnabled] || !([self isEditable] || [self isSelectable])) @@ -1062,12 +1053,10 @@ CPTokenFieldDeleteButtonType = 1; - (void)keyUp:(CPEvent)anEvent { -#if PLATFORM(DOM) - if ([self stringValue] !== CPTokenFieldTextDidChangeValue) + if (CPDOMAvailable && [self stringValue] !== CPTokenFieldTextDidChangeValue) { [self textDidChange:[CPNotification notificationWithName:CPControlTextDidChangeNotification object:self userInfo:nil]]; } -#endif [[[self window] platformWindow] _propagateCurrentDOMEvent:YES]; } diff --git a/AppKit/CPView.j b/AppKit/CPView.j index 53e8da88e..03bab892d 100644 --- a/AppKit/CPView.j +++ b/AppKit/CPView.j @@ -26,9 +26,11 @@ @import "_CPObject+Theme.j" @import "CGAffineTransform.j" +@import "CPDOMDisplayServer.j" @import "CGGeometry.j" @import "CPAppearance.j" @import "CPColor.j" +@import "CPCompatibility.j" @import "CPGraphicsContext.j" @import "CPResponder.j" @import "CPTheme.j" @@ -50,8 +52,6 @@ @typedef _CPViewFullScreenModeState -#if PLATFORM(DOM) - if (typeof(appkit_tag_dom_elements) !== "undefined" && appkit_tag_dom_elements) { AppKitTagDOMElement = function(owner, element) @@ -68,8 +68,6 @@ else } } -#endif - /* @global @group CPViewAutoresizingMasks @@ -123,7 +121,6 @@ CPViewFrameDidChangeNotification = @"CPViewFrameDidChangeNotification"; var CachedNotificationCenter = nil; -#if PLATFORM(DOM) var DOMElementPrototype = nil, BackgroundTrivialColor = 0, @@ -132,7 +129,6 @@ var DOMElementPrototype = nil, BackgroundNinePartImage = 3, BackgroundTransparentColor = 4, BackgroundCSSStyling = 5; -#endif var CPViewFlags = { }, CPViewHasCustomDrawRect = 1 << 0, @@ -189,7 +185,6 @@ var CPViewHighDPIDrawingEnabled = YES; BOOL _inLiveResize; BOOL _isSuperviewAClipView; -#if PLATFORM(DOM) DOMElement _DOMElement; DOMElement _DOMContentsElement; @@ -201,7 +196,6 @@ var CPViewHighDPIDrawingEnabled = YES; // CSS styling CPArray _cssStylePreviousState; DOMElement _cssStyleNode; -#endif CGRect _dirtyRect; @@ -230,7 +224,6 @@ var CPViewHighDPIDrawingEnabled = YES; // Layout Support BOOL _needsLayout; - JSObject _ephemeralSubviews; JSObject _ephemeralSubviewsForNames; CPSet _ephemeralSubviews; @@ -270,16 +263,17 @@ var CPViewHighDPIDrawingEnabled = YES; if (self !== [CPView class]) return; -#if PLATFORM(DOM) - DOMElementPrototype = document.createElement("div"); + if (CPDOMAvailable) + { + DOMElementPrototype = document.createElement("div"); - var style = DOMElementPrototype.style; + var style = DOMElementPrototype.style; - style.overflow = "hidden"; - style.position = "absolute"; - style.visibility = "visible"; - style.zIndex = 0; -#endif + style.overflow = "hidden"; + style.position = "absolute"; + style.visibility = "visible"; + style.zIndex = 0; + } CachedNotificationCenter = [CPNotificationCenter defaultCenter]; } @@ -410,18 +404,19 @@ var CPViewHighDPIDrawingEnabled = YES; _theme = [CPTheme defaultTheme]; _themeState = CPThemeStateNormal; -#if PLATFORM(DOM) - _DOMElement = DOMElementPrototype.cloneNode(false); - AppKitTagDOMElement(self, _DOMElement); + if (CPDOMAvailable) + { + _DOMElement = DOMElementPrototype.cloneNode(false); + AppKitTagDOMElement(self, _DOMElement); - CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(aFrame), CGRectGetMinY(aFrame)); - CPDOMDisplayServerSetStyleSize(_DOMElement, width, height); + CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(aFrame), CGRectGetMinY(aFrame)); + CPDOMDisplayServerSetStyleSize(_DOMElement, width, height); - _DOMImageParts = []; - _DOMImageSizes = []; + _DOMImageParts = []; + _DOMImageSizes = []; - _cssStylePreviousState = @[]; -#endif + _cssStylePreviousState = @[]; + } _animator = nil; _animationsDictionary = @{}; @@ -484,20 +479,21 @@ var CPViewHighDPIDrawingEnabled = YES; if (!_toolTipFunctionOut) _toolTipFunctionOut = function(e) { [_CPToolTip invalidateCurrentToolTipIfNeeded]; }; -#if PLATFORM(DOM) - if (_DOMElement.addEventListener) + if (CPDOMAvailable) { - _DOMElement.addEventListener("mouseover", _toolTipFunctionIn, YES); - _DOMElement.addEventListener("keypress", _toolTipFunctionOut, YES); - _DOMElement.addEventListener("mouseout", _toolTipFunctionOut, YES); + if (_DOMElement.addEventListener) + { + _DOMElement.addEventListener("mouseover", _toolTipFunctionIn, YES); + _DOMElement.addEventListener("keypress", _toolTipFunctionOut, YES); + _DOMElement.addEventListener("mouseout", _toolTipFunctionOut, YES); + } + else if (_DOMElement.attachEvent) + { + _DOMElement.attachEvent("onmouseover", _toolTipFunctionIn); + _DOMElement.attachEvent("onkeypress", _toolTipFunctionOut); + _DOMElement.attachEvent("onmouseout", _toolTipFunctionOut); + } } - else if (_DOMElement.attachEvent) - { - _DOMElement.attachEvent("onmouseover", _toolTipFunctionIn); - _DOMElement.attachEvent("onkeypress", _toolTipFunctionOut); - _DOMElement.attachEvent("onmouseout", _toolTipFunctionOut); - } -#endif _toolTipInstalled = YES; } @@ -511,20 +507,21 @@ var CPViewHighDPIDrawingEnabled = YES; if (!_toolTipInstalled) return; -#if PLATFORM(DOM) - if (_DOMElement.removeEventListener) + if (CPDOMAvailable) { - _DOMElement.removeEventListener("mouseover", _toolTipFunctionIn, YES); - _DOMElement.removeEventListener("keypress", _toolTipFunctionOut, YES); - _DOMElement.removeEventListener("mouseout", _toolTipFunctionOut, YES); + if (_DOMElement.removeEventListener) + { + _DOMElement.removeEventListener("mouseover", _toolTipFunctionIn, YES); + _DOMElement.removeEventListener("keypress", _toolTipFunctionOut, YES); + _DOMElement.removeEventListener("mouseout", _toolTipFunctionOut, YES); + } + else if (_DOMElement.detachEvent) + { + _DOMElement.detachEvent("onmouseover", _toolTipFunctionIn); + _DOMElement.detachEvent("onkeypress", _toolTipFunctionOut); + _DOMElement.detachEvent("onmouseout", _toolTipFunctionOut); + } } - else if (_DOMElement.detachEvent) - { - _DOMElement.detachEvent("onmouseover", _toolTipFunctionIn); - _DOMElement.detachEvent("onkeypress", _toolTipFunctionOut); - _DOMElement.detachEvent("onmouseout", _toolTipFunctionOut); - } -#endif _toolTipFunctionIn = nil; _toolTipFunctionOut = nil; @@ -593,10 +590,8 @@ var CPViewHighDPIDrawingEnabled = YES; { if (aSubview === self) [CPException raise:CPInvalidArgumentException reason:"can't add a view as a subview of itself"]; -#if DEBUG if (!aSubview._superview && _subviews.indexOf(aSubview) !== CPNotFound) [CPException raise:CPInvalidArgumentException reason:"can't insert a subview in duplicate (probably partially decoded)"]; -#endif // Notify the subview that it will be moving. [aSubview viewWillMoveToSuperview:self]; @@ -620,9 +615,10 @@ var CPViewHighDPIDrawingEnabled = YES; [_subviews removeObjectAtIndex:index]; -#if PLATFORM(DOM) - CPDOMDisplayServerRemoveChild(_DOMElement, aSubview._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerRemoveChild(_DOMElement, aSubview._DOMElement); + } if (anIndex > index) --anIndex; @@ -648,25 +644,28 @@ var CPViewHighDPIDrawingEnabled = YES; { _subviews.push(aSubview); -#if PLATFORM(DOM) - // Attach the actual node. - CPDOMDisplayServerAppendChild(_DOMElement, aSubview._DOMElement); -#endif + if (CPDOMAvailable) + { + // Attach the actual node. + CPDOMDisplayServerAppendChild(_DOMElement, aSubview._DOMElement); + } } else { _subviews.splice(anIndex, 0, aSubview); -#if PLATFORM(DOM) - // Attach the actual node. - CPDOMDisplayServerInsertBefore(_DOMElement, aSubview._DOMElement, _subviews[anIndex + 1]._DOMElement); -#endif + if (CPDOMAvailable) + { + // Attach the actual node. + CPDOMDisplayServerInsertBefore(_DOMElement, aSubview._DOMElement, _subviews[anIndex + 1]._DOMElement); + } } -#if PLATFORM(DOM) - var origin = aSubview._frame.origin; - CPDOMDisplayServerSetStyleLeftTop(aSubview._DOMElement, _boundsTransform, origin.x, origin.y); -#endif + if (CPDOMAvailable) + { + var origin = aSubview._frame.origin; + CPDOMDisplayServerSetStyleLeftTop(aSubview._DOMElement, _boundsTransform, origin.x, origin.y); + } [aSubview setNextResponder:self]; [aSubview _scaleSizeUnitSquareToSize:[self _hierarchyScaleSize]]; @@ -738,9 +737,10 @@ var CPViewHighDPIDrawingEnabled = YES; [_superview._subviews removeObjectIdenticalTo:self]; -#if PLATFORM(DOM) - CPDOMDisplayServerRemoveChild(_superview._DOMElement, _DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerRemoveChild(_superview._DOMElement, _DOMElement); + } // If the view is not hidden and one of its ancestors is hidden, // notify the view that it is now unhidden. @@ -823,7 +823,9 @@ var CPViewHighDPIDrawingEnabled = YES; _subviews = [newSubviews copy]; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + var index = 0, count = [_subviews count]; @@ -834,7 +836,6 @@ var CPViewHighDPIDrawingEnabled = YES; CPDOMDisplayServerRemoveChild(_DOMElement, subview._DOMElement); CPDOMDisplayServerAppendChild(_DOMElement, subview._DOMElement); } -#endif } /* @ignore */ @@ -1160,14 +1161,11 @@ var CPViewHighDPIDrawingEnabled = YES; if (_isSuperviewAClipView && !_inhibitFrameAndBoundsChangedNotifications) [[self superview] viewFrameChanged:[[CPNotification alloc] initWithName:CPViewFrameDidChangeNotification object:self userInfo:nil]]; -#if PLATFORM(DOM) - if (!_inhibitDOMUpdates) + if (CPDOMAvailable && !_inhibitDOMUpdates) { var transform = _superview ? _superview._boundsTransform : NULL; - CPDOMDisplayServerSetStyleLeftTop(_DOMElement, transform, origin.x, origin.y); } -#endif if (_window && !_window._inhibitUpdateTrackingAreas && !_inhibitFrameAndBoundsChangedNotifications) [self _updateTrackingAreasWithRecursion:YES]; @@ -1206,7 +1204,8 @@ var CPViewHighDPIDrawingEnabled = YES; [self setNeedsLayout]; [self setNeedsDisplay:YES]; -#if PLATFORM(DOM) + if (CPDOMAvailable) + { [self _setDisplayServerSetStyleSize:size]; if (_backgroundType !== BackgroundTrivialColor) @@ -1315,7 +1314,7 @@ var CPViewHighDPIDrawingEnabled = YES; } } } -#endif + } if (_postsFrameChangedNotifications && !_inhibitFrameAndBoundsChangedNotifications) [CachedNotificationCenter postNotificationName:CPViewFrameDidChangeNotification object:self]; @@ -1334,7 +1333,9 @@ var CPViewHighDPIDrawingEnabled = YES; */ - (void)_setDisplayServerSetStyleSize:(CGSize)aSize { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + var scale = [self scaleSize]; if (!_inhibitDOMUpdates) @@ -1347,7 +1348,6 @@ var CPViewHighDPIDrawingEnabled = YES; _needToSetTransformMatrix = YES; } -#endif } /*! @@ -1426,7 +1426,8 @@ var CPViewHighDPIDrawingEnabled = YES; if (_layer) [_layer _owningViewBoundsChanged]; -#if PLATFORM(DOM) + if (CPDOMAvailable) + { var index = _subviews.length; while (index--) @@ -1436,7 +1437,7 @@ var CPViewHighDPIDrawingEnabled = YES; CPDOMDisplayServerSetStyleLeftTop(view._DOMElement, _boundsTransform, origin.x, origin.y); } -#endif + } if (_postsBoundsChangedNotifications && !_inhibitFrameAndBoundsChangedNotifications) [CachedNotificationCenter postNotificationName:CPViewBoundsDidChangeNotification object:self]; @@ -1688,11 +1689,12 @@ var CPViewHighDPIDrawingEnabled = YES; if (_isHidden === aFlag) return; -// FIXME: Should we return to visibility? This breaks in FireFox, Opera, and IE. -// _DOMElement.style.visibility = (_isHidden = aFlag) ? "hidden" : "visible"; -#if PLATFORM(DOM) - _DOMElement.style.display = aFlag ? "none" : "block"; -#endif + // FIXME: Should we return to visibility? This breaks in FireFox, Opera, and IE. + // _DOMElement.style.visibility = (_isHidden = aFlag) ? "hidden" : "visible"; + if (CPDOMAvailable) + { + _DOMElement.style.display = aFlag ? "none" : "block"; + } if (aFlag) { @@ -1787,9 +1789,10 @@ var CPViewHighDPIDrawingEnabled = YES; _isHiddenOrHasHiddenAncestor = YES; [_subviews enumerateObjectsUsingBlock:function(view, idx, stop) - { - [view _recursiveGainedHiddenAncestor]; - }]; + { + [view _recursiveGainedHiddenAncestor]; + } + ]; } /*! @@ -1807,9 +1810,10 @@ var CPViewHighDPIDrawingEnabled = YES; _clipsToBounds = shouldClip; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + _DOMElement.style.overflow = _clipsToBounds ? "hidden" : "visible"; -#endif } - (BOOL)clipsToBounds @@ -1829,7 +1833,8 @@ var CPViewHighDPIDrawingEnabled = YES; _opacity = anAlphaValue; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; if (CPFeatureIsCompatible(CPOpacityRequiresFilterFeature)) { @@ -1840,8 +1845,6 @@ var CPViewHighDPIDrawingEnabled = YES; } else _DOMElement.style.opacity = anAlphaValue; - -#endif } /*! @@ -2031,7 +2034,9 @@ var CPViewHighDPIDrawingEnabled = YES; _backgroundColor = aColor; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + if (_backgroundType === BackgroundCSSStyling) [CPColor restorePreviousCSSState:@ref(_cssStylePreviousState) forDOMElement:_DOMElement]; @@ -2309,7 +2314,6 @@ var CPViewHighDPIDrawingEnabled = YES; } } } -#endif } /*! @@ -2655,8 +2659,7 @@ setBoundsOrigin: */ - (void)_applyCSSScalingTranformations { -#if PLATFORM(DOM) - if (_isScaled) + if (CPDOMAvailable && _isScaled) { var scale = [self scaleSize], browserPropertyTransform = CPBrowserStyleProperty(@"transform"), @@ -2667,7 +2670,6 @@ setBoundsOrigin: [self _setDisplayServerSetStyleSize:[self frameSize]]; } -#endif } // Displaying @@ -2750,14 +2752,15 @@ setBoundsOrigin: if ([self isHidden]) return; -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + [self lockFocus]; CGContextClearRect([[CPGraphicsContext currentContext] graphicsPort], aRect); [self drawRect:aRect]; [self unlockFocus]; -#endif } - (void)viewWillDraw @@ -2773,42 +2776,41 @@ setBoundsOrigin: { var graphicsPort = CGBitmapGraphicsContextCreate(); -#if PLATFORM(DOM) - var width = CGRectGetWidth(_frame), - height = CGRectGetHeight(_frame), - devicePixelRatio = window.devicePixelRatio || 1, - backingStoreRatio = CPBrowserBackingStorePixelRatio(graphicsPort); + if (CPDOMAvailable) + { + var width = CGRectGetWidth(_frame), + height = CGRectGetHeight(_frame), + devicePixelRatio = window.devicePixelRatio || 1, + backingStoreRatio = CPBrowserBackingStorePixelRatio(graphicsPort); - _highDPIRatio = CPViewHighDPIDrawingEnabled ? (devicePixelRatio / backingStoreRatio) : 1; + _highDPIRatio = CPViewHighDPIDrawingEnabled ? (devicePixelRatio / backingStoreRatio) : 1; - _DOMContentsElement = graphicsPort.DOMElement; + _DOMContentsElement = graphicsPort.DOMElement; - _DOMContentsElement.style.zIndex = -100; + _DOMContentsElement.style.zIndex = -100; - _DOMContentsElement.style.overflow = "hidden"; - _DOMContentsElement.style.position = "absolute"; - _DOMContentsElement.style.visibility = "visible"; + _DOMContentsElement.style.overflow = "hidden"; + _DOMContentsElement.style.position = "absolute"; + _DOMContentsElement.style.visibility = "visible"; - CPDOMDisplayServerSetSize(_DOMContentsElement, width * _highDPIRatio, height * _highDPIRatio); + CPDOMDisplayServerSetSize(_DOMContentsElement, width * _highDPIRatio, height * _highDPIRatio); - CPDOMDisplayServerSetStyleLeftTop(_DOMContentsElement, NULL, 0.0, 0.0); - CPDOMDisplayServerSetStyleSize(_DOMContentsElement, width, height); + CPDOMDisplayServerSetStyleLeftTop(_DOMContentsElement, NULL, 0.0, 0.0); + CPDOMDisplayServerSetStyleSize(_DOMContentsElement, width, height); - // The performance implications of this aren't clear, but without this subviews might not be redrawn when this - // view moves. - if (CPPlatformHasBug(CPCanvasParentDrawErrorsOnMovementBug)) - _DOMElement.style.webkitTransform = 'translateX(0)'; + // The performance implications of this aren't clear, but without this subviews might not be redrawn when this + // view moves. + if (CPPlatformHasBug(CPCanvasParentDrawErrorsOnMovementBug)) + _DOMElement.style.webkitTransform = 'translateX(0)'; - CPDOMDisplayServerAppendChild(_DOMElement, _DOMContentsElement); -#endif + CPDOMDisplayServerAppendChild(_DOMElement, _DOMContentsElement); + } _graphicsContext = [CPGraphicsContext graphicsContextWithGraphicsPort:graphicsPort flipped:YES]; _needToSetTransformMatrix = YES; } -#if PLATFORM(DOM) - if (_needToSetTransformMatrix && _highDPIRatio !== 1) + if (CPDOMAvailable && _needToSetTransformMatrix && _highDPIRatio !== 1) [_graphicsContext graphicsPort].setTransform(_highDPIRatio, 0, 0 , _highDPIRatio, 0, 0); -#endif _needToSetTransformMatrix = NO; [CPGraphicsContext setCurrentContext:_graphicsContext]; @@ -3250,10 +3252,8 @@ setBoundsOrigin: if (_layer) { _layer._owningView = nil; -#if PLATFORM(DOM) - if (_layer._DOMElement && _layer._DOMElement.parentNode === _DOMElement) + if (CPDOMAvailable && _layer._DOMElement && _layer._DOMElement.parentNode === _DOMElement) _DOMElement.removeChild(_layer._DOMElement); -#endif } _layer = aLayer; @@ -3263,10 +3263,11 @@ setBoundsOrigin: [_layer _setOwningView:self]; [_layer setFrame:[self bounds]]; // Sync layer frame with view bounds -#if PLATFORM(DOM) - _layer._DOMElement.style.zIndex = 100; - _DOMElement.appendChild(_layer._DOMElement); -#endif + if (CPDOMAvailable) + { + _layer._DOMElement.style.zIndex = 100; + _DOMElement.appendChild(_layer._DOMElement); + } } } @@ -3598,9 +3599,10 @@ setBoundsOrigin: - (void)setDOMClassName:(CPString)aClassName { -#if PLATFORM(DOM) + if (!CPDOMAvailable) + return; + _DOMElement.className = aClassName; -#endif } - (BOOL)isCSSBased @@ -3874,10 +3876,11 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask", // will decode our superview before we are done decoding, at which point // we have to have an element to place in the tree. Perhaps there is // a more "elegant" way to do this...? -#if PLATFORM(DOM) - _DOMElement = DOMElementPrototype.cloneNode(false); - AppKitTagDOMElement(self, _DOMElement); -#endif + if (CPDOMAvailable) + { + _DOMElement = DOMElementPrototype.cloneNode(false); + AppKitTagDOMElement(self, _DOMElement); + } // Also decode these "early". _frame = [aCoder decodeRectForKey:CPViewFrameKey]; @@ -3934,24 +3937,25 @@ var CPViewAutoresizingMaskKey = @"CPViewAutoresizingMask", [self _installToolTipEventHandlers]; // DOM SETUP -#if PLATFORM(DOM) - _cssStylePreviousState = @[]; - - _DOMImageParts = []; - _DOMImageSizes = []; - - CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(_frame), CGRectGetMinY(_frame)); - [self _setDisplayServerSetStyleSize:_frame.size]; - - var index = 0, - count = _subviews.length; - - for (; index < count; ++index) + if (CPDOMAvailable) { - CPDOMDisplayServerAppendChild(_DOMElement, _subviews[index]._DOMElement); - //_subviews[index]._superview = self; + _cssStylePreviousState = @[]; + + _DOMImageParts = []; + _DOMImageSizes = []; + + CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(_frame), CGRectGetMinY(_frame)); + [self _setDisplayServerSetStyleSize:_frame.size]; + + var index = 0, + count = _subviews.length; + + for (; index < count; ++index) + { + CPDOMDisplayServerAppendChild(_DOMElement, _subviews[index]._DOMElement); + //_subviews[index]._superview = self; + } } -#endif [self setHidden:[aCoder decodeBoolForKey:CPViewIsHiddenKey]]; _isHiddenOrHasHiddenAncestor = NO; diff --git a/AppKit/CPVisualEffectView.j b/AppKit/CPVisualEffectView.j index 45c9e7a92..b5eb5e05f 100644 --- a/AppKit/CPVisualEffectView.j +++ b/AppKit/CPVisualEffectView.j @@ -23,6 +23,7 @@ @import @import "CPAppearance.j" @import "CPView.j" +@import "CPCompatibility.j" @typedef CPVisualEffectMaterial CPVisualEffectMaterialAppearanceBased = 0; @@ -126,9 +127,10 @@ CPVisualEffectStateInactive = 2; [self setBackgroundColor:finalColor]; -#if PLATFORM(DOM) - self._DOMElement.style[prop] = shouldEnable ? "blur(30px)" : nil; -#endif + if (CPDOMAvailable) + { + self._DOMElement.style[prop] = shouldEnable ? "blur(30px)" : nil; + } } diff --git a/AppKit/CPWebView.j b/AppKit/CPWebView.j index 35a65860e..d3a22f13c 100644 --- a/AppKit/CPWebView.j +++ b/AppKit/CPWebView.j @@ -22,6 +22,7 @@ @import "CPView.j" @import "CPScrollView.j" +@import "CPCompatibility.j" @class CPWebScriptObject @@ -1031,9 +1032,10 @@ CPWebViewAppKitScrollMaxPollCount = 3; _forwardStack = []; _scrollMode = CPWebViewScrollAuto; -#if PLATFORM(DOM) - [self _initDOMWithFrame:[self frame]]; -#endif + if (CPDOMAvailable) + { + [self _initDOMWithFrame:[self frame]]; + } if (![self backgroundColor]) [self setBackgroundColor:[CPColor whiteColor]]; diff --git a/AppKit/CPWindow/CPWindow.j b/AppKit/CPWindow/CPWindow.j index 3773b3a0d..849ea5858 100644 --- a/AppKit/CPWindow/CPWindow.j +++ b/AppKit/CPWindow/CPWindow.j @@ -30,9 +30,7 @@ @import "CPDragServer.j" @import "CPEvent.j" @import "CPPlatformWindow.j" -#if PLATFORM(BROWSER) @import "CPPlatformWindow+DOM.j" -#endif @import "CPResponder.j" @import "CPScreen.j" @import "CPText.j" @@ -179,6 +177,7 @@ var CPWindowActionMessageKeys = [ BOOL _isVisible; BOOL _hasBeenOrderedIn @accessors; BOOL _isMiniaturized; + BOOL _isExcludedFromWindowsMenu; BOOL _isAnimating @accessors(setter=_setAnimating:); BOOL _hasShadow; BOOL _isMovableByWindowBackground; @@ -247,9 +246,7 @@ var CPWindowActionMessageKeys = [ BOOL _sharesChromeWithPlatformWindow; // Bridge Support -#if PLATFORM(DOM) DOMElement _DOMElement; -#endif unsigned _autoresizingMask; @@ -373,21 +370,22 @@ CPTexturedBackgroundWindowMask [_windowView setNextResponder:self]; // CSS Styling -#if PLATFORM(DOM) - var radius; - - if (radius = [_windowView actualValueForThemeAttribute:@"border-top-left-radius"]) - _windowView._DOMElement.style.borderTopLeftRadius = radius; - - if (radius = [_windowView actualValueForThemeAttribute:@"border-top-right-radius"]) - _windowView._DOMElement.style.borderTopRightRadius = radius; - - if (radius = [_windowView actualValueForThemeAttribute:@"border-bottom-left-radius"]) - _windowView._DOMElement.style.borderBottomLeftRadius = radius; - - if (radius = [_windowView actualValueForThemeAttribute:@"border-bottom-right-radius"]) - _windowView._DOMElement.style.borderBottomRightRadius = radius; -#endif + if (CPDOMAvailable) + { + var radius; + + if (radius = [_windowView actualValueForThemeAttribute:@"border-top-left-radius"]) + _windowView._DOMElement.style.borderTopLeftRadius = radius; + + if (radius = [_windowView actualValueForThemeAttribute:@"border-top-right-radius"]) + _windowView._DOMElement.style.borderTopRightRadius = radius; + + if (radius = [_windowView actualValueForThemeAttribute:@"border-bottom-left-radius"]) + _windowView._DOMElement.style.borderBottomLeftRadius = radius; + + if (radius = [_windowView actualValueForThemeAttribute:@"border-bottom-right-radius"]) + _windowView._DOMElement.style.borderBottomRightRadius = radius; + } // Size calculation needs _windowView _minSize = [self _calculateMinSizeForProposedSize:CGSizeMake(0.0, 0.0)]; @@ -400,21 +398,22 @@ CPTexturedBackgroundWindowMask _firstResponder = self; -#if PLATFORM(DOM) - _DOMElement = document.createElement("div"); - - _DOMElement.style.position = "absolute"; - _DOMElement.style.visibility = "visible"; - _DOMElement.style.zIndex = 0; - - if (![self _sharesChromeWithPlatformWindow]) + if (CPDOMAvailable) { - CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(_frame), CGRectGetMinY(_frame)); - } + _DOMElement = document.createElement("div"); - CPDOMDisplayServerSetStyleSize(_DOMElement, 1, 1); - CPDOMDisplayServerAppendChild(_DOMElement, _windowView._DOMElement); -#endif + _DOMElement.style.position = "absolute"; + _DOMElement.style.visibility = "visible"; + _DOMElement.style.zIndex = 0; + + if (![self _sharesChromeWithPlatformWindow]) + { + CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, CGRectGetMinX(_frame), CGRectGetMinY(_frame)); + } + + CPDOMDisplayServerSetStyleSize(_DOMElement, 1, 1); + CPDOMDisplayServerAppendChild(_DOMElement, _windowView._DOMElement); + } [self setNextResponder:CPApp]; @@ -580,16 +579,18 @@ CPTexturedBackgroundWindowMask [oldWindowView _setWindow:nil]; [oldWindowView noteToolbarChanged]; -#if PLATFORM(DOM) - CPDOMDisplayServerRemoveChild(_DOMElement, oldWindowView._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerRemoveChild(_DOMElement, oldWindowView._DOMElement); + } } if (_windowView) { -#if PLATFORM(DOM) - CPDOMDisplayServerAppendChild(_DOMElement, _windowView._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerAppendChild(_DOMElement, _windowView._DOMElement); + } var contentRect = [_contentView convertRect:[_contentView bounds] toView:nil]; @@ -785,12 +786,10 @@ CPTexturedBackgroundWindowMask origin.x = newOrigin.x; origin.y = newOrigin.y; -#if PLATFORM(DOM) - if (![self _sharesChromeWithPlatformWindow]) + if (CPDOMAvailable && ![self _sharesChromeWithPlatformWindow]) { CPDOMDisplayServerSetStyleLeftTop(_DOMElement, NULL, origin.x, origin.y); } -#endif // reposition sheet if ([self attachedSheet]) @@ -802,14 +801,15 @@ CPTexturedBackgroundWindowMask var size = _frame.size, newSize = frame.size; + // The delegate may change the proposed size, so ask it before the size is stored in _frame. + if (!_isAnimating && !CGSizeEqualToSize(size, newSize)) + newSize = [self _sendDelegateWindowWillResizeToSize:CGSizeMakeCopy(newSize)]; + if (!CGSizeEqualToSize(size, newSize)) { size.width = newSize.width; size.height = newSize.height; - if (!_isAnimating) - size = [self _sendDelegateWindowWillResizeToSize:size]; - [_windowView setFrameSize:size]; if (_hasShadow) @@ -987,25 +987,25 @@ CPTexturedBackgroundWindowMask - (void)_orderFront { -#if PLATFORM(DOM) + if (CPDOMAvailable) + { + if (!_isVisible) + [_platformWindow _setShouldUpdateContentRect:NO]; - if (!_isVisible) - [_platformWindow _setShouldUpdateContentRect:NO]; + // -dw- if a sheet is clicked, the parent window should come up too + if (_isSheet) + [_parentView orderFront:self]; - // -dw- if a sheet is clicked, the parent window should come up too - if (_isSheet) - [_parentView orderFront:self]; + // Save the boolean since it will be updated in the method order:window:relativeTo: + var wasVisible = _isVisible; - // Save the boolean since it will be updated in the method order:window:relativeTo: - var wasVisible = _isVisible; + [_platformWindow orderFront:self]; + [_platformWindow order:CPWindowAbove window:self relativeTo:nil]; - [_platformWindow orderFront:self]; - [_platformWindow order:CPWindowAbove window:self relativeTo:nil]; - - // setFrame is set after ordering the window as this method can send some notifications - if (!wasVisible) - [self _setFrame:_frame display:YES animate:NO constrainWidth:YES constrainHeight:YES]; -#endif + // setFrame is set after ordering the window as this method can send some notifications + if (!wasVisible) + [self _setFrame:_frame display:YES animate:NO constrainWidth:YES constrainHeight:YES]; + } if (!CPApp._keyWindow) [self makeKeyWindow]; @@ -1094,15 +1094,16 @@ CPTexturedBackgroundWindowMask if (recursive) [_childWindows makeObjectsPerformSelector:@selector(_orderOutRecursively:) withObject:recursive]; -#if PLATFORM(DOM) - if ([self _sharesChromeWithPlatformWindow]) - [_platformWindow orderOut:self]; + if (CPDOMAvailable) + { + if ([self _sharesChromeWithPlatformWindow]) + [_platformWindow orderOut:self]; - if (_isFullPlatformWindow && _platformWindow != [CPPlatformWindow primaryPlatformWindow]) - [_platformWindow orderOut:self]; + if (_isFullPlatformWindow && _platformWindow != [CPPlatformWindow primaryPlatformWindow]) + [_platformWindow orderOut:self]; - [_platformWindow order:CPWindowOut window:self relativeTo:nil]; -#endif + [_platformWindow order:CPWindowOut window:self relativeTo:nil]; + } [self makeFirstResponder:nil]; [self _updateMainAndKeyWindows]; @@ -1127,10 +1128,13 @@ CPTexturedBackgroundWindowMask [self _orderFront]; else if (orderingMode === CPWindowBelow && otherWindowNumber === 0) [self _orderBack]; -#if PLATFORM(DOM) else - [_platformWindow order:orderingMode window:self relativeTo:CPApp._windows[otherWindowNumber]]; -#endif + { + if (CPDOMAvailable) + [_platformWindow order:orderingMode window:self relativeTo:CPApp._windows[otherWindowNumber]]; + } + + [self _updateWindowsMenuItem]; } /*! @@ -1427,9 +1431,10 @@ CPTexturedBackgroundWindowMask { if (_shadowView) { -#if PLATFORM(DOM) - CPDOMDisplayServerRemoveChild(_DOMElement, _shadowView._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerRemoveChild(_DOMElement, _shadowView._DOMElement); + } _shadowView = nil; } @@ -1443,23 +1448,24 @@ CPTexturedBackgroundWindowMask // When using CSS theming, we get rid of _shadowView and add CSS shadowing directly // on the _windowView -#if PLATFORM(DOM) - // We must check that _windowView exists as _updateShadow can be called by - // _setSharesChromeWithPlatformWindow whereas _windowView is not yet created - if (_windowView && _windowView._DOMElement) + if (CPDOMAvailable) { - var currentBoxShadow = _windowView._DOMElement.style.boxShadow; - - if (_hasShadow && (currentBoxShadow.length == 0)) + // We must check that _windowView exists as _updateShadow can be called by + // _setSharesChromeWithPlatformWindow whereas _windowView is not yet created + if (_windowView && _windowView._DOMElement) { - _windowView._DOMElement.style.boxShadow = [_windowView actualValueForThemeAttribute:@"window-shadow-color"]; - } - else if (!_hasShadow && (currentBoxShadow.length > 0)) - { - _windowView._DOMElement.style.boxShadow = ""; + var currentBoxShadow = _windowView._DOMElement.style.boxShadow; + + if (_hasShadow && (currentBoxShadow.length == 0)) + { + _windowView._DOMElement.style.boxShadow = [_windowView actualValueForThemeAttribute:@"window-shadow-color"]; + } + else if (!_hasShadow && (currentBoxShadow.length > 0)) + { + _windowView._DOMElement.style.boxShadow = ""; + } } } -#endif } else { @@ -1471,15 +1477,17 @@ CPTexturedBackgroundWindowMask [_shadowView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; [_shadowView setNeedsLayout]; -#if PLATFORM(DOM) - CPDOMDisplayServerInsertBefore(_DOMElement, _shadowView._DOMElement, _windowView._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerInsertBefore(_DOMElement, _shadowView._DOMElement, _windowView._DOMElement); + } } else if (!_hasShadow && _shadowView) { -#if PLATFORM(DOM) - CPDOMDisplayServerRemoveChild(_DOMElement, _shadowView._DOMElement); -#endif + if (CPDOMAvailable) + { + CPDOMDisplayServerRemoveChild(_DOMElement, _shadowView._DOMElement); + } _shadowView = nil; } } @@ -1790,6 +1798,54 @@ CPTexturedBackgroundWindowMask [_windowView setTitle:aTitle]; [_platformWindow _setTitle:_title window:self]; + + [self _updateWindowsMenuItem]; +} + +/*! + Returns \c YES if the window is never listed in the application's Windows menu. +*/ +- (BOOL)isExcludedFromWindowsMenu +{ + return _isExcludedFromWindowsMenu; +} + +/*! + Sets whether the window is excluded from the application's Windows menu. By default, a window + is listed while it is visible or miniaturized, has a title and can become the main window. + @param shouldBeExcluded \c YES to exclude the window from the Windows menu +*/ +- (void)setExcludedFromWindowsMenu:(BOOL)shouldBeExcluded +{ + if (_isExcludedFromWindowsMenu === shouldBeExcluded) + return; + + _isExcludedFromWindowsMenu = shouldBeExcluded; + + [self _updateWindowsMenuItem]; +} + +/* @ignore */ +- (void)_updateWindowsMenuItem +{ + if (_isExcludedFromWindowsMenu || ![_title length]) + [self _removeFromWindowsMenu]; + + else if ([self canBecomeMainWindow]) + [CPApp changeWindowsItem:self title:_title filename:NO]; + + // A miniaturized window stays listed, so that the user can bring it back from the menu. + else if (_isMiniaturized) + [CPApp updateWindowsItem:self]; + + else + [self _removeFromWindowsMenu]; +} + +/* @ignore */ +- (void)_removeFromWindowsMenu +{ + [CPApp removeWindowsItem:self]; } /*! @@ -1954,34 +2010,42 @@ CPTexturedBackgroundWindowMask return [[self firstResponder] keyUp:anEvent]; case CPKeyDown: + // Tab and Shift-Tab move to the next and previous key view, unless the first responder takes + // them itself, as an editable text view does to insert a tab character or change a list + // item's level. Control-Tab always moves on, as in Cocoa. + var modifierFlags = [anEvent modifierFlags], + firstResponderTakesTab = !(modifierFlags & CPControlKeyMask) && [[self firstResponder] _wantsTabKey]; + if ([anEvent charactersIgnoringModifiers] === CPTabCharacter && - !([anEvent modifierFlags] & (CPAlternateKeyMask | CPCommandKeyMask))) + !(modifierFlags & (CPAlternateKeyMask | CPCommandKeyMask)) && + !firstResponderTakesTab) { if ([anEvent modifierFlags] & CPShiftKeyMask) [self selectPreviousKeyView:self]; else [self selectNextKeyView:self]; -#if PLATFORM(DOM) - // Make sure the browser doesn't try to do its own tab handling. - // This is important or the browser might blur the shared text field or token field input field, - // even that we just moved it to a new first responder. - [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:NO]; -#endif + if (CPDOMAvailable) + { + // Make sure the browser doesn't try to do its own tab handling. + // This is important or the browser might blur the shared text field or token field input field, + // even that we just moved it to a new first responder. + [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:NO]; + } return; } - else if ([anEvent charactersIgnoringModifiers] === CPBackTabCharacter) + else if ([anEvent charactersIgnoringModifiers] === CPBackTabCharacter && !firstResponderTakesTab) { var didTabBack = [self selectPreviousKeyView:self]; if (didTabBack) { -#if PLATFORM(DOM) - // Make sure the browser doesn't try to do its own tab handling. - // This is important or the browser might blur the shared text field or token field input field, - // even that we just moved it to a new first responder. - [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:NO]; -#endif - + if (CPDOMAvailable) + { + // Make sure the browser doesn't try to do its own tab handling. + // This is important or the browser might blur the shared text field or token field input field, + // even that we just moved it to a new first responder. + [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:NO]; + } } return didTabBack; } @@ -1990,6 +2054,10 @@ CPTexturedBackgroundWindowMask return; } + // A first responder that takes Tab gets the key, and the browser must not also move its own focus. + if (firstResponderTakesTab && [anEvent charactersIgnoringModifiers] === CPTabCharacter && CPDOMAvailable) + [[[anEvent window] platformWindow] _propagateCurrentDOMEvent:NO]; + [[self firstResponder] keyDown:anEvent]; // Trigger the default button if needed @@ -2434,6 +2502,8 @@ CPTexturedBackgroundWindowMask [[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidMiniaturizeNotification object:self]; _isMiniaturized = YES; + + [self _updateWindowsMenuItem]; } /*! @@ -2446,6 +2516,8 @@ CPTexturedBackgroundWindowMask [[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidDeminiaturizeNotification object:self]; _isMiniaturized = NO; + + [self _updateWindowsMenuItem]; } /*! @@ -2550,6 +2622,9 @@ CPTexturedBackgroundWindowMask [self _orderOutRecursively:NO]; [self _detachFromChildrenClosing:!_parentWindow]; + // A closed window leaves the Windows menu, even if it was miniaturized. + [self _removeFromWindowsMenu]; + if (_releasedWhenClosed) [_contentView _releaseRecursively]; } @@ -2631,6 +2706,8 @@ CPTexturedBackgroundWindowMask [_windowView noteMainWindowStateChanged]; + [CPApp updateWindowsItem:self]; + [[CPNotificationCenter defaultCenter] postNotificationName:CPWindowDidBecomeMainNotification object:self]; @@ -2650,6 +2727,8 @@ CPTexturedBackgroundWindowMask _platformWindow._currentMainWindow = nil; [_windowView noteMainWindowStateChanged]; + + [CPApp updateWindowsItem:self]; } - (void)_updateMainAndKeyWindows @@ -2764,6 +2843,37 @@ CPTexturedBackgroundWindowMask [toolbar setVisible:![toolbar isVisible]]; } +/*! + Validates the user interface items whose actions the window handles. + + For \c -toggleToolbarShown:, the item's title becomes "Hide Toolbar" or "Show Toolbar" + to reflect the toolbar's visibility, and the item is enabled only when the window has + a toolbar. \c -undo: and \c -redo: are enabled only when the window's undo manager has a + change to undo or redo. Every other item is enabled. + @param anItem the item to validate + @return \c YES if the item should be enabled +*/ +- (BOOL)validateUserInterfaceItem:(id /**/)anItem +{ + var action = [anItem action]; + + if (action === @selector(undo:)) + return [[self undoManager] canUndo]; + + if (action === @selector(redo:)) + return [[self undoManager] canRedo]; + + if (action !== @selector(toggleToolbarShown:)) + return YES; + + var toolbar = [self toolbar]; + + if ([anItem respondsToSelector:@selector(setTitle:)]) + [anItem setTitle:[toolbar isVisible] ? @"Hide Toolbar" : @"Show Toolbar"]; + + return !!toolbar; +} + - (void)_noteToolbarChanged { var frame = CGRectMakeCopy([self frame]), @@ -3718,12 +3828,10 @@ var keyViewComparator = function(lhs, rhs, context) */ - (CGPoint)_nativeScrollOffset { -#if PLATFORM(DOM) - return CGPointMake(_windowView._DOMElement.scrollLeft, _windowView._DOMElement.scrollTop); -#else - CGPointMake(0, 0); -#endif + if (CPDOMAvailable) + return CGPointMake(_windowView._DOMElement.scrollLeft, _windowView._DOMElement.scrollTop); + CGPointMake(0, 0); } /*! diff --git a/AppKit/CPWindow/_CPBorderlessBridgeWindowView.j b/AppKit/CPWindow/_CPBorderlessBridgeWindowView.j index a74994230..80dae400e 100644 --- a/AppKit/CPWindow/_CPBorderlessBridgeWindowView.j +++ b/AppKit/CPWindow/_CPBorderlessBridgeWindowView.j @@ -21,6 +21,7 @@ */ @import "_CPWindowView.j" +@import "CPCompatibility.j" @implementation _CPBorderlessBridgeWindowView : _CPWindowView { @@ -106,10 +107,11 @@ if (_frame.size.width < theWindow._minSize.width || _frame.size.height < theWindow._minSize.height) [theWindow._contentView setFrameSize:CGSizeMake(MAX(_frame.size.width, theWindow._minSize.width), MAX(_frame.size.height, theWindow._minSize.height))]; -#if PLATFORM(DOM) - _DOMElement.style.overflowX = (_frame.size.width < theWindow._minSize.width)? "scroll":"hidden"; - _DOMElement.style.overflowY = (_frame.size.height < theWindow._minSize.height)? "scroll":"hidden"; -#endif + if (CPDOMAvailable) + { + _DOMElement.style.overflowX = (_frame.size.width < theWindow._minSize.width)? "scroll":"hidden"; + _DOMElement.style.overflowY = (_frame.size.height < theWindow._minSize.height)? "scroll":"hidden"; + } } @end diff --git a/AppKit/CPWindow/_CPWindowView.j b/AppKit/CPWindow/_CPWindowView.j index d3abb5ee5..cfb69bbc0 100644 --- a/AppKit/CPWindow/_CPWindowView.j +++ b/AppKit/CPWindow/_CPWindowView.j @@ -715,6 +715,28 @@ _CPWindowViewResizeSlop = 3; frame.size.height += dy; } + // Let the delegate adjust the size. When resizing from the left or top, move the origin + // by the adjustment so that the edge opposite the one being dragged stays in place. + var delegateSize = [theWindow _sendDelegateWindowWillResizeToSize:CGSizeMakeCopy(frame.size)]; + + switch (_resizeRegion) + { + case _CPWindowViewResizeRegionTopLeft: + case _CPWindowViewResizeRegionLeft: + case _CPWindowViewResizeRegionBottomLeft: + frame.origin.x -= delegateSize.width - frame.size.width; + } + + switch (_resizeRegion) + { + case _CPWindowViewResizeRegionTopLeft: + case _CPWindowViewResizeRegionTop: + case _CPWindowViewResizeRegionTopRight: + frame.origin.y -= delegateSize.height - frame.size.height; + } + + frame.size = delegateSize; + [theWindow _setFrame:frame display:YES animate:NO constrainWidth:NO constrainHeight:NO]; [self setCursorForLocation:location resizing:YES]; } diff --git a/AppKit/Cib/_CPCibCustomObject.j b/AppKit/Cib/_CPCibCustomObject.j index 17410767f..152ef81d3 100644 --- a/AppKit/Cib/_CPCibCustomObject.j +++ b/AppKit/Cib/_CPCibCustomObject.j @@ -81,9 +81,7 @@ var _CPCibCustomObjectClassName = @"_CPCibCustomObjectClassName"; if (!theClass) { -#if DEBUG CPLog("Unknown class \"" + _className + "\" in cib file"); -#endif theClass = [CPObject class]; } diff --git a/AppKit/Cib/_CPCibCustomView.j b/AppKit/Cib/_CPCibCustomView.j index 5be132d6b..2d830ceff 100644 --- a/AppKit/Cib/_CPCibCustomView.j +++ b/AppKit/Cib/_CPCibCustomView.j @@ -82,9 +82,7 @@ var _CPCibCustomViewClassNameKey = @"_CPCibCustomViewClassNameKey"; // FIXME: Should we instead throw an exception? if (!theClass) { -#if DEBUG CPLog("Unknown class \"" + _className + "\" in cib file, using CPView instead."); -#endif theClass = [CPView class]; } diff --git a/AppKit/Cib/_CPCibWindowTemplate.j b/AppKit/Cib/_CPCibWindowTemplate.j index 6ae9be3cf..8bec31879 100644 --- a/AppKit/Cib/_CPCibWindowTemplate.j +++ b/AppKit/Cib/_CPCibWindowTemplate.j @@ -91,9 +91,7 @@ if (!windowClass) { -#if DEBUG CPLog.warn("Unknown class \"%@\" in cib file, using CPWindow instead.", [self windowClass]); -#endif windowClass = [CPWindow class]; } diff --git a/AppKit/CoreAnimation/CABackingStore.j b/AppKit/CoreAnimation/CABackingStore.j index 847430f62..7cde15601 100644 --- a/AppKit/CoreAnimation/CABackingStore.j +++ b/AppKit/CoreAnimation/CABackingStore.j @@ -23,7 +23,10 @@ @import "CGGeometry.j" @import "CPCompatibility.j" -#define PIXEL(pixels) pixels + "px"; +function PIXEL(pixels) +{ + return pixels + "px"; +} function CABackingStoreGetContext(aBackingStore) diff --git a/AppKit/CoreAnimation/CALayer.j b/AppKit/CoreAnimation/CALayer.j index 4cd003635..bf1349a4f 100644 --- a/AppKit/CoreAnimation/CALayer.j +++ b/AppKit/CoreAnimation/CALayer.j @@ -28,11 +28,15 @@ @import "CGContext.j" @import "CGGeometry.j" @import "CPColor.j" +@import "CPCompatibility.j" @import "CPView.j" @import "CAMediaTimingFunction.j" -#define DOM(aLayer) aLayer._DOMElement +function DOM(aLayer) +{ + return aLayer._DOMElement; +} var CALayerGeometryBoundsMask = 1, CALayerGeometryPositionMask = 2, @@ -166,18 +170,19 @@ var CALayerRegisteredRunLoopUpdates = nil; _activeAnimations = [CPMutableDictionary dictionary]; -#if PLATFORM(DOM) - _DOMElement = document.createElement("div"); + if (CPDOMAvailable) + { + _DOMElement = document.createElement("div"); - _DOMElement.style.overflow = "visible"; - _DOMElement.style.position = "absolute"; - _DOMElement.style.visibility = "visible"; - _DOMElement.style.top = "0px"; - _DOMElement.style.left = "0px"; - _DOMElement.style.zIndex = 0; - _DOMElement.style.width = "0px"; - _DOMElement.style.height = "0px"; -#endif + _DOMElement.style.overflow = "visible"; + _DOMElement.style.position = "absolute"; + _DOMElement.style.visibility = "visible"; + _DOMElement.style.top = "0px"; + _DOMElement.style.left = "0px"; + _DOMElement.style.zIndex = 0; + _DOMElement.style.width = "0px"; + _DOMElement.style.height = "0px"; + } } return self; @@ -503,47 +508,48 @@ var CALayerRegisteredRunLoopUpdates = nil; */ - (void)display { -#if PLATFORM(DOM) - if (!_context) + if (CPDOMAvailable) { - _context = CGBitmapGraphicsContextCreate(); + if (!_context) + { + _context = CGBitmapGraphicsContextCreate(); - _DOMContentsElement = _context.DOMElement; + _DOMContentsElement = _context.DOMElement; - _DOMContentsElement.style.zIndex = -100; + _DOMContentsElement.style.zIndex = -100; - _DOMContentsElement.style.overflow = "hidden"; - _DOMContentsElement.style.position = "absolute"; - _DOMContentsElement.style.visibility = "visible"; + _DOMContentsElement.style.overflow = "hidden"; + _DOMContentsElement.style.position = "absolute"; + _DOMContentsElement.style.visibility = "visible"; - _DOMContentsElement.width = ROUND(CGRectGetWidth(_backingStoreFrame)); - _DOMContentsElement.height = ROUND(CGRectGetHeight(_backingStoreFrame)); + _DOMContentsElement.width = ROUND(CGRectGetWidth(_backingStoreFrame)); + _DOMContentsElement.height = ROUND(CGRectGetHeight(_backingStoreFrame)); - _DOMContentsElement.style.top = "0px"; - _DOMContentsElement.style.left = "0px"; - _DOMContentsElement.style.width = ROUND(CGRectGetWidth(_backingStoreFrame)) + "px"; - _DOMContentsElement.style.height = ROUND(CGRectGetHeight(_backingStoreFrame)) + "px"; + _DOMContentsElement.style.top = "0px"; + _DOMContentsElement.style.left = "0px"; + _DOMContentsElement.style.width = ROUND(CGRectGetWidth(_backingStoreFrame)) + "px"; + _DOMContentsElement.style.height = ROUND(CGRectGetHeight(_backingStoreFrame)) + "px"; - _DOMElement.appendChild(_DOMContentsElement); + _DOMElement.appendChild(_DOMContentsElement); + } + + if (USE_BUFFER) + { + if (_delegateRespondsToDisplayLayerSelector) + return [_delegate displayLayer:self]; + + if (CGRectGetWidth(_backingStoreFrame) == 0.0 || CGRectGetHeight(_backingStoreFrame) == 0.0) + return; + + if (!_contents) + _contents = CABackingStoreCreate(); + + CABackingStoreSetSize(_contents, _bounds.size); + + [self drawInContext:CABackingStoreGetContext(_contents)]; + } } - if (USE_BUFFER) - { - if (_delegateRespondsToDisplayLayerSelector) - return [_delegate displayLayer:self]; - - if (CGRectGetWidth(_backingStoreFrame) == 0.0 || CGRectGetHeight(_backingStoreFrame) == 0.0) - return; - - if (!_contents) - _contents = CABackingStoreCreate(); - - CABackingStoreSetSize(_contents, _bounds.size); - - [self drawInContext:CABackingStoreGetContext(_contents)]; - } -#endif - [self composite]; } @@ -664,10 +670,6 @@ var CALayerRegisteredRunLoopUpdates = nil; return _superlayer; } -#define ADJUST_CONTENTS_ZINDEX(aLayer)\ -if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex)\ - _DOMContentsElement.style.zIndex -= 100.0;\ - /*! Adds the specified layer as a sublayer of the receiver. */ @@ -720,16 +722,17 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) else if (superlayer != nil) [aLayer removeFromSuperlayer]; - ADJUST_CONTENTS_ZINDEX(aLayer); + [self _adjustContentsZIndexForLayer:aLayer]; [_sublayers insertObject:aLayer atIndex:anIndex]; -#if PLATFORM(DOM) - if (anIndex >= _sublayers.length - 1) - _DOMElement.appendChild(DOM(aLayer)); - else - _DOMElement.insertBefore(DOM(aLayer), _sublayers[anIndex + 1]._DOMElement); -#endif + if (CPDOMAvailable) + { + if (anIndex >= _sublayers.length - 1) + _DOMElement.appendChild(DOM(aLayer)); + else + _DOMElement.insertBefore(DOM(aLayer), _sublayers[anIndex + 1]._DOMElement); + } aLayer._superlayer = self; @@ -782,7 +785,7 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) return; } - ADJUST_CONTENTS_ZINDEX(aLayer); + [self _adjustContentsZIndexForLayer:aLayer]; [_sublayers replaceObjectAtIndex:[_sublayers indexOfObjectIdenticalTo:aSublayer] withObject:aLayer]; _DOMElement.replaceChild(DOM(aSublayer), DOM(aLayer)); @@ -1267,6 +1270,12 @@ if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) window.loop = false; } +- (void)_adjustContentsZIndexForLayer:(CALayer)aLayer +{ + if (_DOMContentsElement && aLayer._zPosition > _DOMContentsElement.style.zIndex) + _DOMContentsElement.style.zIndex -= 100.0; +} + @end function _CALayerUpdateSublayerTransformForSublayers(aLayer) @@ -1284,7 +1293,8 @@ function _CALayerUpdateSublayerTransformForSublayers(aLayer) function _CALayerUpdateDOM(aLayer, aMask) { -#if PLATFORM(DOM) + if (!CPDOMAvailable) return; + var DOMElementStyle = aLayer._DOMElement.style; if (aMask & CALayerZPositionUpdateMask) @@ -1315,7 +1325,6 @@ function _CALayerUpdateDOM(aLayer, aMask) DOMContentsElement.style.height = height + "px"; } } -#endif } function _CALayerRecalculateGeometry(aLayer, aGeometryChange) diff --git a/AppKit/CoreAnimation/CPAnimationContext.j b/AppKit/CoreAnimation/CPAnimationContext.j index e2787cfa4..4ee10a10d 100644 --- a/AppKit/CoreAnimation/CPAnimationContext.j +++ b/AppKit/CoreAnimation/CPAnimationContext.j @@ -70,9 +70,6 @@ var _CPAnimationContextStack = nil, { if (!_animationFlushingObserver) { -#if (DEBUG) - CPLog.debug("create new observer"); -#endif _animationFlushingObserver = CFRunLoopObserverCreate(2, true, 0, _animationFlushingObserverCallback, 0); CFRunLoopAddObserver([CPRunLoop mainRunLoop], _animationFlushingObserver); } @@ -101,9 +98,6 @@ var _CPAnimationContextStack = nil, [context _flushAnimations]; [_CPAnimationContextStack removeLastObject]; -#if (DEBUG) - CPLog.debug(_cmd + "context stack =" + _CPAnimationContextStack); -#endif return YES; } @@ -234,9 +228,6 @@ var _CPAnimationContextStack = nil, { if (_completionHandlerAgent) { -#if (DEBUG) - CPLog.debug("No animations are scheduled. Firing completion handler"); -#endif _completionHandlerAgent.fire(); } } @@ -266,9 +257,6 @@ var _CPAnimationContextStack = nil, { if (n == 0) { -#if (DEBUG) - CPLog.debug("Animations are not needed. Firing completion handler"); -#endif _completionHandlerAgent.fire(); } else @@ -278,18 +266,12 @@ var _CPAnimationContextStack = nil, // start timers while(k--) { -#if (DEBUG) - CPLog.debug("START TIMER " + timers[k].description()); -#endif timers[k].start(); } // start css animations while(n--) { -#if (DEBUG) - CPLog.debug("START ANIMATION " + cssAnimations[n].description()); -#endif cssAnimations[n].start(); } } @@ -350,9 +332,6 @@ var _CPAnimationContextStack = nil, action.completion = function() { [aSubview setFrame:targetFrame]; -#if (DEBUG) - CPLog.debug(aSubview + " setFrame: " + CPStringFromRect(targetFrame)); -#endif if (idx == count - 1) [animatorClass stopUpdaterWithIdentifier:[anAction.root UID]]; }; @@ -401,9 +380,6 @@ var _CPAnimationContextStack = nil, if (aCompletionHandler) { _completionHandlerAgent = new CompletionHandlerAgent(aCompletionHandler); -#if (DEBUG) - CPLog.debug("created a new completion Agent with id " + _completionHandlerAgent.id); -#endif } else { @@ -563,9 +539,6 @@ CompletionHandlerAgent.prototype.invalidate = function() var _animationFlushingObserverCallback = function() { -#if (DEBUG) - CPLog.debug("_animationFlushingObserverCallback"); -#endif if ([_CPAnimationContextStack count] == 1) { var context = [_CPAnimationContextStack lastObject]; @@ -573,15 +546,8 @@ var _animationFlushingObserverCallback = function() [_CPAnimationContextStack removeLastObject]; } -#if (DEBUG) - CPLog.debug("_animationFlushingObserver "+_animationFlushingObserver+" stack:" + [_CPAnimationContextStack count]); -#endif - if (_animationFlushingObserver && ![_CPAnimationContextStack count]) { -#if (DEBUG) - CPLog.debug("removeObserver"); -#endif CFRunLoopObserverInvalidate([CPRunLoop mainRunLoop], _animationFlushingObserver); _animationFlushingObserver = nil; } diff --git a/AppKit/CoreAnimation/CPViewAnimator.j b/AppKit/CoreAnimation/CPViewAnimator.j index 5fb289ce2..96602d737 100644 --- a/AppKit/CoreAnimation/CPViewAnimator.j +++ b/AppKit/CoreAnimation/CPViewAnimator.j @@ -394,11 +394,10 @@ var frameToCSSTranslationTransformMatrix = function(start, current) - (Object)_DOMElement { -#if PLATFORM(DOM) - return _DOMElement; -#else + if (CPDOMAvailable) + return _DOMElement; + return nil; -#endif } - (CPString)debug_description diff --git a/AppKit/CoreGraphics/CGColor.j b/AppKit/CoreGraphics/CGColor.j index 371c6861d..0c305285f 100644 --- a/AppKit/CoreGraphics/CGColor.j +++ b/AppKit/CoreGraphics/CGColor.j @@ -212,7 +212,7 @@ function CGColorEqualToColor(lhs, rhs) if (lhs.pattern != rhs.pattern) return false; - if (CGColorSpaceEqualToColorSpace(lhs.colorspace, rhs.colorspace)) + if (lhs.colorspace !== rhs.colorspace) return false; return true; diff --git a/AppKit/CoreGraphics/CGColorSpace.j b/AppKit/CoreGraphics/CGColorSpace.j index e5d7673e8..c727f4732 100644 --- a/AppKit/CoreGraphics/CGColorSpace.j +++ b/AppKit/CoreGraphics/CGColorSpace.j @@ -69,8 +69,10 @@ kCGColorSpaceSRGB = "CGColorSpaceSRGB"; var _CGNamedColorSpaces = {}; -#define _CGColorSpaceCreateWithModel(aModel, aComponentCount, aBaseColorSpace) \ - { model:aModel, count:aComponentCount, base:aBaseColorSpace } +function _CGColorSpaceCreateWithModel(aModel, aComponentCount, aBaseColorSpace) +{ + return { model:aModel, count:aComponentCount, base:aBaseColorSpace }; +} function CGColorSpaceCreateCalibratedGray(aWhitePoint, aBlackPoint, gamma) { @@ -208,24 +210,6 @@ function CGColorSpaceRetain(aColorSpace) } // FIXME: We should refer to some default values. -#define STANDARDIZE(components, index, minimum, maximum, multiplier) \ -{ \ - if (index > components.length) \ - { \ - components[index] = maximum; \ - return; \ - } \ -\ - var component = components[index]; \ - \ - if (component < minimum) \ - components[index] = minimum; \ - else if (component > maximum) \ - components[index] = maximum; \ - else \ - components[index] = ROUND(component * multiplier) / multiplier; \ -} - function CGColorSpaceStandardizeComponents(aColorSpace, components) { var count = aColorSpace.count; @@ -233,7 +217,20 @@ function CGColorSpaceStandardizeComponents(aColorSpace, components) // Standardize the alpha value. We allow the alpha value to have a // higher precision than other components since it is not ultimately // bound to 256 bits like RGB. - STANDARDIZE(components, count, 0, 1, 1000); + if (count > components.length) + { + components[count] = 1; + return; + } + + var component = components[count]; + + if (component < 0) + components[count] = 0; + else if (component > 1) + components[count] = 1; + else + components[count] = ROUND(component * 1000) / 1000; if (aColorSpace.base) aColorSpace = aColorSpace.base; @@ -245,7 +242,16 @@ function CGColorSpaceStandardizeComponents(aColorSpace, components) case kCGColorSpaceModelCMYK: case kCGColorSpaceModelDeviceN: while (count--) - STANDARDIZE(components, count, 0, 1, 255); + { + var standardizingComponent = components[count]; + + if (standardizingComponent < 0) + components[count] = 0; + else if (standardizingComponent > 1) + components[count] = 1; + else + components[count] = ROUND(standardizingComponent * 255) / 255; + } break; // We don't currently support these color spaces. diff --git a/AppKit/CoreGraphics/CGContext.j b/AppKit/CoreGraphics/CGContext.j index 41aba64be..72b55f99c 100644 --- a/AppKit/CoreGraphics/CGContext.j +++ b/AppKit/CoreGraphics/CGContext.j @@ -730,11 +730,657 @@ function CGContextStrokeRoundedRectangleInRect(aContext, aRect, aRadius, ne, se, */ if (CPFeatureIsCompatible(CPHTMLCanvasFeature)) { -#include "CGContextCanvas.j" -} -else if (CPFeatureIsCompatible(CPVMLFeature)) +// --- begin CoreGraphics/CGContextCanvas.j (inlined; include fragment removed for preprocessor-free build) --- +var CANVAS_LINECAP_TABLE = [ "butt", "round", "square" ], + CANVAS_LINEJOIN_TABLE = [ "miter", "round", "bevel" ], + CANVAS_COMPOSITE_TABLE = [ "source-over", "source-over", "source-over", "source-over", "darker", + "lighter", "source-over", "source-over", "source-over", "source-over", + "source-over", "source-over", "source-over", "source-over", "source-over", + "source-over", "source-over", + "copy", "source-in", "source-out", "source-atop", + "destination-over", "destination-in", "destination-out", "destination-atop", + "xor", "source-over", "source-over" ]; + +function _CGContextAddArcCanvas(aContext, x, y, radius, startAngle, endAngle, anticlockwise) { -#include "CGContextVML.j" + return aContext.arc(x, y, radius, startAngle, endAngle, anticlockwise); +} +function _CGContextAddArcToPointCanvas(aContext, x1, y1, x2, y2, radius) +{ + return aContext.arcTo(x1, y1, x2, y2, radius); +} +function _CGContextAddCurveToPointCanvas(aContext, cp1x, cp1y, cp2x, cp2y, x, y) +{ + return aContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); +} +function _CGContextAddQuadCurveToPointCanvas(aContext, cpx, cpy, x, y) +{ + return aContext.quadraticCurveTo(cpx, cpy, x, y); +} +function _CGContextAddLineToPointCanvas(aContext, x, y) +{ + return aContext.lineTo(x, y); +} +function _CGContextClosePathCanvas(aContext) +{ + return aContext.closePath(); +} +function _CGContextMoveToPointCanvas(aContext, x, y) +{ + return aContext.moveTo(x, y); +} + +function _CGContextAddRectCanvas(aContext, aRect) +{ + return aContext.rect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); +} +function _CGContextBeginPathCanvas(aContext) +{ + return aContext.beginPath(); +} +function _CGContextFillRectCanvas(aContext, aRect) +{ + return aContext.fillRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); +} +function _CGContextClipCanvas(aContext) +{ + return aContext.clip(); +} + +// In Cocoa, all primitives excepts rects and arcs cannot be added to the context's path +// until a move to point has been done, because an empty path has no current point. +var hasPath = function(aContext, methodName) +{ + if (!aContext.hasPath) + CPLog.error(methodName + ": no current point"); + + return aContext.hasPath; +} + +function CGContextSaveGState(aContext) +{ + aContext.save(); +} + +function CGContextRestoreGState(aContext) +{ + aContext.restore(); +} + +function CGContextSetLineCap(aContext, aLineCap) +{ + aContext.lineCap = CANVAS_LINECAP_TABLE[aLineCap]; +} + +function CGContextSetLineDash(aContext, aPhase, someDashes) +{ + if (aContext.setLineDash) + { + aContext.setLineDash(someDashes); + aContext.lineDashOffset = aPhase; + } + else if (typeof aContext['webkitLineDash'] !== 'undefined') + { + aContext.webkitLineDash = someDashes; + aContext.webkitLineDashOffset = aPhase; + } + else if (typeof aContext['mozDash'] !== 'undefined') + { + aContext.mozDash = someDashes; + aContext.mozDashOffset = aPhase; + } + else if (someDashes) + { + CPLog.warn("CGContextSetLineDash not implemented in this environment.") + } +} + +function CGContextSetLineJoin(aContext, aLineJoin) +{ + aContext.lineJoin = CANVAS_LINEJOIN_TABLE[aLineJoin]; +} + +function CGContextSetLineWidth(aContext, aLineWidth) +{ + aContext.lineWidth = aLineWidth; +} + +function CGContextSetMiterLimit(aContext, aMiterLimit) +{ + aContext.miterLimit = aMiterLimit; +} + +function CGContextSetBlendMode(aContext, aBlendMode) +{ + aContext.globalCompositeOperation = CANVAS_COMPOSITE_TABLE[aBlendMode]; +} + +function CGContextAddArc(aContext, x, y, radius, startAngle, endAngle, clockwise) +{ + // Despite the documentation saying otherwise, the last parameter is anti-clockwise not clockwise. + // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes#Arcs + _CGContextAddArcCanvas(aContext, x, y, radius, startAngle, endAngle, !clockwise); + + // AddArc implicitly starts a path + aContext.hasPath = YES; +} + +function CGContextAddArcToPoint(aContext, x1, y1, x2, y2, radius) +{ + if (!hasPath(aContext, "CGContextAddArcToPoint")) + return; + + _CGContextAddArcToPointCanvas(aContext, x1, y1, x2, y2, radius); +} + +function CGContextAddCurveToPoint(aContext, cp1x, cp1y, cp2x, cp2y, x, y) +{ + if (!hasPath(aContext, "CGContextAddCurveToPoint")) + return; + + _CGContextAddCurveToPointCanvas(aContext, cp1x, cp1y, cp2x, cp2y, x, y); +} + +function CGContextAddLines(aContext, points, count) +{ + // implementation mirrors that of CGPathAddLines() + if (count == null) + count = points.length; + + if (count < 1) + return; + + _CGContextMoveToPointCanvas(aContext, points[0].x, points[0].y); + + for (var i = 1; i < count; ++i) + _CGContextAddLineToPointCanvas(aContext, points[i].x, points[i].y); + + aContext.hasPath = YES; +} + +function CGContextAddLineToPoint(aContext, x, y) +{ + if (!hasPath(aContext, "CGContextAddLineToPoint")) + return; + + _CGContextAddLineToPointCanvas(aContext, x, y); +} + +function CGContextAddPath(aContext, aPath) +{ + if (!aContext || CGPathIsEmpty(aPath)) + return; + + // If the context does not have a path, explicitly begin one + if (!aContext.hasPath) + _CGContextBeginPathCanvas(aContext); + + // We must implicitly move to the start of the path + _CGContextMoveToPointCanvas(aContext, aPath.start.x, aPath.start.y); + + var elements = aPath.elements, + i = 0, + count = aPath.count; + + for (; i < count; ++i) + { + var element = elements[i], + type = element.type; + + switch (type) + { + case kCGPathElementMoveToPoint: + _CGContextMoveToPointCanvas(aContext, element.x, element.y); + break; + + case kCGPathElementAddLineToPoint: + _CGContextAddLineToPointCanvas(aContext, element.x, element.y); + break; + + case kCGPathElementAddQuadCurveToPoint: + _CGContextAddQuadCurveToPointCanvas(aContext, element.cpx, element.cpy, element.x, element.y); + break; + + case kCGPathElementAddCurveToPoint: + _CGContextAddCurveToPointCanvas(aContext, element.cp1x, element.cp1y, element.cp2x, element.cp2y, element.x, element.y); + break; + + case kCGPathElementCloseSubpath: + _CGContextClosePathCanvas(aContext); + break; + + case kCGPathElementAddArc: + _CGContextAddArcCanvas(aContext, element.x, element.y, element.radius, element.startAngle, element.endAngle, element.clockwise); + break; + + case kCGPathElementAddArcToPoint: + _CGContextAddArcToPointCanvas(aContext, element.p1x, element.p1y, element.p2x, element.p2y, element.radius); + break; + } + } + + aContext.hasPath = YES; +} + +function CGContextAddRect(aContext, aRect) +{ + _CGContextAddRectCanvas(aContext, aRect); + aContext.hasPath = YES; +} + +function CGContextAddQuadCurveToPoint(aContext, cpx, cpy, x, y) +{ + if (!hasPath(aContext, "CGContextAddQuadCurveToPoint")) + return; + + _CGContextAddQuadCurveToPointCanvas(aContext, cpx, cpy, x, y); +} + +function CGContextAddRects(aContext, rects, count) +{ + if (count == null) + count = rects.length; + + for (var i = 0; i < count; ++i) + { + var rect = rects[i]; + _CGContextAddRectCanvas(aContext, rect); + } + + aContext.hasPath = YES; +} + +function CGContextBeginPath(aContext) +{ + _CGContextBeginPathCanvas(aContext); + aContext.hasPath = NO; +} + +function CGContextClosePath(aContext) +{ + _CGContextClosePathCanvas(aContext); +} + +function CGContextIsPathEmpty(aContext) +{ + return !aContext.hasPath; +} + +function CGContextMoveToPoint(aContext, x, y) +{ + _CGContextMoveToPointCanvas(aContext, x, y); + aContext.hasPath = YES; +} + +function CGContextClearRect(aContext, aRect) +{ + aContext.clearRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); + aContext.hasPath = NO; +} + +function CGContextDrawPath(aContext, aMode) +{ + if (!aContext.hasPath) + return; + + if (aMode === kCGPathFill || aMode === kCGPathFillStroke) + aContext.fill(); + else if (aMode === kCGPathStroke || aMode === kCGPathFillStroke || aMode === kCGPathEOFillStroke) + aContext.stroke(); + else if (aMode === kCGPathEOFill || aMode === kCGPathEOFillStroke) + CPLog.warn("Unimplemented fill mode in CGContextDrawPath: %d", aMode); + + aContext.hasPath = NO; +} + +function CGContextFillRect(aContext, aRect) +{ + _CGContextFillRectCanvas(aContext, aRect); + aContext.hasPath = NO; +} + +function CGContextFillRects(aContext, rects, count) +{ + if (count == null) + count = rects.length; + + for (var i = 0; i < count; ++i) + { + var rect = rects[i]; + _CGContextFillRectCanvas(aContext, rect); + } + + aContext.hasPath = NO; +} + +function CGContextStrokeRect(aContext, aRect) +{ + aContext.strokeRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); + aContext.hasPath = NO; +} + +function CGContextClip(aContext) +{ + _CGContextClipCanvas(aContext); + aContext.hasPath = NO; +} + +function CGContextClipToRect(aContext, aRect) +{ + _CGContextBeginPathCanvas(aContext); + _CGContextAddRectCanvas(aContext, aRect); + _CGContextClosePathCanvas(aContext); + + _CGContextClipCanvas(aContext); + aContext.hasPath = NO; +} + +function CGContextClipToRects(aContext, rects, count) +{ + if (count == null) + count = rects.length; + + _CGContextBeginPathCanvas(aContext); + CGContextAddRects(aContext, rects, count); + _CGContextClipCanvas(aContext); + aContext.hasPath = NO; +} + +function CGContextSetAlpha(aContext, anAlpha) +{ + aContext.globalAlpha = anAlpha; +} + +function CGContextSetFillColor(aContext, aColor) +{ + var patternImage = [aColor patternImage]; + + if ([patternImage isSingleImage]) + { + var pattern = aContext.createPattern([patternImage image], "repeat"); + + aContext.fillStyle = pattern; + } + else + aContext.fillStyle = [aColor cssString]; +} + +/*! + Creates a context into which you can render a fill pattern + of the given size. Once the pattern is rendered, you can + set the fill or stroke pattern to the rendered pattern + with CGContextSetFillPattern or CGContextSetStrokePattern. +*/ +function CGContextCreatePatternContext(aContext, aSize) +{ + var pattern = document.createElement("canvas"); + + pattern.width = aSize.width; + pattern.height = aSize.height; + + return pattern.getContext("2d"); +} + +/*! + Sets the fill pattern for aContext to the rendered pattern context + returned by CGContextCreatePatternContext. +*/ +function CGContextSetFillPattern(aContext, aPatternContext) +{ + var pattern = aContext.createPattern(aPatternContext.canvas, "repeat"); + aContext.fillStyle = pattern; +} + +/*! + Sets the stroke pattern for aContext to the rendered pattern context + returned by CGContextCreatePatternContext. +*/ +function CGContextSetStrokePattern(aContext, aPatternContext) +{ + var pattern = aContext.createPattern(aPatternContext.canvas, "repeat"); + aContext.strokeStyle = pattern; +} + +function CGContextSetStrokeColor(aContext, aColor) +{ + var patternImage = [aColor patternImage]; + + if ([patternImage isSingleImage]) + { + var pattern = aContext.createPattern([patternImage image], "repeat"); + + aContext.strokeStyle = pattern; + } + else + aContext.strokeStyle = [aColor cssString]; +} + +function CGContextSetShadow(aContext, aSize, aBlur) +{ + aContext.shadowOffsetX = aSize.width; + aContext.shadowOffsetY = aSize.height; + aContext.shadowBlur = aBlur; + aContext.shadowColor = [[CPColor shadowColor] cssString]; +} + +function CGContextSetShadowWithColor(aContext, aSize, aBlur, aColor) +{ + aContext.shadowOffsetX = aSize.width; + aContext.shadowOffsetY = aSize.height; + aContext.shadowBlur = aBlur; + aContext.shadowColor = [aColor cssString]; +} + +function CGContextRotateCTM(aContext, anAngle) +{ + aContext.rotate(anAngle); +} + +function CGContextScaleCTM(aContext, sx, sy) +{ + aContext.scale(sx, sy); +} + +function CGContextTranslateCTM(aContext, tx, ty) +{ + aContext.translate(tx, ty); +} + +var scale_rotate = function(a, b, c, d) +{ + var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, + a2 = (ATAN2(b, d) + ATAN2(-sign * c, sign * a)) / 2.0, + cos = COS(a2), + sin = SIN(a2), + abs_cos, + abs_sin; + + if (cos === 0) + { + sx = -c / sin; + sy = b / sin; + } + else if (sin === 0) + { + sx = a / cos; + sy = d / cos; + } + else + { + abs_cos = ABS(cos); + abs_sin = ABS(sin); + + sx = (abs_cos * a / cos + abs_sin * -c / sin) / (abs_cos + abs_sin); + sy = (abs_cos * d / cos + abs_sin * b / sin) / (abs_cos + abs_sin); + } +}; + +var rotate_scale = function(a, b, c, d) +{ + var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, + a1 = (ATAN2(sign * b, sign * a) + ATAN2(-c, d)) / 2.0, + cos = COS(a1), + sin = SIN(a1), + abs_cos, + abs_sin; + + if (cos === 0) + { + sx = b / sin; + sy = -c / sin; + } + else if (sin === 0) + { + sx = a / cos; + sy = d / cos; + } + else + { + abs_cos = ABS(cos); + abs_sin = ABS(sin); + + sx = (abs_cos * a / cos + abs_sin * b / sin) / (abs_cos + abs_sin); + sy = (abs_cos * d / cos + abs_sin * -c / sin) / (abs_cos + abs_sin); + } +}; + +function eigen(anAffineTransform) +{ + CPLog.warn("Unimplemented function: eigen"); +} + + +if (CPFeatureIsCompatible(CPJavaScriptCanvasTransformFeature)) +{ + +CGContextConcatCTM = function(aContext, anAffineTransform) +{ + aContext.transform(anAffineTransform.a, anAffineTransform.b, anAffineTransform.c, anAffineTransform.d, anAffineTransform.tx, anAffineTransform.ty); +}; + +} +else +{ + +CGContextConcatCTM = function(aContext, anAffineTransform) +{ + var a = anAffineTransform.a, + b = anAffineTransform.b, + c = anAffineTransform.c, + d = anAffineTransform.d, + tx = anAffineTransform.tx, + ty = anAffineTransform.ty, + sx = 1.0, + sy = 1.0, + a1 = 0.0, + a2 = 0.0; + + // Detect the simple case of just scaling. + if (b === 0.0 && c === 0.0) + { + sx = a; + sy = d; + } + + // a scale followed by a rotate + else if (a * b === -c * d) + { + scale_rotate(a, b, c, d) + } + + // rotate, then scale. + else if (a * c === -b * d) + { + rotate_scale(a, b, c, d) + } + else + { + var transpose = CGAffineTransformMake(a, c, b, d, 0.0, 0.0), // inline + u = eigen(CGAffineTransformConcat(anAffineTransform, transpose)), + v = eigen(CGAffineTransformConcat(transpose, anAffineTransform)), + U = CGAffineTransformMake(u.vector_1.x, u.vector_2.x, u.vector_1.y, u.vector_2.y, 0.0, 0.0), // inline + VT = CGAffineTransformMake(v.vector_1.x, v.vector_1.y, v.vector_2.x, v.vector_2.y, 0.0, 0.0), + S = CGAffineTransformConcat(CGAffineTransformConcat(CGAffineTransformInvert(U), anAffineTransform), CGAffineTransformInvert(VT)); + + a = VT.a; + b = VT.b; + c = VT.c; + d = VT.d; + scale_rotate(a, b, c, d); + S.a *= sx; + S.d *= sy; + a = U.a; + b = U.b; + c = U.c; + d = U.d; + rotate_scale(a, b, c, d); + sx = S.a * sx; + sy = S.d * sy; + } + + if (tx != 0 || ty != 0) + CGContextTranslateCTM(aContext, tx, ty); + if (a1 != 0.0) + CGContextRotateCTM(aContext, a1); + if (sx != 1.0 || sy != 1.0) + CGContextScaleCTM(aContext, sx, sy); + if (a2 != 0.0) + CGContextRotateCTM(aContext, a2); +}; + +} + +function CGContextDrawImage(aContext, aRect, anImage) +{ + aContext.drawImage(anImage._image, CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); + aContext.hasPath = NO; +} + +function to_string(aColor) +{ + return "rgba(" + ROUND(aColor.components[0] * 255) + ", " + ROUND(aColor.components[1] * 255) + ", " + ROUND(255 * aColor.components[2]) + ", " + aColor.components[3] + ")"; +} + +function CGContextDrawLinearGradient(aContext, aGradient, aStartPoint, anEndPoint, options) +{ + var colors = aGradient.colors, + count = colors.length, + linearGradient = aContext.createLinearGradient(aStartPoint.x, aStartPoint.y, anEndPoint.x, anEndPoint.y); + + while (count--) + linearGradient.addColorStop(aGradient.locations[count], to_string(colors[count])); + + aContext.fillStyle = linearGradient; + aContext.fill(); + aContext.hasPath = NO; +} + +function CGContextDrawRadialGradient(aContext, aGradient, aStartCenter, aStartRadius, anEndCenter, anEndRadius, options) +{ + var colors = aGradient.colors, + count = colors.length, + linearGradient = aContext.createRadialGradient(aStartCenter.x, aStartCenter.y, aStartRadius, anEndCenter.x, anEndCenter.y, anEndRadius); + + while (count--) + linearGradient.addColorStop(aGradient.locations[count], to_string(colors[count])); + + aContext.fillStyle = linearGradient; + aContext.fill(); + aContext.hasPath = NO; +} + +function CGBitmapGraphicsContextCreate() +{ + var DOMElement = document.createElement("canvas"), + context = DOMElement.getContext("2d"); + + context.DOMElement = DOMElement; + + // canvas gives us no way to query whether the path is empty or not, so we have to track it ourselves + context.hasPath = NO; + + return context; +} } else { diff --git a/AppKit/CoreGraphics/CGContextCanvas.j b/AppKit/CoreGraphics/CGContextCanvas.j deleted file mode 100644 index d8d13e1f7..000000000 --- a/AppKit/CoreGraphics/CGContextCanvas.j +++ /dev/null @@ -1,635 +0,0 @@ -/* - * CGContextCanvas.j - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2008, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -var CANVAS_LINECAP_TABLE = [ "butt", "round", "square" ], - CANVAS_LINEJOIN_TABLE = [ "miter", "round", "bevel" ], - CANVAS_COMPOSITE_TABLE = [ "source-over", "source-over", "source-over", "source-over", "darker", - "lighter", "source-over", "source-over", "source-over", "source-over", - "source-over", "source-over", "source-over", "source-over", "source-over", - "source-over", "source-over", - "copy", "source-in", "source-out", "source-atop", - "destination-over", "destination-in", "destination-out", "destination-atop", - "xor", "source-over", "source-over" ]; - -#define _CGContextAddArcCanvas(aContext, x, y, radius, startAngle, endAngle, anticlockwise) aContext.arc(x, y, radius, startAngle, endAngle, anticlockwise) -#define _CGContextAddArcToPointCanvas(aContext, x1, y1, x2, y2, radius) aContext.arcTo(x1, y1, x2, y2, radius) -#define _CGContextAddCurveToPointCanvas(aContext, cp1x, cp1y, cp2x, cp2y, x, y) aContext.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) -#define _CGContextAddQuadCurveToPointCanvas(aContext, cpx, cpy, x, y) aContext.quadraticCurveTo(cpx, cpy, x, y) -#define _CGContextAddLineToPointCanvas(aContext, x, y) aContext.lineTo(x, y) -#define _CGContextClosePathCanvas(aContext) aContext.closePath() -#define _CGContextMoveToPointCanvas(aContext, x, y) aContext.moveTo(x, y) - -#define _CGContextAddRectCanvas(aContext, aRect) aContext.rect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)) -#define _CGContextBeginPathCanvas(aContext) aContext.beginPath() -#define _CGContextFillRectCanvas(aContext, aRect) aContext.fillRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)) -#define _CGContextClipCanvas(aContext) aContext.clip() - -// In Cocoa, all primitives excepts rects and arcs cannot be added to the context's path -// until a move to point has been done, because an empty path has no current point. -var hasPath = function(aContext, methodName) -{ - if (!aContext.hasPath) - CPLog.error(methodName + ": no current point"); - - return aContext.hasPath; -} - -function CGContextSaveGState(aContext) -{ - aContext.save(); -} - -function CGContextRestoreGState(aContext) -{ - aContext.restore(); -} - -function CGContextSetLineCap(aContext, aLineCap) -{ - aContext.lineCap = CANVAS_LINECAP_TABLE[aLineCap]; -} - -function CGContextSetLineDash(aContext, aPhase, someDashes) -{ - if (aContext.setLineDash) - { - aContext.setLineDash(someDashes); - aContext.lineDashOffset = aPhase; - } - else if (typeof aContext['webkitLineDash'] !== 'undefined') - { - aContext.webkitLineDash = someDashes; - aContext.webkitLineDashOffset = aPhase; - } - else if (typeof aContext['mozDash'] !== 'undefined') - { - aContext.mozDash = someDashes; - aContext.mozDashOffset = aPhase; - } - else if (someDashes) - { - CPLog.warn("CGContextSetLineDash not implemented in this environment.") - } -} - -function CGContextSetLineJoin(aContext, aLineJoin) -{ - aContext.lineJoin = CANVAS_LINEJOIN_TABLE[aLineJoin]; -} - -function CGContextSetLineWidth(aContext, aLineWidth) -{ - aContext.lineWidth = aLineWidth; -} - -function CGContextSetMiterLimit(aContext, aMiterLimit) -{ - aContext.miterLimit = aMiterLimit; -} - -function CGContextSetBlendMode(aContext, aBlendMode) -{ - aContext.globalCompositeOperation = CANVAS_COMPOSITE_TABLE[aBlendMode]; -} - -function CGContextAddArc(aContext, x, y, radius, startAngle, endAngle, clockwise) -{ - // Despite the documentation saying otherwise, the last parameter is anti-clockwise not clockwise. - // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes#Arcs - _CGContextAddArcCanvas(aContext, x, y, radius, startAngle, endAngle, !clockwise); - - // AddArc implicitly starts a path - aContext.hasPath = YES; -} - -function CGContextAddArcToPoint(aContext, x1, y1, x2, y2, radius) -{ - if (!hasPath(aContext, "CGContextAddArcToPoint")) - return; - - _CGContextAddArcToPointCanvas(aContext, x1, y1, x2, y2, radius); -} - -function CGContextAddCurveToPoint(aContext, cp1x, cp1y, cp2x, cp2y, x, y) -{ - if (!hasPath(aContext, "CGContextAddCurveToPoint")) - return; - - _CGContextAddCurveToPointCanvas(aContext, cp1x, cp1y, cp2x, cp2y, x, y); -} - -function CGContextAddLines(aContext, points, count) -{ - // implementation mirrors that of CGPathAddLines() - if (count == null) - count = points.length; - - if (count < 1) - return; - - _CGContextMoveToPointCanvas(aContext, points[0].x, points[0].y); - - for (var i = 1; i < count; ++i) - _CGContextAddLineToPointCanvas(aContext, points[i].x, points[i].y); - - aContext.hasPath = YES; -} - -function CGContextAddLineToPoint(aContext, x, y) -{ - if (!hasPath(aContext, "CGContextAddLineToPoint")) - return; - - _CGContextAddLineToPointCanvas(aContext, x, y); -} - -function CGContextAddPath(aContext, aPath) -{ - if (!aContext || CGPathIsEmpty(aPath)) - return; - - // If the context does not have a path, explicitly begin one - if (!aContext.hasPath) - _CGContextBeginPathCanvas(aContext); - - // We must implicitly move to the start of the path - _CGContextMoveToPointCanvas(aContext, aPath.start.x, aPath.start.y); - - var elements = aPath.elements, - i = 0, - count = aPath.count; - - for (; i < count; ++i) - { - var element = elements[i], - type = element.type; - - switch (type) - { - case kCGPathElementMoveToPoint: - _CGContextMoveToPointCanvas(aContext, element.x, element.y); - break; - - case kCGPathElementAddLineToPoint: - _CGContextAddLineToPointCanvas(aContext, element.x, element.y); - break; - - case kCGPathElementAddQuadCurveToPoint: - _CGContextAddQuadCurveToPointCanvas(aContext, element.cpx, element.cpy, element.x, element.y); - break; - - case kCGPathElementAddCurveToPoint: - _CGContextAddCurveToPointCanvas(aContext, element.cp1x, element.cp1y, element.cp2x, element.cp2y, element.x, element.y); - break; - - case kCGPathElementCloseSubpath: - _CGContextClosePathCanvas(aContext); - break; - - case kCGPathElementAddArc: - _CGContextAddArcCanvas(aContext, element.x, element.y, element.radius, element.startAngle, element.endAngle, element.clockwise); - break; - - case kCGPathElementAddArcToPoint: - _CGContextAddArcToPointCanvas(aContext, element.p1x, element.p1y, element.p2x, element.p2y, element.radius); - break; - } - } - - aContext.hasPath = YES; -} - -function CGContextAddRect(aContext, aRect) -{ - _CGContextAddRectCanvas(aContext, aRect); - aContext.hasPath = YES; -} - -function CGContextAddQuadCurveToPoint(aContext, cpx, cpy, x, y) -{ - if (!hasPath(aContext, "CGContextAddQuadCurveToPoint")) - return; - - _CGContextAddQuadCurveToPointCanvas(aContext, cpx, cpy, x, y); -} - -function CGContextAddRects(aContext, rects, count) -{ - if (count == null) - count = rects.length; - - for (var i = 0; i < count; ++i) - { - var rect = rects[i]; - _CGContextAddRectCanvas(aContext, rect); - } - - aContext.hasPath = YES; -} - -function CGContextBeginPath(aContext) -{ - _CGContextBeginPathCanvas(aContext); - aContext.hasPath = NO; -} - -function CGContextClosePath(aContext) -{ - _CGContextClosePathCanvas(aContext); -} - -function CGContextIsPathEmpty(aContext) -{ - return !aContext.hasPath; -} - -function CGContextMoveToPoint(aContext, x, y) -{ - _CGContextMoveToPointCanvas(aContext, x, y); - aContext.hasPath = YES; -} - -function CGContextClearRect(aContext, aRect) -{ - aContext.clearRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); - aContext.hasPath = NO; -} - -function CGContextDrawPath(aContext, aMode) -{ - if (!aContext.hasPath) - return; - - if (aMode === kCGPathFill || aMode === kCGPathFillStroke) - aContext.fill(); - else if (aMode === kCGPathStroke || aMode === kCGPathFillStroke || aMode === kCGPathEOFillStroke) - aContext.stroke(); - else if (aMode === kCGPathEOFill || aMode === kCGPathEOFillStroke) - CPLog.warn("Unimplemented fill mode in CGContextDrawPath: %d", aMode); - - aContext.hasPath = NO; -} - -function CGContextFillRect(aContext, aRect) -{ - _CGContextFillRectCanvas(aContext, aRect); - aContext.hasPath = NO; -} - -function CGContextFillRects(aContext, rects, count) -{ - if (count == null) - count = rects.length; - - for (var i = 0; i < count; ++i) - { - var rect = rects[i]; - _CGContextFillRectCanvas(aContext, rect); - } - - aContext.hasPath = NO; -} - -function CGContextStrokeRect(aContext, aRect) -{ - aContext.strokeRect(CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); - aContext.hasPath = NO; -} - -function CGContextClip(aContext) -{ - _CGContextClipCanvas(aContext); - aContext.hasPath = NO; -} - -function CGContextClipToRect(aContext, aRect) -{ - _CGContextBeginPathCanvas(aContext); - _CGContextAddRectCanvas(aContext, aRect); - _CGContextClosePathCanvas(aContext); - - _CGContextClipCanvas(aContext); - aContext.hasPath = NO; -} - -function CGContextClipToRects(aContext, rects, count) -{ - if (count == null) - count = rects.length; - - _CGContextBeginPathCanvas(aContext); - CGContextAddRects(aContext, rects, count); - _CGContextClipCanvas(aContext); - aContext.hasPath = NO; -} - -function CGContextSetAlpha(aContext, anAlpha) -{ - aContext.globalAlpha = anAlpha; -} - -function CGContextSetFillColor(aContext, aColor) -{ - var patternImage = [aColor patternImage]; - - if ([patternImage isSingleImage]) - { - var pattern = aContext.createPattern([patternImage image], "repeat"); - - aContext.fillStyle = pattern; - } - else - aContext.fillStyle = [aColor cssString]; -} - -/*! - Creates a context into which you can render a fill pattern - of the given size. Once the pattern is rendered, you can - set the fill or stroke pattern to the rendered pattern - with CGContextSetFillPattern or CGContextSetStrokePattern. -*/ -function CGContextCreatePatternContext(aContext, aSize) -{ - var pattern = document.createElement("canvas"); - - pattern.width = aSize.width; - pattern.height = aSize.height; - - return pattern.getContext("2d"); -} - -/*! - Sets the fill pattern for aContext to the rendered pattern context - returned by CGContextCreatePatternContext. -*/ -function CGContextSetFillPattern(aContext, aPatternContext) -{ - var pattern = aContext.createPattern(aPatternContext.canvas, "repeat"); - aContext.fillStyle = pattern; -} - -/*! - Sets the stroke pattern for aContext to the rendered pattern context - returned by CGContextCreatePatternContext. -*/ -function CGContextSetStrokePattern(aContext, aPatternContext) -{ - var pattern = aContext.createPattern(aPatternContext.canvas, "repeat"); - aContext.strokeStyle = pattern; -} - -function CGContextSetStrokeColor(aContext, aColor) -{ - var patternImage = [aColor patternImage]; - - if ([patternImage isSingleImage]) - { - var pattern = aContext.createPattern([patternImage image], "repeat"); - - aContext.strokeStyle = pattern; - } - else - aContext.strokeStyle = [aColor cssString]; -} - -function CGContextSetShadow(aContext, aSize, aBlur) -{ - aContext.shadowOffsetX = aSize.width; - aContext.shadowOffsetY = aSize.height; - aContext.shadowBlur = aBlur; - aContext.shadowColor = [[CPColor shadowColor] cssString]; -} - -function CGContextSetShadowWithColor(aContext, aSize, aBlur, aColor) -{ - aContext.shadowOffsetX = aSize.width; - aContext.shadowOffsetY = aSize.height; - aContext.shadowBlur = aBlur; - aContext.shadowColor = [aColor cssString]; -} - -function CGContextRotateCTM(aContext, anAngle) -{ - aContext.rotate(anAngle); -} - -function CGContextScaleCTM(aContext, sx, sy) -{ - aContext.scale(sx, sy); -} - -function CGContextTranslateCTM(aContext, tx, ty) -{ - aContext.translate(tx, ty); -} - -var scale_rotate = function(a, b, c, d) -{ - var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, - a2 = (ATAN2(b, d) + ATAN2(-sign * c, sign * a)) / 2.0, - cos = COS(a2), - sin = SIN(a2); - - if (cos === 0) - { - sx = -c / sin; - sy = b / sin; - } - else if (sin === 0) - { - sx = a / cos; - sy = d / cos; - } - else - { - abs_cos = ABS(cos); - abs_sin = ABS(sin); - - sx = (abs_cos * a / cos + abs_sin * -c / sin) / (abs_cos + abs_sin); - sy = (abs_cos * d / cos + abs_sin * b / sin) / (abs_cos + abs_sin); - } -}; - -var rotate_scale = function(a, b, c, d) -{ - var sign = (a * d < 0.0 || b * c > 0.0) ? -1.0 : 1.0, - a1 = (ATAN2(sign * b, sign * a) + ATAN2(-c, d)) / 2.0, - cos = COS(a1), - sin = SIN(a1); - - if (cos === 0) - { - sx = b / sin; - sy = -c / sin; - } - else if (sin === 0) - { - sx = a / cos; - sy = d / cos; - } - else - { - abs_cos = ABS(cos); - abs_sin = ABS(sin); - - sx = (abs_cos * a / cos + abs_sin * b / sin) / (abs_cos + abs_sin); - sy = (abs_cos * d / cos + abs_sin * -c / sin) / (abs_cos + abs_sin); - } -}; - -function eigen(anAffineTransform) -{ - CPLog.warn("Unimplemented function: eigen"); -} - - -if (CPFeatureIsCompatible(CPJavaScriptCanvasTransformFeature)) -{ - -CGContextConcatCTM = function(aContext, anAffineTransform) -{ - aContext.transform(anAffineTransform.a, anAffineTransform.b, anAffineTransform.c, anAffineTransform.d, anAffineTransform.tx, anAffineTransform.ty); -}; - -} -else -{ - -CGContextConcatCTM = function(aContext, anAffineTransform) -{ - var a = anAffineTransform.a, - b = anAffineTransform.b, - c = anAffineTransform.c, - d = anAffineTransform.d, - tx = anAffineTransform.tx, - ty = anAffineTransform.ty, - sx = 1.0, - sy = 1.0, - a1 = 0.0, - a2 = 0.0; - - // Detect the simple case of just scaling. - if (b === 0.0 && c === 0.0) - { - sx = a; - sy = d; - } - - // a scale followed by a rotate - else if (a * b === -c * d) - { - scale_rotate(a, b, c, d) - } - - // rotate, then scale. - else if (a * c === -b * d) - { - rotate_scale(a, b, c, d) - } - else - { - var transpose = CGAffineTransformMake(a, c, b, d, 0.0, 0.0), // inline - u = eigen(CGAffineTransformConcat(anAffineTransform, transpose)), - v = eigen(CGAffineTransformConcat(transpose, anAffineTransform)), - U = CGAffineTransformMake(u.vector_1.x, u.vector_2.x, u.vector_1.y, u.vector_2.y, 0.0, 0.0), // inline - VT = CGAffineTransformMake(v.vector_1.x, v.vector_1.y, v.vector_2.x, v.vector_2.y, 0.0, 0.0), - S = CGAffineTransformConcat(CGAffineTransformConcat(CGAffineTransformInvert(U), anAffineTransform), CGAffineTransformInvert(VT)); - - a = VT.a; - b = VT.b; - c = VT.c; - d = VT.d; - scale_rotate(a, b, c, d); - S.a *= sx; - S.d *= sy; - a = U.a; - b = U.b; - c = U.c; - d = U.d; - rotate_scale(a, b, c, d); - sx = S.a * sx; - sy = S.d * sy; - } - - if (tx != 0 || ty != 0) - CGContextTranslateCTM(aContext, tx, ty); - if (a1 != 0.0) - CGContextRotateCTM(aContext, a1); - if (sx != 1.0 || sy != 1.0) - CGContextScaleCTM(aContext, sx, sy); - if (a2 != 0.0) - CGContextRotateCTM(aContext, a2); -}; - -} - -function CGContextDrawImage(aContext, aRect, anImage) -{ - aContext.drawImage(anImage._image, CGRectGetMinX(aRect), CGRectGetMinY(aRect), CGRectGetWidth(aRect), CGRectGetHeight(aRect)); - aContext.hasPath = NO; -} - -function to_string(aColor) -{ - return "rgba(" + ROUND(aColor.components[0] * 255) + ", " + ROUND(aColor.components[1] * 255) + ", " + ROUND(255 * aColor.components[2]) + ", " + aColor.components[3] + ")"; -} - -function CGContextDrawLinearGradient(aContext, aGradient, aStartPoint, anEndPoint, options) -{ - var colors = aGradient.colors, - count = colors.length, - linearGradient = aContext.createLinearGradient(aStartPoint.x, aStartPoint.y, anEndPoint.x, anEndPoint.y); - - while (count--) - linearGradient.addColorStop(aGradient.locations[count], to_string(colors[count])); - - aContext.fillStyle = linearGradient; - aContext.fill(); - aContext.hasPath = NO; -} - -function CGContextDrawRadialGradient(aContext, aGradient, aStartCenter, aStartRadius, anEndCenter, anEndRadius, options) -{ - var colors = aGradient.colors, - count = colors.length, - linearGradient = aContext.createRadialGradient(aStartCenter.x, aStartCenter.y, aStartRadius, anEndCenter.x, anEndCenter.y, anEndRadius); - - while (count--) - linearGradient.addColorStop(aGradient.locations[count], to_string(colors[count])); - - aContext.fillStyle = linearGradient; - aContext.fill(); - aContext.hasPath = NO; -} - -function CGBitmapGraphicsContextCreate() -{ - var DOMElement = document.createElement("canvas"), - context = DOMElement.getContext("2d"); - - context.DOMElement = DOMElement; - - // canvas gives us no way to query whether the path is empty or not, so we have to track it ourselves - context.hasPath = NO; - - return context; -} diff --git a/AppKit/CoreGraphics/CGContextText.j b/AppKit/CoreGraphics/CGContextText.j index 92cf30c9d..1aa5b8508 100644 --- a/AppKit/CoreGraphics/CGContextText.j +++ b/AppKit/CoreGraphics/CGContextText.j @@ -37,7 +37,7 @@ function CGContextSetTextMatrix(/* CGContext */ aContext, /* CGAffineTransform * function CGContextGetTextPosition(/* CGContext */ aContext) { - return aContext._textPosition || _CGPointMakeZero(); + return aContext._textPosition || CGPointMakeZero(); } function CGContextSetTextPosition(/* CGContext */ aContext, /* float */ x, /* float */ y) diff --git a/AppKit/CoreGraphics/CGContextVML.j b/AppKit/CoreGraphics/CGContextVML.j deleted file mode 100644 index 8bb35f649..000000000 --- a/AppKit/CoreGraphics/CGContextVML.j +++ /dev/null @@ -1,335 +0,0 @@ -/* - * CGContextVML.j - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2008, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -var VML_TRUTH_TABLE = [ "f", "t"], - VML_LINECAP_TABLE = [ "flat", "round", "square" ], - VML_LINEJOIN_TABLE = [ "miter", "round", "bevel" ], - VML_ELEMENT_TABLE = [ " m ", " l ", "qb", " c ", " x ", [" at ", " wa "]]; - -var _CGBitmapGraphicsContextCreate = CGBitmapGraphicsContextCreate; - -function CGBitmapGraphicsContextCreate() -{ - // The first time around, we have to set up our environment to support vml. - document.namespaces.add("cg_vml_", "urn:schemas-microsoft-com:vml"); - document.createStyleSheet().cssText = "cg_vml_\\:*{behavior:url(#default#VML)}"; - - CGBitmapGraphicsContextCreate = _CGBitmapGraphicsContextCreate; - - return _CGBitmapGraphicsContextCreate(); -} - -function CGContextSetFillColor(aContext, aColor) -{ - if ([aColor patternImage]) - // Prefix a marker character to the string so we know it's a pattern image filename - aContext.gState.fillStyle = "!" + [[aColor patternImage] filename]; - else - aContext.gState.fillStyle = [aColor cssString]; -} - -// FIXME: aRect is ignored. -function CGContextClearRect(aContext, aRect) -{ - if (aContext.buffer != nil) - aContext.buffer = ""; - else - aContext.DOMElement.innerHTML = ""; - - aContext.path = NULL; -} - -var W = 10.0, - H = 10.0, - Z = 10.0, - Z_2 = Z / 2.0; - -#define COORD(aCoordinate) (aCoordinate === 0.0 ? 0 : ROUND(Z * (aCoordinate) - Z_2)) - -function CGContextDrawImage(aContext, aRect, anImage) -{ - var string = ""; - - if (anImage.buffer != nil) - string = anImage.buffer; - else - { - var ctm = aContext.gState.CTM, - origin = CGPointApplyAffineTransform(aRect.origin, ctm), - similarity = ctm.a == ctm.d && ctm.b == -ctm.c, - vml = [""); - - string = vml.join(""); - } - - if (aContext.buffer != nil) - aContext.buffer += string; - else - aContext.DOMElement.insertAdjacentHTML("BeforeEnd", string); -} - -function CGContextDrawPath(aContext, aMode) -{ - if (!aContext || CGPathIsEmpty(aContext.path)) - return; - - var elements = aContext.path.elements, - - i = 0, - count = aContext.path.count, - - gState = aContext.gState, - fill = (aMode == kCGPathFill || aMode == kCGPathFillStroke) ? 1 : 0, - stroke = (aMode == kCGPathStroke || aMode == kCGPathFillStroke) ? 1 : 0, - opacity = gState.alpha, - vml = ["= x) - { - if (start.y < y) - start.x += 0.125; - else - start.y += 0.125; - } - else - { - if (end.y <= y) - end.x += 0.125; - else - end.y += 0.125; - } - - vml.push(VML_ELEMENT_TABLE[type][clockwise], - COORD(x - radius), ',', COORD(y - radius), " ", - COORD(x + radius), ',', COORD(y + radius), " ", - COORD(start.x), ',', COORD(start.y), " ", - COORD(end.x), ',', COORD(end.y)); - break; - case kCGPathElementAddArcToPoint: break; - } - - // TODO: Following is broken for curves due to - // move to proper paths. - - // Figure out dimensions so we can do gradient fills - // properly - /*if(c) { - if (min.x == null || c.x < min.x) { - min.x = c.x; - } - if (max.x == null || c.x > max.x) { - max.x = c.x; - } - if (min.y == null || c.y < min.y) { - min.y = c.y; - } - if (max.y == null || c.y > max.y) { - max.y = c.y; - } - }*/ - } - - vml.push("\">"); - - if (gState.gradient) - vml.push(gState.gradient); - - else if (fill) - { - if (gState.fillStyle.charAt(0) === "!") - vml.push(""); - else // should be a CSS color spec - vml.push(""); - } - - if (stroke) - vml.push( ""); - - var shadowColor = gState.shadowColor; - //\"", [shadowColor cssString], "\" - if (shadowColor) - { - var shadowOffset = gState.shadowOffset; - - vml.push(""); - } - - vml.push(""); - - if (aContext.buffer != nil) - aContext.buffer += vml.join(""); - else - aContext.DOMElement.insertAdjacentHTML("BeforeEnd", vml.join("")); -} - -function to_string(aColor) -{ - return "rgb(" + ROUND(aColor.components[0] * 255) + ", " + ROUND(aColor.components[1] * 255) + ", " + ROUND(255 * aColor.components[2]) + ")"; -} - -function CGContextDrawLinearGradient(aContext, aGradient, aStartPoint, anEndPoint, options) -{ - if (!aContext || !aGradient) - return; - - var vml = nil; - - if (aGradient.vml_gradient) - { - var stops = [[aGradient.vml_gradient stops] sortedArrayUsingSelector:@selector(comparePosition:)], - count = [stops count]; - - vml = [""); - } - else - { - var colors = aGradient.colors, - count = colors.length; - - vml = [""); - } - - aContext.gState.gradient = vml.join(""); - - // if (aContext.buffer != nil) - // aContext.buffer += vml.join(""); - // else - // aContext.DOMElement.innerHTML = vml.join(""); -} diff --git a/AppKit/Jakefile b/AppKit/Jakefile index 75949255c..ba015344a 100644 --- a/AppKit/Jakefile +++ b/AppKit/Jakefile @@ -9,10 +9,7 @@ const utilsFile = ObjectiveJ.utils.file; $BUILD_PATH = path.join($BUILD_DIR, $CONFIGURATION, 'AppKit'); -AppKitFiles = new FileList("**/*.j").exclude('CoreGraphics/CGContextCanvas.j', 'CoreGraphics/CGContextVML.j', 'Themes/**/*', 'Tools/**/*', "Platform/DOM/CPPlatform.j", "Platform/DOM/CPPlatformString.j"); - -FIXME_fileDependency (path.join("Platform", "CPPlatform.j"), path.join("Platform", "DOM", "CPPlatform.j")); -FIXME_fileDependency (path.join("Platform", "CPPlatformString.j"), path.join("Platform", "DOM", "CPPlatformString.j")); +AppKitFiles = new FileList("**/*.j").exclude('CoreGraphics/CGContextCanvas.j', 'CoreGraphics/CGContextVML.j', 'Themes/**/*', 'Tools/**/*'); appKitTask = framework ("AppKit", function(appKitTask) { diff --git a/AppKit/Platform/CPPlatform.j b/AppKit/Platform/CPPlatform.j index 432875dcd..47068bc80 100644 --- a/AppKit/Platform/CPPlatform.j +++ b/AppKit/Platform/CPPlatform.j @@ -70,11 +70,169 @@ @end -#if PLATFORM(DOM) -#include "DOM/CPPlatform.j" -#else +// --- begin AppKit/Platform/DOM/CPPlatform.j (inlined; file removed to resolve capp-build basename clash) --- +@global CPApp +@global document + +if (typeof window["CPPlatformEnableHTMLDragAndDrop"] === "undefined") + CPPlatformEnableHTMLDragAndDrop = NO; + +CPPlatformDidClearBodyElementNotification = @"CPPlatformDidClearBodyElementNotification"; +CPPlatformWillClearBodyElementNotification = @"CPPlatformWillClearBodyElementNotification"; + +var screenNeedsInitialization = NO, + mainBodyElement = nil, + elementRemovalTest = new RegExp("\\bcpdontremove\\b", "g"); + @implementation CPPlatform : CPBasePlatform { } + ++ (void)initialize +{ + if (self !== [CPPlatform class]) + return; + + screenNeedsInitialization = [CPPlatform isBrowser]; + + // We do this here because doing it later breaks IE. + if (typeof document !== 'undefined' && document.documentElement) + document.documentElement.style.overflow = "hidden"; + + if ([CPPlatform isBrowser]) + { + // This differ from cocoa, where shouldTerminate is called in the method terminate of CPApp + // Cappuccino acts like this because we can not close a window openend by the user with a script (so not possible in terminate), and we can only prevent the action in the method onbeforeunload in js. + window.onbeforeunload = function() + { + if ([CPApp _sendDelegateApplicationShouldTerminate] != CPTerminateNow) + return [CPApp _sendDelegateApplicationShouldTerminateMessage]; + }; + + var terminateApplication = function() + { + [self closeAllPlatformWindows]; + [CPApp terminate:nil]; + }; + + // "unload" is blocked by the permissions policy in current Chrome and prevents the + // back/forward cache. "pagehide" fires reliably instead. If event.persisted is true the + // page goes into the bfcache and may be restored, so the application must not terminate. + if ("onpagehide" in window) + { + window.addEventListener("pagehide", function(anEvent) + { + if (!anEvent.persisted) + terminateApplication(); + }, NO); + } + else + window.onunload = terminateApplication; + } +} + ++ (BOOL)isBrowser +{ + if (!CPDOMAvailable) + return NO; + + return typeof window.cpIsDesktop === "undefined"; +} + ++ (BOOL)supportsDragAndDrop +{ + if (!CPDOMAvailable) + return NO; + + return CPFeatureIsCompatible(CPHTMLDragAndDropFeature) && (CPPlatformEnableHTMLDragAndDrop || ![self isBrowser]); +} + ++ (BOOL)supportsNativeMainMenu +{ + return (typeof window["cpSetMainMenu"] === "function"); +} + ++ (void)terminateApplication +{ + if (typeof window["cpTerminate"] === "function") + window.cpTerminate(); +} + ++ (void)activateIgnoringOtherApps:(BOOL)shouldIgnoreOtherApps +{ + if (typeof window["cpActivateIgnoringOtherApps"] === "function") + window.cpActivateIgnoringOtherApps(!!shouldIgnoreOtherApps); +} + ++ (void)deactivate +{ + if (typeof window["cpDeactivate"] === "function") + window.cpDeactivate(); +} + ++ (void)hideOtherApplications:(id)aSender +{ + if (typeof window["cpHideOtherApplications"] === "function") + window.cpHideOtherApplications(); +} + ++ (void)hide:(id)aSender +{ + if (typeof window["cpHide"] === "function") + window.cpHide(); +} + ++ (DOMElement)mainBodyElement +{ + if (!mainBodyElement) + mainBodyElement = document.getElementById("cappuccino-body") || document.body; + + return mainBodyElement; +} + ++ (void)initializeScreenIfNecessary +{ + if (!screenNeedsInitialization) + return; + + screenNeedsInitialization = NO; + + [[CPNotificationCenter defaultCenter] + postNotificationName:CPPlatformWillClearBodyElementNotification + object:self]; + + var bodyElement = [self mainBodyElement]; + + // Get rid of any of the original contents of the page. + var children = bodyElement.childNodes, + length = children.length; + + while (length--) + { + var element = children[length]; + if (!element.className || element.className.match(elementRemovalTest) === null) + bodyElement.removeChild(element); + } + + bodyElement.style.overflow = "hidden"; + + [[CPNotificationCenter defaultCenter] + postNotificationName:CPPlatformDidClearBodyElementNotification + object:self]; +} + ++ (void)closeAllPlatformWindows +{ + var platformWindows = [CPPlatformWindow visiblePlatformWindows], + primaryPlatformWindow = [CPPlatformWindow primaryPlatformWindow], + platformWindowEnumerator = [platformWindows objectEnumerator], + platformWindow = nil; + + while ((platformWindow = [platformWindowEnumerator nextObject]) != nil) + { + if (platformWindow != primaryPlatformWindow) + [platformWindow orderOut:self]; + } +} + @end -#endif diff --git a/AppKit/Platform/CPPlatformString.j b/AppKit/Platform/CPPlatformString.j index b0a930c6d..71e330e27 100644 --- a/AppKit/Platform/CPPlatformString.j +++ b/AppKit/Platform/CPPlatformString.j @@ -38,24 +38,203 @@ @end -#if PLATFORM(DOM) -#include "DOM/CPPlatformString.j" -#else +// --- begin AppKit/Platform/DOM/CPPlatformString.j (inlined; file removed to resolve capp-build basename clash) --- +@global document + +var DOMFixedWidthSpanElement = nil, + DOMFlexibleWidthSpanElement = nil, + DOMMetricsDivElement = nil, + DOMMetricsImgElement = nil, + DefaultFont = nil; + @implementation CPPlatformString : CPBasePlatformString +{ +} + ++ (void)initialize +{ + if (self !== [CPPlatformString class]) + return; + + DefaultFont = [CPFont systemFontOfSize:CPFontCurrentSystemSize]; +} + ++ (void)bootstrap +{ + [self createDOMElements]; +} + ++ (void)createDOMElements +{ + if (!CPDOMAvailable) return; + + var style, + bodyElement = [CPPlatform mainBodyElement]; + + DOMFlexibleWidthSpanElement = document.createElement("span"); + DOMFlexibleWidthSpanElement.className = "cpdontremove"; + style = DOMFlexibleWidthSpanElement.style; + style.position = "absolute"; + style.left = "-100000px"; + style.zIndex = -10000; + style.visibility = "visible"; + style.padding = "0px"; + style.margin = "0px"; + style.whiteSpace = "pre"; + + DOMFixedWidthSpanElement = document.createElement("span"); + DOMFixedWidthSpanElement.className = "cpdontremove"; + style = DOMFixedWidthSpanElement.style; + style.display = "block"; + style.position = "absolute"; + style.left = "-100000px"; + style.zIndex = -10000; + style.visibility = "visible"; + style.padding = "0px"; + style.margin = "0px"; + style.width = "1px"; + style.wordWrap = "break-word"; + + try + { + style.whiteSpace = "pre"; + style.whiteSpace = "-o-pre-wrap"; + style.whiteSpace = "-pre-wrap"; + style.whiteSpace = "-moz-pre-wrap"; + style.whiteSpace = "pre-wrap"; + } + catch(e) + { + //some versions of IE throw exceptions for unsupported properties. + style.whiteSpace = "pre"; + } + + bodyElement.appendChild(DOMFlexibleWidthSpanElement); + bodyElement.appendChild(DOMFixedWidthSpanElement); +} + ++ (void)createDOMMetricsElements +{ + if (!CPDOMAvailable) return; + + var style, + bodyElement = [CPPlatform mainBodyElement]; + + DOMMetricsDivElement = document.createElement("div"); + DOMMetricsDivElement.className = "cpdontremove"; + style = DOMMetricsDivElement.style; + style.position = "absolute"; + style.left = "-100000px"; + style.zIndex = -10000; + style.width = "100000px"; + style.whiteSpace = "nowrap"; + style.lineHeight = "1em"; + style.padding = "0px"; + style.margin = "0px"; + DOMMetricsDivElement.innerHTML = "x"; + + bodyElement.appendChild(DOMMetricsDivElement); + + var imgPath = [[CPBundle bundleForClass:[CPView class]] pathForResource:@"empty.png"]; + + DOMMetricsImgElement = document.createElement("img"); + DOMMetricsImgElement.className = "cpdontremove"; + DOMMetricsImgElement.setAttribute("src", imgPath); + DOMMetricsImgElement.setAttribute("width", "1"); + DOMMetricsImgElement.setAttribute("height", "1"); + DOMMetricsImgElement.setAttribute("alt", ""); + style = DOMMetricsImgElement.style; + style.zIndex = -10000; + style.visibility = "visible"; + style.padding = "0px"; + style.margin = "0px"; + style.border = "none"; + style.verticalAlign = "baseline"; + + DOMMetricsDivElement.appendChild(DOMMetricsImgElement); +} + ++ (int)charPositionOfString:(CPString)aString withFont:(CPFont)aFont forPoint:(CGPoint)aPoint +{ + if (!CPDOMAvailable) return 0; + + if (!aString) + return 0; + + var position = 0, + stringLength = aString.length, + currentString = ""; + + for (var i = 0; i < stringLength; i++) + { + var lastChar = aString[i]; + + currentString += lastChar; + + var sizeOfString = [self sizeOfString:currentString withFont:aFont forWidth:nil].width, + sizeLastChar = [self sizeOfString:lastChar withFont:aFont forWidth:nil].width; + + if (sizeOfString - sizeLastChar / 2 < aPoint.x) + position++; + else + break; + } + + return position; +} + (CGSize)sizeOfString:(CPString)aString withFont:(CPFont)aFont forWidth:(float)aWidth { - return CGSizeMakeZero(); + if (!CPDOMAvailable) return CGSizeMakeZero(); + + if (!DOMFixedWidthSpanElement) + [self createDOMElements]; + + var span; + + if (!aWidth) + span = DOMFlexibleWidthSpanElement; + else + { + span = DOMFixedWidthSpanElement; + span.style.width = ROUND(aWidth) + "px"; + } + + var effectiveFontCSSString = [(aFont || DefaultFont) cssString]; + if (span.style.font !== effectiveFontCSSString) + span.style.font = effectiveFontCSSString; + + if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature)) + span.innerText = aString; + else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature)) + span.textContent = aString; + + return CGSizeMake(span.clientWidth, span.clientHeight); } + (CPDictionary)metricsOfFont:(CPFont)aFont { - return @{ + if (!CPDOMAvailable) + return @{ @"ascender": 0, @"descender": 0, @"lineHeight": 0, }; + + if (!DOMMetricsDivElement) + [self createDOMMetricsElements]; + + DOMMetricsDivElement.style.font = [(aFont || DefaultFont) cssString]; + + var lineHeight = DOMMetricsDivElement.offsetHeight, + baseline = DOMMetricsImgElement.offsetTop + DOMMetricsImgElement.offsetHeight, + descender = baseline - lineHeight; + + return @{ + @"ascender": baseline, + @"descender": descender, + @"lineHeight": lineHeight, + }; } @end -#endif diff --git a/AppKit/Platform/CPPlatformWindow.j b/AppKit/Platform/CPPlatformWindow.j index 2022138f0..8ad62d116 100644 --- a/AppKit/Platform/CPPlatformWindow.j +++ b/AppKit/Platform/CPPlatformWindow.j @@ -24,6 +24,7 @@ @import @import "CPKeyBinding.j" @import "CPPlatform.j" +@import "CPCompatibility.j" @class CPMenu @class CPPlatformPasteboard @@ -46,7 +47,6 @@ var PrimaryPlatformWindow = NULL; BOOL _shouldUpdateContentRect; BOOL _hasInitializeInstanceWithWindow; -#if PLATFORM(DOM) DOMWindow _DOMWindow; DOMElement _DOMBodyElement; @@ -90,8 +90,6 @@ var PrimaryPlatformWindow = NULL; float _lastTouchMoveTimestamp; float _lastMomentumTimestamp; BOOL _isTwoFingerScrolling; - -#endif } + (CPSet)visiblePlatformWindows @@ -101,11 +99,10 @@ var PrimaryPlatformWindow = NULL; + (BOOL)supportsMultipleInstances { -#if PLATFORM(DOM) - return !CPBrowserIsEngine(CPInternetExplorerBrowserEngine); -#else + if (CPDOMAvailable) + return !CPBrowserIsEngine(CPInternetExplorerBrowserEngine); + return NO; -#endif } + (CPPlatformWindow)primaryPlatformWindow @@ -126,14 +123,15 @@ var PrimaryPlatformWindow = NULL; { _contentRect = CGRectMakeCopy(aRect); -#if PLATFORM(DOM) - _windowLevels = []; - _windowLayers = @{}; + if (CPDOMAvailable) + { + _windowLevels = []; + _windowLayers = @{}; - _charCodes = {}; + _charCodes = {}; - _platformPasteboard = [CPPlatformPasteboard new]; -#endif + _platformPasteboard = [CPPlatformPasteboard new]; + } } return self; @@ -198,9 +196,10 @@ var PrimaryPlatformWindow = NULL; _contentRect = CGRectMakeCopy(aRect); -#if PLATFORM(DOM) - [self updateNativeContentRect]; -#endif + if (CPDOMAvailable) + { + [self updateNativeContentRect]; + } } - (void)updateFromNativeContentRect @@ -224,11 +223,10 @@ var PrimaryPlatformWindow = NULL; - (BOOL)isVisible { -#if PLATFORM(DOM) - return _DOMWindow !== NULL && _DOMWindow !== undefined; -#else + if (CPDOMAvailable) + return _DOMWindow !== NULL && _DOMWindow !== undefined; + return NO; -#endif } - (void)deminiaturize:(id)sender @@ -241,7 +239,8 @@ var PrimaryPlatformWindow = NULL; - (void)moveWindow:(CPWindow)aWindow fromLevel:(int)fromLevel toLevel:(int)toLevel { -#if PLATFORM(DOM) + if (!CPDOMAvailable) return; + if (!aWindow._isVisible) return; @@ -250,7 +249,6 @@ var PrimaryPlatformWindow = NULL; [fromLayer removeWindow:aWindow]; [toLayer insertWindow:aWindow atIndex:CPNotFound]; -#endif } - (void)setLevel:(CPInteger)aLevel @@ -277,14 +275,10 @@ var PrimaryPlatformWindow = NULL; { _title = aTitle; -#if PLATFORM(DOM) - if (_DOMWindow && - _DOMWindow.document && - ([aWindow isFullPlatformWindow])) + if (CPDOMAvailable && _DOMWindow && _DOMWindow.document && ([aWindow isFullPlatformWindow])) { _DOMWindow.document.title = _title; } -#endif } - (CPString)title @@ -310,6 +304,4 @@ var PrimaryPlatformWindow = NULL; @end -#if PLATFORM(BROWSER) //@import "CPPlatformWindow+DOM.j" -#endif diff --git a/AppKit/Platform/DOM/CPDOMDisplayServer.h b/AppKit/Platform/DOM/CPDOMDisplayServer.h deleted file mode 100644 index b99c8ac02..000000000 --- a/AppKit/Platform/DOM/CPDOMDisplayServer.h +++ /dev/null @@ -1,68 +0,0 @@ -/* - * CPDOMDisplayServer.h - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2008, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#define CPDOMDisplayServerSetStyleLeftTop(aDOMElement, aTransform, aLeft, aTop) \ -if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aLeft, aTop), aTransform); \ -else var ____p = CGPointMake(aLeft, aTop); \ -aDOMElement.style.left = ROUND(____p.x) + "px";\ -aDOMElement.style.top = ROUND(____p.y) + "px"; - -#define CPDOMDisplayServerSetStyleRightTop(aDOMElement, aTransform, aRight, aTop) \ -if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aRight, aTop), aTransform); \ -else var ____p = CGPointMake(aRight, aTop); \ -aDOMElement.style.right = ROUND(____p.x) + "px";\ -aDOMElement.style.top = ROUND(____p.y) + "px"; - -#define CPDOMDisplayServerSetStyleLeftBottom(aDOMElement, aTransform, aLeft, aBottom) \ -if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aLeft, aBottom), aTransform); \ -else var ____p = CGPointMake(aLeft, aBottom); \ -aDOMElement.style.left = ROUND(____p.x) + "px";\ -aDOMElement.style.bottom = ROUND(____p.y) + "px"; - -#define CPDOMDisplayServerSetStyleRightBottom(aDOMElement, aTransform, aRight, aBottom) \ -if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aRight, aBottom), aTransform); \ -else var ____p = CGPointMake(aRight, aBottom); \ -aDOMElement.style.right = ROUND(____p.x) + "px";\ -aDOMElement.style.bottom = ROUND(____p.y) + "px"; - -#define CPDOMDisplayServerSetStyleSize(aDOMElement, aWidth, aHeight) \ - aDOMElement.style.width = MAX(0.0, ROUND(aWidth)) + "px";\ - aDOMElement.style.height = MAX(0.0, ROUND(aHeight)) + "px"; - -#define CPDOMDisplayServerSetSize(aDOMElement, aWidth, aHeight) \ - aDOMElement.width = MAX(0.0, ROUND(aWidth));\ - aDOMElement.height = MAX(0.0, ROUND(aHeight)); - -#define CPDOMDisplayServerSetStyleBackgroundSize(aDOMElement, aWidth, aHeight)\ - aDOMElement.style.backgroundSize = aWidth + ' ' + aHeight; - -#define CPDOMDisplayServerAppendChild(aParentElement, aChildElement) \ - aParentElement.appendChild(aChildElement); - -#define CPDOMDisplayServerInsertBefore(aParentElement, aChildElement, aBeforeElement) \ - aParentElement.insertBefore(aChildElement, aBeforeElement); - -#define CPDOMDisplayServerRemoveChild(aParentElement, aChildElement) \ - aParentElement.removeChild(aChildElement); - -#define PREPARE_DOM_OPTIMIZATION() -#define EXECUTE_DOM_INSTRUCTIONS() diff --git a/AppKit/Platform/DOM/CPDOMDisplayServer.j b/AppKit/Platform/DOM/CPDOMDisplayServer.j new file mode 100644 index 000000000..71f98ba74 --- /dev/null +++ b/AppKit/Platform/DOM/CPDOMDisplayServer.j @@ -0,0 +1,96 @@ +/* + * CPDOMDisplayServer.j + * AppKit + * + * Created by Francisco Tolmasky. + * Copyright 2008, 280 North, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/* Formerly a force-included header of function-like macros; promoted to a + real source file for the preprocessor-free build. */ + +function CPDOMDisplayServerSetStyleLeftTop(aDOMElement, aTransform, aLeft, aTop) +{ + if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aLeft, aTop), aTransform); + else var ____p = CGPointMake(aLeft, aTop); + aDOMElement.style.left = ROUND(____p.x) + "px"; + aDOMElement.style.top = ROUND(____p.y) + "px"; +} + +function CPDOMDisplayServerSetStyleRightTop(aDOMElement, aTransform, aRight, aTop) +{ + if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aRight, aTop), aTransform); + else var ____p = CGPointMake(aRight, aTop); + aDOMElement.style.right = ROUND(____p.x) + "px"; + aDOMElement.style.top = ROUND(____p.y) + "px"; +} + +function CPDOMDisplayServerSetStyleLeftBottom(aDOMElement, aTransform, aLeft, aBottom) +{ + if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aLeft, aBottom), aTransform); + else var ____p = CGPointMake(aLeft, aBottom); + aDOMElement.style.left = ROUND(____p.x) + "px"; + aDOMElement.style.bottom = ROUND(____p.y) + "px"; +} + +function CPDOMDisplayServerSetStyleRightBottom(aDOMElement, aTransform, aRight, aBottom) +{ + if (aTransform) var ____p = CGPointApplyAffineTransform(CGPointMake(aRight, aBottom), aTransform); + else var ____p = CGPointMake(aRight, aBottom); + aDOMElement.style.right = ROUND(____p.x) + "px"; + aDOMElement.style.bottom = ROUND(____p.y) + "px"; +} + +function CPDOMDisplayServerSetStyleSize(aDOMElement, aWidth, aHeight) +{ + aDOMElement.style.width = MAX(0.0, ROUND(aWidth)) + "px"; + aDOMElement.style.height = MAX(0.0, ROUND(aHeight)) + "px"; +} + +function CPDOMDisplayServerSetSize(aDOMElement, aWidth, aHeight) +{ + aDOMElement.width = MAX(0.0, ROUND(aWidth)); + aDOMElement.height = MAX(0.0, ROUND(aHeight)); +} + +function CPDOMDisplayServerSetStyleBackgroundSize(aDOMElement, aWidth, aHeight) +{ + aDOMElement.style.backgroundSize = aWidth + ' ' + aHeight; +} + +function CPDOMDisplayServerAppendChild(aParentElement, aChildElement) +{ + aParentElement.appendChild(aChildElement); +} + +function CPDOMDisplayServerInsertBefore(aParentElement, aChildElement, aBeforeElement) +{ + aParentElement.insertBefore(aChildElement, aBeforeElement); +} + +function CPDOMDisplayServerRemoveChild(aParentElement, aChildElement) +{ + aParentElement.removeChild(aChildElement); +} + +function PREPARE_DOM_OPTIMIZATION() +{ +} + +function EXECUTE_DOM_INSTRUCTIONS() +{ +} diff --git a/AppKit/Platform/DOM/CPPlatform.j b/AppKit/Platform/DOM/CPPlatform.j deleted file mode 100644 index 39c2b8a41..000000000 --- a/AppKit/Platform/DOM/CPPlatform.j +++ /dev/null @@ -1,180 +0,0 @@ -/* - * CPPlatform+DOM.j - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2010, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -@global CPApp - -if (typeof window["CPPlatformEnableHTMLDragAndDrop"] === "undefined") - CPPlatformEnableHTMLDragAndDrop = NO; - -CPPlatformDidClearBodyElementNotification = @"CPPlatformDidClearBodyElementNotification"; -CPPlatformWillClearBodyElementNotification = @"CPPlatformWillClearBodyElementNotification"; - -var screenNeedsInitialization = NO, - mainBodyElement = nil, - elementRemovalTest = new RegExp("\\bcpdontremove\\b", "g"); - -@implementation CPPlatform : CPBasePlatform -{ -} - -+ (void)initialize -{ - if (self !== [CPPlatform class]) - return; - - screenNeedsInitialization = [CPPlatform isBrowser]; - - // We do this here because doing it later breaks IE. - if (typeof document !== 'undefined' && document.documentElement) - document.documentElement.style.overflow = "hidden"; - - if ([CPPlatform isBrowser]) - { - // This differ from cocoa, where shouldTerminate is called in the method terminate of CPApp - // Cappuccino acts like this because we can not close a window openend by the user with a script (so not possible in terminate), and we can only prevent the action in the method onbeforeunload in js. - window.onbeforeunload = function() - { - if ([CPApp _sendDelegateApplicationShouldTerminate] != CPTerminateNow) - return [CPApp _sendDelegateApplicationShouldTerminateMessage]; - }; - - var terminateApplication = function() - { - [self closeAllPlatformWindows]; - [CPApp terminate:nil]; - }; - - // "unload" is blocked by the permissions policy in current Chrome and prevents the - // back/forward cache. "pagehide" fires reliably instead. If event.persisted is true the - // page goes into the bfcache and may be restored, so the application must not terminate. - if ("onpagehide" in window) - { - window.addEventListener("pagehide", function(anEvent) - { - if (!anEvent.persisted) - terminateApplication(); - }, NO); - } - else - window.onunload = terminateApplication; - } -} - -+ (BOOL)isBrowser -{ - return typeof window.cpIsDesktop === "undefined"; -} - -+ (BOOL)supportsDragAndDrop -{ - return CPFeatureIsCompatible(CPHTMLDragAndDropFeature) && (CPPlatformEnableHTMLDragAndDrop || ![self isBrowser]); -} - -+ (BOOL)supportsNativeMainMenu -{ - return (typeof window["cpSetMainMenu"] === "function"); -} - -+ (void)terminateApplication -{ - if (typeof window["cpTerminate"] === "function") - window.cpTerminate(); -} - -+ (void)activateIgnoringOtherApps:(BOOL)shouldIgnoreOtherApps -{ - if (typeof window["cpActivateIgnoringOtherApps"] === "function") - window.cpActivateIgnoringOtherApps(!!shouldIgnoreOtherApps); -} - -+ (void)deactivate -{ - if (typeof window["cpDeactivate"] === "function") - window.cpDeactivate(); -} - -+ (void)hideOtherApplications:(id)aSender -{ - if (typeof window["cpHideOtherApplications"] === "function") - window.cpHideOtherApplications(); -} - -+ (void)hide:(id)aSender -{ - if (typeof window["cpHide"] === "function") - window.cpHide(); -} - -+ (DOMElement)mainBodyElement -{ - if (!mainBodyElement) - mainBodyElement = document.getElementById("cappuccino-body") || document.body; - - return mainBodyElement; -} - -+ (void)initializeScreenIfNecessary -{ - if (!screenNeedsInitialization) - return; - - screenNeedsInitialization = NO; - - [[CPNotificationCenter defaultCenter] - postNotificationName:CPPlatformWillClearBodyElementNotification - object:self]; - - var bodyElement = [self mainBodyElement]; - - // Get rid of any of the original contents of the page. - var children = bodyElement.childNodes, - length = children.length; - - while (length--) - { - var element = children[length]; - if (!element.className || element.className.match(elementRemovalTest) === null) - bodyElement.removeChild(element); - } - - bodyElement.style.overflow = "hidden"; - - [[CPNotificationCenter defaultCenter] - postNotificationName:CPPlatformDidClearBodyElementNotification - object:self]; -} - -+ (void)closeAllPlatformWindows -{ - var platformWindows = [CPPlatformWindow visiblePlatformWindows], - primaryPlatformWindow = [CPPlatformWindow primaryPlatformWindow], - platformWindowEnumerator = [platformWindows objectEnumerator], - platformWindow = nil; - - while ((platformWindow = [platformWindowEnumerator nextObject]) != nil) - { - if (platformWindow != primaryPlatformWindow) - [platformWindow orderOut:self]; - } -} - -@end diff --git a/AppKit/Platform/DOM/CPPlatformPasteboard.j b/AppKit/Platform/DOM/CPPlatformPasteboard.j index f65d82d62..b9d674c0b 100644 --- a/AppKit/Platform/DOM/CPPlatformPasteboard.j +++ b/AppKit/Platform/DOM/CPPlatformPasteboard.j @@ -35,11 +35,6 @@ // From CPPlatformWindow+DOM.j @global _CPDOMEventStop -#if PLATFORM(DOM) - -#define SUPPRESS_CAPPUCCINO_CUT_FOR_EVENT(anEvent) anEvent._suppressCappuccinoCut = YES -#define SUPPRESS_CAPPUCCINO_PASTE_FOR_EVENT(anEvent) anEvent._suppressCappuccinoPaste = YES - var hasEditableTarget = function(aDOMEvent) { var target = aDOMEvent.target || aDOMEvent.srcElement; @@ -278,9 +273,9 @@ var hasEditableTarget = function(aDOMEvent) if (!currentEventShouldBeSuppressed) { if (characters === "v") - SUPPRESS_CAPPUCCINO_PASTE_FOR_EVENT(anEvent); + anEvent._suppressCappuccinoPaste = YES; else if (characters === "x") - SUPPRESS_CAPPUCCINO_CUT_FOR_EVENT(anEvent); + anEvent._suppressCappuccinoCut = YES; } } @@ -479,7 +474,7 @@ Return true if the event may be a copy and paste event, but the target is not an var anEvent = [self _fakeClipboardEvent:aDOMEvent type:"v"], platformWindow = [[anEvent window] platformWindow]; - SUPPRESS_CAPPUCCINO_PASTE_FOR_EVENT(anEvent); + anEvent._suppressCappuccinoPaste = YES; // By default we'll stop the native handling of the event since we're handling it ourselves. However, we need to // stop it before we send the event so that the event can overrule our choice. CPTextField for instance wants the @@ -505,7 +500,7 @@ Return true if the event may be a copy and paste event, but the target is not an var anEvent = [self _fakeClipboardEvent:aDOMEvent type:(aDOMEvent.type.indexOf("cut") != CPNotFound ? "x" : "c")], platformWindow = [[anEvent window] platformWindow]; - SUPPRESS_CAPPUCCINO_CUT_FOR_EVENT(anEvent); + anEvent._suppressCappuccinoCut = YES; [platformWindow _propagateCurrentDOMEvent:NO]; @@ -539,4 +534,3 @@ Return true if the event may be a copy and paste event, but the target is not an @end -#endif diff --git a/AppKit/Platform/DOM/CPPlatformString.j b/AppKit/Platform/DOM/CPPlatformString.j deleted file mode 100644 index 29fcea5f5..000000000 --- a/AppKit/Platform/DOM/CPPlatformString.j +++ /dev/null @@ -1,204 +0,0 @@ -/* - * CPPlatformString.j - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2010, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -var DOMFixedWidthSpanElement = nil, - DOMFlexibleWidthSpanElement = nil, - DOMMetricsDivElement = nil, - DOMMetricsImgElement = nil, - DefaultFont = nil; - -@implementation CPPlatformString : CPBasePlatformString -{ -} - -+ (void)initialize -{ - if (self !== [CPPlatformString class]) - return; - - DefaultFont = [CPFont systemFontOfSize:CPFontCurrentSystemSize]; -} - -+ (void)bootstrap -{ - [self createDOMElements]; -} - -+ (void)createDOMElements -{ - var style, - bodyElement = [CPPlatform mainBodyElement]; - - DOMFlexibleWidthSpanElement = document.createElement("span"); - DOMFlexibleWidthSpanElement.className = "cpdontremove"; - style = DOMFlexibleWidthSpanElement.style; - style.position = "absolute"; - style.left = "-100000px"; - style.zIndex = -10000; - style.visibility = "visible"; - style.padding = "0px"; - style.margin = "0px"; - style.whiteSpace = "pre"; - - DOMFixedWidthSpanElement = document.createElement("span"); - DOMFixedWidthSpanElement.className = "cpdontremove"; - style = DOMFixedWidthSpanElement.style; - style.display = "block"; - style.position = "absolute"; - style.left = "-100000px"; - style.zIndex = -10000; - style.visibility = "visible"; - style.padding = "0px"; - style.margin = "0px"; - style.width = "1px"; - style.wordWrap = "break-word"; - - try - { - style.whiteSpace = "pre"; - style.whiteSpace = "-o-pre-wrap"; - style.whiteSpace = "-pre-wrap"; - style.whiteSpace = "-moz-pre-wrap"; - style.whiteSpace = "pre-wrap"; - } - catch(e) - { - //some versions of IE throw exceptions for unsupported properties. - style.whiteSpace = "pre"; - } - - bodyElement.appendChild(DOMFlexibleWidthSpanElement); - bodyElement.appendChild(DOMFixedWidthSpanElement); -} - -+ (void)createDOMMetricsElements -{ - var style, - bodyElement = [CPPlatform mainBodyElement]; - - DOMMetricsDivElement = document.createElement("div"); - DOMMetricsDivElement.className = "cpdontremove"; - style = DOMMetricsDivElement.style; - style.position = "absolute"; - style.left = "-100000px"; - style.zIndex = -10000; - style.width = "100000px"; - style.whiteSpace = "nowrap"; - style.lineHeight = "1em"; - style.padding = "0px"; - style.margin = "0px"; - DOMMetricsDivElement.innerHTML = "x"; - - bodyElement.appendChild(DOMMetricsDivElement); - - var imgPath = [[CPBundle bundleForClass:[CPView class]] pathForResource:@"empty.png"]; - - DOMMetricsImgElement = document.createElement("img"); - DOMMetricsImgElement.className = "cpdontremove"; - DOMMetricsImgElement.setAttribute("src", imgPath); - DOMMetricsImgElement.setAttribute("width", "1"); - DOMMetricsImgElement.setAttribute("height", "1"); - DOMMetricsImgElement.setAttribute("alt", ""); - style = DOMMetricsImgElement.style; - style.zIndex = -10000; - style.visibility = "visible"; - style.padding = "0px"; - style.margin = "0px"; - style.border = "none"; - style.verticalAlign = "baseline"; - - DOMMetricsDivElement.appendChild(DOMMetricsImgElement); -} - -+ (int)charPositionOfString:(CPString)aString withFont:(CPFont)aFont forPoint:(CGPoint)aPoint -{ - if (!aString) - return 0; - - var position = 0, - stringLength = aString.length, - currentString = ""; - - for (var i = 0; i < stringLength; i++) - { - var lastChar = aString[i]; - - currentString += lastChar; - - var sizeOfString = [self sizeOfString:currentString withFont:aFont forWidth:nil].width, - sizeLastChar = [self sizeOfString:lastChar withFont:aFont forWidth:nil].width; - - if (sizeOfString - sizeLastChar / 2 < aPoint.x) - position++; - else - break; - } - - return position; -} - -+ (CGSize)sizeOfString:(CPString)aString withFont:(CPFont)aFont forWidth:(float)aWidth -{ - if (!DOMFixedWidthSpanElement) - [self createDOMElements]; - - var span; - - if (!aWidth) - span = DOMFlexibleWidthSpanElement; - else - { - span = DOMFixedWidthSpanElement; - span.style.width = ROUND(aWidth) + "px"; - } - - var effectiveFontCSSString = [(aFont || DefaultFont) cssString]; - if (span.style.font !== effectiveFontCSSString) - span.style.font = effectiveFontCSSString; - - if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature)) - span.innerText = aString; - else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature)) - span.textContent = aString; - - return CGSizeMake(span.clientWidth, span.clientHeight); -} - -+ (CPDictionary)metricsOfFont:(CPFont)aFont -{ - if (!DOMMetricsDivElement) - [self createDOMMetricsElements]; - - DOMMetricsDivElement.style.font = [(aFont || DefaultFont) cssString]; - - var lineHeight = DOMMetricsDivElement.offsetHeight, - baseline = DOMMetricsImgElement.offsetTop + DOMMetricsImgElement.offsetHeight, - descender = baseline - lineHeight; - - return @{ - @"ascender": baseline, - @"descender": descender, - @"lineHeight": lineHeight, - }; -} - -@end diff --git a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j index bac1994c4..450816e01 100644 --- a/AppKit/Platform/DOM/CPPlatformWindow+DOM.j +++ b/AppKit/Platform/DOM/CPPlatformWindow+DOM.j @@ -246,9 +246,7 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio // When scrolling with an old-style scroll wheel with discete steps ('clicks'), the scroll amount can indicate how many "lines" to // scroll. -#define SCROLLWHEEL_LINE_PIXELS 6.0 - -#if PLATFORM(DOM) +var SCROLLWHEEL_LINE_PIXELS = 6.0; @implementation CPPlatformWindow (DOM) @@ -258,16 +256,26 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio if (self) { - _DOMWindow = window; - _contentRect = CGRectMakeZero(); + if (CPDOMAvailable) + { + _DOMWindow = window; + _contentRect = CGRectMakeZero(); - _windowLevels = []; - _windowLayers = @{}; + _windowLevels = []; + _windowLayers = @{}; - _platformPasteboard = [CPPlatformPasteboard new]; + _platformPasteboard = [CPPlatformPasteboard new]; - [self registerDOMWindow]; - [self updateFromNativeContentRect]; + [self registerDOMWindow]; + [self updateFromNativeContentRect]; + } + else + { + _contentRect = CGRectMakeZero(); + + _windowLevels = []; + _windowLayers = @{}; + } _charCodes = {}; @@ -1928,7 +1936,6 @@ _CPPlatformWindowWillCloseNotification = @"_CPPlatformWindowWillCloseNotificatio } @end -#endif var CPEventClass = [CPEvent class]; diff --git a/AppKit/Platform/Platform.h b/AppKit/Platform/Platform.h deleted file mode 100644 index da33f068a..000000000 --- a/AppKit/Platform/Platform.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Platform.h - * AppKit - * - * Created by Francisco Tolmasky. - * Copyright 2008, 280 North, Inc. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#define PLATFORM(FEATURE) (defined( PLATFORM_##FEATURE ) && PLATFORM_##FEATURE) diff --git a/AppKit/_CPDisplayServer.j b/AppKit/_CPDisplayServer.j index 9ea5dbc12..672b52008 100644 --- a/AppKit/_CPDisplayServer.j +++ b/AppKit/_CPDisplayServer.j @@ -21,6 +21,7 @@ */ @import +@import "CPDOMDisplayServer.j" PREPARE_DOM_OPTIMIZATION(); diff --git a/AppKit/_CPImageAndTextView.j b/AppKit/_CPImageAndTextView.j index 2c8846e29..6389cc950 100644 --- a/AppKit/_CPImageAndTextView.j +++ b/AppKit/_CPImageAndTextView.j @@ -28,6 +28,7 @@ @import "CPView.j" @import "CPControl.j" @import "CPPlatform.j" +@import "CPCompatibility.j" var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, @@ -71,11 +72,9 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, unsigned _flags; -#if PLATFORM(DOM) DOMElement _DOMImageElement; DOMElement _DOMTextElement; DOMElement _DOMTextShadowElement; -#endif } @global document @@ -132,7 +131,8 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, _alignment = anAlignment; -#if PLATFORM(DOM) + if (!CPDOMAvailable) return; + switch (_alignment) { case CPLeftTextAlignment: @@ -155,7 +155,6 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, _DOMElement.style.textAlign = ""; break; } -#endif } - (CPTextAlignment)alignment @@ -250,9 +249,10 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, _textColor = aTextColor; -#if PLATFORM(DOM) - _DOMElement.style.color = [_textColor cssString]; -#endif + if (CPDOMAvailable) + { + _DOMElement.style.color = [_textColor cssString]; + } } - (CPColor)textColor @@ -330,8 +330,7 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, var textFrame = CGRectMakeZero(); -#if PLATFORM(DOM) - if (_DOMTextElement) + if (CPDOMAvailable && _DOMTextElement) { var textStyle = _DOMTextElement.style; @@ -343,7 +342,6 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, textFrame.size.width += _textShadowOffset.width; textFrame.size.height += _textShadowOffset.height; } -#endif return textFrame; } @@ -422,427 +420,428 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, - (void)layoutSubviews { -#if PLATFORM(DOM) - var needsDOMTextElement = _imagePosition !== CPImageOnly && ([_text length] > 0), - hasDOMTextElement = !!_DOMTextElement; - - // Create or destroy the DOM Text Element as necessary - if (needsDOMTextElement !== hasDOMTextElement) + if (CPDOMAvailable) { - if (hasDOMTextElement) + var needsDOMTextElement = _imagePosition !== CPImageOnly && ([_text length] > 0), + hasDOMTextElement = !!_DOMTextElement; + + // Create or destroy the DOM Text Element as necessary + if (needsDOMTextElement !== hasDOMTextElement) { - _DOMElement.removeChild(_DOMTextElement); - _DOMTextElement = nil; - hasDOMTextElement = NO; - } - else - { - _DOMTextElement = document.createElement("div"); - - var textStyle = _DOMTextElement.style; - textStyle.position = "absolute"; - textStyle.whiteSpace = "pre"; - textStyle.zIndex = 200; - textStyle.overflow = "hidden"; - - _DOMElement.appendChild(_DOMTextElement); - hasDOMTextElement = YES; - - // We have to set all these values now. - _flags |= _CPImageAndTextViewTextChangedFlag | _CPImageAndTextViewFontChangedFlag | _CPImageAndTextViewLineBreakModeChangedFlag | _CPImageAndTextViewTextUnderlineChangedFlag; - } - } - - var textStyle = hasDOMTextElement ? _DOMTextElement.style : nil; - - // If theme is CSS based and if shadow color is a CSS dictionary, we don't need a separate DOM element, - // we simply put shadow styling on the regular text element - if (hasDOMTextElement && [[self actualTheme] isCSSBased] && [_textShadowColor cssDictionary]) - { - [[_textShadowColor cssDictionary] enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) - { - _DOMTextElement.style[aKey] = anObject; - }]; - } - else - { - // Create or destroy the DOM Text Shadow element as necessary. - // If _textShadowColor's alphaComponent is 0, don't bother drawing anything (issue #1412). - // This improves performance as we get rid of an invisible element, and makes IE <9.0 capable - // of correctly 'rendering' shadows with [CPColor clearColor]. - var needsDOMTextShadowElement = hasDOMTextElement && [_textShadowColor alphaComponent] > 0.0, - hasDOMTextShadowElement = !!_DOMTextShadowElement; - - if (needsDOMTextShadowElement !== hasDOMTextShadowElement) - { - if (hasDOMTextShadowElement) + if (hasDOMTextElement) { - _DOMElement.removeChild(_DOMTextShadowElement); - - _DOMTextShadowElement = nil; - - hasDOMTextShadowElement = NO; + _DOMElement.removeChild(_DOMTextElement); + _DOMTextElement = nil; + hasDOMTextElement = NO; } else { - _DOMTextShadowElement = document.createElement("div"); + _DOMTextElement = document.createElement("div"); - var shadowStyle = _DOMTextShadowElement.style, - font = (_font || [CPFont systemFontOfSize:CPFontCurrentSystemSize]); + var textStyle = _DOMTextElement.style; + textStyle.position = "absolute"; + textStyle.whiteSpace = "pre"; + textStyle.zIndex = 200; + textStyle.overflow = "hidden"; - shadowStyle.font = [font cssString]; - shadowStyle.position = "absolute"; - shadowStyle.whiteSpace = textStyle.whiteSpace; - shadowStyle.wordWrap = textStyle.wordWrap; - shadowStyle.color = [_textShadowColor cssString]; - shadowStyle.lineHeight = [font defaultLineHeightForFont] + "px"; + _DOMElement.appendChild(_DOMTextElement); + hasDOMTextElement = YES; - shadowStyle.zIndex = 150; - shadowStyle.textOverflow = textStyle.textOverflow; - - if (document.attachEvent) - { - shadowStyle.overflow = textStyle.overflow; - } - else - { - shadowStyle.overflowX = textStyle.overflowX; - shadowStyle.overflowY = textStyle.overflowY; - } - - _DOMElement.appendChild(_DOMTextShadowElement); - - hasDOMTextShadowElement = YES; - - _flags |= _CPImageAndTextViewTextChangedFlag; //sigh... - } - } - } - - var shadowStyle = hasDOMTextShadowElement ? _DOMTextShadowElement.style : nil; - - if (hasDOMTextElement) - { - // Update the text contents if necessary. - if (_flags & _CPImageAndTextViewTextChangedFlag) - if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature)) - { - _DOMTextElement.innerText = _text; - - if (_DOMTextShadowElement) - _DOMTextShadowElement.innerText = _text; - } - else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature)) - { - _DOMTextElement.textContent = _text; - - if (_DOMTextShadowElement) - _DOMTextShadowElement.textContent = _text; - } - - if (_flags & _CPImageAndTextViewFontChangedFlag) - { - var font = (_font || [CPFont systemFontOfSize:CPFontCurrentSystemSize]), - fontStyle = [font cssString]; - textStyle.font = fontStyle; - textStyle.lineHeight = [font defaultLineHeightForFont] + "px"; - - if (shadowStyle) - { - shadowStyle.font = fontStyle; - shadowStyle.lineHeight = [font defaultLineHeightForFont] + "px"; + // We have to set all these values now. + _flags |= _CPImageAndTextViewTextChangedFlag | _CPImageAndTextViewFontChangedFlag | _CPImageAndTextViewLineBreakModeChangedFlag | _CPImageAndTextViewTextUnderlineChangedFlag; } } - if (_flags & _CPImageAndTextViewTextUnderlineChangedFlag) + var textStyle = hasDOMTextElement ? _DOMTextElement.style : nil; + + // If theme is CSS based and if shadow color is a CSS dictionary, we don't need a separate DOM element, + // we simply put shadow styling on the regular text element + if (hasDOMTextElement && [[self actualTheme] isCSSBased] && [_textShadowColor cssDictionary]) { - textStyle.textDecoration = _textUnderline ? "underline" : ""; - } - - // Update the line break mode if necessary. - if (_flags & _CPImageAndTextViewLineBreakModeChangedFlag) - { - switch (_lineBreakMode) - { - case CPLineBreakByClipping: - textStyle.overflow = "hidden"; - textStyle.textOverflow = "clip"; - textStyle.whiteSpace = "pre"; - textStyle.wordWrap = "normal"; - break; - - case CPLineBreakByTruncatingHead: - case CPLineBreakByTruncatingMiddle: // Don't have support for these (yet?), so just degrade to truncating tail. - case CPLineBreakByTruncatingTail: - textStyle.textOverflow = "ellipsis"; - textStyle.whiteSpace = "pre"; - textStyle.overflow = "hidden"; - textStyle.wordWrap = "normal"; - break; - - case CPLineBreakByCharWrapping: - case CPLineBreakByWordWrapping: - textStyle.wordWrap = "break-word"; - try - { - textStyle.whiteSpace = "pre"; - textStyle.whiteSpace = "-o-pre-wrap"; - textStyle.whiteSpace = "-pre-wrap"; - textStyle.whiteSpace = "-moz-pre-wrap"; - textStyle.whiteSpace = "pre-wrap"; - } - catch (e) - { - //internet explorer doesn't like these properties - textStyle.whiteSpace = "pre"; - } - textStyle.overflow = "hidden"; - textStyle.textOverflow = "clip"; - break; - } - - if (shadowStyle) - { - if (document.attachEvent) - { - shadowStyle.overflow = textStyle.overflow; - } - else - { - shadowStyle.overflowX = textStyle.overflowX; - shadowStyle.overflowY = textStyle.overflowY; - } - - shadowStyle.wordWrap = textStyle.wordWrap; - shadowStyle.whiteSpace = textStyle.whiteSpace; - shadowStyle.textOverflow = textStyle.textOverflow; - } - } - } - - var needsDOMImageElement = _image != nil && _imagePosition !== CPNoImage, - hasDOMImageElement = !!_DOMImageElement, - // For CSS theming - isCSSBasedImage = [_image isCSSBased], - isIMGImageElement = hasDOMImageElement && (_DOMImageElement.nodeName == "IMG"); - - // First, check if we need to destroy a current DOM image element. This is the case if : - // - we have one but don't need it anymore - // - we have one but not the right one (that is a DIV but needing an IMG, and vice versa) - - if (hasDOMImageElement) - { - if (!needsDOMImageElement || (isIMGImageElement && isCSSBasedImage) || (!isIMGImageElement && !isCSSBasedImage)) - { - // OK, destroy it - - _DOMElement.removeChild(_DOMImageElement); - - _DOMImageElement = nil; - - hasDOMImageElement = NO; - - // CSS styling cleaning - _cssStylePreviousState = @[]; - _cssStyleNode = nil; - } - } - - // Now, if we need a DOM image element and if we don't have one, create a new one - - if (needsDOMImageElement && !hasDOMImageElement) - { - _DOMImageElement = document.createElement(isCSSBasedImage ? "div" : "img"); - - if ([CPPlatform supportsDragAndDrop]) - { - _DOMImageElement.setAttribute("draggable", "true"); - _DOMImageElement.style["-khtml-user-drag"] = "element"; - } - - var imageStyle = _DOMImageElement.style; - - imageStyle.top = "0px"; - imageStyle.left = "0px"; - imageStyle.position = "absolute"; - imageStyle.zIndex = 100; - - _DOMElement.appendChild(_DOMImageElement); - - hasDOMImageElement = YES; - } - - var size = [self bounds].size, - textRect = CGRectMake(0.0, 0.0, size.width, size.height); - - if (hasDOMImageElement) - { - if (!imageStyle) - var imageStyle = _DOMImageElement.style; - - var centerX = size.width / 2.0, - centerY = size.height / 2.0, - imageSize = [_image size], - imageWidth = imageSize.width, - imageHeight = imageSize.height; - - if (_imageScaling === CPImageScaleAxesIndependently) - { - imageWidth = size.width; - imageHeight = size.height; - } - else if (_imageScaling === CPImageScaleProportionallyDown) - { - var scale = MIN(MIN(size.width, imageWidth) / imageWidth, MIN(size.height, imageHeight) / imageHeight); - - imageWidth *= scale; - imageHeight *= scale; - } - else if (_imageScaling === CPImageScaleProportionallyUpOrDown) - { - var scale = MIN(size.width / imageWidth, size.height / imageHeight); - - imageWidth *= scale; - imageHeight *= scale; - } - - if (CPFeatureIsCompatible(CPOpacityRequiresFilterFeature)) - imageStyle.filter = @"alpha(opacity=" + _shouldDimImage ? 50 : 100 + ")"; - else - imageStyle.opacity = _shouldDimImage ? 0.5 : 1.0; - - _DOMImageElement.width = imageWidth; - _DOMImageElement.height = imageHeight; - imageStyle.width = MAX(imageWidth, 0) + "px"; - imageStyle.height = MAX(imageHeight, 0) + "px"; - - if (_imagePosition === CPImageBelow) - { - imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; - imageStyle.top = FLOOR(size.height - imageHeight) + "px"; - - textRect.size.height = size.height - imageHeight - _imageOffset; - } - else if (_imagePosition === CPImageAbove) - { - imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; - imageStyle.top = 0 + "px"; - - textRect.origin.y += imageHeight + _imageOffset; - textRect.size.height = size.height - imageHeight - _imageOffset; - } - else if (_imagePosition === CPImageLeft) - { - imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; - imageStyle.left = "0px"; - - textRect.origin.x = imageWidth + _imageOffset; - textRect.size.width -= imageWidth + _imageOffset; - } - else if (_imagePosition === CPImageRight) - { - imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; - imageStyle.left = FLOOR(size.width - imageWidth) + "px"; - - textRect.size.width -= imageWidth + _imageOffset; - } - else if (_imagePosition === CPImageOnly || _imagePosition == CPImageOverlaps) - { - imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; - imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; - } - - if (_flags & _CPImageAndTextViewImageChangedFlag) - { - if (isCSSBasedImage) - { - // For material icons images & co. - if ([_image _shouldBeResized]) - [_image _setDisplaySize:CGSizeMake(imageWidth, imageHeight)]; - - _cssStyleNode = [_image applyCSSImageForView:self - onDOMElement:_DOMImageElement - styleNode:_cssStyleNode - previousState:@ref(_cssStylePreviousState)]; - } - else - _DOMImageElement.src = [_image filename]; - } - - } - - if (hasDOMTextElement) - { - var textRectX = CGRectGetMinX(textRect), - textRectY = CGRectGetMinY(textRect), - textRectWidth = CGRectGetWidth(textRect), - textRectHeight = CGRectGetHeight(textRect); - - if (textRectWidth <= 0 || textRectHeight <= 0) - { - // Don't bother trying to position the text in an empty rect. - textRectWidth = 0; - textRectHeight = 0; + [[_textShadowColor cssDictionary] enumerateKeysAndObjectsUsingBlock:function(aKey, anObject, stop) + { + _DOMTextElement.style[aKey] = anObject; + }]; } else { - if (_verticalAlignment !== CPTopVerticalTextAlignment) - { - if (!_textSize) - { - if (_lineBreakMode === CPLineBreakByCharWrapping || - _lineBreakMode === CPLineBreakByWordWrapping) - { - _textSize = [_text sizeWithFont:_font inWidth:textRectWidth]; + // Create or destroy the DOM Text Shadow element as necessary. + // If _textShadowColor's alphaComponent is 0, don't bother drawing anything (issue #1412). + // This improves performance as we get rid of an invisible element, and makes IE <9.0 capable + // of correctly 'rendering' shadows with [CPColor clearColor]. + var needsDOMTextShadowElement = hasDOMTextElement && [_textShadowColor alphaComponent] > 0.0, + hasDOMTextShadowElement = !!_DOMTextShadowElement; - if (_usesSingleLineMode) - _textSize.height = [_font defaultLineHeightForFont]; + if (needsDOMTextShadowElement !== hasDOMTextShadowElement) + { + if (hasDOMTextShadowElement) + { + _DOMElement.removeChild(_DOMTextShadowElement); + + _DOMTextShadowElement = nil; + + hasDOMTextShadowElement = NO; + } + else + { + _DOMTextShadowElement = document.createElement("div"); + + var shadowStyle = _DOMTextShadowElement.style, + font = (_font || [CPFont systemFontOfSize:CPFontCurrentSystemSize]); + + shadowStyle.font = [font cssString]; + shadowStyle.position = "absolute"; + shadowStyle.whiteSpace = textStyle.whiteSpace; + shadowStyle.wordWrap = textStyle.wordWrap; + shadowStyle.color = [_textShadowColor cssString]; + shadowStyle.lineHeight = [font defaultLineHeightForFont] + "px"; + + shadowStyle.zIndex = 150; + shadowStyle.textOverflow = textStyle.textOverflow; + + if (document.attachEvent) + { + shadowStyle.overflow = textStyle.overflow; } else { - _textSize = [_text sizeWithFont:_font]; - - // Account for possible fractional pixels at right edge - _textSize.width += 1; + shadowStyle.overflowX = textStyle.overflowX; + shadowStyle.overflowY = textStyle.overflowY; } - // Account for possible fractional pixels at bottom edge - _textSize.height += 1; - } + _DOMElement.appendChild(_DOMTextShadowElement); - if (_verticalAlignment === CPCenterVerticalTextAlignment) - { - // Since we added +1 px height above to show fractional pixels on the bottom, we have to remove that when calculating vertical centre. - textRectY = textRectY + (textRectHeight - _textSize.height + 1.0) / 2.0; - textRectHeight = _textSize.height; - } + hasDOMTextShadowElement = YES; - else // if (_verticalAlignment === CPBottomVerticalTextAlignment) - { - textRectY = textRectY + textRectHeight - _textSize.height; - textRectHeight = _textSize.height; + _flags |= _CPImageAndTextViewTextChangedFlag; //sigh... } } } - textStyle.top = ROUND(textRectY) + "px"; - textStyle.left = ROUND(textRectX) + "px"; - textStyle.width = MAX(CEIL(textRectWidth), 0) + "px"; - textStyle.height = MAX(CEIL(textRectHeight), 0) + "px"; - textStyle.verticalAlign = @"top"; + var shadowStyle = hasDOMTextShadowElement ? _DOMTextShadowElement.style : nil; - if (shadowStyle) + if (hasDOMTextElement) { - if (_flags & _CPImageAndTextViewTextShadowColorChangedFlag) - shadowStyle.color = [_textShadowColor cssString]; + // Update the text contents if necessary. + if (_flags & _CPImageAndTextViewTextChangedFlag) + if (CPFeatureIsCompatible(CPJavaScriptInnerTextFeature)) + { + _DOMTextElement.innerText = _text; - shadowStyle.top = ROUND(textRectY + _textShadowOffset.height) + "px"; - shadowStyle.left = ROUND(textRectX + _textShadowOffset.width) + "px"; - shadowStyle.width = MAX(CEIL(textRectWidth), 0) + "px"; - shadowStyle.height = MAX(CEIL(textRectHeight), 0) + "px"; + if (_DOMTextShadowElement) + _DOMTextShadowElement.innerText = _text; + } + else if (CPFeatureIsCompatible(CPJavaScriptTextContentFeature)) + { + _DOMTextElement.textContent = _text; + + if (_DOMTextShadowElement) + _DOMTextShadowElement.textContent = _text; + } + + if (_flags & _CPImageAndTextViewFontChangedFlag) + { + var font = (_font || [CPFont systemFontOfSize:CPFontCurrentSystemSize]), + fontStyle = [font cssString]; + textStyle.font = fontStyle; + textStyle.lineHeight = [font defaultLineHeightForFont] + "px"; + + if (shadowStyle) + { + shadowStyle.font = fontStyle; + shadowStyle.lineHeight = [font defaultLineHeightForFont] + "px"; + } + } + + if (_flags & _CPImageAndTextViewTextUnderlineChangedFlag) + { + textStyle.textDecoration = _textUnderline ? "underline" : ""; + } + + // Update the line break mode if necessary. + if (_flags & _CPImageAndTextViewLineBreakModeChangedFlag) + { + switch (_lineBreakMode) + { + case CPLineBreakByClipping: + textStyle.overflow = "hidden"; + textStyle.textOverflow = "clip"; + textStyle.whiteSpace = "pre"; + textStyle.wordWrap = "normal"; + break; + + case CPLineBreakByTruncatingHead: + case CPLineBreakByTruncatingMiddle: // Don't have support for these (yet?), so just degrade to truncating tail. + case CPLineBreakByTruncatingTail: + textStyle.textOverflow = "ellipsis"; + textStyle.whiteSpace = "pre"; + textStyle.overflow = "hidden"; + textStyle.wordWrap = "normal"; + break; + + case CPLineBreakByCharWrapping: + case CPLineBreakByWordWrapping: + textStyle.wordWrap = "break-word"; + try + { + textStyle.whiteSpace = "pre"; + textStyle.whiteSpace = "-o-pre-wrap"; + textStyle.whiteSpace = "-pre-wrap"; + textStyle.whiteSpace = "-moz-pre-wrap"; + textStyle.whiteSpace = "pre-wrap"; + } + catch (e) + { + //internet explorer doesn't like these properties + textStyle.whiteSpace = "pre"; + } + textStyle.overflow = "hidden"; + textStyle.textOverflow = "clip"; + break; + } + + if (shadowStyle) + { + if (document.attachEvent) + { + shadowStyle.overflow = textStyle.overflow; + } + else + { + shadowStyle.overflowX = textStyle.overflowX; + shadowStyle.overflowY = textStyle.overflowY; + } + + shadowStyle.wordWrap = textStyle.wordWrap; + shadowStyle.whiteSpace = textStyle.whiteSpace; + shadowStyle.textOverflow = textStyle.textOverflow; + } + } + } + + var needsDOMImageElement = _image != nil && _imagePosition !== CPNoImage, + hasDOMImageElement = !!_DOMImageElement, + // For CSS theming + isCSSBasedImage = [_image isCSSBased], + isIMGImageElement = hasDOMImageElement && (_DOMImageElement.nodeName == "IMG"); + + // First, check if we need to destroy a current DOM image element. This is the case if : + // - we have one but don't need it anymore + // - we have one but not the right one (that is a DIV but needing an IMG, and vice versa) + + if (hasDOMImageElement) + { + if (!needsDOMImageElement || (isIMGImageElement && isCSSBasedImage) || (!isIMGImageElement && !isCSSBasedImage)) + { + // OK, destroy it + + _DOMElement.removeChild(_DOMImageElement); + + _DOMImageElement = nil; + + hasDOMImageElement = NO; + + // CSS styling cleaning + _cssStylePreviousState = @[]; + _cssStyleNode = nil; + } + } + + // Now, if we need a DOM image element and if we don't have one, create a new one + + if (needsDOMImageElement && !hasDOMImageElement) + { + _DOMImageElement = document.createElement(isCSSBasedImage ? "div" : "img"); + + if ([CPPlatform supportsDragAndDrop]) + { + _DOMImageElement.setAttribute("draggable", "true"); + _DOMImageElement.style["-khtml-user-drag"] = "element"; + } + + var imageStyle = _DOMImageElement.style; + + imageStyle.top = "0px"; + imageStyle.left = "0px"; + imageStyle.position = "absolute"; + imageStyle.zIndex = 100; + + _DOMElement.appendChild(_DOMImageElement); + + hasDOMImageElement = YES; + } + + var size = [self bounds].size, + textRect = CGRectMake(0.0, 0.0, size.width, size.height); + + if (hasDOMImageElement) + { + if (!imageStyle) + var imageStyle = _DOMImageElement.style; + + var centerX = size.width / 2.0, + centerY = size.height / 2.0, + imageSize = [_image size], + imageWidth = imageSize.width, + imageHeight = imageSize.height; + + if (_imageScaling === CPImageScaleAxesIndependently) + { + imageWidth = size.width; + imageHeight = size.height; + } + else if (_imageScaling === CPImageScaleProportionallyDown) + { + var scale = MIN(MIN(size.width, imageWidth) / imageWidth, MIN(size.height, imageHeight) / imageHeight); + + imageWidth *= scale; + imageHeight *= scale; + } + else if (_imageScaling === CPImageScaleProportionallyUpOrDown) + { + var scale = MIN(size.width / imageWidth, size.height / imageHeight); + + imageWidth *= scale; + imageHeight *= scale; + } + + if (CPFeatureIsCompatible(CPOpacityRequiresFilterFeature)) + imageStyle.filter = @"alpha(opacity=" + _shouldDimImage ? 50 : 100 + ")"; + else + imageStyle.opacity = _shouldDimImage ? 0.5 : 1.0; + + _DOMImageElement.width = imageWidth; + _DOMImageElement.height = imageHeight; + imageStyle.width = MAX(imageWidth, 0) + "px"; + imageStyle.height = MAX(imageHeight, 0) + "px"; + + if (_imagePosition === CPImageBelow) + { + imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; + imageStyle.top = FLOOR(size.height - imageHeight) + "px"; + + textRect.size.height = size.height - imageHeight - _imageOffset; + } + else if (_imagePosition === CPImageAbove) + { + imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; + imageStyle.top = 0 + "px"; + + textRect.origin.y += imageHeight + _imageOffset; + textRect.size.height = size.height - imageHeight - _imageOffset; + } + else if (_imagePosition === CPImageLeft) + { + imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; + imageStyle.left = "0px"; + + textRect.origin.x = imageWidth + _imageOffset; + textRect.size.width -= imageWidth + _imageOffset; + } + else if (_imagePosition === CPImageRight) + { + imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; + imageStyle.left = FLOOR(size.width - imageWidth) + "px"; + + textRect.size.width -= imageWidth + _imageOffset; + } + else if (_imagePosition === CPImageOnly || _imagePosition == CPImageOverlaps) + { + imageStyle.top = _imageVerticalOffset + FLOOR(centerY - imageHeight / 2.0) + "px"; + imageStyle.left = FLOOR(centerX - imageWidth / 2.0) + "px"; + } + + if (_flags & _CPImageAndTextViewImageChangedFlag) + { + if (isCSSBasedImage) + { + // For material icons images & co. + if ([_image _shouldBeResized]) + [_image _setDisplaySize:CGSizeMake(imageWidth, imageHeight)]; + + _cssStyleNode = [_image applyCSSImageForView:self + onDOMElement:_DOMImageElement + styleNode:_cssStyleNode + previousState:@ref(_cssStylePreviousState)]; + } + else + _DOMImageElement.src = [_image filename]; + } + + } + + if (hasDOMTextElement) + { + var textRectX = CGRectGetMinX(textRect), + textRectY = CGRectGetMinY(textRect), + textRectWidth = CGRectGetWidth(textRect), + textRectHeight = CGRectGetHeight(textRect); + + if (textRectWidth <= 0 || textRectHeight <= 0) + { + // Don't bother trying to position the text in an empty rect. + textRectWidth = 0; + textRectHeight = 0; + } + else + { + if (_verticalAlignment !== CPTopVerticalTextAlignment) + { + if (!_textSize) + { + if (_lineBreakMode === CPLineBreakByCharWrapping || + _lineBreakMode === CPLineBreakByWordWrapping) + { + _textSize = [_text sizeWithFont:_font inWidth:textRectWidth]; + + if (_usesSingleLineMode) + _textSize.height = [_font defaultLineHeightForFont]; + } + else + { + _textSize = [_text sizeWithFont:_font]; + + // Account for possible fractional pixels at right edge + _textSize.width += 1; + } + + // Account for possible fractional pixels at bottom edge + _textSize.height += 1; + } + + if (_verticalAlignment === CPCenterVerticalTextAlignment) + { + // Since we added +1 px height above to show fractional pixels on the bottom, we have to remove that when calculating vertical centre. + textRectY = textRectY + (textRectHeight - _textSize.height + 1.0) / 2.0; + textRectHeight = _textSize.height; + } + + else // if (_verticalAlignment === CPBottomVerticalTextAlignment) + { + textRectY = textRectY + textRectHeight - _textSize.height; + textRectHeight = _textSize.height; + } + } + } + + textStyle.top = ROUND(textRectY) + "px"; + textStyle.left = ROUND(textRectX) + "px"; + textStyle.width = MAX(CEIL(textRectWidth), 0) + "px"; + textStyle.height = MAX(CEIL(textRectHeight), 0) + "px"; + textStyle.verticalAlign = @"top"; + + if (shadowStyle) + { + if (_flags & _CPImageAndTextViewTextShadowColorChangedFlag) + shadowStyle.color = [_textShadowColor cssString]; + + shadowStyle.top = ROUND(textRectY + _textShadowOffset.height) + "px"; + shadowStyle.left = ROUND(textRectX + _textShadowOffset.width) + "px"; + shadowStyle.width = MAX(CEIL(textRectWidth), 0) + "px"; + shadowStyle.height = MAX(CEIL(textRectHeight), 0) + "px"; + } } } -#endif _flags = 0; } @@ -901,10 +900,10 @@ var _CPimageAndTextViewFrameSizeChangedFlag = 1 << 0, - (void)setSelectedRange:(CPRange)aRange { -#if PLATFORM(DOM) + if (!CPDOMAvailable) return; + if (_DOMTextElement) [[[self window] platformWindow] setSelectedRange:aRange inElement:_DOMTextElement]; -#endif } @end diff --git a/AppKit/_CPObject+Theme.j b/AppKit/_CPObject+Theme.j index 0e813d74d..2cdeaf71b 100644 --- a/AppKit/_CPObject+Theme.j +++ b/AppKit/_CPObject+Theme.j @@ -76,12 +76,8 @@ var CPViewThemeClassKey = @"CPViewThemeClassKey", - (BOOL)hasThemeState:(ThemeState)aState { -#if DEBUG -// TODO: To allow aState to be an array is now deprecated. An exception is thrown only in Debug version as -// the preformance cost for this check is to high. We should remove this check in a future release. if (aState && aState.isa && [aState isKindOfClass:CPArray]) [CPException raise:CPInvalidArgumentException reason:@"aState can't be an array. Please use 'hasThemeStates: instead: " + aState]; -#endif return _themeState.hasThemeState(aState); } @@ -99,12 +95,8 @@ var CPViewThemeClassKey = @"CPViewThemeClassKey", - (BOOL)setThemeState:(ThemeState)aState { -#if DEBUG -// TODO: To allow aState to be an array is now deprecated. An exception is thrown only in Debug version as -// the preformance cost for this check is to high. We should remove this check in a future release. if (aState && aState.isa && [aState isKindOfClass:CPArray]) [CPException raise:CPInvalidArgumentException reason:@"aState can't be an array. Please use 'setThemeStates: instead: " + aState]; -#endif if (_themeState.hasThemeState(aState)) return NO; @@ -116,12 +108,8 @@ var CPViewThemeClassKey = @"CPViewThemeClassKey", - (BOOL)unsetThemeState:(ThemeState)aState { -#if DEBUG -// TODO: To allow aState to be an array is now deprecated. An exception is thrown only in Debug version as -// the preformance cost for this check is to high. We should remove this check in a future release. if (aState && aState.isa && [aState isKindOfClass:CPArray]) [CPException raise:CPInvalidArgumentException reason:@"aState can't be an array. Please use 'unsetThemeStates: instead: " + aState]; -#endif var oldThemeState = _themeState; @@ -318,12 +306,8 @@ var NULL_THEME = {}; - (void)setValue:(id)aValue forThemeAttribute:(CPString)aName inState:(ThemeState)aState { -#if DEBUG -// TODO: To allow aState to be an array is now deprecated. An exception is thrown only in Debug version as -// the preformance cost for this check is to high. We should remove this check in a future release. if (aState.isa && [aState isKindOfClass:CPArray]) [CPException raise:CPInvalidArgumentException reason:self + @": aState can't be an array. Please use 'setValue:forThemeAttribute:inStates:' instead: " + aState]; -#endif var themeAttr = _themeAttributes && _themeAttributes[aName]; @@ -361,10 +345,8 @@ var NULL_THEME = {}; - (id)valueForThemeAttribute:(CPString)aName inState:(ThemeState)aState { -#if DEBUG if (aState.isa && [aState isKindOfClass:CPArray]) [CPException raise:CPInvalidArgumentException reason:@"aState can't be an array. Please use 'valueForThemeAttribute:inStates:' instead: " + aState]; -#endif var themeAttr = _themeAttributes && _themeAttributes[aName]; diff --git a/AppKit/_CPPopoverWindow.j b/AppKit/_CPPopoverWindow.j index 619c7657e..2a5a413f8 100644 --- a/AppKit/_CPPopoverWindow.j +++ b/AppKit/_CPPopoverWindow.j @@ -26,6 +26,7 @@ @import "CPMenu.j" @import "CPPanel.j" @import "CPAnimationContext.j" +@import "CPCompatibility.j" // Use forward declaration because this file is imported by CPPopover @class CPPopover @@ -415,12 +416,12 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, /*! @ignore */ - (void)setCSS3Property:(CPString)aProperty value:(CPString)value { + if (!CPDOMAvailable) return; + var browserProperty = CPBrowserStyleProperty(aProperty); -#if PLATFORM(DOM) if (browserProperty) _DOMElement.style[browserProperty] = value; -#endif } /*! @ignore */ @@ -537,14 +538,16 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, window.setTimeout(function() { // We are watching opacity, so this triggers the next transition -#if PLATFORM(DOM) - // We force the style to recalculate the values, this is needed to avoid a transition issue - // More information here : https://code.google.com/p/chromium/issues/detail?id=388082 - [self _currentTransformMatrix]; - _DOMElement.style.opacity = 1; - _DOMElement.style.height = frame.size.height + @"px"; - _DOMElement.style.width = frame.size.width + @"px"; -#endif + + if (CPDOMAvailable) + { + // We force the style to recalculate the values, this is needed to avoid a transition issue + // More information here : https://code.google.com/p/chromium/issues/detail?id=388082 + [self _currentTransformMatrix]; + _DOMElement.style.opacity = 1; + _DOMElement.style.height = frame.size.height + @"px"; + _DOMElement.style.width = frame.size.width + @"px"; + } // Set up the pop-out transition [self setCSS3Property:@"Transform" value:@"scale(1.1)"]; @@ -552,9 +555,10 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, _orderFrontTransitionFunction = function() { -#if PLATFORM(DOM) - _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _orderFrontTransitionFunction, YES); -#endif + if (CPDOMAvailable) + { + _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _orderFrontTransitionFunction, YES); + } // Now set up the pop-in to normal size transition. // Because we are watching the -webkit-transform, it will occur now. @@ -566,33 +570,37 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, _transitionCompleteFunction = function() { -#if PLATFORM(DOM) - _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _transitionCompleteFunction, YES); + if (CPDOMAvailable) + { + _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _transitionCompleteFunction, YES); + + // Make sure to clear these properties when the animation is done. Without this, + // the window becomes blurry in Chrome, presumably because the browser composits + // a layer with a transform differently even when it's an identity transform. + [self setCSS3Property:@"Transform" value:nil]; + [self setCSS3Property:@"TransformOrigin" value:nil]; + [self setCSS3Property:@"Transition" value:nil]; + } - // Make sure to clear these properties when the animation is done. Without this, - // the window becomes blurry in Chrome, presumably because the browser composits - // a layer with a transform differently even when it's an identity transform. - [self setCSS3Property:@"Transform" value:nil]; - [self setCSS3Property:@"TransformOrigin" value:nil]; - [self setCSS3Property:@"Transition" value:nil]; -#endif _isOpening = NO; [_delegate _popoverWindowDidShow]; }; -#if PLATFORM(DOM) - _DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), _transitionCompleteFunction, YES); -#endif + if (CPDOMAvailable) + { + _DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), _transitionCompleteFunction, YES); + } }, 0); // There are some weird random conditions happening in Chrome 44. If we don't put a timeout to 0 for the end of // the transition, the popover is blinking. It happens on Opera as well... // When the condition is met, the popover will lag a bit when opening...nothing crazy ;) // An issue has been opened here : https://code.google.com/p/chromium/issues/detail?id=523044&thanks=523044&ts=1440095724 }; -#if PLATFORM(DOM) - _DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), _orderFrontTransitionFunction, YES); -#endif + if (CPDOMAvailable) + { + _DOMElement.addEventListener(CPBrowserStyleProperty('transitionend'), _orderFrontTransitionFunction, YES); + } }, 10); // There are some weird race conditions happening in Chrome 34. If this is set to 0 // the transitionend is randomly not called correctly. Setting the timeout to 10ms is not noticealble for the // user, and seems to fix the issue. @@ -601,9 +609,11 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, { _isOpening = NO; [self setCSS3Property:@"Transition" value:@""]; -#if PLATFORM(DOM) - _DOMElement.style.opacity = 1; -#endif + + if (CPDOMAvailable) + { + _DOMElement.style.opacity = 1; + } } } @@ -639,9 +649,8 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, if (_animates && _browserAnimates) { - if (_isOpening) + if (CPDOMAvailable && _isOpening) { -#if PLATFORM(DOM) _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _orderFrontTransitionFunction, YES); _DOMElement.removeEventListener(CPBrowserStyleProperty('transitionend'), _transitionCompleteFunction, YES); @@ -649,15 +658,16 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, currentScale = (matrix.split('(')[1]).split(',')[0]; [self setCSS3Property:@"Transform" value:@"scale(" + currentScale + ")"]; -#endif } // Tell the element to fade out when the opacity changes [self setCSS3Property:@"Transition" value:@"opacity 250ms linear"]; -#if PLATFORM(DOM) - _DOMElement.style.opacity = 0; - _DOMElement.addEventListener(CPBrowserStyleProperty("transitionend"), _orderOutTransitionFunction, YES); -#endif + + if (CPDOMAvailable) + { + _DOMElement.style.opacity = 0; + _DOMElement.addEventListener(CPBrowserStyleProperty("transitionend"), _orderOutTransitionFunction, YES); + } } else { @@ -668,10 +678,10 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, - (void)_orderOutRecursively:(BOOL)recursive { // Make absolutely sure no dangling event listeners are left -#if PLATFORM(DOM) - if (_animates && _browserAnimates) + if (CPDOMAvailable && _animates && _browserAnimates) + { _DOMElement.removeEventListener(CPBrowserStyleProperty("transitionend"), _orderOutTransitionFunction, YES); -#endif + } [self _removeFrameObserver]; [_parentWindow removeChildWindow:self]; @@ -702,9 +712,7 @@ var _CPPopoverWindow_shouldClose_ = 1 << 4, - (CPString)_currentTransformMatrix { -#if PLATFORM(DOM) - return window.getComputedStyle(_DOMElement, null)[CPBrowserStyleProperty(@"transform")]; -#endif + if (CPDOMAvailable) return window.getComputedStyle(_DOMElement, null)[CPBrowserStyleProperty(@"transform")]; } - (BOOL)_hasOnlyTransientChild:(_CPPopoverWindow)aWindow diff --git a/Foundation/CPDecimal.j b/Foundation/CPDecimal.j index 8dba1b75d..98669882d 100644 --- a/Foundation/CPDecimal.j +++ b/Foundation/CPDecimal.j @@ -577,7 +577,8 @@ function CPDecimalAdd(result, leftOperand, rightOperand, roundingMode, longMode) // below is equiv. of simple compare var comp = 0, ll = n1._mantissa.length, - lr = n2._mantissa.length; + lr = n2._mantissa.length, + adderror; if (ll == lr) comp = CPOrderedSame; @@ -607,7 +608,7 @@ function CPDecimalAdd(result, leftOperand, rightOperand, roundingMode, longMode) // swap sign over over/underflow exception if (CPCalculationUnderflow == adderror) adderror = CPCalculationOverflow; - else if (CPCalculationUnderflow == adderror) + else if (CPCalculationOverflow == adderror) adderror = CPCalculationUnderflow; } else @@ -790,6 +791,7 @@ function _SimpleDivide(result, leftOperand, rightOperand, roundingMode) k = 0, firsttime = YES, stopk = CPDecimalMaxDigits + 1, + error1, used = 0; // How many digits of l have been used? _CPDecimalSetZero(result); @@ -957,7 +959,8 @@ function _SimpleMultiply(result, leftOperand, rightOperand, roundingMode, powerM var error = CPCalculationNoError, carry = 0, exp = 0, - n = CPDecimalMakeZero(); + n = CPDecimalMakeZero(), + error1; _CPDecimalSetZero(result); diff --git a/Foundation/CPFunctionOperation.j b/Foundation/CPFunctionOperation.j index 34784374f..8232e78aa 100644 --- a/Foundation/CPFunctionOperation.j +++ b/Foundation/CPFunctionOperation.j @@ -79,7 +79,7 @@ */ + (id)functionOperationWithFunction:(JSObject)jsFunction { - functionOp = [[CPFunctionOperation alloc] init]; + var functionOp = [[CPFunctionOperation alloc] init]; [functionOp addExecutionFunction:jsFunction]; return functionOp; diff --git a/Foundation/CPRunLoop.j b/Foundation/CPRunLoop.j index 6867a4d8d..edcc1403e 100644 --- a/Foundation/CPRunLoop.j +++ b/Foundation/CPRunLoop.j @@ -177,7 +177,8 @@ var _CPRunLoopPerformPool = [], @end -var CPRunLoopLastNativeRunLoop = 0; +var CPRunLoopLastNativeRunLoop = 0, + CPMainRunLoop = nil; /*! @class CPRunLoop diff --git a/Foundation/CPUndoManager.j b/Foundation/CPUndoManager.j index cfcc54b21..ed4affa4e 100644 --- a/Foundation/CPUndoManager.j +++ b/Foundation/CPUndoManager.j @@ -347,8 +347,12 @@ if (_currentGroup == nil) */ - (void)undo { + // Closing the grouping here also cancels the run loop's scheduled close of the event grouping. if ([self groupingLevel] === 1) + { [self endUndoGrouping]; + [self _unregisterWithRunLoop]; + } [self undoNestedGroup]; } diff --git a/Foundation/CPWebDAVManager.j b/Foundation/CPWebDAVManager.j index b761d4eea..3a30fadfe 100644 --- a/Foundation/CPWebDAVManager.j +++ b/Foundation/CPWebDAVManager.j @@ -179,16 +179,6 @@ WebDAVPropertiesForURLKeys[CPURLIsDirectoryKey] = @"resourcetype"; var XMLDocumentFromString = function(anXMLString) { - if (typeof window["ActiveXObject"] !== "undefined") - { - var XMLDocument = new ActiveXObject("Microsoft.XMLDOM"); - - XMLDocument.async = false; - XMLDocument.loadXML(anXMLString); - - return XMLDocument; - } - return new DOMParser().parseFromString(anXMLString,"text/xml"); }; diff --git a/Foundation/Jakefile b/Foundation/Jakefile index 737969803..19ed1ed2c 100644 --- a/Foundation/Jakefile +++ b/Foundation/Jakefile @@ -53,8 +53,6 @@ foundationTask = framework ("Foundation", function(foundationTask) return "--include \"" + aFilename + "\""; }).join(" "); - INCLUDES = "--include \"../AppKit/Platform/Platform.h\" " + INCLUDES; - if ($CONFIGURATION === "Release") foundationTask.setCompilerFlags("-O2 -Wno-unused-but-set-variable " + INCLUDES); else diff --git a/Foundation/_CPFoundationUtilities.j b/Foundation/_CPFoundationUtilities.j index 0d44f09a9..294b24585 100644 --- a/Foundation/_CPFoundationUtilities.j +++ b/Foundation/_CPFoundationUtilities.j @@ -20,8 +20,6 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ -//#define _IS_NUMERIC(n) (!isNaN(parseFloat(n)) && isFinite(n)) - /* Objective-J is a strict superset of JavaScript and compiles down to a shared global runtime scope. This file (_CPFoundationUtilities.j) is the canonical place for otherwise "homeless" low-level diff --git a/Info.plist b/Info.plist new file mode 100644 index 000000000..6370adb81 --- /dev/null +++ b/Info.plist @@ -0,0 +1,29 @@ + + + + + CPBundleIdentifier + com.280n.Cappuccino + CPBundleInfoDictionaryVersion + 6.0 + CPBundleName + Cappuccino + CPBundlePackageType + CPKG + CPBundleVersion + 1.3.1 + CPBuildRuntime + Objective-J + CPBuildFrameworks + + Foundation + AppKit + AppKit/Themes/BlendKit + + CPBuildThemes + + AppKit/Themes/Aristo + AppKit/Themes/Aristo2 + + + diff --git a/Jake/environment.js b/Jake/environment.js index 4598ec9ee..03e8a30ce 100644 --- a/Jake/environment.js +++ b/Jake/environment.js @@ -59,9 +59,7 @@ Environment.prototype.spritesImages = function() exports.Environment = Environment; exports.ObjJ = new Environment("ObjJ"); var CommonJS = new Environment("CommonJS"); -CommonJS.setCompilerFlags(["-DPLATFORM_COMMONJS"]); exports.CommonJS = CommonJS; var Browser = new Environment("Browser"); -Browser.setCompilerFlags(["-DPLATFORM_BROWSER", "-DPLATFORM_DOM"]); Browser.setSpritesImages(true); exports.Browser = Browser; diff --git a/Objective-J/Bootstrap.js b/Objective-J/Bootstrap.js index 7f7be575f..a2477ceb8 100644 --- a/Objective-J/Bootstrap.js +++ b/Objective-J/Bootstrap.js @@ -70,6 +70,10 @@ if (typeof OBJJ_COMPILER_FLAGS !== 'undefined') case "SourceMap": flags.sourceMap = true; break; + + case "SourceMapIncludeSource": + flags.sourceMapIncludeSource = true; + break; } } FileExecutable.setCurrentCompilerFlags(flags); diff --git a/Objective-J/CFDictionary.js b/Objective-J/CFDictionary.js index f61412d37..3405f1382 100644 --- a/Objective-J/CFDictionary.js +++ b/Objective-J/CFDictionary.js @@ -171,6 +171,25 @@ CFDictionary.prototype.toString = function() DISPLAY_NAME(CFDictionary.prototype.toString); +CFDictionary.prototype.toJSObject = function() +{ + var jsObject = new Object(), + keys = this._keys, + index = 0, + count = this._count; + + for (; index < count; ++index) + { + var key = keys[index]; + + jsObject[key] = this.valueForKey(key); + } + + return jsObject; +}; + +DISPLAY_NAME(CFDictionary.prototype.toJSObject); + GLOBAL(CFMutableDictionary) = function(/*CFDictionary*/ aDictionary) { CFDictionary.apply(this, []); diff --git a/Objective-J/CPLog.js b/Objective-J/CPLog.js index 87e325cc7..c04bddcb0 100644 --- a/Objective-J/CPLog.js +++ b/Objective-J/CPLog.js @@ -192,6 +192,7 @@ GLOBAL(CPLogColorize) = function(aString, aLevel) GLOBAL(CPLogPrint) = function(aString, aLevel, aTitle, aFormatter) { + if (CPLogDisable) return; if (stream === undefined) { try { stream = require("narwhal/term").stream; diff --git a/Objective-J/CommonJS/lib/objective-j/jake/environment.js b/Objective-J/CommonJS/lib/objective-j/jake/environment.js index 9627ba95d..50e3aac1d 100644 --- a/Objective-J/CommonJS/lib/objective-j/jake/environment.js +++ b/Objective-J/CommonJS/lib/objective-j/jake/environment.js @@ -51,13 +51,10 @@ exports.ObjJ = new Environment("ObjJ"); var CommonJS = new Environment("CommonJS"); -CommonJS.setCompilerFlags(["-DPLATFORM_COMMONJS"]); - exports.CommonJS = CommonJS; var Browser = new Environment("Browser"); -Browser.setCompilerFlags(["-DPLATFORM_BROWSER", "-DPLATFORM_DOM"]); Browser.setSpritesImages(true); exports.Browser = Browser; diff --git a/Objective-J/Executable.js b/Objective-J/Executable.js index 17dd66a02..1e6636541 100644 --- a/Objective-J/Executable.js +++ b/Objective-J/Executable.js @@ -219,6 +219,8 @@ Executable.prototype.setCode = function(code, sourceMap) { this._code = code; + if (code == null) return; + var parameters = this.functionParameters().join(","); var sourceMapBase64; diff --git a/Tests/AppKit/CGContextTest.j b/Tests/AppKit/CGContextTest.j index b07987a17..a8a2bd4c6 100644 --- a/Tests/AppKit/CGContextTest.j +++ b/Tests/AppKit/CGContextTest.j @@ -19,7 +19,6 @@ //{ [self assert:YES equals:!CPFeatureIsCompatible(CPHTMLCanvasFeature)]; //} - [self assert:YES equals:!CPFeatureIsCompatible(CPVMLFeature)]; } - (void)testGStateCreate diff --git a/Tests/AppKit/CPApplicationWindowsMenuTest.j b/Tests/AppKit/CPApplicationWindowsMenuTest.j new file mode 100644 index 000000000..07529fab8 --- /dev/null +++ b/Tests/AppKit/CPApplicationWindowsMenuTest.j @@ -0,0 +1,172 @@ +@import +@import +@import +@import + +@implementation CPApplicationWindowsMenuTest : OJTestCase +{ + CPApplication app; + CPMenu windowsMenu; +} + +- (void)setUp +{ + app = [[CPApplication alloc] init]; + + // A main menu whose Window submenu carries the name nib2cib gives the Windows menu. + var mainMenu = [[CPMenu alloc] initWithTitle:@"Main"], + windowsMenuItem = [[CPMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; + + windowsMenu = [[CPMenu alloc] initWithTitle:@"Window"]; + [windowsMenu _setMenuName:@"_CPWindowsMenu"]; + [windowsMenu addItemWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; + + [windowsMenuItem setSubmenu:windowsMenu]; + [mainMenu addItem:windowsMenuItem]; + [app setMainMenu:mainMenu]; +} + +- (void)tearDown +{ + CPApp = nil; +} + +- (CPWindow)visibleWindowWithTitle:(CPString)aTitle +{ + var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 200.0, 100.0) styleMask:CPTitledWindowMask]; + + // There is no DOM in the test runner, so ordering a window in does not make it visible. + aWindow._isVisible = YES; + [aWindow setTitle:aTitle]; + + return aWindow; +} + +- (void)testWindowsMenuIsFoundInMainMenu +{ + [self assert:windowsMenu equals:[app windowsMenu]]; +} + +- (void)testVisibleTitledWindowIsListedBelowSeparator +{ + var aWindow = [self visibleWindowWithTitle:@"Report"]; + + [self assert:3 equals:[windowsMenu numberOfItems]]; + [self assertTrue:[[windowsMenu itemAtIndex:1] isSeparatorItem] message:@"a separator should precede the window list"]; + + var item = [windowsMenu itemAtIndex:2]; + + [self assert:@"Report" equals:[item title]]; + [self assert:aWindow equals:[item target]]; + [self assert:@selector(makeKeyAndOrderFront:) equals:[item action]]; +} + +- (void)testRetitlingWindowChangesItsItem +{ + var aWindow = [self visibleWindowWithTitle:@"Report"]; + + [aWindow setTitle:@"Final Report"]; + + [self assert:3 equals:[windowsMenu numberOfItems]]; + [self assert:@"Final Report" equals:[[windowsMenu itemAtIndex:2] title]]; +} + +- (void)testUntitledWindowIsNotListed +{ + [self visibleWindowWithTitle:@""]; + + [self assert:1 equals:[windowsMenu numberOfItems]]; +} + +- (void)testExcludedWindowIsNotListed +{ + var aWindow = [self visibleWindowWithTitle:@"Report"]; + + [aWindow setExcludedFromWindowsMenu:YES]; + + [self assert:1 equals:[windowsMenu numberOfItems]]; + + [aWindow setExcludedFromWindowsMenu:NO]; + + [self assert:3 equals:[windowsMenu numberOfItems]]; +} + +- (void)testPanelIsNotListed +{ + var panel = [[CPPanel alloc] initWithContentRect:CGRectMake(0.0, 0.0, 200.0, 100.0) styleMask:CPTitledWindowMask]; + + panel._isVisible = YES; + [panel setTitle:@"Inspector"]; + + [self assert:1 equals:[windowsMenu numberOfItems]]; +} + +- (void)testMainWindowIsChecked +{ + var firstWindow = [self visibleWindowWithTitle:@"First"], + secondWindow = [self visibleWindowWithTitle:@"Second"]; + + [firstWindow makeMainWindow]; + + [self assert:CPOnState equals:[[windowsMenu itemAtIndex:2] state]]; + [self assert:CPOffState equals:[[windowsMenu itemAtIndex:3] state]]; + + [secondWindow makeMainWindow]; + + [self assert:CPOffState equals:[[windowsMenu itemAtIndex:2] state]]; + [self assert:CPOnState equals:[[windowsMenu itemAtIndex:3] state]]; +} + +- (void)testClosingLastWindowRemovesItemAndSeparator +{ + var aWindow = [self visibleWindowWithTitle:@"Report"]; + + [aWindow close]; + + [self assert:1 equals:[windowsMenu numberOfItems]]; + [self assert:@"Minimize" equals:[[windowsMenu itemAtIndex:0] title]]; +} + +- (void)testClosingOneWindowKeepsSeparatorForOthers +{ + var firstWindow = [self visibleWindowWithTitle:@"First"]; + + [self visibleWindowWithTitle:@"Second"]; + [firstWindow close]; + + [self assert:3 equals:[windowsMenu numberOfItems]]; + [self assert:@"Second" equals:[[windowsMenu itemAtIndex:2] title]]; +} + +- (void)testFilenameItemShowsNameThenDirectory +{ + var aWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 200.0, 100.0) styleMask:CPTitledWindowMask]; + + [app addWindowsItem:aWindow title:@"/Users/me/Report.txt" filename:YES]; + + [self assert:@"Report.txt — /Users/me" equals:[[windowsMenu itemAtIndex:2] title]]; +} + +- (void)testAddingListedWindowAgainDoesNothing +{ + var aWindow = [self visibleWindowWithTitle:@"Report"]; + + [app addWindowsItem:aWindow title:@"Other" filename:NO]; + + [self assert:3 equals:[windowsMenu numberOfItems]]; + [self assert:@"Report" equals:[[windowsMenu itemAtIndex:2] title]]; +} + +- (void)testSetWindowsMenuMovesWindowItems +{ + var aWindow = [self visibleWindowWithTitle:@"Report"], + otherMenu = [[CPMenu alloc] initWithTitle:@"Windows"]; + + [app setWindowsMenu:otherMenu]; + + [self assert:1 equals:[windowsMenu numberOfItems]]; + [self assert:1 equals:[otherMenu numberOfItems]]; + [self assert:aWindow equals:[[otherMenu itemAtIndex:0] target]]; +} + +@end diff --git a/Tests/AppKit/CPCompatibilityTest.j b/Tests/AppKit/CPCompatibilityTest.j new file mode 100644 index 000000000..af228c0e1 --- /dev/null +++ b/Tests/AppKit/CPCompatibilityTest.j @@ -0,0 +1,65 @@ +@import +@import + +var SafariUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0.1 Safari/605.1.15"; + +@implementation CPCompatibilityTest : OJTestCase + +/*! + When CPCompatibility.j loads, it identifies the browser engine from the user + agent string. For WebKit and Blink browsers (user agent containing + "AppleWebKit/"), that detection calls +[CPPlatform isBrowser], which reads + CPDOMAvailable, so CPDOMAvailable must already be assigned at that point. + + The test suite runs outside a browser, with no user agent, so this code is + never reached when the frameworks load for testing. The test reproduces a + WebKit or Blink browser load instead: it supplies an AppleWebKit user agent, + removes CPDOMAvailable, and runs CPCompatibility.j again. +*/ +- (void)testLoadUnderWebKitUserAgentDoesNotReadCPDOMAvailableBeforeAssignment +{ + var executable = compatibilityExecutable(), + runtimeWindow = ObjectiveJ.window, + savedNavigator = runtimeWindow.navigator, + savedDOMAvailable = CPDOMAvailable, + loadException = nil; + + [self assertNotNull:executable message:@"CPCompatibility.j executable not found"]; + + runtimeWindow.navigator = { userAgent: SafariUserAgent }; + delete global.CPDOMAvailable; + + try + { + executable.execute(YES); + } + catch (anException) + { + loadException = anException; + } + + // Restore the Node environment and re-run detection so later tests see it. + if (savedNavigator === undefined) + delete runtimeWindow.navigator; + else + runtimeWindow.navigator = savedNavigator; + + CPDOMAvailable = savedDOMAvailable; + executable.execute(YES); + + if (loadException) + [self fail:@"CPCompatibility.j threw while loading under a WebKit user agent: " + loadException]; +} + +@end + +function compatibilityExecutable() +{ + var executables = ObjectiveJ.FileExecutable.allFileExecutables(); + + for (var i = 0; i < executables.length; i++) + if (/\/CPCompatibility\.j$/.test(executables[i].URL().absoluteString())) + return executables[i]; + + return nil; +} diff --git a/Tests/AppKit/CPDOMDisplayServerTest.j b/Tests/AppKit/CPDOMDisplayServerTest.j index 6d8be8e31..88bcfbdce 100644 --- a/Tests/AppKit/CPDOMDisplayServerTest.j +++ b/Tests/AppKit/CPDOMDisplayServerTest.j @@ -1,6 +1,6 @@ @import +@import -#include "../../AppKit/Platform/DOM/CPDOMDisplayServer.h" @implementation CPDOMDisplayServerTest : OJTestCase { diff --git a/Tests/AppKit/CPLevelIndicatorTest.j b/Tests/AppKit/CPLevelIndicatorTest.j index 08a84967d..2b2d7dff6 100644 --- a/Tests/AppKit/CPLevelIndicatorTest.j +++ b/Tests/AppKit/CPLevelIndicatorTest.j @@ -1,8 +1,6 @@ @import -// You should never do this. This is only for testing purposes. -#define GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, i) [(levelIndicator) layoutEphemeralSubviewNamed:@"segment-bezel-" + (i) positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] - +// You should never access a view's ephemeral layout subviews directly. This is only for testing purposes. @implementation CPLevelIndicatorTest : OJTestCase { } @@ -41,7 +39,7 @@ [levelIndicator setObjectValue:5]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor greenColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor greenColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } - (void)testLevelIndicatorWithLowWarningAndCriticalShouldShowWarningColor @@ -51,7 +49,7 @@ [levelIndicator setObjectValue:4]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor yellowColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor yellowColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } - (void)testLevelIndicatorWithLowWarningAndCriticalShouldShowCriticalColor @@ -61,7 +59,7 @@ [levelIndicator setObjectValue:2]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor redColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor redColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } - (void)testLevelIndicatorWithHighWarningAndCriticalShouldShowNormalColor @@ -71,7 +69,7 @@ [levelIndicator setObjectValue:5]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor greenColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor greenColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } - (void)testLevelIndicatorWithHighWarningAndCriticalShouldShowWarningColor @@ -81,7 +79,7 @@ [levelIndicator setObjectValue:6]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor yellowColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor yellowColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } - (void)testLevelIndicatorWithHighWarningAndCriticalShouldShowCriticalColor @@ -91,6 +89,6 @@ [levelIndicator setObjectValue:8]; [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; - [self assert:[CPColor redColor] equals:[GET_LEVEL_INDICATOR_SEGMENT(levelIndicator, 0) backgroundColor]]; + [self assert:[CPColor redColor] equals:[[levelIndicator layoutEphemeralSubviewNamed:@"segment-bezel-0" positioned:CPWindowAbove relativeToEphemeralSubviewNamed:"bezel"] backgroundColor]]; } @end diff --git a/Tests/AppKit/CPRulerViewTest.j b/Tests/AppKit/CPRulerViewTest.j new file mode 100644 index 000000000..330b7837b --- /dev/null +++ b/Tests/AppKit/CPRulerViewTest.j @@ -0,0 +1,107 @@ +@import +@import +@import +@import + +@implementation CPRulerViewTest : OJTestCase +{ + CPScrollView scrollView; + CPRulerView ruler; +} + +- (void)setUp +{ + [[CPApplication alloc] init]; + + scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)]; + [scrollView setHasHorizontalRuler:YES]; + + ruler = [scrollView horizontalRulerView]; +} + +- (id)roundTrip:(id)anObject +{ + return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]]; +} + +- (void)testRequiredThicknessCombinesTheRulerAreas +{ + [ruler setRuleThickness:16.0]; + [ruler setReservedThicknessForMarkers:9.0]; + [ruler setReservedThicknessForAccessoryView:5.0]; + + [self assert:30.0 equals:[ruler requiredThickness]]; + [self assert:14.0 equals:[ruler baselineLocation]]; +} + +- (void)testMeasurementUnitsAcceptOnlyRegisteredUnits +{ + [ruler setMeasurementUnits:CPRulerViewUnitPoints]; + [self assert:CPRulerViewUnitPoints equals:[ruler measurementUnits]]; + + [ruler setMeasurementUnits:@"Furlongs"]; + [self assert:CPRulerViewUnitPoints equals:[ruler measurementUnits]]; + + [CPRulerView registerUnitWithName:@"Furlongs" abbreviation:@"fur" unitToPointsConversionFactor:570240.0 stepUpCycle:[2.0] stepDownCycle:[0.5]]; + [ruler setMeasurementUnits:@"Furlongs"]; + [self assert:@"Furlongs" equals:[ruler measurementUnits]]; +} + +- (void)testRulerViewArchiving +{ + var marker = [[CPRulerMarker alloc] initWithRulerView:ruler markerLocation:42.0 image:nil imageOrigin:CGPointMake(4.0, 8.0)]; + + [marker setRemovable:YES]; + [marker setRepresentedObject:@"Tab"]; + + [ruler setRuleThickness:18.0]; + [ruler setReservedThicknessForMarkers:9.0]; + [ruler setOriginOffset:7.0]; + [ruler setMeasurementUnits:CPRulerViewUnitPicas]; + [ruler setMarkers:[marker]]; + + var decodedScrollView = [self roundTrip:scrollView], + decodedRuler = [decodedScrollView horizontalRulerView], + decodedMarker = [[decodedRuler markers] firstObject]; + + [self assert:decodedScrollView equals:[decodedRuler scrollView] message:@"the scroll view should reconnect its decoded ruler"]; + [self assert:CPHorizontalRuler equals:[decodedRuler orientation]]; + [self assert:18.0 equals:[decodedRuler ruleThickness]]; + [self assert:9.0 equals:[decodedRuler reservedThicknessForMarkers]]; + [self assert:7.0 equals:[decodedRuler originOffset]]; + [self assert:CPRulerViewUnitPicas equals:[decodedRuler measurementUnits]]; + + [self assert:42.0 equals:[decodedMarker markerLocation]]; + [self assert:4.0 equals:[decodedMarker imageOrigin].x]; + [self assert:8.0 equals:[decodedMarker imageOrigin].y]; + [self assertTrue:[decodedMarker isMovable]]; + [self assertTrue:[decodedMarker isRemovable]]; + [self assert:@"Tab" equals:[decodedMarker representedObject]]; + [self assert:decodedRuler equals:[decodedMarker ruler]]; +} + +- (void)testTextViewUsesRulerArchiving +{ + var textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)]; + + [self assertTrue:[textView usesRuler] message:@"a text view uses a ruler by default"]; + + [textView setUsesRuler:NO]; + [self assertFalse:[[self roundTrip:textView] usesRuler]]; + + [textView setUsesRuler:YES]; + [self assertTrue:[[self roundTrip:textView] usesRuler]]; +} + +- (void)testTextViewWithoutRulerCannotShowIt +{ + var textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 200.0)]; + + [scrollView setDocumentView:textView]; + [textView setUsesRuler:NO]; + [textView setRulerVisible:YES]; + + [self assertFalse:[textView isRulerVisible]]; +} + +@end diff --git a/Tests/AppKit/CPScrollViewTest.j b/Tests/AppKit/CPScrollViewTest.j index cbae14571..dee0e522f 100644 --- a/Tests/AppKit/CPScrollViewTest.j +++ b/Tests/AppKit/CPScrollViewTest.j @@ -394,5 +394,48 @@ [self assertSize:expected.size equals:actual.size message:message] } +- (void)testFrameSizeForContentSizeUsesScrollerStyleForBothScrollers +{ + var frameSize = [CPScrollView frameSizeForContentSize:CGSizeMake(100.0, 100.0) + horizontalScrollerClass:[_CPScrollViewTestScroller class] + verticalScrollerClass:[_CPScrollViewTestScroller class] + borderType:CPNoBorder + controlSize:CPRegularControlSize + scrollerStyle:CPScrollerStyleOverlay]; + + [self assertSize:CGSizeMake(105.0, 105.0) equals:frameSize message:@"both scroller widths come from scrollerWidthInStyle:"]; +} + +- (void)testContentSizeForFrameSizeUsesScrollerStyleForBothScrollers +{ + var contentSize = [CPScrollView contentSizeForFrameSize:CGSizeMake(100.0, 100.0) + horizontalScrollerClass:[_CPScrollViewTestScroller class] + verticalScrollerClass:[_CPScrollViewTestScroller class] + borderType:CPNoBorder + controlSize:CPRegularControlSize + scrollerStyle:CPScrollerStyleOverlay]; + + [self assertSize:CGSizeMake(95.0, 95.0) equals:contentSize message:@"both scroller widths come from scrollerWidthInStyle:"]; +} + @end + +/*! + A scroller whose widths differ by style, and whose control-size width differs + from both, so a size calculation that uses the wrong method is detectable + regardless of the theme. +*/ +@implementation _CPScrollViewTestScroller : CPScroller + ++ (float)scrollerWidthInStyle:(CPScrollerStyle)aStyle +{ + return aStyle === CPScrollerStyleOverlay ? 5.0 : 20.0; +} + ++ (float)scrollerWidthForControlSize:(CPControlSize)aControlSize +{ + return 99.0; +} + +@end diff --git a/Tests/AppKit/CPTextAttachmentTest.j b/Tests/AppKit/CPTextAttachmentTest.j new file mode 100644 index 000000000..0f9ab6079 --- /dev/null +++ b/Tests/AppKit/CPTextAttachmentTest.j @@ -0,0 +1,118 @@ +@import +@import +@import + +@implementation CPTextAttachmentTest : OJTestCase +{ + CPWindow theWindow; + CPTextView textView; +} + +- (void)setUp +{ + [[CPApplication alloc] init]; + + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 1024.0, 768.0) styleMask:CPWindowNotSizable]; + textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)]; + + [textView setRichText:YES]; + [[theWindow contentView] addSubview:textView]; + [theWindow makeFirstResponder:textView]; +} + +/* + Returns an attachment with a fixed size and no image, so that no image needs to load. +*/ +- (CPTextAttachment)attachmentWithSize:(CGSize)aSize +{ + var attachment = [[CPTextAttachment alloc] initWithData:nil ofType:@"image/png"]; + + [attachment setBounds:CGRectMake(0.0, 0.0, aSize.width, aSize.height)]; + + return attachment; +} + +- (void)testAttachmentWithDataURL +{ + var attachment = [CPTextAttachment _attachmentWithDataURL:@"data:image/gif;base64,R0lGODlhAQABAAAAACw="]; + + [self assert:@"image/gif" equals:[attachment fileType]]; + [self assert:@"R0lGODlhAQABAAAAACw=" equals:[[attachment contents] base64]]; + [self assertNull:[CPTextAttachment _attachmentWithDataURL:@"not a data URL"]]; +} + +- (void)testArchiving +{ + var attachment = [[CPTextAttachment alloc] initWithData:[CPData dataWithBase64:@"R0lGODlhAQABAAAAACw="] ofType:@"public.png"]; + + [attachment setBounds:CGRectMake(0.0, 0.0, 40.0, 30.0)]; + + var decoded = [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:attachment]]; + + [self assert:@"R0lGODlhAQABAAAAACw=" equals:[[decoded contents] base64]]; + [self assert:@"public.png" equals:[decoded fileType]]; + [self assert:40.0 equals:[decoded bounds].size.width]; + [self assert:@"image/png" equals:[decoded _MIMEType]]; +} + +- (void)testDisplaySizeFitsTheMaximumWidth +{ + var attachment = [self attachmentWithSize:CGSizeMake(400.0, 200.0)], + view = [attachment _attachmentView]; + + [self assert:400.0 equals:[view frameSize].width]; + + [self assertTrue:[attachment _setMaximumWidth:100.0]]; + [self assert:100.0 equals:[view frameSize].width]; + [self assert:50.0 equals:[view frameSize].height message:@"the image keeps its proportions"]; + + [self assertTrue:[attachment _setMaximumWidth:1000.0]]; + [self assert:400.0 equals:[view frameSize].width message:@"an image is not enlarged beyond its size"]; +} + +- (void)testAttributedStringWithAttachment +{ + var attachment = [self attachmentWithSize:CGSizeMake(20.0, 10.0)], + string = [CPTextStorage attributedStringWithAttachment:attachment], + view = [[CPView alloc] initWithFrame:CGRectMake(0.0, 0.0, 10.0, 10.0)], + viewString = [CPTextStorage attributedStringWithAttachment:view]; + + [self assert:_CPAttachmentCharacterAsString equals:[string string]]; + [self assert:attachment equals:[string attribute:CPAttachmentAttributeName atIndex:0 effectiveRange:nil]]; + [self assert:[attachment _attachmentView] equals:[string attribute:_CPAttachmentView atIndex:0 effectiveRange:nil]]; + + [self assert:view equals:[viewString attribute:_CPAttachmentView atIndex:0 effectiveRange:nil] message:@"a view is still inserted directly"]; + [self assertNull:[viewString attribute:CPAttachmentAttributeName atIndex:0 effectiveRange:nil]]; +} + +- (void)testInsertedAttachmentFitsTheText +{ + var attachment = [self attachmentWithSize:CGSizeMake(1000.0, 500.0)]; + + [textView setString:@"ab"]; + [textView setSelectedRange:CPMakeRange(1, 0)]; + [textView insertText:[CPTextStorage attributedStringWithAttachment:attachment]]; + + var maximumWidth = [textView _textAttachmentMaximumWidth]; + + [self assert:@"a" + _CPAttachmentCharacterAsString + @"b" equals:[textView string]]; + [self assert:maximumWidth equals:[[attachment _attachmentView] frameSize].width]; + [self assert:1 equals:[textView _indexOfTextAttachment:attachment]]; +} + +- (void)testArchivedTextHasNoAttachmentViews +{ + var attachment = [self attachmentWithSize:CGSizeMake(20.0, 10.0)], + archived = [textView _attributedStringForArchiving:[CPTextStorage attributedStringWithAttachment:attachment]]; + + [self assert:attachment equals:[archived attribute:CPAttachmentAttributeName atIndex:0 effectiveRange:nil]]; + [self assertNull:[archived attribute:_CPAttachmentView atIndex:0 effectiveRange:nil]]; + + // Inserting the text gives the attachment its view again. + [textView setString:@""]; + [textView insertText:archived]; + + [self assert:[attachment _attachmentView] equals:[[textView textStorage] attribute:_CPAttachmentView atIndex:0 effectiveRange:nil]]; +} + +@end diff --git a/Tests/AppKit/CPTextListTest.j b/Tests/AppKit/CPTextListTest.j new file mode 100644 index 000000000..c70ac9d1c --- /dev/null +++ b/Tests/AppKit/CPTextListTest.j @@ -0,0 +1,238 @@ +@import +@import +@import + +@implementation CPTextListTest : OJTestCase +{ + CPWindow theWindow; + CPTextView textView; +} + +- (void)setUp +{ + [[CPApplication alloc] init]; + + theWindow = [[CPWindow alloc] initWithContentRect:CGRectMake(0.0, 0.0, 1024.0, 768.0) styleMask:CPWindowNotSizable]; + textView = [[CPTextView alloc] initWithFrame:CGRectMake(0.0, 0.0, 300.0, 300.0)]; + + [textView setRichText:YES]; + [[theWindow contentView] addSubview:textView]; + [theWindow makeFirstResponder:textView]; +} + +- (id)roundTrip:(id)anObject +{ + return [CPKeyedUnarchiver unarchiveObjectWithData:[CPKeyedArchiver archivedDataWithRootObject:anObject]]; +} + +// MARK: CPTextList + +- (void)testBulletMarkers +{ + [self assert:@"•" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0] markerForItemNumber:3]]; + [self assert:@"◦" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerCircle options:0] markerForItemNumber:1]]; + [self assert:@"⁃" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerHyphen options:0] markerForItemNumber:1]]; +} + +- (void)testOrderedMarkers +{ + [self assert:@"12." equals:[[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0] markerForItemNumber:12]]; + [self assert:@"(c)" equals:[[[CPTextList alloc] initWithMarkerFormat:@"({lower-alpha})" options:0] markerForItemNumber:3]]; + [self assert:@"AA" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseAlpha options:0] markerForItemNumber:27]]; + [self assert:@"xiv" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerLowercaseRoman options:0] markerForItemNumber:14]]; + [self assert:@"MCMXCIX" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseRoman options:0] markerForItemNumber:1999]]; + [self assert:@"FF" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerUppercaseHexadecimal options:0] markerForItemNumber:255]]; + [self assert:@"17" equals:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerOctal options:0] markerForItemNumber:15]]; +} + +- (void)testIsOrdered +{ + [self assertFalse:[[[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerSquare options:0] isOrdered]]; + [self assertTrue:[[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0] isOrdered]]; +} + +- (void)testTextListArchiving +{ + var list = [[CPTextList alloc] initWithMarkerFormat:@"{upper-roman}." options:CPTextListPrependEnclosingMarker startingItemNumber:4], + decodedList = [self roundTrip:list]; + + [self assert:@"{upper-roman}." equals:[decodedList markerFormat]]; + [self assert:CPTextListPrependEnclosingMarker equals:[decodedList listOptions]]; + [self assert:4 equals:[decodedList startingItemNumber]]; +} + +- (void)testParagraphStyleTextLists +{ + var list = [[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0], + paragraphStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + + [self assert:0 equals:[[paragraphStyle textLists] count]]; + + [paragraphStyle setTextLists:[list]]; + [self assert:list equals:[[[paragraphStyle copy] textLists] firstObject]]; + [self assertFalse:[paragraphStyle isEqual:[CPParagraphStyle defaultParagraphStyle]] message:@"a paragraph style with a list differs from one without"]; + [self assert:@"{disc}" equals:[[[[self roundTrip:paragraphStyle] textLists] firstObject] markerFormat]]; +} + +// MARK: CPAttributedString + +- (void)testRangeAndItemNumberOfTextList +{ + var outer = [[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0], + inner = [[CPTextList alloc] initWithMarkerFormat:CPTextListMarkerDisc options:0], + outerStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy], + innerStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy], + string = [[CPAttributedString alloc] initWithString:@"Intro\none\ntwo\nnested\nthree\nEnd"]; + + [outerStyle setTextLists:[outer]]; + [innerStyle setTextLists:[outer, inner]]; + + [string addAttribute:CPParagraphStyleAttributeName value:outerStyle range:CPMakeRange(6, 8)]; + [string addAttribute:CPParagraphStyleAttributeName value:innerStyle range:CPMakeRange(14, 7)]; + [string addAttribute:CPParagraphStyleAttributeName value:outerStyle range:CPMakeRange(21, 6)]; + + var outerRange = [string rangeOfTextList:outer atIndex:15]; + + [self assert:6 equals:outerRange.location]; + [self assert:21 equals:outerRange.length]; + [self assert:CPNotFound equals:[string rangeOfTextList:outer atIndex:2].location]; + + [self assert:1 equals:[string itemNumberInTextList:outer atIndex:7]]; + [self assert:2 equals:[string itemNumberInTextList:outer atIndex:11]]; + [self assert:3 equals:[string itemNumberInTextList:outer atIndex:23] message:@"nested items do not count in the enclosing list"]; + [self assert:1 equals:[string itemNumberInTextList:inner atIndex:16]]; +} + +// MARK: CPTextView + +- (void)testSetTextListAddsAndRemovesMarkers +{ + [textView setString:@"one\ntwo"]; + [textView setSelectedRange:CPMakeRange(0, 7)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]]; + + [self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]]; + [self assert:36.0 equals:[[[textView textStorage] attribute:CPParagraphStyleAttributeName atIndex:0 effectiveRange:nil] headIndent]]; + + [textView setSelectedRange:CPMakeRange(0, [[textView string] length])]; + [textView setTextList:nil]; + + [self assert:@"one\ntwo" equals:[textView string]]; +} + +- (void)testNewlineContinuesAndEndsList +{ + [textView setString:@"one"]; + [textView setSelectedRange:CPMakeRange(3, 0)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]]; + + [textView setSelectedRange:CPMakeRange([[textView string] length], 0)]; + [textView insertNewline:nil]; + [textView insertText:@"two"]; + + [self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]]; + + [textView insertNewline:nil]; + [textView insertNewline:nil]; + + [self assert:@"\t1.\tone\n\t2.\ttwo\n" equals:[textView string] message:@"Return in an empty item ends the list"]; +} + +- (void)testTabNestsAndBacktabReturnsItem +{ + [textView setString:@"one\ntwo\nthree"]; + [textView setSelectedRange:CPMakeRange(0, 13)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]]; + + // The insertion point at the start of the second item's text. + var secondItemText = [[textView string] rangeOfString:@"two"].location; + + [textView setSelectedRange:CPMakeRange(secondItemText, 0)]; + [textView insertTab:nil]; + + [self assert:@"\t1.\tone\n\t1.\ttwo\n\t2.\tthree" equals:[textView string]]; + [self assert:2 equals:[[[[textView textStorage] attribute:CPParagraphStyleAttributeName atIndex:secondItemText effectiveRange:nil] textLists] count]]; + + [textView insertBacktab:nil]; + + [self assert:@"\t1.\tone\n\t2.\ttwo\n\t3.\tthree" equals:[textView string]]; +} + +- (void)testDeleteBackwardAtItemStartRemovesItem +{ + [textView setString:@"one\ntwo"]; + [textView setSelectedRange:CPMakeRange(0, 7)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]]; + + [textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"one"].location, 0)]; + [textView deleteBackward:nil]; + + [self assert:@"one\n\t1.\ttwo" equals:[textView string] message:@"the remaining item is numbered again"]; +} + +- (void)testPrependEnclosingMarker +{ + [textView setString:@"one\ntwo"]; + [textView setSelectedRange:CPMakeRange(0, 7)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:CPTextListPrependEnclosingMarker]]; + + [textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)]; + [textView insertTab:nil]; + + [self assert:@"\t1.\tone\n\t1.1.\ttwo" equals:[textView string]]; +} + +- (void)testPasteRulerAddsAndRemovesMarkers +{ + [textView setString:@"one\ntwo\nthree"]; + [textView setSelectedRange:CPMakeRange(0, 0)]; + [textView setTextList:[[CPTextList alloc] initWithMarkerFormat:@"{decimal}." options:0]]; + + // Copy the first item's ruler and paste it on the next paragraph, which joins the list. + [textView setSelectedRange:CPMakeRange(5, 0)]; + [textView copyRuler:nil]; + [textView setSelectedRange:CPMakeRange([[textView string] rangeOfString:@"two"].location, 0)]; + [textView pasteRuler:nil]; + + [self assert:@"\t1.\tone\n\t2.\ttwo\nthree" equals:[textView string]]; + + // Copy the plain paragraph's ruler and paste it on the first item, which leaves the list. + [textView setSelectedRange:CPMakeRange([[textView string] length], 0)]; + [textView copyRuler:nil]; + [textView setSelectedRange:CPMakeRange(5, 0)]; + [textView pasteRuler:nil]; + + [self assert:@"one\n\t1.\ttwo\nthree" equals:[textView string]]; +} + +- (void)testChangeTextListFromMenuItem +{ + var numberedItem = [[CPMenuItem alloc] initWithTitle:@"1. 2. 3." action:@selector(changeTextList:) keyEquivalent:@""], + noneItem = [[CPMenuItem alloc] initWithTitle:@"None" action:@selector(changeTextList:) keyEquivalent:@""]; + + [numberedItem setTag:10]; + + [textView setString:@"one\ntwo"]; + [textView setSelectedRange:CPMakeRange(0, 0)]; + + [self assertTrue:[textView validateUserInterfaceItem:noneItem]]; + [self assert:CPOnState equals:[noneItem state]]; + + [textView changeTextList:numberedItem]; + [self assert:@"\t1.\tone\ntwo" equals:[textView string]]; + + [textView validateUserInterfaceItem:numberedItem]; + [textView validateUserInterfaceItem:noneItem]; + [self assert:CPOnState equals:[numberedItem state]]; + [self assert:CPOffState equals:[noneItem state]]; + + // The next paragraph joins the list above it, so its numbering continues. + [textView setSelectedRange:CPMakeRange([[textView string] length], 0)]; + [textView changeTextList:numberedItem]; + [self assert:@"\t1.\tone\n\t2.\ttwo" equals:[textView string]]; + + [textView setRichText:NO]; + [self assertFalse:[textView validateUserInterfaceItem:numberedItem]]; +} + +@end diff --git a/Tests/AppKit/CPTextViewTest.j b/Tests/AppKit/CPTextViewTest.j index f198a5ed9..8af19a6ce 100644 --- a/Tests/AppKit/CPTextViewTest.j +++ b/Tests/AppKit/CPTextViewTest.j @@ -136,6 +136,236 @@ [delegateSpy verifyThatAllExpectationsHaveBeenMet]; } +- (void)testTextViewRespondsToUndoOnlyWhenUndoIsAllowed +{ + [textView setAllowsUndo:YES]; + + [self assertTrue:[textView respondsToSelector:@selector(undo:)]]; + [self assertTrue:[textView respondsToSelector:@selector(redo:)]]; + + [textView setAllowsUndo:NO]; + + [self assertFalse:[textView respondsToSelector:@selector(undo:)] message:@"undo: should continue to the window"]; + [self assertFalse:[textView respondsToSelector:@selector(redo:)] message:@"redo: should continue to the window"]; +} + +- (void)testTextViewRegistersUndoOnlyWhenUndoIsAllowed +{ + var undoManager = [theWindow undoManager]; + + [self assertTrue:[theWindow makeFirstResponder:textView]]; + + [textView setAllowsUndo:NO]; + [undoManager removeAllActions]; + [textView insertText:@"Not recorded"]; + + [self assertFalse:[undoManager canUndo] message:@"a text view that does not allow undo should register nothing"]; + + [textView setAllowsUndo:YES]; + [textView insertText:@"Recorded"]; + + [self assertTrue:[undoManager canUndo] message:@"a text view that allows undo should register its change"]; +} + +- (void)testSuperscriptSubscriptAndUnscript +{ + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [textView setSelectedRange:CPMakeRange(0, 2)]; + + [textView superscript:nil]; + [self assert:1 equals:[[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:0 effectiveRange:nil]]; + + [textView superscript:nil]; + [self assert:2 equals:[[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:1 effectiveRange:nil]]; + + [textView subscript:nil]; + [textView subscript:nil]; + [self assertTrue:([[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:0 effectiveRange:nil] == nil) message:@"a level of zero removes the attribute"]; + + [textView subscript:nil]; + [textView raiseBaseline:nil]; + [self assert:-1 equals:[[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:0 effectiveRange:nil]]; + [self assert:1 equals:[[textView textStorage] attribute:CPBaselineOffsetAttributeName atIndex:0 effectiveRange:nil]]; + + [textView unscript:nil]; + [self assertTrue:([[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:0 effectiveRange:nil] == nil)]; + [self assertTrue:([[textView textStorage] attribute:CPBaselineOffsetAttributeName atIndex:0 effectiveRange:nil] == nil)]; + + [self assertTrue:([[textView textStorage] attribute:CPSuperscriptAttributeName atIndex:5 effectiveRange:nil] == nil) message:@"text outside the selection is unchanged"]; +} + +- (void)testSuperscriptWithoutSelectionChangesTypingAttributes +{ + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [textView setSelectedRange:CPMakeRange(3, 0)]; + + [textView superscript:nil]; + [self assert:1 equals:[[textView typingAttributes] objectForKey:CPSuperscriptAttributeName]]; + + [textView unscript:nil]; + [self assertTrue:([[textView typingAttributes] objectForKey:CPSuperscriptAttributeName] == nil)]; +} + +- (void)testTypingAttributesComeFromTheCharacterBeforeTheInsertionPoint +{ + var font = [CPFont boldSystemFontOfSize:20.0], + length = [stringValue length]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttribute:CPFontAttributeName value:font range:CPMakeRange(length - 4, 4)]; + + [textView setSelectedRange:CPMakeRange(length, 0)]; + [self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"at the end of the text"]; + + [textView setSelectedRange:CPMakeRange(length - 4, 0)]; + [self assertFalse:[font cssString] === [[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"before the styled text"]; + + [textView setSelectedRange:CPMakeRange(length - 4, 2)]; + [self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString] message:@"for a selection"]; +} + +- (void)testPasteOfSystemClipboardText +{ + var font = [CPFont boldSystemFontOfSize:20.0], + length = [stringValue length]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttribute:CPFontAttributeName value:font range:CPMakeRange(0, 2)]; + + [textView setSelectedRange:CPMakeRange(0, 2)]; + [textView copy:nil]; + + // The system clipboard still holds the copied text, so the rich text is pasted. + [textView setSelectedRange:CPMakeRange(length, 0)]; + [textView _pasteSystemClipboardString:@"My" asPlainText:NO]; + + [self assert:stringValue + @"My" equals:[textView stringValue]]; + [self assert:[font cssString] equals:[[[textView textStorage] attribute:CPFontAttributeName atIndex:length effectiveRange:nil] cssString]]; + + // Text copied elsewhere replaces the internal pasteboard, and is pasted as plain text. + [textView _pasteSystemClipboardString:@"other" asPlainText:NO]; + + [self assert:stringValue + @"Myother" equals:[textView stringValue]]; + [self assert:@"other" equals:[[CPPasteboard generalPasteboard] stringForType:CPStringPboardType]]; + [self assertTrue:([[CPPasteboard generalPasteboard] stringForType:_CPASPboardType] == nil) message:@"the out of date rich text is removed"]; +} + +- (void)testRepeatedFontChangeWithoutSelectionAccumulates +{ + var biggerSender = [[_CPTextViewTestBiggerFontSender alloc] init], + startSize = [[textView font] size]; + + [textView setRichText:YES]; + [textView setString:@""]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [textView setSelectedRange:CPMakeRange(0, 0)]; + + [textView changeFont:biggerSender]; + [textView changeFont:biggerSender]; + [textView changeFont:biggerSender]; + + [self assert:startSize + 3.0 equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] size]]; +} + +- (void)testCopyAndPasteStyle +{ + var font = [CPFont boldSystemFontOfSize:20.0], + color = [CPColor redColor], + paragraphStyle = [[CPParagraphStyle defaultParagraphStyle] mutableCopy]; + + [paragraphStyle setAlignment:CPCenterTextAlignment]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttributes:@{CPFontAttributeName: font, CPForegroundColorAttributeName: color} range:CPMakeRange(0, 2)]; + [[textView textStorage] addAttribute:CPParagraphStyleAttributeName value:paragraphStyle range:CPMakeRange(3, 6)]; + + [textView setSelectedRange:CPMakeRange(0, 2)]; + [textView copyFont:nil]; + + [textView setSelectedRange:CPMakeRange(3, 6)]; + [textView pasteFont:nil]; + + var storage = [textView textStorage]; + + [self assert:[font cssString] equals:[[storage attribute:CPFontAttributeName atIndex:5 effectiveRange:nil] cssString]]; + [self assert:[color cssString] equals:[[storage attribute:CPForegroundColorAttributeName atIndex:5 effectiveRange:nil] cssString]]; + [self assert:CPCenterTextAlignment equals:[[storage attribute:CPParagraphStyleAttributeName atIndex:5 effectiveRange:nil] alignment] message:@"Paste Style keeps the paragraph style"]; + [self assertTrue:([storage attribute:CPForegroundColorAttributeName atIndex:9 effectiveRange:nil] == nil) message:@"Paste Style changes only the selection"]; +} + +- (void)testPasteStyleWithoutSelectionChangesTypingAttributes +{ + var font = [CPFont boldSystemFontOfSize:20.0]; + + [textView setRichText:YES]; + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [[textView textStorage] addAttribute:CPFontAttributeName value:font range:CPMakeRange(0, 2)]; + + [textView setSelectedRange:CPMakeRange(0, 2)]; + [textView copyFont:nil]; + + [textView setSelectedRange:CPMakeRange(5, 0)]; + [textView pasteFont:nil]; + + [self assert:[font cssString] equals:[[[textView typingAttributes] objectForKey:CPFontAttributeName] cssString]]; +} + +- (void)testStyleActionsAreEnabledOnlyInRichText +{ + var copyItem = [[CPMenuItem alloc] initWithTitle:@"Copy Style" action:@selector(copyFont:) keyEquivalent:@""], + pasteItem = [[CPMenuItem alloc] initWithTitle:@"Paste Style" action:@selector(pasteFont:) keyEquivalent:@""]; + + [textView setRichText:NO]; + [self assertFalse:[textView validateUserInterfaceItem:copyItem]]; + [self assertFalse:[textView validateUserInterfaceItem:pasteItem]]; + + [textView setRichText:YES]; + [textView copyFont:nil]; + [self assertTrue:[textView validateUserInterfaceItem:copyItem]]; + [self assertTrue:[textView validateUserInterfaceItem:pasteItem]]; + + [textView setEditable:NO]; + [self assertFalse:[textView validateUserInterfaceItem:pasteItem]]; +} + +- (void)testBaselineActionsAreEnabledOnlyInEditableRichText +{ + var item = [[CPMenuItem alloc] initWithTitle:@"Superscript" action:@selector(superscript:) keyEquivalent:@""]; + + [textView setRichText:NO]; + [self assertFalse:[textView validateUserInterfaceItem:item]]; + + [textView setRichText:YES]; + [self assertTrue:[textView validateUserInterfaceItem:item]]; + + [textView setEditable:NO]; + [self assertFalse:[textView validateUserInterfaceItem:item]]; +} + +- (void)testUndoItemIsEnabledOnlyWhenThereIsAChangeToUndo +{ + var undoManager = [theWindow undoManager], + undoItem = [[CPMenuItem alloc] initWithTitle:@"Undo" action:@selector(undo:) keyEquivalent:@""], + redoItem = [[CPMenuItem alloc] initWithTitle:@"Redo" action:@selector(redo:) keyEquivalent:@""]; + + [self assertTrue:[theWindow makeFirstResponder:textView]]; + [undoManager removeAllActions]; + + [self assertFalse:[textView validateUserInterfaceItem:undoItem]]; + [self assertFalse:[theWindow validateUserInterfaceItem:undoItem]]; + [self assertFalse:[textView validateUserInterfaceItem:redoItem]]; + + [textView insertText:@"Recorded"]; + + [self assertTrue:[textView validateUserInterfaceItem:undoItem]]; + [self assertTrue:[theWindow validateUserInterfaceItem:undoItem]]; +} + @end @implementation CPTextViewTest (CPTextViewTestDelegate) @@ -186,3 +416,15 @@ } @end + +/* + Converts a font as the font manager's Bigger action does, one point larger. +*/ +@implementation _CPTextViewTestBiggerFontSender : CPObject + +- (CPFont)convertFont:(CPFont)aFont +{ + return [[CPFontManager sharedFontManager] convertFont:aFont toSize:[aFont size] + 1.0]; +} + +@end diff --git a/Tests/Foundation/CPDecimalTest.j b/Tests/Foundation/CPDecimalTest.j index 2c17741c7..3326d628d 100644 --- a/Tests/Foundation/CPDecimalTest.j +++ b/Tests/Foundation/CPDecimalTest.j @@ -468,6 +468,12 @@ d1 = CPDecimalMakeWithString(@"1e127"); d2 = CPDecimalMakeWithString(@"99999999999999999999999999999999999999e127"); [self assert:CPDecimalAdd(d1, d1, d2, CPRoundPlain) equals:CPCalculationOverflow message:"CPDecimalAdd() Tao1: addition loss of precision"]; + + // Overflow of two negative operands is reported as underflow + d1 = CPDecimalMakeWithString(@"-1e127"); + d2 = CPDecimalMakeWithString(@"-99999999999999999999999999999999999999e127"); + [self assert:CPCalculationUnderflow equals:CPDecimalAdd(d1, d1, d2, CPRoundPlain) message:"CPDecimalAdd() Tao2: negative overflow is reported as underflow"]; + [self assert:YES equals:d1._isNegative message:"CPDecimalAdd() Tao2: sign"]; } - (void)testSubtract diff --git a/Tests/Foundation/CPUndoManagerTest.j b/Tests/Foundation/CPUndoManagerTest.j index 480d4a0ed..fd008635b 100644 --- a/Tests/Foundation/CPUndoManagerTest.j +++ b/Tests/Foundation/CPUndoManagerTest.j @@ -57,6 +57,26 @@ [[CPNotificationCenter defaultCenter] removeObserver:self]; } +- (void)testUndoInSameRunLoopCycleAsChange +{ + [self doIt]; + + // The change opened an event grouping that the run loop is scheduled to close. Undo closes it first. + [undoManager undo]; + + [self assertFalse:itIsDone message:@"undo should revert the change"]; + + // The scheduled close must not run against the grouping that undo already closed. + [[CPRunLoop currentRunLoop] limitDateForMode:CPDefaultRunLoopMode]; + + [self assert:0 equals:[undoManager groupingLevel]]; + [self assertTrue:[undoManager canRedo] message:@"the undone change should be available to redo"]; + + [undoManager redo]; + + [self assertTrue:itIsDone message:@"redo should reapply the change"]; +} + - (void)receiveNotification:(CPNotification)aNotification { [receivedNotifications addObject:aNotification]; @@ -74,7 +94,7 @@ - (void)undoIt { [undoManager registerUndoWithTarget:self - selector:@selector(doit) + selector:@selector(doIt) object:nil]; itIsDone = NO; diff --git a/Tests/Manual/CPAnimationContextTest/AppController.j b/Tests/Manual/CPAnimationContextTest/AppController.j index a9d657859..0bc88da82 100644 --- a/Tests/Manual/CPAnimationContextTest/AppController.j +++ b/Tests/Manual/CPAnimationContextTest/AppController.j @@ -11,8 +11,6 @@ @import @import -#define UIAssert(a) [self markTest:_cmd didPass:a]; - // A new custom view class to draw the animation path. @implementation PathView : CPView { @@ -300,7 +298,7 @@ [context setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; [CPAnimationContext endGrouping]; } @@ -315,7 +313,7 @@ [context setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; [CPAnimationContext endGrouping]; } @@ -336,7 +334,7 @@ [outerContext setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; // Inner group @@ -356,7 +354,7 @@ [context setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; [[sender animator] setObjectValue:[sender objectValue]]; @@ -373,7 +371,7 @@ [context setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; [[sender animator] setFrame:[sender frame]]; @@ -390,14 +388,14 @@ [context setCompletionHandler:function() { // This handler should be replaced and never called. - UIAssert(NO); + [self markTest:_cmd didPass:NO]; }]; // Set the handler again, which should replace the first one. [context setCompletionHandler:function() { [completions addObject:@"done"]; - UIAssert([completions isEqualToArray:@["done"]]); + [self markTest:_cmd didPass:[completions isEqualToArray:@["done"]]]; }]; [[sender animator] setFrame:[sender frame]]; diff --git a/Tests/Manual/Jakefile b/Tests/Manual/Jakefile index bc143690c..7ec31fb78 100644 --- a/Tests/Manual/Jakefile +++ b/Tests/Manual/Jakefile @@ -23,8 +23,13 @@ var ENV = process.env, task = JAKE.task, cp = require("child_process"), + fs = require("fs"), path = require("path"); +// The frameworks an application's Frameworks directory links to, in the layout +// capp gen creates: release builds directly inside, debug builds in Debug/. +var ConfiguredFrameworks = ["Objective-J", "Foundation", "AppKit", "BlendKit"]; + /* * Standard build targets are neutered at this directory level to prevent automated * CI runners from triggering exhaustive, recursive compilations across all manual test suites. @@ -47,17 +52,84 @@ task ("release", ["default"]); */ /* - * Traverses every manual test subdirectory containing a Jakefile and symlinks its - * internal 'Frameworks' directory directly to the concrete Build/Frameworks - * directory generated at the project root. + * Gives every manual test application the Frameworks layout capp gen creates: + * Frameworks/ links to the release build in Build/Release, and + * Frameworks/Debug links to the debug build in Build/Debug, so index.html loads + * release frameworks and index-debug.html loads debug frameworks. An existing + * Frameworks link, or a Frameworks directory holding only links, is replaced; + * a Frameworks directory holding real files is left untouched. */ task ("configure", function() { var manualDir = __dirname; - var targetPath = path.resolve(manualDir, "../../Build/Frameworks"); - cp.execSync("find . -mindepth 2 -iname 'jakefile' -execdir sh -c 'echo \"Configuring $(basename \\\"$PWD\\\")\"; ln -sfn \"$1\" Frameworks' _ '" + targetPath + "' \\;", { stdio: "inherit", cwd: manualDir }); + + provisionFrameworks( + manualDir, + path.resolve(manualDir, "../../Build/Release"), + path.resolve(manualDir, "../../Build/Debug"), + "build the frameworks in both configurations first" + ); }); +/* + * Gives every manual test application the same Frameworks layout as configure, + * linked to the capp-build package in Build/modern/Frameworks instead of the + * legacy builds. capp-build produces a single build, so Frameworks/Debug links + * to the same package. + */ +task ("configure-modern", function() +{ + var manualDir = __dirname, + modernPath = path.resolve(manualDir, "../../Build/modern/Frameworks"); + + provisionFrameworks( + manualDir, + modernPath, + modernPath, + "build the package with capp-build first (capp-build build . at the repository root)" + ); +}); + +/* + * Links Frameworks/ to releasePath/ and Frameworks/Debug to + * debugPath in every manual test application, after checking that both hold + * every framework. An existing Frameworks link, or a Frameworks directory + * holding only links, is replaced; one holding real files is left untouched. + */ +function provisionFrameworks(manualDir, releasePath, debugPath, missingHint) +{ + [releasePath, debugPath].forEach(function(buildPath) + { + ConfiguredFrameworks.forEach(function(framework) + { + if (!fs.existsSync(path.join(buildPath, framework))) + throw new Error(path.join(buildPath, framework) + " not found; " + missingHint); + }); + }); + + manualApplicationDirectories(manualDir).forEach(function(appDir) + { + var name = path.basename(appDir), + frameworksPath = path.join(appDir, "Frameworks"); + + if (!removeConfiguredFrameworks(appDir)) + { + console.log("Skipping " + name + ": Frameworks contains files that are not links"); + return; + } + + console.log("Configuring " + name); + fs.mkdirSync(frameworksPath); + + ConfiguredFrameworks.forEach(function(framework) + { + fs.symlinkSync(path.join(releasePath, framework), path.join(frameworksPath, framework)); + }); + + fs.symlinkSync(debugPath, path.join(frameworksPath, "Debug")); + }); +} + /* * Cleans the subsidiary applications' Build directories exclusively (-mindepth 2 -maxdepth 2). */ @@ -69,14 +141,91 @@ task ("clean", function() }); /* - * Sweeps Frameworks symlinks across subsidiary test applications after delegating Build directory removal to clean. + * Removes the Frameworks layout configure created from every manual test + * application, after delegating Build directory removal to clean. A Frameworks + * directory holding real files is left untouched. */ task ("clobber", ["clean"], function() { var manualDir = __dirname; - cp.execSync("find . -mindepth 2 -maxdepth 2 -type l -name 'Frameworks' -exec rm -f {} +", { stdio: "inherit", cwd: manualDir }); + + manualApplicationDirectories(manualDir).forEach(function(appDir) + { + if (!removeConfiguredFrameworks(appDir)) + console.log("Skipping " + path.basename(appDir) + ": Frameworks contains files that are not links"); + }); }); +/* + * Returns the absolute paths of the manual test applications: every directory + * below this one that contains a Jakefile. + */ +function manualApplicationDirectories(manualDir) +{ + var jakefiles = cp.execSync("find . -mindepth 2 -iname 'jakefile'", { cwd: manualDir, encoding: "utf8" }); + + return jakefiles.split("\n").filter(Boolean).map(function(jakefile) + { + return path.resolve(manualDir, path.dirname(jakefile)); + }); +} + +/* + * Removes an application's Frameworks link, or its Frameworks directory when + * that holds only links and directories, and returns true; returns false and + * leaves it in place when it holds any real file. Links are removed, never + * followed. + */ +function removeConfiguredFrameworks(appDir) +{ + var frameworksPath = path.join(appDir, "Frameworks"), + stat; + + try + { + stat = fs.lstatSync(frameworksPath); + } + catch (anError) + { + return true; + } + + if (stat.isSymbolicLink()) + { + fs.unlinkSync(frameworksPath); + return true; + } + + if (stat.isDirectory() && containsOnlyLinks(frameworksPath)) + { + fs.rmSync(frameworksPath, { recursive: true }); + return true; + } + + return false; +} + +/* + * Returns true when aPath is a directory tree holding only symbolic links and + * directories. + */ +function containsOnlyLinks(aPath) +{ + return fs.readdirSync(aPath).every(function(name) + { + var entry = path.join(aPath, name), + stat = fs.lstatSync(entry); + + if (stat.isSymbolicLink()) + return true; + + if (stat.isDirectory()) + return containsOnlyLinks(entry); + + return false; + }); +} + /* * ----------------------------------------------------------------------------- * Manual Execution Targets diff --git a/Tests/Manual/SmartFoldersDemo/AppController.j b/Tests/Manual/SmartFoldersDemo/AppController.j new file mode 100644 index 000000000..c1d8f9209 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/AppController.j @@ -0,0 +1,109 @@ +/* + * AppController.j + * SmartFoldersDemo + * + * Non-document-based Cappuccino integration test / demo, built entirely in + * code (no cib/xib). The application delegate only creates the model, the + * main menu and the main window controller; everything else lives in the + * view controllers. + * + * AppController + * └─ MainWindowController (CPWindowController, bridge window, toolbar) + * ├─ SourceListViewController (view-based CPOutlineView, badges, DnD target) + * ├─ PeopleViewController (bound CPTableView, CPArrayController, DnD source) + * │ └─ PersonDetailViewController (selection bindings) + * ├─ SmartFolderEditorController (sheet, CPPredicateEditor) + * └─ NameSheetController (sheet) + * + * Data (people, groups, smart folders) is persisted in CPUserDefaults. + * Use File ▸ Reset Library… to return to the deterministic sample data. + */ + +@import +@import + +@import "Library.j" +@import "MainWindowController.j" + +@implementation AppController : CPObject +{ + Library _library; + MainWindowController _mainWindowController; +} + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + _library = [[Library alloc] init]; + + [self _buildMainMenu]; + [CPMenu setMenuBarVisible:YES]; + + _mainWindowController = [[MainWindowController alloc] initWithLibrary:_library]; + [_mainWindowController showWindow:self]; +} + +// ---------------------------------------------------------------- Menu + +- (CPMenuItem)_addItem:(CPString)aTitle action:(SEL)anAction key:(CPString)aKey modifiers:(unsigned)aMask to:(CPMenu)aMenu +{ + var item = [aMenu addItemWithTitle:aTitle action:anAction keyEquivalent:aKey || @""]; + + if (aMask) + [item setKeyEquivalentModifierMask:aMask]; + + return item; +} + +- (CPMenu)_addSubmenu:(CPString)aTitle to:(CPMenu)aMainMenu +{ + var item = [aMainMenu addItemWithTitle:aTitle action:nil keyEquivalent:@""], + menu = [[CPMenu alloc] initWithTitle:aTitle]; + + [aMainMenu setSubmenu:menu forItem:item]; + + return menu; +} + +- (void)_buildMainMenu +{ + var mainMenu = [CPApp mainMenu]; + + while ([mainMenu numberOfItems] > 0) + [mainMenu removeItemAtIndex:0]; + + var commandOption = CPCommandKeyMask | CPAlternateKeyMask; + + // File + var fileMenu = [self _addSubmenu:@"File" to:mainMenu]; + [self _addItem:@"New Smart Folder…" action:@selector(newSmartFolder:) key:@"n" modifiers:commandOption to:fileMenu]; + [self _addItem:@"New Group…" action:@selector(newGroup:) key:@"g" modifiers:commandOption to:fileMenu]; + [self _addItem:@"New Person" action:@selector(newPerson:) key:@"p" modifiers:commandOption to:fileMenu]; + [fileMenu addItem:[CPMenuItem separatorItem]]; + [self _addItem:@"Edit Smart Folder…" action:@selector(editSelectedSmartFolder:) key:@"e" modifiers:commandOption to:fileMenu]; + [self _addItem:@"Rename Group…" action:@selector(renameSelectedGroup:) key:nil modifiers:0 to:fileMenu]; + [self _addItem:@"Delete Group or Smart Folder" action:@selector(deleteSelectedSourceItem:) key:nil modifiers:0 to:fileMenu]; + [fileMenu addItem:[CPMenuItem separatorItem]]; + [self _addItem:@"Reset Library…" action:@selector(resetLibrary:) key:nil modifiers:0 to:fileMenu]; + + // Edit + var editMenu = [self _addSubmenu:@"Edit" to:mainMenu]; + [self _addItem:@"Undo" action:@selector(undo:) key:@"z" modifiers:0 to:editMenu]; + [self _addItem:@"Redo" action:@selector(redo:) key:@"Z" modifiers:0 to:editMenu]; + [editMenu addItem:[CPMenuItem separatorItem]]; + [self _addItem:@"Cut" action:@selector(cut:) key:@"x" modifiers:0 to:editMenu]; + [self _addItem:@"Copy" action:@selector(copy:) key:@"c" modifiers:0 to:editMenu]; + [self _addItem:@"Paste" action:@selector(paste:) key:@"v" modifiers:0 to:editMenu]; + [self _addItem:@"Delete" action:@selector(delete:) key:nil modifiers:0 to:editMenu]; + [self _addItem:@"Select All" action:@selector(selectAll:) key:@"a" modifiers:0 to:editMenu]; + [editMenu addItem:[CPMenuItem separatorItem]]; + [self _addItem:@"Find" action:@selector(focusSearchField:) key:@"f" modifiers:0 to:editMenu]; + + // View + var viewMenu = [self _addSubmenu:@"View" to:mainMenu]; + [self _addItem:@"Hide Sidebar" action:@selector(toggleSidebar:) key:@"s" modifiers:CPCommandKeyMask | CPControlKeyMask to:viewMenu]; + [self _addItem:@"Hide Details" action:@selector(toggleDetail:) key:@"d" modifiers:CPCommandKeyMask | CPControlKeyMask to:viewMenu]; + [viewMenu addItem:[CPMenuItem separatorItem]]; + [self _addItem:@"Show All People" action:@selector(showAllPeople:) key:@"1" modifiers:0 to:viewMenu]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/Info.plist b/Tests/Manual/SmartFoldersDemo/Info.plist new file mode 100644 index 000000000..fba1328e4 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Info.plist @@ -0,0 +1,18 @@ + + + + + CPBundlePackageType + APPL + CPBundleIdentifier + com.yourorganization.SmartFoldersDemo + CPDefaultTheme + Aristo2 + CPApplicationDelegateClass + AppController + CPBundleName + SmartFolderDemo + CPPrincipalClass + CPApplication + + diff --git a/Tests/Manual/SmartFoldersDemo/Jakefile b/Tests/Manual/SmartFoldersDemo/Jakefile new file mode 100644 index 000000000..80b5b8cf0 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Jakefile @@ -0,0 +1,94 @@ +/* + * Jakefile + * SmartFoldersTest + * + * Created by You on November 25, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +var ENV = process.env, + cp = require("child_process"), + fs = require("fs"), + path = require("path"), + task = JAKE.task, + FileList = JAKE.FileList, + app = CAPPUCCINO.Jake.applicationtask.app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug"; + +app ("SmartFoldersTest", function(task) +{ + task.setBuildIntermediatesPath(path.join("Build", "SmartFoldersTest.build", configuration)); + task.setBuildPath(path.join("Build", configuration)); + + task.setProductName("SmartFoldersTest"); + task.setIdentifier("com.yourcompany."); + task.setVersion("1.0"); + task.setAuthor("Your Company"); + task.setEmail("feedback @nospam@ yourcompany.com"); + task.setSummary("SmartFoldersTest"); + task.setSources((new FileList("**/*.j")).exclude(path.join("Build", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + task.setNib2CibFlags("-R Resources/"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g"); + else + task.setCompilerFlags("-O"); +}); + +task ("default", ["SmartFoldersTest"], function() +{ + printResults(configuration); +}); + +task ("build", ["default"]); + +task ("debug", function() +{ + ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + cp.execSync(["open", path.join("Build", "Debug", "SmartFoldersTest", "index.html")].join(" "), { stdio: "inherit" }); +}); + +task ("run-release", ["release"], function() +{ + cp.execSync(["open", path.join("Build", "Release", "SmartFoldersTest", "index.html")].join(" "), { stdio: "inherit" }); +}); + +task ("deploy", ["release"], function() +{ + fs.mkdirSync(path.join("Build", "Deployment", "SmartFoldersTest"), { recursive: true }); + cp.execSync(["press", "-f", path.join("Build", "Release", "SmartFoldersTest"), path.join("Build", "Deployment", "SmartFoldersTest")].join(" "), { stdio: "inherit" }); + printResults("Deployment") +}); + +task ("desktop", ["release"], function() +{ + fs.mkdirSync(path.join("Build", "Desktop", "SmartFoldersTest"), { recursive: true }); + require("@objj/nativehost").buildNativeHost(path.join("Build", "Release", "SmartFoldersTest"), path.join("Build", "Desktop", "SmartFoldersTest", "SmartFoldersTest.app")); + printResults("Desktop") +}); + +task ("run-desktop", ["desktop"], function() +{ + cp.execSync([path.join("Build", "Desktop", "SmartFoldersTest", "SmartFoldersTest.app", "Contents", "MacOS", "NativeHost"), "-i"].join(" "), { stdio: "inherit" }); +}); + +function printResults(configuration) +{ + console.log("----------------------------"); + console.log(configuration+" app built at path: "+path.join("Build", configuration, "SmartFoldersTest")); + console.log("----------------------------"); +} diff --git a/Tests/Manual/SmartFoldersDemo/Library.j b/Tests/Manual/SmartFoldersDemo/Library.j new file mode 100644 index 000000000..b7ec8c682 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Library.j @@ -0,0 +1,787 @@ +/* + * Library.j + * SmartFoldersDemo + * + * The application model. Holds all people plus the three source list + * sections (Library, Groups, Smart Folders), persists itself to + * CPUserDefaults (local storage) and observes every Person via KVO so that + * edits made anywhere in the UI are noticed, saved and reflected in the + * smart folder badges. + * + * Change notifications: + * - structural changes (people added/removed, group membership, folders + * added/removed/renamed/reordered/edited) are posted synchronously; + * - property edits on a Person are coalesced and posted on the next run + * loop turn. + */ + +@import +@import "Person.j" + +LibraryDidChangeNotification = @"LibraryDidChangeNotification"; +LibraryChangeKindKey = @"kind"; +LibraryChangeStructural = @"structural"; +LibraryChangePersonEdited = @"personEdited"; + +PeopleDragType = @"SmartFoldersDemoPeopleDragType"; +SourceItemDragType = @"SmartFoldersDemoSourceItemDragType"; + +LibraryDepartments = [@"Engineering", @"Design", @"Marketing", @"Sales", @"Support", @"Finance", @"Legal"]; +LibraryCities = [@"Berlin", @"Hamburg", @"Munich", @"Zürich", @"Vienna", @"Paris", @"London", @"Lisbon", @"New York", @"San Francisco", @"Tokyo"]; + +var LibraryDefaultsKey = @"SmartFoldersDemo.Library.v1", + ObservedPersonKeys = [@"firstName", @"lastName", @"email", @"department", @"city", @"birthDate", @"salary", @"isManager"], + SourceItemUIDCounter = 0; + + +// -------------------------------------------------------------------------------- +// Source list items +// -------------------------------------------------------------------------------- + +@implementation SourceItem : CPObject +{ + CPString uid @accessors; + CPString name @accessors; +} + +- (id)initWithName:(CPString)aName +{ + if (self = [super init]) + { + uid = [self className] + "-" + (++SourceItemUIDCounter); + name = aName; + } + + return self; +} + +- (BOOL)isSection +{ + return NO; +} + +- (BOOL)isSelectable +{ + return YES; +} + +// Can be renamed, deleted and reordered by the user. +- (BOOL)isUserItem +{ + return NO; +} + +@end + + +@implementation SourceSection : SourceItem +{ + CPMutableArray children @accessors; +} + +- (id)initWithName:(CPString)aName +{ + if (self = [super initWithName:aName]) + children = []; + + return self; +} + +- (BOOL)isSection +{ + return YES; +} + +- (BOOL)isSelectable +{ + return NO; +} + +@end + + +@implementation AllPeopleItem : SourceItem +@end + + +@implementation LibraryGroup : SourceItem +{ + CPMutableArray memberIDs @accessors; +} + +- (id)initWithName:(CPString)aName memberIDs:(CPArray)theIDs +{ + if (self = [super initWithName:aName]) + memberIDs = [CPMutableArray arrayWithArray:theIDs || []]; + + return self; +} + +- (id)initWithDictionary:(CPDictionary)aDictionary +{ + return [self initWithName:[aDictionary objectForKey:@"name"] || @"Group" + memberIDs:[aDictionary objectForKey:@"members"] || []]; +} + +- (CPDictionary)dictionaryRepresentation +{ + return [CPDictionary dictionaryWithObjectsAndKeys:name, @"name", [memberIDs copy], @"members"]; +} + +- (BOOL)isUserItem +{ + return YES; +} + +@end + + +@implementation SmartFolder : SourceItem +{ + CPPredicate predicate @accessors; +} + +- (id)initWithName:(CPString)aName predicate:(CPPredicate)aPredicate +{ + if (self = [super initWithName:aName]) + predicate = aPredicate; + + return self; +} + +- (id)initWithDictionary:(CPDictionary)aDictionary +{ + var format = [aDictionary objectForKey:@"predicate"], + aPredicate = nil; + + try + { + if (format) + aPredicate = [CPPredicate predicateWithFormat:format]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: could not parse stored predicate '" + format + "': " + e); + } + + return [self initWithName:[aDictionary objectForKey:@"name"] || @"Smart Folder" + predicate:aPredicate || [CPPredicate predicateWithValue:YES]]; +} + +- (CPDictionary)dictionaryRepresentation +{ + return [CPDictionary dictionaryWithObjectsAndKeys:name, @"name", [predicate predicateFormat], @"predicate"]; +} + +- (BOOL)isUserItem +{ + return YES; +} + +@end + + +// -------------------------------------------------------------------------------- +// Library +// -------------------------------------------------------------------------------- + +@implementation Library : CPObject +{ + CPMutableArray _people; + Object _peopleByUID; + + SourceSection _librarySection; + SourceSection _groupsSection; + SourceSection _smartFoldersSection; + AllPeopleItem _allPeopleItem; + CPArray _sections; + + BOOL _editNotificationScheduled; + BOOL _saveScheduled; +} + +- (id)init +{ + if (self = [super init]) + { + _people = []; + _peopleByUID = {}; + + _allPeopleItem = [[AllPeopleItem alloc] initWithName:@"All People"]; + + _librarySection = [[SourceSection alloc] initWithName:@"LIBRARY"]; + [[_librarySection children] addObject:_allPeopleItem]; + + _groupsSection = [[SourceSection alloc] initWithName:@"GROUPS"]; + _smartFoldersSection = [[SourceSection alloc] initWithName:@"SMART FOLDERS"]; + + _sections = [_librarySection, _groupsSection, _smartFoldersSection]; + + if (![self _loadFromDefaults]) + [self _populateWithSampleData]; + } + + return self; +} + +// ---------------------------------------------------------------- Accessors + +- (CPArray)people +{ + return _people; +} + +- (CPArray)sections +{ + return _sections; +} + +- (AllPeopleItem)allPeopleItem +{ + return _allPeopleItem; +} + +- (SourceSection)groupsSection +{ + return _groupsSection; +} + +- (SourceSection)smartFoldersSection +{ + return _smartFoldersSection; +} + +- (CPArray)groups +{ + return [_groupsSection children]; +} + +- (CPArray)smartFolders +{ + return [_smartFoldersSection children]; +} + +- (CPArray)childrenOfItem:(id)anItem +{ + if (anItem == nil) + return _sections; + + if ([anItem isSection]) + return [anItem children]; + + return []; +} + +- (SourceSection)sectionForItem:(id)anItem +{ + if ([anItem isKindOfClass:[AllPeopleItem class]]) + return _librarySection; + + if ([anItem isKindOfClass:[LibraryGroup class]]) + return _groupsSection; + + if ([anItem isKindOfClass:[SmartFolder class]]) + return _smartFoldersSection; + + return nil; +} + +- (BOOL)containsItem:(id)anItem +{ + if (!anItem) + return NO; + + if ([_sections indexOfObjectIdenticalTo:anItem] !== CPNotFound) + return YES; + + var section = [self sectionForItem:anItem]; + return section && [[section children] indexOfObjectIdenticalTo:anItem] !== CPNotFound; +} + +- (id)itemWithUID:(CPString)aUID +{ + for (var i = 0; i < _sections.length; i++) + { + var section = _sections[i], + children = [section children]; + + if ([section uid] === aUID) + return section; + + for (var j = 0; j < children.length; j++) + if ([children[j] uid] === aUID) + return children[j]; + } + + return nil; +} + +- (Person)personWithUID:(CPString)aUID +{ + return _peopleByUID[aUID] || nil; +} + +- (CPArray)peopleWithUIDs:(CPArray)theUIDs +{ + var result = []; + + for (var i = 0; i < theUIDs.length; i++) + { + var person = _peopleByUID[theUIDs[i]]; + + if (person) + result.push(person); + } + + return result; +} + +// The unfiltered content for a source item (what goes into the array controller). +- (CPArray)baseContentForItem:(id)anItem +{ + if ([anItem isKindOfClass:[LibraryGroup class]]) + return [self peopleWithUIDs:[anItem memberIDs]]; + + if ([anItem isKindOfClass:[AllPeopleItem class]] || [anItem isKindOfClass:[SmartFolder class]]) + return _people; + + return []; +} + +- (CPPredicate)predicateForItem:(id)anItem +{ + if ([anItem isKindOfClass:[SmartFolder class]]) + return [anItem predicate]; + + return nil; +} + +- (CPArray)peopleMatchingPredicate:(CPPredicate)aPredicate +{ + if (!aPredicate) + return _people; + + try + { + return [_people filteredArrayUsingPredicate:aPredicate]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: predicate evaluation failed: " + e); + return nil; + } +} + +- (int)countForItem:(id)anItem +{ + if ([anItem isSection]) + return CPNotFound; + + var base = [self baseContentForItem:anItem], + predicate = [self predicateForItem:anItem]; + + if (!predicate) + return [base count]; + + var matches = [self peopleMatchingPredicate:predicate]; + return matches ? [matches count] : CPNotFound; +} + +// ---------------------------------------------------------------- People mutation + +- (Person)createPerson +{ + var person = [[Person alloc] init]; + + [person setFirstName:@"New"]; + [person setLastName:@"Person"]; + [person setEmail:@"new.person@example.com"]; + [person setCity:LibraryCities[0]]; + + [self addPeople:[person]]; + + return person; +} + +- (void)addPeople:(CPArray)thePeople +{ + if (![thePeople count]) + return; + + for (var i = 0; i < thePeople.length; i++) + [self _insertPerson:thePeople[i]]; + + [self _noteStructuralChange]; +} + +- (void)removePeople:(CPArray)thePeople +{ + if (![thePeople count]) + return; + + var uids = []; + + for (var i = 0; i < thePeople.length; i++) + { + var person = thePeople[i]; + + if (!_peopleByUID[[person uid]]) + continue; + + [self _stopObservingPerson:person]; + [_people removeObjectIdenticalTo:person]; + delete _peopleByUID[[person uid]]; + uids.push([person uid]); + } + + var groups = [self groups]; + + for (var i = 0; i < groups.length; i++) + [[groups[i] memberIDs] removeObjectsInArray:uids]; + + [self _noteStructuralChange]; +} + +- (void)addPeople:(CPArray)thePeople toGroup:(LibraryGroup)aGroup +{ + var members = [aGroup memberIDs], + changed = NO; + + for (var i = 0; i < thePeople.length; i++) + { + var uid = [thePeople[i] uid]; + + if (![members containsObject:uid]) + { + [members addObject:uid]; + changed = YES; + } + } + + if (changed) + [self _noteStructuralChange]; +} + +- (void)removePeople:(CPArray)thePeople fromGroup:(LibraryGroup)aGroup +{ + [[aGroup memberIDs] removeObjectsInArray:[thePeople valueForKey:@"uid"]]; + [self _noteStructuralChange]; +} + +// ---------------------------------------------------------------- Source item mutation + +- (LibraryGroup)addGroupWithName:(CPString)aName people:(CPArray)thePeople +{ + var group = [[LibraryGroup alloc] initWithName:aName memberIDs:[thePeople valueForKey:@"uid"] || []]; + + [[_groupsSection children] addObject:group]; + [self _noteStructuralChange]; + + return group; +} + +- (SmartFolder)addSmartFolderWithName:(CPString)aName predicate:(CPPredicate)aPredicate +{ + var folder = [[SmartFolder alloc] initWithName:aName predicate:aPredicate]; + + [[_smartFoldersSection children] addObject:folder]; + [self _noteStructuralChange]; + + return folder; +} + +- (void)updateSmartFolder:(SmartFolder)aFolder name:(CPString)aName predicate:(CPPredicate)aPredicate +{ + [aFolder setName:aName]; + [aFolder setPredicate:aPredicate]; + [self _noteStructuralChange]; +} + +- (void)renameItem:(SourceItem)anItem to:(CPString)aName +{ + if (![anItem isUserItem] || ![aName length]) + return; + + [anItem setName:aName]; + [self _noteStructuralChange]; +} + +- (void)removeItem:(SourceItem)anItem +{ + var section = [self sectionForItem:anItem]; + + if (![anItem isUserItem] || !section) + return; + + [[section children] removeObjectIdenticalTo:anItem]; + [self _noteStructuralChange]; +} + +// Moves a user item to a new index inside its own section. +// anIndex is the proposed insertion index *before* removal (outline view semantics). +- (void)moveItem:(SourceItem)anItem toIndex:(int)anIndex +{ + var section = [self sectionForItem:anItem], + children = [section children], + oldIndex = [children indexOfObjectIdenticalTo:anItem]; + + if (oldIndex === CPNotFound) + return; + + if (anIndex > oldIndex) + anIndex--; + + anIndex = MAX(0, MIN(anIndex, [children count] - 1)); + + if (anIndex === oldIndex) + return; + + [children removeObjectAtIndex:oldIndex]; + [children insertObject:anItem atIndex:anIndex]; + [self _noteStructuralChange]; +} + +- (void)resetToSampleData +{ + var people = [_people copy]; + + for (var i = 0; i < people.length; i++) + [self _stopObservingPerson:people[i]]; + + _people = []; + _peopleByUID = {}; + [[_groupsSection children] removeAllObjects]; + [[_smartFoldersSection children] removeAllObjects]; + + [[CPUserDefaults standardUserDefaults] removeObjectForKey:LibraryDefaultsKey]; + + [self _populateWithSampleData]; + [self _noteStructuralChange]; +} + +// ---------------------------------------------------------------- KVO + +- (void)_insertPerson:(Person)aPerson +{ + if (_peopleByUID[[aPerson uid]]) + return; + + _people.push(aPerson); + _peopleByUID[[aPerson uid]] = aPerson; + + for (var i = 0; i < ObservedPersonKeys.length; i++) + [aPerson addObserver:self forKeyPath:ObservedPersonKeys[i] options:CPKeyValueObservingOptionNew context:nil]; +} + +- (void)_stopObservingPerson:(Person)aPerson +{ + for (var i = 0; i < ObservedPersonKeys.length; i++) + [aPerson removeObserver:self forKeyPath:ObservedPersonKeys[i]]; +} + +- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext +{ + if (_editNotificationScheduled) + return; + + _editNotificationScheduled = YES; + [CPTimer scheduledTimerWithTimeInterval:0 target:self selector:@selector(_postEditNotification:) userInfo:nil repeats:NO]; +} + +- (void)_postEditNotification:(CPTimer)aTimer +{ + _editNotificationScheduled = NO; + [self _scheduleSave]; + + [[CPNotificationCenter defaultCenter] postNotificationName:LibraryDidChangeNotification + object:self + userInfo:[CPDictionary dictionaryWithObject:LibraryChangePersonEdited forKey:LibraryChangeKindKey]]; +} + +- (void)_noteStructuralChange +{ + [self _scheduleSave]; + + [[CPNotificationCenter defaultCenter] postNotificationName:LibraryDidChangeNotification + object:self + userInfo:[CPDictionary dictionaryWithObject:LibraryChangeStructural forKey:LibraryChangeKindKey]]; +} + +// ---------------------------------------------------------------- Persistence + +- (void)_scheduleSave +{ + if (_saveScheduled) + return; + + _saveScheduled = YES; + [CPTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(_saveTimerFired:) userInfo:nil repeats:NO]; +} + +- (void)_saveTimerFired:(CPTimer)aTimer +{ + _saveScheduled = NO; + [self save]; +} + +- (void)save +{ + var people = [], + groups = [], + folders = [], + groupItems = [self groups], + folderItems = [self smartFolders]; + + for (var i = 0; i < _people.length; i++) + people.push([_people[i] dictionaryRepresentation]); + + for (var i = 0; i < groupItems.length; i++) + groups.push([groupItems[i] dictionaryRepresentation]); + + for (var i = 0; i < folderItems.length; i++) + folders.push([folderItems[i] dictionaryRepresentation]); + + var defaults = [CPUserDefaults standardUserDefaults], + dictionary = [CPDictionary dictionaryWithObjectsAndKeys: + 1, @"version", + people, @"people", + groups, @"groups", + folders, @"smartFolders"]; + + [defaults setObject:dictionary forKey:LibraryDefaultsKey]; + [defaults synchronize]; +} + +- (BOOL)_loadFromDefaults +{ + var dictionary = [[CPUserDefaults standardUserDefaults] objectForKey:LibraryDefaultsKey]; + + if (!dictionary || ![dictionary isKindOfClass:[CPDictionary class]]) + return NO; + + try + { + var people = [dictionary objectForKey:@"people"] || [], + groups = [dictionary objectForKey:@"groups"] || [], + folders = [dictionary objectForKey:@"smartFolders"] || []; + + for (var i = 0; i < people.length; i++) + [self _insertPerson:[[Person alloc] initWithDictionary:people[i]]]; + + for (var i = 0; i < groups.length; i++) + [[_groupsSection children] addObject:[[LibraryGroup alloc] initWithDictionary:groups[i]]]; + + for (var i = 0; i < folders.length; i++) + [[_smartFoldersSection children] addObject:[[SmartFolder alloc] initWithDictionary:folders[i]]]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: stored library is unreadable, using sample data. " + e); + + _people = []; + _peopleByUID = {}; + [[_groupsSection children] removeAllObjects]; + [[_smartFoldersSection children] removeAllObjects]; + + return NO; + } + + return [_people count] > 0; +} + +// ---------------------------------------------------------------- Sample data + +- (void)_populateWithSampleData +{ + // Deterministic Park-Miller generator so every fresh launch shows identical data. + var seed = 20101125; + + function random() + { + seed = (seed * 16807) % 2147483647; + return (seed - 1) / 2147483646; + } + + function pick(anArray) + { + return anArray[Math.floor(random() * anArray.length)]; + } + + // Intentionally includes diacritics to exercise [cd] predicate options. + var firstNames = [@"Anna", @"Ben", @"Chloé", @"David", @"Elif", @"François", @"Greta", @"Hiro", @"Ingrid", @"José", + @"Karl", @"Lena", @"Mateo", @"Nina", @"Oskar", @"Paula", @"Quentin", @"Renée", @"Søren", @"Tomás", + @"Ute", @"Viktor", @"Wiebke", @"Xavier", @"Yuki", @"Zoë", @"Amélie", @"Björn", @"Clara", @"Dario"], + lastNames = [@"Müller", @"Schmidt", @"Bauer", @"García", @"Dubois", @"Rossi", @"Nowak", @"Jensen", @"Tanaka", @"Smith", + @"Johnson", @"Öztürk", @"Novák", @"Silva", @"Fischer", @"Weber", @"Lefèvre", @"Kowalski", @"Andersson", @"Brown", + @"Costa", @"Hoffmann", @"Ivanova", @"Kim", @"Lambert", @"Martín", @"Nilsson", @"O'Brien", @"Peña", @"Wagner"]; + + function emailFor(aFirst, aLast) + { + var raw = (aFirst + "." + aLast).toLowerCase(); + + if (raw.normalize) + raw = raw.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); + + return raw.replace(/ø/g, "o").replace(/[^a-z.]/g, "") + "@example.com"; + } + + var count = 240; + + for (var i = 0; i < count; i++) + { + var uid = "p" + ("000" + i).slice(-4), + person = [[Person alloc] initWithUID:uid], + first = pick(firstNames), + last = pick(lastNames); + + [person setFirstName:first]; + [person setLastName:last]; + [person setEmail:emailFor(first, last)]; + [person setDepartment:pick(LibraryDepartments)]; + [person setCity:pick(LibraryCities)]; + [person setBirthDate:new Date(1955 + Math.floor(random() * 48), Math.floor(random() * 12), 1 + Math.floor(random() * 28))]; + [person setSalary:38000 + Math.round(random() * 240) * 500]; + [person setIsManager:random() < 0.12]; + + [self _insertPerson:person]; + } + + // Groups (static membership) + var someIDs = function(n) + { + var ids = []; + + while (ids.length < n) + { + var candidate = _people[Math.floor(random() * _people.length)]; + + if (ids.indexOf([candidate uid]) === -1) + ids.push([candidate uid]); + } + + return ids; + }; + + [[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Favorites" memberIDs:someIDs(6)]]; + [[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Hiking Club" memberIDs:someIDs(14)]]; + [[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Book Club" memberIDs:someIDs(9)]]; + [[_groupsSection children] addObject:[[LibraryGroup alloc] initWithName:@"Drop People Here" memberIDs:[]]]; + + // Smart folders. Every predicate below is representable by the row templates + // in SmartFolderEditorController, so CPPredicateEditor -setObjectValue: is + // exercised on all of them. + var folders = [ + [@"Engineering over 40", [CPPredicate predicateWithFormat:@"department == %@ AND age > %@", @"Engineering", 40]], + [@"Names starting with A", [CPPredicate predicateWithFormat:@"firstName BEGINSWITH[cd] %@", @"a"]], + [@"Müller (diacritics test)",[CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @"muller"]], + [@"High earners in Berlin", [CPPredicate predicateWithFormat:@"salary >= %@ AND city ==[cd] %@", 120000, @"berlin"]], + [@"Sales or Marketing", [CPPredicate predicateWithFormat:@"department == %@ OR department == %@", @"Sales", @"Marketing"]], + [@"Not in Support", [CPPredicate predicateWithFormat:@"NOT (department == %@)", @"Support"]], + [@"Nested compound", [CPPredicate predicateWithFormat:@"age < %@ AND (city ==[cd] %@ OR city ==[cd] %@)", 35, @"tokyo", @"lisbon"]] + ]; + + for (var i = 0; i < folders.length; i++) + [[_smartFoldersSection children] addObject:[[SmartFolder alloc] initWithName:folders[i][0] predicate:folders[i][1]]]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/MainWindowController.j b/Tests/Manual/SmartFoldersDemo/MainWindowController.j new file mode 100644 index 000000000..575784a74 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/MainWindowController.j @@ -0,0 +1,585 @@ +/* + * MainWindowController.j + * SmartFoldersDemo + * + * Owns the full-platform (bridge) main window. Instantiates the sidebar and + * people view controllers and loads their views into a vertical CPSplitView. + * Exercises: + * - CPWindowController created in code, window delegate + responder chain + * target for menu / toolbar / button bar actions (sent to nil) + * - CPToolbar with view items (buttons, CPSearchField) + * - CPSplitView delegate API: min/max constraints, collapsing, custom + * resizeSubviewsWithOldSize:, autosave, button bar resize control + * - menu validation (-validateMenuItem:) + * - CPAlert as a sheet + * - CPPasteboard copy / paste + */ + +@import +@import "Library.j" +@import "SourceListViewController.j" +@import "PeopleViewController.j" +@import "SmartFolderEditorController.j" +@import "NameSheetController.j" + +var ToolbarSidebarIdentifier = @"SidebarItem", + ToolbarNewFolderIdentifier = @"NewSmartFolderItem", + ToolbarNewGroupIdentifier = @"NewGroupItem", + ToolbarEditIdentifier = @"EditFolderItem", + ToolbarDetailIdentifier = @"DetailItem", + ToolbarSearchIdentifier = @"SearchItem", + SidebarDefaultWidth = 230.0, + SidebarMinWidth = 160.0, + SidebarMaxWidth = 420.0, + ResetAlertResetTag = 1001, + ResetAlertCancelTag = 1002; + +@implementation MainWindowController : CPWindowController +{ + Library _library; + + CPSplitView _splitView; + SourceListViewController _sourceListController; + PeopleViewController _peopleController; + SmartFolderEditorController _folderEditor; + NameSheetController _nameSheet; + + CPSearchField _searchField; + CPPredicate _searchPredicate; + id _selectedItem; + float _lastSidebarWidth; +} + +- (id)initWithLibrary:(Library)aLibrary +{ + var window = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask]; + + if (self = [super initWithWindow:window]) + { + _library = aLibrary; + _lastSidebarWidth = SidebarDefaultWidth; + + [window setDelegate:self]; + [window setTitle:@"Smart Folders Demo"]; + + _sourceListController = [[SourceListViewController alloc] initWithLibrary:_library]; + [_sourceListController setDelegate:self]; + + _peopleController = [[PeopleViewController alloc] initWithLibrary:_library]; + + [self _buildToolbar]; + [self _buildSplitView]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(libraryDidChange:) + name:LibraryDidChangeNotification + object:_library]; + + [self _selectSourceItem:[_library allPeopleItem]]; + } + + return self; +} + +// ---------------------------------------------------------------- Interface + +- (void)_buildSplitView +{ + var contentView = [[self window] contentView], + bounds = [contentView bounds]; + + _splitView = [[CPSplitView alloc] initWithFrame:bounds]; + [_splitView setVertical:YES]; + [_splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_splitView setDelegate:self]; + + var sidebar = [_sourceListController view], + people = [_peopleController view]; + + [sidebar setFrame:CGRectMake(0, 0, SidebarDefaultWidth, CGRectGetHeight(bounds))]; + [people setFrame:CGRectMake(SidebarDefaultWidth + 1, 0, MAX(0, CGRectGetWidth(bounds) - SidebarDefaultWidth - 1), CGRectGetHeight(bounds))]; + + [_splitView addSubview:sidebar]; + [_splitView addSubview:people]; + [contentView addSubview:_splitView]; + + if ([_splitView respondsToSelector:@selector(setButtonBar:forDividerAtIndex:)]) + [_splitView setButtonBar:[_sourceListController buttonBar] forDividerAtIndex:0]; + + if ([_splitView respondsToSelector:@selector(setAutosaveName:)]) + [_splitView setAutosaveName:@"SmartFoldersDemo.MainSplit"]; +} + +- (void)_buildToolbar +{ + _searchField = [[CPSearchField alloc] initWithFrame:CGRectMake(0, 0, 220, 30)]; + [_searchField setPlaceholderString:@"Search people"]; + [_searchField setTarget:self]; + [_searchField setAction:@selector(searchFieldDidChange:)]; + [_searchField setSendsSearchStringImmediately:YES]; + [_searchField setRecentsAutosaveName:@"SmartFoldersDemo.RecentSearches"]; + [_searchField setSearchMenuTemplate:[_searchField defaultSearchMenuTemplate]]; + + var toolbar = [[CPToolbar alloc] initWithIdentifier:@"SmartFoldersDemoToolbar"]; + [toolbar setDelegate:self]; + + // Buttons carry their own titles, so labels below them would be redundant. + [toolbar setDisplayMode:CPToolbarDisplayModeIconOnly]; + + [toolbar setVisible:YES]; + [[self window] setToolbar:toolbar]; +} + +- (CPArray)toolbarAllowedItemIdentifiers:(CPToolbar)aToolbar +{ + return [ToolbarSidebarIdentifier, ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier, + ToolbarDetailIdentifier, ToolbarSearchIdentifier, + CPToolbarFlexibleSpaceItemIdentifier, CPToolbarSpaceItemIdentifier, CPToolbarSeparatorItemIdentifier]; +} + +- (CPArray)toolbarDefaultItemIdentifiers:(CPToolbar)aToolbar +{ + return [ToolbarSidebarIdentifier, CPToolbarSpaceItemIdentifier, + ToolbarNewFolderIdentifier, ToolbarNewGroupIdentifier, ToolbarEditIdentifier, + CPToolbarFlexibleSpaceItemIdentifier, + ToolbarDetailIdentifier, ToolbarSearchIdentifier]; +} + +- (CPToolbarItem)toolbar:(CPToolbar)aToolbar itemForItemIdentifier:(CPString)anIdentifier willBeInsertedIntoToolbar:(BOOL)aFlag +{ + var item = [[CPToolbarItem alloc] initWithItemIdentifier:anIdentifier]; + + switch (anIdentifier) + { + case ToolbarSidebarIdentifier: + [self _configureItem:item label:@"Sidebar" buttonTitle:@"Sidebar" action:@selector(toggleSidebar:) toolTip:@"Show or hide the sidebar"]; + break; + + case ToolbarNewFolderIdentifier: + [self _configureItem:item label:@"New Smart Folder" buttonTitle:@"New Smart Folder" action:@selector(newSmartFolder:) toolTip:@"Create a smart folder from a predicate"]; + break; + + case ToolbarNewGroupIdentifier: + [self _configureItem:item label:@"New Group" buttonTitle:@"New Group" action:@selector(newGroup:) toolTip:@"Create an empty group (or drop people on GROUPS)"]; + break; + + case ToolbarEditIdentifier: + [self _configureItem:item label:@"Edit" buttonTitle:@"Edit…" action:@selector(editSelectedSmartFolder:) toolTip:@"Edit the selected smart folder"]; + break; + + case ToolbarDetailIdentifier: + [self _configureItem:item label:@"Details" buttonTitle:@"Details" action:@selector(toggleDetail:) toolTip:@"Show or hide the detail pane"]; + break; + + case ToolbarSearchIdentifier: + [item setLabel:@"Search"]; + [item setPaletteLabel:@"Search"]; + [item setView:_searchField]; + [item setMinSize:CGSizeMake(140, 30)]; + [item setMaxSize:CGSizeMake(260, 30)]; + break; + } + + return item; +} + +- (void)_configureItem:(CPToolbarItem)anItem label:(CPString)aLabel buttonTitle:(CPString)aTitle action:(SEL)anAction toolTip:(CPString)aToolTip +{ + var button = [CPButton buttonWithTitle:aTitle]; + + [button setBezelStyle:CPRoundedBezelStyle]; + [button setTarget:self]; + [button setAction:anAction]; + [button sizeToFit]; + + var size = CGSizeMake(MAX(CGRectGetWidth([button frame]), 40), 25); + + [anItem setLabel:aLabel]; + [anItem setPaletteLabel:aLabel]; + [anItem setToolTip:aToolTip]; + [anItem setTarget:self]; + [anItem setAction:anAction]; + [anItem setView:button]; + [anItem setMinSize:size]; + [anItem setMaxSize:size]; +} + +// ---------------------------------------------------------------- Split view delegate (sidebar) + +- (BOOL)splitView:(CPSplitView)aSplitView canCollapseSubview:(CPView)aSubview +{ + return aSubview === [_sourceListController view]; +} + +- (float)splitView:(CPSplitView)aSplitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)anIndex +{ + return SidebarMinWidth; +} + +- (float)splitView:(CPSplitView)aSplitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)anIndex +{ + return MIN(SidebarMaxWidth, CGRectGetWidth([aSplitView bounds]) - 300.0); +} + +- (void)splitView:(CPSplitView)aSplitView resizeSubviewsWithOldSize:(CGSize)oldSize +{ + // Sidebar keeps its width, the people pane takes up the rest. + var subviews = [aSplitView subviews], + sidebar = subviews[0], + main = subviews[1], + size = [aSplitView bounds].size, + divider = [aSplitView dividerThickness], + sidebarWidth = [aSplitView isSubviewCollapsed:sidebar] ? 0 : CGRectGetWidth([sidebar frame]); + + [sidebar setFrame:CGRectMake(0, 0, sidebarWidth, size.height)]; + [main setFrame:CGRectMake(sidebarWidth + divider, 0, MAX(0, size.width - sidebarWidth - divider), size.height)]; + + // Custom layout bypasses CPSplitView's own pass: redraw the divider. + [aSplitView setNeedsDisplay:YES]; +} + +- (BOOL)isSidebarVisible +{ + var sidebar = [_sourceListController view]; + return ![_splitView isSubviewCollapsed:sidebar] && CGRectGetWidth([sidebar frame]) > 0; +} + +// ---------------------------------------------------------------- Source list delegate + +- (void)sourceListViewController:(SourceListViewController)aController didSelectItem:(id)anItem +{ + _selectedItem = anItem; + [self _refreshPeople]; +} + +- (void)_selectSourceItem:(id)anItem +{ + _selectedItem = anItem; + [_sourceListController reloadAndSelectItem:anItem]; + [self _refreshPeople]; +} + +// ---------------------------------------------------------------- Model -> view + +- (void)libraryDidChange:(CPNotification)aNotification +{ + var kind = [[aNotification userInfo] objectForKey:LibraryChangeKindKey]; + + if (![_library containsItem:_selectedItem]) + _selectedItem = [_library allPeopleItem]; + + // Names, order and badges are all rendered by the outline. + [_sourceListController reloadAndSelectItem:_selectedItem]; + + if (kind === LibraryChangeStructural) + [self _refreshPeople]; + else + [_peopleController rearrange]; +} + +- (CPPredicate)_combinedPredicate +{ + var subpredicates = [], + folderPredicate = [_library predicateForItem:_selectedItem]; + + if (folderPredicate) + subpredicates.push(folderPredicate); + + if (_searchPredicate) + subpredicates.push(_searchPredicate); + + if (subpredicates.length === 0) + return nil; + + if (subpredicates.length === 1) + return subpredicates[0]; + + return [CPCompoundPredicate andPredicateWithSubpredicates:subpredicates]; +} + +- (void)_refreshPeople +{ + var item = _selectedItem || [_library allPeopleItem]; + + [_peopleController setPeople:[_library baseContentForItem:item] + filterPredicate:[self _combinedPredicate] + title:[item name]]; +} + +// ---------------------------------------------------------------- Search + +- (void)searchFieldDidChange:(id)sender +{ + var searchString = [[_searchField stringValue] stringByTrimmingWhitespace]; + + if (![searchString length]) + _searchPredicate = nil; + else + _searchPredicate = [CPPredicate predicateWithFormat:@"(firstName CONTAINS[cd] %@) OR (lastName CONTAINS[cd] %@) OR (email CONTAINS[cd] %@) OR (city CONTAINS[cd] %@) OR (department CONTAINS[cd] %@)", + searchString, searchString, searchString, searchString, searchString]; + + [self _refreshPeople]; +} + +- (void)focusSearchField:(id)sender +{ + [[self window] makeFirstResponder:_searchField]; +} + +// ---------------------------------------------------------------- Source item actions + +- (SmartFolderEditorController)_folderEditor +{ + if (!_folderEditor) + _folderEditor = [[SmartFolderEditorController alloc] initWithLibrary:_library]; + + return _folderEditor; +} + +- (NameSheetController)_nameSheet +{ + if (!_nameSheet) + _nameSheet = [[NameSheetController alloc] init]; + + return _nameSheet; +} + +- (BOOL)_isSheetVisible +{ + return !![[self window] attachedSheet]; +} + +- (void)newSmartFolder:(id)sender +{ + if ([self _isSheetVisible]) + return; + + [[self _folderEditor] beginEditingFolder:nil modalForWindow:[self window] completion:function(folder) + { + [self _selectSourceItem:folder]; + }]; +} + +- (void)editSelectedSmartFolder:(id)sender +{ + if (![_selectedItem isKindOfClass:[SmartFolder class]] || [self _isSheetVisible]) + return; + + var folder = _selectedItem; + + [[self _folderEditor] beginEditingFolder:folder modalForWindow:[self window] completion:function(aFolder) + { + [self _selectSourceItem:aFolder]; + }]; +} + +- (void)newGroup:(id)sender +{ + if ([self _isSheetVisible]) + return; + + [[self _nameSheet] beginWithTitle:@"New Group" name:@"Untitled Group" okTitle:@"Create" modalForWindow:[self window] completion:function(name) + { + var group = [_library addGroupWithName:name people:[]]; + [self _selectSourceItem:group]; + }]; +} + +- (void)renameSelectedGroup:(id)sender +{ + if (![_selectedItem isKindOfClass:[LibraryGroup class]] || [self _isSheetVisible]) + return; + + var group = _selectedItem; + + [[self _nameSheet] beginWithTitle:@"Rename Group" name:[group name] okTitle:@"Rename" modalForWindow:[self window] completion:function(name) + { + [_library renameItem:group to:name]; + }]; +} + +- (void)deleteSelectedSourceItem:(id)sender +{ + if (![_selectedItem isUserItem]) + return; + + [_library removeItem:_selectedItem]; +} + +// ---------------------------------------------------------------- People actions + +- (void)newPerson:(id)sender +{ + // A new person would be invisible in a smart folder or a filtered list. + if ([_selectedItem isKindOfClass:[SmartFolder class]]) + _selectedItem = [_library allPeopleItem]; + + if (_searchPredicate) + { + [_searchField setStringValue:@""]; + _searchPredicate = nil; + } + + var group = [_selectedItem isKindOfClass:[LibraryGroup class]] ? _selectedItem : nil, + person = [_library createPerson]; + + if (group) + [_library addPeople:[person] toGroup:group]; + + [_peopleController selectPerson:person edit:YES]; +} + +- (void)removeSelectedPeople:(id)sender +{ + var people = [_peopleController selectedPeople]; + + if (![people count]) + return; + + if ([_selectedItem isKindOfClass:[LibraryGroup class]]) + [_library removePeople:people fromGroup:_selectedItem]; + else + [_library removePeople:people]; +} + +- (void)delete:(id)sender +{ + [self removeSelectedPeople:sender]; +} + +- (void)copy:(id)sender +{ + [_peopleController copySelectionToPasteboard:[CPPasteboard generalPasteboard]]; +} + +- (void)cut:(id)sender +{ + [self copy:sender]; + [self removeSelectedPeople:sender]; +} + +- (void)paste:(id)sender +{ + var pasteboard = [CPPasteboard generalPasteboard], + people = [_peopleController peopleFromTabSeparatedString:[pasteboard stringForType:CPStringPboardType]]; + + if (![people count]) + return; + + if ([_selectedItem isKindOfClass:[SmartFolder class]]) + _selectedItem = [_library allPeopleItem]; + + [_library addPeople:people]; + + if ([_selectedItem isKindOfClass:[LibraryGroup class]]) + [_library addPeople:people toGroup:_selectedItem]; + + [[_peopleController arrayController] setSelectedObjects:people]; +} + +// ---------------------------------------------------------------- View actions + +- (void)toggleSidebar:(id)sender +{ + if ([self isSidebarVisible]) + { + _lastSidebarWidth = CGRectGetWidth([[_sourceListController view] frame]); + [_splitView setPosition:0 ofDividerAtIndex:0]; + } + else + [_splitView setPosition:MAX(_lastSidebarWidth, SidebarMinWidth) ofDividerAtIndex:0]; +} + +- (void)toggleDetail:(id)sender +{ + [_peopleController toggleDetail]; +} + +- (void)showAllPeople:(id)sender +{ + [self _selectSourceItem:[_library allPeopleItem]]; +} + +- (void)resetLibrary:(id)sender +{ + if ([self _isSheetVisible]) + return; + + var alert = [[CPAlert alloc] init]; + + [alert setAlertStyle:CPWarningAlertStyle]; + [alert setMessageText:@"Reset the library?"]; + [alert setInformativeText:@"All people, groups and smart folders will be replaced by the sample data. This cannot be undone."]; + [alert addButtonWithTitle:@"Reset"]; + [alert addButtonWithTitle:@"Cancel"]; + + // CPAlert reports the clicked button's tag as the return code. Assign our own + // tags rather than relying on a framework-specific numbering scheme. + var buttons = [alert buttons]; + [buttons[0] setTag:ResetAlertResetTag]; + [buttons[1] setTag:ResetAlertCancelTag]; + + [alert beginSheetModalForWindow:[self window] + modalDelegate:self + didEndSelector:@selector(resetAlertDidEnd:returnCode:contextInfo:) + contextInfo:nil]; +} + +- (void)resetAlertDidEnd:(CPAlert)anAlert returnCode:(int)aReturnCode contextInfo:(id)aContext +{ + if (aReturnCode !== ResetAlertResetTag) + return; + + [_searchField setStringValue:@""]; + _searchPredicate = nil; + _selectedItem = [_library allPeopleItem]; + + [_library resetToSampleData]; +} + +// ---------------------------------------------------------------- Validation + +- (BOOL)validateMenuItem:(CPMenuItem)anItem +{ + var action = [anItem action], + sheet = [self _isSheetVisible], + hasPeopleSelection = [[_peopleController selectedPeople] count] > 0; + + switch (action) + { + case @selector(newSmartFolder:): + case @selector(newGroup:): + case @selector(resetLibrary:): + return !sheet; + + case @selector(editSelectedSmartFolder:): + return !sheet && [_selectedItem isKindOfClass:[SmartFolder class]]; + + case @selector(renameSelectedGroup:): + return !sheet && [_selectedItem isKindOfClass:[LibraryGroup class]]; + + case @selector(deleteSelectedSourceItem:): + return !sheet && [_selectedItem isUserItem]; + + case @selector(copy:): + case @selector(cut:): + case @selector(delete:): + case @selector(removeSelectedPeople:): + return !sheet && hasPeopleSelection; + + case @selector(paste:): + return !sheet && [[[CPPasteboard generalPasteboard] types] containsObject:CPStringPboardType]; + + case @selector(toggleSidebar:): + [anItem setTitle:[self isSidebarVisible] ? @"Hide Sidebar" : @"Show Sidebar"]; + return YES; + + case @selector(toggleDetail:): + [anItem setTitle:[_peopleController isDetailVisible] ? @"Hide Details" : @"Show Details"]; + return YES; + } + + return YES; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/NameSheetController.j b/Tests/Manual/SmartFoldersDemo/NameSheetController.j new file mode 100644 index 000000000..fafc614d7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/NameSheetController.j @@ -0,0 +1,102 @@ +/* + * NameSheetController.j + * SmartFoldersDemo + * + * Minimal reusable sheet asking for a name (new group, rename group). + * Exercises a second, independent CPWindowController-owned sheet, first + * responder handling and default/escape key equivalents. + */ + +@import + +@implementation NameSheetController : CPWindowController +{ + CPTextField _titleField; + CPTextField _nameField; + CPButton _okButton; + Function _completion; +} + +- (id)init +{ + var window = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, 380, 140) styleMask:CPDocModalWindowMask]; + + if (self = [super initWithWindow:window]) + [self _buildInterface]; + + return self; +} + +- (void)_buildInterface +{ + var content = [[self window] contentView], + width = CGRectGetWidth([content bounds]), + height = CGRectGetHeight([content bounds]); + + _titleField = [CPTextField labelWithTitle:@""]; + [_titleField setFont:[CPFont boldSystemFontOfSize:13.0]]; + [_titleField setFrame:CGRectMake(20, 16, width - 40, 20)]; + [content addSubview:_titleField]; + + _nameField = [[CPTextField alloc] initWithFrame:CGRectMake(20, 46, width - 40, 29)]; + [_nameField setEditable:YES]; + [_nameField setBezeled:YES]; + [content addSubview:_nameField]; + + var cancelButton = [CPButton buttonWithTitle:@"Cancel"]; + [cancelButton setFrame:CGRectMake(width - 200, height - 44, 85, 25)]; + [cancelButton setTag:CPCancelButton]; + [cancelButton setTarget:self]; + [cancelButton setAction:@selector(closeSheet:)]; + [cancelButton setKeyEquivalent:CPEscapeFunctionKey]; + [content addSubview:cancelButton]; + + _okButton = [CPButton buttonWithTitle:@"OK"]; + [_okButton setFrame:CGRectMake(width - 105, height - 44, 85, 25)]; + [_okButton setTag:CPOKButton]; + [_okButton setTarget:self]; + [_okButton setAction:@selector(closeSheet:)]; + [content addSubview:_okButton]; + + [[self window] setDefaultButton:_okButton]; +} + +// aCallback(name) is invoked only when the user confirms with a non-empty name. +- (void)beginWithTitle:(CPString)aTitle name:(CPString)aName okTitle:(CPString)anOKTitle modalForWindow:(CPWindow)aWindow completion:(Function)aCallback +{ + _completion = aCallback; + + [_titleField setStringValue:aTitle]; + [_nameField setStringValue:aName || @""]; + [_okButton setTitle:anOKTitle || @"OK"]; + + [CPApp beginSheet:[self window] + modalForWindow:aWindow + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; + + [[self window] makeFirstResponder:_nameField]; + [_nameField selectText:self]; +} + +- (void)closeSheet:(id)sender +{ + if ([sender tag] === CPOKButton && ![[[_nameField stringValue] stringByTrimmingWhitespace] length]) + return; + + [CPApp endSheet:[self window] returnCode:[sender tag]]; +} + +- (void)sheetDidEnd:(CPWindow)aSheet returnCode:(int)aReturnCode contextInfo:(id)aContext +{ + [aSheet orderOut:self]; + + var callback = _completion; + _completion = nil; + + if (aReturnCode === CPOKButton && callback) + callback([[_nameField stringValue] stringByTrimmingWhitespace]); +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/PeopleViewController.j b/Tests/Manual/SmartFoldersDemo/PeopleViewController.j new file mode 100644 index 000000000..efd3794cd --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/PeopleViewController.j @@ -0,0 +1,461 @@ +/* + * PeopleViewController.j + * SmartFoldersDemo + * + * Right hand side of the main split view. Exercises: + * - CPArrayController with content, filterPredicate, sortDescriptors and + * selection (incl. setSelectedObjects: across content changes) + * - cell-based CPTableView fully driven by bindings (value, selection, + * sort descriptors), editable columns, formatters (CPDateFormatter, + * CPNumberFormatter), checkbox data views, column reordering/resizing, + * autosaved column state, alternating rows, grid lines + * - the table as drag source (rows -> source list groups) + * - CPPasteboard (copy / paste as tab separated text) + * - nested CPSplitView hosting a child view controller (detail pane) + */ + +@import +@import "Library.j" +@import "PersonDetailViewController.j" + +@implementation PeopleViewController : CPViewController +{ + Library _library; + CPArrayController _arrayController; + CPTableView _tableView; + CPSplitView _splitView; + CPView _tablePane; + CPTextField _statusField; + PersonDetailViewController _detailController; + CPString _contentTitle; + float _lastDetailHeight; +} + +- (id)initWithLibrary:(Library)aLibrary +{ + if (self = [super initWithCibName:nil bundle:nil]) + { + _library = aLibrary; + _contentTitle = @""; + _lastDetailHeight = PersonDetailPreferredHeight; + + _arrayController = [[CPArrayController alloc] init]; + [_arrayController setEditable:YES]; + [_arrayController setAvoidsEmptySelection:NO]; + [_arrayController setPreservesSelection:YES]; + [_arrayController setSelectsInsertedObjects:YES]; + [_arrayController setSortDescriptors:[ + [CPSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES selector:@selector(caseInsensitiveCompare:)], + [CPSortDescriptor sortDescriptorWithKey:@"firstName" ascending:YES selector:@selector(caseInsensitiveCompare:)] + ]]; + + [_arrayController addObserver:self forKeyPath:@"arrangedObjects" options:0 context:nil]; + [_arrayController addObserver:self forKeyPath:@"selectionIndexes" options:0 context:nil]; + } + + return self; +} + +- (CPArrayController)arrayController +{ + return _arrayController; +} + +- (CPTableView)tableView +{ + [self view]; + return _tableView; +} + +- (void)loadView +{ + var frame = CGRectMake(0, 0, 800, 600), + view = [[CPView alloc] initWithFrame:frame]; + + [view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + _splitView = [[CPSplitView alloc] initWithFrame:frame]; + [_splitView setVertical:NO]; + [_splitView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_splitView setDelegate:self]; + + _tablePane = [self _buildTablePane:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) - _lastDetailHeight)]; + [_splitView addSubview:_tablePane]; + + _detailController = [[PersonDetailViewController alloc] initWithArrayController:_arrayController]; + var detailView = [_detailController view]; + [detailView setFrame:CGRectMake(0, 0, CGRectGetWidth(frame), _lastDetailHeight)]; + [_splitView addSubview:detailView]; + + [view addSubview:_splitView]; + [self setView:view]; + + if ([_splitView respondsToSelector:@selector(setAutosaveName:)]) + [_splitView setAutosaveName:@"SmartFoldersDemo.PeopleSplit"]; + + [self _updateStatus]; +} + +- (CPView)_buildTablePane:(CGRect)aFrame +{ + var pane = [[CPView alloc] initWithFrame:aFrame], + barHeight = 26.0; + + [pane setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(aFrame), CGRectGetHeight(aFrame) - barHeight)]; + [scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [scrollView setAutohidesScrollers:YES]; + [scrollView setBorderType:CPNoBorder]; + + _tableView = [[CPTableView alloc] initWithFrame:[[scrollView contentView] bounds]]; + [_tableView setUsesAlternatingRowBackgroundColors:YES]; + [_tableView setAllowsMultipleSelection:YES]; + [_tableView setAllowsEmptySelection:YES]; + [_tableView setAllowsColumnReordering:YES]; + [_tableView setAllowsColumnResizing:YES]; + [_tableView setAllowsColumnSelection:NO]; + [_tableView setGridStyleMask:CPTableViewSolidVerticalGridLineMask]; + [_tableView setColumnAutoresizingStyle:CPTableViewLastColumnOnlyAutoresizingStyle]; + [_tableView setRowHeight:24.0]; + [_tableView setIntercellSpacing:CGSizeMake(6.0, 0.0)]; + + var dateFormatter = [[CPDateFormatter alloc] init]; + [dateFormatter setDateStyle:CPDateFormatterMediumStyle]; + [dateFormatter setTimeStyle:CPDateFormatterNoStyle]; + + var numberFormatter = [[CPNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:CPNumberFormatterDecimalStyle]; + + [self _addColumn:@"lastName" title:@"Last Name" width:120 editable:YES dataView:nil]; + [self _addColumn:@"firstName" title:@"First Name" width:110 editable:YES dataView:nil]; + [self _addColumn:@"department" title:@"Department" width:110 editable:YES dataView:nil]; + [self _addColumn:@"city" title:@"City" width:110 editable:YES dataView:nil]; + [self _addColumn:@"age" title:@"Age" width:46 editable:NO dataView:[self _textDataViewWithFormatter:nil alignment:CPRightTextAlignment]]; + [self _addColumn:@"birthDate" title:@"Born" width:110 editable:NO dataView:[self _textDataViewWithFormatter:dateFormatter alignment:CPRightTextAlignment]]; + [self _addColumn:@"salary" title:@"Salary" width:90 editable:YES dataView:[self _textDataViewWithFormatter:numberFormatter alignment:CPRightTextAlignment]]; + + // Title-less checkbox, image centred in the cell. + var checkBox = [CPCheckBox checkBoxWithTitle:@""]; + [checkBox setImagePosition:CPImageOnly]; + [checkBox setAlignment:CPCenterTextAlignment]; + + var managerColumn = [self _addColumn:@"isManager" title:@"Manager" width:64 editable:YES dataView:checkBox]; + [[managerColumn headerView] setAlignment:CPCenterTextAlignment]; + [managerColumn setResizingMask:CPTableColumnNoResizing]; + + [self _addColumn:@"email" title:@"Email" width:220 editable:YES dataView:nil]; + + // Selection and sorting through bindings. + [_tableView bind:CPSelectionIndexesBinding toObject:_arrayController withKeyPath:@"selectionIndexes" options:nil]; + [_tableView bind:@"sortDescriptors" toObject:_arrayController withKeyPath:@"sortDescriptors" options:nil]; + + // The data source only provides drag & drop; content comes from the bindings + // (the column value bindings above establish the table's content binding). + [_tableView setDataSource:self]; + [_tableView setDelegate:self]; + [_tableView setDraggingSourceOperationMask:CPDragOperationCopy | CPDragOperationMove forLocal:YES]; + [_tableView setDraggingSourceOperationMask:CPDragOperationCopy forLocal:NO]; + + if ([_tableView respondsToSelector:@selector(setAutosaveName:)]) + { + [_tableView setAutosaveName:@"SmartFoldersDemo.PeopleTable"]; + + if ([_tableView respondsToSelector:@selector(setAutosaveTableColumns:)]) + [_tableView setAutosaveTableColumns:YES]; + } + + [scrollView setDocumentView:_tableView]; + [pane addSubview:scrollView]; + + // --- Button bar with status text + var buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(aFrame) - barHeight, CGRectGetWidth(aFrame), barHeight)]; + [buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; + + if ([buttonBar respondsToSelector:@selector(setHasResizeControl:)]) + [buttonBar setHasResizeControl:NO]; + + var plusButton = [CPButtonBar plusButton]; + [plusButton setTarget:nil]; + [plusButton setAction:@selector(newPerson:)]; + [plusButton setToolTip:@"Add a person"]; + + var minusButton = [CPButtonBar minusButton]; + [minusButton setTarget:nil]; + [minusButton setAction:@selector(removeSelectedPeople:)]; + [minusButton setToolTip:@"Remove the selected people (from the group, if a group is selected)"]; + [minusButton bind:CPEnabledBinding toObject:_arrayController withKeyPath:@"canRemove" options:nil]; + + [buttonBar setButtons:[plusButton, minusButton]]; + + _statusField = [CPTextField labelWithTitle:@""]; + [_statusField setFont:[CPFont systemFontOfSize:11.0]]; + [_statusField setTextColor:[CPColor colorWithWhite:0.35 alpha:1.0]]; + [_statusField setAlignment:CPRightTextAlignment]; + [_statusField setVerticalAlignment:CPCenterVerticalTextAlignment]; + [_statusField setFrame:CGRectMake(80, 0, CGRectGetWidth(aFrame) - 92, barHeight)]; + [_statusField setLineBreakMode:CPLineBreakByTruncatingHead]; + [_statusField setAutoresizingMask:CPViewWidthSizable]; + [buttonBar addSubview:_statusField]; + + [pane addSubview:buttonBar]; + + return pane; +} + +- (CPTextField)_textDataViewWithFormatter:(CPFormatter)aFormatter alignment:(CPTextAlignment)anAlignment +{ + var field = [[CPTextField alloc] initWithFrame:CGRectMakeZero()]; + + [field setAlignment:anAlignment]; + [field setVerticalAlignment:CPCenterVerticalTextAlignment]; + [field setLineBreakMode:CPLineBreakByTruncatingTail]; + + if (aFormatter) + [field setFormatter:aFormatter]; + + return field; +} + +- (CPTableColumn)_addColumn:(CPString)aKey title:(CPString)aTitle width:(float)aWidth editable:(BOOL)isEditable dataView:(CPView)aDataView +{ + var column = [[CPTableColumn alloc] initWithIdentifier:aKey], + selector = [@"firstName", @"lastName", @"department", @"city", @"email"].indexOf(aKey) !== -1 ? @selector(caseInsensitiveCompare:) : @selector(compare:); + + [[column headerView] setStringValue:aTitle]; + [column setWidth:aWidth]; + [column setMinWidth:36]; + [column setEditable:isEditable]; + [column setSortDescriptorPrototype:[CPSortDescriptor sortDescriptorWithKey:aKey ascending:YES selector:selector]]; + + if (!aDataView) + { + aDataView = [self _textDataViewWithFormatter:nil alignment:CPLeftTextAlignment]; + [aDataView setEditable:isEditable]; + } + + [column setDataView:aDataView]; + [_tableView addTableColumn:column]; + [column bind:CPValueBinding toObject:_arrayController withKeyPath:@"arrangedObjects." + aKey options:nil]; + + return column; +} + +// ---------------------------------------------------------------- Content + +- (void)setPeople:(CPArray)thePeople filterPredicate:(CPPredicate)aPredicate title:(CPString)aTitle +{ + var selected = [_arrayController selectedObjects] || []; + + _contentTitle = aTitle || @""; + + try + { + [_arrayController setFilterPredicate:aPredicate]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: filter predicate failed: " + e); + [_arrayController setFilterPredicate:nil]; + } + + [_arrayController setContent:[thePeople copy]]; + [_arrayController setSelectedObjects:selected]; + + [self _updateStatus]; +} + +- (void)rearrange +{ + [_arrayController rearrangeObjects]; + [self _updateStatus]; +} + +- (CPArray)selectedPeople +{ + return [_arrayController selectedObjects] || []; +} + +- (void)selectPerson:(Person)aPerson edit:(BOOL)shouldEdit +{ + [_arrayController setSelectedObjects:[aPerson]]; + + var row = [[_arrayController arrangedObjects] indexOfObjectIdenticalTo:aPerson]; + + if (row === CPNotFound) + return; + + [_tableView scrollRowToVisible:row]; + + if (shouldEdit) + { + [[_tableView window] makeFirstResponder:_tableView]; + [_tableView editColumn:[_tableView columnWithIdentifier:@"firstName"] row:row withEvent:nil select:YES]; + } +} + +- (void)focus +{ + [[_tableView window] makeFirstResponder:_tableView]; +} + +// ---------------------------------------------------------------- Detail pane + +- (BOOL)isDetailVisible +{ + var detailView = [[_splitView subviews] objectAtIndex:1]; + return ![_splitView isSubviewCollapsed:detailView] && CGRectGetHeight([detailView frame]) > 0; +} + +- (void)toggleDetail +{ + var total = CGRectGetHeight([_splitView bounds]), + divider = [_splitView dividerThickness]; + + if ([self isDetailVisible]) + { + _lastDetailHeight = CGRectGetHeight([[[_splitView subviews] objectAtIndex:1] frame]); + [_splitView setPosition:total ofDividerAtIndex:0]; + } + else + [_splitView setPosition:total - divider - MAX(_lastDetailHeight, 150) ofDividerAtIndex:0]; +} + +// Split view delegate (detail pane) +- (BOOL)splitView:(CPSplitView)aSplitView canCollapseSubview:(CPView)aSubview +{ + return aSubview === [[aSplitView subviews] objectAtIndex:1]; +} + +- (float)splitView:(CPSplitView)aSplitView constrainMinCoordinate:(float)proposedMin ofSubviewAt:(int)anIndex +{ + return 120.0; +} + +- (float)splitView:(CPSplitView)aSplitView constrainMaxCoordinate:(float)proposedMax ofSubviewAt:(int)anIndex +{ + return CGRectGetHeight([aSplitView bounds]) - 150.0; +} + +- (void)splitView:(CPSplitView)aSplitView resizeSubviewsWithOldSize:(CGSize)oldSize +{ + // Keep the detail pane at a fixed height while the window resizes. + var subviews = [aSplitView subviews], + top = subviews[0], + bottom = subviews[1], + size = [aSplitView bounds].size, + divider = [aSplitView dividerThickness], + bottomHeight = [aSplitView isSubviewCollapsed:bottom] ? 0 : CGRectGetHeight([bottom frame]); + + bottomHeight = MIN(bottomHeight, MAX(0, size.height - divider - 120)); + + [top setFrame:CGRectMake(0, 0, size.width, MAX(0, size.height - bottomHeight - divider))]; + [bottom setFrame:CGRectMake(0, size.height - bottomHeight, size.width, bottomHeight)]; + + // A custom resizeSubviewsWithOldSize: bypasses CPSplitView's own layout pass, + // so the divider must be told to redraw at the new width. + [aSplitView setNeedsDisplay:YES]; +} + +// ---------------------------------------------------------------- Pasteboard helpers + +- (CPString)tabSeparatedStringForPeople:(CPArray)thePeople +{ + var lines = [[Person tabSeparatedHeader]]; + + for (var i = 0; i < thePeople.length; i++) + lines.push([thePeople[i] tabSeparatedRepresentation]); + + return lines.join("\n"); +} + +- (CPArray)peopleFromTabSeparatedString:(CPString)aString +{ + var lines = (aString || "").split(/\r?\n/), + result = []; + + for (var i = 0; i < lines.length; i++) + { + var line = lines[i]; + + if (!line || line === [Person tabSeparatedHeader]) + continue; + + var parts = line.split("\t"); + + if (parts.length < 2) + continue; + + var person = [[Person alloc] init]; + [person setFirstName:parts[0] || @""]; + [person setLastName:parts[1] || @""]; + [person setEmail:parts[2] || @""]; + [person setDepartment:parts[3] || LibraryDepartments[0]]; + [person setCity:parts[4] || @""]; + [person setSalary:parseInt(parts[5], 10) || 0]; + + result.push(person); + } + + return result; +} + +- (void)copySelectionToPasteboard:(CPPasteboard)aPasteboard +{ + var people = [self selectedPeople]; + + if (![people count]) + return; + + [aPasteboard declareTypes:[CPStringPboardType, PeopleDragType] owner:nil]; + [aPasteboard setString:[self tabSeparatedStringForPeople:people] forType:CPStringPboardType]; + [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[people valueForKey:@"uid"]] forType:PeopleDragType]; +} + +// ---------------------------------------------------------------- Table data source: drag source + +- (BOOL)tableView:(CPTableView)aTableView writeRowsWithIndexes:(CPIndexSet)theRows toPasteboard:(CPPasteboard)aPasteboard +{ + var people = [[_arrayController arrangedObjects] objectsAtIndexes:theRows]; + + if (![people count]) + return NO; + + [aPasteboard declareTypes:[PeopleDragType, CPStringPboardType] owner:self]; + [aPasteboard setData:[CPKeyedArchiver archivedDataWithRootObject:[people valueForKey:@"uid"]] forType:PeopleDragType]; + [aPasteboard setString:[self tabSeparatedStringForPeople:people] forType:CPStringPboardType]; + + return YES; +} + +// ---------------------------------------------------------------- Table delegate + +- (void)tableViewDeleteKeyPressed:(CPTableView)aTableView +{ + [CPApp sendAction:@selector(removeSelectedPeople:) to:nil from:aTableView]; +} + +// ---------------------------------------------------------------- KVO / status + +- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)aChange context:(id)aContext +{ + [self _updateStatus]; +} + +- (void)_updateStatus +{ + if (!_statusField) + return; + + var shown = [[_arrayController arrangedObjects] count], + total = [[_arrayController content] count], + selected = [[_arrayController selectionIndexes] count], + text = [CPString stringWithFormat:@"%@ — %d of %d people", _contentTitle, shown, total]; + + if (selected) + text += [CPString stringWithFormat:@", %d selected", selected]; + + [_statusField setStringValue:text]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/Person.j b/Tests/Manual/SmartFoldersDemo/Person.j new file mode 100644 index 000000000..4f15f25cc --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/Person.j @@ -0,0 +1,141 @@ +/* + * Person.j + * SmartFoldersDemo + * + * The record type shown in the people table. Plain KVC/KVO compliant model + * object: every property is exercised by bindings (table columns, detail + * pane), by CPPredicate evaluation (smart folders, search) and by + * CPSortDescriptor (column sorting). + * + * "age" and "fullName" are derived properties registered with + * +keyPathsForValuesAffecting so that KVO dependent-key support is + * exercised as well. + */ + +@import + +var PersonUIDCounter = 0; + +@implementation Person : CPObject +{ + CPString uid @accessors; + CPString firstName @accessors; + CPString lastName @accessors; + CPString email @accessors; + CPString department @accessors; + CPString city @accessors; + CPDate birthDate @accessors; + CPNumber salary @accessors; + BOOL isManager @accessors; +} + ++ (CPString)nextUID +{ + return @"p" + (new Date()).getTime().toString(36) + "-" + (++PersonUIDCounter); +} + ++ (CPSet)keyPathsForValuesAffectingAge +{ + return [CPSet setWithObject:@"birthDate"]; +} + ++ (CPSet)keyPathsForValuesAffectingFullName +{ + return [CPSet setWithObjects:@"firstName", @"lastName"]; +} + +- (id)init +{ + return [self initWithUID:[Person nextUID]]; +} + +- (id)initWithUID:(CPString)aUID +{ + if (self = [super init]) + { + uid = aUID; + firstName = @""; + lastName = @""; + email = @""; + department = @"Engineering"; + city = @""; + birthDate = new Date(1990, 0, 1); + salary = 50000; + isManager = NO; + } + + return self; +} + +- (id)initWithDictionary:(CPDictionary)aDictionary +{ + if (self = [self initWithUID:[aDictionary objectForKey:@"uid"] || [Person nextUID]]) + { + firstName = [aDictionary objectForKey:@"firstName"] || @""; + lastName = [aDictionary objectForKey:@"lastName"] || @""; + email = [aDictionary objectForKey:@"email"] || @""; + department = [aDictionary objectForKey:@"department"] || @""; + city = [aDictionary objectForKey:@"city"] || @""; + + var interval = [aDictionary objectForKey:@"birthDate"]; + birthDate = (interval !== nil && interval !== undefined) ? [CPDate dateWithTimeIntervalSince1970:interval] : nil; + + salary = Number([aDictionary objectForKey:@"salary"] || 0); + isManager = !![aDictionary objectForKey:@"isManager"]; + } + + return self; +} + +// Property-list safe representation used for CPUserDefaults persistence. +- (CPDictionary)dictionaryRepresentation +{ + return [CPDictionary dictionaryWithObjectsAndKeys: + uid, @"uid", + firstName || @"", @"firstName", + lastName || @"", @"lastName", + email || @"", @"email", + department || @"", @"department", + city || @"", @"city", + birthDate ? [birthDate timeIntervalSince1970] : 0, @"birthDate", + Number(salary || 0), @"salary", + isManager ? 1 : 0, @"isManager"]; +} + +- (int)age +{ + if (!birthDate) + return 0; + + var now = new Date(), + years = now.getFullYear() - birthDate.getFullYear(), + months = now.getMonth() - birthDate.getMonth(); + + if (months < 0 || (months === 0 && now.getDate() < birthDate.getDate())) + years--; + + return years; +} + +- (CPString)fullName +{ + return [[CPString stringWithFormat:@"%@ %@", firstName || @"", lastName || @""] stringByTrimmingWhitespace]; +} + +// One line of tab separated text; used for copy / paste and drag & drop. +- (CPString)tabSeparatedRepresentation +{ + return [firstName || @"", lastName || @"", email || @"", department || @"", city || @"", String(salary || 0)].join("\t"); +} + ++ (CPString)tabSeparatedHeader +{ + return ["First Name", "Last Name", "Email", "Department", "City", "Salary"].join("\t"); +} + +- (CPString)description +{ + return [CPString stringWithFormat:@"", uid, [self fullName]]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j b/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j new file mode 100644 index 000000000..28de9481e --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/PersonDetailViewController.j @@ -0,0 +1,180 @@ +/* + * PersonDetailViewController.j + * SmartFoldersDemo + * + * Detail pane below the people table. Every control is bound to + * arrayController.selection., which exercises CPControllerSelectionProxy, + * placeholder binding options (no selection / multiple values / null) and a + * read-only binding to a KVO-dependent derived key ("age"). + * + * Controls covered: CPTextField (+formatter), CPComboBox, CPPopUpButton + * (selectedValue binding), CPDatePicker, CPCheckBox, CPBox. + */ + +@import +@import "Library.j" + +// Layout grid. Every row is RowHeight tall; labels and controls are centred on +// the row's midline regardless of each control's intrinsic height. +var RowHeight = 30.0, + RowPitch = 36.0, + LabelWidth = 90.0, + LabelGap = 8.0, + FieldWidth = 230.0, + ColumnGap = 40.0, + TextFieldHeight = 29.0, + PopUpHeight = 25.0, + DatePickerHeight= 25.0; + +PersonDetailPreferredHeight = 5 * RowPitch + 52.0; + +@implementation PersonDetailViewController : CPViewController +{ + CPArrayController _arrayController; +} + +- (id)initWithArrayController:(CPArrayController)anArrayController +{ + if (self = [super initWithCibName:nil bundle:nil]) + _arrayController = anArrayController; + + return self; +} + +- (CPDictionary)_placeholderOptions +{ + return [CPDictionary dictionaryWithObjectsAndKeys: + @"No Selection", CPNoSelectionPlaceholderBindingOption, + @"Multiple Values", CPMultipleValuesPlaceholderBindingOption, + @"", CPNullPlaceholderBindingOption]; +} + +- (void)loadView +{ + var frame = CGRectMake(0, 0, 800, PersonDetailPreferredHeight), + view = [[CPView alloc] initWithFrame:frame]; + + [view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [view setBackgroundColor:[CPColor colorWithWhite:0.96 alpha:1.0]]; + + var box = [[CPBox alloc] initWithFrame:CGRectInset(frame, 12, 8)]; + [box setTitle:@"Selected Person"]; + [box setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [view addSubview:box]; + + var content = [box contentView], + options = [self _placeholderOptions], + left = 12.0, + right = left + LabelWidth + LabelGap + FieldWidth + ColumnGap; + + // ---- Left column + [[self _textFieldInRow:0 column:left label:@"First Name:" in:content] + bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.firstName" options:options]; + + [[self _textFieldInRow:1 column:left label:@"Last Name:" in:content] + bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.lastName" options:options]; + + [[self _textFieldInRow:2 column:left label:@"Email:" in:content] + bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.email" options:options]; + + [self _labelInRow:3 column:left title:@"Department:" in:content]; + var department = [[CPPopUpButton alloc] initWithFrame:CGRectMakeZero() pullsDown:NO]; + [department addItemsWithTitles:LibraryDepartments]; + [self _place:department row:3 x:left + LabelWidth + LabelGap width:FieldWidth height:PopUpHeight in:content]; + [department bind:CPSelectedValueBinding toObject:_arrayController withKeyPath:@"selection.department" options:nil]; + + var manager = [CPCheckBox checkBoxWithTitle:@"Manager"]; + [manager setAllowsMixedState:YES]; + [manager sizeToFit]; + [self _place:manager row:4 x:left + LabelWidth + LabelGap width:CGRectGetWidth([manager frame]) height:CGRectGetHeight([manager frame]) in:content]; + [manager bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.isManager" options:nil]; + + // ---- Right column + [self _labelInRow:0 column:right title:@"City:" in:content]; + var city = [[CPComboBox alloc] initWithFrame:CGRectMakeZero()]; + [city addItemsWithObjectValues:LibraryCities]; + [city setEditable:YES]; + [self _place:city row:0 x:right + LabelWidth + LabelGap width:FieldWidth height:TextFieldHeight in:content]; + [city bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.city" options:options]; + + [self _labelInRow:1 column:right title:@"Birth Date:" in:content]; + var birthDate = [[CPDatePicker alloc] initWithFrame:CGRectMake(0, 0, 140, DatePickerHeight)]; + [birthDate setDatePickerStyle:CPTextFieldAndStepperDatePickerStyle]; + [birthDate setDatePickerElements:CPYearMonthDayDatePickerElementFlag]; + [self _place:birthDate row:1 x:right + LabelWidth + LabelGap width:140 height:CGRectGetHeight([birthDate frame]) in:content]; + [birthDate bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.birthDate" options:nil]; + + [self _labelInRow:2 column:right title:@"Age:" in:content]; + var age = [self _valueLabelInRow:2 x:right + LabelWidth + LabelGap in:content]; + [age bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.age" options:options]; + + var salary = [self _textFieldInRow:3 column:right label:@"Salary:" in:content], + numberFormatter = [[CPNumberFormatter alloc] init]; + [numberFormatter setNumberStyle:CPNumberFormatterDecimalStyle]; + [salary setFormatter:numberFormatter]; + [salary setAlignment:CPRightTextAlignment]; + [salary setFrameSize:CGSizeMake(140, CGRectGetHeight([salary frame]))]; + [salary bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.salary" options:options]; + + [self _labelInRow:4 column:right title:@"Record ID:" in:content]; + var uid = [self _valueLabelInRow:4 x:right + LabelWidth + LabelGap in:content]; + [uid setSelectable:YES]; + [uid setTextColor:[CPColor grayColor]]; + [uid bind:CPValueBinding toObject:_arrayController withKeyPath:@"selection.uid" options:options]; + + [self setView:view]; +} + +// ---------------------------------------------------------------- Layout helpers + +- (float)_rowTop:(int)aRow +{ + return 10.0 + aRow * RowPitch; +} + +// Centres aView vertically on the row's midline. +- (void)_place:(CPView)aView row:(int)aRow x:(float)anX width:(float)aWidth height:(float)aHeight in:(CPView)aSuperview +{ + var y = [self _rowTop:aRow] + Math.round((RowHeight - aHeight) / 2); + + [aView setFrame:CGRectMake(anX, y, aWidth, aHeight)]; + [aSuperview addSubview:aView]; +} + +- (CPTextField)_labelInRow:(int)aRow column:(float)anX title:(CPString)aTitle in:(CPView)aSuperview +{ + var label = [CPTextField labelWithTitle:aTitle]; + + [label setAlignment:CPRightTextAlignment]; + [label setVerticalAlignment:CPCenterVerticalTextAlignment]; + [label setTextColor:[CPColor colorWithWhite:0.25 alpha:1.0]]; + [self _place:label row:aRow x:anX width:LabelWidth height:RowHeight in:aSuperview]; + + return label; +} + +- (CPTextField)_valueLabelInRow:(int)aRow x:(float)anX in:(CPView)aSuperview +{ + var label = [CPTextField labelWithTitle:@""]; + + [label setVerticalAlignment:CPCenterVerticalTextAlignment]; + [self _place:label row:aRow x:anX + 4 width:FieldWidth - 4 height:RowHeight in:aSuperview]; + + return label; +} + +- (CPTextField)_textFieldInRow:(int)aRow column:(float)anX label:(CPString)aTitle in:(CPView)aSuperview +{ + [self _labelInRow:aRow column:anX title:aTitle in:aSuperview]; + + var field = [[CPTextField alloc] initWithFrame:CGRectMakeZero()]; + + [field setEditable:YES]; + [field setBezeled:YES]; + [field setVerticalAlignment:CPCenterVerticalTextAlignment]; + [self _place:field row:aRow x:anX + LabelWidth + LabelGap width:FieldWidth height:TextFieldHeight in:aSuperview]; + + return field; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/README.md b/Tests/Manual/SmartFoldersDemo/README.md new file mode 100644 index 000000000..101cca2de --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/README.md @@ -0,0 +1,40 @@ +# SmartFoldersDemo + +Non-document-based Cappuccino integration test, built entirely in code (no cib). + +Copy `index.html`, `index-debug.html` and `Jakefile` from `Tests/Manual/SmartFoldersTest` +(they are toolchain specific and need no changes apart from the title). No `Resources/` +are required: all icons and badges are drawn with `CPBezierPath`. + +## Structure + +| File | Role | +|---|---| +| `main.j`, `Info.plist` | entry point, `CPApplicationDelegateClass` = `AppController` | +| `AppController.j` | creates model, main menu, main window controller | +| `MainWindowController.j` | bridge window, toolbar, sidebar split view, routed actions, menu validation | +| `SourceListViewController.j` | view-based `CPOutlineView` source list, badges, drop target, button bar | +| `PeopleViewController.j` | `CPArrayController` + bound `CPTableView`, drag source, pasteboard, detail split | +| `PersonDetailViewController.j` | controls bound to `selection.*` | +| `SmartFolderEditorController.j` | sheet with `CPPredicateEditor` built from row templates | +| `NameSheetController.j` | small reusable name sheet | +| `Library.j`, `Person.j` | model, KVO observation, `CPUserDefaults` persistence, sample data | + +## Manual test checklist + +1. Launch: sidebar shows LIBRARY / GROUPS / SMART FOLDERS with count badges; table shows 240 people. +2. Click each smart folder: table filters, status line shows "n of 240", badge equals n. +3. Double-click a smart folder: sheet opens with the stored predicate reconstructed in the editor + (incl. nested compound and NOT). Add/remove rows: sheet resizes, match count and format update live. +4. OK: folder renamed/updated, badge updated. Cancel: nothing changes. +5. "Müller (diacritics test)" must match Müller with predicate `muller` (`[cd]`). +6. Type in the search field: filter combines with the selected folder (AND). +7. Sort by every column header; reorder and resize columns; reload: column layout restored. +8. Select people, drag onto a group: badge increases. Drag onto the GROUPS header: new group created and selected. +9. Drag groups / smart folders to reorder them within their section; reload: order persisted. +10. Edit a cell (name, salary, Mgr checkbox) and the detail pane fields: other views and badges follow; + multi-select shows "Multiple Values"; changing Birth Date updates Age (dependent key). +11. Copy (⌘C) then Paste (⌘V): duplicated people appear selected. Delete key removes people + (from the group only, when a group is selected). +12. View menu / toolbar: hide and show sidebar and detail pane; drag the sidebar resize control in the button bar. +13. File ▸ Reset Library…: alert sheet, then sample data restored. diff --git a/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j b/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j new file mode 100644 index 000000000..cd69152f9 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/SmartFolderEditorController.j @@ -0,0 +1,390 @@ +/* + * SmartFolderEditorController.j + * SmartFoldersDemo + * + * Document-modal sheet for creating / editing a smart folder. Exercises: + * - CPWindowController owning a CPDocModalWindowMask window, run as a sheet + * with -beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: + * - CPPredicateEditor (CPRuleEditor subclass) built entirely from + * CPPredicateEditorRowTemplate instances: compound (AND/OR/NOT), + * key path + string attribute type with [cd] options, key path + integer + * attribute type, key path + constant right expressions (pop-up) + * - round trip predicate -> editor (setObjectValue:) -> predicate + * - CPRuleEditorRowsDidChangeNotification + target/action for live preview + * - resizing a sheet while it is attached + * - default button / escape key equivalent + */ + +@import +@import "Library.j" + +var EditorWidth = 680.0, + EditorChromeHeight = 200.0, + EditorMinHeight = 300.0, + EditorMaxHeight = 600.0, + EditorRowHeight = 28.0, + EditorTitles = { + "firstName" : "First Name", + "lastName" : "Last Name", + "email" : "Email", + "city" : "City", + "department" : "Department", + "age" : "Age", + "salary" : "Salary" + }; + + +// -------------------------------------------------------------------------------- +// LabeledRowTemplate – shows human readable titles for key path expressions +// -------------------------------------------------------------------------------- + +@implementation LabeledRowTemplate : CPPredicateEditorRowTemplate +{ +} + +- (CPArray)templateViews +{ + var views = [super templateViews], + first = [views firstObject]; + + if ([first isKindOfClass:[CPPopUpButton class]]) + { + var items = [first itemArray]; + + for (var i = 0; i < items.length; i++) + { + var expression = [items[i] representedObject]; + + if ([expression respondsToSelector:@selector(expressionType)] && [expression expressionType] === CPKeyPathExpressionType) + { + var title = EditorTitles[[expression keyPath]]; + + if (title && [items[i] title] !== title) + [items[i] setTitle:title]; + } + } + } + + return views; +} + +@end + + +// -------------------------------------------------------------------------------- +// SmartFolderEditorController +// -------------------------------------------------------------------------------- + +@implementation SmartFolderEditorController : CPWindowController +{ + Library _library; + CPTextField _nameField; + CPScrollView _editorScrollView; + CPPredicateEditor _predicateEditor; + CPTextField _formatField; + CPTextField _matchField; + CPButton _okButton; + + SmartFolder _folder; + Function _completion; +} + +- (id)initWithLibrary:(Library)aLibrary +{ + var window = [[CPWindow alloc] initWithContentRect:CGRectMake(0, 0, EditorWidth, EditorMinHeight) styleMask:CPDocModalWindowMask]; + + if (self = [super initWithWindow:window]) + { + _library = aLibrary; + [self _buildInterface]; + + [[CPNotificationCenter defaultCenter] addObserver:self + selector:@selector(ruleEditorRowsDidChange:) + name:CPRuleEditorRowsDidChangeNotification + object:_predicateEditor]; + } + + return self; +} + +- (void)_buildInterface +{ + var window = [self window], + content = [window contentView], + bounds = [content bounds], + width = CGRectGetWidth(bounds), + height = CGRectGetHeight(bounds); + + var title = [CPTextField labelWithTitle:@"Smart Folder"]; + [title setFont:[CPFont boldSystemFontOfSize:13.0]]; + [title setFrameOrigin:CGPointMake(20, 14)]; + [content addSubview:title]; + + var nameLabel = [CPTextField labelWithTitle:@"Name:"]; + [nameLabel setAlignment:CPRightTextAlignment]; + [nameLabel setFrame:CGRectMake(20, 46, 60, 20)]; + [content addSubview:nameLabel]; + + _nameField = [[CPTextField alloc] initWithFrame:CGRectMake(85, 40, width - 105, 29)]; + [_nameField setEditable:YES]; + [_nameField setBezeled:YES]; + [_nameField setAutoresizingMask:CPViewWidthSizable]; + [_nameField setPlaceholderString:@"Smart folder name"]; + [content addSubview:_nameField]; + + var matchLabel = [CPTextField labelWithTitle:@"Match people where:"]; + [matchLabel setFrameOrigin:CGPointMake(20, 80)]; + [content addSubview:matchLabel]; + + // --- Predicate editor inside a scroll view + var top = 102.0, + bottomChrome = EditorChromeHeight - top; + + _editorScrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(20, top, width - 40, height - top - bottomChrome)]; + [_editorScrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_editorScrollView setAutohidesScrollers:YES]; + [_editorScrollView setHasHorizontalScroller:NO]; + [_editorScrollView setBorderType:CPBezelBorder]; + + _predicateEditor = [[CPPredicateEditor alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth([[_editorScrollView contentView] bounds]), EditorRowHeight)]; + [_predicateEditor setAutoresizingMask:CPViewWidthSizable]; + [_predicateEditor setRowHeight:EditorRowHeight]; + [_predicateEditor setNestingMode:CPRuleEditorNestingModeCompound]; + [_predicateEditor setCanRemoveAllRows:NO]; + [_predicateEditor setRowTemplates:[self _rowTemplates]]; + [_predicateEditor setTarget:self]; + [_predicateEditor setAction:@selector(predicateEditorChanged:)]; + + [_editorScrollView setDocumentView:_predicateEditor]; + [content addSubview:_editorScrollView]; + + // --- Footer + _formatField = [CPTextField labelWithTitle:@""]; + [_formatField setFrame:CGRectMake(20, height - 88, width - 40, 18)]; + [_formatField setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; + [_formatField setFont:[CPFont fontWithName:@"Menlo, Courier" size:11.0]]; + [_formatField setTextColor:[CPColor grayColor]]; + [_formatField setLineBreakMode:CPLineBreakByTruncatingTail]; + [_formatField setSelectable:YES]; + [content addSubview:_formatField]; + + _matchField = [CPTextField labelWithTitle:@""]; + [_matchField setFrame:CGRectMake(20, height - 40, 300, 20)]; + [_matchField setAutoresizingMask:CPViewMaxXMargin | CPViewMinYMargin]; + [content addSubview:_matchField]; + + var cancelButton = [CPButton buttonWithTitle:@"Cancel"]; + [cancelButton setFrame:CGRectMake(width - 200, height - 44, 85, 25)]; + [cancelButton setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin]; + [cancelButton setTag:CPCancelButton]; + [cancelButton setTarget:self]; + [cancelButton setAction:@selector(closeSheet:)]; + [cancelButton setKeyEquivalent:CPEscapeFunctionKey]; + [content addSubview:cancelButton]; + + _okButton = [CPButton buttonWithTitle:@"OK"]; + [_okButton setFrame:CGRectMake(width - 105, height - 44, 85, 25)]; + [_okButton setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin]; + [_okButton setTag:CPOKButton]; + [_okButton setTarget:self]; + [_okButton setAction:@selector(closeSheet:)]; + [content addSubview:_okButton]; + + [window setDefaultButton:_okButton]; +} + +- (CPArray)_rowTemplates +{ + var keyPaths = function(keys) + { + var result = []; + + for (var i = 0; i < keys.length; i++) + result.push([CPExpression expressionForKeyPath:keys[i]]); + + return result; + }; + + var compound = [[CPPredicateEditorRowTemplate alloc] initWithCompoundTypes:[CPAndPredicateType, CPOrPredicateType, CPNotPredicateType]]; + + var strings = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"lastName", @"firstName", @"email", @"city"]) + rightExpressionAttributeType:CPStringAttributeType + modifier:CPDirectPredicateModifier + operators:[CPContainsPredicateOperatorType, + CPBeginsWithPredicateOperatorType, + CPEndsWithPredicateOperatorType, + CPEqualToPredicateOperatorType, + CPNotEqualToPredicateOperatorType, + CPMatchesPredicateOperatorType] + options:CPCaseInsensitivePredicateOption | CPDiacriticInsensitivePredicateOption]; + + var numbers = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"age", @"salary"]) + rightExpressionAttributeType:CPInteger32AttributeType + modifier:CPDirectPredicateModifier + operators:[CPEqualToPredicateOperatorType, + CPNotEqualToPredicateOperatorType, + CPLessThanPredicateOperatorType, + CPLessThanOrEqualToPredicateOperatorType, + CPGreaterThanPredicateOperatorType, + CPGreaterThanOrEqualToPredicateOperatorType] + options:0]; + + var departmentValues = []; + + for (var i = 0; i < LibraryDepartments.length; i++) + departmentValues.push([CPExpression expressionForConstantValue:LibraryDepartments[i]]); + + var departments = [[LabeledRowTemplate alloc] initWithLeftExpressions:keyPaths([@"department"]) + rightExpressions:departmentValues + modifier:CPDirectPredicateModifier + operators:[CPEqualToPredicateOperatorType, CPNotEqualToPredicateOperatorType] + options:0]; + + return [compound, strings, numbers, departments]; +} + +// ---------------------------------------------------------------- Running the sheet + +// aFolder == nil creates a new folder. aCallback(folderOrNil) is invoked after OK. +- (void)beginEditingFolder:(SmartFolder)aFolder modalForWindow:(CPWindow)aWindow completion:(Function)aCallback +{ + _folder = aFolder; + _completion = aCallback; + + [_nameField setStringValue:aFolder ? [aFolder name] : @"Untitled Smart Folder"]; + + var predicate = aFolder ? [aFolder predicate] : [CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @""]; + [self _setEditorPredicate:predicate]; + + [CPApp beginSheet:[self window] + modalForWindow:aWindow + modalDelegate:self + didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) + contextInfo:nil]; + + [[self window] makeFirstResponder:_nameField]; + [_nameField selectText:self]; + + [self _updatePreview]; + [self _fitWindowToRows]; +} + +- (void)_setEditorPredicate:(CPPredicate)aPredicate +{ + // In compound nesting mode the root row must be a compound predicate. + if (![aPredicate isKindOfClass:[CPCompoundPredicate class]]) + aPredicate = [CPCompoundPredicate andPredicateWithSubpredicates:[aPredicate]]; + + try + { + [_predicateEditor setObjectValue:aPredicate]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: CPPredicateEditor could not represent " + [aPredicate predicateFormat] + ": " + e); + [_predicateEditor setObjectValue:[CPCompoundPredicate andPredicateWithSubpredicates:[[CPPredicate predicateWithFormat:@"lastName CONTAINS[cd] %@", @""]]]]; + } +} + +- (CPPredicate)_currentPredicate +{ + try + { + [_predicateEditor reloadPredicate]; + return [_predicateEditor predicate]; + } + catch (e) + { + CPLog.warn("SmartFoldersDemo: could not build predicate: " + e); + return nil; + } +} + +- (void)closeSheet:(id)sender +{ + if ([sender tag] === CPOKButton) + { + if (![[[_nameField stringValue] stringByTrimmingWhitespace] length]) + { + [[self window] makeFirstResponder:_nameField]; + return; + } + + if (![self _currentPredicate]) + return; + } + + [CPApp endSheet:[self window] returnCode:[sender tag]]; +} + +- (void)sheetDidEnd:(CPWindow)aSheet returnCode:(int)aReturnCode contextInfo:(id)aContext +{ + [aSheet orderOut:self]; + + var callback = _completion, + folder = _folder; + + _completion = nil; + _folder = nil; + + if (aReturnCode !== CPOKButton) + return; + + var name = [[_nameField stringValue] stringByTrimmingWhitespace], + predicate = [self _currentPredicate]; + + if (folder) + [_library updateSmartFolder:folder name:name predicate:predicate]; + else + folder = [_library addSmartFolderWithName:name predicate:predicate]; + + if (callback) + callback(folder); +} + +// ---------------------------------------------------------------- Live preview + +- (void)predicateEditorChanged:(id)sender +{ + [self _updatePreview]; +} + +- (void)ruleEditorRowsDidChange:(CPNotification)aNotification +{ + [self _updatePreview]; + [self _fitWindowToRows]; +} + +- (void)_updatePreview +{ + var predicate = [self _currentPredicate], + matches = predicate ? [_library peopleMatchingPredicate:predicate] : nil; + + [_formatField setStringValue:predicate ? [predicate predicateFormat] : @"(incomplete predicate)"]; + + if (matches) + [_matchField setStringValue:[CPString stringWithFormat:@"%d of %d people match", [matches count], [[_library people] count]]]; + else + [_matchField setStringValue:@"Predicate cannot be evaluated"]; + + [_okButton setEnabled:!!matches]; +} + +- (void)_fitWindowToRows +{ + var window = [self window], + rows = MAX([_predicateEditor numberOfRows], 1), + desired = EditorChromeHeight + rows * EditorRowHeight + 6, + height = MIN(MAX(desired, EditorMinHeight), EditorMaxHeight), + frame = [window frame]; + + // Make sure the editor's own frame tracks its rows so the scroll view can scroll. + [_predicateEditor setFrameSize:CGSizeMake(CGRectGetWidth([[_editorScrollView contentView] bounds]), rows * EditorRowHeight)]; + + if (ABS(CGRectGetHeight(frame) - height) < 1) + return; + + [window setFrame:CGRectMake(CGRectGetMinX(frame), CGRectGetMinY(frame), CGRectGetWidth(frame), height) display:YES animate:NO]; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/SourceListViewController.j b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j new file mode 100644 index 000000000..49f2b0353 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/SourceListViewController.j @@ -0,0 +1,721 @@ +/* + * SourceListViewController.j + * SmartFoldersDemo + * + * Sidebar. Exercises: + * - CPViewController with a programmatic -loadView + * - view-based CPOutlineView (outlineView:viewForTableColumn:item:, + * makeViewWithIdentifier:owner:, CPTableCellView subclass) + * - source list highlight style, expand/collapse, non-selectable headers + * - custom drawing (CPBezierPath) for icons and count badges + * - drag & drop: reordering groups / smart folders inside their section, + * dropping people (from the table) onto a group, or onto the GROUPS + * header to create a new group + * - CPButtonBar with plus / minus / action pop-up and a split view + * resize control + */ + +@import +@import "Library.j" + +var SourceCellIdentifier = @"SourceCell", + SourceRowHeight = 26.0; + +// Tags of the action pop-up items. +SourceActionNewGroup = 1; +SourceActionNewSmartFolder = 2; +SourceActionEdit = 3; +SourceActionRename = 4; +SourceActionDelete = 5; + + +// -------------------------------------------------------------------------------- +// SourceIconView – small vector icon drawn with CPBezierPath +// -------------------------------------------------------------------------------- + +@implementation SourceIconView : CPView +{ + CPString _iconKind; + BOOL _isHighlighted; +} + +- (void)setKind:(CPString)aKind +{ + if (_iconKind === aKind) + return; + + _iconKind = aKind; + [self setNeedsDisplay:YES]; +} + +- (void)setSelected:(BOOL)aFlag +{ + if (_isHighlighted === aFlag) + return; + + _isHighlighted = aFlag; + [self setNeedsDisplay:YES]; +} + +- (void)drawRect:(CGRect)aRect +{ + var bounds = [self bounds], + w = CGRectGetWidth(bounds), + h = CGRectGetHeight(bounds); + + if (_iconKind === @"all") + { + var color = _isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.33 green:0.45 blue:0.62 alpha:1.0], + head = [CPBezierPath bezierPath], + body = [CPBezierPath bezierPath]; + + [color set]; + [head appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 3.5, 1.5, 7, 7)]; + [head fill]; + [body appendBezierPathWithRoundedRect:CGRectMake(w / 2 - 6.5, 9.5, 13, 6) xRadius:3 yRadius:3]; + [body fill]; + return; + } + + // Folder shape for groups and smart folders. + var fill = _isHighlighted ? [CPColor whiteColor] : + (_iconKind === @"smart" ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0] + : [CPColor colorWithRed:0.38 green:0.62 blue:0.88 alpha:1.0]), + folder = [CPBezierPath bezierPath]; + + [folder moveToPoint:CGPointMake(1, 3)]; + [folder lineToPoint:CGPointMake(6, 3)]; + [folder lineToPoint:CGPointMake(8, 5)]; + [folder lineToPoint:CGPointMake(w - 1, 5)]; + [folder lineToPoint:CGPointMake(w - 1, h - 2)]; + [folder lineToPoint:CGPointMake(1, h - 2)]; + [folder closePath]; + + [fill set]; + [folder fill]; + + if (_iconKind === @"smart") + { + // Gear-ish dot to distinguish smart folders. + var mark = [CPBezierPath bezierPath], + markColor = _isHighlighted ? [CPColor colorWithRed:0.55 green:0.42 blue:0.78 alpha:1.0] : [CPColor whiteColor]; + + [markColor set]; + [mark appendBezierPathWithOvalInRect:CGRectMake(w / 2 - 2.5, 7.5, 5, 5)]; + [mark fill]; + } +} + +@end + + +// -------------------------------------------------------------------------------- +// SourceBadgeView – rounded count badge +// -------------------------------------------------------------------------------- + +var BadgeFont = nil, + BadgeHeight = 16.0, + BadgeMinWidth = 24.0, + BadgeHPadding = 7.0; + +@implementation SourceBadgeView : CPView +{ + CPTextField _badgeLabel; + int _badgeCount; + BOOL _isHighlighted; +} + ++ (void)initialize +{ + if (self !== [SourceBadgeView class]) + return; + + BadgeFont = [CPFont boldSystemFontOfSize:11.0]; +} + +- (id)initWithFrame:(CGRect)aFrame +{ + if (self = [super initWithFrame:aFrame]) + { + // A plain, inset-free label that is positioned by hand: the text field's + // own vertical centering includes theme insets and ends up a pixel or two low. + _badgeLabel = [[CPTextField alloc] initWithFrame:CGRectMakeZero()]; + [_badgeLabel setFont:BadgeFont]; + [_badgeLabel setAlignment:CPCenterTextAlignment]; + [_badgeLabel setEditable:NO]; + [_badgeLabel setSelectable:NO]; + [_badgeLabel setBezeled:NO]; + [_badgeLabel setBordered:NO]; + [_badgeLabel setDrawsBackground:NO]; + [_badgeLabel setValue:CGInsetMakeZero() forThemeAttribute:@"content-inset"]; + [self addSubview:_badgeLabel]; + + [self _updateColors]; + } + + return self; +} + ++ (CPString)stringForCount:(int)aCount +{ + // Keep badges compact: 1234 -> "1.2k", 12345 -> "12k" + if (aCount >= 10000) + return Math.round(aCount / 1000) + "k"; + + if (aCount >= 1000) + return (Math.round(aCount / 100) / 10) + "k"; + + return String(aCount); +} + +- (void)setCount:(int)aCount +{ + _badgeCount = aCount; + + var hidden = aCount === CPNotFound || aCount === nil || aCount === undefined; + + [self setHidden:hidden]; + + if (!hidden) + { + [_badgeLabel setStringValue:[SourceBadgeView stringForCount:aCount]]; + [self _layoutLabel]; + } + + [self setNeedsDisplay:YES]; +} + +- (void)setSelected:(BOOL)aFlag +{ + if (_isHighlighted === aFlag) + return; + + _isHighlighted = aFlag; + [self _updateColors]; + [self setNeedsDisplay:YES]; +} + +- (float)preferredWidth +{ + var size = [[_badgeLabel stringValue] sizeWithFont:BadgeFont]; + return MAX(BadgeMinWidth, Math.ceil(size.width) + 2 * BadgeHPadding); +} + +- (void)setFrameSize:(CGSize)aSize +{ + [super setFrameSize:aSize]; + [self _layoutLabel]; +} + +- (void)_layoutLabel +{ + var bounds = [self bounds], + textHeight = Math.ceil([[_badgeLabel stringValue] sizeWithFont:BadgeFont].height); + + [_badgeLabel setFrame:CGRectMake(0, Math.round((CGRectGetHeight(bounds) - textHeight) / 2), CGRectGetWidth(bounds), textHeight)]; +} + +- (void)_updateColors +{ + [_badgeLabel setTextColor:_isHighlighted ? [CPColor colorWithRed:0.29 green:0.54 blue:0.82 alpha:1.0] : [CPColor whiteColor]]; +} + +- (void)drawRect:(CGRect)aRect +{ + var bounds = [self bounds], + radius = CGRectGetHeight(bounds) / 2, + path = [CPBezierPath bezierPath]; + + [path appendBezierPathWithRoundedRect:bounds xRadius:radius yRadius:radius]; + + [(_isHighlighted ? [CPColor whiteColor] : [CPColor colorWithRed:0.56 green:0.63 blue:0.76 alpha:1.0]) set]; + [path fill]; +} + +@end + + +// -------------------------------------------------------------------------------- +// SourceListCellView – view-based outline cell +// -------------------------------------------------------------------------------- + +@implementation SourceListCellView : CPTableCellView +{ + SourceIconView _iconView; + CPTextField _titleField; + SourceBadgeView _badgeView; + BOOL _isHeader; +} + +- (id)initWithFrame:(CGRect)aFrame +{ + if (self = [super initWithFrame:aFrame]) + { + var height = CGRectGetHeight(aFrame); + + _iconView = [[SourceIconView alloc] initWithFrame:CGRectMake(2, (height - 16) / 2, 18, 16)]; + [_iconView setAutoresizingMask:CPViewMinYMargin | CPViewMaxYMargin]; + [self addSubview:_iconView]; + + _titleField = [[CPTextField alloc] initWithFrame:CGRectMake(24, 0, CGRectGetWidth(aFrame) - 24, height)]; + [_titleField setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [_titleField setVerticalAlignment:CPCenterVerticalTextAlignment]; + [_titleField setLineBreakMode:CPLineBreakByTruncatingTail]; + [_titleField setEditable:NO]; + [_titleField setBezeled:NO]; + [_titleField setDrawsBackground:NO]; + [self addSubview:_titleField]; + + _badgeView = [[SourceBadgeView alloc] initWithFrame:CGRectMake(CGRectGetWidth(aFrame) - 30, (height - BadgeHeight) / 2, BadgeMinWidth, BadgeHeight)]; + [_badgeView setAutoresizingMask:CPViewMinXMargin | CPViewMinYMargin | CPViewMaxYMargin]; + [self addSubview:_badgeView]; + + [self setIdentifier:SourceCellIdentifier]; + } + + return self; +} + +- (void)configureWithItem:(SourceItem)anItem count:(int)aCount +{ + _isHeader = [anItem isSection]; + + var bounds = [self bounds], + width = CGRectGetWidth(bounds), + height = CGRectGetHeight(bounds); + + [_titleField setStringValue:[anItem name]]; + + if (_isHeader) + { + [_iconView setHidden:YES]; + [_badgeView setHidden:YES]; + [_titleField setFont:[CPFont boldSystemFontOfSize:11.0]]; + [_titleField setFrame:CGRectMake(0, 0, width, height)]; + } + else + { + var kind = [anItem isKindOfClass:[SmartFolder class]] ? @"smart" : + [anItem isKindOfClass:[LibraryGroup class]] ? @"group" : @"all"; + + [_iconView setHidden:NO]; + [_iconView setKind:kind]; + [_titleField setFont:[CPFont systemFontOfSize:12.0]]; + + [_badgeView setCount:aCount]; + + var badgeWidth = [_badgeView isHidden] ? 0 : [_badgeView preferredWidth]; + + [_badgeView setFrame:CGRectMake(width - badgeWidth - 8, Math.round((height - BadgeHeight) / 2), badgeWidth, BadgeHeight)]; + [_titleField setFrame:CGRectMake(24, 0, MAX(0, width - 24 - badgeWidth - 14), height)]; + } + + [self _updateSelectionAppearance]; +} + +- (void)setThemeState:(ThemeState)aState +{ + [super setThemeState:aState]; + [self _updateSelectionAppearance]; +} + +- (void)unsetThemeState:(ThemeState)aState +{ + [super unsetThemeState:aState]; + [self _updateSelectionAppearance]; +} + +- (void)_updateSelectionAppearance +{ + // CPTableView marks data views of selected rows with CPThemeStateSelectedDataView + // and, only while the table is focused (first responder in the key window), + // additionally with CPThemeStateFirstResponder. The unfocused selection colour + // is a light gray, so white text is only legible in the focused case. + var selected = [self hasThemeState:CPThemeStateSelectedDataView], + emphasized = selected && [self hasThemeState:CPThemeStateFirstResponder]; + + [_iconView setSelected:emphasized]; + [_badgeView setSelected:emphasized]; + + if (_isHeader) + [_titleField setTextColor:[CPColor colorWithWhite:0.45 alpha:1.0]]; + else + [_titleField setTextColor:emphasized ? [CPColor whiteColor] : [CPColor colorWithWhite:0.15 alpha:1.0]]; +} + +@end + + +// -------------------------------------------------------------------------------- +// SourceListViewController +// -------------------------------------------------------------------------------- + +@implementation SourceListViewController : CPViewController +{ + Library _library; + CPOutlineView _outlineView; + CPButtonBar _buttonBar; + BOOL _suppressSelectionCallbacks; + + id _sourceListDelegate @accessors(property=delegate); +} + +- (id)initWithLibrary:(Library)aLibrary +{ + if (self = [super initWithCibName:nil bundle:nil]) + _library = aLibrary; + + return self; +} + +- (CPButtonBar)buttonBar +{ + [self view]; + return _buttonBar; +} + +- (void)loadView +{ + var frame = CGRectMake(0, 0, 230, 500), + view = [[CPView alloc] initWithFrame:frame], + barHeight = 26.0; + + [view setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + + // --- Outline in scroll view + var scrollView = [[CPScrollView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame) - barHeight)]; + [scrollView setAutoresizingMask:CPViewWidthSizable | CPViewHeightSizable]; + [scrollView setAutohidesScrollers:YES]; + [scrollView setHasHorizontalScroller:NO]; + [scrollView setBorderType:CPNoBorder]; + + _outlineView = [[CPOutlineView alloc] initWithFrame:[[scrollView contentView] bounds]]; + + var column = [[CPTableColumn alloc] initWithIdentifier:@"source"]; + [column setWidth:CGRectGetWidth(frame) - 20]; + [column setResizingMask:CPTableColumnAutoresizingMask]; + [column setEditable:NO]; + [_outlineView addTableColumn:column]; + [_outlineView setOutlineTableColumn:column]; + + [_outlineView setHeaderView:nil]; + [_outlineView setCornerView:nil]; + [_outlineView setColumnAutoresizingStyle:CPTableViewLastColumnOnlyAutoresizingStyle]; + [_outlineView setSelectionHighlightStyle:CPTableViewSelectionHighlightStyleSourceList]; + [_outlineView setBackgroundColor:[CPColor colorWithRed:212.0 / 255.0 green:221.0 / 255.0 blue:230.0 / 255.0 alpha:1.0]]; + [_outlineView setRowHeight:SourceRowHeight]; + [_outlineView setAllowsMultipleSelection:NO]; + [_outlineView setAllowsEmptySelection:YES]; + + [_outlineView setTarget:self]; + [_outlineView setDoubleAction:@selector(outlineViewDoubleClicked:)]; + + [_outlineView registerForDraggedTypes:[PeopleDragType, SourceItemDragType]]; + [_outlineView setDraggingSourceOperationMask:CPDragOperationMove forLocal:YES]; + + [_outlineView setDataSource:self]; + [_outlineView setDelegate:self]; + + [scrollView setDocumentView:_outlineView]; + [view addSubview:scrollView]; + + // --- Button bar + _buttonBar = [[CPButtonBar alloc] initWithFrame:CGRectMake(0, CGRectGetHeight(frame) - barHeight, CGRectGetWidth(frame), barHeight)]; + [_buttonBar setAutoresizingMask:CPViewWidthSizable | CPViewMinYMargin]; + + if ([_buttonBar respondsToSelector:@selector(setHasResizeControl:)]) + [_buttonBar setHasResizeControl:YES]; + + if ([_buttonBar respondsToSelector:@selector(setResizeControlIsLeftAligned:)]) + [_buttonBar setResizeControlIsLeftAligned:NO]; + + var plusButton = [CPButtonBar plusButton]; + [plusButton setTarget:self]; + [plusButton setAction:@selector(_plusClicked:)]; + [plusButton setToolTip:@"New smart folder"]; + + var minusButton = [CPButtonBar minusButton]; + [minusButton setTarget:self]; + [minusButton setAction:@selector(_minusClicked:)]; + [minusButton setToolTip:@"Delete the selected group or smart folder"]; + + var actionButton = [CPButtonBar actionPopupButton]; + + if (![[actionButton itemArray] count]) + [actionButton addItemWithTitle:@""]; + + [self _addActionItem:@"New Group…" tag:SourceActionNewGroup to:actionButton]; + [self _addActionItem:@"New Smart Folder…" tag:SourceActionNewSmartFolder to:actionButton]; + [[actionButton menu] addItem:[CPMenuItem separatorItem]]; + [self _addActionItem:@"Edit Smart Folder…" tag:SourceActionEdit to:actionButton]; + [self _addActionItem:@"Rename Group…" tag:SourceActionRename to:actionButton]; + [self _addActionItem:@"Delete" tag:SourceActionDelete to:actionButton]; + [actionButton setTarget:self]; + [actionButton setAction:@selector(_actionPopupChosen:)]; + + [_buttonBar setButtons:[plusButton, minusButton, actionButton]]; + [view addSubview:_buttonBar]; + + [self setView:view]; + + [_outlineView reloadData]; + [self _expandSections]; +} + +- (void)_addActionItem:(CPString)aTitle tag:(int)aTag to:(CPPopUpButton)aPopUp +{ + [aPopUp addItemWithTitle:aTitle]; + [[aPopUp lastItem] setTag:aTag]; +} + +- (void)_expandSections +{ + var sections = [_library sections]; + + for (var i = 0; i < sections.length; i++) + [_outlineView expandItem:sections[i]]; +} + +// ---------------------------------------------------------------- Public + +- (id)selectedItem +{ + var row = [_outlineView selectedRow]; + return row >= 0 ? [_outlineView itemAtRow:row] : nil; +} + +- (void)selectItem:(id)anItem +{ + var row = [_outlineView rowForItem:anItem]; + + if (row === CPNotFound || row < 0) + return; + + [_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; + [_outlineView scrollRowToVisible:row]; +} + +// Reloads the outline (names, order, badges) without emitting selection callbacks. +- (void)reloadAndSelectItem:(id)anItem +{ + _suppressSelectionCallbacks = YES; + + [_outlineView reloadData]; + [self _expandSections]; + + var row = anItem ? [_outlineView rowForItem:anItem] : CPNotFound; + + if (row !== CPNotFound && row >= 0) + [_outlineView selectRowIndexes:[CPIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; + else + [_outlineView deselectAll:nil]; + + _suppressSelectionCallbacks = NO; +} + +- (void)focus +{ + [[_outlineView window] makeFirstResponder:_outlineView]; +} + +// ---------------------------------------------------------------- Actions + +- (void)_plusClicked:(id)sender +{ + [CPApp sendAction:@selector(newSmartFolder:) to:nil from:sender]; +} + +- (void)_minusClicked:(id)sender +{ + [CPApp sendAction:@selector(deleteSelectedSourceItem:) to:nil from:sender]; +} + +- (void)_actionPopupChosen:(id)sender +{ + var selector = nil; + + switch ([[sender selectedItem] tag]) + { + case SourceActionNewGroup: selector = @selector(newGroup:); break; + case SourceActionNewSmartFolder: selector = @selector(newSmartFolder:); break; + case SourceActionEdit: selector = @selector(editSelectedSmartFolder:); break; + case SourceActionRename: selector = @selector(renameSelectedGroup:); break; + case SourceActionDelete: selector = @selector(deleteSelectedSourceItem:); break; + } + + if (selector) + [CPApp sendAction:selector to:nil from:sender]; +} + +- (void)outlineViewDoubleClicked:(id)sender +{ + var row = [_outlineView clickedRow]; + + if (row < 0) + return; + + var item = [_outlineView itemAtRow:row]; + + if ([item isKindOfClass:[SmartFolder class]]) + [CPApp sendAction:@selector(editSelectedSmartFolder:) to:nil from:self]; + else if ([item isKindOfClass:[LibraryGroup class]]) + [CPApp sendAction:@selector(renameSelectedGroup:) to:nil from:self]; +} + +// ---------------------------------------------------------------- Data source + +- (int)outlineView:(CPOutlineView)anOutlineView numberOfChildrenOfItem:(id)anItem +{ + return [[_library childrenOfItem:anItem] count]; +} + +- (id)outlineView:(CPOutlineView)anOutlineView child:(int)anIndex ofItem:(id)anItem +{ + return [[_library childrenOfItem:anItem] objectAtIndex:anIndex]; +} + +- (BOOL)outlineView:(CPOutlineView)anOutlineView isItemExpandable:(id)anItem +{ + return [anItem isSection]; +} + +- (id)outlineView:(CPOutlineView)anOutlineView objectValueForTableColumn:(CPTableColumn)aColumn byItem:(id)anItem +{ + return anItem; +} + +// ---------------------------------------------------------------- Delegate (view based) + +- (CPView)outlineView:(CPOutlineView)anOutlineView viewForTableColumn:(CPTableColumn)aColumn item:(id)anItem +{ + var cell = [anOutlineView makeViewWithIdentifier:SourceCellIdentifier owner:self]; + + if (!cell) + cell = [[SourceListCellView alloc] initWithFrame:CGRectMake(0, 0, [aColumn width], SourceRowHeight)]; + + [cell configureWithItem:anItem count:[_library countForItem:anItem]]; + + return cell; +} + +- (BOOL)outlineView:(CPOutlineView)anOutlineView shouldSelectItem:(id)anItem +{ + return [anItem isSelectable]; +} + +- (void)outlineViewSelectionDidChange:(CPNotification)aNotification +{ + if (_suppressSelectionCallbacks) + return; + + var item = [self selectedItem]; + + if (item && [_sourceListDelegate respondsToSelector:@selector(sourceListViewController:didSelectItem:)]) + [_sourceListDelegate sourceListViewController:self didSelectItem:item]; +} + +// ---------------------------------------------------------------- Drag & drop + +- (BOOL)outlineView:(CPOutlineView)anOutlineView writeItems:(CPArray)theItems toPasteboard:(CPPasteboard)aPasteboard +{ + var item = [theItems firstObject]; + + if ([theItems count] !== 1 || ![item isUserItem]) + return NO; + + [aPasteboard declareTypes:[SourceItemDragType] owner:self]; + [aPasteboard setString:[item uid] forType:SourceItemDragType]; + + return YES; +} + +- (CPDragOperation)outlineView:(CPOutlineView)anOutlineView validateDrop:(id)theInfo proposedItem:(id)anItem proposedChildIndex:(int)anIndex +{ + var pasteboard = [theInfo draggingPasteboard], + types = [pasteboard types]; + + // People from the table + if ([types containsObject:PeopleDragType]) + { + if ([anItem isKindOfClass:[LibraryGroup class]] && anIndex === CPOutlineViewDropOnItemIndex) + return CPDragOperationCopy; + + // Anywhere on/inside the GROUPS section: create a new group. + if (anItem === [_library groupsSection]) + { + [anOutlineView setDropItem:anItem dropChildIndex:CPOutlineViewDropOnItemIndex]; + return CPDragOperationCopy; + } + + return CPDragOperationNone; + } + + // Reordering of groups / smart folders + if ([types containsObject:SourceItemDragType]) + { + var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]], + section = [_library sectionForItem:dragged]; + + if (!dragged || !section) + return CPDragOperationNone; + + if (anItem === section && anIndex !== CPOutlineViewDropOnItemIndex) + return CPDragOperationMove; + + // Dropped onto a sibling: retarget to "insert above sibling". + if (anIndex === CPOutlineViewDropOnItemIndex && [_library sectionForItem:anItem] === section) + { + [anOutlineView setDropItem:section dropChildIndex:[[section children] indexOfObjectIdenticalTo:anItem]]; + return CPDragOperationMove; + } + } + + return CPDragOperationNone; +} + +- (BOOL)outlineView:(CPOutlineView)anOutlineView acceptDrop:(id)theInfo item:(id)anItem childIndex:(int)anIndex +{ + var pasteboard = [theInfo draggingPasteboard], + types = [pasteboard types]; + + if ([types containsObject:PeopleDragType]) + { + var uids = [CPKeyedUnarchiver unarchiveObjectWithData:[pasteboard dataForType:PeopleDragType]], + people = [_library peopleWithUIDs:uids || []]; + + if (![people count]) + return NO; + + if ([anItem isKindOfClass:[LibraryGroup class]]) + { + [_library addPeople:people toGroup:anItem]; + return YES; + } + + if (anItem === [_library groupsSection]) + { + var name = [people count] === 1 ? [people[0] fullName] : [CPString stringWithFormat:@"%d People", [people count]], + group = [_library addGroupWithName:name people:people]; + + [self selectItem:group]; + return YES; + } + + return NO; + } + + if ([types containsObject:SourceItemDragType]) + { + var dragged = [_library itemWithUID:[pasteboard stringForType:SourceItemDragType]]; + + if (!dragged) + return NO; + + [_library moveItem:dragged toIndex:anIndex]; + return YES; + } + + return NO; +} + +@end diff --git a/Tests/Manual/SmartFoldersDemo/index-debug.html b/Tests/Manual/SmartFoldersDemo/index-debug.html new file mode 100644 index 000000000..c5df711dc --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/index-debug.html @@ -0,0 +1,213 @@ + + + + + + + + + + + + + + + + + + SmartFoldersDemoUpdated + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/SmartFoldersDemo/index.html b/Tests/Manual/SmartFoldersDemo/index.html new file mode 100644 index 000000000..ac42c98a7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/index.html @@ -0,0 +1,166 @@ + + + + + + + + + + + + + + + + + + __project.name__ + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/SmartFoldersDemo/main.j b/Tests/Manual/SmartFoldersDemo/main.j new file mode 100644 index 000000000..c446f82f7 --- /dev/null +++ b/Tests/Manual/SmartFoldersDemo/main.j @@ -0,0 +1,18 @@ +/* + * AppController.j + * null + * + * Created by You on November 25, 2010. + * Copyright 2010, Your Company All rights reserved. + */ + +@import +@import + +@import "AppController.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} diff --git a/Tests/Manual/TextEditor/AppController.j b/Tests/Manual/TextEditor/AppController.j new file mode 100644 index 000000000..88e83ba76 --- /dev/null +++ b/Tests/Manual/TextEditor/AppController.j @@ -0,0 +1,39 @@ +/* + * AppController.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import +@import + + +@implementation AppController : CPObject + +- (void)applicationDidFinishLaunching:(CPNotification)aNotification +{ + // This is called when the application is done loading. + // Uncomment the following line to hide the standard menu bar. + // [CPMenu setMenuBarVisible:NO]; +} + +@end diff --git a/Tests/Manual/TextEditor/Document.j b/Tests/Manual/TextEditor/Document.j new file mode 100644 index 000000000..5192c79f3 --- /dev/null +++ b/Tests/Manual/TextEditor/Document.j @@ -0,0 +1,39 @@ +/* + * Document.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import +@import + +@import "DocumentWindowController.j" + + +@implementation Document : CPDocument + +- (void)makeWindowControllers +{ + [self addWindowController:[[DocumentWindowController alloc] init]]; +} + +@end diff --git a/Tests/Manual/TextEditor/DocumentWindowController.j b/Tests/Manual/TextEditor/DocumentWindowController.j new file mode 100644 index 000000000..e1ecd6264 --- /dev/null +++ b/Tests/Manual/TextEditor/DocumentWindowController.j @@ -0,0 +1,193 @@ +/* + * DocumentWindowController.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import +@import + +@import "LoremIpsum.j" +@import "TableExample.j" + + +/* + The smallest size, in points, to which the user can resize a document + window. The size includes the title bar, the toolbar and the ruler, so + the values leave room for a few lines of text below them. + + These values are provisional. Adjust them as the window gains more + content, such as an inspector pane. +*/ +var DocumentWindowMinimumWidth = 360.0, + DocumentWindowMinimumHeight = 240.0; + + +/*! + Owns DocumentWindow.cib. The window's delegate outlet is connected to + File's Owner in DocumentWindow.xib, so this controller is the window's + delegate. windowDidLoad assigns it again, so the delegate is set even if + the connection is missing from the xib. +*/ +@implementation DocumentWindowController : CPWindowController +{ + /* + The text view inside the window's scroll view. The outlet is connected + to the text view in DocumentWindow.xib. + */ + @outlet CPTextView textView; +} + +- (id)init +{ + return [self initWithWindowCibName:@"DocumentWindow"]; +} + +- (void)windowDidLoad +{ + [super windowDidLoad]; + + [[self window] setDelegate:self]; + + /* + Show a horizontal ruler above the text. + + A ruler is not a separate control. It belongs to the scroll view that + encloses the text view, and the text view keeps its markers in step + with the tab stops and indents of the paragraph at the selection. + setRulerVisible: asks the text view to show the ruler, and the + Format menu's Show Ruler item sends toggleRuler: to hide or show it + again. + + In Interface Builder the text view's "Uses Ruler" setting decides + whether the text view may show a ruler at all, but nib2cib does not + yet carry the ruler settings into the cib, so the ruler is shown here + in code. + */ + [textView setRulerVisible:YES]; +} + +/*! + Inserts the placeholder text from LoremIpsum at the text view's selection, + replacing any selected text. + + The Edit menu item in MainMenu.xib sends this action to First Responder. + The menu does not know about any document. AppKit passes the action along + the responder chain of the key window, then of the main window: the text + view, its superviews, the window, and then this controller, which is the + window's next responder. So the text goes into the active document's + window, and AppKit disables the menu item when no document window is + active. + + insertText: changes the text the same way typing does. It registers the + change with the document's undo manager, so Edit > Undo removes the + inserted text. +*/ +- (@action)insertLoremIpsum:(id)aSender +{ + [textView insertText:[LoremIpsum text]]; +} + +/*! + Inserts the sample document from TableExample at the text view's + selection, replacing any selected text. The sample contains a table. + + Like Insert Lorem Ipsum, the Edit menu item sends this action to First + Responder, and the responder chain delivers it to the controller of the + active document's window. + + The sample is an attributed string, so insertText: keeps its fonts and + its table, but only in a rich text view. A plain text view would keep + only the characters, so the table needs rich text. Edit > Undo removes + the inserted text, with the table. +*/ +- (@action)insertTableExample:(id)aSender +{ + [textView insertText:[TableExample attributedString]]; +} + +/*! + Keeps the window at or above its minimum size while the user resizes it. + + This is a CPWindowDelegate method. The window calls it with the size it + is about to take, and uses the size that this method returns. Returning + a larger size than the one proposed stops the window from shrinking past + the minimum, while it still follows the pointer in every other direction. + + A fixed minimum could also be set on the window in Interface Builder. + The delegate method is used here because it can compute the minimum, for + example from the content of the window. +*/ +- (CGSize)windowWillResize:(CPWindow)aWindow toSize:(CGSize)aSize +{ + return CGSizeMake(Math.max(aSize.width, DocumentWindowMinimumWidth), + Math.max(aSize.height, DocumentWindowMinimumHeight)); +} + +/*! + Turns undo on or off for this document's text view. + + This is test scaffolding, not a Cocoa convention: Cocoa apps set + allowsUndo once, in Interface Builder or in code. The Edit > Allow Undo + menu item exists so that both settings can be tried while the app runs. + It will move to a Settings window. + + Like Insert Lorem Ipsum, the menu item sends its action to First + Responder, and the responder chain delivers it to the controller of the + active document's window. Neither the text view nor the window responds + to toggleAllowsUndo:, so the action passes them by. +*/ +- (@action)toggleAllowsUndo:(id)aSender +{ + var allowsUndo = ![textView allowsUndo]; + + [textView setAllowsUndo:allowsUndo]; + + /* + A text view that does not allow undo registers no changes, and does + not respond to undo: or redo:, so those actions go on to the window + and its undo manager. Remove the changes the text view registered + before undo was turned off, so that Edit > Undo cannot still undo + them through the window. + */ + if (!allowsUndo) + [[[self window] undoManager] removeAllActionsWithTarget:textView]; +} + +/*! + Validates the menu items whose actions this controller handles. + + AppKit calls this method before it shows a menu, for each item whose + action reaches this controller through the responder chain. For Allow + Undo, it sets the item's check mark from the active document's text + view, so the mark follows the document whose window is in front. Every + item is enabled. +*/ +- (BOOL)validateMenuItem:(CPMenuItem)aMenuItem +{ + if ([aMenuItem action] === @selector(toggleAllowsUndo:)) + [aMenuItem setState:[textView allowsUndo] ? CPOnState : CPOffState]; + + return YES; +} + +@end diff --git a/Tests/Manual/TextEditor/Info.plist b/Tests/Manual/TextEditor/Info.plist new file mode 100644 index 000000000..290c9e51e --- /dev/null +++ b/Tests/Manual/TextEditor/Info.plist @@ -0,0 +1,29 @@ + + + + + CPBundlePackageType + APPL + CPBundleIdentifier + dev.cappuccino.TextEditor + Main cib file base name + MainMenu.cib + CPBundleName + TextEditor + CPBundleDocumentTypes + + + CPBundleTypeName + Document + CPDocumentClass + Document + + + CPBundleVersion + 1.0 + CPHumanReadableCopyright + Copyright © 2026, David Richardson, MIT Licensed + CPBundleDisplayName + TextEdit by Cappuccino + + diff --git a/Tests/Manual/TextEditor/Jakefile b/Tests/Manual/TextEditor/Jakefile new file mode 100644 index 000000000..b06ee5fc0 --- /dev/null +++ b/Tests/Manual/TextEditor/Jakefile @@ -0,0 +1,197 @@ +/* + * Jakefile + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +const path = require("path"); +const fs = require("fs"); + +var ENV = process.env, + task = JAKE.task, + FileList = JAKE.FileList, + app = CAPPUCCINO.Jake.applicationtask.app, + configuration = ENV["CONFIG"] || ENV["CONFIGURATION"] || ENV["c"] || "Debug", + OS = require("os"), + projectName = "TextEditor"; + +var buildDir = path.resolve(ENV["BUILD_PATH"] || ENV["CAPP_BUILD"] || "Build"); + +app (projectName, function(task) +{ + ENV["OBJJ_INCLUDE_PATHS"] = "Frameworks"; + + if (configuration === "Debug") + ENV["OBJJ_INCLUDE_PATHS"] = path.join(ENV["OBJJ_INCLUDE_PATHS"], configuration); + + task.setBuildIntermediatesPath(path.join(buildDir, "TextEditor.build", configuration)); + task.setBuildPath(path.join(buildDir, configuration)); + + task.setProductName("TextEditor"); + task.setIdentifier("dev.cappuccino.TextEditor"); + task.setVersion("1.0"); + task.setAuthor("David Richardson"); + task.setSummary("TextEditor"); + task.setSources(new FileList("**/*.j").exclude(path.join("Build", "**")).exclude(path.join("Frameworks", "Source", "**"))); + task.setResources(new FileList("Resources/**")); + task.setIndexFilePath("index.html"); + task.setInfoPlistPath("Info.plist"); + + if (configuration === "Debug") + task.setCompilerFlags("-DDEBUG -g -S --inline-msg-send"); + else + task.setCompilerFlags("-O2"); +}); + +task ("default", [projectName], function() +{ + printResults(configuration); +}); + +task ("build", ["default"], function() +{ + updateApplicationSize(); +}); + +task ("debug", function() +{ + configuration = ENV["CONFIGURATION"] = "Debug"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("release", function() +{ + configuration = ENV["CONFIGURATION"] = "Release"; + JAKE.subjake(["."], "build", ENV); +}); + +task ("run", ["debug"], function() +{ + OS.system(["open", path.join(buildDir, "Debug", projectName, "index.html")]); +}); + +task ("run-release", ["release"], function() +{ + OS.system(["open", path.join(buildDir, "Release", projectName, "index.html")]); +}); + +task ("deploy", ["release"], function() +{ + FILE.mkdirs(path.join(buildDir, "Deployment", projectName)); + OS.system(["press", "-f", path.join(buildDir, "Release", projectName), path.join(buildDir, "Deployment", projectName)]); + printResults("Deployment") +}); + +function printResults(configuration) +{ + console.log("----------------------------"); + console.log(configuration+" app built at path: " + path.join(buildDir, configuration, projectName)); + console.log("----------------------------"); +} + +function updateApplicationSize() +{ + console.log("Calculating application file sizes..."); + + var contents = fs.readFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), { encoding: "utf8" }), + format = CFPropertyList.sniffedFormatOfString(contents), + plist = CFPropertyList.propertyListFromString(contents), + totalBytes = {executable:0, data:0, mhtml:0}; + + // Get the size of all framework executables and sprite data + var frameworksDir = "Frameworks"; + + if (configuration === "Debug") + frameworksDir = path.join(frameworksDir, "Debug"); + + var frameworks = []; + + if (fs.existsSync(frameworksDir)) { + frameworks = fs.readdirSync(frameworksDir); + } + //var frameworks = FILE.list(frameworksDir); + + frameworks.forEach(function(framework) + { + if (framework !== "Source") + addBundleFileSizes(path.join(frameworksDir, framework), totalBytes); + }); + + // Read in the default theme name, and attempt to get its size + var themeName = plist.valueForKey("CPDefaultTheme") || "Aristo2", + themePath = nil; + + if (themeName === "Aristo" || themeName === "Aristo2") + themePath = path.join(frameworksDir, "AppKit", "Resources", themeName + ".blend"); + else + themePath = path.join("Frameworks", "Resources", themeName + ".blend"); + + if (fs.existsSync(themePath) && fs.lstatSync(themePath).isDirectory()) + addBundleFileSizes(themePath, totalBytes); + + // Add sizes for the app + addBundleFileSizes(path.join(buildDir, configuration, projectName), totalBytes); + + console.log("Executables: " + totalBytes.executable + ", sprite data: " + totalBytes.data + ", total: " + (totalBytes.executable + totalBytes.data)); + + var dict = new CFMutableDictionary(); + + dict.setValueForKey("executable", totalBytes.executable); + dict.setValueForKey("data", totalBytes.data); + dict.setValueForKey("mhtml", totalBytes.mhtml); + + plist.setValueForKey("CPApplicationSize", dict); + fs.writeFileSync(path.join(buildDir, configuration, projectName, "Info.plist"), CFPropertyList.stringFromPropertyList(plist, format), { encoding: "utf8" }); + //FILE.write(path.join(buildDir, configuration, projectName, "Info.plist"), CFPropertyList.stringFromPropertyList(plist, format), { charset:"UTF-8" }); +} + +function addBundleFileSizes(bundlePath, totalBytes) +{ + var bundleName = path.basename(bundlePath), + environment = bundleName === "Foundation" ? "Objj" : "Browser", + bundlePath = path.join(bundlePath, environment + ".environment"); + + if (fs.existsSync(bundlePath) && fs.lstatSync(bundlePath).isDirectory()) + { + var filename = bundleName + ".sj", + filePath = path.join(bundlePath, filename); + + if (fs.existsSync(filePath)) { + totalBytes.executable += fs.lstatSync(filePath).size; + } + + filePath = path.join(bundlePath, "dataURLs.txt"); + + if (fs.existsSync(filePath)) + totalBytes.data += fs.lstatSync(filePath).size; + + filePath = path.join(bundlePath, "MHTMLData.txt"); + + if (fs.existsSync(filePath)) + totalBytes.mhtml += fs.lstatSync(filePath).size; + + filePath = path.join(bundlePath, "MHTMLPaths.txt"); + + if (fs.existsSync(filePath)) + totalBytes.mhtml += fs.lstatSync(filePath).size; + } +} diff --git a/Tests/Manual/TextEditor/LICENSE b/Tests/Manual/TextEditor/LICENSE new file mode 100644 index 000000000..8193cfd04 --- /dev/null +++ b/Tests/Manual/TextEditor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 David Richardson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Tests/Manual/TextEditor/LoremIpsum.j b/Tests/Manual/TextEditor/LoremIpsum.j new file mode 100644 index 000000000..fa28354e2 --- /dev/null +++ b/Tests/Manual/TextEditor/LoremIpsum.j @@ -0,0 +1,49 @@ +/* + * LoremIpsum.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import + + +/*! + Supplies placeholder text for filling a document with sample content. + + The text is the traditional "Lorem ipsum" passage. It is kept in the + application itself, so inserting it needs no network access and no + resource file. +*/ +@implementation LoremIpsum : CPObject + +/*! + Returns the placeholder text as paragraphs separated by blank lines, + ending with a newline. +*/ ++ (CPString)text +{ + return @"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n\n" + + @"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt.\n\n" + + @"Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur.\n"; +} + +@end diff --git a/Tests/Manual/TextEditor/Resources/DocumentWindow.cib b/Tests/Manual/TextEditor/Resources/DocumentWindow.cib new file mode 100644 index 000000000..a2eff4e03 --- /dev/null +++ b/Tests/Manual/TextEditor/Resources/DocumentWindow.cib @@ -0,0 +1 @@ +280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;16E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;40E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;32E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;41E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;2;42E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;27E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;43E;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;19E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;2;44E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;2;45E;E;D;K;10;$classnameS;20;_CPCibWindowTemplateK;8;$classesA;S;20;_CPCibWindowTemplateS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;18E;K;32;_CPCibWindowTemplateViewClassKeyD;K;6;CP$UIDd;2;32E;K;34;_CPCibWindowTemplateWindowClassKeyD;K;6;CP$UIDd;2;46E;K;30;_CPCibWindowTemplateWTFlagsKeyD;K;6;CP$UIDd;2;47E;K;33;_CPCibWindowTemplateWindowRectKeyD;K;6;CP$UIDd;2;48E;K;33;_CPCibWindowTemplateScreenRectKeyD;K;6;CP$UIDd;2;49E;K;30;_CPCibWindowTempatStyleMaskKeyD;K;6;CP$UIDd;2;50E;K;34;_CPCibWindowTemplateWindowTitleKeyD;K;6;CP$UIDd;2;51E;K;33;_CPCibWindowTemplateWindowViewKeyD;K;6;CP$UIDd;2;21E;E;D;K;10;$classnameS;6;CPViewK;8;$classesA;S;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;20E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;54E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;60E;E;D;K;10;$classnameS;12;CPScrollViewK;8;$classesA;S;12;CPScrollViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;22E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;21E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;61E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;21E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;62E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;2;64E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;65E;K;23;CPScrollViewContentViewD;K;6;CP$UIDd;2;25E;K;29;CPScrollViewHeaderClipViewKeyD;K;6;CP$UIDd;1;0E;K;21;CPScrollViewVScrollerD;K;6;CP$UIDd;2;30E;K;21;CPScrollViewHScrollerD;K;6;CP$UIDd;2;29E;K;23;CPScrollViewVLineScrollD;K;6;CP$UIDd;2;66E;K;23;CPScrollViewVPageScrollD;K;6;CP$UIDd;2;66E;K;23;CPScrollViewHLineScrollD;K;6;CP$UIDd;2;66E;K;23;CPScrollViewHPageScrollD;K;6;CP$UIDd;2;66E;K;24;CPScrollViewHasVScrollerD;K;6;CP$UIDd;2;67E;K;24;CPScrollViewHasHScrollerD;K;6;CP$UIDd;2;59E;K;29;CPScrollViewAutohidesScrollerD;K;6;CP$UIDd;2;59E;K;25;CPScrollViewCornerViewKeyD;K;6;CP$UIDd;1;0E;K;31;CPScrollViewBottomCornerViewKeyD;K;6;CP$UIDd;2;68E;K;25;CPScrollViewBorderTypeKeyD;K;6;CP$UIDd;2;52E;K;28;CPScrollViewScrollerStyleKeyD;K;6;CP$UIDd;1;0E;K;32;CPScrollViewScrollerKnobStyleKeyD;K;6;CP$UIDd;2;52E;K;21;CPScrollViewHasVRulerD;K;6;CP$UIDd;1;0E;K;21;CPScrollViewHasHRulerD;K;6;CP$UIDd;1;0E;K;25;CPScrollViewRulersVisibleD;K;6;CP$UIDd;1;0E;K;18;CPScrollViewVRulerD;K;6;CP$UIDd;1;0E;K;18;CPScrollViewHRulerD;K;6;CP$UIDd;1;0E;E;D;K;10;$classnameS;10;CPClipViewK;8;$classesA;S;10;CPClipViewS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;24E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;23E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;17;CPViewSubviewsKeyD;K;6;CP$UIDd;2;69E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;23E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;70E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;71E;K;24;CPScrollViewDocumentViewD;K;6;CP$UIDd;2;27E;E;D;K;10;$classnameS;10;CPTextViewK;8;$classesA;S;10;CPTextViewS;6;CPTextS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;26E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;25E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;53E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;53E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;25E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;55E;K;21;CPViewBackgroundColorD;K;6;CP$UIDd;2;73E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;74E;K;23;CPTextViewIsEditableKeyD;K;6;CP$UIDd;2;67E;K;25;CPTextViewIsSelectableKeyD;K;6;CP$UIDd;2;67E;K;23;CPTextViewIsRichTextKeyD;K;6;CP$UIDd;2;67E;K;21;CPTextViewDelegateKeyD;K;6;CP$UIDd;1;0E;K;22;CPTextViewContainerKeyD;K;6;CP$UIDd;2;76E;K;32;CPTextViewInsertionPointColorKeyD;K;6;CP$UIDd;2;77E;K;35;CPTextViewSelectedTextAttributesKeyD;K;6;CP$UIDd;2;79E;K;23;CPTextViewAllowsUndoKeyD;K;6;CP$UIDd;2;67E;K;26;CPTextViewUsesFontPanelKeyD;K;6;CP$UIDd;2;67E;K;34;CPTextViewHorizontallyResizableKeyD;K;6;CP$UIDd;2;59E;K;32;CPTextViewVerticallyResizableKeyD;K;6;CP$UIDd;2;67E;K;9;CPMaxSizeD;K;6;CP$UIDd;2;80E;E;D;K;10;$classnameS;10;CPScrollerK;8;$classesA;S;10;CPScrollerS;9;CPControlS;6;CPViewS;11;CPResponderS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;28E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;23E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;81E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;82E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;23E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;70E;K;17;CPViewIsHiddenKeyD;K;6;CP$UIDd;2;83E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;84E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;85E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;86E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;52E;K;18;CPControlTargetKeyD;K;6;CP$UIDd;2;23E;K;18;CPControlActionKeyD;K;6;CP$UIDd;2;87E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;88E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;1;0E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;1;0E;K;23;CPScrollerIsVerticalKeyD;K;6;CP$UIDd;2;59E;K;24;CPScrollerKnobProportionD;K;6;CP$UIDd;2;89E;K;18;CPScrollerStyleKeyD;K;6;CP$UIDd;1;0E;E;D;K;6;$classD;K;6;CP$UIDd;2;28E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;23E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;12;CPViewTagKeyD;K;6;CP$UIDd;2;52E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;2;90E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;2;91E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;23E;K;22;CPViewAutoresizingMaskD;K;6;CP$UIDd;2;70E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;84E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;85E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;2;92E;K;17;CPControlValueKeyD;K;6;CP$UIDd;2;52E;K;18;CPControlTargetKeyD;K;6;CP$UIDd;2;23E;K;18;CPControlActionKeyD;K;6;CP$UIDd;2;93E;K;24;CPControlSendActionOnKeyD;K;6;CP$UIDd;2;88E;K;23;CPControlControlSizeKeyD;K;6;CP$UIDd;1;0E;K;33;CPControlBaseWrittingDirectionKeyD;K;6;CP$UIDd;1;0E;K;23;CPScrollerIsVerticalKeyD;K;6;CP$UIDd;2;67E;K;24;CPScrollerKnobProportionD;K;6;CP$UIDd;2;89E;K;18;CPScrollerStyleKeyD;K;6;CP$UIDd;1;0E;E;D;K;10;$classnameS;9;CPToolbarK;8;$classesA;S;9;CPToolbarS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;31E;K;22;CPToolbarIdentifierKeyD;K;6;CP$UIDd;2;94E;K;23;CPToolbarDisplayModeKeyD;K;6;CP$UIDd;2;89E;K;34;CPToolbarShowsBaselineSeparatorKeyD;K;6;CP$UIDd;2;59E;K;35;CPToolbarAllowsUserCustomizationKeyD;K;6;CP$UIDd;2;67E;K;21;CPToolbarIsVisibleKeyD;K;6;CP$UIDd;2;67E;K;20;CPToolbarSizeModeKeyD;K;6;CP$UIDd;2;89E;K;27;CPToolbarIdentifiedItemsKeyD;K;6;CP$UIDd;2;96E;K;24;CPToolbarDefaultItemsKeyD;K;6;CP$UIDd;2;97E;K;24;CPToolbarAllowedItemsKeyD;K;6;CP$UIDd;2;98E;K;27;CPToolbarSelectableItemsKeyD;K;6;CP$UIDd;2;99E;K;20;CPToolbarDelegateKeyD;K;6;CP$UIDd;1;0E;E;D;K;10;$classnameS;13;CPToolbarItemK;8;$classesA;S;13;CPToolbarItemS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;30;CPToolbarItemItemIdentifierKeyD;K;6;CP$UIDd;3;100E;K;21;CPToolbarItemLabelKeyD;K;6;CP$UIDd;3;101E;K;28;CPToolbarItemPaletteLabelKeyD;K;6;CP$UIDd;3;101E;K;23;CPToolbarItemToolTipKeyD;K;6;CP$UIDd;3;102E;K;19;CPToolbarItemTagKeyD;K;6;CP$UIDd;3;103E;K;22;CPToolbarItemTargetKeyD;K;6;CP$UIDd;1;0E;K;22;CPToolbarItemActionKeyD;K;6;CP$UIDd;3;104E;K;23;CPToolbarItemEnabledKeyD;K;6;CP$UIDd;2;67E;K;21;CPToolbarItemImageKeyD;K;6;CP$UIDd;3;106E;K;30;CPToolbarItemAlternateImageKeyD;K;6;CP$UIDd;1;0E;K;20;CPToolbarItemViewKeyD;K;6;CP$UIDd;1;0E;K;23;CPToolbarItemMinSizeKeyD;K;6;CP$UIDd;3;107E;K;23;CPToolbarItemMaxSizeKeyD;K;6;CP$UIDd;3;107E;K;34;CPToolbarItemVisibilityPriorityKeyD;K;6;CP$UIDd;2;52E;K;29;CPToolbarItemAutovalidatesKeyD;K;6;CP$UIDd;2;67E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;30;CPToolbarItemItemIdentifierKeyD;K;6;CP$UIDd;3;108E;K;21;CPToolbarItemLabelKeyD;K;6;CP$UIDd;3;109E;K;28;CPToolbarItemPaletteLabelKeyD;K;6;CP$UIDd;3;109E;K;23;CPToolbarItemToolTipKeyD;K;6;CP$UIDd;3;110E;K;19;CPToolbarItemTagKeyD;K;6;CP$UIDd;3;103E;K;22;CPToolbarItemTargetKeyD;K;6;CP$UIDd;1;0E;K;22;CPToolbarItemActionKeyD;K;6;CP$UIDd;3;111E;K;23;CPToolbarItemEnabledKeyD;K;6;CP$UIDd;2;67E;K;21;CPToolbarItemImageKeyD;K;6;CP$UIDd;3;113E;K;30;CPToolbarItemAlternateImageKeyD;K;6;CP$UIDd;1;0E;K;20;CPToolbarItemViewKeyD;K;6;CP$UIDd;1;0E;K;23;CPToolbarItemMinSizeKeyD;K;6;CP$UIDd;3;114E;K;23;CPToolbarItemMaxSizeKeyD;K;6;CP$UIDd;3;114E;K;34;CPToolbarItemVisibilityPriorityKeyD;K;6;CP$UIDd;2;52E;K;29;CPToolbarItemAutovalidatesKeyD;K;6;CP$UIDd;2;67E;E;D;K;6;$classD;K;6;CP$UIDd;2;33E;K;30;CPToolbarItemItemIdentifierKeyD;K;6;CP$UIDd;3;115E;K;21;CPToolbarItemLabelKeyD;K;6;CP$UIDd;3;116E;K;28;CPToolbarItemPaletteLabelKeyD;K;6;CP$UIDd;3;116E;K;23;CPToolbarItemToolTipKeyD;K;6;CP$UIDd;3;116E;K;19;CPToolbarItemTagKeyD;K;6;CP$UIDd;3;103E;K;22;CPToolbarItemTargetKeyD;K;6;CP$UIDd;1;0E;K;22;CPToolbarItemActionKeyD;K;6;CP$UIDd;3;117E;K;23;CPToolbarItemEnabledKeyD;K;6;CP$UIDd;2;67E;K;21;CPToolbarItemImageKeyD;K;6;CP$UIDd;3;118E;K;30;CPToolbarItemAlternateImageKeyD;K;6;CP$UIDd;1;0E;K;20;CPToolbarItemViewKeyD;K;6;CP$UIDd;1;0E;K;23;CPToolbarItemMinSizeKeyD;K;6;CP$UIDd;3;119E;K;23;CPToolbarItemMaxSizeKeyD;K;6;CP$UIDd;3;119E;K;34;CPToolbarItemVisibilityPriorityKeyD;K;6;CP$UIDd;2;52E;K;29;CPToolbarItemAutovalidatesKeyD;K;6;CP$UIDd;2;67E;E;D;K;10;$classnameS;19;_CPToolbarSpaceItemK;8;$classesA;S;19;_CPToolbarSpaceItemS;13;CPToolbarItemS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;37E;E;D;K;10;$classnameS;27;_CPToolbarFlexibleSpaceItemK;8;$classesA;S;27;_CPToolbarFlexibleSpaceItemS;13;CPToolbarItemS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;39E;E;S;24;DocumentWindowControllerD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;19E;E;E;S;8;textViewS;6;windowS;13;CPApplicationS;8;CPWindowd;10;1618477056S;24;{{196, 540}, {480, 270}}S;22;{{0, 0}, {1920, 1050}}d;2;15S;6;Windowd;1;0S;20;{{0, 0}, {480, 270}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;23E;E;E;d;2;18S;4;viewS;6;normalS;6;{1, 1}F;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;68E;E;E;S;10;scrollviewD;K;10;$classnameS;12;CPAppearanceK;8;$classesA;S;12;CPAppearanceS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;63E;K;5;_nameD;K;6;CP$UIDd;3;120E;K;15;_allowsVibrancyD;K;6;CP$UIDd;1;0E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;d;2;10T;D;K;6;$classD;K;6;CP$UIDd;2;20E;K;27;CPResponderNextResponderKeyD;K;6;CP$UIDd;2;23E;K;18;CPResponderMenuKeyD;K;6;CP$UIDd;1;0E;K;14;CPViewFrameKeyD;K;6;CP$UIDd;3;121E;K;15;CPViewBoundsKeyD;K;6;CP$UIDd;3;121E;K;18;CPViewSuperviewKeyD;K;6;CP$UIDd;2;23E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;2;56E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;K;14;CPViewScaleKeyD;K;6;CP$UIDd;2;58E;K;18;CPViewSizeScaleKeyD;K;6;CP$UIDd;2;58E;K;17;CPViewIsScaledKeyD;K;6;CP$UIDd;2;59E;K;19;CPViewAppearanceKeyD;K;6;CP$UIDd;1;0E;K;22;CPViewTrackingAreasKeyD;K;6;CP$UIDd;3;122E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;27E;E;E;d;1;8D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;7;CPColorK;8;$classesA;S;7;CPColorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;72E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;123E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;124E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;15;CPTextContainerK;8;$classesA;S;15;CPTextContainerS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;75E;K;22;CPTextContainerSizeKeyD;K;6;CP$UIDd;2;80E;K;31;CPTextContainerLayoutManagerKeyD;K;6;CP$UIDd;3;126E;K;37;CPTextContainerWidthTracksTextViewKeyD;K;6;CP$UIDd;2;67E;K;38;CPTextContainerHeightTracksTextViewKeyD;K;6;CP$UIDd;2;59E;K;37;CPTextContainerLineFragmentPaddingKeyD;K;6;CP$UIDd;1;0E;E;D;K;6;$classD;K;6;CP$UIDd;2;72E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;127E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;124E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;E;D;K;10;$classnameS;12;CPDictionaryK;8;$classesA;S;12;CPDictionaryS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;78E;K;10;CP.objectsD;K;7;NSColorD;K;6;CP$UIDd;3;128E;K;17;NSBackgroundColorD;K;6;CP$UIDd;3;129E;E;E;S;15;{480, 10000000}S;24;{{-100, 355}, {240, 15}}S;19;{{0, 0}, {240, 15}}d;11;-2147483648S;8;scrollerS;27;controlSizeRegular+disabledD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;29;_horizontalScrollerDidScroll:d;1;4d;1;1S;21;{{463, 0}, {15, 270}}S;19;{{0, 0}, {15, 270}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;27;_verticalScrollerDidScroll:S;36;CF0D66E0-8702-4344-A8C0-DD87E0AF7A51D;K;10;$classnameS;19;CPMutableDictionaryK;8;$classesA;S;19;CPMutableDictionaryS;12;CPDictionaryS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;95E;K;10;CP.objectsD;K;23;CPToolbarShowColorsItemD;K;6;CP$UIDd;2;34E;K;22;CPToolbarShowFontsItemD;K;6;CP$UIDd;2;35E;K;18;CPToolbarSpaceItemD;K;6;CP$UIDd;2;38E;K;18;CPToolbarPrintItemD;K;6;CP$UIDd;2;36E;K;26;CPToolbarFlexibleSpaceItemD;K;6;CP$UIDd;2;40E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;36E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;40E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;23;CPToolbarShowColorsItemS;6;ColorsS;16;Show Color Paneld;2;-1S;21;orderFrontColorPanel:D;K;10;$classnameS;20;_CPCibCustomResourceK;8;$classesA;S;20;_CPCibCustomResourceS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;105E;K;32;_CPCibCustomResourceClassNameKeyD;K;6;CP$UIDd;3;130E;K;35;_CPCibCustomResourceResourceNameKeyD;K;6;CP$UIDd;3;131E;K;33;_CPCibCustomResourcePropertiesKeyD;K;6;CP$UIDd;3;132E;E;S;8;{45, 32}S;22;CPToolbarShowFontsItemS;5;FontsS;15;Show Font PanelS;20;orderFrontFontPanel:D;K;10;$classnameS;7;CPImageK;8;$classesA;S;7;CPImageS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;112E;K;10;CPFilenameD;K;6;CP$UIDd;3;133E;K;6;CPSizeD;K;6;CP$UIDd;3;134E;E;S;8;{37, 24}S;18;CPToolbarPrintItemS;5;PrintS;14;printDocument:D;K;6;$classD;K;6;CP$UIDd;3;112E;K;10;CPFilenameD;K;6;CP$UIDd;3;133E;K;6;CPSizeD;K;6;CP$UIDd;3;135E;E;S;8;{36, 24}S;20;CPAppearanceNameAquaS;16;{{0, 0}, {0, 0}}D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;89E;E;E;S;5;colorD;K;10;$classnameS;15;CPLayoutManagerK;8;$classesA;S;15;CPLayoutManagerS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;125E;K;29;CPLayoutManagerTextStorageKeyD;K;6;CP$UIDd;3;137E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;3;138E;D;K;6;CP$UIDd;2;89E;E;E;D;K;6;$classD;K;6;CP$UIDd;2;72E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;139E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;124E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;E;D;K;6;$classD;K;6;CP$UIDd;2;72E;K;20;CPColorComponentsKeyD;K;6;CP$UIDd;3;140E;K;19;CPViewThemeClassKeyD;K;6;CP$UIDd;3;124E;K;19;CPViewThemeStateKeyD;K;6;CP$UIDd;2;57E;E;S;7;CPImageS;21;CPImageNameColorPanelD;K;6;$classD;K;6;CP$UIDd;2;78E;K;10;CP.objectsD;K;9;frameworkD;K;6;CP$UIDd;3;133E;K;16;bundleIdentifierD;K;6;CP$UIDd;3;133E;K;4;sizeD;K;6;CP$UIDd;3;142E;E;E;S;0;S;8;{19, 12}S;8;{19, 16}D;K;10;$classnameS;13;CPTextStorageK;8;$classesA;S;13;CPTextStorageS;25;CPMutableAttributedStringS;18;CPAttributedStringS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;136E;K;24;CPAttributedStringStringD;K;6;CP$UIDd;3;133E;K;24;CPAttributedStringRangesD;K;6;CP$UIDd;3;143E;K;28;CPAttributedStringAttributesD;K;6;CP$UIDd;3;144E;E;f;12;0.9981992245D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;89E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;2;89E;E;E;D;K;10;$classnameS;21;_CPKeyedArchiverValueK;8;$classesA;S;21;_CPKeyedArchiverValueS;7;CPValueS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;141E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;146E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;147E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;148E;E;E;f;11;0.602715373S;22;{"width":0,"height":0}D;K;6;$classD;K;6;CP$UIDd;3;141E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;149E;E;D;K;6;$classD;K;6;CP$UIDd;2;78E;K;10;CP.objectsD;E;E;S;25;{"location":0,"length":0}E;K;9;$archiverS;15;CPKeyedArchiverK;8;$versionS;6;100000E; \ No newline at end of file diff --git a/Tests/Manual/TextEditor/Resources/DocumentWindow.xib b/Tests/Manual/TextEditor/Resources/DocumentWindow.xib new file mode 100644 index 000000000..b4ec1e926 --- /dev/null +++ b/Tests/Manual/TextEditor/Resources/DocumentWindow.xib @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Manual/TextEditor/Resources/MainMenu.cib b/Tests/Manual/TextEditor/Resources/MainMenu.cib new file mode 100644 index 000000000..2e8a2e10d --- /dev/null +++ b/Tests/Manual/TextEditor/Resources/MainMenu.cib @@ -0,0 +1 @@ +280NPLIST;1.0;D;K;4;$topD;K;18;CPCibObjectDataKeyD;K;6;CP$UIDd;1;2E;E;K;8;$objectsA;S;5;$nullD;K;10;$classnameS;16;_CPCibObjectDataK;8;$classesA;S;16;_CPCibObjectDataS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;1E;K;28;_CPCibObjectDataNamesKeysKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataNamesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataClassesKeysKeyD;K;6;CP$UIDd;1;0E;K;32;_CPCibObjectDataClassesValuesKeyD;K;6;CP$UIDd;1;0E;K;30;_CPCibObjectDataConnectionsKeyD;K;6;CP$UIDd;1;4E;K;28;_CPCibObjectDataFrameworkKeyD;K;6;CP$UIDd;1;0E;K;26;_CPCibObjectDataNextOidKeyD;K;6;CP$UIDd;1;5E;K;30;_CPCibObjectDataObjectsKeysKeyD;K;6;CP$UIDd;1;6E;K;32;_CPCibObjectDataObjectsValuesKeyD;K;6;CP$UIDd;1;7E;K;26;_CPCibObjectDataOidKeysKeyD;K;6;CP$UIDd;1;8E;K;28;_CPCibObjectDataOidValuesKeyD;K;6;CP$UIDd;1;9E;K;28;_CPCibObjectDataFileOwnerKeyD;K;6;CP$UIDd;2;11E;K;33;_CPCibObjectDataVisibleWindowsKeyD;K;6;CP$UIDd;2;13E;E;D;K;10;$classnameS;7;CPArrayK;8;$classesA;S;7;CPArrayS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;15E;D;K;6;CP$UIDd;2;17E;D;K;6;CP$UIDd;2;18E;D;K;6;CP$UIDd;2;19E;D;K;6;CP$UIDd;2;20E;D;K;6;CP$UIDd;2;21E;D;K;6;CP$UIDd;2;22E;D;K;6;CP$UIDd;2;23E;D;K;6;CP$UIDd;2;24E;D;K;6;CP$UIDd;2;25E;D;K;6;CP$UIDd;2;26E;D;K;6;CP$UIDd;2;27E;D;K;6;CP$UIDd;2;28E;D;K;6;CP$UIDd;2;29E;D;K;6;CP$UIDd;2;30E;D;K;6;CP$UIDd;2;31E;D;K;6;CP$UIDd;2;32E;D;K;6;CP$UIDd;2;33E;D;K;6;CP$UIDd;2;34E;D;K;6;CP$UIDd;2;35E;D;K;6;CP$UIDd;2;36E;D;K;6;CP$UIDd;2;37E;D;K;6;CP$UIDd;2;38E;D;K;6;CP$UIDd;2;39E;D;K;6;CP$UIDd;2;40E;D;K;6;CP$UIDd;2;41E;D;K;6;CP$UIDd;2;42E;D;K;6;CP$UIDd;2;43E;D;K;6;CP$UIDd;2;44E;D;K;6;CP$UIDd;2;45E;D;K;6;CP$UIDd;2;46E;D;K;6;CP$UIDd;2;47E;D;K;6;CP$UIDd;2;48E;D;K;6;CP$UIDd;2;49E;D;K;6;CP$UIDd;2;50E;D;K;6;CP$UIDd;2;51E;D;K;6;CP$UIDd;2;52E;D;K;6;CP$UIDd;2;53E;D;K;6;CP$UIDd;2;54E;D;K;6;CP$UIDd;2;55E;D;K;6;CP$UIDd;2;56E;D;K;6;CP$UIDd;2;57E;D;K;6;CP$UIDd;2;58E;D;K;6;CP$UIDd;2;59E;D;K;6;CP$UIDd;2;60E;D;K;6;CP$UIDd;2;61E;D;K;6;CP$UIDd;2;62E;D;K;6;CP$UIDd;2;63E;D;K;6;CP$UIDd;2;64E;D;K;6;CP$UIDd;2;65E;D;K;6;CP$UIDd;2;66E;D;K;6;CP$UIDd;2;67E;D;K;6;CP$UIDd;2;68E;D;K;6;CP$UIDd;2;69E;D;K;6;CP$UIDd;2;70E;D;K;6;CP$UIDd;2;71E;D;K;6;CP$UIDd;2;72E;D;K;6;CP$UIDd;2;73E;D;K;6;CP$UIDd;2;74E;D;K;6;CP$UIDd;2;75E;D;K;6;CP$UIDd;2;76E;D;K;6;CP$UIDd;2;77E;D;K;6;CP$UIDd;2;78E;D;K;6;CP$UIDd;2;79E;D;K;6;CP$UIDd;2;80E;D;K;6;CP$UIDd;2;81E;D;K;6;CP$UIDd;2;82E;D;K;6;CP$UIDd;2;83E;D;K;6;CP$UIDd;2;84E;D;K;6;CP$UIDd;2;85E;D;K;6;CP$UIDd;2;86E;D;K;6;CP$UIDd;2;87E;D;K;6;CP$UIDd;2;88E;D;K;6;CP$UIDd;2;89E;D;K;6;CP$UIDd;2;90E;D;K;6;CP$UIDd;2;91E;D;K;6;CP$UIDd;2;92E;D;K;6;CP$UIDd;2;93E;D;K;6;CP$UIDd;2;94E;D;K;6;CP$UIDd;2;95E;D;K;6;CP$UIDd;2;96E;D;K;6;CP$UIDd;2;97E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;98E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;102E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;105E;D;K;6;CP$UIDd;3;106E;D;K;6;CP$UIDd;3;107E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;109E;D;K;6;CP$UIDd;3;110E;D;K;6;CP$UIDd;3;111E;D;K;6;CP$UIDd;3;112E;D;K;6;CP$UIDd;3;113E;D;K;6;CP$UIDd;3;114E;D;K;6;CP$UIDd;3;115E;D;K;6;CP$UIDd;3;116E;D;K;6;CP$UIDd;3;117E;D;K;6;CP$UIDd;3;118E;D;K;6;CP$UIDd;3;119E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;121E;D;K;6;CP$UIDd;3;122E;D;K;6;CP$UIDd;3;123E;D;K;6;CP$UIDd;3;124E;D;K;6;CP$UIDd;3;125E;D;K;6;CP$UIDd;3;126E;D;K;6;CP$UIDd;3;127E;D;K;6;CP$UIDd;3;128E;D;K;6;CP$UIDd;3;129E;D;K;6;CP$UIDd;3;130E;D;K;6;CP$UIDd;3;131E;D;K;6;CP$UIDd;3;132E;D;K;6;CP$UIDd;3;133E;D;K;6;CP$UIDd;3;134E;D;K;6;CP$UIDd;3;135E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;137E;D;K;6;CP$UIDd;3;138E;D;K;6;CP$UIDd;3;139E;D;K;6;CP$UIDd;3;140E;D;K;6;CP$UIDd;3;141E;D;K;6;CP$UIDd;3;142E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;144E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;146E;D;K;6;CP$UIDd;3;147E;D;K;6;CP$UIDd;3;148E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;150E;D;K;6;CP$UIDd;3;151E;D;K;6;CP$UIDd;3;152E;D;K;6;CP$UIDd;3;153E;D;K;6;CP$UIDd;3;154E;D;K;6;CP$UIDd;3;155E;D;K;6;CP$UIDd;3;156E;D;K;6;CP$UIDd;3;157E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;159E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;161E;D;K;6;CP$UIDd;3;162E;D;K;6;CP$UIDd;3;163E;D;K;6;CP$UIDd;3;164E;D;K;6;CP$UIDd;3;165E;D;K;6;CP$UIDd;3;166E;D;K;6;CP$UIDd;3;167E;D;K;6;CP$UIDd;3;168E;D;K;6;CP$UIDd;3;169E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;171E;D;K;6;CP$UIDd;3;172E;D;K;6;CP$UIDd;3;173E;D;K;6;CP$UIDd;3;174E;D;K;6;CP$UIDd;3;175E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;177E;D;K;6;CP$UIDd;3;178E;D;K;6;CP$UIDd;3;179E;D;K;6;CP$UIDd;3;180E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;182E;D;K;6;CP$UIDd;3;183E;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;D;K;6;CP$UIDd;3;186E;D;K;6;CP$UIDd;3;187E;D;K;6;CP$UIDd;3;188E;D;K;6;CP$UIDd;3;189E;D;K;6;CP$UIDd;3;190E;D;K;6;CP$UIDd;3;191E;D;K;6;CP$UIDd;3;192E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;194E;D;K;6;CP$UIDd;3;195E;D;K;6;CP$UIDd;3;196E;D;K;6;CP$UIDd;3;197E;D;K;6;CP$UIDd;3;198E;D;K;6;CP$UIDd;3;199E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;201E;D;K;6;CP$UIDd;3;202E;D;K;6;CP$UIDd;3;203E;D;K;6;CP$UIDd;3;204E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;206E;D;K;6;CP$UIDd;3;207E;D;K;6;CP$UIDd;3;208E;D;K;6;CP$UIDd;3;209E;D;K;6;CP$UIDd;3;210E;D;K;6;CP$UIDd;3;211E;D;K;6;CP$UIDd;3;212E;D;K;6;CP$UIDd;3;213E;D;K;6;CP$UIDd;3;214E;D;K;6;CP$UIDd;3;215E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;217E;D;K;6;CP$UIDd;3;218E;D;K;6;CP$UIDd;3;219E;D;K;6;CP$UIDd;3;220E;D;K;6;CP$UIDd;3;221E;D;K;6;CP$UIDd;3;222E;D;K;6;CP$UIDd;3;223E;D;K;6;CP$UIDd;3;224E;D;K;6;CP$UIDd;3;225E;D;K;6;CP$UIDd;3;226E;D;K;6;CP$UIDd;3;227E;D;K;6;CP$UIDd;3;228E;D;K;6;CP$UIDd;3;229E;D;K;6;CP$UIDd;3;230E;D;K;6;CP$UIDd;3;231E;D;K;6;CP$UIDd;3;232E;D;K;6;CP$UIDd;3;233E;D;K;6;CP$UIDd;3;234E;D;K;6;CP$UIDd;3;235E;D;K;6;CP$UIDd;3;236E;D;K;6;CP$UIDd;3;237E;D;K;6;CP$UIDd;3;238E;D;K;6;CP$UIDd;3;239E;D;K;6;CP$UIDd;3;240E;D;K;6;CP$UIDd;3;241E;D;K;6;CP$UIDd;3;242E;D;K;6;CP$UIDd;3;243E;D;K;6;CP$UIDd;3;244E;D;K;6;CP$UIDd;3;245E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;102E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;103E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;107E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;111E;D;K;6;CP$UIDd;3;112E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;108E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;119E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;135E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;136E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;142E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;143E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;148E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;149E;D;K;6;CP$UIDd;3;120E;D;K;6;CP$UIDd;3;153E;D;K;6;CP$UIDd;3;154E;D;K;6;CP$UIDd;3;154E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;157E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;159E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;169E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;170E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;175E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;176E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;180E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;181E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;160E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;192E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;199E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;200E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;193E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;158E;D;K;6;CP$UIDd;3;215E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;216E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;231E;D;K;6;CP$UIDd;3;232E;D;K;6;CP$UIDd;3;232E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;235E;D;K;6;CP$UIDd;3;236E;D;K;6;CP$UIDd;3;236E;D;K;6;CP$UIDd;3;236E;D;K;6;CP$UIDd;3;236E;D;K;6;CP$UIDd;3;100E;D;K;6;CP$UIDd;3;241E;D;K;6;CP$UIDd;3;242E;D;K;6;CP$UIDd;2;11E;D;K;6;CP$UIDd;2;11E;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;D;K;10;$classnameS;18;_CPCibCustomObjectK;8;$classesA;S;18;_CPCibCustomObjectS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;246E;E;D;K;10;$classnameS;5;CPSetK;8;$classesA;S;5;CPSetS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;12E;K;15;CPSetObjectsKeyD;K;6;CP$UIDd;3;247E;E;D;K;10;$classnameS;20;CPCibOutletConnectorK;8;$classesA;S;20;CPCibOutletConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;14E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;2;11E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;244E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;248E;E;D;K;10;$classnameS;21;CPCibControlConnectorK;8;$classesA;S;21;CPCibControlConnectorS;14;CPCibConnectorS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;240E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;249E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;237E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;250E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;104E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;2;11E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;251E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;110E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;252E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;115E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;253E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;116E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;254E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;117E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;255E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;109E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;256E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;243E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;257E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;118E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;258E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;113E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;259E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;156E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;260E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;155E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;261E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;127E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;262E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;130E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;263E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;126E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;264E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;145E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;265E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;129E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;266E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;128E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;267E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;144E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;268E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;121E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;269E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;137E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;270E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;141E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;271E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;122E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;272E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;146E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;273E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;238E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;274E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;233E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;275E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;234E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;276E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;147E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;277E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;150E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;278E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;151E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;279E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;152E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;280E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;162E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;245E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;281E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;195E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;282E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;171E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;283E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;191E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;284E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;204E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;285E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;196E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;286E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;190E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;287E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;182E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;288E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;227E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;211E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;290E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;228E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;222E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;166E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;245E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;291E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;178E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;292E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;172E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;293E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;133E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;294E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;208E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;295E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;207E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;296E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;131E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;297E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;209E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;298E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;186E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;299E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;124E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;300E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;212E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;301E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;230E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;225E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;194E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;302E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;219E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;226E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;179E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;303E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;183E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;304E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;184E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;305E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;224E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;197E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;306E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;164E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;307E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;202E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;308E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;221E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;173E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;309E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;220E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;167E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;245E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;291E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;163E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;245E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;281E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;203E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;310E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;217E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;289E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;185E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;311E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;213E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;312E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;188E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;313E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;174E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;314E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;161E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;3;245E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;315E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;177E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;316E;E;D;K;6;$classD;K;6;CP$UIDd;2;16E;K;24;_CPCibConnectorSourceKeyD;K;6;CP$UIDd;3;132E;K;29;_CPCibConnectorDestinationKeyD;K;6;CP$UIDd;1;0E;K;23;_CPCibConnectorLabelKeyD;K;6;CP$UIDd;3;317E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;246E;E;D;K;10;$classnameS;6;CPMenuK;8;$classesA;S;6;CPMenuS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;318E;K;13;CPMenuNameKeyD;K;6;CP$UIDd;3;319E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;320E;E;D;K;10;$classnameS;10;CPMenuItemK;8;$classesA;S;10;CPMenuItemS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;321E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;103E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;103E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;321E;K;13;CPMenuNameKeyD;K;6;CP$UIDd;3;327E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;328E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;329E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;103E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;103E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;333E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;103E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;334E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;335E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;108E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;108E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;335E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;336E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;337E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;338E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;339E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;340E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;341E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;112E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;112E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;341E;K;13;CPMenuNameKeyD;K;6;CP$UIDd;3;342E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;343E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;344E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;112E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;345E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;346E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;347E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;348E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;349E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;350E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;351E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;352E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;108E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;353E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;120E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;120E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;353E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;354E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;355E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;356E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;357E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;358E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;351E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;359E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;360E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;361E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;362E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;363E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;364E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;365E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;366E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;367E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;368E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;369E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;370E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;371E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;372E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;136E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;136E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;372E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;373E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;374E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;375E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;136E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;376E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;377E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;378E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;136E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;379E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;380E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;381E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;136E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;382E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;351E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;383E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;384E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;136E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;385E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;386E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;136E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;387E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;388E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;143E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;143E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;388E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;389E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;390E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;143E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;391E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;392E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;143E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;393E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;394E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;143E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;395E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;143E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;396E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;149E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;149E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;396E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;397E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;398E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;375E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;149E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;376E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;399E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;378E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;149E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;379E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;400E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;381E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;149E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;382E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;351E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;401E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;154E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;154E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;120E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;401E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;402E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;403E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;154E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;404E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;154E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;405E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;158E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;158E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;405E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;406E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;407E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;160E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;160E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;158E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;407E;K;13;CPMenuNameKeyD;K;6;CP$UIDd;3;408E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;409E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;410E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;411E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;412E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;378E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;413E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;414E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;375E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;415E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;416E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;417E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;418E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;381E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;419E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;420E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;421E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;422E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;423E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;170E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;170E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;423E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;424E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;425E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;170E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;426E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;170E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;427E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;170E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;428E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;170E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;429E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;176E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;176E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;429E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;430E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;425E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;176E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;426E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;176E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;431E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;176E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;432E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;181E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;181E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;432E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;433E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;425E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;181E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;434E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;181E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;435E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;181E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;436E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;181E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;437E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;181E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;438E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;439E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;440E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;363E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;441E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;442E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;160E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;365E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;441E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;443E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;193E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;193E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;158E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;443E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;444E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;445E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;446E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;447E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;448E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;449E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;450E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;451E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;452E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;200E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;200E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;452E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;453E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;454E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;455E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;456E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;457E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;458E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;455E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;456E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;457E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;200E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;459E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;460E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;363E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;461E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;462E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;193E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;365E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;461E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;158E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;463E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;216E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;216E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;158E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;463E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;464E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;465E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;466E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;375E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;467E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;378E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;468E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;381E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;469E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;421E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;470E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;471E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;472E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;473E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;474E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;475E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;476E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;477E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;478E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;479E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;480E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;16;CPMenuItemTagKeyD;K;6;CP$UIDd;3;481E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;216E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;482E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;232E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;232E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;482E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;483E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;484E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;232E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;411E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;441E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;485E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;232E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;486E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;236E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;236E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;486E;K;13;CPMenuNameKeyD;K;6;CP$UIDd;3;487E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;488E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;489E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;236E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;490E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;491E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;236E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;24;CPMenuItemIsSeparatorKeyD;K;6;CP$UIDd;3;330E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;331E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;22;CPMenuItemIsEnabledKeyD;K;6;CP$UIDd;3;332E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;236E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;492E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;236E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;493E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;3;242E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;3;322E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;20;CPMenuItemSubmenuKeyD;K;6;CP$UIDd;3;242E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;100E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;99E;K;14;CPMenuTitleKeyD;K;6;CP$UIDd;3;493E;K;14;CPMenuItemsKeyD;K;6;CP$UIDd;3;494E;E;D;K;6;$classD;K;6;CP$UIDd;3;101E;K;18;CPMenuItemTitleKeyD;K;6;CP$UIDd;3;495E;K;19;CPMenuItemTargetKeyD;K;6;CP$UIDd;1;0E;K;19;CPMenuItemActionKeyD;K;6;CP$UIDd;1;0E;K;25;CPMenuItemOnStateImageKeyD;K;6;CP$UIDd;3;324E;K;28;CPMenuItemMixedStateImageKeyD;K;6;CP$UIDd;3;325E;K;17;CPMenuItemMenuKeyD;K;6;CP$UIDd;3;242E;K;26;CPMenuItemKeyEquivalentKeyD;K;6;CP$UIDd;3;496E;K;38;CPMenuItemKeyEquivalentModifierMaskKeyD;K;6;CP$UIDd;3;326E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;497E;E;D;K;6;$classD;K;6;CP$UIDd;2;10E;K;27;_CPCibCustomObjectClassNameD;K;6;CP$UIDd;3;498E;E;S;13;CPApplicationD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;E;E;S;8;delegateS;15;arrangeInFront:S;19;performMiniaturize:S;29;orderFrontStandardAboutPanel:S;13;openDocument:S;13;performClose:S;13;saveDocument:S;15;saveDocumentAs:S;12;newDocument:S;9;showHelp:S;22;revertDocumentToSaved:S;21;clearRecentDocuments:S;13;stopSpeaking:S;14;startSpeaking:S;5;copy:S;10;selectAll:S;4;cut:S;14;checkSpelling:S;7;delete:S;6;paste:S;15;showGuessPanel:S;5;undo:S;23;performFindPanelAction:S;29;centerSelectionInVisibleArea:S;5;redo:S;30;toggleContinuousSpellChecking:S;12;performZoom:S;19;toggleToolbarShown:S;31;runToolbarCustomizationPalette:S;22;toggleGrammarChecking:S;24;toggleSmartInsertDelete:S;33;toggleAutomaticQuoteSubstitution:S;29;toggleAutomaticLinkDetection:S;13;addFontTrait:S;12;alignCenter:S;19;useStandardKerning:S;10;pasteFont:S;36;makeBaseWritingDirectionRightToLeft:S;15;alignJustified:S;9;copyFont:S;9;unscript:S;15;changeTextList:S;12;toggleRuler:S;11;modifyFont:S;17;turnOffLigatures:S;15;turnOffKerning:S;12;insertImage:S;36;makeTextWritingDirectionLeftToRight:S;32;makeTextWritingDirectionNatural:S;17;insertLoremIpsum:S;36;makeTextWritingDirectionRightToLeft:S;14;lowerBaseline:S;17;toggleAllowsUndo:S;10;copyRuler:S;10;alignLeft:S;16;useAllLigatures:S;12;superscript:S;10;subscript:S;11;alignRight:S;10;underline:S;32;makeBaseWritingDirectionNatural:S;15;tightenKerning:S;36;makeBaseWritingDirectionLeftToRight:S;14;raiseBaseline:S;11;pasteRuler:S;21;orderFrontColorPanel:S;14;loosenKerning:S;20;orderFrontFontPanel:S;21;useStandardLigatures:S;19;insertTableExample:S;9;AMainMenuS;11;_CPMainMenuD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;102E;D;K;6;CP$UIDd;3;107E;D;K;6;CP$UIDd;3;119E;D;K;6;CP$UIDd;3;157E;D;K;6;CP$UIDd;3;231E;D;K;6;CP$UIDd;3;235E;D;K;6;CP$UIDd;3;241E;E;E;S;22;TextEdit by CappuccinoS;14;submenuAction:D;K;10;$classnameS;20;_CPCibCustomResourceK;8;$classesA;S;20;_CPCibCustomResourceS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;323E;K;32;_CPCibCustomResourceClassNameKeyD;K;6;CP$UIDd;3;499E;K;35;_CPCibCustomResourceResourceNameKeyD;K;6;CP$UIDd;3;500E;K;33;_CPCibCustomResourcePropertiesKeyD;K;6;CP$UIDd;3;502E;E;D;K;6;$classD;K;6;CP$UIDd;3;323E;K;32;_CPCibCustomResourceClassNameKeyD;K;6;CP$UIDd;3;499E;K;35;_CPCibCustomResourceResourceNameKeyD;K;6;CP$UIDd;3;503E;K;33;_CPCibCustomResourcePropertiesKeyD;K;6;CP$UIDd;3;504E;E;d;7;1048576S;18;_CPApplicationMenuD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;104E;D;K;6;CP$UIDd;3;105E;D;K;6;CP$UIDd;3;106E;E;E;S;28;About TextEdit by CappuccinoT;S;0;F;S;9;Settings…S;1;,S;4;FileD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;109E;D;K;6;CP$UIDd;3;110E;D;K;6;CP$UIDd;3;111E;D;K;6;CP$UIDd;3;114E;D;K;6;CP$UIDd;3;115E;D;K;6;CP$UIDd;3;116E;D;K;6;CP$UIDd;3;117E;D;K;6;CP$UIDd;3;118E;E;E;S;3;NewS;1;nS;5;Open…S;1;oS;11;Open RecentS;22;_CPRecentDocumentsMenuD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;113E;E;E;S;10;Clear MenuS;5;CloseS;1;wS;4;SaveS;1;sS;8;Save As…S;1;Sd;7;1179648S;15;Revert to SavedS;4;EditD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;121E;D;K;6;CP$UIDd;3;122E;D;K;6;CP$UIDd;3;123E;D;K;6;CP$UIDd;3;124E;D;K;6;CP$UIDd;3;125E;D;K;6;CP$UIDd;3;126E;D;K;6;CP$UIDd;3;127E;D;K;6;CP$UIDd;3;128E;D;K;6;CP$UIDd;3;129E;D;K;6;CP$UIDd;3;130E;D;K;6;CP$UIDd;3;131E;D;K;6;CP$UIDd;3;132E;D;K;6;CP$UIDd;3;133E;D;K;6;CP$UIDd;3;134E;D;K;6;CP$UIDd;3;135E;D;K;6;CP$UIDd;3;142E;D;K;6;CP$UIDd;3;148E;D;K;6;CP$UIDd;3;153E;E;E;S;4;UndoS;1;zS;4;RedoS;1;ZS;11;Allows UndoS;3;CutS;1;xS;4;CopyS;1;cS;5;PasteS;1;vS;6;DeleteS;10;Select AllS;1;aS;18;Insert Lorem IpsumS;20;Insert Table ExampleS;13;Insert Image…S;4;FindD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;137E;D;K;6;CP$UIDd;3;138E;D;K;6;CP$UIDd;3;139E;D;K;6;CP$UIDd;3;140E;D;K;6;CP$UIDd;3;141E;E;E;S;5;Find…d;1;1S;1;fS;9;Find Nextd;1;2S;1;gS;13;Find Previousd;1;3S;1;GS;22;Use Selection for Findd;1;7S;1;eS;17;Jump to SelectionS;1;jS;20;Spelling and GrammarD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;144E;D;K;6;CP$UIDd;3;145E;D;K;6;CP$UIDd;3;146E;D;K;6;CP$UIDd;3;147E;E;E;S;14;Show Spelling…S;1;:S;14;Check SpellingS;1;;S;27;Check Spelling While TypingS;27;Check Grammar With SpellingS;13;SubstitutionsD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;150E;D;K;6;CP$UIDd;3;151E;D;K;6;CP$UIDd;3;152E;E;E;S;16;Smart Copy/PasteS;12;Smart QuotesS;11;Smart LinksS;6;SpeechD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;155E;D;K;6;CP$UIDd;3;156E;E;E;S;14;Start SpeakingS;13;Stop SpeakingS;6;FormatD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;159E;D;K;6;CP$UIDd;3;192E;D;K;6;CP$UIDd;3;214E;D;K;6;CP$UIDd;3;215E;E;E;S;4;FontS;11;_CPFontMenuD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;161E;D;K;6;CP$UIDd;3;162E;D;K;6;CP$UIDd;3;163E;D;K;6;CP$UIDd;3;164E;D;K;6;CP$UIDd;3;165E;D;K;6;CP$UIDd;3;166E;D;K;6;CP$UIDd;3;167E;D;K;6;CP$UIDd;3;168E;D;K;6;CP$UIDd;3;169E;D;K;6;CP$UIDd;3;175E;D;K;6;CP$UIDd;3;180E;D;K;6;CP$UIDd;3;187E;D;K;6;CP$UIDd;3;188E;D;K;6;CP$UIDd;3;189E;D;K;6;CP$UIDd;3;190E;D;K;6;CP$UIDd;3;191E;E;E;S;10;Show FontsS;1;tS;4;BoldS;1;bS;6;ItalicS;1;iS;9;UnderlineS;1;uS;6;BiggerS;1;+S;7;Smallerd;1;4S;1;-S;4;KernD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;171E;D;K;6;CP$UIDd;3;172E;D;K;6;CP$UIDd;3;173E;D;K;6;CP$UIDd;3;174E;E;E;S;11;Use DefaultS;8;Use NoneS;7;TightenS;6;LoosenS;9;LigaturesD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;177E;D;K;6;CP$UIDd;3;178E;D;K;6;CP$UIDd;3;179E;E;E;S;7;Use AllS;8;BaselineD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;182E;D;K;6;CP$UIDd;3;183E;D;K;6;CP$UIDd;3;184E;D;K;6;CP$UIDd;3;185E;D;K;6;CP$UIDd;3;186E;E;E;S;11;SuperscriptS;9;SubscriptS;5;RaiseS;5;LowerS;11;Show ColorsS;1;CS;10;Copy Styled;7;1572864S;11;Paste StyleS;4;TextD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;194E;D;K;6;CP$UIDd;3;195E;D;K;6;CP$UIDd;3;196E;D;K;6;CP$UIDd;3;197E;D;K;6;CP$UIDd;3;198E;D;K;6;CP$UIDd;3;199E;D;K;6;CP$UIDd;3;210E;D;K;6;CP$UIDd;3;211E;D;K;6;CP$UIDd;3;212E;D;K;6;CP$UIDd;3;213E;E;E;S;10;Align LeftS;1;{S;6;CenterS;1;|S;7;JustifyS;11;Align RightS;1;}S;17;Writing DirectionD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;201E;D;K;6;CP$UIDd;3;202E;D;K;6;CP$UIDd;3;203E;D;K;6;CP$UIDd;3;204E;D;K;6;CP$UIDd;3;205E;D;K;6;CP$UIDd;3;206E;D;K;6;CP$UIDd;3;207E;D;K;6;CP$UIDd;3;208E;D;K;6;CP$UIDd;3;209E;E;E;S;9;ParagraphS;8; DefaultS;14; Left to RightS;14; Right to LeftS;9;SelectionS;10;Show RulerS;10;Copy Rulerd;7;1310720S;11;Paste RulerS;4;ListD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;217E;D;K;6;CP$UIDd;3;218E;D;K;6;CP$UIDd;3;219E;D;K;6;CP$UIDd;3;220E;D;K;6;CP$UIDd;3;221E;D;K;6;CP$UIDd;3;222E;D;K;6;CP$UIDd;3;223E;D;K;6;CP$UIDd;3;224E;D;K;6;CP$UIDd;3;225E;D;K;6;CP$UIDd;3;226E;D;K;6;CP$UIDd;3;227E;D;K;6;CP$UIDd;3;228E;D;K;6;CP$UIDd;3;229E;D;K;6;CP$UIDd;3;230E;E;E;S;4;NoneS;8;• BulletS;8;◦ CircleS;8;▪ SquareS;8;⁃ HyphenS;8;1. 2. 3.d;2;10S;8;a. b. c.d;2;11S;8;A. B. C.d;2;12S;11;i. ii. iii.d;2;13S;11;I. II. III.d;2;14S;14;1. 1.1. 1.1.1.d;2;20S;4;ViewD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;233E;D;K;6;CP$UIDd;3;234E;E;E;S;12;Show ToolbarS;18;Customize Toolbar…S;6;WindowS;14;_CPWindowsMenuD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;237E;D;K;6;CP$UIDd;3;238E;D;K;6;CP$UIDd;3;239E;D;K;6;CP$UIDd;3;240E;E;E;S;8;MinimizeS;1;mS;4;ZoomS;18;Bring All to FrontS;4;HelpD;K;6;$classD;K;6;CP$UIDd;1;3E;K;10;CP.objectsA;D;K;6;CP$UIDd;3;243E;E;E;S;27;TextEdit by Cappuccino HelpS;1;?S;13;AppControllerS;13;CPFontManagerS;7;CPImageS;15;CPMenuCheckmarkD;K;10;$classnameS;12;CPDictionaryK;8;$classesA;S;12;CPDictionaryS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;501E;K;10;CP.objectsD;K;9;frameworkD;K;6;CP$UIDd;3;331E;K;16;bundleIdentifierD;K;6;CP$UIDd;3;331E;K;4;sizeD;K;6;CP$UIDd;3;506E;E;E;S;16;CPMenuMixedStateD;K;6;$classD;K;6;CP$UIDd;3;501E;K;10;CP.objectsD;K;9;frameworkD;K;6;CP$UIDd;3;331E;K;16;bundleIdentifierD;K;6;CP$UIDd;3;331E;K;4;sizeD;K;6;CP$UIDd;3;507E;E;E;D;K;10;$classnameS;21;_CPKeyedArchiverValueK;8;$classesA;S;21;_CPKeyedArchiverValueS;7;CPValueS;8;CPObjectE;E;D;K;6;$classD;K;6;CP$UIDd;3;505E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;508E;E;D;K;6;$classD;K;6;CP$UIDd;3;505E;K;15;CPValueValueKeyD;K;6;CP$UIDd;3;508E;E;S;22;{"width":0,"height":0}E;K;9;$archiverS;15;CPKeyedArchiverK;8;$versionS;6;100000E; \ No newline at end of file diff --git a/Tests/Manual/TextEditor/Resources/MainMenu.xib b/Tests/Manual/TextEditor/Resources/MainMenu.xib new file mode 100644 index 000000000..d8d88353c --- /dev/null +++ b/Tests/Manual/TextEditor/Resources/MainMenu.xib @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + Default + + + + + + + Left to Right + + + + + + + Right to Left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests/Manual/TextEditor/TableExample.j b/Tests/Manual/TextEditor/TableExample.j new file mode 100644 index 000000000..ddd916f47 --- /dev/null +++ b/Tests/Manual/TextEditor/TableExample.j @@ -0,0 +1,75 @@ +/* + * TableExample.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import +@import + + +/*! + Supplies a short sample document that contains a table. + + AppKit has no public table class for text yet. Tables in a text view come + from CPMarkdownParser, which turns Markdown into an attributed string. + When the parser finds a Markdown table, it builds a table view, holds it + in one attachment character, and lays it out in the text like a single + large character. Each cell of the table is a text view of its own. + + The sample is kept as Markdown in the application itself, so inserting it + needs no network access and no resource file. It also shows the Markdown + that the parser understands: headings, bold and italic text, and tables. +*/ +@implementation TableExample : CPObject + +/*! + Returns the sample document as Markdown: a heading, a sentence, a table + with a header row, and a closing sentence. + + The empty line after the table is needed. The parser does not end the + table's line, so without the empty line the closing sentence would follow + the table on the same line. +*/ ++ (CPString)markdown +{ + return @"## Table Example\n" + + @"The table below comes from Markdown, through CPMarkdownParser.\n" + + @"| Feature | Status | Notes |\n" + + @"| --- | --- | --- |\n" + + @"| **Lists** | Done | Bulleted, numbered and nested |\n" + + @"| **Images** | Done | Fit the width of the text |\n" + + @"| **Tables** | *Testing* | Each cell can be edited |\n" + + @"\n" + + @"Click in a cell to edit its text.\n"; +} + +/*! + Returns the sample document as an attributed string, ready to insert into + a rich text view. +*/ ++ (CPAttributedString)attributedString +{ + return [CPMarkdownParser attributedStringFromMarkdown:[self markdown]]; +} + +@end diff --git a/Tests/Manual/TextEditor/WORKLIST.md b/Tests/Manual/TextEditor/WORKLIST.md new file mode 100644 index 000000000..94dc631dd --- /dev/null +++ b/Tests/Manual/TextEditor/WORKLIST.md @@ -0,0 +1,91 @@ +# TextEditor Work List + +TextEditor is a document-based test application and a tutorial. When the app uses AppKit, it shows gaps in the framework. This list records those gaps and the planned additions to the app. + +## AppKit gaps + +### Windows and documents + +- **The "save changes" alert is modal for the whole app.** `CPDocument` shows it with `runModal` (`CPDocument.j:903`). Cocoa attaches it as a sheet to the document window, so that only that document is blocked. +- **Save cannot work without a server.** With no file URL, Save uses `CPSavePanel`, which falls back to `window.prompt` (`CPSavePanel.j:79`). `CPDocument` then sends an HTTP POST. A storage facade is needed, with local (IndexedDB) and remote (HTTP) implementations. + +### Toolbar + +- **The Fonts and Print standard items are not implemented.** `_standardItemWithItemIdentifier:` returns `nil` for both (`_CPToolbarItem.j:641`). AppKit needs `_CPToolbarShowFontsItem` (sends `orderFrontFontPanel:`) and `_CPToolbarPrintItem` (sends `printDocument:`). nib2cib needs matching classes. + +### Text features not yet supported + +- **Images in text are not fully tested or complete.** `CPTextAttachment` holds an image as one attachment character, and Edit > Insert Image… and Command-V with an image on the clipboard insert images. Not yet tested: several images inserted at once, copying, pasting and undoing an image within the document, and pasting a screenshot. Not yet implemented: dropping image files on the text view, which needs file access in the drag pasteboard (`_CPDOMDataTransferPasteboard`); pasting images with Edit > Paste from the menu, which reads plain text only; and RTF `\pict` and Markdown `![]()`, so an image is lost when text is copied to another application as RTF. +- **Tables can only be inserted from Markdown, and are not yet tested interactively.** The only source of tables is CPMarkdownParser, which builds the private `_CPTableTextAttachment`, a view in the text whose cells are text views of their own. Edit > Insert Table Example inserts a sample through it. Not yet tested: editing in a cell and what Tab does there, deleting the table and undoing, copying and pasting it within the document, narrowing the window, and Format menu commands while a cell is being edited. Known from the code: the table has no archiving, so copying it within the application may fail; nothing calls its `resizeToWidth:`, so it keeps its width of 500 points; and in the sample its right edge and the last row's bottom border are not drawn. A command to insert a table, with a fixed default size and no options, waits on these results. +- **Lists are not read from or written to RTF.** `CPTextList` and `textLists` in `CPParagraphStyle` now exist, but the RTF parser and producer do not handle `\listtable`, `\listoverridetable`, `\ls` and `\ilvl`. A list copied as RTF keeps its marker text and indents, but loses its list structure. CPMarkdownParser also writes "- item" as the literal text " • item", with no list. + +### Text view + +How the text system works, for the entries below: `CPTextView` does not use CSS text layout. `CPSimpleTypesetter` measures text with the canvas `measureText` (`sizeWithFont:inWidth:` in `CPStringDrawing.j`), measuring growing substrings so that each character's advancement includes the browser's kerning. It then breaks lines, places tabs, and applies indents and alignment itself (`CPTypesetter.j`). `_CPLineFragment` (`CPLayoutManager.j`) draws each run of identical attributes as one absolutely positioned ``, at the computed position, with `white-space: pre` and the run's font, colour and underline in CSS. Inside a span the browser shapes the glyphs, including its own kerning and ligatures. + +- **Kerning and ligature settings are not implemented.** Format > Font > Kern sends `useStandardKerning:`, `turnOffKerning:`, `tightenKerning:` and `loosenKerning:`, and Format > Font > Ligatures sends `useStandardLigatures:`, `turnOffLigatures:` and `useAllLigatures:`. `CPTextView` implements none of them, so the items are disabled. `CPText.j` defines `CPKernAttributeName` and `CPLigatureAttributeName`, but only the RTF producer reads them. A complete implementation needs: the seven actions, changing the attributes over the selection or the typing attributes with undo, as the baseline actions do; the typesetter adding a run's `CPKernAttributeName` value to each character's advancement, and measuring with the same ligature setting it draws with; and `_CPLineFragment` setting CSS `letter-spacing` and `font-variant-ligatures` on each run's span to match. +- **The font panel needs polish.** Found during the Format menu review on 24 October: the panel works with the text view, but its appearance and behaviour need refinement. The specific problems are not yet recorded; list them here, compared with the native font panel, when the panel is reviewed. `CPFontPanel` is in `AppKit/CPTextView/CPFontPanel.j`. + +### Ruler + +- **Marker appearance and vertical alignment differ from TextEdit.** The marker images, and where markers sit across the ruler, are interim until Aristo3 supplies its own; adjust them to match TextEdit then. A tab stop dragged up off the ruler follows the pointer and is clipped by the ruler's edge, as in TextEdit. Dragged down off the ruler, it stays on its line and is drawn faded until release; TextEdit's behaviour in that direction has not been checked. + +### nib2cib + +- **nib2cib ignores the ruler settings.** The "Uses Ruler" setting in Interface Builder does not reach the cib. `CPTextView` and `CPRulerView` also need to archive their ruler settings. +- **The content view's resizing setting does not reach the cib.** Making the document window's content view resize with the window in `DocumentWindow.xib` produces a byte-identical `DocumentWindow.cib`. CPView omits the autoresizing mask from the archive only when it is `CPViewNotSizable` (`CPView.j:4016`), so the content view is archived as not sizable. Not yet determined whether `ibtool` or nib2cib drops the setting. The app still behaves correctly, because the window resizes its content view itself. + +### Menus and key equivalents (needs discussion) + +- **The key equivalent of a disabled menu item reaches the browser.** `-[CPMenu performKeyEquivalent:]` (`CPMenu.j:1059`) stops the browser's default action only when the matching item is enabled. For a disabled item it reports the key as handled, but the browser still acts on it: with File > Close disabled, Cmd-W closes the browser tab. Cocoa consumes the key equivalent of a disabled item with a beep. Stopping the browser's default for disabled items as well would match Cocoa for most keys, but not all: some browsers do not let a page block certain shortcuts, such as Cmd-W, Cmd-N and Cmd-T in Chrome, and Copy, Cut and Paste must keep reaching the browser for its clipboard events. This needs careful thought, and perhaps discussion, before any change. + +### Platform + +- **Key equivalents on platforms other than macOS.** Filed as an issue. + +## Opportunities for improvement + +- **Rich text through the asynchronous Clipboard API.** Edit > Copy, Cut and Paste chosen from the menu reach the system clipboard through `navigator.clipboard`, which carries plain text only. Rich text still moves between applications through the keyboard path, in the browser's clipboard events, and within the application through the internal pasteboard. Handling attributed text in general is an unbounded task, but a limited subset could be supported, for example through `text/html` in a `ClipboardItem`. + +- **Justification is nominally functional, but its quality is limited.** `CPSimpleTypesetter` breaks lines greedily: it fills each line as far as it fits, as Cocoa does, and then spreads the spare width of each wrapped line evenly over that line's spaces. This naive algorithm does not and cannot produce the quality that modern typesetting expects. It decides each line alone, so word spacing varies widely from line to line, and loose lines are common. Quality needs paragraph-wide line breaking in the manner of Knuth-Plass, as Typst also uses, with language-aware hyphenation. Cappuccino measures and lays out text itself, so this belongs in the typesetter, as its only behaviour, with no setting for users or developers. + +## Planned additions to the app + +- Fonts through `CPFontManager` and `CPFontPanel`. +- RTF reading and writing. +- Images in text: dropping image files, and the image formats in RTF and Markdown. +- A storage facade, with saving and opening in IndexedDB and through HTTP. +- An asynchronous fetch test: fetch the text of a page and show it for editing. The page must allow cross-origin requests. +- An inspector pane with `CPSplitViewController` and `CPViewController`, with a View menu item that sends `toggleInspector:`. +- Documents in separate browser windows with `CPPlatformWindow`. +- A Settings window. The first setting is the minimum size of document windows, now the constants `DocumentWindowMinimumWidth` and `DocumentWindowMinimumHeight` in `DocumentWindowController.j`. + +## Tests done + +- One window for each document, each loaded from its own cib. +- Insert Lorem Ipsum through the responder chain. +- Undo and redo over several levels, separate for each document. +- Text colour through `CPColorPanel`. +- Paragraph indents through the ruler markers. +- The "save changes" alert when a document closes. +- A minimum size for document windows, through the window delegate. The window stops at the minimum while the pointer keeps moving, as in macOS, and it can still be moved and resized afterwards. +- View > Show Toolbar and Hide Toolbar: the item's title follows the toolbar's visibility. +- The Window menu lists the open document windows, marks the main window with a check mark and brings a window to the front when chosen. Bring All to Front can be tested once documents open in separate browser windows. +- The About panel shows the display name from `CPBundleDisplayName`. +- Edit > Allow Undo turns undo on and off for the active document. With undo off, Undo and Redo stay disabled, and the check mark follows the document in front. +- The ruler: markers follow the paragraph at the selection; first line, head and right indents change the layout as shown; clicking adds a tab stop, and dragging moves or removes one; the Control-click menu changes a tab stop's type; the ruler's zero lines up with the start of the text, and markers at 0 are fully visible. +- Text stays inside the visible width, with Cocoa's 5-point line fragment padding. +- Tab characters: the Tab key inserts a tab in the text view, and left, centre, right and decimal tab stops align their columns. +- Format > Text > Copy Ruler and Paste Ruler apply one paragraph's indents and tab stops to others. +- Format > Font > Baseline: superscript, subscript, raised and lowered baselines lay out correctly, and Use Default restores the text, for an exact or a wider selection. +- Format > Text > Justify: wrapped lines fill the width between the indents, with the spare width spread over the spaces; the last line of a paragraph stays left aligned. Centred text is centred between the indents, and spaces at the end of a wrapped line hang outside the alignment. +- Format > Font > Copy Style and Paste Style: the style of the first character of the selection is applied to the whole target selection, which keeps its paragraph style. +- Format > List: bulleted and numbered lists, applied to one or several paragraphs and removed with None. Return starts the next item and keeps the item's text style; Return in an empty item ends the list. Tab at the start of an item nests it, and Shift-Tab returns it. Delete at the start of an item's text removes it from the list. The items are numbered again after each change. +- Format > Text > Copy Ruler and Paste Ruler with lists: pasting adds or removes list markers, on one or several paragraphs, and a pasted item joins the neighbouring list. +- Typing at the end of styled text, and the font panel, use the style of the character before the insertion point. +- Edit > Copy, Cut and Paste from the menu: plain text moves to and from other applications through the system clipboard; text copied and pasted within the application keeps its style. Command-V pastes text copied in another application, not an earlier copy within the application. +- Edit > Insert Image…: a JPEG or PNG image is inserted inside the text and at the end of the text. +- A wide image fits the text width, and scales with the window without growing beyond its natural size. At the end of the text it is not clipped, and the scroll bars follow it. +- Format > Font > Bigger and Smaller (Command-+ and Command--) without a selection change the size of the text typed next, step by step, also in a blank document. +- Edit > Insert Table Example: the sample table appears between the surrounding sentences, with its header row and its bold and italic cells. +- Builds with both the legacy toolchain and capp-build. diff --git a/Tests/Manual/TextEditor/index-debug.html b/Tests/Manual/TextEditor/index-debug.html new file mode 100644 index 000000000..a31c0f6cc --- /dev/null +++ b/Tests/Manual/TextEditor/index-debug.html @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + TextEditor + + + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/TextEditor/index.html b/Tests/Manual/TextEditor/index.html new file mode 100644 index 000000000..ff17b9ebc --- /dev/null +++ b/Tests/Manual/TextEditor/index.html @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + TextEditor + + + + + + + + + + + + +
+
+
+ +
+
+ +
+ + diff --git a/Tests/Manual/TextEditor/main.j b/Tests/Manual/TextEditor/main.j new file mode 100755 index 000000000..f68192139 --- /dev/null +++ b/Tests/Manual/TextEditor/main.j @@ -0,0 +1,36 @@ +/* + * main.j + * TextEditor + * + * Copyright (c) 2026 David Richardson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +@import +@import + +@import "AppController.j" +@import "Document.j" + + +function main(args, namedArgs) +{ + CPApplicationMain(args, namedArgs); +} diff --git a/Tools/nib2cib/Converter.j b/Tools/nib2cib/Converter.j index e1dc57897..e6514c42d 100644 --- a/Tools/nib2cib/Converter.j +++ b/Tools/nib2cib/Converter.j @@ -115,6 +115,7 @@ ConverterConversionException = @"ConverterConversionException"; - (CPData)CPCompliantNibDataAtFilePath:(CPString)aFilePath { var temporaryNibFilePath = "", + temporaryXibFilePath = "", temporaryPlistFilePath = ""; diff --git a/Tools/nib2cib/NSTextView.j b/Tools/nib2cib/NSTextView.j index 731fc0008..4d730bf84 100644 --- a/Tools/nib2cib/NSTextView.j +++ b/Tools/nib2cib/NSTextView.j @@ -42,6 +42,9 @@ [self setAllowsUndo:[aTextViewSharedData allowsUndo]]; [self setUsesFontPanel:[aTextViewSharedData usesFontPanel]]; + // Interface Builder's "Uses Ruler". Whether the ruler is shown is decided at run time. + _usesRuler = [aTextViewSharedData usesRuler]; + [self setBackgroundColor:[aTextViewSharedData backgroundColor]]; [self setInsertionPointColor:[aTextViewSharedData insertionColor]]; [self setSelectedTextAttributes:[aTextViewSharedData selectedTextAttributes]]; diff --git a/Tools/nib2cib/NSTextViewSharedData.j b/Tools/nib2cib/NSTextViewSharedData.j index 8c4ad513f..45f16b149 100644 --- a/Tools/nib2cib/NSTextViewSharedData.j +++ b/Tools/nib2cib/NSTextViewSharedData.j @@ -33,6 +33,7 @@ BOOL _richText @accessors(getter=isRichText); BOOL _selectable @accessors(getter=isSelectable); BOOL _usesFontPanel @accessors(getter=usesFontPanel); + BOOL _usesRuler @accessors(getter=usesRuler); CPColor _backgroundColor @accessors(getter=backgroundColor); CPColor _insertionColor @accessors(getter=insertionColor); CPDictionary _selectedTextAttributes @accessors(getter=selectedTextAttributes); @@ -64,6 +65,7 @@ _richText = (flags & 0x00000004) ? YES : NO; _selectable = (flags & 0x00000001) ? YES : NO; _usesFontPanel = (flags & 0x00000020) ? YES : NO; + _usesRuler = (flags & 0x00000040) ? YES : NO; _backgroundColor = [aCoder decodeObjectForKey:@"NSBackgroundColor"]; _insertionColor = [aCoder decodeObjectForKey:@"NSInsertionColor"];