author | Gregory Szorc <gps@mozilla.com> |
Sun, 08 Jul 2012 20:48:04 -0700 | |
changeset 98770 | 24312b9671c64fc218772bf1320b33690c54d0fd |
parent 98769 | 5099eb0d0287ed8214ce5c650e43ec2eb8322bcc (current diff) |
parent 98663 | 1ee6c61e0a0bcfe916f577d60439aadc7a181e97 (diff) |
child 98771 | 37fc31499d8a9013a26549f432be1faf0e1d78e7 |
push id | 11640 |
push user | ryanvm@gmail.com |
push date | Tue, 10 Jul 2012 00:53:29 +0000 |
treeherder | mozilla-inbound@6b0d194eabed [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 16.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/accessible/build/Makefile.in +++ b/accessible/build/Makefile.in @@ -27,14 +27,18 @@ SHARED_LIBRARY_LIBS = \ ../src/base/$(LIB_PREFIX)accessibility_base_s.$(LIB_SUFFIX) \ ../src/generic/$(LIB_PREFIX)accessibility_generic_s.$(LIB_SUFFIX) \ ../src/html/$(LIB_PREFIX)accessibility_html_s.$(LIB_SUFFIX) \ ../src/xpcom/$(LIB_PREFIX)accessibility_xpcom_s.$(LIB_SUFFIX) \ ../src/$(LIB_PREFIX)accessibility_toolkit_s.$(LIB_SUFFIX) \ ../src/xforms/$(LIB_PREFIX)accessibility_xforms_s.$(LIB_SUFFIX) \ $(NULL) +ifeq ($(MOZ_WIDGET_TOOLKIT),windows) +SHARED_LIBRARY_LIBS += ../src/windows/uia/$(LIB_PREFIX)accessibility_toolkit_uia_s.$(LIB_SUFFIX) +endif + ifdef MOZ_XUL SHARED_LIBRARY_LIBS += ../src/xul/$(LIB_PREFIX)accessibility_xul_s.$(LIB_SUFFIX) endif include $(topsrcdir)/config/rules.mk
--- a/accessible/public/nsIAccessibleProvider.idl +++ b/accessible/public/nsIAccessibleProvider.idl @@ -57,18 +57,18 @@ interface nsIAccessibleProvider : nsISup const long XULStatusBar = 0x00001014; const long XULRadioButton = 0x00001015; const long XULRadioGroup = 0x00001016; /** Used for XUL tab element */ const long XULTab = 0x00001017; /** Used for XUL tabs element, a container for tab elements */ const long XULTabs = 0x00001018; - /** Used for XUL tabpanels container element */ - const long XULTabpanels = 0x00001019; + /** Used for XUL deck frame */ + const long XULDeck = 0x00001019; const long XULText = 0x0000101A; const long XULTextBox = 0x0000101B; const long XULThumb = 0x0000101C; const long XULTree = 0x0000101D; const long XULTreeColumns = 0x0000101E; const long XULTreeColumnItem = 0x0000101F; const long XULToolbar = 0x00001020;
--- a/accessible/src/Makefile.in +++ b/accessible/src/Makefile.in @@ -9,17 +9,20 @@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk ifeq ($(MOZ_WIDGET_TOOLKIT),gtk2) PLATFORM_DIR = atk else ifeq ($(MOZ_WIDGET_TOOLKIT),windows) -PLATFORM_DIR = msaa +PLATFORM_DIR = \ + msaa \ + windows \ + $(null) else ifeq ($(MOZ_WIDGET_TOOLKIT),cocoa) PLATFORM_DIR += mac else PLATFORM_DIR += other endif endif endif
--- a/accessible/src/base/nsAccessibilityService.cpp +++ b/accessible/src/base/nsAccessibilityService.cpp @@ -1148,21 +1148,24 @@ nsAccessibilityService::GetOrCreateAcces if (!newAcc) { // Elements may implement nsIAccessibleProvider via XBL. This allows them to // say what kind of accessible to create. newAcc = CreateAccessibleByType(content, docAcc); } if (!newAcc) { - // Create generic accessibles for SVG and MathML nodes. - if (content->IsSVG(nsGkAtoms::svg)) { + // xul:deck does not have XBL and nsIFrame::CreateAccessible() is only called + // on HTML elements + nsIAtom* tag = content->Tag(); + if ((tag == nsGkAtoms::deck) || (tag == nsGkAtoms::tabpanels)) { + newAcc = new XULDeckAccessible(content, docAcc); + } else if (content->IsSVG(nsGkAtoms::svg)) { newAcc = new EnumRoleAccessible(content, docAcc, roles::DIAGRAM); - } - else if (content->IsMathML(nsGkAtoms::math)) { + } else if (content->IsMathML(nsGkAtoms::math)) { newAcc = new EnumRoleAccessible(content, docAcc, roles::EQUATION); } } if (!newAcc) { newAcc = CreateAccessibleForDeckChild(weakFrame.GetFrame(), content, docAcc); } @@ -1321,16 +1324,20 @@ nsAccessibilityService::CreateAccessible case nsIAccessibleProvider::XULColorPickerTile: accessible = new XULColorPickerTileAccessible(aContent, aDoc); break; case nsIAccessibleProvider::XULCombobox: accessible = new XULComboboxAccessible(aContent, aDoc); break; + case nsIAccessibleProvider::XULDeck: + accessible = new XULDeckAccessible(aContent, aDoc); + break; + case nsIAccessibleProvider::XULDropmarker: accessible = new XULDropmarkerAccessible(aContent, aDoc); break; case nsIAccessibleProvider::XULGroupbox: accessible = new XULGroupboxAccessible(aContent, aDoc); break; @@ -1425,20 +1432,16 @@ nsAccessibilityService::CreateAccessible case nsIAccessibleProvider::XULTab: accessible = new XULTabAccessible(aContent, aDoc); break; case nsIAccessibleProvider::XULTabs: accessible = new XULTabsAccessible(aContent, aDoc); break; - case nsIAccessibleProvider::XULTabpanels: - accessible = new XULTabpanelsAccessible(aContent, aDoc); - break; - case nsIAccessibleProvider::XULText: accessible = new XULLabelAccessible(aContent, aDoc); break; case nsIAccessibleProvider::XULTextBox: accessible = new XULTextFieldAccessible(aContent, aDoc); break;
--- a/accessible/src/generic/Accessible.h +++ b/accessible/src/generic/Accessible.h @@ -503,16 +503,18 @@ public: mozilla::a11y::ImageAccessible* AsImage(); bool IsImageMapAccessible() const { return mFlags & eImageMapAccessible; } mozilla::a11y::HTMLImageMapAccessible* AsImageMap(); inline bool IsXULTree() const { return mFlags & eXULTreeAccessible; } mozilla::a11y::XULTreeAccessible* AsXULTree(); + inline bool IsXULDeck() const { return mFlags & eXULDeckAccessible; } + inline bool IsListControl() const { return mFlags & eListControlAccessible; } inline bool IsMenuButton() const { return mFlags & eMenuButtonAccessible; } inline bool IsMenuPopup() const { return mFlags & eMenuPopupAccessible; } inline bool IsRoot() const { return mFlags & eRootAccessible; } mozilla::a11y::RootAccessible* AsRoot(); @@ -761,17 +763,18 @@ protected: eHTMLListItemAccessible = 1 << 11, eImageAccessible = 1 << 12, eImageMapAccessible = 1 << 13, eListControlAccessible = 1 << 14, eMenuButtonAccessible = 1 << 15, eMenuPopupAccessible = 1 << 16, eRootAccessible = 1 << 17, eTextLeafAccessible = 1 << 18, - eXULTreeAccessible = 1 << 19 + eXULDeckAccessible = 1 << 19, + eXULTreeAccessible = 1 << 20 }; ////////////////////////////////////////////////////////////////////////////// // Miscellaneous helpers /** * Return ARIA role (helper method). */
--- a/accessible/src/mac/AccessibleWrap.mm +++ b/accessible/src/mac/AccessibleWrap.mm @@ -54,16 +54,19 @@ AccessibleWrap::GetNativeInterface (void // overridden in subclasses to create the right kind of object. by default we create a generic // 'mozAccessible' node. Class AccessibleWrap::GetNativeType () { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; + if (IsXULDeck()) + return [mozPaneAccessible class]; + roles::Role role = Role(); switch (role) { case roles::PUSHBUTTON: case roles::SPLITBUTTON: case roles::TOGGLE_BUTTON: { // if this button may show a popup, let's make it of the popupbutton type. return HasPopup() ? [mozPopupButtonAccessible class] : @@ -76,25 +79,27 @@ AccessibleWrap::GetNativeType () case roles::CHECKBUTTON: return [mozCheckboxAccessible class]; case roles::HEADING: return [mozHeadingAccessible class]; case roles::PAGETABLIST: return [mozTabsAccessible class]; - + case roles::ENTRY: case roles::STATICTEXT: case roles::CAPTION: case roles::ACCEL_LABEL: - case roles::TEXT_LEAF: case roles::PASSWORD_TEXT: // normal textfield (static or editable) - return [mozTextAccessible class]; + return [mozTextAccessible class]; + + case roles::TEXT_LEAF: + return [mozTextLeafAccessible class]; case roles::LINK: return [mozLinkAccessible class]; case roles::COMBOBOX: return [mozPopupButtonAccessible class]; default:
--- a/accessible/src/mac/Makefile.in +++ b/accessible/src/mac/Makefile.in @@ -9,18 +9,16 @@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk MODULE = accessibility LIBRARY_NAME = accessibility_toolkit_s EXPORT_LIBRARY = .. LIBXUL_LIBRARY = 1 - - CMMSRCS = \ AccessibleWrap.mm \ ApplicationAccessibleWrap.mm \ DocAccessibleWrap.mm \ nsAccessNodeWrap.mm \ mozAccessible.mm \ mozDocAccessible.mm \ mozActionElements.mm \ @@ -43,10 +41,12 @@ include $(topsrcdir)/config/rules.mk LOCAL_INCLUDES += \ -I$(srcdir) \ -I$(srcdir)/../base \ -I$(srcdir)/../generic \ -I$(srcdir)/../html \ -I$(srcdir)/../xul \ -I$(topsrcdir)/widget/cocoa \ -I$(topsrcdir)/widget/xpwidgets \ + -I$(topsrcdir)/layout/xul/base/src \ + -I$(topsrcdir)/layout/generic \ $(NULL)
--- a/accessible/src/mac/mozAccessible.h +++ b/accessible/src/mac/mozAccessible.h @@ -18,41 +18,41 @@ * to give it the represented view, in the latter case. */ inline id <mozAccessible> GetObjectOrRepresentedView(id <mozAccessible> aObject) { return [aObject hasRepresentedView] ? [aObject representedView] : aObject; } +inline mozAccessible* +GetNativeFromGeckoAccessible(nsIAccessible* aAccessible) +{ + mozAccessible* native = nil; + aAccessible->GetNativeInterface((void**)&native); + return native; +} + @interface mozAccessible : NSObject <mozAccessible> { /** * Weak reference; it owns us. */ AccessibleWrap* mGeckoAccessible; /** * Strong ref to array of children */ NSMutableArray* mChildren; /** * Weak reference to the parent */ mozAccessible* mParent; - - /** - * We can be marked as 'expired' if Shutdown() is called on our geckoAccessible. - * since we might still be retained by some third-party, we need to do cleanup - * in |expire|, and prevent any potential harm that could come from someone using us - * after this point. - */ - BOOL mIsExpired; - + /** * The nsIAccessible role of our gecko accessible. */ mozilla::a11y::role mRole; } // inits with the gecko owner. - (id)initWithAccessible:(AccessibleWrap*)geckoParent;
--- a/accessible/src/mac/mozAccessible.mm +++ b/accessible/src/mac/mozAccessible.mm @@ -4,28 +4,30 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #import "mozAccessible.h" #import "MacUtils.h" #import "mozView.h" #include "Accessible-inl.h" +#include "nsAccUtils.h" #include "nsIAccessibleRelation.h" #include "nsIAccessibleText.h" #include "nsIAccessibleEditableText.h" #include "Relation.h" #include "Role.h" #include "RootAccessible.h" #include "mozilla/Services.h" #include "nsRect.h" #include "nsCocoaUtils.h" #include "nsCoord.h" #include "nsObjCExceptions.h" +#include "nsWhitespaceTokenizer.h" using namespace mozilla; using namespace mozilla::a11y; // converts a screen-global point in the cocoa coordinate system (with origo in the bottom-left corner // of the screen), into a top-left screen point, that gecko can use. static inline void ConvertCocoaToGeckoPoint(NSPoint &aInPoint, nsPoint &aOutPoint) @@ -58,39 +60,26 @@ GetClosestInterestingAccessible(id anObj if ([unignoredObject respondsToSelector:@selector(hasRepresentedView)]) return GetObjectOrRepresentedView(unignoredObject); return unignoredObject; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } -static inline mozAccessible* -GetNativeFromGeckoAccessible(nsIAccessible *anAccessible) -{ - NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NSNULL; - - mozAccessible *native = nil; - anAccessible->GetNativeInterface ((void**)&native); - return native; - - NS_OBJC_END_TRY_ABORT_BLOCK_NSNULL; -} - #pragma mark - @implementation mozAccessible - (id)initWithAccessible:(AccessibleWrap*)geckoAccessible { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if ((self = [super init])) { mGeckoAccessible = geckoAccessible; - mIsExpired = NO; mRole = geckoAccessible->Role(); } return self; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } @@ -107,27 +96,27 @@ GetNativeFromGeckoAccessible(nsIAccessib #pragma mark - - (BOOL)accessibilityIsIgnored { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; // unknown (either unimplemented, or irrelevant) elements are marked as ignored // as well as expired elements. - return mIsExpired || [[self role] isEqualToString:NSAccessibilityUnknownRole]; + return !mGeckoAccessible || [[self role] isEqualToString:NSAccessibilityUnknownRole]; NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(NO); } - (NSArray*)accessibilityAttributeNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; // if we're expired, we don't support any attributes. - if (mIsExpired) + if (!mGeckoAccessible) return [NSArray array]; static NSArray *generalAttributes = nil; if (!generalAttributes) { // standard attributes that are shared and supported by all generic elements. generalAttributes = [[NSArray alloc] initWithObjects: NSAccessibilityChildrenAttribute, NSAccessibilityParentAttribute, @@ -155,17 +144,17 @@ GetNativeFromGeckoAccessible(nsIAccessib NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (id)accessibilityAttributeValue:(NSString*)attribute { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; - if (mIsExpired) + if (!mGeckoAccessible) return nil; #if DEBUG if ([attribute isEqualToString:@"AXMozDescription"]) return [NSString stringWithFormat:@"role = %u native = %@", mRole, [self class]]; #endif if ([attribute isEqualToString:NSAccessibilityChildrenAttribute]) @@ -241,17 +230,17 @@ GetNativeFromGeckoAccessible(nsIAccessib if ([attribute isEqualToString:NSAccessibilityFocusedAttribute] && [value boolValue]) [self focus]; NS_OBJC_END_TRY_ABORT_BLOCK; } - (id)accessibilityHitTest:(NSPoint)point { - if (mIsExpired) + if (!mGeckoAccessible) return nil; // Convert from cocoa's coordinate system to gecko's. According to the docs // the point we're given is guaranteed to be bottom-left screen coordinates. nsPoint geckoPoint; ConvertCocoaToGeckoPoint (point, geckoPoint); nsCOMPtr<nsIAccessible> deepestFoundChild; @@ -283,17 +272,17 @@ GetNativeFromGeckoAccessible(nsIAccessib } - (void)accessibilityPerformAction:(NSString*)action { } - (id)accessibilityFocusedUIElement { - if (mIsExpired) + if (!mGeckoAccessible) return nil; Accessible* focusedGeckoChild = mGeckoAccessible->FocusedChild(); if (focusedGeckoChild) { mozAccessible *focusedChild = GetNativeFromGeckoAccessible(focusedGeckoChild); if (focusedChild) return GetClosestInterestingAccessible(focusedChild); } @@ -415,17 +404,17 @@ GetNativeFromGeckoAccessible(nsIAccessib mGeckoAccessible->GetBounds (&x, &y, &width, &height); return [NSValue valueWithSize:NSMakeSize (width, height)]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (NSString*)role { - if (mIsExpired) + if (!mGeckoAccessible) return nil; #ifdef DEBUG NS_ASSERTION(nsAccUtils::IsTextInterfaceSupportCorrect(mGeckoAccessible), "Does not support nsIAccessibleText when it should"); #endif #define ROLE(geckoRole, stringRole, atkRole, macRole, msaaRole, ia2Role) \ @@ -439,16 +428,52 @@ GetNativeFromGeckoAccessible(nsIAccessib return NSAccessibilityUnknownRole; } #undef ROLE } - (NSString*)subrole { + if (!mGeckoAccessible) + return nil; + + // XXX maybe we should cache the subrole. + nsAutoString xmlRoles; + nsCOMPtr<nsIPersistentProperties> attributes; + + // XXX we don't need all the attributes (see bug 771113) + nsresult rv = mGeckoAccessible->GetAttributes(getter_AddRefs(attributes)); + if (NS_SUCCEEDED(rv) && attributes) + nsAccUtils::GetAccAttr(attributes, nsGkAtoms::xmlroles, xmlRoles); + + nsWhitespaceTokenizer tokenizer(xmlRoles); + + while (tokenizer.hasMoreTokens()) { + const nsDependentSubstring token(tokenizer.nextToken()); + + if (token.EqualsLiteral("banner")) + return @"AXLandmarkBanner"; + + if (token.EqualsLiteral("complementary")) + return @"AXLandmarkComplementary"; + + if (token.EqualsLiteral("contentinfo")) + return @"AXLandmarkContentInfo"; + + if (token.EqualsLiteral("main")) + return @"AXLandmarkMain"; + + if (token.EqualsLiteral("navigation")) + return @"AXLandmarkNavigation"; + + if (token.EqualsLiteral("search")) + return @"AXLandmarkSearch"; + } + switch (mRole) { case roles::LIST: return @"AXContentList"; // 10.6+ NSAccessibilityContentListSubrole; case roles::DEFINITION_LIST: return @"AXDefinitionList"; // 10.6+ NSAccessibilityDefinitionListSubrole; case roles::TERM: @@ -463,34 +488,52 @@ GetNativeFromGeckoAccessible(nsIAccessib return nil; } - (NSString*)roleDescription { if (mRole == roles::DOCUMENT) return utils::LocalizedString(NS_LITERAL_STRING("htmlContent")); - + NSString* subrole = [self subrole]; - + if ((mRole == roles::LISTITEM) && [subrole isEqualToString:@"AXTerm"]) return utils::LocalizedString(NS_LITERAL_STRING("term")); if ((mRole == roles::PARAGRAPH) && [subrole isEqualToString:@"AXDefinition"]) return utils::LocalizedString(NS_LITERAL_STRING("definition")); - - return NSAccessibilityRoleDescription([self role], subrole); + + NSString* role = [self role]; + + // the WAI-ARIA Landmarks + if ([role isEqualToString:NSAccessibilityGroupRole]) { + if ([subrole isEqualToString:@"AXLandmarkBanner"]) + return utils::LocalizedString(NS_LITERAL_STRING("banner")); + if ([subrole isEqualToString:@"AXLandmarkComplementary"]) + return utils::LocalizedString(NS_LITERAL_STRING("complementary")); + if ([subrole isEqualToString:@"AXLandmarkContentInfo"]) + return utils::LocalizedString(NS_LITERAL_STRING("content")); + if ([subrole isEqualToString:@"AXLandmarkMain"]) + return utils::LocalizedString(NS_LITERAL_STRING("main")); + if ([subrole isEqualToString:@"AXLandmarkNavigation"]) + return utils::LocalizedString(NS_LITERAL_STRING("navigation")); + if ([subrole isEqualToString:@"AXLandmarkSearch"]) + return utils::LocalizedString(NS_LITERAL_STRING("search")); + } + + return NSAccessibilityRoleDescription(role, subrole); } - (NSString*)title { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; nsAutoString title; mGeckoAccessible->Name(title); - return title.IsEmpty() ? nil : [NSString stringWithCharacters:title.BeginReading() length:title.Length()]; + return nsCocoaUtils::ToNSString(title); NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (id)value { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; @@ -523,17 +566,18 @@ GetNativeFromGeckoAccessible(nsIAccessib { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if (mGeckoAccessible->IsDefunct()) return nil; nsAutoString desc; mGeckoAccessible->Description(desc); - return desc.IsEmpty() ? nil : [NSString stringWithCharacters:desc.BeginReading() length:desc.Length()]; + + return nsCocoaUtils::ToNSString(desc); NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (NSString*)help { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; @@ -556,28 +600,31 @@ GetNativeFromGeckoAccessible(nsIAccessib - (BOOL)isFocused { return FocusMgr()->IsFocused(mGeckoAccessible); } - (BOOL)canBeFocused { - return mGeckoAccessible->InteractiveState() & states::FOCUSABLE; + return mGeckoAccessible && (mGeckoAccessible->InteractiveState() & states::FOCUSABLE); } - (BOOL)focus { + if (!mGeckoAccessible) + return NO; + nsresult rv = mGeckoAccessible->TakeFocus(); return NS_SUCCEEDED(rv); } - (BOOL)isEnabled { - return (mGeckoAccessible->InteractiveState() & states::UNAVAILABLE) == 0; + return mGeckoAccessible && ((mGeckoAccessible->InteractiveState() & states::UNAVAILABLE) == 0); } // The root accessible calls this when the focused node was // changed to us. - (void)didReceiveFocus { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; @@ -632,25 +679,24 @@ GetNativeFromGeckoAccessible(nsIAccessib } - (void)expire { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [self invalidateChildren]; - mIsExpired = YES; mGeckoAccessible = nsnull; NS_OBJC_END_TRY_ABORT_BLOCK; } - (BOOL)isExpired { - return mIsExpired; + return !mGeckoAccessible; } #pragma mark - #pragma mark Debug methods #pragma mark - #ifdef DEBUG
--- a/accessible/src/mac/mozActionElements.h +++ b/accessible/src/mac/mozActionElements.h @@ -24,8 +24,15 @@ /* Class for tabs - not individual tabs */ @interface mozTabsAccessible : mozAccessible { NSMutableArray* mTabs; } -(id)tabs; @end + +/** + * Accessible for a PANE + */ +@interface mozPaneAccessible : mozAccessible + +@end
--- a/accessible/src/mac/mozActionElements.mm +++ b/accessible/src/mac/mozActionElements.mm @@ -2,18 +2,20 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #import "mozActionElements.h" #import "MacUtils.h" #include "Accessible-inl.h" +#include "DocAccessible.h" #include "XULTabAccessible.h" +#include "nsDeckFrame.h" #include "nsObjCExceptions.h" using namespace mozilla::a11y; enum CheckboxValue { // these constants correspond to the values in the OS kUnchecked = 0, kChecked = 1, @@ -66,17 +68,17 @@ enum CheckboxValue { return [super accessibilityAttributeValue:attribute]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (BOOL)accessibilityIsIgnored { - return mIsExpired; + return !mGeckoAccessible; } - (NSArray*)accessibilityActionNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if ([self isEnabled]) return [NSArray arrayWithObject:NSAccessibilityPressAction]; @@ -328,8 +330,35 @@ enum CheckboxValue { { [super invalidateChildren]; [mTabs release]; mTabs = nil; } @end + +@implementation mozPaneAccessible + +- (NSArray*)children +{ + if (!mGeckoAccessible) + return nil; + + nsDeckFrame* deckFrame = do_QueryFrame(mGeckoAccessible->GetFrame()); + nsIFrame* selectedFrame = deckFrame ? deckFrame->GetSelectedBox() : nsnull; + + Accessible* selectedAcc = nsnull; + if (selectedFrame) { + nsINode* node = selectedFrame->GetContent(); + selectedAcc = mGeckoAccessible->Document()->GetAccessible(node); + } + + if (selectedAcc) { + mozAccessible *curNative = GetNativeFromGeckoAccessible(selectedAcc); + if (curNative) + return [NSArray arrayWithObjects:GetObjectOrRepresentedView(curNative), nil]; + } + + return nil; +} + +@end
--- a/accessible/src/mac/mozDocAccessible.mm +++ b/accessible/src/mac/mozDocAccessible.mm @@ -26,17 +26,17 @@ getNativeViewFromRootAccessible(Accessib @implementation mozRootAccessible - (NSArray*)accessibilityAttributeNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; // if we're expired, we don't support any attributes. - if (mIsExpired) + if (!mGeckoAccessible) return [NSArray array]; // standard attributes that are shared and supported by root accessible (AXMain) elements. static NSMutableArray* attributes = nil; if (!attributes) { attributes = [[super accessibilityAttributeNames] mutableCopy]; [attributes addObject:NSAccessibilityMainAttribute];
--- a/accessible/src/mac/mozHTMLAccessible.mm +++ b/accessible/src/mac/mozHTMLAccessible.mm @@ -8,16 +8,27 @@ #import "mozHTMLAccessible.h" #import "HyperTextAccessible.h" #import "nsCocoaUtils.h" @implementation mozHeadingAccessible +- (NSString*)title +{ + nsAutoString title; + // XXX use the flattening API when there are available + // see bug 768298 + nsresult rv = mGeckoAccessible->GetContent()->GetTextContent(title); + NS_ENSURE_SUCCESS(rv, nil); + + return nsCocoaUtils::ToNSString(title); +} + - (id)value { if (!mGeckoAccessible || !mGeckoAccessible->IsHyperText()) return nil; PRUint32 level = mGeckoAccessible->AsHyperText()->GetLevelInternal(); return [NSNumber numberWithInt:level]; } @@ -28,17 +39,17 @@ -(NSURL*)url; @end @implementation mozLinkAccessible - (NSArray*)accessibilityAttributeNames { // if we're expired, we don't support any attributes. - if (mIsExpired) + if (!mGeckoAccessible) return [NSArray array]; static NSMutableArray* attributes = nil; if (!attributes) { attributes = [[super accessibilityAttributeNames] mutableCopy]; [attributes addObject:NSAccessibilityURLAttribute]; } @@ -52,17 +63,17 @@ return [self url]; return [super accessibilityAttributeValue:attribute]; } - (NSArray*)accessibilityActionNames { // if we're expired, we don't support any attributes. - if (mIsExpired) + if (!mGeckoAccessible) return [NSArray array]; static NSArray* actionNames = nil; if (!actionNames) { actionNames = [[NSArray alloc] initWithObjects:NSAccessibilityPressAction, nil]; }
--- a/accessible/src/mac/mozTextAccessible.h +++ b/accessible/src/mac/mozTextAccessible.h @@ -9,8 +9,13 @@ @interface mozTextAccessible : mozAccessible { // both of these are the same old mGeckoAccessible, but already // QI'd for us, to the right type, for convenience. HyperTextAccessible *mGeckoTextAccessible; // strong nsIAccessibleEditableText *mGeckoEditableTextAccessible; // strong } @end + +@interface mozTextLeafAccessible : mozAccessible +{ +} +@end
--- a/accessible/src/mac/mozTextAccessible.mm +++ b/accessible/src/mac/mozTextAccessible.mm @@ -1,15 +1,16 @@ /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AccessibleWrap.h" +#include "TextLeafAccessible.h" #include "nsCocoaUtils.h" #include "nsObjCExceptions.h" #import "mozTextAccessible.h" using namespace mozilla::a11y; @@ -62,17 +63,17 @@ ToNSString(id aValue) } return self; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (BOOL)accessibilityIsIgnored { - return mIsExpired; + return !mGeckoAccessible; } - (NSArray*)accessibilityAttributeNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; static NSMutableArray* supportedAttributes = nil; if (!supportedAttributes) { @@ -105,23 +106,28 @@ ToNSString(id aValue) return [self caretLineNumber]; if ([attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute]) return [self selectedTextRange]; if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute]) return [self selectedText]; + if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) + return @""; + if ([attribute isEqualToString:NSAccessibilityValueAttribute]) { // Apple's SpeechSynthesisServer expects AXValue to return an AXStaticText // object's AXSelectedText attribute. See bug 674612 for details. // Also if there is no selected text, we return the full text. // See bug 369710 for details. - if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) - return [self selectedText] ? : [self text]; + if ([[self role] isEqualToString:NSAccessibilityStaticTextRole]) { + NSString* selectedText = [self selectedText]; + return (selectedText && [selectedText length]) ? selectedText : [self text]; + } return [self text]; } if ([attribute isEqualToString:@"AXRequired"]) return [NSNumber numberWithBool:!!(mGeckoAccessible->State() & states::REQUIRED)]; if ([attribute isEqualToString:@"AXInvalid"]) @@ -221,17 +227,17 @@ ToNSString(id aValue) return nil; } - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; if ([attribute isEqualToString:NSAccessibilityValueAttribute]) - return [self isReadOnly]; + return ![self isReadOnly]; if ([attribute isEqualToString:NSAccessibilitySelectedTextAttribute] || [attribute isEqualToString:NSAccessibilitySelectedTextRangeAttribute] || [attribute isEqualToString:NSAccessibilityVisibleCharacterRangeAttribute]) return YES; return [super accessibilityIsAttributeSettable:attribute]; @@ -354,17 +360,17 @@ ToNSString(id aValue) mGeckoEditableTextAccessible->SetTextContents(text); } NS_OBJC_END_TRY_ABORT_BLOCK; } - (NSString*)text { - if (!mGeckoTextAccessible) + if (!mGeckoAccessible || !mGeckoTextAccessible) return nil; // A password text field returns an empty value if (mRole == roles::PASSWORD_TEXT) return @""; nsAutoString text; nsresult rv = mGeckoTextAccessible-> @@ -373,16 +379,19 @@ ToNSString(id aValue) return nsCocoaUtils::ToNSString(text); } - (long)textLength { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; + if (!mGeckoAccessible || !mGeckoTextAccessible) + return 0; + return mGeckoTextAccessible ? mGeckoTextAccessible->CharacterCount() : 0; NS_OBJC_END_TRY_ABORT_BLOCK_RETURN(0); } - (long)selectedTextLength { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_RETURN; @@ -479,8 +488,50 @@ ToNSString(id aValue) nsAutoString text; mGeckoTextAccessible->GetText(range->location, range->location + range->length, text); return nsCocoaUtils::ToNSString(text); } @end + +@implementation mozTextLeafAccessible + +- (NSArray*)accessibilityAttributeNames +{ + static NSMutableArray* supportedAttributes = nil; + if (!supportedAttributes) { + supportedAttributes = [[super accessibilityAttributeNames] mutableCopy]; + [supportedAttributes removeObject:NSAccessibilityChildrenAttribute]; + } + + return supportedAttributes; +} + +- (id)accessibilityAttributeValue:(NSString*)attribute +{ + if ([attribute isEqualToString:NSAccessibilityTitleAttribute]) + return @""; + + if ([attribute isEqualToString:NSAccessibilityValueAttribute]) + return [self text]; + + return [super accessibilityAttributeValue:attribute]; +} + +- (NSString*)text +{ + if (!mGeckoAccessible) + return nil; + + return nsCocoaUtils::ToNSString(mGeckoAccessible->AsTextLeaf()->Text()); +} + +- (long)textLength +{ + if (!mGeckoAccessible) + return 0; + + return mGeckoAccessible->AsTextLeaf()->Text().Length(); +} + +@end
--- a/accessible/src/msaa/AccessibleWrap.cpp +++ b/accessible/src/msaa/AccessibleWrap.cpp @@ -3,49 +3,48 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "AccessibleWrap.h" #include "Compatibility.h" #include "DocAccessible-inl.h" #include "EnumVariant.h" +#include "ia2AccessibleRelation.h" #include "nsAccUtils.h" #include "nsCoreUtils.h" +#include "nsIAccessibleEvent.h" +#include "nsIAccessibleRelation.h" #include "nsWinUtils.h" #include "Relation.h" #include "Role.h" +#include "RootAccessible.h" #include "States.h" - -#include "ia2AccessibleRelation.h" - -#include "nsIAccessibleEvent.h" -#include "nsIAccessibleRelation.h" - -#include "Accessible2_i.c" -#include "AccessibleRole.h" -#include "AccessibleStates.h" -#include "RootAccessible.h" +#include "uiaRawElmProvider.h" #ifdef DEBUG #include "Logging.h" #endif #include "nsIMutableArray.h" #include "nsIFrame.h" #include "nsIScrollableFrame.h" #include "nsINameSpaceManager.h" #include "nsINodeInfo.h" #include "nsIServiceManager.h" #include "nsTextFormatter.h" #include "nsIView.h" #include "nsIViewManager.h" #include "nsEventMap.h" #include "nsArrayUtils.h" +#include "mozilla/Preferences.h" +#include "Accessible2_i.c" +#include "AccessibleRole.h" +#include "AccessibleStates.h" #include "OLEACC.H" using namespace mozilla; using namespace mozilla::a11y; const PRUint32 USE_ROLE_STRING = 0; /* For documentation of the accessibility architecture, @@ -117,16 +116,42 @@ AccessibleWrap::QueryInterface(REFIID ii return nsAccessNodeWrap::QueryInterface(iid, ppv); (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); } __except(FilterA11yExceptions(::GetExceptionCode(), GetExceptionInformation())) { } return S_OK; } +//////////////////////////////////////////////////////////////////////////////// +// IServiceProvider + +STDMETHODIMP +AccessibleWrap::QueryService(REFGUID aGuidService, REFIID aIID, + void** aInstancePtr) +{ + if (!aInstancePtr) + return E_INVALIDARG; + + *aInstancePtr = NULL; + + // UIA IAccessibleEx + if (aGuidService == IID_IAccessibleEx && + Preferences::GetBool("accessibility.uia.enable")) { + IAccessibleEx* accEx = new uiaRawElmProvider(this); + HRESULT hr = accEx->QueryInterface(aIID, aInstancePtr); + if (FAILED(hr)) + delete accEx; + + return hr; + } + + return nsAccessNodeWrap::QueryService(aGuidService, aIID, aInstancePtr); +} + //----------------------------------------------------- // IAccessible methods //----------------------------------------------------- STDMETHODIMP AccessibleWrap::get_accParent( IDispatch __RPC_FAR *__RPC_FAR *ppdispParent) { __try {
--- a/accessible/src/msaa/AccessibleWrap.h +++ b/accessible/src/msaa/AccessibleWrap.h @@ -12,61 +12,114 @@ #include "nsCOMPtr.h" #include "Accessible.h" #include "Accessible2.h" #include "ia2AccessibleComponent.h" #include "ia2AccessibleHyperlink.h" #include "CAccessibleValue.h" -#define DECL_IUNKNOWN_INHERITED \ -public: \ -STDMETHODIMP QueryInterface(REFIID, void**); \ +#define DECL_IUNKNOWN \ +public: \ + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); \ + virtual ULONG STDMETHODCALLTYPE AddRef() MOZ_FINAL \ + { return ++mRefCnt; } \ + virtual ULONG STDMETHODCALLTYPE Release() MOZ_FINAL \ + { \ + mRefCnt--; \ + if (mRefCnt) \ + return mRefCnt; \ + \ + delete this; \ + return 0; \ + } \ +private: \ + ULONG mRefCnt; \ +public: + +#define DECL_IUNKNOWN_INHERITED \ +public: \ +virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID, void**); \ -#define IMPL_IUNKNOWN_QUERY_HEAD(Class) \ -STDMETHODIMP \ -Class::QueryInterface(REFIID iid, void** ppv) \ -{ \ - HRESULT hr = E_NOINTERFACE; \ - *ppv = NULL; \ +#define IMPL_IUNKNOWN_QUERY_HEAD(Class) \ +STDMETHODIMP \ +Class::QueryInterface(REFIID aIID, void** aInstancePtr) \ +{ \ +__try { \ + if (!aInstancePtr) \ + return E_INVALIDARG; \ + *aInstancePtr = NULL; \ + \ + HRESULT hr = E_NOINTERFACE; -#define IMPL_IUNKNOWN_QUERY_TAIL \ - return hr; \ -} \ +#define IMPL_IUNKNOWN_QUERY_TAIL \ + return hr; \ +} __except(nsAccessNodeWrap::FilterA11yExceptions(::GetExceptionCode(), \ + GetExceptionInformation())) { } \ + return E_NOINTERFACE; \ +} -#define IMPL_IUNKNOWN_QUERY_ENTRY(Class) \ - hr = Class::QueryInterface(iid, ppv); \ - if (SUCCEEDED(hr)) \ - return hr; \ +#define IMPL_IUNKNOWN_QUERY_IFACE(Iface) \ + if (aIID == IID_##Iface) { \ + *aInstancePtr = static_cast<Iface*>(this); \ + AddRef(); \ + return S_OK; \ + } -#define IMPL_IUNKNOWN_QUERY_ENTRY_COND(Class, Cond) \ - if (Cond) { \ - hr = Class::QueryInterface(iid, ppv); \ - if (SUCCEEDED(hr)) \ - return hr; \ - } \ +#define IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(Iface, aResolveIface) \ + if (aIID == IID_##Iface) { \ + *aInstancePtr = static_cast<Iface*>(static_cast<aResolveIface*>(this)); \ + AddRef(); \ + return S_OK; \ + } + +#define IMPL_IUNKNOWN_QUERY_CLASS(Class) \ + hr = Class::QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) \ + return hr; -#define IMPL_IUNKNOWN_INHERITED0(Class, Super) \ - IMPL_IUNKNOWN_QUERY_HEAD(Class) \ - IMPL_IUNKNOWN_QUERY_ENTRY(Super) \ - IMPL_IUNKNOWN_QUERY_TAIL \ +#define IMPL_IUNKNOWN_QUERY_CLASS_COND(Class, Cond) \ + if (Cond) { \ + hr = Class::QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) \ + return hr; \ + } + +#define IMPL_IUNKNOWN_QUERY_AGGR_COND(Member, Cond) \ + if (Cond) { \ + hr = Member->QueryInterface(aIID, aInstancePtr); \ + if (SUCCEEDED(hr)) \ + return hr; \ + } -#define IMPL_IUNKNOWN_INHERITED1(Class, Super, I1) \ - IMPL_IUNKNOWN_QUERY_HEAD(Class) \ - IMPL_IUNKNOWN_QUERY_ENTRY(I1); \ - IMPL_IUNKNOWN_QUERY_ENTRY(Super) \ - IMPL_IUNKNOWN_QUERY_TAIL \ +#define IMPL_IUNKNOWN1(Class, I1) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_IFACE(I1); \ + IMPL_IUNKNOWN_QUERY_IFACE(IUnknown); \ + IMPL_IUNKNOWN_QUERY_TAIL \ + +#define IMPL_IUNKNOWN2(Class, I1, I2) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_IFACE(I1); \ + IMPL_IUNKNOWN_QUERY_IFACE(I2); \ + IMPL_IUNKNOWN_QUERY_IFACE_AMBIGIOUS(IUnknown, I1); \ + IMPL_IUNKNOWN_QUERY_TAIL \ -#define IMPL_IUNKNOWN_INHERITED2(Class, Super, I1, I2) \ - IMPL_IUNKNOWN_QUERY_HEAD(Class) \ - IMPL_IUNKNOWN_QUERY_ENTRY(I1); \ - IMPL_IUNKNOWN_QUERY_ENTRY(I2); \ - IMPL_IUNKNOWN_QUERY_ENTRY(Super) \ - IMPL_IUNKNOWN_QUERY_TAIL \ +#define IMPL_IUNKNOWN_INHERITED1(Class, Super0, Super1) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_CLASS(Super1); \ + IMPL_IUNKNOWN_QUERY_CLASS(Super0) \ + IMPL_IUNKNOWN_QUERY_TAIL \ +#define IMPL_IUNKNOWN_INHERITED2(Class, Super0, Super1, Super2) \ + IMPL_IUNKNOWN_QUERY_HEAD(Class) \ + IMPL_IUNKNOWN_QUERY_CLASS(Super1); \ + IMPL_IUNKNOWN_QUERY_CLASS(Super2); \ + IMPL_IUNKNOWN_QUERY_CLASS(Super0) \ + IMPL_IUNKNOWN_QUERY_TAIL class AccessibleWrap : public Accessible, public ia2AccessibleComponent, public ia2AccessibleHyperlink, public CAccessibleValue, public IAccessible2 { public: // construction, destruction @@ -78,16 +131,21 @@ public: // construction, destruction NS_DECL_ISUPPORTS_INHERITED public: // IUnknown methods - see iunknown.h for documentation STDMETHODIMP QueryInterface(REFIID, void**); // Return the registered OLE class ID of this object's CfDataObj. CLSID GetClassID() const; + // IServiceProvider + virtual HRESULT STDMETHODCALLTYPE QueryService(REFGUID aGuidService, + REFIID aIID, + void** aInstancePtr); + public: // COM interface IAccessible virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accParent( /* [retval][out] */ IDispatch __RPC_FAR *__RPC_FAR *ppdispParent); virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChildCount( /* [retval][out] */ long __RPC_FAR *pcountChildren); virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_accChild(
--- a/accessible/src/msaa/EnumVariant.cpp +++ b/accessible/src/msaa/EnumVariant.cpp @@ -8,61 +8,21 @@ using namespace mozilla; using namespace mozilla::a11y; //////////////////////////////////////////////////////////////////////////////// // ChildrenEnumVariant //////////////////////////////////////////////////////////////////////////////// -STDMETHODIMP -ChildrenEnumVariant::QueryInterface(REFIID aIID, void** aObject) -{ -__try { - if (!aObject) - return E_INVALIDARG; - - if (aIID == IID_IEnumVARIANT) { - *aObject = static_cast<IEnumVARIANT*>(this); - AddRef(); - return S_OK; - } - - if (aIID == IID_IUnknown) { - *aObject = static_cast<IUnknown*>(this); - AddRef(); - return S_OK; - } - - // Redirect QI to IAccessible this enum was retrieved for. - if (!mAnchorAcc->IsDefunct()) - return mAnchorAcc->QueryInterface(aIID, aObject); - -} __except(nsAccessNodeWrap::FilterA11yExceptions(::GetExceptionCode(), - GetExceptionInformation())) { } - - return E_NOINTERFACE; -} - -ULONG STDMETHODCALLTYPE -ChildrenEnumVariant::AddRef() -{ - return ++mRefCnt; -} - -ULONG STDMETHODCALLTYPE -ChildrenEnumVariant::Release() -{ - mRefCnt--; - ULONG r = mRefCnt; - if (r == 0) - delete this; - - return r; -} +IMPL_IUNKNOWN_QUERY_HEAD(ChildrenEnumVariant) +IMPL_IUNKNOWN_QUERY_IFACE(IEnumVARIANT); +IMPL_IUNKNOWN_QUERY_IFACE(IUnknown); +IMPL_IUNKNOWN_QUERY_AGGR_COND(mAnchorAcc, !mAnchorAcc->IsDefunct()); +IMPL_IUNKNOWN_QUERY_TAIL STDMETHODIMP ChildrenEnumVariant::Next(ULONG aCount, VARIANT FAR* aItems, ULONG FAR* aCountFetched) { __try { if (!aItems || !aCountFetched) return E_INVALIDARG;
--- a/accessible/src/msaa/EnumVariant.h +++ b/accessible/src/msaa/EnumVariant.h @@ -17,22 +17,17 @@ namespace a11y { */ class ChildrenEnumVariant MOZ_FINAL : public IEnumVARIANT { public: ChildrenEnumVariant(AccessibleWrap* aAnchor) : mAnchorAcc(aAnchor), mCurAcc(mAnchorAcc->GetChildAt(0)), mCurIndex(0), mRefCnt(0) { } // IUnknown - virtual HRESULT STDMETHODCALLTYPE QueryInterface( - /* [in] */ REFIID aRefIID, - /* [annotation][iid_is][out] */ void** aObject); - - virtual ULONG STDMETHODCALLTYPE AddRef(); - virtual ULONG STDMETHODCALLTYPE Release(); + DECL_IUNKNOWN // IEnumVariant virtual /* [local] */ HRESULT STDMETHODCALLTYPE Next( /* [in] */ ULONG aCount, /* [length_is][size_is][out] */ VARIANT* aItems, /* [out] */ ULONG* aCountFetched); virtual HRESULT STDMETHODCALLTYPE Skip( @@ -51,17 +46,14 @@ private: mAnchorAcc(aEnumVariant.mAnchorAcc), mCurAcc(aEnumVariant.mCurAcc), mCurIndex(aEnumVariant.mCurIndex), mRefCnt(0) { } virtual ~ChildrenEnumVariant() { } protected: nsRefPtr<AccessibleWrap> mAnchorAcc; Accessible* mCurAcc; PRUint32 mCurIndex; - -private: - ULONG mRefCnt; }; } // a11y namespace } // mozilla namespace #endif
--- a/accessible/src/msaa/Makefile.in +++ b/accessible/src/msaa/Makefile.in @@ -64,11 +64,12 @@ include $(topsrcdir)/config/rules.mk LOCAL_INCLUDES += \ -I$(srcdir) \ -I$(srcdir)/../base \ -I$(srcdir)/../generic \ -I$(srcdir)/../html \ -I$(srcdir)/../xpcom \ -I$(srcdir)/../xul \ + -I$(srcdir)/../windows/uia \ -I$(srcdir)/../../../content/base/src \ -I$(srcdir)/../../../content/events/src \ $(NULL)
--- a/accessible/src/msaa/XULListboxAccessibleWrap.cpp +++ b/accessible/src/msaa/XULListboxAccessibleWrap.cpp @@ -16,18 +16,18 @@ XULListboxAccessibleWrap:: XULListboxAccessible(aContent, aDoc) { } NS_IMPL_ISUPPORTS_INHERITED0(XULListboxAccessibleWrap, XULListboxAccessible) IMPL_IUNKNOWN_QUERY_HEAD(XULListboxAccessibleWrap) -IMPL_IUNKNOWN_QUERY_ENTRY_COND(CAccessibleTable, IsMulticolumn()); -IMPL_IUNKNOWN_QUERY_ENTRY(AccessibleWrap) +IMPL_IUNKNOWN_QUERY_CLASS_COND(CAccessibleTable, IsMulticolumn()); +IMPL_IUNKNOWN_QUERY_CLASS(AccessibleWrap) IMPL_IUNKNOWN_QUERY_TAIL //////////////////////////////////////////////////////////////////////////////// // XULListCellAccessibleWrap //////////////////////////////////////////////////////////////////////////////// XULListCellAccessibleWrap::
--- a/accessible/src/msaa/nsAccessNodeWrap.cpp +++ b/accessible/src/msaa/nsAccessNodeWrap.cpp @@ -19,18 +19,16 @@ #include "nsAttrName.h" #include "nsIDOMNodeList.h" #include "nsIDOMHTMLElement.h" #include "nsIFrame.h" #include "nsINameSpaceManager.h" #include "nsPIDOMWindow.h" #include "nsIServiceManager.h" -#include "mozilla/Preferences.h" - using namespace mozilla; using namespace mozilla::a11y; AccTextChangeEvent* nsAccessNodeWrap::gTextEvent = nsnull; //////////////////////////////////////////////////////////////////////////////// // nsAccessNodeWrap //////////////////////////////////////////////////////////////////////////////// @@ -82,32 +80,23 @@ STDMETHODIMP nsAccessNodeWrap::QueryInte return S_OK; } STDMETHODIMP nsAccessNodeWrap::QueryService(REFGUID guidService, REFIID iid, void** ppv) { *ppv = nsnull; - static const GUID IID_SimpleDOMDeprecated = {0x0c539790,0x12e4,0x11cf,0xb6,0x61,0x00,0xaa,0x00,0x4c,0xd6,0xd8}; - // Provide a special service ID for getting the accessible for the browser tab // document that contains this accessible object. If this accessible object // is not inside a browser tab then the service fails with E_NOINTERFACE. // A use case for this is for screen readers that need to switch context or // 'virtual buffer' when focus moves from one browser tab area to another. - static const GUID SID_IAccessibleContentDocument = {0xa5d8e1f3,0x3571,0x4d8f,0x95,0x21,0x07,0xed,0x28,0xfb,0x07,0x2e}; - - if (guidService != IID_ISimpleDOMNode && - guidService != IID_SimpleDOMDeprecated && - guidService != IID_IAccessible && guidService != IID_IAccessible2 && - guidService != IID_IAccessibleApplication && - guidService != SID_IAccessibleContentDocument) - return E_INVALIDARG; - + static const GUID SID_IAccessibleContentDocument = + { 0xa5d8e1f3,0x3571,0x4d8f,0x95,0x21,0x07,0xed,0x28,0xfb,0x07,0x2e }; if (guidService == SID_IAccessibleContentDocument) { if (iid != IID_IAccessible) return E_NOINTERFACE; nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem = nsCoreUtils::GetDocShellTreeItemFor(mContent); if (!docShellTreeItem) return E_UNEXPECTED; @@ -134,17 +123,17 @@ nsAccessNodeWrap::QueryService(REFGUID g *ppv = static_cast<IAccessible*>(docAcc); (reinterpret_cast<IUnknown*>(*ppv))->AddRef(); return NS_OK; } // Can get to IAccessibleApplication from any node via QS - if (iid == IID_IAccessibleApplication) { + if (guidService == IID_IAccessibleApplication) { ApplicationAccessible* applicationAcc = GetApplicationAccessible(); if (!applicationAcc) return E_NOINTERFACE; nsresult rv = applicationAcc->QueryNativeInterface(iid, ppv); return NS_SUCCEEDED(rv) ? S_OK : E_NOINTERFACE; } @@ -157,17 +146,24 @@ nsAccessNodeWrap::QueryService(REFGUID g * pAcc->QueryInterface(IID_IServiceProvider, (void**)&pServProv); * if (pServProv) { * const GUID unused; * pServProv->QueryService(unused, IID_ISimpleDOMDocument, (void**)&pAccDoc); * pServProv->Release(); * } */ - return QueryInterface(iid, ppv); + static const GUID IID_SimpleDOMDeprecated = + { 0x0c539790,0x12e4,0x11cf,0xb6,0x61,0x00,0xaa,0x00,0x4c,0xd6,0xd8 }; + if (guidService == IID_ISimpleDOMNode || + guidService == IID_SimpleDOMDeprecated || + guidService == IID_IAccessible || guidService == IID_IAccessible2) + return QueryInterface(iid, ppv); + + return E_INVALIDARG; } //----------------------------------------------------- // ISimpleDOMNode methods //----------------------------------------------------- STDMETHODIMP nsAccessNodeWrap::get_nodeInfo( /* [out] */ BSTR __RPC_FAR *aNodeName,
--- a/accessible/src/msaa/nsAccessNodeWrap.h +++ b/accessible/src/msaa/nsAccessNodeWrap.h @@ -29,40 +29,50 @@ #include "OLEACC.H" #include <winuser.h> #ifdef MOZ_CRASHREPORTER #include "nsICrashReporter.h" #endif #include "nsRefPtrHashtable.h" +#define A11Y_TRYBLOCK_BEGIN \ + __try { + +#define A11Y_TRYBLOCK_END \ + } __except(nsAccessNodeWrap::FilterA11yExceptions(::GetExceptionCode(), \ + GetExceptionInformation()))\ + { } \ + return E_FAIL; class AccTextChangeEvent; class nsAccessNodeWrap : public nsAccessNode, public nsIWinAccessNode, public ISimpleDOMNode, public IServiceProvider { public: NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIWINACCESSNODE - public: // IServiceProvider - STDMETHODIMP QueryService(REFGUID guidService, REFIID riid, void** ppv); - public: // construction, destruction nsAccessNodeWrap(nsIContent* aContent, DocAccessible* aDoc); virtual ~nsAccessNodeWrap(); - // IUnknown - STDMETHODIMP QueryInterface(REFIID, void**); + // IUnknown + virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID aIID, + void** aInstancePtr); - public: + // IServiceProvider + virtual HRESULT STDMETHODCALLTYPE QueryService(REFGUID aGuidService, + REFIID aIID, + void** aInstancePtr); + // ISimpleDOMNode virtual /* [id][propget] */ HRESULT STDMETHODCALLTYPE get_nodeInfo( /* [out] */ BSTR __RPC_FAR *tagName, /* [out] */ short __RPC_FAR *nameSpaceID, /* [out] */ BSTR __RPC_FAR *nodeValue, /* [out] */ unsigned int __RPC_FAR *numChildren, /* [out] */ unsigned int __RPC_FAR *aUniqueID, /* [out][retval] */ unsigned short __RPC_FAR *nodeType);
new file mode 100644 --- /dev/null +++ b/accessible/src/windows/Makefile.in @@ -0,0 +1,16 @@ +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +DIRS += uia \ + $(null) + +include $(topsrcdir)/config/rules.mk
new file mode 100644 --- /dev/null +++ b/accessible/src/windows/uia/Makefile.in @@ -0,0 +1,36 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DEPTH = ../../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +MODULE = accessibility +LIBRARY_NAME = accessibility_toolkit_uia_s +EXPORT_LIBRARY = .. +LIBXUL_LIBRARY = 1 + + +CPPSRCS += \ + uiaRawElmProvider.cpp \ + $(NULL) + +# we don't want the shared lib, but we want to force the creation of a static lib. +FORCE_STATIC_LIB = 1 + +include $(topsrcdir)/config/config.mk +include $(topsrcdir)/config/rules.mk + +LOCAL_INCLUDES += \ + -I$(srcdir) \ + -I$(srcdir)/../../base \ + -I$(srcdir)/../../generic \ + -I$(srcdir)/../../html \ + -I$(srcdir)/../../msaa \ + -I$(srcdir)/../../xpcom \ + -I$(srcdir)/../../xul \ + $(NULL)
new file mode 100644 --- /dev/null +++ b/accessible/src/windows/uia/uiaRawElmProvider.cpp @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "uiaRawElmProvider.h" + +#include "AccessibleWrap.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +//////////////////////////////////////////////////////////////////////////////// +// uiaRawElmProvider +//////////////////////////////////////////////////////////////////////////////// + +IMPL_IUNKNOWN2(uiaRawElmProvider, + IAccessibleEx, + IRawElementProviderSimple) + +//////////////////////////////////////////////////////////////////////////////// +// IAccessibleEx + +STDMETHODIMP +uiaRawElmProvider::GetObjectForChild(long aIdChild, + __RPC__deref_out_opt IAccessibleEx** aAccEx) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aAccEx) + return E_INVALIDARG; + + *aAccEx = NULL; + + return mAcc->IsDefunct() ? CO_E_OBJNOTCONNECTED : S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::GetIAccessiblePair(__RPC__deref_out_opt IAccessible** aAcc, + __RPC__out long* aIdChild) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aAcc || !aIdChild) + return E_INVALIDARG; + + *aAcc = NULL; + *aIdChild = 0; + + if (mAcc->IsDefunct()) + return CO_E_OBJNOTCONNECTED; + + *aIdChild = CHILDID_SELF; + *aAcc = mAcc; + mAcc->AddRef(); + + return S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::GetRuntimeId(__RPC__deref_out_opt SAFEARRAY** aRuntimeIds) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aRuntimeIds) + return E_INVALIDARG; + + int ids[] = { UiaAppendRuntimeId, reinterpret_cast<int>(mAcc->UniqueID()) }; + *aRuntimeIds = SafeArrayCreateVector(VT_I4, 0, 2); + if (!*aRuntimeIds) + return E_OUTOFMEMORY; + + for (LONG i = 0; i < ArrayLength(ids); i++) + SafeArrayPutElement(*aRuntimeIds, &i, (void*)&(ids[i])); + + return S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::ConvertReturnedElement(__RPC__in_opt IRawElementProviderSimple* aRawElmProvider, + __RPC__deref_out_opt IAccessibleEx** aAccEx) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aRawElmProvider || !aAccEx) + return E_INVALIDARG; + + *aAccEx = NULL; + + void* instancePtr = NULL; + HRESULT hr = aRawElmProvider->QueryInterface(IID_IAccessibleEx, &instancePtr); + if (SUCCEEDED(hr)) + *aAccEx = static_cast<IAccessibleEx*>(instancePtr); + + return hr; + + A11Y_TRYBLOCK_END +} + +//////////////////////////////////////////////////////////////////////////////// +// IRawElementProviderSimple + +STDMETHODIMP +uiaRawElmProvider::get_ProviderOptions(__RPC__out enum ProviderOptions* aOptions) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aOptions) + return E_INVALIDARG; + + // This method is not used with IAccessibleEx implementations. + *aOptions = ProviderOptions_ServerSideProvider; + return S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::GetPatternProvider(PATTERNID aPatternId, + __RPC__deref_out_opt IUnknown** aPatternProvider) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aPatternProvider) + return E_INVALIDARG; + + *aPatternProvider = NULL; + return S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::GetPropertyValue(PROPERTYID aPropertyId, + __RPC__out VARIANT* aPropertyValue) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aPropertyValue) + return E_INVALIDARG; + + // UI Automation will attempt to get the property from the host + //window provider. + aPropertyValue->vt = VT_EMPTY; + return S_OK; + + A11Y_TRYBLOCK_END +} + +STDMETHODIMP +uiaRawElmProvider::get_HostRawElementProvider(__RPC__deref_out_opt IRawElementProviderSimple** aRawElmProvider) +{ + A11Y_TRYBLOCK_BEGIN + + if (!aRawElmProvider) + return E_INVALIDARG; + + // This method is not used with IAccessibleEx implementations. + *aRawElmProvider = NULL; + return S_OK; + + A11Y_TRYBLOCK_END +}
new file mode 100644 --- /dev/null +++ b/accessible/src/windows/uia/uiaRawElmProvider.h @@ -0,0 +1,74 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_a11y_uiaRawElmProvider_h__ +#define mozilla_a11y_uiaRawElmProvider_h__ + +#include "objbase.h" +#include "AccessibleWrap.h" +#include "UIAutomation.h" + +class AccessibleWrap; + +namespace mozilla { +namespace a11y { + +/** + * IRawElementProviderSimple implementation (maintains IAccessibleEx approach). + */ +class uiaRawElmProvider MOZ_FINAL : public IAccessibleEx, + public IRawElementProviderSimple +{ +public: + uiaRawElmProvider(AccessibleWrap* aAcc) : mAcc(aAcc), mRefCnt(0) { } + + // IUnknown + DECL_IUNKNOWN + + // IAccessibleEx + virtual HRESULT STDMETHODCALLTYPE GetObjectForChild( + /* [in] */ long aIdChild, + /* [retval][out] */ __RPC__deref_out_opt IAccessibleEx** aAccEx); + + virtual HRESULT STDMETHODCALLTYPE GetIAccessiblePair( + /* [out] */ __RPC__deref_out_opt IAccessible** aAcc, + /* [out] */ __RPC__out long* aIdChild); + + virtual HRESULT STDMETHODCALLTYPE GetRuntimeId( + /* [retval][out] */ __RPC__deref_out_opt SAFEARRAY** aRuntimeIds); + + virtual HRESULT STDMETHODCALLTYPE ConvertReturnedElement( + /* [in] */ __RPC__in_opt IRawElementProviderSimple* aRawElmProvider, + /* [out] */ __RPC__deref_out_opt IAccessibleEx** aAccEx); + + // IRawElementProviderSimple + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_ProviderOptions( + /* [retval][out] */ __RPC__out enum ProviderOptions* aProviderOptions); + + virtual HRESULT STDMETHODCALLTYPE GetPatternProvider( + /* [in] */ PATTERNID aPatternId, + /* [retval][out] */ __RPC__deref_out_opt IUnknown** aPatternProvider); + + virtual HRESULT STDMETHODCALLTYPE GetPropertyValue( + /* [in] */ PROPERTYID aPropertyId, + /* [retval][out] */ __RPC__out VARIANT* aPropertyValue); + + virtual /* [propget] */ HRESULT STDMETHODCALLTYPE get_HostRawElementProvider( + /* [retval][out] */ __RPC__deref_out_opt IRawElementProviderSimple** aRawElmProvider); + +private: + uiaRawElmProvider() MOZ_DELETE; + uiaRawElmProvider& operator =(const uiaRawElmProvider&) MOZ_DELETE; + uiaRawElmProvider(const uiaRawElmProvider&) MOZ_DELETE; + +protected: + nsRefPtr<AccessibleWrap> mAcc; +}; + +} // a11y namespace +} // mozilla namespace + +#endif
--- a/accessible/src/xul/XULTabAccessible.cpp +++ b/accessible/src/xul/XULTabAccessible.cpp @@ -159,32 +159,25 @@ nsresult XULTabsAccessible::GetNameInternal(nsAString& aName) { // no name return NS_OK; } //////////////////////////////////////////////////////////////////////////////// -// XULTabpanelsAccessible +// XULDeckAccessible //////////////////////////////////////////////////////////////////////////////// -XULTabpanelsAccessible:: - XULTabpanelsAccessible(nsIContent* aContent, DocAccessible* aDoc) : - AccessibleWrap(aContent, aDoc) -{ -} - role -XULTabpanelsAccessible::NativeRole() +XULDeckAccessible::NativeRole() { return roles::PANE; } - //////////////////////////////////////////////////////////////////////////////// // XULTabpanelAccessible //////////////////////////////////////////////////////////////////////////////// XULTabpanelAccessible:: XULTabpanelAccessible(nsIContent* aContent, DocAccessible* aDoc) : AccessibleWrap(aContent, aDoc) {
--- a/accessible/src/xul/XULTabAccessible.h +++ b/accessible/src/xul/XULTabAccessible.h @@ -51,23 +51,25 @@ public: virtual nsresult GetNameInternal(nsAString& aName); virtual a11y::role NativeRole(); // ActionAccessible virtual PRUint8 ActionCount(); }; -/** +/** * A container of tab panels, xul:tabpanels element. */ -class XULTabpanelsAccessible : public AccessibleWrap +class XULDeckAccessible : public AccessibleWrap { public: - XULTabpanelsAccessible(nsIContent* aContent, DocAccessible* aDoc); + XULDeckAccessible(nsIContent* aContent, DocAccessible* aDoc) : + AccessibleWrap(aContent, aDoc) + { mFlags |= eXULDeckAccessible; } // Accessible virtual a11y::role NativeRole(); }; /** * A tabpanel object, child elements of xul:tabpanels element. Note,the object
--- a/accessible/tests/mochitest/attributes/test_text.html +++ b/accessible/tests/mochitest/attributes/test_text.html @@ -537,16 +537,33 @@ var attrs = { "auto-generated": "true" }; testTextAttrs(ID, 0, attrs, defAttrs, 0, 2); testTextAttrs(ID, 2, { }, defAttrs, 2, 6); testTextAttrs(ID, 6, attrs, defAttrs, 6, 7); + ////////////////////////////////////////////////////////////////////////// + // area19, "HTML5 mark tag" test + // text enclosed in mark tag will have a different background color + ID = "area19"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 10); + + tempElem = getNode(ID).firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem, ""); + attrs = { "background-color": gComputedStyle.backgroundColor }; + testTextAttrs(ID, 11, attrs, defAttrs, 10, 17); + + attrs = {}; + testTextAttrs(ID, 18, attrs, defAttrs, 17, 28); + SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTest); </script> </head> <body style="font-size: 12pt"> @@ -687,10 +704,14 @@ </span><span style="text-decoration: line-through; -moz-text-decoration-style: wavy;">wavy </span> </p> <ul> <li id="area18" class="gencontent">item</li> </ul> + <p id="area19">uncolored + <mark>colored</mark> uncolored + </p> + </body> </html>
--- a/b2g/app/b2g.js +++ b/b2g/app/b2g.js @@ -373,16 +373,19 @@ pref("dom.ipc.tabs.disabled", false); pref("dom.ipc.browser_frames.oop_by_default", false); // Temporary permission hack for WebSMS pref("dom.sms.enabled", true); // Temporary permission hack for WebContacts pref("dom.mozContacts.enabled", true); +// WebAlarms +pref("dom.mozAlarms.enabled", true); + // WebSettings pref("dom.mozSettings.enabled", true); // Ignore X-Frame-Options headers. pref("b2g.ignoreXFrameOptions", true); // controls if we want camera support pref("device.camera.enabled", true);
--- a/b2g/chrome/content/dbg-browser-actors.js +++ b/b2g/chrome/content/dbg-browser-actors.js @@ -156,17 +156,17 @@ DeviceTabActor.prototype = { grip: function DTA_grip() { dbg_assert(!this.exited, 'grip() should not be called on exited browser actor.'); dbg_assert(this.actorID, 'tab should have an actorID.'); return { 'actor': this.actorID, - 'title': this.browser.contentTitle, + 'title': this.browser.title, 'url': this.browser.document.documentURI } }, /** * Called when the actor is removed from the connection. */ disconnect: function DTA_disconnect() { @@ -217,17 +217,17 @@ DeviceTabActor.prototype = { */ _pushContext: function DTA_pushContext() { dbg_assert(!this._contextPool, "Can't push multiple contexts"); this._contextPool = new ActorPool(this.conn); this.conn.addActorPool(this._contextPool); this.threadActor = new ThreadActor(this); - this._addDebuggees(this.browser.content.wrappedJSObject); + this._addDebuggees(this.browser.wrappedJSObject); this._contextPool.addActor(this.threadActor); }, /** * Add the provided window and all windows in its frame tree as debuggees. */ _addDebuggees: function DTA__addDebuggees(content) { this.threadActor.addDebuggee(content); @@ -288,28 +288,28 @@ DeviceTabActor.prototype = { return { type: 'detached' }; }, /** * Prepare to enter a nested event loop by disabling debuggee events. */ preNest: function DTA_preNest() { - let windowUtils = this.browser.content + let windowUtils = this.browser .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); windowUtils.suppressEventHandling(true); windowUtils.suspendTimeouts(); }, /** * Prepare to exit a nested event loop by enabling debuggee events. */ postNest: function DTA_postNest(aNestData) { - let windowUtils = this.browser.content + let windowUtils = this.browser .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIDOMWindowUtils); windowUtils.resumeTimeouts(); windowUtils.suppressEventHandling(false); } };
--- a/b2g/chrome/content/settings.js +++ b/b2g/chrome/content/settings.js @@ -74,20 +74,23 @@ SettingsListener.observe('language.curre SettingsListener.observe(key, false, function(value) { Services.prefs.setIntPref(key, value); }); }); })(); // =================== Debugger ==================== -SettingsListener.observe('devtools.debugger.enabled', false, function(enabled) { - Services.prefs.setBoolPref('devtools.debugger.enabled', value); +SettingsListener.observe('devtools.debugger.remote-enabled', false, function(enabled) { + Services.prefs.setBoolPref('devtools.debugger.remote-enabled', value); }); SettingsListener.observe('devtools.debugger.log', false, function(value) { Services.prefs.setBoolPref('devtools.debugger.log', value); }); -SettingsListener.observe('devtools.debugger.port', 6000, function(value) { - Services.prefs.setIntPref('devtools.debugger.port', value); +SettingsListener.observe('devtools.debugger.remote-port', 6000, function(value) { + Services.prefs.setIntPref('devtools.debugger.remote-port', value); }); +SettingsListener.observe('devtools.debugger.force-local', true, function(value) { + Services.prefs.setBoolPref('devtools.debugger.force-local', value); +});
--- a/b2g/chrome/content/shell.js +++ b/b2g/chrome/content/shell.js @@ -9,16 +9,17 @@ const Ci = Components.interfaces; const Cu = Components.utils; const Cr = Components.results; Cu.import('resource://gre/modules/XPCOMUtils.jsm'); Cu.import('resource://gre/modules/Services.jsm'); Cu.import('resource://gre/modules/ContactService.jsm'); Cu.import('resource://gre/modules/SettingsChangeNotifier.jsm'); Cu.import('resource://gre/modules/Webapps.jsm'); +Cu.import('resource://gre/modules/AlarmService.jsm'); XPCOMUtils.defineLazyServiceGetter(Services, 'env', '@mozilla.org/process/environment;1', 'nsIEnvironment'); XPCOMUtils.defineLazyServiceGetter(Services, 'ss', '@mozilla.org/content/style-sheet-service;1', 'nsIStyleSheetService'); @@ -43,20 +44,20 @@ XPCOMUtils.defineLazyGetter(this, 'Debug }); // FIXME Bug 707625 // until we have a proper security model, add some rights to // the pre-installed web applications // XXX never grant 'content-camera' to non-gaia apps function addPermissions(urls) { let permissions = [ - 'indexedDB', 'indexedDB-unlimited', 'webapps-manage', 'offline-app', 'pin-app', + 'indexedDB-unlimited', 'webapps-manage', 'offline-app', 'pin-app', 'websettings-read', 'websettings-readwrite', 'content-camera', 'webcontacts-manage', 'wifi-manage', 'desktop-notification', - 'geolocation', 'device-storage' + 'geolocation', 'device-storage', 'alarms' ]; urls.forEach(function(url) { url = url.trim(); let uri = Services.io.newURI(url, null, null); let allow = Ci.nsIPermissionManager.ALLOW_ACTION; permissions.forEach(function(permission) { Services.perms.add(uri, permission, allow);
--- a/b2g/config/mozconfigs/gb_armv7a_gecko/debug +++ b/b2g/config/mozconfigs/gb_armv7a_gecko/debug @@ -2,17 +2,16 @@ mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/ob mk_add_options MOZ_MAKE_FLAGS="-j8" ac_add_options --enable-application=b2g ac_add_options --target=arm-android-eabi ac_add_options --with-gonk="$topsrcdir/gonk-toolchain" ac_add_options --with-gonk-toolchain-prefix="$topsrcdir/gonk-toolchain/prebuilt/$TOOLCHAIN_HOST/toolchain/arm-eabi-4.4.3/bin/arm-eabi-" -ac_add_options --with-endian=little ac_add_options --disable-elf-hack ac_add_options --enable-debug-symbols ac_add_options --enable-debug ac_add_options --with-ccache ac_add_options --enable-marionette # Enable dump() from JS. export CXXFLAGS=-DMOZ_ENABLE_JS_DUMP
--- a/b2g/config/mozconfigs/gb_armv7a_gecko/nightly +++ b/b2g/config/mozconfigs/gb_armv7a_gecko/nightly @@ -2,17 +2,16 @@ mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/ob mk_add_options MOZ_MAKE_FLAGS="-j8" ac_add_options --enable-application=b2g ac_add_options --target=arm-android-eabi ac_add_options --with-gonk="$topsrcdir/gonk-toolchain" ac_add_options --with-gonk-toolchain-prefix="$topsrcdir/gonk-toolchain/prebuilt/$TOOLCHAIN_HOST/toolchain/arm-eabi-4.4.3/bin/arm-eabi-" -ac_add_options --with-endian=little ac_add_options --disable-elf-hack ac_add_options --enable-debug-symbols ac_add_options --enable-profiling ac_add_options --with-ccache ac_add_options --enable-marionette # Enable dump() from JS. export CXXFLAGS=-DMOZ_ENABLE_JS_DUMP
--- a/b2g/installer/Makefile.in +++ b/b2g/installer/Makefile.in @@ -50,16 +50,20 @@ ifeq (bundle, $(MOZ_FS_LAYOUT)) BINPATH = $(_BINPATH) DEFINES += -DAPPNAME=$(_APPNAME) else # Every other platform just winds up in dist/bin BINPATH = bin endif DEFINES += -DBINPATH=$(BINPATH) +ifneq (,$(filter WINNT Darwin Android,$(OS_TARGET))) +DEFINES += -DMOZ_SHARED_MOZGLUE=1 +endif + ifdef MOZ_PKG_MANIFEST_P $(MOZ_PKG_MANIFEST): $(MOZ_PKG_MANIFEST_P) FORCE $(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $(ACDEFINES) $< > $@ GARBAGE += $(MOZ_PKG_MANIFEST) endif ifneq (,$(filter mac cocoa,$(MOZ_WIDGET_TOOLKIT)))
--- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -77,21 +77,23 @@ @BINPATH@/msvcp100.dll @BINPATH@/msvcr100.dll #endif #else @BINPATH@/mozcrt19.dll @BINPATH@/mozcpp19.dll #endif #endif +#ifdef MOZ_SHARED_MOZGLUE +@BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@ +#endif #ifdef ANDROID @BINPATH@/AndroidManifest.xml @BINPATH@/resources.arsc @BINPATH@/classes.dex -@BINPATH@/@DLL_PREFIX@mozglue@DLL_SUFFIX@ @BINPATH@/res/drawable @BINPATH@/res/drawable-hdpi @BINPATH@/res/layout #endif [browser] ; [Base Browser Files] #ifndef XP_UNIX @@ -156,16 +158,17 @@ @BINPATH@/components/dom_system_gonk.xpt #endif @BINPATH@/components/dom_battery.xpt #ifdef MOZ_B2G_BT @BINPATH@/components/dom_bluetooth.xpt #endif @BINPATH@/components/dom_canvas.xpt @BINPATH@/components/dom_contacts.xpt +@BINPATH@/components/dom_alarm.xpt @BINPATH@/components/dom_core.xpt @BINPATH@/components/dom_css.xpt @BINPATH@/components/dom_devicestorage.xpt @BINPATH@/components/dom_events.xpt @BINPATH@/components/dom_file.xpt @BINPATH@/components/dom_geolocation.xpt @BINPATH@/components/dom_media.xpt @BINPATH@/components/dom_network.xpt @@ -312,16 +315,18 @@ ; JavaScript components @BINPATH@/components/ConsoleAPI.manifest @BINPATH@/components/ConsoleAPI.js @BINPATH@/components/BrowserElementParent.manifest @BINPATH@/components/BrowserElementParent.js @BINPATH@/components/ContactManager.js @BINPATH@/components/ContactManager.manifest +@BINPATH@/components/AlarmsManager.js +@BINPATH@/components/AlarmsManager.manifest @BINPATH@/components/FeedProcessor.manifest @BINPATH@/components/FeedProcessor.js @BINPATH@/components/BrowserFeeds.manifest @BINPATH@/components/FeedConverter.js @BINPATH@/components/FeedWriter.js @BINPATH@/components/fuelApplication.manifest @BINPATH@/components/fuelApplication.js @BINPATH@/components/WebContentConverter.js
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1018,17 +1018,17 @@ pref("services.sync.prefs.sync.signon.re pref("services.sync.prefs.sync.spellchecker.dictionary", true); pref("services.sync.prefs.sync.xpinstall.whitelist.required", true); #endif // Disable the error console pref("devtools.errorconsole.enabled", false); // Developer toolbar and GCLI preferences -pref("devtools.toolbar.enabled", false); +pref("devtools.toolbar.enabled", true); pref("devtools.toolbar.visible", false); pref("devtools.gcli.allowSet", false); pref("devtools.commands.dir", ""); // Enable the Inspector pref("devtools.inspector.enabled", true); pref("devtools.inspector.htmlHeight", 112); pref("devtools.inspector.htmlPanelOpen", false); @@ -1068,16 +1068,21 @@ pref("devtools.tilt.intro_transition", t pref("devtools.tilt.outro_transition", true); // Enable the rules view pref("devtools.ruleview.enabled", true); // Enable the Scratchpad tool. pref("devtools.scratchpad.enabled", true); +// The maximum number of recently-opened files stored. +// Setting this preference to 0 will not clear any recent files, but rather hide +// the 'Open Recent'-menu. +pref("devtools.scratchpad.recentFilesMax", 10); + // Enable the Style Editor. pref("devtools.styleeditor.enabled", true); pref("devtools.styleeditor.transitions", true); // Enable tools for Chrome development. pref("devtools.chrome.enabled", false); // Display the introductory text @@ -1172,8 +1177,11 @@ pref("pdfjs.firstRun", true); // became the default. pref("pdfjs.previousHandler.preferredAction", 0); pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false); // The maximum amount of decoded image data we'll willingly keep around (we // might keep around more than this, but we'll try to get down to this value). // (This is intentionally on the high side; see bug 746055.) pref("image.mem.max_decoded_image_kb", 256000); + +// Example social provider +pref("social.manifest.motown", "{\"origin\":\"https://motown-dev.mozillalabs.com\",\"name\":\"MoTown\",\"workerURL\":\"https://motown-dev.mozillalabs.com/social/worker.js\"}");
--- a/browser/base/content/browser-appmenu.inc +++ b/browser/base/content/browser-appmenu.inc @@ -113,17 +113,17 @@ key="key_find"/> <menuseparator class="appmenu-menuseparator"/> <menuitem id="appmenu_savePage" class="menuitem-tooltip" label="&savePageCmd.label;" command="Browser:SavePage" key="key_savePage"/> <menuitem id="appmenu_sendLink" - label="&sendPageCmd.label;" + label="&emailPageCmd.label;" command="Browser:SendLink"/> <splitmenu id="appmenu_print" iconic="true" label="&printCmd.label;" command="cmd_print"> <menupopup> <menuitem id="appmenu_print_popup" class="menuitem-iconic"
--- a/browser/base/content/browser-context.inc +++ b/browser/base/content/browser-context.inc @@ -32,20 +32,16 @@ <menuitem id="context-bookmarklink" label="&bookmarkThisLinkCmd.label;" accesskey="&bookmarkThisLinkCmd.accesskey;" oncommand="gContextMenu.bookmarkLink();"/> <menuitem id="context-savelink" label="&saveLinkCmd.label;" accesskey="&saveLinkCmd.accesskey;" oncommand="gContextMenu.saveLink();"/> - <menuitem id="context-sendlink" - label="&sendLinkCmd.label;" - accesskey="&sendLinkCmd.accesskey;" - oncommand="gContextMenu.sendLink();"/> <menuitem id="context-copyemail" label="©EmailCmd.label;" accesskey="©EmailCmd.accesskey;" oncommand="gContextMenu.copyEmail();"/> <menuitem id="context-copylink" label="©LinkCmd.label;" accesskey="©LinkCmd.accesskey;" oncommand="goDoCommand('cmd_copyLink');"/> @@ -124,18 +120,18 @@ accesskey="©AudioURLCmd.accesskey;" oncommand="gContextMenu.copyMediaLocation();"/> <menuseparator id="context-sep-copyimage"/> <menuitem id="context-saveimage" label="&saveImageCmd.label;" accesskey="&saveImageCmd.accesskey;" oncommand="gContextMenu.saveMedia();"/> <menuitem id="context-sendimage" - label="&sendImageCmd.label;" - accesskey="&sendImageCmd.accesskey;" + label="&emailImageCmd.label;" + accesskey="&emailImageCmd.accesskey;" oncommand="gContextMenu.sendMedia();"/> <menuitem id="context-setDesktopBackground" label="&setDesktopBackgroundCmd.label;" accesskey="&setDesktopBackgroundCmd.accesskey;" oncommand="gContextMenu.setDesktopBackground();"/> <menuitem id="context-viewimageinfo" label="&viewImageInfoCmd.label;" accesskey="&viewImageInfoCmd.accesskey;" @@ -148,22 +144,22 @@ label="&saveAudioCmd.label;" accesskey="&saveAudioCmd.accesskey;" oncommand="gContextMenu.saveMedia();"/> <menuitem id="context-video-saveimage" accesskey="&videoSaveImage.accesskey;" label="&videoSaveImage.label;" oncommand="gContextMenu.saveVideoFrameAsImage();"/> <menuitem id="context-sendvideo" - label="&sendVideoCmd.label;" - accesskey="&sendVideoCmd.accesskey;" + label="&emailVideoCmd.label;" + accesskey="&emailVideoCmd.accesskey;" oncommand="gContextMenu.sendMedia();"/> <menuitem id="context-sendaudio" - label="&sendAudioCmd.label;" - accesskey="&sendAudioCmd.accesskey;" + label="&emailAudioCmd.label;" + accesskey="&emailAudioCmd.accesskey;" oncommand="gContextMenu.sendMedia();"/> <menuitem id="context-back" label="&backCmd.label;" accesskey="&backCmd.accesskey;" command="Browser:BackOrBackDuplicate" onclick="checkForMiddleClick(this, event);"/> <menuitem id="context-forward" label="&forwardCmd.label;" @@ -183,20 +179,16 @@ <menuitem id="context-bookmarkpage" label="&bookmarkPageCmd2.label;" accesskey="&bookmarkPageCmd2.accesskey;" oncommand="gContextMenu.bookmarkThisPage();"/> <menuitem id="context-savepage" label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey2;" oncommand="gContextMenu.savePageAs();"/> - <menuitem id="context-sendpage" - label="&sendPageCmd.label;" - accesskey="&sendPageCmd.accesskey;" - oncommand="gContextMenu.sendPage();"/> <menuseparator id="context-sep-viewbgimage"/> <menuitem id="context-viewbgimage" label="&viewBGImageCmd.label;" accesskey="&viewBGImageCmd.accesskey;" oncommand="gContextMenu.viewBGImage(event);" onclick="checkForMiddleClick(this, event);"/> <menuitem id="context-undo" label="&undoCmd.label;"
--- a/browser/base/content/browser-menubar.inc +++ b/browser/base/content/browser-menubar.inc @@ -48,18 +48,18 @@ accesskey="&closeWindow.accesskey;"/> <menuseparator/> <menuitem id="menu_savePage" label="&savePageCmd.label;" accesskey="&savePageCmd.accesskey;" key="key_savePage" command="Browser:SavePage"/> <menuitem id="menu_sendLink" - label="&sendPageCmd.label;" - accesskey="&sendPageCmd.accesskey;" + label="&emailPageCmd.label;" + accesskey="&emailPageCmd.accesskey;" command="Browser:SendLink"/> <menuseparator/> <menuitem id="menu_printSetup" label="&printSetupCmd.label;" accesskey="&printSetupCmd.accesskey;" command="cmd_pageSetup"/> #ifndef XP_MACOSX <menuitem id="menu_printPreview" @@ -514,17 +514,16 @@ label="&webDeveloperMenu.label;" accesskey="&webDeveloperMenu.accesskey;"> <menupopup id="menuWebDeveloperPopup"> <menuitem id="menu_devToolbar" type="checkbox" autocheck="false" hidden="true" label="&devToolbarMenu.label;" - accesskey="&devToolbarMenu.accesskey;" key="key_devToolbar" command="Tools:DevToolbar"/> <menuitem id="webConsole" type="checkbox" label="&webConsoleCmd.label;" accesskey="&webConsoleCmd.accesskey;" key="key_webConsole" command="Tools:WebConsole"/>
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -220,23 +220,16 @@ #ifdef XP_GNOME <key id="key_search2" key="&searchFocusUnix.commandkey;" command="Tools:Search" modifiers="accel"/> <key id="key_openDownloads" key="&downloadsUnix.commandkey;" command="Tools:Downloads" modifiers="accel,shift"/> #else <key id="key_openDownloads" key="&downloads.commandkey;" command="Tools:Downloads" modifiers="accel"/> #endif <key id="key_openAddons" key="&addons.commandkey;" command="Tools:Addons" modifiers="accel,shift"/> <key id="key_errorConsole" key="&errorConsoleCmd.commandkey;" oncommand="toJavaScriptConsole();" modifiers="accel,shift" disabled="true"/> - <key id="key_devToolbar" key="&devToolbar.commandkey;" command="Tools:DevToolbar" -#ifdef XP_MACOSX - modifiers="accel,alt" -#else - modifiers="accel,shift" -#endif - /> <key id="key_webConsole" key="&webConsoleCmd.commandkey;" oncommand="HUDConsoleUI.toggleHUD();" #ifdef XP_MACOSX modifiers="accel,alt" #else modifiers="accel,shift" #endif /> <key id="key_debugger" key="&debuggerMenu.commandkey;" command="Tools:Debugger"
--- a/browser/base/content/browser.css +++ b/browser/base/content/browser.css @@ -563,16 +563,26 @@ html|*#gcli-output-frame, } .gclitoolbar-input-node, .gclitoolbar-complete-node, .gclitoolbar-prompt { direction: ltr; } +#developer-toolbar-webconsole[error-count] > .toolbarbutton-icon { + display: none; +} + +#developer-toolbar-webconsole[error-count]:before { + content: attr(error-count); + display: -moz-box; + -moz-box-pack: center; +} + /* Responsive Mode */ vbox[anonid=browserContainer][responsivemode] { overflow: auto; } .devtools-responsiveui-toolbar:-moz-locale-dir(rtl) { -moz-box-pack: end;
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -1395,17 +1395,17 @@ var gBrowserInit = { document.getElementById("menu_devToolbar").hidden = false; document.getElementById("Tools:DevToolbar").removeAttribute("disabled"); #ifdef MENUBAR_CAN_AUTOHIDE document.getElementById("appmenu_devToolbar").hidden = false; #endif // Show the toolbar if it was previously visible if (gPrefService.getBoolPref("devtools.toolbar.visible")) { - DeveloperToolbar.show(); + DeveloperToolbar.show(false); } } // Enable Inspector? let enabled = gPrefService.getBoolPref("devtools.inspector.enabled"); if (enabled) { document.getElementById("menu_pageinspect").hidden = false; document.getElementById("Tools:Inspect").removeAttribute("disabled"); @@ -2605,23 +2605,26 @@ function BrowserOnClick(event) { let notificationBox = gBrowser.getNotificationBox(); let value = "blocked-badware-page"; let previousNotification = notificationBox.getNotificationWithValue(value); if (previousNotification) notificationBox.removeNotification(previousNotification); - notificationBox.appendNotification( + let notification = notificationBox.appendNotification( title, value, "chrome://global/skin/icons/blacklist_favicon.png", notificationBox.PRIORITY_CRITICAL_HIGH, buttons ); + // Persist the notification until the user removes so it + // doesn't get removed on redirects. + notification.persistence = -1; } } else if (/^about:home$/i.test(ownerDoc.documentURI)) { if (ot == ownerDoc.getElementById("restorePreviousSession")) { let ss = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); if (ss.canRestoreLastSession) ss.restoreLastSession(); @@ -5256,20 +5259,19 @@ function middleMousePaste(event) { try { addToUrlbarHistory(url); } catch (ex) { // Things may go wrong when adding url to session history, // but don't let that interfere with the loading of the url. Cu.reportError(ex); } - // FIXME: Bug 631500, use openUILink directly - let where = whereToOpenLink(event, true); - openUILinkIn(url, where, - { disallowInheritPrincipal: !mayInheritPrincipal.value }); + openUILink(url, event, + { ignoreButton: true, + disallowInheritPrincipal: !mayInheritPrincipal.value }); event.stopPropagation(); } function handleDroppedLink(event, url, name) { let postData = { }; let uri = getShortcutOrURI(url, postData); @@ -6909,38 +6911,53 @@ function getTabModalPromptBox(aWindow) { function getBrowser() gBrowser; function getNavToolbox() gNavToolbox; let gPrivateBrowsingUI = { _privateBrowsingService: null, _searchBarValue: null, _findBarValue: null, _inited: false, + _initCallbacks: [], init: function PBUI_init() { Services.obs.addObserver(this, "private-browsing", false); Services.obs.addObserver(this, "private-browsing-transition-complete", false); this._privateBrowsingService = Cc["@mozilla.org/privatebrowsing;1"]. getService(Ci.nsIPrivateBrowsingService); if (this.privateBrowsingEnabled) this.onEnterPrivateBrowsing(true); this._inited = true; + + this._initCallbacks.forEach(function (callback) callback.apply()); + this._initCallbacks = []; }, uninit: function PBUI_unint() { if (!this._inited) return; Services.obs.removeObserver(this, "private-browsing"); Services.obs.removeObserver(this, "private-browsing-transition-complete"); }, + get initialized() { + return this._inited; + }, + + addInitializationCallback: function PBUI_addInitializationCallback(aCallback) { + if (this._inited) + return; + + this._initCallbacks.push(aCallback); + }, + get _disableUIOnToggle() { if (this._privateBrowsingService.autoStarted) return false; try { return !gPrefService.getBoolPref("browser.privatebrowsing.keep_current_session"); } catch (e) { @@ -7133,24 +7150,26 @@ let gPrivateBrowsingUI = { if (!this.privateBrowsingEnabled) if (!this._shouldEnter()) return; this._privateBrowsingService.privateBrowsingEnabled = !this.privateBrowsingEnabled; }, + get autoStarted() { + return this._privateBrowsingService.autoStarted; + }, + get privateBrowsingEnabled() { return this._privateBrowsingService.privateBrowsingEnabled; }, /** - * These accessors are used to support per-window Private Browsing mode. - * For now the getter returns nsIPrivateBrowsingService.privateBrowsingEnabled, - * and the setter should only be used in tests. + * This accessor is used to support per-window Private Browsing mode. */ get privateWindow() { if (!gBrowser) return false; return gBrowser.docShell.QueryInterface(Ci.nsILoadContext) .usePrivateBrowsing; }
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -1032,31 +1032,31 @@ <hbox class="gclitoolbar-prompt"> <label class="gclitoolbar-prompt-label">»</label> </hbox> <hbox class="gclitoolbar-complete-node"/> <textbox class="gclitoolbar-input-node" rows="1"/> </stack> <toolbarbutton id="developer-toolbar-webconsole" label="&webConsoleButton.label;" - class="devtools-toolbarbutton" + class="developer-toolbar-button" command="Tools:WebConsole"/> <toolbarbutton id="developer-toolbar-inspector" label="&inspectorButton.label;" - class="devtools-toolbarbutton" + class="developer-toolbar-button" hidden="true" command="Tools:Inspect"/> <toolbarbutton id="developer-toolbar-styleeditor" label="&styleeditor.label;" - class="devtools-toolbarbutton" + class="developer-toolbar-button" hidden="true" command="Tools:StyleEditor"/> <toolbarbutton id="developer-toolbar-debugger" label="&debuggerMenu.label2;" - class="devtools-toolbarbutton" + class="developer-toolbar-button" hidden="true" command="Tools:Debugger"/> #ifndef XP_MACOSX <toolbarbutton id="developer-toolbar-closebutton" class="devtools-closebutton" oncommand="DeveloperToolbar.hide();" tooltiptext="&devToolbarCloseButton.tooltiptext;"/> #endif
--- a/browser/base/content/newtab/drag.js +++ b/browser/base/content/newtab/drag.js @@ -33,20 +33,23 @@ let gDrag = { * @param aSite The site that's being dragged. * @param aEvent The 'dragstart' event. */ start: function Drag_start(aSite, aEvent) { this._draggedSite = aSite; // Mark nodes as being dragged. let selector = ".newtab-site, .newtab-control, .newtab-thumbnail"; - let nodes = aSite.node.parentNode.querySelectorAll(selector); + let parentCell = aSite.node.parentNode; + let nodes = parentCell.querySelectorAll(selector); for (let i = 0; i < nodes.length; i++) nodes[i].setAttribute("dragged", "true"); + parentCell.setAttribute("dragged", "true"); + this._setDragData(aSite, aEvent); // Store the cursor offset. let node = aSite.node; let rect = node.getBoundingClientRect(); this._offsetX = aEvent.clientX - rect.left; this._offsetY = aEvent.clientY - rect.top; @@ -83,17 +86,17 @@ let gDrag = { }, /** * Ends the current drag operation. * @param aSite The site that's being dragged. * @param aEvent The 'dragend' event. */ end: function Drag_end(aSite, aEvent) { - let nodes = aSite.node.parentNode.querySelectorAll("[dragged]"); + let nodes = gGrid.node.querySelectorAll("[dragged]") for (let i = 0; i < nodes.length; i++) nodes[i].removeAttribute("dragged"); // Slide the dragged site back into its cell (may be the old or the new cell). gTransformation.slideSiteTo(aSite, aSite.cell, {unfreeze: true}); this._draggedSite = null; },
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -143,18 +143,19 @@ nsContextMenu.prototype = { }, initNavigationItems: function CM_initNavigationItems() { var shouldShow = !(this.isContentSelected || this.onLink || this.onImage || this.onCanvas || this.onVideo || this.onAudio || this.onTextInput); this.showItem("context-back", shouldShow); this.showItem("context-forward", shouldShow); - this.showItem("context-reload", shouldShow); - this.showItem("context-stop", shouldShow); + var shouldShowReload = XULBrowserWindow.stopCommand.getAttribute("disabled") == "true"; + this.showItem("context-reload", shouldShow && shouldShowReload); + this.showItem("context-stop", shouldShow && !shouldShowReload); this.showItem("context-sep-stop", shouldShow); // XXX: Stop is determined in browser.js; the canStop broadcaster is broken //this.setItemAttrFromNode( "context-stop", "disabled", "canStop" ); }, initLeaveDOMFullScreenItems: function CM_initLeaveFullScreenItem() { // only show the option if the user is in DOM fullscreen @@ -166,21 +167,19 @@ nsContextMenu.prototype = { this.showItem("context-media-sep-commands", true); }, initSaveItems: function CM_initSaveItems() { var shouldShow = !(this.onTextInput || this.onLink || this.isContentSelected || this.onImage || this.onCanvas || this.onVideo || this.onAudio); this.showItem("context-savepage", shouldShow); - this.showItem("context-sendpage", shouldShow); - // Save+Send link depends on whether we're in a link, or selected text matches valid URL pattern. + // Save link depends on whether we're in a link, or selected text matches valid URL pattern. this.showItem("context-savelink", this.onSaveableLink || this.onPlainTextLink); - this.showItem("context-sendlink", this.onSaveableLink || this.onPlainTextLink); // Save image depends on having loaded its content, video and audio don't. this.showItem("context-saveimage", this.onLoadedImage || this.onCanvas); this.showItem("context-savevideo", this.onVideo); this.showItem("context-saveaudio", this.onAudio); this.showItem("context-video-saveimage", this.onVideo); this.setItemAttr("context-savevideo", "disabled", !this.mediaURL); this.setItemAttr("context-saveaudio", "disabled", !this.mediaURL); @@ -1045,21 +1044,16 @@ nsContextMenu.prototype = { linkText = document.commandDispatcher.focusedWindow.getSelection().toString().trim(); else linkText = this.linkText(); urlSecurityCheck(this.linkURL, doc.nodePrincipal); this.saveHelper(this.linkURL, linkText, null, true, doc); }, - sendLink: function() { - // we don't know the title of the link so pass in an empty string - MailIntegration.sendMessage( this.linkURL, "" ); - }, - // Backwards-compatibility wrapper saveImage : function() { if (this.onCanvas || this.onImage) this.saveMedia(); }, // Save URL of the clicked upon image, video, or audio. saveMedia: function() { @@ -1398,20 +1392,16 @@ nsContextMenu.prototype = { }, window.top); } }, savePageAs: function CM_savePageAs() { saveDocument(this.browser.contentDocument); }, - sendPage: function CM_sendPage() { - MailIntegration.sendLinkForWindow(this.browser.contentWindow); - }, - printFrame: function CM_printFrame() { PrintUtils.print(this.target.ownerDocument.defaultView); }, switchPageDirection: function CM_switchPageDirection() { SwitchDocumentDirection(this.browser.contentWindow); },
--- a/browser/base/content/pageinfo/pageInfo.xul +++ b/browser/base/content/pageinfo/pageInfo.xul @@ -349,27 +349,32 @@ <radio id="geo#2" command="cmd_geoToggle" label="&permBlock;"/> </radiogroup> </hbox> </vbox> <vbox class="permission"> <label class="permissionLabel" id="permIndexedDBLabel" value="&permIndexedDB;" control="indexedDBRadioGroup"/> <hbox role="group" aria-labelledby="permIndexedDBLabel"> - <checkbox id="indexedDBDef" command="cmd_indexedDBDef" label="&permAskAlways;"/> + <checkbox id="indexedDBDef" command="cmd_indexedDBDef" label="&permUseDefault;"/> + <spacer flex="1"/> + <radiogroup id="indexedDBRadioGroup" orient="horizontal"> + <!-- Ask and Allow are purposefully reversed here! --> + <radio id="indexedDB#1" command="cmd_indexedDBToggle" label="&permAskAlways;"/> + <radio id="indexedDB#0" command="cmd_indexedDBToggle" label="&permAllow;"/> + <radio id="indexedDB#2" command="cmd_indexedDBToggle" label="&permBlock;"/> + </radiogroup> + </hbox> + <hbox> <spacer flex="1"/> <vbox pack="center"> - <label id="indexedDBStatus" control="indexedDBClear"/> + <label id="indexedDBStatus" control="indexedDBClear" hidden="true"/> </vbox> - <button id="indexedDBClear" label="&permClearStorage;" + <button id="indexedDBClear" label="&permClearStorage;" hidden="true" accesskey="&permClearStorage.accesskey;" onclick="onIndexedDBClear();"/> - <radiogroup id="indexedDBRadioGroup" orient="horizontal"> - <radio id="indexedDB#1" command="cmd_indexedDBToggle" label="&permAllow;"/> - <radio id="indexedDB#2" command="cmd_indexedDBToggle" label="&permBlock;"/> - </radiogroup> </hbox> </vbox> <vbox class="permission" id="permPluginsRow"> <label class="permissionLabel" id="permPluginsLabel" value="&permPlugins;" control="pluginsRadioGroup"/> <hbox role="group" aria-labelledby="permPluginsLabel"> <checkbox id="pluginsDef" command="cmd_pluginsDef" label="&permAskAlways;"/> <spacer flex="1"/>
--- a/browser/base/content/pageinfo/permissions.js +++ b/browser/base/content/pageinfo/permissions.js @@ -46,17 +46,17 @@ var gPermObj = { return BLOCK; }, geo: function getGeoDefaultPermissions() { return BLOCK; }, indexedDB: function getIndexedDBDefaultPermissions() { - return BLOCK; + return UNKNOWN; }, plugins: function getPluginsDefaultPermissions() { if (gPrefs.getBoolPref("plugins.click_to_play")) return BLOCK; return ALLOW; }, fullscreen: function getFullscreenDefaultPermissions() @@ -144,19 +144,16 @@ function onCheckboxClick(aPartId) { var permissionManager = Components.classes[PERMISSION_CONTRACTID] .getService(nsIPermissionManager); var command = document.getElementById("cmd_" + aPartId + "Toggle"); var checkbox = document.getElementById(aPartId + "Def"); if (checkbox.checked) { permissionManager.remove(gPermURI.host, aPartId); - if (aPartId == "indexedDB") { - permissionManager.remove(gPermURI.host, "indexedDB-unlimited"); - } command.setAttribute("disabled", "true"); var perm = gPermObj[aPartId](); setRadioState(aPartId, perm); } else { onRadioClick(aPartId); command.removeAttribute("disabled"); } @@ -166,17 +163,18 @@ function onRadioClick(aPartId) { var permissionManager = Components.classes[PERMISSION_CONTRACTID] .getService(nsIPermissionManager); var radioGroup = document.getElementById(aPartId + "RadioGroup"); var id = radioGroup.selectedItem.id; var permission = id.split('#')[1]; permissionManager.add(gPermURI, aPartId, permission); - if (aPartId == "indexedDB" && permission == BLOCK) { + if (aPartId == "indexedDB" && + (permission == ALLOW || permission == BLOCK)) { permissionManager.remove(gPermURI.host, "indexedDB-unlimited"); } if (aPartId == "fullscreen" && permission == UNKNOWN) { permissionManager.remove(gPermURI.host, "fullscreen"); } } function setRadioState(aPartId, aValue) @@ -202,17 +200,16 @@ function initIndexedDBRow() function onIndexedDBClear() { Components.classes["@mozilla.org/dom/indexeddb/manager;1"] .getService(nsIIndexedDatabaseManager) .clearDatabasesForURI(gPermURI); var permissionManager = Components.classes[PERMISSION_CONTRACTID] .getService(nsIPermissionManager); - permissionManager.remove(gPermURI.host, "indexedDB"); permissionManager.remove(gPermURI.host, "indexedDB-unlimited"); initIndexedDBRow(); } function onIndexedDBUsageCallback(uri, usage, fileUsage) { if (!uri.equals(gPermURI)) { throw new Error("Callback received for bad URI: " + uri);
--- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1408,20 +1408,19 @@ <![CDATA[ var tabsToClose = (aAll ? this.tabs.length - this._removingTabs.length : this.visibleTabs.length - 1) - gBrowser._numPinnedTabs; if (tabsToClose <= 1) return true; + var canDisablePrompt = !!aAll; const pref = "browser.tabs.warnOnClose"; - var shouldPrompt = Services.prefs.getBoolPref(pref); - - if (!shouldPrompt) + if (canDisablePrompt && !Services.prefs.getBoolPref(pref)) return true; var ps = Services.prompt; // default to true: if it were false, we wouldn't get this far var warnOnClose = { value: true }; var bundle = this.mStringBundle; @@ -1435,21 +1434,23 @@ ps.confirmEx(window, bundle.getString("tabs.closeWarningTitle"), bundle.getFormattedString("tabs.closeWarningMultipleTabs", [tabsToClose]), (ps.BUTTON_TITLE_IS_STRING * ps.BUTTON_POS_0) + (ps.BUTTON_TITLE_CANCEL * ps.BUTTON_POS_1), bundle.getString("tabs.closeButtonMultiple"), null, null, - bundle.getString('tabs.closeWarningPromptMe'), + canDisablePrompt ? + bundle.getString("tabs.closeWarningPromptMe") : null, warnOnClose); var reallyClose = (buttonPressed == 0); + // don't set the pref unless they press OK and it's false - if (reallyClose && !warnOnClose.value) + if (canDisablePrompt && reallyClose && !warnOnClose.value) Services.prefs.setBoolPref(pref, false); return reallyClose; ]]> </body> </method> <method name="removeAllTabsBut">
--- a/browser/base/content/test/Makefile.in +++ b/browser/base/content/test/Makefile.in @@ -148,16 +148,17 @@ endif browser_bug655584.js \ browser_bug664672.js \ browser_bug710878.js \ browser_bug719271.js \ browser_bug735471.js \ browser_bug743421.js \ browser_bug749738.js \ browser_bug763468.js \ + browser_bug767836.js \ browser_canonizeURL.js \ browser_customize.js \ browser_findbarClose.js \ browser_homeDrop.js \ browser_keywordBookmarklets.js \ browser_contextSearchTabPosition.js \ browser_ctrlTab.js \ browser_customize_popupNotification.js \
--- a/browser/base/content/test/browser_bug477014.js +++ b/browser/base/content/test/browser_bug477014.js @@ -6,22 +6,22 @@ const iconURLSpec = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="; var testPage="data:text/plain,test bug 477014"; function test() { waitForExplicitFinish(); var newWindow; var tabToDetach; + var documentToDetach; function onPageShow(event) { // we get here if the test is executed before the pageshow // event for the window's first tab - if (!tabToDetach || - tabToDetach.linkedBrowser.contentDocument != event.target) + if (!tabToDetach || documentToDetach != event.target) return; event.currentTarget.removeEventListener("pageshow", onPageShow, false); if (!newWindow) { // prepare the tab (set icon and busy state) // we have to set these only after onState* notification, otherwise // they're overriden @@ -41,11 +41,15 @@ function test() { } is(newWindow.gBrowser.selectedTab.hasAttribute("busy"), true); is(newWindow.gBrowser.getIcon(), iconURLSpec); newWindow.close(); finish(); } - gBrowser.addEventListener("pageshow", onPageShow, false); tabToDetach = gBrowser.addTab(testPage); + tabToDetach.linkedBrowser.addEventListener("load", function onLoad() { + tabToDetach.linkedBrowser.removeEventListener("load", onLoad, true); + documentToDetach = tabToDetach.linkedBrowser.contentDocument; + gBrowser.addEventListener("pageshow", onPageShow, false); + }, true); }
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/browser_bug767836.js @@ -0,0 +1,93 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// initialization +const pb = Cc["@mozilla.org/privatebrowsing;1"]. + getService(Ci.nsIPrivateBrowsingService); +const PREF = "browser.newtab.url"; +const NEWTABURL = Services.prefs.getCharPref(PREF) || "about:blank"; +const TESTURL = "http://example.com/"; + +function test() { + + waitForExplicitFinish(); + // check whether the mode that we start off with is normal or not + ok(!pb.privateBrowsingEnabled, "private browsing is disabled"); + // check whether any custom new tab url has been configured + ok(!Services.prefs.prefHasUserValue(PREF), "No custom newtab url is set"); + + openNewTab(function () { + // Check the new tab opened while in normal mode + is(gBrowser.selectedBrowser.currentURI.spec, NEWTABURL, + "URL of NewTab should be browser.newtab.url in Normal mode"); + // Set the custom newtab url + Services.prefs.setCharPref(PREF, TESTURL); + ok(Services.prefs.prefHasUserValue(PREF), "Custom newtab url is set"); + + // Open a newtab after setting the custom newtab url + openNewTab(function () { + is(gBrowser.selectedBrowser.currentURI.spec, TESTURL, + "URL of NewTab should be the custom url"); + + // clear the custom url preference + Services.prefs.clearUserPref(PREF); + ok(!Services.prefs.prefHasUserValue(PREF), "No custom newtab url is set"); + + // enter private browsing mode + togglePrivateBrowsing(function () { + ok(pb.privateBrowsingEnabled, "private browsing is enabled"); + + // Open a new tab page in private browsing mode + openNewTab(function () { + // Check the new tab opened while in private browsing mode + is(gBrowser.selectedBrowser.currentURI.spec, "about:privatebrowsing", + "URL of NewTab should be about:privatebrowsing in PB mode"); + + Services.prefs.setCharPref(PREF, TESTURL); + ok(Services.prefs.prefHasUserValue(PREF), "Custom newtab url is set"); + + // Open a newtab after setting the custom newtab url + openNewTab(function () { + is(gBrowser.selectedBrowser.currentURI.spec, TESTURL, + "URL of NewTab should be the custom url"); + + Services.prefs.clearUserPref(PREF); + ok(!Services.prefs.prefHasUserValue(PREF), "No custom newtab url is set"); + + // exit private browsing mode + togglePrivateBrowsing(function () { + ok(!pb.privateBrowsingEnabled, "private browsing is disabled"); + + gBrowser.removeTab(gBrowser.selectedTab); + gBrowser.removeTab(gBrowser.selectedTab); + finish(); + }); + }); + }); + }); + }); + }); +} + +function togglePrivateBrowsing(aCallback) { + let topic = "private-browsing-transition-complete"; + + Services.obs.addObserver(function observe() { + Services.obs.removeObserver(observe, topic); + executeSoon(aCallback); + }, topic, false); + + pb.privateBrowsingEnabled = !pb.privateBrowsingEnabled; +} + +function openNewTab(aCallback) { + // Open a new tab + BrowserOpenTab(); + + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function onLoad() { + browser.removeEventListener("load", onLoad, true); + executeSoon(aCallback); + }, true); +}
--- a/browser/base/content/test/newtab/Makefile.in +++ b/browser/base/content/test/newtab/Makefile.in @@ -11,16 +11,17 @@ relativesrcdir = browser/base/content/t include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/rules.mk _BROWSER_FILES = \ browser_newtab_block.js \ browser_newtab_disable.js \ browser_newtab_drag_drop.js \ browser_newtab_drop_preview.js \ + browser_newtab_focus.js \ browser_newtab_private_browsing.js \ browser_newtab_reset.js \ browser_newtab_tabsync.js \ browser_newtab_unpin.js \ browser_newtab_bug721442.js \ browser_newtab_bug722273.js \ browser_newtab_bug723102.js \ browser_newtab_bug723121.js \
--- a/browser/base/content/test/newtab/browser_newtab_block.js +++ b/browser/base/content/test/newtab/browser_newtab_block.js @@ -4,58 +4,58 @@ /* * These tests make sure that blocking/removing sites from the grid works * as expected. Pinned tabs should not be moved. Gaps will be re-filled * if more sites are available. */ function runTests() { // we remove sites and expect the gaps to be filled as long as there still // are some sites available - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); yield blockCell(4); checkGrid("0,1,2,3,5,6,7,8,9"); yield blockCell(4); checkGrid("0,1,2,3,6,7,8,9,"); yield blockCell(4); checkGrid("0,1,2,3,7,8,9,,"); // we removed a pinned site yield restore(); - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",1"); yield addNewTabPageTab(); checkGrid("0,1p,2,3,4,5,6,7,8"); yield blockCell(1); checkGrid("0,2,3,4,5,6,7,8,"); // we remove the last site on the grid (which is pinned) and expect the gap // to be re-filled and the new site to be unpinned yield restore(); - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); setPinnedLinks(",,,,,,,,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8p"); yield blockCell(8); checkGrid("0,1,2,3,4,5,6,7,9"); // we remove the first site on the grid with the last one pinned. all cells // but the last one should shift to the left and a new site fades in yield restore(); - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); setPinnedLinks(",,,,,,,,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8p"); yield blockCell(0); checkGrid("1,2,3,4,5,6,7,9,8p"); }
--- a/browser/base/content/test/newtab/browser_newtab_bug721442.js +++ b/browser/base/content/test/newtab/browser_newtab_bug721442.js @@ -1,23 +1,23 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { - setLinks("0,1,2,3,4,5,6,7,8"); - NewTabUtils.pinnedLinks._links = [ - {url: "about:blank#7", title: ""}, - {url: "about:blank#8", title: "title"}, - {url: "about:blank#9", title: "about:blank#9"} - ]; + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks([ + {url: "http://example.com/#7", title: ""}, + {url: "http://example.com/#8", title: "title"}, + {url: "http://example.com/#9", title: "http://example.com/#9"} + ]); yield addNewTabPageTab(); checkGrid("7p,8p,9p,0,1,2,3,4,5"); - checkTooltip(0, "about:blank#7", "1st tooltip is correct"); - checkTooltip(1, "title\nabout:blank#8", "2nd tooltip is correct"); - checkTooltip(2, "about:blank#9", "3rd tooltip is correct"); + checkTooltip(0, "http://example.com/#7", "1st tooltip is correct"); + checkTooltip(1, "title\nhttp://example.com/#8", "2nd tooltip is correct"); + checkTooltip(2, "http://example.com/#9", "3rd tooltip is correct"); } function checkTooltip(aIndex, aExpected, aMessage) { let link = getCell(aIndex).node.querySelector(".newtab-link"); is(link.getAttribute("title"), aExpected, aMessage); }
--- a/browser/base/content/test/newtab/browser_newtab_bug722273.js +++ b/browser/base/content/test/newtab/browser_newtab_bug722273.js @@ -1,59 +1,60 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ const NOW = Date.now() * 1000; const URL = "http://fake-site.com/"; let tmp = {}; -Cu.import("resource:///modules/NewTabUtils.jsm", tmp); Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) .loadSubScript("chrome://browser/content/sanitize.js", tmp); -let {NewTabUtils, Sanitizer} = tmp; - -let bhist = Cc["@mozilla.org/browser/global-history;2"] - .getService(Ci.nsIBrowserHistory); +let {Sanitizer} = tmp; function runTests() { - clearHistory(); - yield fillHistory(); + sanitizeHistory(); + yield addFakeVisits(); yield addNewTabPageTab(); is(getCell(0).site.url, URL, "first site is our fake site"); whenPagesUpdated(); - yield clearHistory(); + yield sanitizeHistory(); ok(!getCell(0).site, "the fake site is gone"); } -function fillHistory() { +function addFakeVisits() { let visits = []; for (let i = 59; i > 0; i--) { visits.push({ visitDate: NOW - i * 60 * 1000000, transitionType: Ci.nsINavHistoryService.TRANSITION_LINK }); } let place = { uri: makeURI(URL), title: "fake site", visits: visits }; PlacesUtils.asyncHistory.updatePlaces(place, { - handleError: function () do_throw("Unexpected error in adding visit."), - handleResult: function () { }, - handleCompletion: function () TestRunner.next() + handleError: function () ok(false, "couldn't add visit"), + handleResult: function () {}, + handleCompletion: function () { + NewTabUtils.links.populateCache(function () { + NewTabUtils.allPages.update(); + TestRunner.next(); + }, true); + } }); } -function clearHistory() { +function sanitizeHistory() { let s = new Sanitizer(); s.prefDomain = "privacy.cpd."; let prefs = gPrefService.getBranch(s.prefDomain); prefs.setBoolPref("history", true); prefs.setBoolPref("downloads", false); prefs.setBoolPref("cache", false); prefs.setBoolPref("cookies", false);
--- a/browser/base/content/test/newtab/browser_newtab_bug723102.js +++ b/browser/base/content/test/newtab/browser_newtab_bug723102.js @@ -1,18 +1,18 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { // create a new tab page and hide it. - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); let firstTab = gBrowser.selectedTab; yield addNewTabPageTab(); gBrowser.removeTab(firstTab); - ok(NewTabUtils.allPages.enabled, true, "page is enabled"); + ok(NewTabUtils.allPages.enabled, "page is enabled"); NewTabUtils.allPages.enabled = false; ok(getGrid().node.hasAttribute("page-disabled"), "page is disabled"); }
--- a/browser/base/content/test/newtab/browser_newtab_bug723121.js +++ b/browser/base/content/test/newtab/browser_newtab_bug723121.js @@ -1,13 +1,13 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGridLocked(false, "grid is unlocked"); let cell = getCell(0).node; let site = getCell(0).site.node; let link = site.querySelector(".newtab-link");
--- a/browser/base/content/test/newtab/browser_newtab_bug725996.js +++ b/browser/base/content/test/newtab/browser_newtab_bug725996.js @@ -1,23 +1,23 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); let cell = getCell(0).node; - sendDragEvent("drop", cell, "about:blank#99\nblank"); - is(NewTabUtils.pinnedLinks.links[0].url, "about:blank#99", + sendDragEvent("drop", cell, "http://example.com/#99\nblank"); + is(NewTabUtils.pinnedLinks.links[0].url, "http://example.com/#99", "first cell is pinned and contains the dropped site"); yield whenPagesUpdated(); checkGrid("99p,0,1,2,3,4,5,6,7"); sendDragEvent("drop", cell, ""); - is(NewTabUtils.pinnedLinks.links[0].url, "about:blank#99", + is(NewTabUtils.pinnedLinks.links[0].url, "http://example.com/#99", "first cell is still pinned with the site we dropped before"); }
--- a/browser/base/content/test/newtab/browser_newtab_bug734043.js +++ b/browser/base/content/test/newtab/browser_newtab_bug734043.js @@ -1,13 +1,13 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); let receivedError = false; let block = getContentDocument().querySelector(".newtab-control-block");
--- a/browser/base/content/test/newtab/browser_newtab_bug735987.js +++ b/browser/base/content/test/newtab/browser_newtab_bug735987.js @@ -1,13 +1,13 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ function runTests() { - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); yield simulateDrop(1); checkGrid("0,99p,1,2,3,4,5,6,7");
--- a/browser/base/content/test/newtab/browser_newtab_disable.js +++ b/browser/base/content/test/newtab/browser_newtab_disable.js @@ -2,17 +2,17 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ /* * These tests make sure that the 'New Tab Page' feature can be disabled if the * decides not to use it. */ function runTests() { // create a new tab page and hide it. - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); let gridNode = getGrid().node; ok(!gridNode.hasAttribute("page-disabled"), "page is not disabled"); NewTabUtils.allPages.enabled = false;
--- a/browser/base/content/test/newtab/browser_newtab_drag_drop.js +++ b/browser/base/content/test/newtab/browser_newtab_drag_drop.js @@ -4,112 +4,112 @@ /* * These tests make sure that dragging and dropping sites works as expected. * Sites contained in the grid need to shift around to indicate the result * of the drag-and-drop operation. If the grid is full and we're dragging * a new site into it another one gets pushed out. */ function runTests() { // test a simple drag-and-drop scenario - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); yield simulateDrop(1, 0); checkGrid("1,0p,2,3,4,5,6,7,8"); // drag a cell to its current cell and make sure it's not pinned afterwards - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8"); yield simulateDrop(0, 0); checkGrid("0,1,2,3,4,5,6,7,8"); // ensure that pinned pages aren't moved if that's not necessary - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",1,2"); yield addNewTabPageTab(); checkGrid("0,1p,2p,3,4,5,6,7,8"); yield simulateDrop(3, 0); checkGrid("3,1p,2p,0p,4,5,6,7,8"); // pinned sites should always be moved around as blocks. if a pinned site is // moved around, neighboring pinned are affected as well - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1"); yield addNewTabPageTab(); checkGrid("0p,1p,2,3,4,5,6,7,8"); yield simulateDrop(0, 2); checkGrid("2p,0p,1p,3,4,5,6,7,8"); // pinned sites should not be pushed out of the grid (unless there are only // pinned ones left on the grid) - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); yield simulateDrop(8, 2); checkGrid("0,1,3,4,5,6,7p,8p,2p"); // make sure that pinned sites are re-positioned correctly - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1,2,,,5"); yield addNewTabPageTab(); checkGrid("0p,1p,2p,3,4,5p,6,7,8"); yield simulateDrop(4, 0); checkGrid("3,1p,2p,4,0p,5p,6,7,8"); // drag a new site onto the very first cell - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); yield simulateDrop(0); checkGrid("99p,0,1,2,3,4,5,7p,8p"); // drag a new site onto the grid and make sure that pinned cells don't get // pushed out - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,7,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7p,8p"); yield simulateDrop(7); checkGrid("0,1,2,3,4,5,7p,99p,8p"); // drag a new site beneath a pinned cell and make sure the pinned cell is // not moved - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",,,,,,,,8"); yield addNewTabPageTab(); checkGrid("0,1,2,3,4,5,6,7,8p"); yield simulateDrop(7); checkGrid("0,1,2,3,4,5,6,99p,8p"); // drag a new site onto a block of pinned sites and make sure they're shifted // around accordingly - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1,2,,,,,,"); yield addNewTabPageTab(); checkGrid("0p,1p,2p"); yield simulateDrop(1); checkGrid("0p,99p,1p,2p,3,4,5,6,7"); }
--- a/browser/base/content/test/newtab/browser_newtab_drop_preview.js +++ b/browser/base/content/test/newtab/browser_newtab_drop_preview.js @@ -2,17 +2,17 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ /* * These tests ensure that the drop preview correctly arranges sites when * dragging them around. */ function runTests() { // the first three sites are pinned - make sure they're re-arranged correctly - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks("0,1,2,,,5"); yield addNewTabPageTab(); checkGrid("0p,1p,2p,3,4,5p,6,7,8"); let cw = getContentWindow(); cw.gDrag._draggedSite = getCell(0).site; let sites = cw.gDropPreview.rearrange(getCell(4));
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/newtab/browser_newtab_focus.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* + * These tests make sure that focusing the 'New Tage Page' works as expected. + */ +function runTests() { + // Focus count in new tab page. + // 28 = 9 * 3 + 1 = 9 sites and 1 toggle button, each site has a link, a pin + // and a remove button. + let FOCUS_COUNT = 28; + if ("nsILocalFileMac" in Ci) { + // 19 = Mac doesn't focus links, so 9 focus targets less than Windows/Linux. + FOCUS_COUNT = 19; + } + + // Create a new tab page. + yield setLinks("0,1,2,3,4,5,6,7,8"); + setPinnedLinks(""); + + yield addNewTabPageTab(); + gURLBar.focus(); + + // Count the focus with the enabled page. + yield countFocus(FOCUS_COUNT); + + // Disable page and count the focus with the disabled page. + NewTabUtils.allPages.enabled = false; + yield countFocus(1); +} + +/** + * Focus the urlbar and count how many focus stops to return again to the urlbar. + */ +function countFocus(aExpectedCount) { + let focusCount = 0; + let contentDoc = getContentDocument(); + + window.addEventListener("focus", function onFocus() { + let focusedElement = document.commandDispatcher.focusedElement; + if (focusedElement && focusedElement.classList.contains("urlbar-input")) { + window.removeEventListener("focus", onFocus, true); + is(focusCount, aExpectedCount, "Validate focus count in the new tab page."); + executeSoon(TestRunner.next); + } else { + if (focusedElement && focusedElement.ownerDocument == contentDoc && + focusedElement instanceof HTMLElement) { + focusCount++; + } + document.commandDispatcher.advanceFocus(); + } + }, true); + + document.commandDispatcher.advanceFocus(); +}
--- a/browser/base/content/test/newtab/browser_newtab_private_browsing.js +++ b/browser/base/content/test/newtab/browser_newtab_private_browsing.js @@ -7,17 +7,17 @@ * The private browsing mode should start with the current grid shown in normal * mode. */ let pb = Cc["@mozilla.org/privatebrowsing;1"] .getService(Ci.nsIPrivateBrowsingService); function runTests() { // prepare the grid - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); ok(!pb.privateBrowsingEnabled, "private browsing is disabled"); yield addNewTabPageTab(); pinCell(0); checkGrid("0p,1,2,3,4,5,6,7,8"); // enter private browsing mode yield togglePrivateBrowsing();
--- a/browser/base/content/test/newtab/browser_newtab_reset.js +++ b/browser/base/content/test/newtab/browser_newtab_reset.js @@ -4,17 +4,17 @@ /* * These tests make sure that resetting the 'New Tage Page' works as expected. */ function runTests() { // Disabled until bug 716543 is fixed. return; // create a new tab page and check its modified state after blocking a site - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(""); yield addNewTabPageTab(); let resetButton = getContentDocument().getElementById("toolbar-button-reset"); checkGrid("0,1,2,3,4,5,6,7,8"); ok(!resetButton.hasAttribute("modified"), "page is not modified");
--- a/browser/base/content/test/newtab/browser_newtab_tabsync.js +++ b/browser/base/content/test/newtab/browser_newtab_tabsync.js @@ -6,17 +6,17 @@ * 'New Tab Page' are synchronized with all other open 'New Tab Pages' * automatically. All about:newtab pages should always be in the same * state. */ function runTests() { // Disabled until bug 716543 is fixed. return; - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); setPinnedLinks(",1"); yield addNewTabPageTab(); checkGrid("0,1p,2,3,4,5,6,7,8"); let resetButton = getContentDocument().getElementById("toolbar-button-reset"); ok(!resetButton.hasAttribute("modified"), "page is not modified");
--- a/browser/base/content/test/newtab/browser_newtab_unpin.js +++ b/browser/base/content/test/newtab/browser_newtab_unpin.js @@ -3,54 +3,54 @@ /* * These tests make sure that when a site gets unpinned it is either moved to * its actual place in the grid or removed in case it's not on the grid anymore. */ function runTests() { // we have a pinned link that didn't change its position since it was pinned. // nothing should happend when we unpin it. - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",1"); yield addNewTabPageTab(); checkGrid("0,1p,2,3,4,5,6,7,8"); yield unpinCell(1); checkGrid("0,1,2,3,4,5,6,7,8"); // we have a pinned link that is not anymore in the list of the most-visited // links. this should disappear, the remaining links adjust their positions // and a new link will appear at the end of the grid. - setLinks("0,1,2,3,4,5,6,7,8"); + yield setLinks("0,1,2,3,4,5,6,7,8"); setPinnedLinks(",99"); yield addNewTabPageTab(); checkGrid("0,99p,1,2,3,4,5,6,7"); yield unpinCell(1); checkGrid("0,1,2,3,4,5,6,7,8"); // we have a pinned link that changed its position since it was pinned. it // should be moved to its new position after being unpinned. - setLinks("0,1,2,3,4,5,6,7"); + yield setLinks("0,1,2,3,4,5,6,7"); setPinnedLinks(",1,,,,,,,0"); yield addNewTabPageTab(); checkGrid("2,1p,3,4,5,6,7,,0p"); yield unpinCell(1); checkGrid("1,2,3,4,5,6,7,,0p"); yield unpinCell(8); checkGrid("0,1,2,3,4,5,6,7,"); // we have pinned link that changed its position since it was pinned. the // link will disappear from the grid because it's now a much lower priority - setLinks("0,1,2,3,4,5,6,7,8,9"); + yield setLinks("0,1,2,3,4,5,6,7,8,9"); setPinnedLinks("9"); yield addNewTabPageTab(); checkGrid("9p,0,1,2,3,4,5,6,7"); yield unpinCell(0); checkGrid("0,1,2,3,4,5,6,7,8"); }
--- a/browser/base/content/test/newtab/head.js +++ b/browser/base/content/test/newtab/head.js @@ -2,31 +2,36 @@ http://creativecommons.org/publicdomain/zero/1.0/ */ const PREF_NEWTAB_ENABLED = "browser.newtabpage.enabled"; Services.prefs.setBoolPref(PREF_NEWTAB_ENABLED, true); let tmp = {}; Cu.import("resource:///modules/NewTabUtils.jsm", tmp); -let NewTabUtils = tmp.NewTabUtils; +Cc["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Ci.mozIJSSubScriptLoader) + .loadSubScript("chrome://browser/content/sanitize.js", tmp); + +let {NewTabUtils, Sanitizer} = tmp; + +let uri = Services.io.newURI("about:newtab", null, null); +let principal = Services.scriptSecurityManager.getCodebasePrincipal(uri); + +let sm = Services.domStorageManager; +let storage = sm.getLocalStorageForPrincipal(principal, ""); registerCleanupFunction(function () { while (gBrowser.tabs.length > 1) gBrowser.removeTab(gBrowser.tabs[1]); Services.prefs.clearUserPref(PREF_NEWTAB_ENABLED); }); /** - * We'll want to restore the original links provider later. - */ -let originalProvider = NewTabUtils.links._provider; - -/** * Provide the default test function to start our test runner. */ function test() { TestRunner.run(); } /** * The test runner that controls the execution flow of our tests. @@ -53,19 +58,17 @@ let TestRunner = { } }, /** * Finishes all tests and cleans up. */ finish: function () { function cleanupAndFinish() { - // Restore the old provider. - NewTabUtils.links._provider = originalProvider; - + clearHistory(); whenPagesUpdated(finish); NewTabUtils.restore(); } let callbacks = NewTabUtils.links._populateCallbacks; let numCallbacks = callbacks.length; if (numCallbacks) @@ -108,52 +111,86 @@ function getCell(aIndex) { return getGrid().cells[aIndex]; } /** * Allows to provide a list of links that is used to construct the grid. * @param aLinksPattern the pattern (see below) * * Example: setLinks("1,2,3") - * Result: [{url: "about:blank#1", title: "site#1"}, - * {url: "about:blank#2", title: "site#2"} - * {url: "about:blank#3", title: "site#3"}] + * Result: [{url: "http://example.com/#1", title: "site#1"}, + * {url: "http://example.com/#2", title: "site#2"} + * {url: "http://example.com/#3", title: "site#3"}] */ -function setLinks(aLinksPattern) { - let links = aLinksPattern.split(/\s*,\s*/).map(function (id) { - return {url: "about:blank#" + id, title: "site#" + id}; +function setLinks(aLinks) { + let links = aLinks; + + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + return {url: "http://example.com/#" + id, title: "site#" + id}; + }); + } + + clearHistory(); + fillHistory(links, function () { + NewTabUtils.links.populateCache(function () { + NewTabUtils.allPages.update(); + TestRunner.next(); + }, true); }); +} - NewTabUtils.links._provider = {getLinks: function (c) c(links)}; - NewTabUtils.links._links = links; +function clearHistory() { + PlacesUtils.history.removeAllPages(); +} + +function fillHistory(aLinks, aCallback) { + let numLinks = aLinks.length; + let transitionLink = Ci.nsINavHistoryService.TRANSITION_LINK; + + for (let link of aLinks.reverse()) { + let place = { + uri: makeURI(link.url), + title: link.title, + visits: [{visitDate: Date.now() * 1000, transitionType: transitionLink}] + }; + + PlacesUtils.asyncHistory.updatePlaces(place, { + handleError: function () ok(false, "couldn't add visit to history"), + handleResult: function () {}, + handleCompletion: function () { + if (--numLinks == 0) + aCallback(); + } + }); + } } /** * Allows to specify the list of pinned links (that have a fixed position in * the grid. * @param aLinksPattern the pattern (see below) * * Example: setPinnedLinks("3,,1") - * Result: 'about:blank#3' is pinned in the first cell. 'about:blank#1' is + * Result: 'http://example.com/#3' is pinned in the first cell. 'http://example.com/#1' is * pinned in the third cell. */ -function setPinnedLinks(aLinksPattern) { - let pinnedLinks = []; - - aLinksPattern.split(/\s*,\s*/).forEach(function (id, index) { - let link; +function setPinnedLinks(aLinks) { + let links = aLinks; - if (id) - link = {url: "about:blank#" + id, title: "site#" + id}; + if (typeof links == "string") { + links = aLinks.split(/\s*,\s*/).map(function (id) { + if (id) + return {url: "http://example.com/#" + id, title: "site#" + id}; + }); + } - pinnedLinks[index] = link; - }); - - // Inject the list of pinned links to work with. - NewTabUtils.pinnedLinks._links = pinnedLinks; + storage.setItem("pinnedLinks", JSON.stringify(links)); + NewTabUtils.pinnedLinks.resetCache(); + NewTabUtils.allPages.update(); } /** * Restore the grid state. */ function restore() { whenPagesUpdated(); NewTabUtils.restore(); @@ -182,35 +219,35 @@ function addNewTabPageTab() { } /** * Compares the current grid arrangement with the given pattern. * @param the pattern (see below) * @param the array of sites to compare with (optional) * * Example: checkGrid("3p,2,,1p") - * Result: We expect the first cell to contain the pinned site 'about:blank#3'. - * The second cell contains 'about:blank#2'. The third cell is empty. - * The fourth cell contains the pinned site 'about:blank#4'. + * Result: We expect the first cell to contain the pinned site 'http://example.com/#3'. + * The second cell contains 'http://example.com/#2'. The third cell is empty. + * The fourth cell contains the pinned site 'http://example.com/#4'. */ function checkGrid(aSitesPattern, aSites) { let length = aSitesPattern.split(",").length; let sites = (aSites || getGrid().sites).slice(0, length); let current = sites.map(function (aSite) { if (!aSite) return ""; let pinned = aSite.isPinned(); let pinButton = aSite.node.querySelector(".newtab-control-pin"); let hasPinnedAttr = pinButton.hasAttribute("pinned"); if (pinned != hasPinnedAttr) ok(false, "invalid state (site.isPinned() != site[pinned])"); - return aSite.url.replace(/^about:blank#(\d+)$/, "$1") + (pinned ? "p" : ""); + return aSite.url.replace(/^http:\/\/example\.com\/#(\d+)$/, "$1") + (pinned ? "p" : ""); }); is(current, aSitesPattern, "grid status = " + aSitesPattern); } /** * Blocks a site from the grid. * @param aIndex The cell index. @@ -241,17 +278,17 @@ function unpinCell(aIndex) { /** * Simulates a drop and drop operation. * @param aDropIndex The cell index of the drop target. * @param aDragIndex The cell index containing the dragged site (optional). */ function simulateDrop(aDropIndex, aDragIndex) { let draggedSite; let {gDrag: drag, gDrop: drop} = getContentWindow(); - let event = createDragEvent("drop", "about:blank#99\nblank"); + let event = createDragEvent("drop", "http://example.com/#99\nblank"); if (typeof aDragIndex != "undefined") draggedSite = getCell(aDragIndex).site; if (draggedSite) drag.start(draggedSite, event); whenPagesUpdated();
--- a/browser/base/content/test/test_contextmenu.html +++ b/browser/base/content/test/test_contextmenu.html @@ -267,21 +267,19 @@ function runTest(testNum) { openContextMenuFor(text); break; case 2: // Context menu for plain text plainTextItems = ["context-back", false, "context-forward", false, "context-reload", true, - "context-stop", false, "---", null, "context-bookmarkpage", true, "context-savepage", true, - "context-sendpage", true, "---", null, "context-viewbgimage", false, "context-selectall", true, "---", null, "context-viewsource", true, "context-viewinfo", true ].concat(inspectItems); checkContextMenu(plainTextItems); @@ -291,17 +289,16 @@ function runTest(testNum) { case 3: // Context menu for text link checkContextMenu(["context-openlinkintab", true, "context-openlink", true, "---", null, "context-bookmarklink", true, "context-savelink", true, - "context-sendlink", true, "context-copylink", true ].concat(inspectItems)); closeContextMenu(); openContextMenuFor(mailto); // Invoke context menu for next test. break; case 4: // Context menu for text mailto-link @@ -425,21 +422,19 @@ function runTest(testNum) { openContextMenuFor(iframe); // Invoke context menu for next test. break; case 12: // Context menu for an iframe checkContextMenu(["context-back", false, "context-forward", false, "context-reload", true, - "context-stop", false, "---", null, "context-bookmarkpage", true, "context-savepage", true, - "context-sendpage", true, "---", null, "context-viewbgimage", false, "context-selectall", true, "frame", null, ["context-showonlythisframe", true, "context-openframeintab", true, "context-openframe", true, "---", null, @@ -651,21 +646,19 @@ function runTest(testNum) { "+Radio2", {type: "checkbox", icon: "", checked: true, disabled: false}, "+Radio3", {type: "checkbox", icon: "", checked: false, disabled: false}, "---", null, "+Checkbox", {type: "checkbox", icon: "", checked: false, disabled: false}], null, "---", null, "context-back", false, "context-forward", false, "context-reload", true, - "context-stop", false, "---", null, "context-bookmarkpage", true, "context-savepage", true, - "context-sendpage", true, "---", null, "context-viewbgimage", false, "context-selectall", true, "---", null, "context-viewsource", true, "context-viewinfo", true ].concat(inspectItems)); @@ -686,21 +679,19 @@ function runTest(testNum) { case 22: // Context menu for DOM Fullscreen mode (NOTE: this is *NOT* on an img) checkContextMenu(["context-leave-dom-fullscreen", true, "---", null, "context-back", false, "context-forward", false, "context-reload", true, - "context-stop", false, "---", null, "context-bookmarkpage", true, "context-savepage", true, - "context-sendpage", true, "---", null, "context-viewbgimage", false, "context-selectall", true, "---", null, "context-viewsource", true, "context-viewinfo", true ].concat(inspectItems)); closeContextMenu(); @@ -716,21 +707,19 @@ function runTest(testNum) { break; case 23: // Context menu for element with assigned content context menu // The shift key should bypass content context menu processing checkContextMenu(["context-back", false, "context-forward", false, "context-reload", true, - "context-stop", false, "---", null, "context-bookmarkpage", true, "context-savepage", true, - "context-sendpage", true, "---", null, "context-viewbgimage", false, "context-selectall", true, "---", null, "context-viewsource", true, "context-viewinfo", true ].concat(inspectItems)); closeContextMenu(); @@ -759,17 +748,16 @@ function runTest(testNum) { if (Services.appinfo.OS == "Darwin") { // This test is only enabled on Mac due to bug 736399. checkContextMenu(["context-openlinkincurrent", true, "context-openlinkintab", true, "context-openlink", true, "---", null, "context-bookmarklink", true, "context-savelink", true, - "context-sendlink", true, "context-copy", true, "context-selectall", true, "---", null, "context-searchselect", true, "context-viewpartialsource-selection", true ].concat(inspectItems)); } closeContextMenu();
--- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -1,26 +1,33 @@ -# -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- +# -*- Mode: javascript; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- # This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. // Services = object with smart getters for common XPCOM services Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "BROWSER_NEW_TAB_URL", function () { const PREF = "browser.newtab.url"; const TOPIC = "private-browsing-transition-complete"; function getNewTabPageURL() { - if (("gPrivateBrowsingUI" in window) && gPrivateBrowsingUI.privateWindow) - return "about:privatebrowsing"; - else - return Services.prefs.getCharPref(PREF) || "about:blank"; + if (("gPrivateBrowsingUI" in window) && + !Services.prefs.prefHasUserValue(PREF)) { + // gPrivateBrowsingUI may not be initialized yet, in that case we'll + // update BROWSER_NEW_TAB_URL when it gets initialized. + if (!gPrivateBrowsingUI.initialized) + gPrivateBrowsingUI.addInitializationCallback(update); + else if (gPrivateBrowsingUI.privateWindow && + !gPrivateBrowsingUI.autoStarted) + return "about:privatebrowsing"; + } + return Services.prefs.getCharPref(PREF) || "about:blank"; } function update() { BROWSER_NEW_TAB_URL = getNewTabPageURL(); } Services.prefs.addObserver(PREF, update, false); Services.obs.addObserver(update, TOPIC, false);
--- a/browser/components/dirprovider/DirectoryProvider.h +++ b/browser/components/dirprovider/DirectoryProvider.h @@ -4,32 +4,33 @@ #ifndef DirectoryProvider_h__ #define DirectoryProvider_h__ #include "nsIDirectoryService.h" #include "nsComponentManagerUtils.h" #include "nsISimpleEnumerator.h" #include "nsIFile.h" +#include "mozilla/Attributes.h" #define NS_BROWSERDIRECTORYPROVIDER_CONTRACTID \ "@mozilla.org/browser/directory-provider;1" namespace mozilla { namespace browser { -class DirectoryProvider : public nsIDirectoryServiceProvider2 +class DirectoryProvider MOZ_FINAL : public nsIDirectoryServiceProvider2 { public: NS_DECL_ISUPPORTS NS_DECL_NSIDIRECTORYSERVICEPROVIDER NS_DECL_NSIDIRECTORYSERVICEPROVIDER2 private: - class AppendingEnumerator : public nsISimpleEnumerator + class AppendingEnumerator MOZ_FINAL : public nsISimpleEnumerator { public: NS_DECL_ISUPPORTS NS_DECL_NSISIMPLEENUMERATOR AppendingEnumerator(nsISimpleEnumerator* aBase, char const *const *aAppendList);
--- a/browser/components/feeds/src/FeedWriter.js +++ b/browser/components/feeds/src/FeedWriter.js @@ -46,17 +46,17 @@ function makeURI(aURLSpec, aCharset) { const XML_NS = "http://www.w3.org/XML/1998/namespace" const HTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; const TYPE_MAYBE_FEED = "application/vnd.mozilla.maybe.feed"; const TYPE_MAYBE_AUDIO_FEED = "application/vnd.mozilla.maybe.audio.feed"; const TYPE_MAYBE_VIDEO_FEED = "application/vnd.mozilla.maybe.video.feed"; const URI_BUNDLE = "chrome://browser/locale/feeds/subscribe.properties"; -const SUBSCRIBE_PAGE_URI = "chrome://browser/content/feeds/subscribe.xhtml"; +const FEEDHANDLER_URI = "about:feeds"; const PREF_SELECTED_APP = "browser.feeds.handlers.application"; const PREF_SELECTED_WEB = "browser.feeds.handlers.webservice"; const PREF_SELECTED_ACTION = "browser.feeds.handler"; const PREF_SELECTED_READER = "browser.feeds.handler.default"; const PREF_VIDEO_SELECTED_APP = "browser.videoFeeds.handlers.application"; const PREF_VIDEO_SELECTED_WEB = "browser.videoFeeds.handlers.webservice"; @@ -668,17 +668,17 @@ FeedWriter.prototype = { /** * Get moz-icon url for a file * @param file * A nsIFile object for which the moz-icon:// is returned * @returns moz-icon url of the given file as a string */ _getFileIconURL: function FW__getFileIconURL(file) { var ios = Cc["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); + getService(Ci.nsIIOService); var fph = ios.getProtocolHandler("file") .QueryInterface(Ci.nsIFileProtocolHandler); var urlSpec = fph.getURLSpecFromFile(file); return "moz-icon://" + urlSpec + "?size=16"; }, /** * Helper method to set the selected application and system default @@ -1084,20 +1084,19 @@ FeedWriter.prototype = { * @param aWindow * The window of the document invoking the BrowserFeedWriter */ _getOriginalURI: function FW__getOriginalURI(aWindow) { var chan = aWindow.QueryInterface(Ci.nsIInterfaceRequestor). getInterface(Ci.nsIWebNavigation). QueryInterface(Ci.nsIDocShell).currentDocumentChannel; - var uri = makeURI(SUBSCRIBE_PAGE_URI); - var resolvedURI = Cc["@mozilla.org/chrome/chrome-registry;1"]. - getService(Ci.nsIChromeRegistry). - convertChromeURL(uri); + var resolvedURI = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService). + newChannel(FEEDHANDLER_URI, null, null).URI; if (resolvedURI.equals(chan.URI)) return chan.originalURI; return null; }, _window: null,
--- a/browser/components/feeds/src/nsFeedSniffer.h +++ b/browser/components/feeds/src/nsFeedSniffer.h @@ -2,18 +2,20 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsIContentSniffer.h" #include "nsIStreamListener.h" #include "nsStringAPI.h" +#include "mozilla/Attributes.h" -class nsFeedSniffer : public nsIContentSniffer, nsIStreamListener +class nsFeedSniffer MOZ_FINAL : public nsIContentSniffer, + nsIStreamListener { public: NS_DECL_ISUPPORTS NS_DECL_NSICONTENTSNIFFER NS_DECL_NSIREQUESTOBSERVER NS_DECL_NSISTREAMLISTENER static NS_METHOD AppendSegmentToString(nsIInputStream* inputStream,
--- a/browser/components/privatebrowsing/src/nsPrivateBrowsingServiceWrapper.h +++ b/browser/components/privatebrowsing/src/nsPrivateBrowsingServiceWrapper.h @@ -1,20 +1,21 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsCOMPtr.h" #include "nsIPrivateBrowsingService.h" #include "nsIObserver.h" +#include "mozilla/Attributes.h" class nsIJSContextStack; -class nsPrivateBrowsingServiceWrapper : public nsIPrivateBrowsingService, - public nsIObserver +class nsPrivateBrowsingServiceWrapper MOZ_FINAL : public nsIPrivateBrowsingService, + public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_NSIPRIVATEBROWSINGSERVICE NS_DECL_NSIOBSERVER nsresult Init();
--- a/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js +++ b/browser/components/privatebrowsing/test/browser/browser_privatebrowsing_zoomrestore.js @@ -7,18 +7,16 @@ function test() { // initialization let pb = Cc["@mozilla.org/privatebrowsing;1"]. getService(Ci.nsIPrivateBrowsingService); waitForExplicitFinish(); let tabBlank = gBrowser.selectedTab; - gBrowser.removeAllTabsBut(tabBlank); - let blankBrowser = gBrowser.getBrowserForTab(tabBlank); blankBrowser.addEventListener("load", function() { blankBrowser.removeEventListener("load", arguments.callee, true); // change the zoom on the blank page FullZoom.enlarge(); isnot(ZoomManager.zoom, 1, "Zoom level for about:blank should be changed");
--- a/browser/components/sessionstore/src/SessionStore.jsm +++ b/browser/components/sessionstore/src/SessionStore.jsm @@ -2819,16 +2819,118 @@ let SessionStoreInternal = { // set smoothScroll back to the original value tabstrip.smoothScroll = smoothScroll; this._sendRestoreCompletedNotifications(); }, /** + * Sets the tabs restoring order with the following priority: + * Selected tab, pinned tabs, visible tabs, unhidden tabs and hidden tabs. + * @param aTabBrowser + * Tab browser object + * @param aTabs + * Array of tab references + * @param aTabData + * Array of tab data + * @param aSelectedTab + * Index of selected tab + */ + _setTabsRestoringOrder : function ssi__setTabsRestoringOrder( + aTabBrowser, aTabs, aTabData, aSelectedTab) { + // Temporally store the pinned tabs before moving the hidden tabs and + // optimizing the visible tabs. In case the selected tab is a pinned tab, + // the index is also stored. + let pinnedTabs = aTabData.filter(function (aData) aData.pinned).length; + let pinnedTabsArray = []; + let pinnedTabsDataArray = []; + let pinnedSelectedTab = null; + if (pinnedTabs && aTabs.length > 1) { + for (let t = aTabs.length - 1; t >= 0; t--) { + if (aTabData[t].pinned) { + pinnedTabsArray.unshift(aTabs.splice(t, 1)[0]); + pinnedTabsDataArray.unshift(aTabData.splice(t, 1)[0]); + if (aSelectedTab) { + if (aSelectedTab > (t + 1)) + --aSelectedTab; + else if (aSelectedTab == (t + 1)) { + aSelectedTab = null; + pinnedSelectedTab = 1; + } + } else if (pinnedSelectedTab) + ++pinnedSelectedTab; + } + } + } + + // Without the pinned tabs, we move the hidden tabs to the end of the list + // and optimize the visible tabs. + let unhiddenTabs = aTabData.filter(function (aData) !aData.hidden).length; + if (unhiddenTabs && aTabs.length > 1) { + // Load hidden tabs last, by pushing them to the end of the list. + for (let t = 0, tabsToReorder = aTabs.length - unhiddenTabs; tabsToReorder > 0; ) { + if (aTabData[t].hidden) { + aTabs = aTabs.concat(aTabs.splice(t, 1)); + aTabData = aTabData.concat(aTabData.splice(t, 1)); + if (aSelectedTab && aSelectedTab > t) + --aSelectedTab; + --tabsToReorder; + continue; + } + ++t; + } + + // Determine if we can optimize & load visible tabs first + let maxVisibleTabs = Math.ceil(aTabBrowser.tabContainer.mTabstrip.scrollClientSize / + aTabs[unhiddenTabs - 1].getBoundingClientRect().width); + + // Make sure we restore visible tabs first, if there are enough + if (aSelectedTab && maxVisibleTabs < unhiddenTabs && aSelectedTab > 1) { + let firstVisibleTab = 0; + if (unhiddenTabs - maxVisibleTabs > aSelectedTab) { + // aSelectedTab is leftmost since we scroll to it when possible + firstVisibleTab = aSelectedTab - 1; + } else { + // aSelectedTab is rightmost or no more room to scroll right + firstVisibleTab = unhiddenTabs - maxVisibleTabs; + } + aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); + aTabData = aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); + aSelectedTab -= firstVisibleTab; + } + } + + // Load the pinned tabs at the beginning of the list and restore the + // selected tab index. + if (pinnedTabsArray) { + // Restore the selected tab index. + if (pinnedSelectedTab) { + aSelectedTab = pinnedSelectedTab; + } else { + aSelectedTab += pinnedTabsArray.length; + } + // Load the pinned tabs at the beginning of the list. + for (let t = pinnedTabsArray.length - 1; t >= 0; t--) { + aTabs.unshift(pinnedTabsArray.splice(t, 1)[0]); + aTabData.unshift(pinnedTabsDataArray.splice(t, 1)[0]); + } + } + + // Load the selected tab to the first position. + if (aSelectedTab-- && aTabs[aSelectedTab]) { + aTabs.unshift(aTabs.splice(aSelectedTab, 1)[0]); + aTabData.unshift(aTabData.splice(aSelectedTab, 1)[0]); + aTabBrowser.selectedTab = aTabs[0]; + } + + return [aTabs, aTabData]; + }, + + /** * Manage history restoration for a window * @param aWindow * Window to restore the tabs into * @param aTabs * Array of tab references * @param aTabData * Array of tab data * @param aSelectTab @@ -2870,58 +2972,19 @@ let SessionStoreInternal = { if (aTabs.length == 0) { // this is normally done in restoreHistory() but as we're returning early // here we need to take care of it. this._setWindowStateReady(aWindow); return; } - let unhiddenTabs = aTabData.filter(function (aData) !aData.hidden).length; - - if (unhiddenTabs && aTabs.length > 1) { - // Load hidden tabs last, by pushing them to the end of the list - for (let t = 0, tabsToReorder = aTabs.length - unhiddenTabs; tabsToReorder > 0; ) { - if (aTabData[t].hidden) { - aTabs = aTabs.concat(aTabs.splice(t, 1)); - aTabData = aTabData.concat(aTabData.splice(t, 1)); - if (aSelectTab > t) - --aSelectTab; - --tabsToReorder; - continue; - } - ++t; - } - - // Determine if we can optimize & load visible tabs first - let maxVisibleTabs = Math.ceil(tabbrowser.tabContainer.mTabstrip.scrollClientSize / - aTabs[unhiddenTabs - 1].getBoundingClientRect().width); - - // make sure we restore visible tabs first, if there are enough - if (maxVisibleTabs < unhiddenTabs && aSelectTab > 1) { - let firstVisibleTab = 0; - if (unhiddenTabs - maxVisibleTabs > aSelectTab) { - // aSelectTab is leftmost since we scroll to it when possible - firstVisibleTab = aSelectTab - 1; - } else { - // aSelectTab is rightmost or no more room to scroll right - firstVisibleTab = unhiddenTabs - maxVisibleTabs; - } - aTabs = aTabs.splice(firstVisibleTab, maxVisibleTabs).concat(aTabs); - aTabData = aTabData.splice(firstVisibleTab, maxVisibleTabs).concat(aTabData); - aSelectTab -= firstVisibleTab; - } - } - - // make sure to restore the selected tab first (if any) - if (aSelectTab-- && aTabs[aSelectTab]) { - aTabs.unshift(aTabs.splice(aSelectTab, 1)[0]); - aTabData.unshift(aTabData.splice(aSelectTab, 1)[0]); - tabbrowser.selectedTab = aTabs[0]; - } + // Sets the tabs restoring order. + [aTabs, aTabData] = + this._setTabsRestoringOrder(tabbrowser, aTabs, aTabData, aSelectTab); // Prepare the tabs so that they can be properly restored. We'll pin/unpin // and show/hide tabs as necessary. We'll also set the labels, user typed // value, and attach a copy of the tab's data in case we close it before // it's been restored. for (t = 0; t < aTabs.length; t++) { let tab = aTabs[t]; let browser = tabbrowser.getBrowserForTab(tab);
--- a/browser/components/sessionstore/test/browser_480148.js +++ b/browser/components/sessionstore/test/browser_480148.js @@ -3,123 +3,208 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ function test() { /** Test for Bug 484108 **/ waitForExplicitFinish(); requestLongerTimeout(3); // builds the tests state based on a few parameters - function buildTestState(num, selected, hidden) { - let state = { windows: [ { "tabs": [], "selected": selected } ] }; + function buildTestState(num, selected, hidden, pinned) { + let state = { windows: [ { "tabs": [], "selected": selected + 1 } ] }; while (num--) { - state.windows[0].tabs.push({entries: [{url: "http://example.com/"}]}); + state.windows[0].tabs.push({ + entries: [ + { url: "http://example.com/?t=" + state.windows[0].tabs.length } + ] + }); let i = state.windows[0].tabs.length - 1; if (hidden.length > 0 && i == hidden[0]) { state.windows[0].tabs[i].hidden = true; hidden.splice(0, 1); } + if (pinned.length > 0 && i == pinned[0]) { + state.windows[0].tabs[i].pinned = true; + pinned.splice(0, 1); + } } return state; } let tests = [ { testNum: 1, totalTabs: 13, - selectedTab: 1, + selectedTab: 0, shownTabs: 6, hiddenTabs: [], + pinnedTabs: [], order: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] }, { testNum: 2, totalTabs: 13, - selectedTab: 13, + selectedTab: 12, shownTabs: 6, hiddenTabs: [], + pinnedTabs: [], order: [12, 7, 8, 9, 10, 11, 0, 1, 2, 3, 4, 5, 6] }, { testNum: 3, totalTabs: 13, - selectedTab: 4, + selectedTab: 3, shownTabs: 6, hiddenTabs: [], + pinnedTabs: [], order: [3, 4, 5, 6, 7, 8, 0, 1, 2, 9, 10, 11, 12] }, { testNum: 4, totalTabs: 13, - selectedTab: 11, + selectedTab: 10, shownTabs: 6, hiddenTabs: [], + pinnedTabs: [], order: [10, 7, 8, 9, 11, 12, 0, 1, 2, 3, 4, 5, 6] }, { testNum: 5, totalTabs: 13, - selectedTab: 13, + selectedTab: 12, shownTabs: 6, hiddenTabs: [0, 4, 9], + pinnedTabs: [], order: [12, 6, 7, 8, 10, 11, 1, 2, 3, 5, 0, 4, 9] }, { testNum: 6, totalTabs: 13, - selectedTab: 4, + selectedTab: 3, shownTabs: 6, hiddenTabs: [1, 7, 12], + pinnedTabs: [], order: [3, 4, 5, 6, 8, 9, 0, 2, 10, 11, 1, 7, 12] }, { testNum: 7, totalTabs: 13, - selectedTab: 4, + selectedTab: 3, shownTabs: 6, hiddenTabs: [0, 1, 2], + pinnedTabs: [], order: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 0, 1, 2] + }, + { testNum: 8, + totalTabs: 13, + selectedTab: 0, + shownTabs: 6, + hiddenTabs: [], + pinnedTabs: [0], + order: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { testNum: 9, + totalTabs: 13, + selectedTab: 1, + shownTabs: 6, + hiddenTabs: [], + pinnedTabs: [0], + order: [1, 0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { testNum: 10, + totalTabs: 13, + selectedTab: 3, + shownTabs: 6, + hiddenTabs: [2], + pinnedTabs: [0,1], + order: [3, 0, 1, 4, 5, 6, 7, 8, 9, 10, 11, 12, 2] + }, + { testNum: 11, + totalTabs: 13, + selectedTab: 12, + shownTabs: 6, + hiddenTabs: [], + pinnedTabs: [0,1,2], + order: [12, 0, 1, 2, 7, 8, 9, 10, 11, 3, 4, 5, 6] + }, + { testNum: 12, + totalTabs: 13, + selectedTab: 6, + shownTabs: 6, + hiddenTabs: [3,4,5], + pinnedTabs: [0,1,2], + order: [6, 0, 1, 2, 7, 8, 9, 10, 11, 12, 3, 4, 5] + }, + { testNum: 13, + totalTabs: 13, + selectedTab: 1, + shownTabs: 6, + hiddenTabs: [3,4,5], + pinnedTabs: [0,1,2], + order: [1, 0, 2, 6, 7, 8, 9, 10, 11, 12, 3, 4, 5] + }, + { testNum: 14, + totalTabs: 13, + selectedTab: 2, + shownTabs: 6, + hiddenTabs: [], + pinnedTabs: [0,1,2], + order: [2, 0, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + }, + { testNum: 15, + totalTabs: 13, + selectedTab: 3, + shownTabs: 6, + hiddenTabs: [1,4], + pinnedTabs: [0,1,2], + order: [3, 0, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 4] } ]; - let tabMinWidth = parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth); + let tabMinWidth = + parseInt(getComputedStyle(gBrowser.selectedTab, null).minWidth); let testIndex = 0; function runNextTest() { if (tests.length == 0) { finish(); return; } info ("Starting test " + (++testIndex)); let test = tests.shift(); - let state = buildTestState(test.totalTabs, test.selectedTab, test.hiddenTabs); + let state = buildTestState(test.totalTabs, test.selectedTab, + test.hiddenTabs, test.pinnedTabs); let tabbarWidth = Math.floor((test.shownTabs - 0.5) * tabMinWidth); let win = openDialog(location, "_blank", "chrome,all,dialog=no"); - let actualOrder = []; + let tabsRestored = []; win.addEventListener("SSTabRestoring", function onSSTabRestoring(aEvent) { let tab = aEvent.originalTarget; - let currentIndex = Array.indexOf(win.gBrowser.tabs, tab); - actualOrder.push(currentIndex); + let tabLink = tab.linkedBrowser.currentURI.spec; + let tabIndex = + tabLink.substring(tabLink.indexOf("?t=") + 3, tabLink.length); - if (actualOrder.length < state.windows[0].tabs.length) + // we need to compare with the tab's restoring index, no with the + // position index, since the pinned tabs change the positions in the + // tabbar. + tabsRestored.push(tabIndex); + + if (tabsRestored.length < state.windows[0].tabs.length) return; // all of the tabs should be restoring or restored by now - is(actualOrder.length, state.windows[0].tabs.length, + is(tabsRestored.length, state.windows[0].tabs.length, "Test #" + testIndex + ": Number of restored tabs is as expected"); - is(actualOrder.join(" "), test.order.join(" "), + is(tabsRestored.join(" "), test.order.join(" "), "Test #" + testIndex + ": 'visible' tabs restored first"); - // Cleanup. + // cleanup win.removeEventListener("SSTabRestoring", onSSTabRestoring, false); win.close(); executeSoon(runNextTest); }, false); - win.addEventListener("load", function onLoad(aEvent) { - win.removeEventListener("load", onLoad, false); - executeSoon(function () { - let extent = win.outerWidth - win.gBrowser.tabContainer.mTabstrip.scrollClientSize; - let windowWidth = tabbarWidth + extent; - win.resizeTo(windowWidth, win.outerHeight); - ss.setWindowState(win, JSON.stringify(state), true); - }); - }, false); + whenWindowLoaded(win, function(aEvent) { + let extent = + win.outerWidth - win.gBrowser.tabContainer.mTabstrip.scrollClientSize; + let windowWidth = tabbarWidth + extent; + win.resizeTo(windowWidth, win.outerHeight); + ss.setWindowState(win, JSON.stringify(state), true); + }); }; runNextTest(); }
--- a/browser/components/tabview/content.js +++ b/browser/components/tabview/content.js @@ -36,19 +36,17 @@ let WindowEventHandler = { sendSyncMessage("Panorama:DOMWillOpenModalDialog"); }, // ---------- // Function: onMozAfterPaint // Sends an asynchronous message when the "onMozAfterPaint" event // is fired. onMozAfterPaint: function WEH_onMozAfterPaint(event) { - if (event.clientRects.length > 0) { - sendAsyncMessage("Panorama:MozAfterPaint"); - } + sendAsyncMessage("Panorama:MozAfterPaint"); } }; // add event listeners addEventListener("DOMWillOpenModalDialog", WindowEventHandler.onDOMWillOpenModalDialog, false); addEventListener("MozAfterPaint", WindowEventHandler.onMozAfterPaint, false); // ----------
--- a/browser/config/mozconfigs/linux32/nightly +++ b/browser/config/mozconfigs/linux32/nightly @@ -1,13 +1,13 @@ ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL} ac_add_options --enable-update-packaging ac_add_options --enable-codesighs ac_add_options --enable-signmar -ac_add_options --enable-profiling +#ac_add_options --enable-profiling # Nightlies only since this has a cost in performance ac_add_options --enable-js-diagnostics . $topsrcdir/build/unix/mozconfig.linux # Avoid dependency on libstdc++ 4.5 ac_add_options --enable-stdcxx-compat
--- a/browser/config/tooltool-manifests/linux32/clang.manifest +++ b/browser/config/tooltool-manifests/linux32/clang.manifest @@ -1,15 +1,15 @@ [ -{"clang_version": "r159409"}, +{"clang_version": "r159509"}, { "size": 47, "digest": "2005a41fe97a5e00997063705f39d42b6a43b1cf7ba306cbc7b1513de34cdcd050fc6326efa2107f19ba0cc67914745dbf13154fa748010a93cf072481ef4aaa", "algorithm": "sha512", "filename": "setup.sh" }, { -"size": 74004324, -"digest": "c5bb558a5958458b11da385d57897a1f7e57a7be2f452438f6813cf1e9f2a4ce93769c36e304126f97dbd753732b70446c7fa3c779267e80152d7ac0175057fc", +"size": 74071397, +"digest": "390e499161e8b5e91c179f3352ecbb07431e3d5a190298de7f338b48fe3807f3ddbeca72d8df39d11f89864fb1135f14500471faa741d3886b875b8e2b1d4416", "algorithm": "sha512", "filename": "clang.tar.bz2" } ]
--- a/browser/config/tooltool-manifests/linux64/clang.manifest +++ b/browser/config/tooltool-manifests/linux64/clang.manifest @@ -1,15 +1,15 @@ [ -{"clang_version": "r159409"}, +{"clang_version": "r159509"}, { "size": 47, "digest": "2005a41fe97a5e00997063705f39d42b6a43b1cf7ba306cbc7b1513de34cdcd050fc6326efa2107f19ba0cc67914745dbf13154fa748010a93cf072481ef4aaa", "algorithm": "sha512", "filename": "setup.sh" }, { -"size": 72533541, -"digest": "f092080caed28db1ed7d9f0612aef1d885d2587b4e069d3662af5c241950ee772b6bc249d766a47b4fe2c170a46cfe05010269a3cbc1123d1f4126bc182b7b40", +"size": 72518043, +"digest": "447dac319a8d7fa902cc065b758440bf6d4a7a98104362162fbdb0479a44d9b84c5878506f09be4398053a6ddc07935c7fb4f71e59b178073876fb5f90a45219", "algorithm": "sha512", "filename": "clang.tar.bz2" } ]
--- a/browser/config/tooltool-manifests/macosx64/clang.manifest +++ b/browser/config/tooltool-manifests/macosx64/clang.manifest @@ -1,15 +1,15 @@ [ -{"clang_version": "r159409"}, +{"clang_version": "r159509"}, { "size": 47, "digest": "2005a41fe97a5e00997063705f39d42b6a43b1cf7ba306cbc7b1513de34cdcd050fc6326efa2107f19ba0cc67914745dbf13154fa748010a93cf072481ef4aaa", "algorithm": "sha512", "filename": "setup.sh" }, { -"size": 63604770, -"digest": "c664beadb01a4e8ba7f32698b3ef694ade9044a9117d90d57f1475f2cefea82d74d1b0f3da89dcb9932ba90e048ae3d908bff05ab09d0ce2c82731e949e62a47", +"size": 63679229, +"digest": "5257503e537b8d440b17e40aa06f0f70f1b124129c02f10e45b46ac642fc4170bfa77ae737a8bcac3ed7602ccd934a88cbe349986eb971d66a6fb553ae31f13c", "algorithm": "sha512", "filename": "clang.tar.bz2" } ]
--- a/browser/devtools/commandline/gcli.jsm +++ b/browser/devtools/commandline/gcli.jsm @@ -143,31 +143,28 @@ define('gcli/index', ['require', 'export * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/types/basic', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { +define('gcli/types/basic', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/selection', 'gcli/argument'], function(require, exports, module) { var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Type = require('gcli/types').Type; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayConversion = require('gcli/types').ArrayConversion; -var Speller = require('gcli/types/spell').Speller; var SelectionType = require('gcli/types/selection').SelectionType; -var Argument = require('gcli/argument').Argument; -var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; -var FalseNamedArgument = require('gcli/argument').FalseNamedArgument; +var BlankArgument = require('gcli/argument').BlankArgument; var ArrayArgument = require('gcli/argument').ArrayArgument; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(StringType); @@ -258,17 +255,17 @@ NumberType.prototype.getMax = function() if (typeof this._max === 'number') { return this._max; } } return undefined; }; NumberType.prototype.parse = function(arg) { - if (arg.text.replace(/\s/g, '').length === 0) { + if (arg.text.replace(/^\s*-?/, '').length === 0) { return new Conversion(undefined, arg, Status.INCOMPLETE, ''); } var value = parseInt(arg.text, 10); if (isNaN(value)) { return new Conversion(undefined, arg, Status.ERROR, l10n.lookupFormat('typesNumberNan', [ arg.text ])); } @@ -343,34 +340,34 @@ function BooleanType(typeSpec) { BooleanType.prototype = Object.create(SelectionType.prototype); BooleanType.prototype.lookup = [ { name: 'false', value: false }, { name: 'true', value: true } ]; BooleanType.prototype.parse = function(arg) { - if (arg instanceof TrueNamedArgument) { + if (arg.type === 'TrueNamedArgument') { return new Conversion(true, arg); } - if (arg instanceof FalseNamedArgument) { + if (arg.type === 'FalseNamedArgument') { return new Conversion(false, arg); } return SelectionType.prototype.parse.call(this, arg); }; BooleanType.prototype.stringify = function(value) { if (value == null) { return ''; } return '' + value; }; BooleanType.prototype.getBlank = function() { - return new Conversion(false, new Argument(), Status.VALID, '', this.lookup); + return new Conversion(false, new BlankArgument(), Status.VALID, '', this.lookup); }; BooleanType.prototype.name = 'boolean'; exports.BooleanType = BooleanType; /** @@ -470,17 +467,17 @@ ArrayType.prototype.stringify = function if (values == null) { return ''; } // BUG 664204: Check for strings with spaces and add quotes return values.join(' '); }; ArrayType.prototype.parse = function(arg) { - if (arg instanceof ArrayArgument) { + if (arg.type === 'ArrayArgument') { var conversions = arg.getArguments().map(function(subArg) { var conversion = this.subtype.parse(subArg); // Hack alert. ArrayConversion needs to be able to answer questions // about the status of individual conversions in addition to the // overall state. This allows us to do that easily. subArg.conversion = conversion; return conversion; }, this); @@ -604,20 +601,20 @@ exports.lookupFormat = function(key, swa * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ define('gcli/types', ['require', 'exports', 'module' , 'gcli/argument'], function(require, exports, module) { -var types = exports; var Argument = require('gcli/argument').Argument; +var BlankArgument = require('gcli/argument').BlankArgument; /** * Some types can detect validity, that is to say they can distinguish between * valid and invalid values. * We might want to change these constants to be numbers for better performance */ var Status = { @@ -666,17 +663,19 @@ var Status = { } if (status > combined) { combined = status; } } return combined; } }; -types.Status = Status; + +exports.Status = Status; + /** * The type.parse() method converts an Argument into a value, Conversion is * a wrapper to that value. * Conversion is needed to collect a number of properties related to that * conversion in one place, i.e. to handle errors and provide traceability. * @param value The result of the conversion * @param arg The data from which the conversion was made @@ -715,33 +714,31 @@ function Conversion(value, arg, status, throw new Error('Missing arg'); } this._status = status || Status.VALID; this.message = message; this.predictions = predictions; } -types.Conversion = Conversion; - /** * Ensure that all arguments that are part of this conversion know what they * are assigned to. * @param assignment The Assignment (param/conversion link) to inform the * argument about. */ Conversion.prototype.assign = function(assignment) { this.arg.assign(assignment); }; /** * Work out if there is information provided in the contained argument. */ Conversion.prototype.isDataProvided = function() { - return !this.arg.isBlank(); + return this.arg.type !== 'BlankArgument'; }; /** * 2 conversions are equal if and only if their args are equal (argEquals) and * their values are equal (valueEquals). * @param that The conversion object to compare against. */ Conversion.prototype.equals = function(that) { @@ -853,16 +850,25 @@ Conversion.prototype.constrainPrediction index = index % predictions.length; if (index < 0) { index = predictions.length + index; } return index; }; /** + * Constant to allow everyone to agree on the maximum number of predictions + * that should be provided. We actually display 1 less than this number. + */ +Conversion.maxPredictions = 11; + +exports.Conversion = Conversion; + + +/** * ArrayConversion is a special Conversion, needed because arrays are converted * member by member rather then as a whole, which means we can track the * conversion if individual array elements. So an ArrayConversion acts like a * normal Conversion (which is needed as Assignment requires a Conversion) but * it can also be devolved into a set of Conversions for each array member. */ function ArrayConversion(conversions, arg) { this.arg = arg; @@ -926,17 +932,17 @@ ArrayConversion.prototype.valueEquals = }; ArrayConversion.prototype.toString = function() { return '[ ' + this.conversions.map(function(conversion) { return conversion.toString(); }, this).join(', ') + ' ]'; }; -types.ArrayConversion = ArrayConversion; +exports.ArrayConversion = ArrayConversion; /** * Most of our types are 'static' e.g. there is only one type of 'string', * however some types like 'selection' and 'deferred' are customizable. * The basic Type type isn't useful, but does provide documentation about what * types do. */ @@ -998,50 +1004,50 @@ Type.prototype.decrement = function(valu /** * The 'blank value' of most types is 'undefined', but there are exceptions; * This allows types to specify a better conversion from empty string than * 'undefined'. * 2 known examples of this are boolean -> false and array -> [] */ Type.prototype.getBlank = function() { - return this.parse(new Argument()); + return this.parse(new BlankArgument()); }; /** * This is something of a hack for the benefit of DeferredType which needs to * be able to lie about it's type for fields to accept it as one of their own. * Sub-types can ignore this unless they're DeferredType. */ Type.prototype.getType = function() { return this; }; -types.Type = Type; +exports.Type = Type; /** * Private registry of types * Invariant: types[name] = type.name */ var registeredTypes = {}; -types.getTypeNames = function() { +exports.getTypeNames = function() { return Object.keys(registeredTypes); }; /** * Add a new type to the list available to the system. * You can pass 2 things to this function - either an instance of Type, in * which case we return this instance when #getType() is called with a 'name' * that matches type.name. * Also you can pass in a constructor (i.e. function) in which case when * #getType() is called with a 'name' that matches Type.prototype.name we will * pass the typeSpec into this constructor. */ -types.registerType = function(type) { +exports.registerType = function(type) { if (typeof type === 'object') { if (type instanceof Type) { if (!type.name) { throw new Error('All registered types must have a name'); } registeredTypes[type.name] = type; } else { @@ -1054,35 +1060,35 @@ types.registerType = function(type) { } registeredTypes[type.prototype.name] = type; } else { throw new Error('Unknown type: ' + type); } }; -types.registerTypes = function registerTypes(newTypes) { +exports.registerTypes = function registerTypes(newTypes) { Object.keys(newTypes).forEach(function(name) { var type = newTypes[name]; type.name = name; newTypes.registerType(type); }); }; /** * Remove a type from the list available to the system */ -types.deregisterType = function(type) { +exports.deregisterType = function(type) { delete registeredTypes[type.name]; }; /** * Find a type, previously registered using #registerType() */ -types.getType = function(typeSpec) { +exports.getType = function(typeSpec) { var type; if (typeof typeSpec === 'string') { type = registeredTypes[typeSpec]; if (typeof type === 'function') { type = new type({}); } return type; } @@ -1120,16 +1126,27 @@ types.getType = function(typeSpec) { * limitations under the License. */ define('gcli/argument', ['require', 'exports', 'module' ], function(require, exports, module) { var argument = exports; /** + * Thinking out loud here: + * Arguments are an area where we could probably refactor things a bit better. + * The split process in Requisition creates a set of Arguments, which are then + * assigned. The assign process sometimes converts them into subtypes of + * Argument. We might consider that what gets assigned is _always_ one of the + * subtypes (or actually a different type hierarchy entirely) and that we + * don't manipulate the prefix/text/suffix but just use the 'subtypes' as + * filters which present a view of the underlying original Argument. + */ + +/** * We record where in the input string an argument comes so we can report * errors against those string positions. * @param text The string (trimmed) that contains the argument * @param prefix Knowledge of quotation marks and whitespace used prior to the * text in the input string allows us to re-generate the original input from * the arguments. * @param suffix Any quotation marks and whitespace used after the text. * Whitespace is normally placed in the prefix to the succeeding argument, but @@ -1144,16 +1161,18 @@ function Argument(text, prefix, suffix) } else { this.text = text; this.prefix = prefix !== undefined ? prefix : ''; this.suffix = suffix !== undefined ? suffix : ''; } } +Argument.prototype.type = 'Argument'; + /** * Return the result of merging these arguments. * case and some of the arguments are in quotation marks? */ Argument.prototype.merge = function(following) { // Is it possible that this gets called when we're merging arguments // for the single string? return new Argument( @@ -1178,25 +1197,16 @@ Argument.prototype.beget = function(repl prefix = (options.prefixSpace ? ' ' : '') + quote; suffix = quote; } return new Argument(replText, prefix, suffix); }; /** - * Is there any visible content to this argument? - */ -Argument.prototype.isBlank = function() { - return this.text === '' && - this.prefix.trim() === '' && - this.suffix.trim() === ''; -}; - -/** * We need to keep track of which assignment we've been assigned to */ Argument.prototype.assign = function(assignment) { this.assignment = assignment; }; /** * Sub-classes of Argument are collections of arguments, getArgs() gets access @@ -1251,20 +1261,53 @@ Argument.merge = function(argArray, star } else { joined = joined.merge(arg); } } return joined; }; +/** + * For test/debug use only. The output from this function is subject to wanton + * random change without notice, and should not be relied upon to even exist + * at some later date. + */ +Object.defineProperty(Argument.prototype, '_summaryJson', { + get: function() { + var assignStatus = this.assignment == null ? + 'null' : + this.assignment.param.name; + return '<' + this.prefix + ':' + this.text + ':' + this.suffix + '>' + + ' (a=' + assignStatus + ',' + ' t=' + this.type + ')'; + }, + enumerable: true +}); + argument.Argument = Argument; /** + * BlankArgument is a marker that the argument wasn't typed but is there to + * fill a slot. Assignments begin with their arg set to a BlankArgument. + */ +function BlankArgument() { + this.text = ''; + this.prefix = ''; + this.suffix = ''; +} + +BlankArgument.prototype = Object.create(Argument.prototype); + +BlankArgument.prototype.type = 'BlankArgument'; + +argument.BlankArgument = BlankArgument; + + +/** * ScriptArgument is a marker that the argument is designed to be Javascript. * It also implements the special rules that spaces after the { or before the * } are part of the pre/suffix rather than the content, and that they are * never 'blank' so they can be used by Requisition._split() and not raise an * ERROR status due to being blank. */ function ScriptArgument(text, prefix, suffix) { this.text = text !== undefined ? text : ''; @@ -1279,16 +1322,18 @@ function ScriptArgument(text, prefix, su while (this.text.charAt(this.text.length - 1) === ' ') { this.suffix = ' ' + this.suffix; this.text = this.text.slice(0, -1); } } ScriptArgument.prototype = Object.create(Argument.prototype); +ScriptArgument.prototype.type = 'ScriptArgument'; + /** * Returns a new Argument like this one but with the text set to * <tt>replText</tt> and the end adjusted to fit. * @param replText Text to replace the old text value */ ScriptArgument.prototype.beget = function(replText, options) { var prefix = this.prefix; var suffix = this.suffix; @@ -1296,25 +1341,16 @@ ScriptArgument.prototype.beget = functio if (options && options.normalize) { prefix = '{ '; suffix = ' }'; } return new ScriptArgument(replText, prefix, suffix); }; -/** - * ScriptArguments are never blank due to the '{' and '}' and their special use - * for the command argument requires them not to be blank even when there is - * no text. - */ -ScriptArgument.prototype.isBlank = function() { - return false; -}; - argument.ScriptArgument = ScriptArgument; /** * Commands like 'echo' with a single string argument, and used with the * special format like: 'echo a b c' effectively have a number of arguments * merged together. */ @@ -1333,16 +1369,18 @@ function MergedArgument(args, start, end var arg = Argument.merge(this.args); this.text = arg.text; this.prefix = arg.prefix; this.suffix = arg.suffix; } MergedArgument.prototype = Object.create(Argument.prototype); +MergedArgument.prototype.type = 'MergedArgument'; + /** * Keep track of which assignment we've been assigned to, and allow the * original args to do the same. */ MergedArgument.prototype.assign = function(assignment) { this.args.forEach(function(arg) { arg.assign(assignment); }, this); @@ -1379,16 +1417,18 @@ function TrueNamedArgument(name, arg) { this.arg = arg; this.text = arg ? arg.text : '--' + name; this.prefix = arg ? arg.prefix : ' '; this.suffix = arg ? arg.suffix : ''; } TrueNamedArgument.prototype = Object.create(Argument.prototype); +TrueNamedArgument.prototype.type = 'TrueNamedArgument'; + TrueNamedArgument.prototype.assign = function(assignment) { if (this.arg) { this.arg.assign(assignment); } this.assignment = assignment; }; TrueNamedArgument.prototype.getArgs = function() { @@ -1422,16 +1462,18 @@ argument.TrueNamedArgument = TrueNamedAr function FalseNamedArgument() { this.text = ''; this.prefix = ''; this.suffix = ''; } FalseNamedArgument.prototype = Object.create(Argument.prototype); +FalseNamedArgument.prototype.type = 'FalseNamedArgument'; + FalseNamedArgument.prototype.getArgs = function() { return [ ]; }; FalseNamedArgument.prototype.equals = function(that) { if (this === that) { return true; } @@ -1447,39 +1489,44 @@ argument.FalseNamedArgument = FalseNamed /** * A named argument is for cases where we have input in one of the following * formats: * <ul> * <li>--param value * <li>-p value - * <li>--pa value - * <li>-p:value - * <li>--param=value - * <li>etc * </ul> - * The general format is: - * /--?{unique-param-name-prefix}[ :=]{value}/ * We model this as a normal argument but with a long prefix. */ function NamedArgument(nameArg, valueArg) { this.nameArg = nameArg; this.valueArg = valueArg; - this.text = valueArg.text; - this.prefix = nameArg.toString() + valueArg.prefix; - this.suffix = valueArg.suffix; + if (valueArg == null) { + this.text = ''; + this.prefix = nameArg.toString(); + this.suffix = ''; + } + else { + this.text = valueArg.text; + this.prefix = nameArg.toString() + valueArg.prefix; + this.suffix = valueArg.suffix; + } } NamedArgument.prototype = Object.create(Argument.prototype); +NamedArgument.prototype.type = 'NamedArgument'; + NamedArgument.prototype.assign = function(assignment) { this.nameArg.assign(assignment); - this.valueArg.assign(assignment); + if (this.valueArg != null) { + this.valueArg.assign(assignment); + } this.assignment = assignment; }; NamedArgument.prototype.getArgs = function() { return [ this.nameArg, this.valueArg ]; }; NamedArgument.prototype.equals = function(that) { @@ -1508,16 +1555,18 @@ argument.NamedArgument = NamedArgument; * can be jointly assigned to a single array parameter */ function ArrayArgument() { this.args = []; } ArrayArgument.prototype = Object.create(Argument.prototype); +ArrayArgument.prototype.type = 'ArrayArgument'; + ArrayArgument.prototype.addArgument = function(arg) { this.args.push(arg); }; ArrayArgument.prototype.addArguments = function(args) { Array.prototype.push.apply(this.args, args); }; @@ -1540,17 +1589,17 @@ ArrayArgument.prototype.getArgs = functi ArrayArgument.prototype.equals = function(that) { if (this === that) { return true; } if (that == null) { return false; } - if (!(that instanceof ArrayArgument)) { + if (!(that.type === 'ArrayArgument')) { return false; } if (this.args.length !== that.args.length) { return false; } for (var i = 0; i < this.args.length; i++) { @@ -1571,16 +1620,318 @@ ArrayArgument.prototype.toString = funct }, this).join(',') + '}'; }; argument.ArrayArgument = ArrayArgument; }); /* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/types/selection', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell'], function(require, exports, module) { + + +var l10n = require('gcli/l10n'); +var types = require('gcli/types'); +var Type = require('gcli/types').Type; +var Status = require('gcli/types').Status; +var Conversion = require('gcli/types').Conversion; +var Speller = require('gcli/types/spell').Speller; + + +/** + * Registration and de-registration. + */ +exports.startup = function() { + types.registerType(SelectionType); +}; + +exports.shutdown = function() { + types.unregisterType(SelectionType); +}; + + +/** + * A selection allows the user to pick a value from known set of options. + * An option is made up of a name (which is what the user types) and a value + * (which is passed to exec) + * @param typeSpec Object containing properties that describe how this + * selection functions. Properties include: + * - lookup: An array of objects, one for each option, which contain name and + * value properties. lookup can be a function which returns this array + * - data: An array of strings - alternative to 'lookup' where the valid values + * are strings. i.e. there is no mapping between what is typed and the value + * that is used by the program + * - stringifyProperty: Conversion from value to string is generally a process + * of looking through all the valid options for a matching value, and using + * the associated name. However the name maybe available directly from the + * value using a property lookup. Setting 'stringifyProperty' allows + * SelectionType to take this shortcut. + * - cacheable : If lookup is a function, then we normally assume that + * the values fetched can change. Setting 'cacheable' enables internal + * caching. + */ +function SelectionType(typeSpec) { + if (typeSpec) { + Object.keys(typeSpec).forEach(function(key) { + this[key] = typeSpec[key]; + }, this); + } +} + +SelectionType.prototype = Object.create(Type.prototype); + +SelectionType.prototype.stringify = function(value) { + if (value == null) { + return ''; + } + if (this.stringifyProperty != null) { + return value[this.stringifyProperty]; + } + var name = null; + var lookup = this.getLookup(); + lookup.some(function(item) { + if (item.value === value) { + name = item.name; + return true; + } + return false; + }, this); + return name; +}; + +/** + * If typeSpec contained cacheable:true then calls to parse() work on cached + * data. clearCache() enables the cache to be cleared. + */ +SelectionType.prototype.clearCache = function() { + delete this._cachedLookup; +}; + +/** + * There are several ways to get selection data. This unifies them into one + * single function. + * @return An array of objects with name and value properties. + */ +SelectionType.prototype.getLookup = function() { + if (this._cachedLookup) { + return this._cachedLookup; + } + + if (this.lookup) { + if (typeof this.lookup === 'function') { + if (this.cacheable) { + this._cachedLookup = this.lookup(); + return this._cachedLookup; + } + return this.lookup(); + } + return this.lookup; + } + + if (Array.isArray(this.data)) { + this.lookup = this._dataToLookup(this.data); + return this.lookup; + } + + if (typeof(this.data) === 'function') { + return this._dataToLookup(this.data()); + } + + throw new Error('SelectionType has no data'); +}; + +/** + * Selection can be provided with either a lookup object (in the 'lookup' + * property) or an array of strings (in the 'data' property). Internally we + * always use lookup, so we need a way to convert a 'data' array to a lookup. + */ +SelectionType.prototype._dataToLookup = function(data) { + return data.map(function(option) { + return { name: option, value: option }; + }, this); +}; + +/** + * Return a list of possible completions for the given arg. + * @param arg The initial input to match + * @return A trimmed array of string:value pairs + */ +SelectionType.prototype._findPredictions = function(arg) { + var predictions = []; + var lookup = this.getLookup(); + var i, option; + var maxPredictions = Conversion.maxPredictions; + + // If the arg has a suffix then we're kind of 'done'. Only an exact match + // will do. + if (arg.suffix.length > 0) { + for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { + option = lookup[i]; + if (option.name === arg.text) { + this._addToPredictions(predictions, option, arg); + } + } + + return predictions; + } + + // Start with prefix matching + for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { + option = lookup[i]; + if (option.name.indexOf(arg.text) === 0) { + this._addToPredictions(predictions, option, arg); + } + } + + // Try infix matching if we get less half max matched + if (predictions.length < (maxPredictions / 2)) { + for (i = 0; i < lookup.length && predictions.length < maxPredictions; i++) { + option = lookup[i]; + if (option.name.indexOf(arg.text) !== -1) { + if (predictions.indexOf(option) === -1) { + this._addToPredictions(predictions, option, arg); + } + } + } + } + + // Try fuzzy matching if we don't get a prefix match + if (false && predictions.length === 0) { + var speller = new Speller(); + var names = lookup.map(function(opt) { + return opt.name; + }); + speller.train(names); + var corrected = speller.correct(arg.text); + if (corrected) { + lookup.forEach(function(opt) { + if (opt.name === corrected) { + predictions.push(opt); + } + }, this); + } + } + + return predictions; +}; + +/** + * Add an option to our list of predicted options. + * We abstract out this portion of _findPredictions() because CommandType needs + * to make an extra check before actually adding which SelectionType does not + * need to make. + */ +SelectionType.prototype._addToPredictions = function(predictions, option, arg) { + predictions.push(option); +}; + +SelectionType.prototype.parse = function(arg) { + var predictions = this._findPredictions(arg); + + if (predictions.length === 0) { + var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); + return new Conversion(undefined, arg, Status.ERROR, msg, predictions); + } + + // This is something of a hack it basically allows us to tell the + // setting type to forget its last setting hack. + if (this.noMatch) { + this.noMatch(); + } + + var value = predictions[0].value; + + if (predictions[0].name === arg.text) { + return new Conversion(value, arg, Status.VALID, '', predictions); + } + + return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictions); +}; + +/** + * For selections, up is down and black is white. It's like this, given a list + * [ a, b, c, d ], it's natural to think that it starts at the top and that + * going up the list, moves towards 'a'. However 'a' has the lowest index, so + * for SelectionType, up is down and down is up. + * Sorry. + */ +SelectionType.prototype.decrement = function(value) { + var lookup = this.getLookup(); + var index = this._findValue(lookup, value); + if (index === -1) { + index = 0; + } + index++; + if (index >= lookup.length) { + index = 0; + } + return lookup[index].value; +}; + +/** + * See note on SelectionType.decrement() + */ +SelectionType.prototype.increment = function(value) { + var lookup = this.getLookup(); + var index = this._findValue(lookup, value); + if (index === -1) { + // For an increment operation when there is nothing to start from, we + // want to start from the top, i.e. index 0, so the value before we + // 'increment' (see note above) must be 1. + index = 1; + } + index--; + if (index < 0) { + index = lookup.length - 1; + } + return lookup[index].value; +}; + +/** + * Walk through an array of { name:.., value:... } objects looking for a + * matching value (using strict equality), returning the matched index (or -1 + * if not found). + * @param lookup Array of objects with name/value properties to search through + * @param value The value to search for + * @return The index at which the match was found, or -1 if no match was found + */ +SelectionType.prototype._findValue = function(lookup, value) { + var index = -1; + for (var i = 0; i < lookup.length; i++) { + var pair = lookup[i]; + if (pair.value === value) { + index = i; + break; + } + } + return index; +}; + +SelectionType.prototype.name = 'selection'; + +exports.SelectionType = SelectionType; + + +}); +/* * Copyright (c) 2009 Panagiotis Astithas * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY @@ -1737,339 +2088,74 @@ exports.Speller = Speller; * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/types/selection', ['require', 'exports', 'module' , 'gcli/l10n', 'gcli/types', 'gcli/types/spell'], function(require, exports, module) { - - -var l10n = require('gcli/l10n'); -var types = require('gcli/types'); -var Type = require('gcli/types').Type; -var Status = require('gcli/types').Status; -var Conversion = require('gcli/types').Conversion; -var Speller = require('gcli/types/spell').Speller; - - -/** - * Registration and de-registration. - */ -exports.startup = function() { - types.registerType(SelectionType); -}; - -exports.shutdown = function() { - types.unregisterType(SelectionType); -}; - - -/** - * A selection allows the user to pick a value from known set of options. - * An option is made up of a name (which is what the user types) and a value - * (which is passed to exec) - * @param typeSpec Object containing properties that describe how this - * selection functions. Properties include: - * - lookup: An array of objects, one for each option, which contain name and - * value properties. lookup can be a function which returns this array - * - data: An array of strings - alternative to 'lookup' where the valid values - * are strings. i.e. there is no mapping between what is typed and the value - * that is used by the program - * - stringifyProperty: Conversion from value to string is generally a process - * of looking through all the valid options for a matching value, and using - * the associated name. However the name maybe available directly from the - * value using a property lookup. Setting 'stringifyProperty' allows - * SelectionType to take this shortcut. - * - cacheable : If lookup is a function, then we normally assume that - * the values fetched can change. Setting 'cacheable' enables internal - * caching. - */ -function SelectionType(typeSpec) { - if (typeSpec) { - Object.keys(typeSpec).forEach(function(key) { - this[key] = typeSpec[key]; - }, this); - } -} - -SelectionType.prototype = Object.create(Type.prototype); - -SelectionType.prototype.maxPredictions = 10; - -SelectionType.prototype.stringify = function(value) { - if (value == null) { - return ''; - } - if (this.stringifyProperty != null) { - return value[this.stringifyProperty]; - } - var name = null; - var lookup = this.getLookup(); - lookup.some(function(item) { - if (item.value === value) { - name = item.name; - return true; - } - return false; - }, this); - return name; -}; - -/** - * If typeSpec contained cacheable:true then calls to parse() work on cached - * data. clearCache() enables the cache to be cleared. - */ -SelectionType.prototype.clearCache = function() { - delete this._cachedLookup; -}; - -/** - * There are several ways to get selection data. This unifies them into one - * single function. - * @return An array of objects with name and value properties. - */ -SelectionType.prototype.getLookup = function() { - if (this._cachedLookup) { - return this._cachedLookup; - } - - if (this.lookup) { - if (typeof this.lookup === 'function') { - if (this.cacheable) { - this._cachedLookup = this.lookup(); - return this._cachedLookup; - } - return this.lookup(); - } - return this.lookup; - } - - if (Array.isArray(this.data)) { - this.lookup = this._dataToLookup(this.data); - return this.lookup; - } - - if (typeof(this.data) === 'function') { - return this._dataToLookup(this.data()); - } - - throw new Error('SelectionType has no data'); -}; - -/** - * Selection can be provided with either a lookup object (in the 'lookup' - * property) or an array of strings (in the 'data' property). Internally we - * always use lookup, so we need a way to convert a 'data' array to a lookup. - */ -SelectionType.prototype._dataToLookup = function(data) { - return data.map(function(option) { - return { name: option, value: option }; - }, this); -}; - -/** - * Return a list of possible completions for the given arg. - * @param arg The initial input to match - * @return A trimmed array of string:value pairs - */ -SelectionType.prototype._findPredictions = function(arg) { - var predictions = []; - var lookup = this.getLookup(); - var i, option; - - // If the arg has a suffix then we're kind of 'done'. Only an exact match - // will do. - if (arg.suffix.length > 0) { - for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { - option = lookup[i]; - if (option.name === arg.text) { - this._addToPredictions(predictions, option, arg); - } - } - - return predictions; - } - - // Start with prefix matching - for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { - option = lookup[i]; - if (option.name.indexOf(arg.text) === 0) { - this._addToPredictions(predictions, option, arg); - } - } - - // Try infix matching if we get less half max matched - if (predictions.length < (this.maxPredictions / 2)) { - for (i = 0; i < lookup.length && predictions.length < this.maxPredictions; i++) { - option = lookup[i]; - if (option.name.indexOf(arg.text) !== -1) { - if (predictions.indexOf(option) === -1) { - this._addToPredictions(predictions, option, arg); - } - } - } - } - - // Try fuzzy matching if we don't get a prefix match - if (false && predictions.length === 0) { - var speller = new Speller(); - var names = lookup.map(function(opt) { - return opt.name; - }); - speller.train(names); - var corrected = speller.correct(arg.text); - if (corrected) { - lookup.forEach(function(opt) { - if (opt.name === corrected) { - predictions.push(opt); - } - }, this); - } - } - - return predictions; -}; - -/** - * Add an option to our list of predicted options. - * We abstract out this portion of _findPredictions() because CommandType needs - * to make an extra check before actually adding which SelectionType does not - * need to make. - */ -SelectionType.prototype._addToPredictions = function(predictions, option, arg) { - predictions.push(option); -}; - -SelectionType.prototype.parse = function(arg) { - var predictions = this._findPredictions(arg); - - if (predictions.length === 0) { - var msg = l10n.lookupFormat('typesSelectionNomatch', [ arg.text ]); - return new Conversion(undefined, arg, Status.ERROR, msg, predictions); - } - - // This is something of a hack it basically allows us to tell the - // setting type to forget its last setting hack. - if (this.noMatch) { - this.noMatch(); - } - - var value = predictions[0].value; - - if (predictions[0].name === arg.text) { - return new Conversion(value, arg, Status.VALID, '', predictions); - } - - return new Conversion(undefined, arg, Status.INCOMPLETE, '', predictions); -}; - -/** - * For selections, up is down and black is white. It's like this, given a list - * [ a, b, c, d ], it's natural to think that it starts at the top and that - * going up the list, moves towards 'a'. However 'a' has the lowest index, so - * for SelectionType, up is down and down is up. - * Sorry. - */ -SelectionType.prototype.decrement = function(value) { - var lookup = this.getLookup(); - var index = this._findValue(lookup, value); - if (index === -1) { - index = 0; - } - index++; - if (index >= lookup.length) { - index = 0; - } - return lookup[index].value; -}; - -/** - * See note on SelectionType.decrement() - */ -SelectionType.prototype.increment = function(value) { - var lookup = this.getLookup(); - var index = this._findValue(lookup, value); - if (index === -1) { - // For an increment operation when there is nothing to start from, we - // want to start from the top, i.e. index 0, so the value before we - // 'increment' (see note above) must be 1. - index = 1; - } - index--; - if (index < 0) { - index = lookup.length - 1; - } - return lookup[index].value; -}; - -/** - * Walk through an array of { name:.., value:... } objects looking for a - * matching value (using strict equality), returning the matched index (or -1 - * if not found). - * @param lookup Array of objects with name/value properties to search through - * @param value The value to search for - * @return The index at which the match was found, or -1 if no match was found - */ -SelectionType.prototype._findValue = function(lookup, value) { - var index = -1; - for (var i = 0; i < lookup.length; i++) { - var pair = lookup[i]; - if (pair.value === value) { - index = i; - break; - } - } - return index; -}; - -SelectionType.prototype.name = 'selection'; - -exports.SelectionType = SelectionType; - - -}); -/* - * Copyright 2012, Mozilla Foundation and contributors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - define('gcli/types/command', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/l10n', 'gcli/types', 'gcli/types/selection'], function(require, exports, module) { var canon = require('gcli/canon'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var SelectionType = require('gcli/types/selection').SelectionType; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; /** * Registration and de-registration. */ exports.startup = function() { types.registerType(CommandType); + types.registerType(ParamType); }; exports.shutdown = function() { types.unregisterType(CommandType); + types.unregisterType(ParamType); +}; + + +/** + * Select from the available commands. + * This is very similar to a SelectionType, however the level of hackery in + * SelectionType to make it handle Commands correctly was to high, so we + * simplified. + * If you are making changes to this code, you should check there too. + */ +function ParamType(typeSpec) { + this.requisition = typeSpec.requisition; + this.isIncompleteName = typeSpec.isIncompleteName; + this.stringifyProperty = 'name'; +} + +ParamType.prototype = Object.create(SelectionType.prototype); + +ParamType.prototype.name = 'param'; + +ParamType.prototype.lookup = function() { + var displayedParams = []; + var command = this.requisition.commandAssignment.value; + command.params.forEach(function(param) { + var arg = this.requisition.getAssignment(param.name).arg; + if (!param.isPositionalAllowed && arg.type === "BlankArgument") { + displayedParams.push({ name: '--' + param.name, value: param }); + } + }, this); + return displayedParams; +}; + +ParamType.prototype.parse = function(arg) { + return this.isIncompleteName ? + SelectionType.prototype.parse.call(this, arg) : + new Conversion(undefined, arg, Status.ERROR, l10n.lookup('cliUnusedArg')); }; /** * Select from the available commands. * This is very similar to a SelectionType, however the level of hackery in * SelectionType to make it handle Commands correctly was to high, so we * simplified. @@ -2155,26 +2241,27 @@ CommandType.prototype.parse = function(a * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/canon', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/types/basic'], function(require, exports, module) { +define('gcli/canon', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/types', 'gcli/types/basic', 'gcli/types/selection'], function(require, exports, module) { var canon = exports; var util = require('gcli/util'); var l10n = require('gcli/l10n'); var types = require('gcli/types'); var Status = require('gcli/types').Status; var BooleanType = require('gcli/types/basic').BooleanType; +var SelectionType = require('gcli/types/selection').SelectionType; /** * Implement the localization algorithm for any documentation objects (i.e. * description and manual) in a command. * @param data The data assigned to a description or manual property * @param onUndefined If data == null, should we return the data untouched or * lookup a 'we don't know' key in it's place. */ @@ -2210,16 +2297,17 @@ function lookup(data, onUndefined) { 'locales=' + JSON.stringify(locales) + ', ' + 'description=' + JSON.stringify(data)); return '(No description)'; } return l10n.lookup(onUndefined); } + /** * The command object is mostly just setup around a commandSpec (as passed to * #addCommand()). */ function Command(commandSpec) { Object.keys(commandSpec).forEach(function(key) { this[key] = commandSpec[key]; }, this); @@ -2336,21 +2424,28 @@ function Parameter(paramSpec, command, g } } catch (ex) { console.error('In ' + this.command.name + '/' + this.name + ': ' + ex); } } - // Some typed (boolean, array) have a non 'undefined' blank value. Give the + // Some types (boolean, array) have a non 'undefined' blank value. Give the // type a chance to override the default defaultValue of undefined if (this.defaultValue === undefined) { this.defaultValue = this.type.getBlank().value; } + + // All parameters that can only be set via a named parameter must have a + // non-undefined default value + if (!this.isPositionalAllowed && this.defaultValue === undefined) { + console.error('In ' + this.command.name + '/' + this.name + + ': Missing defaultValue for optional parameter.'); + } } /** * Does the given name uniquely identify this param (among the other params * in this command) * @param name The name to check */ Parameter.prototype.isKnownAs = function(name) { @@ -4853,30 +4948,32 @@ define('gcli/ui/domtemplate', ['require' * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/cli', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/view', 'gcli/canon', 'gcli/promise', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) { +define('gcli/cli', ['require', 'exports', 'module' , 'gcli/util', 'gcli/ui/view', 'gcli/l10n', 'gcli/canon', 'gcli/promise', 'gcli/types', 'gcli/types/basic', 'gcli/argument'], function(require, exports, module) { var util = require('gcli/util'); var view = require('gcli/ui/view'); +var l10n = require('gcli/l10n'); var canon = require('gcli/canon'); var Promise = require('gcli/promise').Promise; var Status = require('gcli/types').Status; var Conversion = require('gcli/types').Conversion; var ArrayType = require('gcli/types/basic').ArrayType; var StringType = require('gcli/types/basic').StringType; var BooleanType = require('gcli/types/basic').BooleanType; +var NumberType = require('gcli/types/basic').NumberType; var Argument = require('gcli/argument').Argument; var ArrayArgument = require('gcli/argument').ArrayArgument; var NamedArgument = require('gcli/argument').NamedArgument; var TrueNamedArgument = require('gcli/argument').TrueNamedArgument; var MergedArgument = require('gcli/argument').MergedArgument; var ScriptArgument = require('gcli/argument').ScriptArgument; @@ -4911,39 +5008,32 @@ exports.shutdown = function() { * <li>onAssignmentChange: Either the value or the text has changed. It is * likely that any UI component displaying this argument will need to be * updated. * The event object looks like: * <tt>{ assignment: ..., conversion: ..., oldConversion: ... }</tt> * @constructor */ function Assignment(param, paramIndex) { + // The parameter that we are assigning to this.param = param; + + this.conversion = undefined; + + // The index of this parameter in the parent Requisition. paramIndex === -1 + // is the command assignment although this should not be relied upon, it is + // better to test param instanceof CommandAssignment this.paramIndex = paramIndex; + this.onAssignmentChange = util.createEvent('Assignment.onAssignmentChange'); this.setBlank(); } /** - * The parameter that we are assigning to - * @readonly - */ -Assignment.prototype.param = undefined; - -Assignment.prototype.conversion = undefined; - -/** - * The index of this parameter in the parent Requisition. paramIndex === -1 - * is the command assignment although this should not be relied upon, it is - * better to test param instanceof CommandAssignment - */ -Assignment.prototype.paramIndex = undefined; - -/** * Easy accessor for conversion.arg. * This is a read-only property because writes to arg should be done through * the 'conversion' property. */ Object.defineProperty(Assignment.prototype, 'arg', { get: function() { return this.conversion.arg; }, @@ -5016,17 +5106,17 @@ Assignment.prototype.setBlank = function * Argument of '' if needed. */ Assignment.prototype.ensureVisibleArgument = function() { // It isn't clear if we should be sending events from this method. // It should only be called when structural changes are happening in which // case we're going to ignore the event anyway. But on the other hand // perhaps this function shouldn't need to know how it is used, and should // do the inefficient thing. - if (!this.conversion.arg.isBlank()) { + if (this.conversion.arg.type !== 'BlankArgument') { return false; } var arg = this.conversion.arg.beget('', { prefixSpace: this.param instanceof CommandAssignment }); this.conversion = this.param.type.parse(arg); this.conversion.assign(this); @@ -5038,23 +5128,23 @@ Assignment.prototype.ensureVisibleArgume * Work out what the status of the current conversion is which involves looking * not only at the conversion, but also checking if data has been provided * where it should. * @param arg For assignments with multiple args (e.g. array assignments) we * can narrow the search for status to a single argument. */ Assignment.prototype.getStatus = function(arg) { if (this.param.isDataRequired && !this.conversion.isDataProvided()) { - return Status.ERROR; + return Status.INCOMPLETE; } // Selection/Boolean types with a defined range of values will say that // '' is INCOMPLETE, but the parameter may be optional, so we don't ask // if the user doesn't need to enter something and hasn't done so. - if (!this.param.isDataRequired && this.arg.isBlank()) { + if (!this.param.isDataRequired && this.arg.type === 'BlankArgument') { return Status.VALID; } return this.conversion.getStatus(arg); }; /** * Replace the current value with the lower value if such a concept exists. @@ -5084,16 +5174,36 @@ Assignment.prototype.increment = functio /** * Helper when we're rebuilding command lines. */ Assignment.prototype.toString = function() { return this.conversion.toString(); }; +/** + * For test/debug use only. The output from this function is subject to wanton + * random change without notice, and should not be relied upon to even exist + * at some later date. + */ +Object.defineProperty(Assignment.prototype, '_summaryJson', { + get: function() { + return { + param: this.param.name + '/' + this.param.type.name, + defaultValue: this.param.defaultValue, + arg: this.conversion.arg._summaryJson, + value: this.value, + message: this.getMessage(), + status: this.getStatus().toString(), + predictionCount: this.getPredictions().length + }; + }, + enumerable: true +}); + exports.Assignment = Assignment; /** * How to dynamically execute JavaScript code */ var customEval = eval; @@ -5177,41 +5287,37 @@ CommandAssignment.prototype.getStatus = }; exports.CommandAssignment = CommandAssignment; /** * Special assignment used when ignoring parameters that don't have a home */ -function UnassignedAssignment() { +function UnassignedAssignment(requisition, arg, isIncompleteName) { this.param = new canon.Parameter({ name: '__unassigned', - type: 'string' + description: l10n.lookup('cliOptions'), + type: { + name: 'param', + requisition: requisition, + isIncompleteName: isIncompleteName + }, }); this.paramIndex = -1; this.onAssignmentChange = util.createEvent('UnassignedAssignment.onAssignmentChange'); - this.setBlank(); + this.conversion = this.param.type.parse(arg); + this.conversion.assign(this); } UnassignedAssignment.prototype = Object.create(Assignment.prototype); UnassignedAssignment.prototype.getStatus = function(arg) { - return Status.ERROR; -}; - -UnassignedAssignment.prototype.setUnassigned = function(args) { - if (!args || args.length === 0) { - this.setBlank(); - } - else { - var conversion = this.param.type.parse(new MergedArgument(args)); - this.setConversion(conversion); - } + return this.conversion.getStatus(); }; /** * A Requisition collects the information needed to execute a command. * * (For a definition of the term, see http://en.wikipedia.org/wiki/Requisition) * This term is used because carries the notion of a work-flow, or process to @@ -5263,17 +5369,17 @@ function Requisition(environment, doc) { // The count of assignments. Excludes the commandAssignment this.assignmentCount = 0; // Used to store cli arguments in the order entered on the cli this._args = []; // Used to store cli arguments that were not assigned to parameters - this._unassigned = new UnassignedAssignment(); + this._unassigned = []; // Temporarily set this to true to prevent _assignmentChanged resetting // argument positions this._structuralChangeInProgress = false; this.commandAssignment.onAssignmentChange.add(this._commandAssignmentChanged, this); this.commandAssignment.onAssignmentChange.add(this._assignmentChanged, this); @@ -5426,17 +5532,17 @@ Requisition.prototype.cloneAssignments = * The overall status is the most severe status. * There is no such thing as an INCOMPLETE overall status because the * definition of INCOMPLETE takes into account the cursor position to say 'this * isn't quite ERROR because the user can fix it by typing', however overall, * this is still an error status. */ Requisition.prototype.getStatus = function() { var status = Status.VALID; - if (!this._unassigned.arg.isBlank()) { + if (this._unassigned.length !== 0) { return Status.ERROR; } this.getAssignments(true).forEach(function(assignment) { var assignStatus = assignment.getStatus(); if (assignStatus > status) { status = assignStatus; } }, this); @@ -5473,16 +5579,38 @@ Requisition.prototype.getAssignments = f } Object.keys(this._assignments).forEach(function(name) { assignments.push(this.getAssignment(name)); }, this); return assignments; }; /** + * Alter the given assignment using the given arg. This function is better than + * calling assignment.setConversion(assignment.param.type.parse(arg)) because + * it adjusts the args in this requisition to keep things up to date + */ +Requisition.prototype.setAssignment = function(assignment, arg) { + var originalArg = assignment.arg; + var conversion = assignment.param.type.parse(arg); + assignment.setConversion(conversion); + + // If this argument isn't assigned to anything (i.e. it was created by + // assignment.setBlank) we need to add it into the _args array so + // requisition.toString can make sense + if (originalArg.type === 'BlankArgument') { + this._args.push(arg); + } + else { + var index = this._args.indexOf(originalArg); + this._args[index] = conversion.arg; + } +}; + +/** * Reset all the assignments to their default values */ Requisition.prototype.setBlankArguments = function() { this.getAssignments().forEach(function(assignment) { assignment.setBlank(); }, this); }; @@ -5505,25 +5633,17 @@ Requisition.prototype.complete = functio var predictions = assignment.conversion.getPredictions(); if (predictions.length > 0) { this.onTextChange.holdFire(); var prediction = assignment.conversion.getPredictionAt(predictionChoice); // Mutate this argument to hold the completion var arg = assignment.arg.beget(prediction.name); - var conversion = assignment.param.type.parse(arg); - assignment.setConversion(conversion); - - // If this argument isn't assigned to anything (i.e. it was created by - // assignment.setBlank) we need to add it into the _args array so - // requisition.toString can make sense - if (this._args.indexOf(arg) === -1) { - this._args.push(arg); - } + this.setAssignment(assignment, arg); if (prediction.incomplete) { // This is the easy case - the prediction is incomplete - no need to add // any spaces return; } // The prediction reported !incomplete, which means it's complete so we @@ -5537,46 +5657,40 @@ Requisition.prototype.complete = functio // Also if there is already a space in those positions, don't add another var nextIndex = assignment.paramIndex + 1; var nextAssignment = this.getAssignment(nextIndex); if (nextAssignment) { // Add a space onto the next argument (if there isn't one there already) var nextArg = nextAssignment.conversion.arg; if (nextArg.prefix.charAt(0) !== ' ') { - nextArg.prefix = ' ' + nextArg.prefix; - var nextConversion = nextAssignment.param.type.parse(nextArg); - nextAssignment.setConversion(nextConversion); - - // If this argument isn't assigned to anything (i.e. it was created by - // assignment.setBlank) we need to add it into the _args array so - // requisition.toString can make sense - if (this._args.indexOf(nextArg) === -1) { - this._args.push(nextArg); - } + nextArg = new Argument(nextArg.text, ' ' + nextArg.prefix, nextArg.suffix); + this.setAssignment(nextAssignment, nextArg); } } else { // There is no next argument, this must be the last assignment, so just // add the space to the prefix of this argument - var conversion = assignment.conversion; - var arg = conversion.arg; + arg = assignment.conversion.arg; if (arg.suffix.charAt(arg.suffix.length - 1) !== ' ') { - arg.suffix = arg.suffix + ' '; - - // It's tempting to think - "we're calling setConversion twice in one + // It's tempting to think - "we're calling setAssignment twice in one // call to complete, the first time to complete the text, the second // to add a space, why not save the event cascade and do it once" // However if we're setting up the command, the number of parameters // changes as a result, so our call to getAssignment(nextIndex) will // produce the wrong answer - assignment.setConversion(conversion); + arg = new Argument(arg.text, arg.prefix, arg.suffix + ' '); + this.setAssignment(assignment, arg); } } + if (assignment instanceof UnassignedAssignment) { + this.update(this.toString()); + } + this.onTextChange(); this.onTextChange.resumeFire(); } }; /** * Extract a canonical version of the input */ @@ -5672,19 +5786,26 @@ Requisition.prototype.toString = functio * the end don't need a space prefix. * While this is quite a niche function, it has 2 benefits: * - it's more correct because we can distinguish between final whitespace that * is part of an unclosed string, and parameter separating whitespace. * - also it's faster than toString() the whole thing and checking the end char * @return true iff the last character is interpreted as parameter separating * whitespace */ -Requisition.prototype.typedEndsWithWhitespace = function() { +Requisition.prototype.typedEndsWithSeparator = function() { + // This is not as easy as doing (this.toString().slice(-1) === ' ') + // See the doc comments above; We're checking for separators, not spaces if (this._args) { - return this._args.slice(-1)[0].suffix.slice(-1) === ' '; + var lastArg = this._args.slice(-1)[0]; + if (lastArg.suffix.slice(-1) === ' ') { + return true; + } + return lastArg.text === '' && lastArg.suffix === '' + && lastArg.prefix.slice(-1) === ' '; } return this.toCanonicalString().slice(-1) === ' '; }; /** * Return an array of Status scores so we can create a marked up * version of the command line input. @@ -5705,18 +5826,26 @@ Requisition.prototype.getInputStatusMark for (var i = 0; i < argTraces.length; i++) { var argTrace = argTraces[i]; var arg = argTrace.arg; var status = Status.VALID; if (argTrace.part === 'text') { status = arg.assignment.getStatus(arg); // Promote INCOMPLETE to ERROR ... if (status === Status.INCOMPLETE) { - // If the cursor is not in a position to be able to complete it - if (arg !== cTrace.arg || cTrace.part !== 'text') { + // If the cursor is in the prefix or suffix of an argument then we + // don't consider it in the argument for the purposes of preventing + // the escalation to ERROR. However if this is a NamedArgument, then we + // allow the suffix (as space between 2 parts of the argument) to be in. + // We use arg.assignment.arg not arg because we're looking at the arg + // that got put into the assignment not as returned by tokenize() + var isNamed = (cTrace.arg.assignment.arg.type === 'NamedArgument'); + var isInside = cTrace.part === 'text' || + (isNamed && cTrace.part === 'suffix'); + if (arg.assignment !== cTrace.arg.assignment || !isInside) { // And if we're not in the command if (!(arg.assignment instanceof CommandAssignment)) { status = Status.ERROR; } } } } @@ -5764,40 +5893,43 @@ Requisition.prototype.getAssignmentAt = // prefix and text are clearly part of the argument for (j = 0; j < arg.prefix.length; j++) { assignForPos.push(assignment); } for (j = 0; j < arg.text.length; j++) { assignForPos.push(assignment); } - // suffix looks forwards - if (this._args.length > i + 1) { + // suffix is part of the argument only if this is a named parameter, + // otherwise it looks forwards + if (arg.assignment.arg.type === 'NamedArgument') { + // leave the argument as it is + } + else if (this._args.length > i + 1) { // first to the next argument assignment = this._args[i + 1].assignment; } - else if (assignment && - assignment.paramIndex + 1 < this.assignmentCount) { + else if (assignment && assignment.paramIndex + 1 < this.assignmentCount) { // then to the next assignment assignment = this.getAssignment(assignment.paramIndex + 1); } for (j = 0; j < arg.suffix.length; j++) { assignForPos.push(assignment); } } // Possible shortcut, we don't really need to go through all the args // to work out the solution to this var reply = assignForPos[cursor - 1]; if (!reply) { throw new Error('Missing assignment.' + - ' cursor=' + cursor + ' text.length=' + this.toString().length); + ' cursor=' + cursor + ' text=' + this.toString()); } return reply; }; /** * Entry point for keyboard accelerators or anything else that wants to execute * a command. There are 3 ways to call <tt>exec()</tt>: @@ -5893,36 +6025,56 @@ Requisition.prototype.exec = function(in this.update(''); return output; }; /** * Called by the UI when ever the user interacts with a command line input * @param typed The contents of the input field - * <p>The general sequence is: - * <ul> - * <li>_tokenize(): convert _typed into _parts - * <li>_split(): convert _parts into _command and _unparsedArgs - * <li>_assign(): convert _unparsedArgs into requisition - * </ul> */ Requisition.prototype.update = function(typed) { this._structuralChangeInProgress = true; this._args = this._tokenize(typed); var args = this._args.slice(0); // i.e. clone this._split(args); this._assign(args); this._structuralChangeInProgress = false; this.onTextChange(); }; /** + * For test/debug use only. The output from this function is subject to wanton + * random change without notice, and should not be relied upon to even exist + * at some later date. + */ +Object.defineProperty(Requisition.prototype, '_summaryJson', { + get: function() { + var summary = { + $args: this._args.map(function(arg) { + return arg._summaryJson; + }), + _command: this.commandAssignment._summaryJson, + _unassigned: this._unassigned.forEach(function(assignment) { + return assignment._summaryJson; + }) + }; + + Object.keys(this._assignments).forEach(function(name) { + summary[name] = this.getAssignment(name)._summaryJson; + }.bind(this)); + + return summary; + }, + enumerable: true +}); + +/** * Requisition._tokenize() is a state machine. These are the states. */ var In = { /** * The last character was ' '. * Typing a ' ' character will not change the mode * Typing one of '"{ will change mode to SINGLE_Q, DOUBLE_Q or SCRIPT. * Anything else goes into SIMPLE mode. @@ -6156,17 +6308,17 @@ function isSimple(typed) { * Looks in the canon for a command extension that matches what has been * typed at the command line. */ Requisition.prototype._split = function(args) { // Handle the special case of the user typing { javascript(); } // We use the hidden 'eval' command directly rather than shift()ing one of // the parameters, and parse()ing it. var conversion; - if (args[0] instanceof ScriptArgument) { + if (args[0].type === 'ScriptArgument') { // Special case: if the user enters { console.log('foo'); } then we need to // use the hidden 'eval' command conversion = new Conversion(evalCommand, new ScriptArgument()); this.commandAssignment.setConversion(conversion); return; } var argsUsed = 1; @@ -6197,48 +6349,57 @@ Requisition.prototype._split = function( for (var i = 0; i < argsUsed; i++) { args.shift(); } // This could probably be re-written to consume args as we go }; /** + * Add all the passed args to the list of unassigned assignments. + */ +Requisition.prototype._addUnassignedArgs = function(args) { + args.forEach(function(arg) { + this._unassigned.push(new UnassignedAssignment(this, arg, false)); + }.bind(this)); +}; + +/** * Work out which arguments are applicable to which parameters. */ Requisition.prototype._assign = function(args) { + this._unassigned = []; + if (!this.commandAssignment.value) { - this._unassigned.setUnassigned(args); + this._addUnassignedArgs(args); return; } if (args.length === 0) { this.setBlankArguments(); - this._unassigned.setBlank(); return; } // Create an error if the command does not take parameters, but we have // been given them ... if (this.assignmentCount === 0) { - this._unassigned.setUnassigned(args); + this._addUnassignedArgs(args); return; } // Special case: if there is only 1 parameter, and that's of type // text, then we put all the params into the first param if (this.assignmentCount === 1) { var assignment = this.getAssignment(0); if (assignment.param.type instanceof StringType) { var arg = (args.length === 1) ? args[0] : new MergedArgument(args); var conversion = assignment.param.type.parse(arg); assignment.setConversion(conversion); - this._unassigned.setBlank(); return; } } // Positional arguments can still be specified by name, but if they are // then we need to ignore them when working them out positionally var names = this.getParameterNames(); @@ -6258,17 +6419,17 @@ Requisition.prototype._assign = function }); // boolean parameters don't have values, default to false if (assignment.param.type instanceof BooleanType) { arg = new TrueNamedArgument(null, arg); } else { var valueArg = null; - if (i + 1 >= args.length) { + if (i + 1 <= args.length) { valueArg = args.splice(i, 1)[0]; } arg = new NamedArgument(arg, valueArg); } if (assignment.param.type instanceof ArrayType) { var arrayArg = arrayArgs[assignment.param.name]; if (!arrayArg) { @@ -6307,33 +6468,47 @@ Requisition.prototype._assign = function if (!arrayArg) { arrayArg = new ArrayArgument(); arrayArgs[assignment.param.name] = arrayArg; } arrayArg.addArguments(args); args = []; } else { - var arg = (args.length > 0) ? - args.splice(0, 1)[0] : - new Argument(); - - var conversion = assignment.param.type.parse(arg); - assignment.setConversion(conversion); + if (args.length === 0) { + assignment.setBlank(); + } + else { + var arg = args.splice(0, 1)[0]; + // --foo and -f are named parameters, -4 is a number. So '-' is either + // the start of a named parameter or a number depending on the context + var isIncompleteName = assignment.param.type instanceof NumberType ? + /-[-a-zA-Z_]/.test(arg.text) : + arg.text.charAt(0) === '-'; + + if (isIncompleteName) { + this._unassigned.push(new UnassignedAssignment(this, arg, true)); + } + else { + var conversion = assignment.param.type.parse(arg); + assignment.setConversion(conversion); + } + } } }, this); // Now we need to assign the array argument (if any) Object.keys(arrayArgs).forEach(function(name) { var assignment = this.getAssignment(name); var conversion = assignment.param.type.parse(arrayArgs[name]); assignment.setConversion(conversion); }, this); - this._unassigned.setUnassigned(args); + // What's left is can't be assigned, but we need to extract + this._addUnassignedArgs(args); }; exports.Requisition = Requisition; /** * A simple object to hold information about the output of a command */ function Output(options) { @@ -7690,22 +7865,24 @@ JavascriptField.DEFAULT_VALUE = '__Javas * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gcli/ui/fields/menu', ['require', 'exports', 'module' , 'gcli/util', 'gcli/argument', 'gcli/canon', 'gcli/ui/domtemplate', 'text!gcli/ui/fields/menu.css', 'text!gcli/ui/fields/menu.html'], function(require, exports, module) { +define('gcli/ui/fields/menu', ['require', 'exports', 'module' , 'gcli/util', 'gcli/l10n', 'gcli/argument', 'gcli/types', 'gcli/canon', 'gcli/ui/domtemplate', 'text!gcli/ui/fields/menu.css', 'text!gcli/ui/fields/menu.html'], function(require, exports, module) { var util = require('gcli/util'); +var l10n = require('gcli/l10n'); var Argument = require('gcli/argument').Argument; +var Conversion = require('gcli/types').Conversion; var canon = require('gcli/canon'); var domtemplate = require('gcli/ui/domtemplate'); var menuCss = require('text!gcli/ui/fields/menu.css'); var menuHtml = require('text!gcli/ui/fields/menu.html'); @@ -7746,16 +7923,21 @@ function Menu(options) { // Contains the items that should be displayed this.items = null; this.onItemClick = util.createEvent('Menu.onItemClick'); } /** + * Allow the template engine to get at localization strings + */ +Menu.prototype.l10n = l10n.propertyLookup; + +/** * Avoid memory leaks */ Menu.prototype.destroy = function() { delete this.element; delete this.template; delete this.document; }; @@ -7790,16 +7972,21 @@ Menu.prototype.show = function(items, ma }.bind(this)); } if (this.items.length === 0) { this.element.style.display = 'none'; return; } + if (this.items.length >= Conversion.maxPredictions) { + this.items.splice(-1); + this.items.hasMore = true; + } + var options = this.template.cloneNode(true); domtemplate.template(options, this, this.templateOptions); util.clearElement(this.element); this.element.appendChild(options); this.element.style.display = 'block'; }; @@ -7890,23 +8077,26 @@ Menu.prototype.setMaxHeight = function(h exports.Menu = Menu; }); define("text!gcli/ui/fields/menu.css", [], ""); define("text!gcli/ui/fields/menu.html", [], "\n" + - "<table class=\"gcli-menu-template\" aria-live=\"polite\">\n" + - " <tr class=\"gcli-menu-option\" foreach=\"item in ${items}\"\n" + - " onclick=\"${onItemClickInternal}\" title=\"${item.manual}\">\n" + - " <td class=\"gcli-menu-name\">${item.name}</td>\n" + - " <td class=\"gcli-menu-desc\">${item.description}</td>\n" + - " </tr>\n" + - "</table>\n" + + "<div>\n" + + " <table class=\"gcli-menu-template\" aria-live=\"polite\">\n" + + " <tr class=\"gcli-menu-option\" foreach=\"item in ${items}\"\n" + + " onclick=\"${onItemClickInternal}\" title=\"${item.manual}\">\n" + + " <td class=\"gcli-menu-name\">${item.name}</td>\n" + + " <td class=\"gcli-menu-desc\">${item.description}</td>\n" + + " </tr>\n" + + " </table>\n" + + " <div class=\"gcli-menu-more\" if=\"${items.hasMore}\">${l10n.fieldMenuMore}</div>\n" + + "</div>\n" + ""); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at @@ -9232,23 +9422,26 @@ Inputter.prototype.onKeyUp = function(ev // should select the incorrect part of the input for an easy fix } this._choice = null; return; } if (ev.keyCode === KeyEvent.DOM_VK_TAB && !ev.shiftKey) { + // Being able to complete 'nothing' is OK if there is some context, but + // when there is nothing on the command line it jsut looks bizarre. + var hasContents = (this.element.value.length > 0); // If the TAB keypress took the cursor from another field to this one, // then they get the keydown/keypress, and we get the keyup. In this // case we don't want to do any completion. // If the time of the keydown/keypress of TAB was close (i.e. within // 1 second) to the time of the keyup then we assume that we got them // both, and do the completion. - if (this.lastTabDownAt + 1000 > ev.timeStamp) { + if (hasContents && this.lastTabDownAt + 1000 > ev.timeStamp) { // It's possible for TAB to not change the input, in which case the // textChanged event will not fire, and the caret move will not be // processed. So we check that this is done first this._caretChange = Caret.TO_ARG_END; var inputState = this.getInputState(); this._processCaretChange(inputState); if (this._choice == null) { this._choice = 0; @@ -9502,185 +9695,147 @@ Completer.prototype.resized = function(e this.element.style.top = ev.top + 'px'; this.element.style.height = ev.height + 'px'; this.element.style.lineHeight = ev.height + 'px'; this.element.style.left = ev.left + 'px'; this.element.style.width = ev.width + 'px'; }; /** - * Is the completion given, a "strict" completion of the user inputted value? - * A completion is considered "strict" only if it the user inputted value is an - * exact prefix of the completion (ignoring leading whitespace) - */ -function isStrictCompletion(inputValue, completion) { - // Strip any leading whitespace from the user inputted value because the - // completion will never have leading whitespace. - inputValue = inputValue.replace(/^\s*/, ''); - // Strict: "ec" -> "echo" - // Non-Strict: "ls *" -> "ls foo bar baz" - return completion.indexOf(inputValue) === 0; -} - -/** * Bring the completion element up to date with what the requisition says */ Completer.prototype.update = function(ev) { if (ev && ev.choice != null) { this.choice = ev.choice; } - this._preTemplateUpdate(); - + + var data = this._getCompleterTemplateData(); var template = this.template.cloneNode(true); - domtemplate.template(template, this, { stack: 'completer.html' }); + domtemplate.template(template, data, { stack: 'completer.html' }); util.clearElement(this.element); while (template.hasChildNodes()) { this.element.appendChild(template.firstChild); } }; /** - * Update the state of a number of internal variables in preparation for - * templating. Some of these properties are interdependent, so it makes sense - * to do them in one go. - */ -Completer.prototype._preTemplateUpdate = function() { - this.input = this.inputter.getInputState(); - - this.directTabText = ''; - this.arrowTabText = ''; - - // What text should we display as the tab text, and should it be given as a - // '-> full' or as 'suffix' (which depends on if the completion is a strict - // completion or not) - if (this.input.typed.trim().length === 0) { - return; - } - + * Calculate the properties required by the template process for completer.html + */ +Completer.prototype._getCompleterTemplateData = function() { + var input = this.inputter.getInputState(); + + // directTabText is for when the current input is a prefix of the completion + // arrowTabText is for when we need to use an -> to show what will be used + var directTabText = ''; + var arrowTabText = ''; var current = this.inputter.assignment; - var prediction = current.conversion.getPredictionAt(this.choice); - if (!prediction) { - return; - } - - var tabText = prediction.name; - var existing = current.arg.text; - - if (existing === tabText) { - return; - } - - if (isStrictCompletion(existing, tabText) && - this.input.cursor.start === this.input.typed.length) { - // Display the suffix of the prediction as the completion - var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; - - this.directTabText = tabText.slice(existing.length - numLeadingSpaces); - } - else { - // Display the '-> prediction' at the end of the completer element - // These JS escapes are aka → the right arrow - this.arrowTabText = ' \u00a0\u21E5 ' + tabText; - } -}; - -/** - * A proxy to requisition.getInputStatusMarkup which converts space to - * in the string member (for HTML display) and converts status to an - * appropriate class name (i.e. lower cased, prefixed with gcli-in-) - */ -Object.defineProperty(Completer.prototype, 'statusMarkup', { - get: function() { - var markup = this.requisition.getInputStatusMarkup(this.input.cursor.start); - markup.forEach(function(member) { - member.string = member.string.replace(/ /g, '\u00a0'); // i.e. - member.className = 'gcli-in-' + member.status.toString().toLowerCase(); - }, this); - return markup; - }, - enumerable: true -}); - -/** - * The text for the 'jump to scratchpad' feature, or null if it is disabled - */ -Object.defineProperty(Completer.prototype, 'scratchLink', { - get: function() { - if (!this.scratchpad) { - return null; - } - var command = this.requisition.commandAssignment.value; - return command && command.name === '{' ? this.scratchpad.linkText : null; - }, - enumerable: true -}); - -/** - * Is the entered command a JS command with no closing '}'? - * TWEAK: This code should be considered for promotion to Requisition - */ -Object.defineProperty(Completer.prototype, 'unclosedJs', { - get: function() { - var command = this.requisition.commandAssignment.value; - var jsCommand = command && command.name === '{'; - var unclosedJs = jsCommand && - this.requisition.getAssignment(0).arg.suffix.indexOf('}') === -1; - return unclosedJs; - }, - enumerable: true -}); - -/** - * Accessor for the list of parameters to be filled in - */ -Object.defineProperty(Completer.prototype, 'emptyParameters', { - get: function() { - var typedEndSpace = this.requisition.typedEndsWithWhitespace(); - // Cache computed property - var directTabText = this.directTabText; - // If this is the first blank assignment we might not need a space prefix - // also we skip [param] text if we have directTabText, but only for the - // first blank param. - var firstBlankParam = true; - var params = []; - this.requisition.getAssignments().forEach(function(assignment) { - if (!assignment.param.isPositionalAllowed) { - return; + + if (input.typed.trim().length !== 0) { + var prediction = current.conversion.getPredictionAt(this.choice); + if (prediction) { + var tabText = prediction.name; + var existing = current.arg.text; + + if (existing !== tabText) { + // Decide to use directTabText or arrowTabText + // Strip any leading whitespace from the user inputted value because the + // tabText will never have leading whitespace. + var inputValue = existing.replace(/^\s*/, ''); + var isStrictCompletion = tabText.indexOf(inputValue) === 0; + if (isStrictCompletion && input.cursor.start === input.typed.length) { + // Display the suffix of the prediction as the completion + var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; + + directTabText = tabText.slice(existing.length - numLeadingSpaces); + } + else { + // Display the '-> prediction' at the end of the completer element + // These JS escapes are aka → the right arrow + arrowTabText = ' \u00a0\u21E5 ' + tabText; + } } - - if (!assignment.arg.isBlank()) { - if (directTabText !== '') { - firstBlankParam = false; - } - return; - } - - if (directTabText !== '' && firstBlankParam) { + } + } + + // statusMarkup is wrapper around requisition.getInputStatusMarkup converting + // space to in the string member (for HTML display) and status to an + // appropriate class name (i.e. lower cased, prefixed with gcli-in-) + var statusMarkup = this.requisition.getInputStatusMarkup(input.cursor.start); + statusMarkup.forEach(function(member) { + member.string = member.string.replace(/ /g, '\u00a0'); // i.e. + member.className = 'gcli-in-' + member.status.toString().toLowerCase(); + }, this); + + // Calculate the list of parameters to be filled in + var trailingSeparator = this.requisition.typedEndsWithSeparator(); + // We generate an array of emptyParameter markers for each positional + // parameter to the current command. + // Generally each emptyParameter marker begins with a space to separate it + // from whatever came before, unless what comes before ends in a space. + // Also if we've got a directTabText prediction or we're in a NamedParameter + // then we don't want any text for that parameter at all. + // The algorithm to add spaces needs to take this into account. + + var firstBlankParam = true; + var emptyParameters = []; + this.requisition.getAssignments().forEach(function(assignment) { + if (!assignment.param.isPositionalAllowed) { + return; + } + if (current.arg.type === 'NamedArgument') { + return; + } + + if (assignment.arg.toString().trim() !== '') { + if (directTabText !== '') { firstBlankParam = false; - return; } - - var text = (assignment.param.isDataRequired) ? - '<' + assignment.param.name + '>' : - '[' + assignment.param.name + ']'; - - // Add a space if we don't have one at the end of the input or if - // this isn't the first param we've mentioned - if (!typedEndSpace || !firstBlankParam) { - text = '\u00a0' + text; // i.e. - } - + return; + } + + if (directTabText !== '' && firstBlankParam) { firstBlankParam = false; - params.push(text); - }.bind(this)); - return params; - }, - enumerable: true -}); + return; + } + + var text = (assignment.param.isDataRequired) ? + '<' + assignment.param.name + '>' : + '[' + assignment.param.name + ']'; + + // Add a space if we don't have one at the end of the input or if + // this isn't the first param we've mentioned + if (!trailingSeparator || !firstBlankParam) { + text = '\u00a0' + text; // i.e. + } + + firstBlankParam = false; + emptyParameters.push(text); + }.bind(this)); + + var command = this.requisition.commandAssignment.value; + var jsCommand = command && command.name === '{'; + + // Is the entered command a JS command with no closing '}'? + // TWEAK: This code should be considered for promotion to Requisition + var unclosedJs = jsCommand && + this.requisition.getAssignment(0).arg.suffix.indexOf('}') === -1; + + // The text for the 'jump to scratchpad' feature, or '' if it is disabled + var link = this.scratchpad && jsCommand ? this.scratchpad.linkText : ''; + + return { + statusMarkup: statusMarkup, + directTabText: directTabText, + emptyParameters: emptyParameters, + arrowTabText: arrowTabText, + unclosedJs: unclosedJs, + scratchLink: link + }; +}; exports.Completer = Completer; }); define("text!gcli/ui/completer.html", [], "\n" + "<description>\n" + " <loop foreach=\"member in ${statusMarkup}\">\n" +
--- a/browser/devtools/commandline/test/browser_gcli_web.js +++ b/browser/devtools/commandline/test/browser_gcli_web.js @@ -80,19 +80,21 @@ registerCleanupFunction(function tearDow * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite'], function(require, exports, module) { +define('gclitest/index', ['require', 'exports', 'module' , 'gclitest/suite', 'gcli/settings', 'gcli/ui/display'], function(require, exports, module) { var examiner = require('gclitest/suite').examiner; + var settings = require('gcli/settings'); + var Display = require('gcli/ui/display').Display; // A minimum fake dom to get us through the JS tests var fakeWindow = { isFake: true, document: { title: 'Fake DOM' } }; fakeWindow.window = fakeWindow; examiner.defaultOptions = { @@ -113,27 +115,73 @@ define('gclitest/index', ['require', 'ex * A reduced set of tests will run if left undefined * - detailedResultLog (default=false) do we output a test summary to * |console.log| on test completion. * - hideExec (default=false) Set the |hidden| property in calls to * |requisition.exec()| which prevents the display from becoming messed up, * however use of hideExec restricts the set of tests that are run */ exports.run = function(options) { - examiner.run(options || {}); + options = options || {}; + examiner.mergeDefaultOptions(options); + + examiner.reset(); + examiner.run(options); // A better set of default than those specified above, come from the set // that are passed to run(). examiner.defaultOptions = { window: options.window, display: options.display, hideExec: options.hideExec }; }; + /** + * This is the equivalent of gcli/index.createDisplay() except it: + * - Sets window.display: to actual Display object (not the thin proxy + * returned by gcli/index.createDisplay() and this function) + * - Registers all the test commands, and provides a function to re-register + * them - window.testCommands() (running the test suite un-registers them) + * - Runs the unit tests automatically on startup + * - Registers a 'test' command to re-run the unit tests + */ + exports.createDisplay = function(options) { + options = options || {}; + if (options.settings != null) { + settings.setDefaults(options.settings); + } + + window.display = new Display(options); + var requisition = window.display.requisition; + + exports.run({ + window: window, + display: window.display, + hideExec: true + }); + + window.testCommands = function() { + require([ 'gclitest/mockCommands' ], function(mockCommands) { + mockCommands.setup(); + }); + }; + window.testCommands(); + + return { + /** + * The exact shape of the object returned by exec is likely to change in + * the near future. If you do use it, please expect your code to break. + */ + exec: requisition.exec.bind(requisition), + update: requisition.update.bind(requisition), + destroy: window.display.destroy.bind(window.display) + }; + }; + }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -141,33 +189,34 @@ define('gclitest/index', ['require', 'ex * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], function(require, exports, module) { +define('gclitest/suite', ['require', 'exports', 'module' , 'gcli/index', 'test/examiner', 'gclitest/testCanon', 'gclitest/testCli', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testPref', 'gclitest/testRequire', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil'], function(require, exports, module) { // We need to make sure GCLI is initialized before we begin testing it require('gcli/index'); var examiner = require('test/examiner'); // It's tempting to want to unify these strings and make addSuite() do the // call to require(), however that breaks the build system which looks for // the strings passed to require examiner.addSuite('gclitest/testCanon', require('gclitest/testCanon')); examiner.addSuite('gclitest/testCli', require('gclitest/testCli')); examiner.addSuite('gclitest/testCompletion', require('gclitest/testCompletion')); examiner.addSuite('gclitest/testExec', require('gclitest/testExec')); examiner.addSuite('gclitest/testHelp', require('gclitest/testHelp')); examiner.addSuite('gclitest/testHistory', require('gclitest/testHistory')); examiner.addSuite('gclitest/testInputter', require('gclitest/testInputter')); + examiner.addSuite('gclitest/testIncomplete', require('gclitest/testIncomplete')); examiner.addSuite('gclitest/testIntro', require('gclitest/testIntro')); examiner.addSuite('gclitest/testJs', require('gclitest/testJs')); examiner.addSuite('gclitest/testKeyboard', require('gclitest/testKeyboard')); examiner.addSuite('gclitest/testPref', require('gclitest/testPref')); examiner.addSuite('gclitest/testRequire', require('gclitest/testRequire')); examiner.addSuite('gclitest/testResource', require('gclitest/testResource')); examiner.addSuite('gclitest/testScratchpad', require('gclitest/testScratchpad')); examiner.addSuite('gclitest/testSettings', require('gclitest/testSettings')); @@ -226,30 +275,28 @@ examiner.addSuite = function(name, suite * with the specified options in |run()|. */ examiner.defaultOptions = {}; /** * Add properties to |options| from |examiner.defaultOptions| when |options| * does not have a value for a given name. */ -function mergeDefaultOptions(options) { +examiner.mergeDefaultOptions = function(options) { Object.keys(examiner.defaultOptions).forEach(function(name) { if (options[name] == null) { options[name] = examiner.defaultOptions[name]; } }); -} +}; /** * Run the tests defined in the test suite synchronously */ examiner.run = function(options) { - mergeDefaultOptions(options); - Object.keys(examiner.suites).forEach(function(suiteName) { var suite = examiner.suites[suiteName]; suite.run(options); }.bind(this)); if (options.detailedResultLog) { examiner.detailedResultLog(); } @@ -259,17 +306,16 @@ examiner.run = function(options) { return examiner.suites; }; /** * Run all the tests asynchronously */ examiner.runAsync = function(options, callback) { - mergeDefaultOptions(options); this._runAsyncInternal(0, options, callback); }; /** * Run all the test suits asynchronously */ examiner._runAsyncInternal = function(i, options, callback) { if (i >= Object.keys(examiner.suites).length) { @@ -298,16 +344,25 @@ examiner.toRemote = function() { summary: { checks: this.checks, status: this.status } }; }; /** + * Reset all the tests to their original state + */ +examiner.reset = function() { + Object.keys(examiner.suites).forEach(function(suiteName) { + examiner.suites[suiteName].reset(); + }, this); +}; + +/** * The number of checks in this set of test suites is the sum of the checks in * the test suites. */ Object.defineProperty(examiner, 'checks', { get: function() { return Object.keys(examiner.suites).reduce(function(current, suiteName) { return current + examiner.suites[suiteName].checks; }.bind(this), 0); @@ -371,27 +426,35 @@ function Suite(suiteName, suite) { if (testName !== 'setup' && testName !== 'shutdown') { var test = new Test(this, testName, suite[testName]); this.tests[testName] = test; } }.bind(this)); } /** + * Reset all the tests to their original state + */ +Suite.prototype.reset = function() { + Object.keys(this.tests).forEach(function(testName) { + this.tests[testName].reset(); + }, this); +}; + +/** * Run all the tests in this suite synchronously */ Suite.prototype.run = function(options) { if (!this._setup(options)) { return; } Object.keys(this.tests).forEach(function(testName) { - var test = this.tests[testName]; - test.run(options); - }.bind(this)); + this.tests[testName].run(options); + }, this); this._shutdown(options); }; /** * Run all the tests in this suite asynchronously */ Suite.prototype.runAsync = function(options, callback) { @@ -536,30 +599,39 @@ function Test(suite, name, func) { this.title = name.replace(/^test/, '').replace(/([A-Z])/g, ' $1'); this.failures = []; this.status = stati.notrun; this.checks = 0; } /** + * Reset the test to its original state + */ +Test.prototype.reset = function() { + this.failures = []; + this.status = stati.notrun; + this.checks = 0; +}; + +/** * Run just a single test */ Test.prototype.run = function(options) { assert.currentTest = this; this.status = stati.executing; this.failures = []; this.checks = 0; try { this.func.apply(this.suite, [ options ]); } catch (ex) { assert.ok(false, '' + ex); - console.error(ex); + console.error(ex.stack); if ((options.isNode || options.isFirefox) && ex.stack) { console.error(ex.stack); } } if (this.status === stati.executing) { this.status = stati.pass; } @@ -765,20 +837,32 @@ define('gclitest/testCanon', ['require', * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert'], function(require, exports, module) { +define('gclitest/helpers', ['require', 'exports', 'module' , 'test/assert', 'gcli/util'], function(require, exports, module) { var test = require('test/assert'); +var util = require('gcli/util'); + + +var cachedOptions = undefined; + +exports.setup = function(opts) { + cachedOptions = opts; +}; + +exports.shutdown = function(opts) { + cachedOptions = undefined; +}; /** * Check that we can parse command input. * Doesn't execute the command, just checks that we grok the input properly: * * helpers.status({ * // Test inputs * typed: "ech", // Required @@ -787,68 +871,243 @@ var test = require('test/assert'); * // Thing to check * status: "INCOMPLETE", // One of "VALID", "ERROR", "INCOMPLETE" * emptyParameters: [ "<message>" ], // Still to type * directTabText: "o", // Simple completion text * arrowTabText: "", // When the completion is not an extension * markup: "VVVIIIEEE", // What state should the error markup be in * }); */ -exports.status = function(options, tests) { +exports.status = function(options, checks) { var requisition = options.display.requisition; var inputter = options.display.inputter; var completer = options.display.completer; - if (tests.typed) { - inputter.setInput(tests.typed); + if (checks.typed) { + inputter.setInput(checks.typed); } else { - test.ok(false, "Missing typed for " + JSON.stringify(tests)); + test.ok(false, "Missing typed for " + JSON.stringify(checks)); return; } - if (tests.cursor) { - inputter.setCursor(tests.cursor); + if (checks.cursor) { + inputter.setCursor(checks.cursor); } - if (tests.status) { - test.is(requisition.getStatus().toString(), tests.status, - "status for " + tests.typed); + if (checks.status) { + test.is(requisition.getStatus().toString(), + checks.status, + "status for " + checks.typed); } - if (tests.emptyParameters != null) { - var realParams = completer.emptyParameters; - test.is(realParams.length, tests.emptyParameters.length, - 'emptyParameters.length for \'' + tests.typed + '\''); - - if (realParams.length === tests.emptyParameters.length) { + var data = completer._getCompleterTemplateData(); + if (checks.emptyParameters != null) { + var realParams = data.emptyParameters; + test.is(realParams.length, + checks.emptyParameters.length, + 'emptyParameters.length for \'' + checks.typed + '\''); + + if (realParams.length === checks.emptyParameters.length) { for (var i = 0; i < realParams.length; i++) { - test.is(realParams[i].replace(/\u00a0/g, ' '), tests.emptyParameters[i], - 'emptyParameters[' + i + '] for \'' + tests.typed + '\''); + test.is(realParams[i].replace(/\u00a0/g, ' '), + checks.emptyParameters[i], + 'emptyParameters[' + i + '] for \'' + checks.typed + '\''); } } } - if (tests.markup) { - var cursor = tests.cursor ? tests.cursor.start : tests.typed.length; + if (checks.markup) { + var cursor = checks.cursor ? checks.cursor.start : checks.typed.length; var statusMarkup = requisition.getInputStatusMarkup(cursor); var actualMarkup = statusMarkup.map(function(s) { return Array(s.string.length + 1).join(s.status.toString()[0]); }).join(''); - test.is(tests.markup, actualMarkup, 'markup for ' + tests.typed); + + test.is(checks.markup, + actualMarkup, + 'markup for ' + checks.typed); + } + + if (checks.directTabText) { + test.is(data.directTabText, + checks.directTabText, + 'directTabText for \'' + checks.typed + '\''); + } + + if (checks.arrowTabText) { + test.is(' \u00a0\u21E5 ' + checks.arrowTabText, + data.arrowTabText, + 'arrowTabText for \'' + checks.typed + '\''); + } +}; + +/** + * We're splitting status into setup() which alters the state of the system + * and check() which ensures that things are in the right place afterwards. + */ +exports.setInput = function(typed, cursor) { + cachedOptions.display.inputter.setInput(typed); + + if (cursor) { + cachedOptions.display.inputter.setCursor({ start: cursor, end: cursor }); + } +}; + +/** + * Simulate pressing TAB in the input field + */ +exports.pressTab = function() { + // requisition.complete({ start: 5, end: 5 }, 0); + + var fakeEvent = { + keyCode: util.KeyEvent.DOM_VK_TAB, + preventDefault: function() { }, + timeStamp: new Date().getTime() + }; + cachedOptions.display.inputter.onKeyDown(fakeEvent); + cachedOptions.display.inputter.onKeyUp(fakeEvent); +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * emptyParameters: Array of parameters still to type. e.g. [ "<message>" ] + * directTabText: Simple completion text + * arrowTabText: When the completion is not an extension (without arrow) + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +exports.check = function(checks) { + var requisition = cachedOptions.display.requisition; + var completer = cachedOptions.display.completer; + var actual = completer._getCompleterTemplateData(); + + if (checks.input) { + test.is(cachedOptions.display.inputter.element.value, + checks.input, + 'input'); + } + + if (checks.cursor) { + test.is(cachedOptions.display.inputter.element.selectionStart, + checks.cursor, + 'cursor'); + } + + if (checks.status) { + test.is(requisition.getStatus().toString(), + checks.status, + 'status'); } - if (tests.directTabText) { - test.is(completer.directTabText, tests.directTabText, - 'directTabText for \'' + tests.typed + '\''); + if (checks.markup) { + var cursor = cachedOptions.display.inputter.element.selectionStart; + var statusMarkup = requisition.getInputStatusMarkup(cursor); + var actualMarkup = statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + + test.is(checks.markup, + actualMarkup, + 'markup'); + } + + if (checks.emptyParameters) { + var actualParams = actual.emptyParameters; + test.is(actualParams.length, + checks.emptyParameters.length, + 'emptyParameters.length'); + + if (actualParams.length === checks.emptyParameters.length) { + for (var i = 0; i < actualParams.length; i++) { + test.is(actualParams[i].replace(/\u00a0/g, ' '), + checks.emptyParameters[i], + 'emptyParameters[' + i + ']'); + } + } + } + + if (checks.directTabText) { + test.is(actual.directTabText, + checks.directTabText, + 'directTabText'); + } + + if (checks.arrowTabText) { + test.is(actual.arrowTabText, + ' \u00a0\u21E5 ' + checks.arrowTabText, + 'arrowTabText'); } - if (tests.arrowTabText) { - test.is(completer.arrowTabText, ' \u00a0\u21E5 ' + tests.arrowTabText, - 'arrowTabText for \'' + tests.typed + '\''); + if (checks.args) { + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + test.ok(false, 'Unknown parameter: ' + paramName); + return; + } + + if (check.value) { + test.is(assignment.value, + check.value, + 'checkStatus value for ' + paramName); + } + + if (check.name) { + test.is(assignment.value.name, + check.name, + 'checkStatus name for ' + paramName); + } + + if (check.type) { + test.is(assignment.arg.type, + check.type, + 'checkStatus type for ' + paramName); + } + + if (check.arg) { + test.is(assignment.arg.toString(), + check.arg, + 'checkStatus arg for ' + paramName); + } + + if (check.status) { + test.is(assignment.getStatus().toString(), + check.status, + 'checkStatus status for ' + paramName); + } + + if (check.message) { + test.is(assignment.getMessage(), + check.message, + 'checkStatus message for ' + paramName); + } + }); } }; /** * Execute a command: * * helpers.exec({ * // Test inputs @@ -1163,45 +1422,46 @@ exports.testTsv = function() { test.is('number', typeof assign2.value); test.is(1, assignC.paramIndex); }; exports.testInvalid = function() { update({ typed: 'zxjq', cursor: { start: 4, end: 4 } }); test.is( 'EEEE', statuses); test.is('zxjq', requ.commandAssignment.arg.text); - test.is('', requ._unassigned.arg.text); + test.is(0, requ._unassigned.length); test.is(-1, assignC.paramIndex); update({ typed: 'zxjq ', cursor: { start: 5, end: 5 } }); test.is( 'EEEEV', statuses); test.is('zxjq', requ.commandAssignment.arg.text); - test.is('', requ._unassigned.arg.text); + test.is(0, requ._unassigned.length); test.is(-1, assignC.paramIndex); update({ typed: 'zxjq one', cursor: { start: 8, end: 8 } }); test.is( 'EEEEVEEE', statuses); test.is('zxjq', requ.commandAssignment.arg.text); - test.is('one', requ._unassigned.arg.text); + test.is(1, requ._unassigned.length); + test.is('one', requ._unassigned[0].arg.text); }; exports.testSingleString = function() { update({ typed: 'tsr', cursor: { start: 3, end: 3 } }); test.is( 'VVV', statuses); test.is(Status.ERROR, status); test.is('tsr', requ.commandAssignment.value.name); - test.ok(assign1.arg.isBlank()); + test.ok(assign1.arg.type === 'BlankArgument'); test.is(undefined, assign1.value); test.is(undefined, assign2); update({ typed: 'tsr ', cursor: { start: 4, end: 4 } }); test.is( 'VVVV', statuses); test.is(Status.ERROR, status); test.is('tsr', requ.commandAssignment.value.name); - test.ok(assign1.arg.isBlank()); + test.ok(assign1.arg.type === 'BlankArgument'); test.is(undefined, assign1.value); test.is(undefined, assign2); update({ typed: 'tsr h', cursor: { start: 5, end: 5 } }); test.is( 'VVVVV', statuses); test.is(Status.VALID, status); test.is('tsr', requ.commandAssignment.value.name); test.is('h', assign1.arg.text); @@ -1252,17 +1512,17 @@ exports.testSingleNumber = function() { test.is(undefined, assign1.value); }; exports.testElement = function(options) { update({ typed: 'tse', cursor: { start: 3, end: 3 } }); test.is( 'VVV', statuses); test.is(Status.ERROR, status); test.is('tse', requ.commandAssignment.value.name); - test.ok(assign1.arg.isBlank()); + test.ok(assign1.arg.type === 'BlankArgument'); test.is(undefined, assign1.value); if (!options.isNode) { update({ typed: 'tse :root', cursor: { start: 9, end: 9 } }); test.is( 'VVVVVVVVV', statuses); test.is(Status.VALID, status); test.is('tse', requ.commandAssignment.value.name); test.is(':root', assign1.arg.text); @@ -1598,17 +1858,18 @@ exports.tsu = { }; exports.tsn = { name: 'tsn' }; exports.tsnDif = { name: 'tsn dif', - params: [ { name: 'text', type: 'string' } ], + description: 'tsn dif', + params: [ { name: 'text', type: 'string', description: 'tsn dif text' } ], exec: createExec('tsnDif') }; exports.tsnExt = { name: 'tsn ext', params: [ { name: 'text', type: 'string' } ], exec: createExec('tsnExt') }; @@ -1667,29 +1928,52 @@ exports.tsm = { ], exec: createExec('tsm') }; exports.tsg = { name: 'tsg', description: 'a param group test', params: [ - { name: 'solo', type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] } }, + { + name: 'solo', + type: { name: 'selection', data: [ 'aaa', 'bbb', 'ccc' ] }, + description: 'solo param' + }, { group: 'First', params: [ - { name: 'txt1', type: 'string', defaultValue: null }, - { name: 'bool', type: 'boolean' } + { + name: 'txt1', + type: 'string', + defaultValue: null, + description: 'txt1 param' + }, + { + name: 'bool', + type: 'boolean', + description: 'bool param' + } ] }, { group: 'Second', params: [ - { name: 'txt2', type: 'string', defaultValue: 'd' }, - { name: 'num', type: { name: 'number', min: 40 }, defaultValue: 42 } + { + name: 'txt2', + type: 'string', + defaultValue: 'd', + description: 'txt2 param' + }, + { + name: 'num', + type: { name: 'number', min: 40 }, + defaultValue: 42, + description: 'num param' + } ] } ], exec: createExec('tsg') }; }); @@ -1704,211 +1988,312 @@ exports.tsg = { * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gclitest/testCompletion', ['require', 'exports', 'module' , 'test/assert', 'gclitest/mockCommands'], function(require, exports, module) { +define('gclitest/testCompletion', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) { var test = require('test/assert'); +var helpers = require('gclitest/helpers'); var mockCommands = require('gclitest/mockCommands'); -exports.setup = function() { +exports.setup = function(options) { mockCommands.setup(); -}; - -exports.shutdown = function() { - mockCommands.shutdown(); + helpers.setup(options); }; - -function type(typed, tests, options) { - var inputter = options.display.inputter; - var completer = options.display.completer; - - inputter.setInput(typed); - - if (tests.cursor) { - inputter.setCursor({ start: tests.cursor, end: tests.cursor }); - } - - if (tests.emptyParameters == null) { - tests.emptyParameters = []; - } - - var realParams = completer.emptyParameters; - test.is(tests.emptyParameters.length, realParams.length, - 'emptyParameters.length for \'' + typed + '\''); - - if (realParams.length === tests.emptyParameters.length) { - for (var i = 0; i < realParams.length; i++) { - test.is(tests.emptyParameters[i], realParams[i].replace(/\u00a0/g, ' '), - 'emptyParameters[' + i + '] for \'' + typed + '\''); - } - } - - if (tests.directTabText) { - test.is(tests.directTabText, completer.directTabText, - 'directTabText for \'' + typed + '\''); - } - else { - test.is('', completer.directTabText, - 'directTabText for \'' + typed + '\''); - } - - if (tests.arrowTabText) { - test.is(' \u00a0\u21E5 ' + tests.arrowTabText, - completer.arrowTabText, - 'arrowTabText for \'' + typed + '\''); - } - else { - test.is('', completer.arrowTabText, - 'arrowTabText for \'' + typed + '\''); - } -} +exports.shutdown = function(options) { + mockCommands.shutdown(); + helpers.shutdown(options); +}; exports.testActivate = function(options) { if (!options.display) { test.log('No display. Skipping activate tests'); return; } - type('', { }, options); - - type(' ', { }, options); - - type('tsr', { + helpers.setInput(''); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput(' '); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsr'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <text>' ] - }, options); - - type('tsr ', { + }); + + helpers.setInput('tsr '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ '<text>' ] - }, options); - - type('tsr b', { }, options); - - type('tsb', { + }); + + helpers.setInput('tsr b'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsb'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' [toggle]' ] - }, options); - - type('tsm', { + }); + + helpers.setInput('tsm'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <abc>', ' <txt>', ' <num>' ] - }, options); - - type('tsm ', { + }); + + helpers.setInput('tsm '); + helpers.check({ emptyParameters: [ ' <txt>', ' <num>' ], + arrowTabText: '', directTabText: 'a' - }, options); - - type('tsm a', { + }); + + helpers.setInput('tsm a'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <txt>', ' <num>' ] - }, options); - - type('tsm a ', { + }); + + helpers.setInput('tsm a '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ '<txt>', ' <num>' ] - }, options); - - type('tsm a ', { + }); + + helpers.setInput('tsm a '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ '<txt>', ' <num>' ] - }, options); - - type('tsm a d', { + }); + + helpers.setInput('tsm a d'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <num>' ] - }, options); - - type('tsm a "d d"', { + }); + + helpers.setInput('tsm a "d d"'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <num>' ] - }, options); - - type('tsm a "d ', { + }); + + helpers.setInput('tsm a "d '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <num>' ] - }, options); - - type('tsm a "d d" ', { + }); + + helpers.setInput('tsm a "d d" '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ '<num>' ] - }, options); - - type('tsm a "d d ', { + }); + + helpers.setInput('tsm a "d d '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <num>' ] - }, options); - - type('tsm d r', { + }); + + helpers.setInput('tsm d r'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <num>' ] - }, options); - - type('tsm a d ', { + }); + + helpers.setInput('tsm a d '); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ '<num>' ] - }, options); - - type('tsm a d 4', { }, options); - - type('tsg', { + }); + + helpers.setInput('tsm a d 4'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg'); + helpers.check({ + directTabText: '', + arrowTabText: '', emptyParameters: [ ' <solo>' ] - }, options); - - type('tsg ', { + }); + + helpers.setInput('tsg '); + helpers.check({ + emptyParameters: [], + arrowTabText: '', directTabText: 'aaa' - }, options); - - type('tsg a', { + }); + + helpers.setInput('tsg a'); + helpers.check({ + emptyParameters: [], + arrowTabText: '', directTabText: 'aa' - }, options); - - type('tsg b', { + }); + + helpers.setInput('tsg b'); + helpers.check({ + emptyParameters: [], + arrowTabText: '', directTabText: 'bb' - }, options); - - type('tsg d', { }, options); - - type('tsg aa', { + }); + + helpers.setInput('tsg d'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aa'); + helpers.check({ + emptyParameters: [], + arrowTabText: '', directTabText: 'a' - }, options); - - type('tsg aaa', { }, options); - - type('tsg aaa ', { }, options); - - type('tsg aaa d', { }, options); - - type('tsg aaa dddddd', { }, options); - - type('tsg aaa dddddd ', { }, options); - - type('tsg aaa "d', { }, options); - - type('tsg aaa "d d', { }, options); - - type('tsg aaa "d d"', { }, options); - - type('tsn ex ', { }, options); - - type('selarr', { + }); + + helpers.setInput('tsg aaa'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa '); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa d'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa dddddd'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa dddddd '); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa "d'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa "d d'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsg aaa "d d"'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tsn ex '); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('selarr'); + helpers.check({ + directTabText: '', + emptyParameters: [], arrowTabText: 'tselarr' - }, options); - - type('tselar 1', { }, options); - - type('tselar 1', { - cursor: 7 - }, options); - - type('tselar 1', { - cursor: 6, + }); + + helpers.setInput('tselar 1'); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tselar 1', 7); + helpers.check({ + directTabText: '', + arrowTabText: '', + emptyParameters: [] + }); + + helpers.setInput('tselar 1', 6); + helpers.check({ + directTabText: '', + emptyParameters: [], arrowTabText: 'tselarr' - }, options); - - type('tselar 1', { - cursor: 5, + }); + + helpers.setInput('tselar 1', 5); + helpers.check({ + directTabText: '', + emptyParameters: [], arrowTabText: 'tselarr' - }, options); + }); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -2342,16 +2727,204 @@ exports.testOutput = function(options) { test.ok(!focusManager._helpRequested, 'ESCAPE = anti help'); latestOutput.onClose(); }; }); /* + * Copyright 2009-2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE.txt or: + * http://opensource.org/licenses/BSD-3-Clause + */ + +define('gclitest/testIncomplete', ['require', 'exports', 'module' , 'test/assert', 'gclitest/helpers', 'gclitest/mockCommands'], function(require, exports, module) { + + +var test = require('test/assert'); +var helpers = require('gclitest/helpers'); +var mockCommands = require('gclitest/mockCommands'); + + +exports.setup = function(options) { + mockCommands.setup(); + helpers.setup(options); +}; + +exports.shutdown = function(options) { + mockCommands.shutdown(); + helpers.shutdown(options); +}; + +exports.testBasic = function(options) { + var requisition = options.display.requisition; + + helpers.setInput('tsu 2 extra'); + helpers.check({ + args: { + num: { value: 2, type: 'Argument' } + } + }); + test.is(requisition._unassigned.length, 1, 'single unassigned: tsu 2 extra'); + test.is(requisition._unassigned[0].param.type.isIncompleteName, false, + 'unassigned.isIncompleteName: tsu 2 extra'); + + helpers.setInput('tsu'); + helpers.check({ + args: { + num: { value: undefined, type: 'BlankArgument' } + } + }); + + helpers.setInput('tsg'); + helpers.check({ + args: { + solo: { type: 'BlankArgument' }, + txt1: { type: 'BlankArgument' }, + bool: { type: 'BlankArgument' }, + txt2: { type: 'BlankArgument' }, + num: { type: 'BlankArgument' } + } + }); +}; + +exports.testCompleted = function(options) { + helpers.setInput('tsela'); + helpers.pressTab(); + helpers.check({ + args: { + command: { name: 'tselarr', type: 'Argument' }, + num: { type: 'Argument' }, + arr: { type: 'ArrayArgument' }, + } + }); + + helpers.setInput('tsn dif '); + helpers.check({ + input: 'tsn dif ', + markup: 'VVVVVVVV', + cursor: 8, + directTabText: '', + arrowTabText: '', + status: 'ERROR', + emptyParameters: [ '<text>' ], + args: { + command: { name: 'tsn dif', type: 'MergedArgument' }, + text: { type: 'BlankArgument', status: 'INCOMPLETE' } + } + }); + + helpers.setInput('tsn di'); + helpers.pressTab(); + helpers.check({ + input: 'tsn dif ', + markup: 'VVVVVVVV', + cursor: 8, + directTabText: '', + arrowTabText: '', + status: 'ERROR', + emptyParameters: [ '<text>' ], + args: { + command: { name: 'tsn dif', type: 'Argument' }, + text: { type: 'Argument', status: 'INCOMPLETE' } + } + }); + + // The above 2 tests take different routes to 'tsn dif '. The results should + // be similar. The difference is in args.command.type. + + helpers.setInput('tsg -'); + helpers.check({ + input: 'tsg -', + markup: 'VVVVI', + cursor: 5, + directTabText: '-txt1', + arrowTabText: '', + status: 'ERROR', + emptyParameters: [ ], + args: { + solo: { value: undefined, status: 'INCOMPLETE' }, + txt1: { value: undefined, status: 'VALID' }, + bool: { value: undefined, status: 'VALID' }, + txt2: { value: undefined, status: 'VALID' }, + num: { value: undefined, status: 'VALID' } + } + }); + + helpers.pressTab(); + helpers.check({ + input: 'tsg --txt1 ', + markup: 'VVVVIIIIIIV', + cursor: 11, + directTabText: '', + arrowTabText: '', + status: 'ERROR', + emptyParameters: [ ], // Bug 770830: '<txt1>', ' <solo>' + args: { + solo: { value: undefined, status: 'INCOMPLETE' }, + txt1: { value: undefined, status: 'INCOMPLETE' }, + bool: { value: undefined, status: 'VALID' }, + txt2: { value: undefined, status: 'VALID' }, + num: { value: undefined, status: 'VALID' } + } + }); + + helpers.setInput('tsg --txt1 fred'); + helpers.check({ + input: 'tsg --txt1 fred', + markup: 'VVVVVVVVVVVVVVV', + directTabText: '', + arrowTabText: '', + status: 'ERROR', + emptyParameters: [ ], // Bug 770830: ' <solo>' + args: { + solo: { value: undefined, status: 'INCOMPLETE' }, + txt1: { value: 'fred', status: 'VALID' }, + bool: { value: undefined, status: 'VALID' }, + txt2: { value: undefined, status: 'VALID' }, + num: { value: undefined, status: 'VALID' } + } + }); + + // Expand out to christmas tree command line +}; + +exports.testIncomplete = function(options) { + var requisition = options.display.requisition; + + helpers.setInput('tsm a a -'); + helpers.check({ + args: { + abc: { value: 'a', type: 'Argument' }, + txt: { value: 'a', type: 'Argument' }, + num: { value: undefined, arg: ' -', type: 'Argument', status: 'INCOMPLETE' } + } + }); + + helpers.setInput('tsg -'); + helpers.check({ + args: { + solo: { type: 'BlankArgument' }, + txt1: { type: 'BlankArgument' }, + bool: { type: 'BlankArgument' }, + txt2: { type: 'BlankArgument' }, + num: { type: 'BlankArgument' } + } + }); + test.is(requisition._unassigned[0], requisition.getAssignmentAt(5), + 'unassigned -'); + test.is(requisition._unassigned.length, 1, 'single unassigned - tsg -'); + test.is(requisition._unassigned[0].param.type.isIncompleteName, true, + 'unassigned.isIncompleteName: tsg -'); +}; + + +}); +/* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * @@ -3686,23 +4259,21 @@ exports.testJavascript = function() { * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ -define('gclitest/testTokenize', ['require', 'exports', 'module' , 'test/assert', 'gcli/cli', 'gcli/argument'], function(require, exports, module) { +define('gclitest/testTokenize', ['require', 'exports', 'module' , 'test/assert', 'gcli/cli'], function(require, exports, module) { var test = require('test/assert'); var Requisition = require('gcli/cli').Requisition; -var Argument = require('gcli/argument').Argument; -var ScriptArgument = require('gcli/argument').ScriptArgument; exports.testBlanks = function() { var args; var requ = new Requisition(); args = requ._tokenize(''); test.is(1, args.length); test.is('', args[0].text); @@ -3720,257 +4291,257 @@ exports.testTokSimple = function() { var args; var requ = new Requisition(); args = requ._tokenize('s'); test.is(1, args.length); test.is('s', args[0].text); test.is('', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); args = requ._tokenize('s s'); test.is(2, args.length); test.is('s', args[0].text); test.is('', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); test.is('s', args[1].text); test.is(' ', args[1].prefix); test.is('', args[1].suffix); - test.ok(args[1] instanceof Argument); + test.is('Argument', args[1].type); }; exports.testJavascript = function() { var args; var requ = new Requisition(); args = requ._tokenize('{x}'); test.is(1, args.length); test.is('x', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{ x }'); test.is(1, args.length); test.is('x', args[0].text); test.is('{ ', args[0].prefix); test.is(' }', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{x} {y}'); test.is(2, args.length); test.is('x', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); test.is('y', args[1].text); test.is(' {', args[1].prefix); test.is('}', args[1].suffix); - test.ok(args[1] instanceof ScriptArgument); + test.is('ScriptArgument', args[1].type); args = requ._tokenize('{x}{y}'); test.is(2, args.length); test.is('x', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); test.is('y', args[1].text); test.is('{', args[1].prefix); test.is('}', args[1].suffix); - test.ok(args[1] instanceof ScriptArgument); + test.is('ScriptArgument', args[1].type); args = requ._tokenize('{'); test.is(1, args.length); test.is('', args[0].text); test.is('{', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{ '); test.is(1, args.length); test.is('', args[0].text); test.is('{ ', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{x'); test.is(1, args.length); test.is('x', args[0].text); test.is('{', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); }; exports.testRegularNesting = function() { var args; var requ = new Requisition(); args = requ._tokenize('{"x"}'); test.is(1, args.length); test.is('"x"', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{\'x\'}'); test.is(1, args.length); test.is('\'x\'', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('"{x}"'); test.is(1, args.length); test.is('{x}', args[0].text); test.is('"', args[0].prefix); test.is('"', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); args = requ._tokenize('\'{x}\''); test.is(1, args.length); test.is('{x}', args[0].text); test.is('\'', args[0].prefix); test.is('\'', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); }; exports.testDeepNesting = function() { var args; var requ = new Requisition(); args = requ._tokenize('{{}}'); test.is(1, args.length); test.is('{}', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{{x} {y}}'); test.is(1, args.length); test.is('{x} {y}', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); args = requ._tokenize('{{w} {{{x}}}} {y} {{{z}}}'); test.is(3, args.length); test.is('{w} {{{x}}}', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); test.is('y', args[1].text); test.is(' {', args[1].prefix); test.is('}', args[1].suffix); - test.ok(args[1] instanceof ScriptArgument); + test.is('ScriptArgument', args[1].type); test.is('{{z}}', args[2].text); test.is(' {', args[2].prefix); test.is('}', args[2].suffix); - test.ok(args[2] instanceof ScriptArgument); + test.is('ScriptArgument', args[2].type); args = requ._tokenize('{{w} {{{x}}} {y} {{{z}}}'); test.is(1, args.length); test.is('{w} {{{x}}} {y} {{{z}}}', args[0].text); test.is('{', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); }; exports.testStrangeNesting = function() { var args; var requ = new Requisition(); // Note: When we get real JS parsing this should break args = requ._tokenize('{"x}"}'); test.is(2, args.length); test.is('"x', args[0].text); test.is('{', args[0].prefix); test.is('}', args[0].suffix); - test.ok(args[0] instanceof ScriptArgument); + test.is('ScriptArgument', args[0].type); test.is('}', args[1].text); test.is('"', args[1].prefix); test.is('', args[1].suffix); - test.ok(args[1] instanceof Argument); + test.is('Argument', args[1].type); }; exports.testComplex = function() { var args; var requ = new Requisition(); args = requ._tokenize(' 1234 \'12 34\''); test.is(2, args.length); test.is('1234', args[0].text); test.is(' ', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); test.is('12 34', args[1].text); test.is(' \'', args[1].prefix); test.is('\'', args[1].suffix); - test.ok(args[1] instanceof Argument); + test.is('Argument', args[1].type); args = requ._tokenize('12\'34 "12 34" \\'); // 12'34 "12 34" \ test.is(3, args.length); test.is('12\'34', args[0].text); test.is('', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); test.is('12 34', args[1].text); test.is(' "', args[1].prefix); test.is('"', args[1].suffix); - test.ok(args[1] instanceof Argument); + test.is('Argument', args[1].type); test.is('\\', args[2].text); test.is(' ', args[2].prefix); test.is('', args[2].suffix); - test.ok(args[2] instanceof Argument); + test.is('Argument', args[2].type); }; exports.testPathological = function() { var args; var requ = new Requisition(); args = requ._tokenize('a\\ b \\t\\n\\r \\\'x\\\" \'d'); // a_b \t\n\r \'x\" 'd test.is(4, args.length); test.is('a b', args[0].text); test.is('', args[0].prefix); test.is('', args[0].suffix); - test.ok(args[0] instanceof Argument); + test.is('Argument', args[0].type); test.is('\t\n\r', args[1].text); test.is(' ', args[1].prefix); test.is('', args[1].suffix); - test.ok(args[1] instanceof Argument); + test.is('Argument', args[1].type); test.is('\'x"', args[2].text); test.is(' ', args[2].prefix); test.is('', args[2].suffix); - test.ok(args[2] instanceof Argument); + test.is('Argument', args[2].type); test.is('d', args[3].text); test.is(' \'', args[3].prefix); test.is('', args[3].suffix); - test.ok(args[3] instanceof Argument); + test.is('Argument', args[3].type); }; }); /* * Copyright 2012, Mozilla Foundation and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -4189,66 +4760,761 @@ exports.testFindCssSelector = function(o test.is(matches.length, 1, 'multiple matches for ' + selector); test.is(matches[0], nodes[i], 'non-matching selector: ' + selector); } }; }); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/ui/display', ['require', 'exports', 'module' , 'gcli/util', 'gcli/settings', 'gcli/ui/intro', 'gcli/ui/domtemplate', 'gcli/ui/tooltip', 'gcli/ui/output_terminal', 'gcli/ui/inputter', 'gcli/ui/completer', 'gcli/ui/focus', 'gcli/ui/prompt', 'gcli/cli', 'text!gcli/ui/display.css', 'text!gcli/ui/display.html'], function(require, exports, module) { + + +var util = require('gcli/util'); +var settings = require('gcli/settings'); +var intro = require('gcli/ui/intro'); +var domtemplate = require('gcli/ui/domtemplate'); + +var Tooltip = require('gcli/ui/tooltip').Tooltip; +var OutputTerminal = require('gcli/ui/output_terminal').OutputTerminal; +var Inputter = require('gcli/ui/inputter').Inputter; +var Completer = require('gcli/ui/completer').Completer; +var FocusManager = require('gcli/ui/focus').FocusManager; +var Prompt = require('gcli/ui/prompt').Prompt; + +var Requisition = require('gcli/cli').Requisition; + +var displayCss = require('text!gcli/ui/display.css'); +var displayHtml = require('text!gcli/ui/display.html'); + + +/** + * createDisplay() calls 'new Display()' but returns an object which exposes a + * much restricted set of functions rather than all those exposed by Display. + * This allows for robust testing without exposing too many internals. + * @param options See Display() for a description of the available options. + */ +exports.createDisplay = function(options) { + if (options.settings != null) { + settings.setDefaults(options.settings); + } + var display = new Display(options); + var requisition = display.requisition; + return { + /** + * The exact shape of the object returned by exec is likely to change in + * the near future. If you do use it, please expect your code to break. + */ + exec: requisition.exec.bind(requisition), + update: requisition.update.bind(requisition), + destroy: display.destroy.bind(display) + }; +}; + +/** + * View is responsible for generating the web UI for GCLI. + * @param options Object containing user customization properties. + * See the documentation for the other components for more details. + * Options supported directly include: + * - document (default=document): + * - environment (default={}): + * - dontDecorate (default=false): + * - inputElement (default=#gcli-input): + * - completeElement (default=#gcli-row-complete): + * - displayElement (default=#gcli-display): + * - promptElement (default=#gcli-prompt): + */ +function Display(options) { + var doc = options.document || document; + + this.displayStyle = undefined; + if (displayCss != null) { + this.displayStyle = util.importCss(displayCss, doc, 'gcli-css-display'); + } + + // Configuring the document is complex because on the web side, there is an + // active desire to have nothing to configure, where as when embedded in + // Firefox there could be up to 4 documents, some of which can/should be + // derived from some root element. + // When a component uses a document to create elements for use under a known + // root element, then we pass in the element (if we have looked it up + // already) or an id/document + this.requisition = new Requisition(options.enviroment || {}, doc); + + this.focusManager = new FocusManager(options, { + document: doc + }); + + this.inputElement = find(doc, options.inputElement || null, 'gcli-input'); + this.inputter = new Inputter(options, { + requisition: this.requisition, + focusManager: this.focusManager, + element: this.inputElement + }); + + // autoResize logic: we want Completer to keep the elements at the same + // position if we created the completion element, but if someone else created + // it, then it's their job. + this.completeElement = insert(this.inputElement, + options.completeElement || null, 'gcli-row-complete'); + this.completer = new Completer(options, { + requisition: this.requisition, + inputter: this.inputter, + autoResize: this.completeElement.gcliCreated, + element: this.completeElement + }); + + this.prompt = new Prompt(options, { + inputter: this.inputter, + element: insert(this.inputElement, + options.promptElement || null, 'gcli-prompt') + }); + + this.element = find(doc, options.displayElement || null, 'gcli-display'); + this.element.classList.add('gcli-display'); + + this.template = util.toDom(doc, displayHtml); + this.elements = {}; + domtemplate.template(this.template, this.elements, { stack: 'display.html' }); + this.element.appendChild(this.template); + + this.tooltip = new Tooltip(options, { + requisition: this.requisition, + inputter: this.inputter, + focusManager: this.focusManager, + element: this.elements.tooltip, + panelElement: this.elements.panel + }); + + this.inputter.tooltip = this.tooltip; + + this.outputElement = util.createElement(doc, 'div'); + this.outputElement.classList.add('gcli-output'); + this.outputList = new OutputTerminal(options, { + requisition: this.requisition, + element: this.outputElement + }); + + this.element.appendChild(this.outputElement); + + intro.maybeShowIntro(this.outputList.commandOutputManager, this.requisition); +} + +/** + * Call the destroy functions of the components that we created + */ +Display.prototype.destroy = function() { + delete this.element; + delete this.template; + + this.outputList.destroy(); + delete this.outputList; + delete this.outputElement; + + this.tooltip.destroy(); + delete this.tooltip; + + this.prompt.destroy(); + delete this.prompt; + + this.completer.destroy(); + delete this.completer; + delete this.completeElement; + + this.inputter.destroy(); + delete this.inputter; + delete this.inputElement; + + this.focusManager.destroy(); + delete this.focusManager; + + this.requisition.destroy(); + delete this.requisition; + + if (this.displayStyle) { + this.displayStyle.parentNode.removeChild(this.displayStyle); + } + delete this.displayStyle; +}; + +exports.Display = Display; + +/** + * Utility to help find an element by id, throwing if it wasn't found + */ +function find(doc, element, id) { + if (!element) { + element = doc.getElementById(id); + if (!element) { + throw new Error('Missing element, id=' + id); + } + } + return element; +} + +/** + * Utility to help find an element by id, creating it if it wasn't found + */ +function insert(sibling, element, id) { + var doc = sibling.ownerDocument; + if (!element) { + element = doc.getElementById('gcli-row-complete'); + if (!element) { + element = util.createElement(doc, 'div'); + sibling.parentNode.insertBefore(element, sibling.nextSibling); + element.gcliCreated = true; + } + } + return element; +} + + +}); +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/ui/output_terminal', ['require', 'exports', 'module' , 'gcli/util', 'gcli/canon', 'gcli/ui/domtemplate', 'text!gcli/ui/output_view.css', 'text!gcli/ui/output_terminal.html'], function(require, exports, module) { + +var util = require('gcli/util'); + +var canon = require('gcli/canon'); +var domtemplate = require('gcli/ui/domtemplate'); + +var outputViewCss = require('text!gcli/ui/output_view.css'); +var outputViewHtml = require('text!gcli/ui/output_terminal.html'); + + +/** + * A wrapper for a set of rows|command outputs. + * Register with the canon to be notified when commands have output to be + * displayed. + * @param options Object containing user customization properties, including: + * - commandOutputManager + * @param components Object that links to other UI components. GCLI provided: + * - element: Root element to populate + * - requisition (optional): A click/double-click to an input row causes the + * command to be sent to the input/executed if we know the requisition use + */ +function OutputTerminal(options, components) { + this.element = components.element; + this.requisition = components.requisition; + + this.commandOutputManager = options.commandOutputManager || + canon.commandOutputManager; + this.commandOutputManager.onOutput.add(this.outputted, this); + + var document = components.element.ownerDocument; + if (outputViewCss != null) { + this.style = util.importCss(outputViewCss, document, 'gcli-output-view'); + } + + this.template = util.toDom(document, outputViewHtml); + this.templateOptions = { allowEval: true, stack: 'output_terminal.html' }; +} + +/** + * Avoid memory leaks + */ +OutputTerminal.prototype.destroy = function() { + if (this.style) { + this.style.parentNode.removeChild(this.style); + delete this.style; + } + + this.commandOutputManager.onOutput.remove(this.outputted, this); + + delete this.commandOutputManager; + delete this.requisition; + delete this.element; + delete this.template; +}; + +/** + * Monitor for new command executions + */ +OutputTerminal.prototype.outputted = function(ev) { + if (ev.output.hidden) { + return; + } + + ev.output.view = new OutputView(ev.output, this); +}; + +/** + * Display likes to be able to control the height of its children + */ +OutputTerminal.prototype.setHeight = function(height) { + this.element.style.height = height + 'px'; +}; + +exports.OutputTerminal = OutputTerminal; + + +/** + * Adds a row to the CLI output display + */ +function OutputView(outputData, outputTerminal) { + this.outputData = outputData; + this.outputTerminal = outputTerminal; + + this.url = util.createUrlLookup(module); + + // Elements attached to this by template(). + this.elems = { + rowin: null, + rowout: null, + hide: null, + show: null, + duration: null, + throb: null, + prompt: null + }; + + var template = this.outputTerminal.template.cloneNode(true); + domtemplate.template(template, this, this.outputTerminal.templateOptions); + + this.outputTerminal.element.appendChild(this.elems.rowin); + this.outputTerminal.element.appendChild(this.elems.rowout); + + this.outputData.onClose.add(this.closed, this); + this.outputData.onChange.add(this.changed, this); +} + +OutputView.prototype.destroy = function() { + this.outputData.onChange.remove(this.changed, this); + this.outputData.onClose.remove(this.closed, this); + + this.outputTerminal.element.removeChild(this.elems.rowin); + this.outputTerminal.element.removeChild(this.elems.rowout); + + delete this.outputData; + delete this.outputTerminal; + delete this.url; + delete this.elems; +}; + +/** + * Only display a prompt if there is a command, otherwise, leave blank + */ +Object.defineProperty(OutputView.prototype, 'prompt', { + get: function() { + return this.outputData.canonical ? '\u00bb' : ''; + }, + enumerable: true +}); + +/** + * A single click on an invocation line in the console copies the command + * to the command line + */ +OutputView.prototype.copyToInput = function() { + if (this.outputTerminal.requisition) { + this.outputTerminal.requisition.update(this.outputData.typed); + } +}; + +/** + * A double click on an invocation line in the console executes the command + */ +OutputView.prototype.execute = function(ev) { + if (this.outputTerminal.requisition) { + this.outputTerminal.requisition.exec({ typed: this.outputData.typed }); + } +}; + +OutputView.prototype.hideOutput = function(ev) { + this.elems.rowout.style.display = 'none'; + this.elems.hide.classList.add('cmd_hidden'); + this.elems.show.classList.remove('cmd_hidden'); + + ev.stopPropagation(); +}; + +OutputView.prototype.showOutput = function(ev) { + this.elems.rowout.style.display = 'block'; + this.elems.hide.classList.remove('cmd_hidden'); + this.elems.show.classList.add('cmd_hidden'); + + ev.stopPropagation(); +}; + +OutputView.prototype.closed = function(ev) { + this.destroy(); +}; + +OutputView.prototype.changed = function(ev) { + var document = this.elems.rowout.ownerDocument; + var duration = this.outputData.duration != null ? + 'completed in ' + (this.outputData.duration / 1000) + ' sec ' : + ''; + duration = document.createTextNode(duration); + this.elems.duration.appendChild(duration); + + if (this.outputData.completed) { + this.elems.prompt.classList.add('gcli-row-complete'); + } + if (this.outputData.error) { + this.elems.prompt.classList.add('gcli-row-error'); + } + + this.outputData.toDom(this.elems.rowout); + + // We need to see the output of the latest command entered + // Certain browsers have a bug such that scrollHeight is too small + // when content does not fill the client area of the element + var scrollHeight = Math.max(this.outputTerminal.element.scrollHeight, + this.outputTerminal.element.clientHeight); + this.outputTerminal.element.scrollTop = + scrollHeight - this.outputTerminal.element.clientHeight; + + this.elems.throb.style.display = this.outputData.completed ? 'none' : 'block'; +}; + +exports.OutputView = OutputView; + + +}); +define("text!gcli/ui/output_view.css", [], "\n" + + ".gcli-row-in {\n" + + " padding: 0 4px;\n" + + " box-shadow: 0 -6px 10px -6px #ddd;\n" + + " border-top: 1px solid #bbb;\n" + + "}\n" + + "\n" + + ".gcli-row-in > img {\n" + + " cursor: pointer;\n" + + "}\n" + + "\n" + + ".gcli-row-hover {\n" + + " display: none;\n" + + " float: right;\n" + + " padding: 2px 2px 0;\n" + + "}\n" + + "\n" + + ".gcli-row-in:hover > .gcli-row-hover {\n" + + " display: inline;\n" + + "}\n" + + "\n" + + ".gcli-row-in:hover > .gcli-row-hover.gcli-row-hidden {\n" + + " display: none;\n" + + "}\n" + + "\n" + + ".gcli-row-duration {\n" + + " color: #666;\n" + + "}\n" + + "\n" + + ".gcli-row-prompt {\n" + + " color: #00F;\n" + + " font-weight: bold;\n" + + " font-size: 120%;\n" + + "}\n" + + "\n" + + ".gcli-row-prompt.gcli-row-complete {\n" + + " color: #060;\n" + + "}\n" + + "\n" + + ".gcli-row-prompt.gcli-row-error {\n" + + " color: #F00;\n" + + "}\n" + + "\n" + + ".gcli-row-duration {\n" + + " font-size: 80%;\n" + + "}\n" + + "\n" + + ".gcli-row-out {\n" + + " margin: 0 10px 15px;\n" + + " padding: 0 10px;\n" + + " line-height: 1.2em;\n" + + " font-size: 95%;\n" + + "}\n" + + "\n" + + ".gcli-row-out strong,\n" + + ".gcli-row-out b,\n" + + ".gcli-row-out th,\n" + + ".gcli-row-out h1,\n" + + ".gcli-row-out h2,\n" + + ".gcli-row-out h3 {\n" + + " color: #000;\n" + + "}\n" + + "\n" + + ".gcli-row-out p {\n" + + " margin: 5px 0;\n" + + "}\n" + + "\n" + + ".gcli-row-out a {\n" + + " color: hsl(200,40%,40%);\n" + + " text-decoration: none;\n" + + "}\n" + + "\n" + + ".gcli-row-out a:hover {\n" + + " cursor: pointer;\n" + + " border-bottom: 1px dotted hsl(200,40%,60%);\n" + + "}\n" + + "\n" + + ".gcli-row-out input[type=password],\n" + + ".gcli-row-out input[type=text],\n" + + ".gcli-row-out textarea {\n" + + " font-size: 120%;\n" + + " background: transparent;\n" + + " padding: 3px;\n" + + " border-radius: 3px;\n" + + " border: 1px solid #bbb;\n" + + "}\n" + + "\n" + + ".gcli-row-out table,\n" + + ".gcli-row-out td,\n" + + ".gcli-row-out th {\n" + + " border: 0;\n" + + " padding: 0 2px;\n" + + "}\n" + + "\n" + + ".gcli-row-terminal,\n" + + ".gcli-row-subterminal {\n" + + " border-radius: 3px;\n" + + " border: 1px solid #ddd;\n" + + "}\n" + + "\n" + + ".gcli-row-terminal {\n" + + " height: 200px;\n" + + " width: 620px;\n" + + " font-size: 80%;\n" + + "}\n" + + "\n" + + ".gcli-row-subterminal {\n" + + " height: 150px;\n" + + " width: 300px;\n" + + " font-size: 75%;\n" + + "}\n" + + "\n" + + ".gcli-out-shortcut {\n" + + " font-weight: normal;\n" + + " border: 1px solid #999;\n" + + " border-radius: 3px;\n" + + " color: #666;\n" + + " cursor: pointer;\n" + + " padding: 0 3px 1px;\n" + + " margin: 1px 4px;\n" + + " display: inline-block;\n" + + "}\n" + + "\n" + + ".gcli-out-shortcut:before {\n" + + " content: '\\bb';\n" + + " padding-right: 2px;\n" + + " color: hsl(25,78%,50%);\n" + + " font-weight: bold;\n" + + " font-size: 110%;\n" + + "}\n" + + ""); + +define("text!gcli/ui/output_terminal.html", [], "\n" + + "<div class=\"gcli-row\">\n" + + " <!-- The div for the input (i.e. what was typed) -->\n" + + " <div class=\"gcli-row-in\" save=\"${elems.rowin}\" aria-live=\"assertive\"\n" + + " onclick=\"${copyToInput}\" ondblclick=\"${execute}\">\n" + + "\n" + + " <!-- What the user actually typed -->\n" + + " <span save=\"${elems.prompt}\" class=\"gcli-row-prompt ${elems.error ? 'gcli-row-error' : ''} ${elems.completed ? 'gcli-row-complete' : ''}\">${prompt}</span>\n" + + " <span class=\"gcli-row-in-typed\">${outputData.canonical}</span>\n" + + "\n" + + " <!-- The extra details that appear on hover -->\n" + + " <span class=\"gcli-row-duration gcli-row-hover\" save=\"${elems.duration}\"></span>\n" + + " <!--\n" + + " <img class=\"gcli-row-hover\" onclick=\"${hideOutput}\" save=\"${elems.hide}\"\n" + + " alt=\"Hide command output\" _src=\"${url('images/minus.png')}\"/>\n" + + " <img class=\"gcli-row-hover gcli-row-hidden\" onclick=\"${showOutput}\" save=\"${elems.show}\"\n" + + " alt=\"Show command output\" _src=\"${url('images/plus.png')}\"/>\n" + + " <img class=\"gcli-row-hover\" onclick=\"${remove}\"\n" + + " alt=\"Remove this command from the history\"\n" + + " _src=\"${url('images/closer.png')}\"/>\n" + + " -->\n" + + " <img style=\"float:right;\" _src=\"${url('images/throbber.gif')}\" save=\"${elems.throb}\"/>\n" + + " </div>\n" + + "\n" + + " <!-- The div for the command output -->\n" + + " <div class=\"gcli-row-out\" aria-live=\"assertive\" save=\"${elems.rowout}\">\n" + + " </div>\n" + + "</div>\n" + + ""); + +/* + * Copyright 2012, Mozilla Foundation and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +define('gcli/ui/prompt', ['require', 'exports', 'module' ], function(require, exports, module) { + + +/** + * Prompt is annoying because some systems provide a UI elements (i.e. firefox) + * while some expect you to overlay them on an input element (i.e. the web) + * Also we want to provide click -> show menu ability. + * @param options Object containing user customization properties, including: + * - promptChar (default='\u00bb') (double greater-than, a.k.a right guillemet) + * The prompt is used directly in a TextNode, so no HTML entities. + * @param components Object that links to other UI components. GCLI provided: + * - element + * - inputter + */ +function Prompt(options, components) { + this.element = components.element; + this.element.classList.add('gcli-prompt'); + + var prompt = options.promptChar || '\u00bb'; + var text = this.element.ownerDocument.createTextNode(prompt); + this.element.appendChild(text); + + this.inputter = components.inputter; + if (this.inputter) { + this.inputter.onResize.add(this.resized, this); + + var dimensions = this.inputter.getDimensions(); + if (dimensions) { + this.resized(dimensions); + } + } +} + +/** + * Avoid memory leaks + */ +Prompt.prototype.destroy = function() { + if (this.inputter) { + this.inputter.onResize.remove(this.resized, this); + } + + delete this.element; +}; + +/** + * Ensure that the completion element is the same size and the inputter element + */ +Prompt.prototype.resized = function(ev) { + this.element.style.top = ev.top + 'px'; + this.element.style.height = ev.height + 'px'; + this.element.style.left = ev.left + 'px'; + this.element.style.width = ev.width + 'px'; +}; + +exports.Prompt = Prompt; + + +}); +define("text!gcli/ui/display.css", [], "\n" + + ".gcli-output {\n" + + " height: 100%;\n" + + " overflow-x: hidden;\n" + + " overflow-y: auto;\n" + + " font-family: Segoe UI, Helvetica Neue, Verdana, Arial, sans-serif;\n" + + "}\n" + + ""); + +define("text!gcli/ui/display.html", [], "\n" + + "<div class=\"gcli-panel\" save=\"${panel}\">\n" + + " <div save=\"${tooltip}\"></div>\n" + + " <div class=\"gcli-panel-connector\"></div>\n" + + "</div>\n" + + ""); + let testModuleNames = [ 'gclitest/index', 'gclitest/suite', 'test/examiner', 'test/assert', 'test/status', 'gclitest/testCanon', 'gclitest/helpers', 'gclitest/testCli', 'gclitest/mockCommands', 'gclitest/testCompletion', 'gclitest/testExec', 'gclitest/testHelp', 'gclitest/testHistory', 'gclitest/testInputter', + 'gclitest/testIncomplete', 'gclitest/testIntro', 'gclitest/testJs', 'gclitest/testKeyboard', 'gclitest/testPref', 'gclitest/mockSettings', 'gclitest/testRequire', 'gclitest/requirable', 'gclitest/testResource', 'gclitest/testScratchpad', 'gclitest/testSettings', 'gclitest/testSpell', 'gclitest/testSplit', 'gclitest/testTokenize', 'gclitest/testTooltip', 'gclitest/testTypes', 'gclitest/testUtil', + 'gcli/ui/display', + 'gcli/ui/output_terminal', + 'text!gcli/ui/output_view.css', + 'text!gcli/ui/output_terminal.html', + 'gcli/ui/prompt', + 'text!gcli/ui/display.css', + 'text!gcli/ui/display.html', ]; // Cached so it still exists during cleanup until we need it to let localDefine; const TEST_URI = "data:text/html;charset=utf-8,gcli-web"; function test() { localDefine = define; DeveloperToolbarTest.test(TEST_URI, function(browser, tab) { - var gclitest = define.globalDomain.require("gclitest/index"); - gclitest.run({ + var examiner = define.globalDomain.require('gclitest/suite').examiner; + examiner.runAsync({ display: DeveloperToolbar.display, isFirefox: true, window: browser.contentDocument.defaultView - }); - finish(); + }, finish); }); } registerCleanupFunction(function() { testModuleNames.forEach(function(moduleName) { delete localDefine.modules[moduleName]; delete localDefine.globalDomain.modules[moduleName]; });
--- a/browser/devtools/commandline/test/head.js +++ b/browser/devtools/commandline/test/head.js @@ -41,280 +41,368 @@ registerCleanupFunction(function tearDow }); /** * Various functions for testing DeveloperToolbar. * Parts of this code exist in: * - browser/devtools/commandline/test/head.js * - browser/devtools/shared/test/head.js */ -let DeveloperToolbarTest = { - /** - * Paranoid DeveloperToolbar.show(); - */ - show: function DTT_show(aCallback) { - if (DeveloperToolbar.visible) { - ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar"); - } - else { - DeveloperToolbar.show(aCallback); - } - }, +let DeveloperToolbarTest = { }; + +/** + * Paranoid DeveloperToolbar.show(); + */ +DeveloperToolbarTest.show = function DTT_show(aCallback) { + if (DeveloperToolbar.visible) { + ok(false, "DeveloperToolbar.visible at start of openDeveloperToolbar"); + } + else { + DeveloperToolbar.show(true, aCallback); + } +}; + +/** + * Paranoid DeveloperToolbar.hide(); + */ +DeveloperToolbarTest.hide = function DTT_hide() { + if (!DeveloperToolbar.visible) { + ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar"); + } + else { + DeveloperToolbar.display.inputter.setInput(""); + DeveloperToolbar.hide(); + } +}; + +/** + * check() is the new status. Similar API except that it doesn't attempt to + * alter the display/requisition at all, and it makes extra checks. + * Test inputs + * typed: The text to type at the input + * Available checks: + * input: The text displayed in the input field + * cursor: The position of the start of the cursor + * status: One of "VALID", "ERROR", "INCOMPLETE" + * emptyParameters: Array of parameters still to type. e.g. [ "<message>" ] + * directTabText: Simple completion text + * arrowTabText: When the completion is not an extension (without arrow) + * markup: What state should the error markup be in. e.g. "VVVIIIEEE" + * args: Maps of checks to make against the arguments: + * value: i.e. assignment.value (which ignores defaultValue) + * type: Argument/BlankArgument/MergedArgument/etc i.e. what's assigned + * Care should be taken with this since it's something of an + * implementation detail + * arg: The toString value of the argument + * status: i.e. assignment.getStatus + * message: i.e. assignment.getMessage + * name: For commands - checks assignment.value.name + */ +DeveloperToolbarTest.checkInputStatus = function DTT_checkInputStatus(checks) { + if (!checks.emptyParameters) { + checks.emptyParameters = []; + } + if (!checks.directTabText) { + checks.directTabText = ''; + } + if (!checks.arrowTabText) { + checks.arrowTabText = ''; + } - /** - * Paranoid DeveloperToolbar.hide(); - */ - hide: function DTT_hide() { - if (!DeveloperToolbar.visible) { - ok(false, "!DeveloperToolbar.visible at start of closeDeveloperToolbar"); - } - else { - DeveloperToolbar.display.inputter.setInput(""); - DeveloperToolbar.hide(); + var display = DeveloperToolbar.display; + + if (checks.typed) { + display.inputter.setInput(checks.typed); + } + else { + ok(false, "Missing typed for " + JSON.stringify(checks)); + return; + } + + if (checks.cursor) { + display.inputter.setCursor(checks.cursor) + } + + var cursor = checks.cursor ? checks.cursor.start : checks.typed.length; + + var requisition = display.requisition; + var completer = display.completer; + var actual = completer._getCompleterTemplateData(); + + /* + if (checks.input) { + is(display.inputter.element.value, + checks.input, + 'input'); + } + + if (checks.cursor) { + is(display.inputter.element.selectionStart, + checks.cursor, + 'cursor'); + } + */ + + if (checks.status) { + is(requisition.getStatus().toString(), + checks.status, + 'status'); + } + + if (checks.markup) { + var statusMarkup = requisition.getInputStatusMarkup(cursor); + var actualMarkup = statusMarkup.map(function(s) { + return Array(s.string.length + 1).join(s.status.toString()[0]); + }).join(''); + + is(checks.markup, + actualMarkup, + 'markup'); + } + + if (checks.emptyParameters) { + var actualParams = actual.emptyParameters; + is(actualParams.length, + checks.emptyParameters.length, + 'emptyParameters.length'); + + if (actualParams.length === checks.emptyParameters.length) { + for (var i = 0; i < actualParams.length; i++) { + is(actualParams[i].replace(/\u00a0/g, ' '), + checks.emptyParameters[i], + 'emptyParameters[' + i + ']'); + } } - }, + } + + if (checks.directTabText) { + is(actual.directTabText, + checks.directTabText, + 'directTabText'); + } + + if (checks.arrowTabText) { + is(actual.arrowTabText, + ' \u00a0\u21E5 ' + checks.arrowTabText, + 'arrowTabText'); + } + + if (checks.args) { + Object.keys(checks.args).forEach(function(paramName) { + var check = checks.args[paramName]; + + var assignment; + if (paramName === 'command') { + assignment = requisition.commandAssignment; + } + else { + assignment = requisition.getAssignment(paramName); + } + + if (assignment == null) { + ok(false, 'Unknown parameter: ' + paramName); + return; + } + + if (check.value) { + is(assignment.value, + check.value, + 'checkStatus value for ' + paramName); + } + + if (check.name) { + is(assignment.value.name, + check.name, + 'checkStatus name for ' + paramName); + } + + if (check.type) { + is(assignment.arg.type, + check.type, + 'checkStatus type for ' + paramName); + } + + if (check.arg) { + is(assignment.arg.toString(), + check.arg, + 'checkStatus arg for ' + paramName); + } + + if (check.status) { + is(assignment.getStatus().toString(), + check.status, + 'checkStatus status for ' + paramName); + } + + if (check.message) { + is(assignment.getMessage(), + check.message, + 'checkStatus message for ' + paramName); + } + }); + } +}; - /** - * Check that we can parse command input. - * Doesn't execute the command, just checks that we grok the input properly: - * - * DeveloperToolbarTest.checkInputStatus({ - * // Test inputs - * typed: "ech", // Required - * cursor: 3, // Optional cursor position - * - * // Thing to check - * status: "INCOMPLETE", // One of "VALID", "ERROR", "INCOMPLETE" - * emptyParameters: [ "<message>" ], // Still to type - * directTabText: "o", // Simple completion text - * arrowTabText: "", // When the completion is not an extension - * markup: "VVVIIIEEE", // What state should the error markup be in - * }); - */ - checkInputStatus: function DTT_checkInputStatus(tests) { - let display = DeveloperToolbar.display; +/** + * Execute a command: + * + * DeveloperToolbarTest.exec({ + * // Test inputs + * typed: "echo hi", // Optional, uses existing if undefined + * + * // Thing to check + * args: { message: "hi" }, // Check that the args were understood properly + * outputMatch: /^hi$/, // RegExp to test against textContent of output + * // (can also be array of RegExps) + * blankOutput: true, // Special checks when there is no output + * }); + */ +DeveloperToolbarTest.exec = function DTT_exec(tests) { + tests = tests || {}; + + if (tests.typed) { + DeveloperToolbar.display.inputter.setInput(tests.typed); + } + + let typed = DeveloperToolbar.display.inputter.getInputState().typed; + let output = DeveloperToolbar.display.requisition.exec(); + + is(typed, output.typed, 'output.command for: ' + typed); + + if (tests.completed !== false) { + ok(output.completed, 'output.completed false for: ' + typed); + } + else { + // It is actually an error if we say something is async and it turns + // out not to be? For now we're saying 'no' + // ok(!output.completed, 'output.completed true for: ' + typed); + } + + if (tests.args != null) { + is(Object.keys(tests.args).length, Object.keys(output.args).length, + 'arg count for ' + typed); - if (tests.typed) { - display.inputter.setInput(tests.typed); + Object.keys(output.args).forEach(function(arg) { + let expectedArg = tests.args[arg]; + let actualArg = output.args[arg]; + + if (typeof expectedArg === 'function') { + ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg); + } + else { + if (Array.isArray(expectedArg)) { + if (!Array.isArray(actualArg)) { + ok(false, 'actual is not an array. ' + typed + '/' + arg); + return; + } + + is(expectedArg.length, actualArg.length, + 'array length: ' + typed + '/' + arg); + for (let i = 0; i < expectedArg.length; i++) { + is(expectedArg[i], actualArg[i], + 'member: "' + typed + '/' + arg + '/' + i); + } + } + else { + is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg); + } + } + }); + } + + let displayed = DeveloperToolbar.outputPanel._div.textContent; + + if (tests.outputMatch) { + function doTest(match, against) { + if (!match.test(against)) { + ok(false, "html output for " + typed + " against " + match.source + + " (textContent sent to info)"); + info("Actual textContent"); + info(against); + } + } + if (Array.isArray(tests.outputMatch)) { + tests.outputMatch.forEach(function(match) { + doTest(match, displayed); + }); } else { - ok(false, "Missing typed for " + JSON.stringify(tests)); - return; - } - - if (tests.cursor) { - display.inputter.setCursor(tests.cursor) + doTest(tests.outputMatch, displayed); } - - if (tests.status) { - is(display.requisition.getStatus().toString(), - tests.status, "status for " + tests.typed); - } - - if (tests.emptyParameters == null) { - tests.emptyParameters = []; - } + } - let realParams = display.completer.emptyParameters; - is(realParams.length, tests.emptyParameters.length, - 'emptyParameters.length for \'' + tests.typed + '\''); - - if (realParams.length === tests.emptyParameters.length) { - for (let i = 0; i < realParams.length; i++) { - is(realParams[i].replace(/\u00a0/g, ' '), tests.emptyParameters[i], - 'emptyParameters[' + i + '] for \'' + tests.typed + '\''); - } + if (tests.blankOutput != null) { + if (!/^$/.test(displayed)) { + ok(false, "html output for " + typed + " (textContent sent to info)"); + info("Actual textContent"); + info(displayed); } - - if (tests.directTabText) { - is(display.completer.directTabText, tests.directTabText, - 'directTabText for \'' + tests.typed + '\''); - } - else { - is(display.completer.directTabText, '', - 'directTabText for \'' + tests.typed + '\''); - } + } +}; - if (tests.arrowTabText) { - is(display.completer.arrowTabText, ' \u00a0\u21E5 ' + tests.arrowTabText, - 'arrowTabText for \'' + tests.typed + '\''); - } - else { - is(display.completer.arrowTabText, '', - 'arrowTabText for \'' + tests.typed + '\''); - } - - if (tests.markup) { - let cursor = tests.cursor ? tests.cursor.start : tests.typed.length; - let statusMarkup = display.requisition.getInputStatusMarkup(cursor); - let actualMarkup = statusMarkup.map(function(s) { - return Array(s.string.length + 1).join(s.status.toString()[0]); - }).join(''); - is(tests.markup, actualMarkup, 'markup for ' + tests.typed); - } - }, +/** + * Quick wrapper around the things you need to do to run DeveloperToolbar + * command tests: + * - Set the pref 'devtools.toolbar.enabled' to true + * - Add a tab pointing at |uri| + * - Open the DeveloperToolbar + * - Register a cleanup function to undo the above + * - Run the tests + * + * @param uri The uri of a page to load. Can be 'about:blank' or 'data:...' + * @param testFunc A function containing the tests to run. This should + * arrange for 'finish()' to be called on completion. + */ +DeveloperToolbarTest.test = function DTT_test(uri, testFunc) { + let menuItem = document.getElementById("menu_devToolbar"); + let command = document.getElementById("Tools:DevToolbar"); + let appMenuItem = document.getElementById("appmenu_devToolbar"); - /** - * Execute a command: - * - * DeveloperToolbarTest.exec({ - * // Test inputs - * typed: "echo hi", // Optional, uses existing if undefined - * - * // Thing to check - * args: { message: "hi" }, // Check that the args were understood properly - * outputMatch: /^hi$/, // RegExp to test against textContent of output - * // (can also be array of RegExps) - * blankOutput: true, // Special checks when there is no output - * }); - */ - exec: function DTT_exec(tests) {