author | David Anderson <danderson@mozilla.com> |
Fri, 04 Nov 2011 14:02:27 -0700 | |
changeset 111599 | 89fff0ee52d5e237060a44b133a68e723dab8ae4 |
parent 111598 | 5b43306f9b174e7b02fa1bf2d708943b976f00a9 (current diff) |
parent 81115 | ae9e5bf847fc727fc0358bdf6619922b464f2136 (diff) |
child 111600 | e784f2911b5bd7956f52be6a93f010a032cf364e |
push id | 1708 |
push user | akeybl@mozilla.com |
push date | Mon, 19 Nov 2012 21:10:21 +0000 |
treeherder | mozilla-beta@27b14fe50103 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
milestone | 10.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
|
new file mode 100644 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# .gitignore - List of filenames git should ignore + +# Filenames that should be ignored wherever they appear +*~ +*.pyc +*.pyo +TAGS +tags +ID +.DS_Store* + +# Vim swap files. +.*.sw[a-z] + +# User files that may appear at the root +.mozconfig +mozconfig +configure +config.cache +config.log + +# Empty marker file that's generated when we check out NSS +security/manager/.nss.checkout + +# Build directories +obj/* + +# Build directories for js shell +*/_DBG.OBJ/ +*/_OPT.OBJ/ + +# SpiderMonkey configury +js/src/configure +js/src/autom4te.cache +# SpiderMonkey test result logs +js/src/tests/results-*.html +js/src/tests/results-*.txt + +# Java HTML5 parser classes +parser/html/java/htmlparser/ +parser/html/java/javaparser/
--- a/accessible/public/nsIAccessibleEvent.idl +++ b/accessible/public/nsIAccessibleEvent.idl @@ -54,17 +54,17 @@ interface nsIDOMNode; * the event and its target. To listen to in-process accessibility invents, * make your object an nsIObserver, and listen for accessible-event by * using code something like this: * nsCOMPtr<nsIObserverService> observerService = * do_GetService("@mozilla.org/observer-service;1", &rv); * if (NS_SUCCEEDED(rv)) * rv = observerService->AddObserver(this, "accessible-event", PR_TRUE); */ -[scriptable, uuid(fd1378c5-c606-4a5e-a321-8e7fc107e5cf)] +[scriptable, uuid(7f66a33a-9ed7-4fd4-87a8-e431b0f43368)] interface nsIAccessibleEvent : nsISupports { /** * An object has been created. */ const unsigned long EVENT_SHOW = 0x0001; /** @@ -275,17 +275,22 @@ interface nsIAccessibleEvent : nsISuppor const unsigned long EVENT_DOCUMENT_ATTRIBUTES_CHANGED = 0x002A; /** * The contents of the document have changed. */ const unsigned long EVENT_DOCUMENT_CONTENT_CHANGED = 0x002B; const unsigned long EVENT_PROPERTY_CHANGED = 0x002C; - const unsigned long EVENT_SELECTION_CHANGED = 0x002D; + + /** + * A slide changed in a presentation document or a page boundary was + * crossed in a word processing document. + */ + const unsigned long EVENT_PAGE_CHANGED = 0x002D; /** * A text object's attributes changed. * Also see EVENT_OBJECT_ATTRIBUTE_CHANGED. */ const unsigned long EVENT_TEXT_ATTRIBUTE_CHANGED = 0x002E; /** @@ -432,25 +437,19 @@ interface nsIAccessibleEvent : nsISuppor const unsigned long EVENT_HYPERTEXT_NLINKS_CHANGED = 0x0054; /** * An object's attributes changed. Also see EVENT_TEXT_ATTRIBUTE_CHANGED. */ const unsigned long EVENT_OBJECT_ATTRIBUTE_CHANGED = 0x0055; /** - * A slide changed in a presentation document or a page boundary was - * crossed in a word processing document. - */ - const unsigned long EVENT_PAGE_CHANGED = 0x0056; - - /** * Help make sure event map does not get out-of-line. */ - const unsigned long EVENT_LAST_ENTRY = 0x0057; + const unsigned long EVENT_LAST_ENTRY = 0x0056; /** * The type of event, based on the enumerated event values * defined in this interface. */ readonly attribute unsigned long eventType; /**
--- a/accessible/src/atk/nsAccessibleWrap.cpp +++ b/accessible/src/atk/nsAccessibleWrap.cpp @@ -1084,20 +1084,34 @@ nsAccessibleWrap::FirePlatformEvent(AccE nsCOMPtr<nsIAccessibleValue> value(do_QueryObject(accessible)); if (value) { // Make sure this is a numeric value // Don't fire for MSAA string value changes (e.g. text editing) // ATK values are always numeric g_object_notify( (GObject*)atkObj, "accessible-value" ); } } break; - case nsIAccessibleEvent::EVENT_SELECTION_CHANGED: - MAI_LOG_DEBUG(("\n\nReceived: EVENT_SELECTION_CHANGED\n")); - g_signal_emit_by_name(atkObj, "selection_changed"); - break; + case nsIAccessibleEvent::EVENT_SELECTION: + case nsIAccessibleEvent::EVENT_SELECTION_ADD: + case nsIAccessibleEvent::EVENT_SELECTION_REMOVE: + { + // XXX: dupe events may be fired + MAI_LOG_DEBUG(("\n\nReceived: EVENT_SELECTION_CHANGED\n")); + AccSelChangeEvent* selChangeEvent = downcast_accEvent(aEvent); + g_signal_emit_by_name(nsAccessibleWrap::GetAtkObject(selChangeEvent->Widget()), + "selection_changed"); + break; + } + + case nsIAccessibleEvent::EVENT_SELECTION_WITHIN: + { + MAI_LOG_DEBUG(("\n\nReceived: EVENT_SELECTION_CHANGED\n")); + g_signal_emit_by_name(atkObj, "selection_changed"); + break; + } case nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED: MAI_LOG_DEBUG(("\n\nReceived: EVENT_TEXT_SELECTION_CHANGED\n")); g_signal_emit_by_name(atkObj, "text_selection_changed"); break; case nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED: {
--- a/accessible/src/base/AccEvent.cpp +++ b/accessible/src/base/AccEvent.cpp @@ -326,16 +326,38 @@ AccCaretMoveEvent::CreateXPCOMObject() { nsAccEvent* event = new nsAccCaretMoveEvent(this); NS_IF_ADDREF(event); return event; } //////////////////////////////////////////////////////////////////////////////// +// AccSelChangeEvent +//////////////////////////////////////////////////////////////////////////////// + +AccSelChangeEvent:: + AccSelChangeEvent(nsAccessible* aWidget, nsAccessible* aItem, + SelChangeType aSelChangeType) : + AccEvent(0, aItem, eAutoDetect, eCoalesceSelectionChange), + mWidget(aWidget), mItem(aItem), mSelChangeType(aSelChangeType), + mPreceedingCount(0), mPackedEvent(nsnull) +{ + if (aSelChangeType == eSelectionAdd) { + if (mWidget->GetSelectedItem(1)) + mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; + else + mEventType = nsIAccessibleEvent::EVENT_SELECTION; + } else { + mEventType = nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + } +} + + +//////////////////////////////////////////////////////////////////////////////// // AccTableChangeEvent //////////////////////////////////////////////////////////////////////////////// AccTableChangeEvent:: AccTableChangeEvent(nsAccessible* aAccessible, PRUint32 aEventType, PRInt32 aRowOrColIndex, PRInt32 aNumRowsOrCols) : AccEvent(aEventType, aAccessible), mRowOrColIndex(aRowOrColIndex), mNumRowsOrCols(aNumRowsOrCols)
--- a/accessible/src/base/AccEvent.h +++ b/accessible/src/base/AccEvent.h @@ -77,16 +77,19 @@ public: // subtree or the same node, only the umbrella event on the ancestor // will be emitted. eCoalesceFromSameSubtree, // eCoalesceOfSameType : For events of the same type, only the newest event // will be processed. eCoalesceOfSameType, + // eCoalesceSelectionChange: coalescence of selection change events. + eCoalesceSelectionChange, + // eRemoveDupes : For repeat events, only the newest event in queue // will be emitted. eRemoveDupes, // eDoNotEmit : This event is confirmed as a duplicate, do not emit it. eDoNotEmit }; @@ -120,16 +123,17 @@ public: enum EventGroup { eGenericEvent, eStateChangeEvent, eTextChangeEvent, eMutationEvent, eHideEvent, eShowEvent, eCaretMoveEvent, + eSelectionChangeEvent, eTableChangeEvent }; static const EventGroup kEventGroup = eGenericEvent; virtual unsigned int GetEventGroups() const { return 1U << eGenericEvent; } @@ -322,20 +326,47 @@ public: private: PRInt32 mCaretOffset; }; /** * Accessible widget selection change event. */ -class AccSelectionChangeEvent : public AccEvent +class AccSelChangeEvent : public AccEvent { public: + enum SelChangeType { + eSelectionAdd, + eSelectionRemove + }; + AccSelChangeEvent(nsAccessible* aWidget, nsAccessible* aItem, + SelChangeType aSelChangeType); + + virtual ~AccSelChangeEvent() { } + + // AccEvent + static const EventGroup kEventGroup = eSelectionChangeEvent; + virtual unsigned int GetEventGroups() const + { + return AccEvent::GetEventGroups() | (1U << eSelectionChangeEvent); + } + + // AccSelChangeEvent + nsAccessible* Widget() const { return mWidget; } + +private: + nsRefPtr<nsAccessible> mWidget; + nsRefPtr<nsAccessible> mItem; + SelChangeType mSelChangeType; + PRUint32 mPreceedingCount; + AccSelChangeEvent* mPackedEvent; + + friend class NotificationController; }; /** * Accessible table change event. */ class AccTableChangeEvent : public AccEvent {
--- a/accessible/src/base/NotificationController.cpp +++ b/accessible/src/base/NotificationController.cpp @@ -46,16 +46,20 @@ #include "nsTextAccessible.h" #include "FocusManager.h" #include "TextUpdater.h" #include "mozilla/dom/Element.h" using namespace mozilla::a11y; +// Defines the number of selection add/remove events in the queue when they +// aren't packed into single selection within event. +const unsigned int kSelChangeCountToPack = 5; + //////////////////////////////////////////////////////////////////////////////// // NotificationCollector //////////////////////////////////////////////////////////////////////////////// NotificationController::NotificationController(nsDocAccessible* aDocument, nsIPresShell* aPresShell) : mObservingState(eNotObservingRefresh), mDocument(aDocument), mPresShell(aPresShell) @@ -486,16 +490,36 @@ NotificationController::CoalesceEvents() accEvent->mEventRule == tailEvent->mEventRule && accEvent->mNode == tailEvent->mNode) { tailEvent->mEventRule = AccEvent::eDoNotEmit; return; } } } break; // case eRemoveDupes + case AccEvent::eCoalesceSelectionChange: + { + AccSelChangeEvent* tailSelChangeEvent = downcast_accEvent(tailEvent); + PRInt32 index = tail - 1; + for (; index >= 0; index--) { + AccEvent* thisEvent = mEvents[index]; + if (thisEvent->mEventRule == tailEvent->mEventRule) { + AccSelChangeEvent* thisSelChangeEvent = + downcast_accEvent(thisEvent); + + // Coalesce selection change events within same control. + if (tailSelChangeEvent->mWidget == thisSelChangeEvent->mWidget) { + CoalesceSelChangeEvents(tailSelChangeEvent, thisSelChangeEvent, index); + return; + } + } + } + + } break; // eCoalesceSelectionChange + default: break; // case eAllowDupes, eDoNotEmit } // switch } void NotificationController::ApplyToSiblings(PRUint32 aStart, PRUint32 aEnd, PRUint32 aEventType, nsINode* aNode, @@ -507,16 +531,96 @@ NotificationController::ApplyToSiblings( accEvent->mEventRule != AccEvent::eDoNotEmit && accEvent->mNode && accEvent->mNode->GetNodeParent() == aNode->GetNodeParent()) { accEvent->mEventRule = aEventRule; } } } void +NotificationController::CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, + AccSelChangeEvent* aThisEvent, + PRInt32 aThisIndex) +{ + aTailEvent->mPreceedingCount = aThisEvent->mPreceedingCount + 1; + + // Pack all preceding events into single selection within event + // when we receive too much selection add/remove events. + if (aTailEvent->mPreceedingCount >= kSelChangeCountToPack) { + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_WITHIN; + aTailEvent->mAccessible = aTailEvent->mWidget; + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + + // Do not emit any preceding selection events for same widget if they + // weren't coalesced yet. + if (aThisEvent->mEventType != nsIAccessibleEvent::EVENT_SELECTION_WITHIN) { + for (PRInt32 jdx = aThisIndex - 1; jdx >= 0; jdx--) { + AccEvent* prevEvent = mEvents[jdx]; + if (prevEvent->mEventRule == aTailEvent->mEventRule) { + AccSelChangeEvent* prevSelChangeEvent = + downcast_accEvent(prevEvent); + if (prevSelChangeEvent->mWidget == aTailEvent->mWidget) + prevSelChangeEvent->mEventRule = AccEvent::eDoNotEmit; + } + } + } + return; + } + + // Pack sequential selection remove and selection add events into + // single selection change event. + if (aTailEvent->mPreceedingCount == 1 && + aTailEvent->mItem != aThisEvent->mItem) { + if (aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aThisEvent->mEventRule = AccEvent::eDoNotEmit; + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aTailEvent->mPackedEvent = aThisEvent; + return; + } + + if (aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd && + aTailEvent->mSelChangeType == AccSelChangeEvent::eSelectionRemove) { + aTailEvent->mEventRule = AccEvent::eDoNotEmit; + aThisEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION; + aThisEvent->mPackedEvent = aThisEvent; + return; + } + } + + // Unpack the packed selection change event because we've got one + // more selection add/remove. + if (aThisEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) { + if (aThisEvent->mPackedEvent) { + aThisEvent->mPackedEvent->mEventType = + aThisEvent->mPackedEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd ? + nsIAccessibleEvent::EVENT_SELECTION_ADD : + nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + aThisEvent->mPackedEvent->mEventRule = + AccEvent::eCoalesceSelectionChange; + + aThisEvent->mPackedEvent = nsnull; + } + + aThisEvent->mEventType = + aThisEvent->mSelChangeType == AccSelChangeEvent::eSelectionAdd ? + nsIAccessibleEvent::EVENT_SELECTION_ADD : + nsIAccessibleEvent::EVENT_SELECTION_REMOVE; + + return; + } + + // Convert into selection add since control has single selection but other + // selection events for this control are queued. + if (aTailEvent->mEventType == nsIAccessibleEvent::EVENT_SELECTION) + aTailEvent->mEventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; +} + +void NotificationController::CoalesceTextChangeEventsFor(AccHideEvent* aTailEvent, AccHideEvent* aThisEvent) { // XXX: we need a way to ignore SplitNode and JoinNode() when they do not // affect the text within the hypertext. AccTextChangeEvent* textEvent = aThisEvent->mTextChangeEvent; if (!textEvent)
--- a/accessible/src/base/NotificationController.h +++ b/accessible/src/base/NotificationController.h @@ -246,21 +246,21 @@ private: * @param aEventRule the event rule to be applied * (should be eDoNotEmit or eAllowDupes) */ void ApplyToSiblings(PRUint32 aStart, PRUint32 aEnd, PRUint32 aEventType, nsINode* aNode, AccEvent::EEventRule aEventRule); /** - * Do not emit one of two given reorder events fired for DOM nodes in the case - * when one DOM node is in parent chain of second one. + * Coalesce two selection change events within the same select control. */ - void CoalesceReorderEventsFromSameTree(AccEvent* aAccEvent, - AccEvent* aDescendantAccEvent); + void CoalesceSelChangeEvents(AccSelChangeEvent* aTailEvent, + AccSelChangeEvent* aThisEvent, + PRInt32 aThisIndex); /** * Coalesce text change events caused by sibling hide events. */ void CoalesceTextChangeEventsFor(AccHideEvent* aTailEvent, AccHideEvent* aThisEvent); void CoalesceTextChangeEventsFor(AccShowEvent* aTailEvent, AccShowEvent* aThisEvent);
--- a/accessible/src/base/nsAccessibilityService.h +++ b/accessible/src/base/nsAccessibilityService.h @@ -468,17 +468,17 @@ static const char kEventTypeNames[][40] "minimize start", // EVENT_MINIMIZE_START "minimize end", // EVENT_MINIMIZE_END "document load complete", // EVENT_DOCUMENT_LOAD_COMPLETE "document reload", // EVENT_DOCUMENT_RELOAD "document load stopped", // EVENT_DOCUMENT_LOAD_STOPPED "document attributes changed", // EVENT_DOCUMENT_ATTRIBUTES_CHANGED "document content changed", // EVENT_DOCUMENT_CONTENT_CHANGED "property changed", // EVENT_PROPERTY_CHANGED - "selection changed", // EVENT_SELECTION_CHANGED + "page changed", // EVENT_PAGE_CHANGED "text attribute changed", // EVENT_TEXT_ATTRIBUTE_CHANGED "text caret moved", // EVENT_TEXT_CARET_MOVED "text changed", // EVENT_TEXT_CHANGED "text inserted", // EVENT_TEXT_INSERTED "text removed", // EVENT_TEXT_REMOVED "text updated", // EVENT_TEXT_UPDATED "text selection changed", // EVENT_TEXT_SELECTION_CHANGED "visible data changed", // EVENT_VISIBLE_DATA_CHANGED @@ -509,17 +509,16 @@ static const char kEventTypeNames[][40] "hyperlink number of anchors changed", // EVENT_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED "hyperlink selected link changed", // EVENT_HYPERLINK_SELECTED_LINK_CHANGED "hypertext link activated", // EVENT_HYPERTEXT_LINK_ACTIVATED "hypertext link selected", // EVENT_HYPERTEXT_LINK_SELECTED "hyperlink start index changed", // EVENT_HYPERLINK_START_INDEX_CHANGED "hypertext changed", // EVENT_HYPERTEXT_CHANGED "hypertext links count changed", // EVENT_HYPERTEXT_NLINKS_CHANGED "object attribute changed", // EVENT_OBJECT_ATTRIBUTE_CHANGED - "page changed" // EVENT_PAGE_CHANGED }; /** * Map nsIAccessibleRelation constants to strings. Used by * nsIAccessibleRetrieval::getStringRelationType() method. */ static const char kRelationTypeNames[][20] = { "unknown", // RELATION_NUL
--- a/accessible/src/base/nsDocAccessible.cpp +++ b/accessible/src/base/nsDocAccessible.cpp @@ -1037,42 +1037,33 @@ nsDocAccessible::AttributeChangedImpl(ns if (aAttribute == nsGkAtoms::aria_busy) { bool isOn = aContent->AttrValueIs(aNameSpaceID, aAttribute, nsGkAtoms::_true, eCaseMatters); nsRefPtr<AccEvent> event = new AccStateChangeEvent(aContent, states::BUSY, isOn); FireDelayedAccessibleEvent(event); return; } - if (aAttribute == nsGkAtoms::selected || + // ARIA or XUL selection + if ((aContent->IsXUL() && aAttribute == nsGkAtoms::selected) || aAttribute == nsGkAtoms::aria_selected) { - // ARIA or XUL selection + nsAccessible* item = GetAccessible(aContent); + nsAccessible* widget = + nsAccUtils::GetSelectableContainer(item, item->State()); + if (widget) { + AccSelChangeEvent::SelChangeType selChangeType = + aContent->AttrValueIs(aNameSpaceID, aAttribute, + nsGkAtoms::_true, eCaseMatters) ? + AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; - nsAccessible *multiSelect = - nsAccUtils::GetMultiSelectableContainer(aContent); - // XXX: Multi selects are handled here only (bug 414302). - if (multiSelect) { - // Need to find the right event to use here, SELECTION_WITHIN would - // seem right but we had started using it for something else - FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN, - multiSelect->GetNode(), - AccEvent::eAllowDupes); - - static nsIContent::AttrValuesArray strings[] = - {&nsGkAtoms::_empty, &nsGkAtoms::_false, nsnull}; - if (aContent->FindAttrValueIn(kNameSpaceID_None, aAttribute, - strings, eCaseMatters) >= 0) { - FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_REMOVE, - aContent); - return; - } - - FireDelayedAccessibleEvent(nsIAccessibleEvent::EVENT_SELECTION_ADD, - aContent); + nsRefPtr<AccEvent> event = + new AccSelChangeEvent(widget, item, selChangeType); + FireDelayedAccessibleEvent(event); } + return; } if (aAttribute == nsGkAtoms::contenteditable) { nsRefPtr<AccEvent> editableChangeEvent = new AccStateChangeEvent(aContent, states::EDITABLE); FireDelayedAccessibleEvent(editableChangeEvent); return; } @@ -1204,17 +1195,28 @@ void nsDocAccessible::ContentAppended(ns { } void nsDocAccessible::ContentStateChanged(nsIDocument* aDocument, nsIContent* aContent, nsEventStates aStateMask) { if (aStateMask.HasState(NS_EVENT_STATE_CHECKED)) { - nsHTMLSelectOptionAccessible::SelectionChangedIfOption(aContent); + nsAccessible* item = GetAccessible(aContent); + if (item) { + nsAccessible* widget = item->ContainerWidget(); + if (widget && widget->IsSelect()) { + AccSelChangeEvent::SelChangeType selChangeType = + aContent->AsElement()->State().HasState(NS_EVENT_STATE_CHECKED) ? + AccSelChangeEvent::eSelectionAdd : AccSelChangeEvent::eSelectionRemove; + nsRefPtr<AccEvent> event = new AccSelChangeEvent(widget, item, + selChangeType); + FireDelayedAccessibleEvent(event); + } + } } if (aStateMask.HasState(NS_EVENT_STATE_INVALID)) { nsRefPtr<AccEvent> event = new AccStateChangeEvent(aContent, states::INVALID, true); FireDelayedAccessibleEvent(event); } }
--- a/accessible/src/base/nsRootAccessible.cpp +++ b/accessible/src/base/nsRootAccessible.cpp @@ -468,16 +468,18 @@ nsRootAccessible::ProcessDOMEvent(nsIDOM nsRefPtr<AccEvent> accEvent = new AccStateChangeEvent(accessible, states::EXPANDED, isEnabled); nsEventShell::FireEvent(accEvent); return; } if (treeItemAccessible && eventType.EqualsLiteral("select")) { + // XXX: We shouldn't be based on DOM select event which doesn't provide us + // any context info. We should integrate into nsTreeSelection instead. // If multiselect tree, we should fire selectionadd or selection removed if (FocusMgr()->HasDOMFocus(targetNode)) { nsCOMPtr<nsIDOMXULMultiSelectControlElement> multiSel = do_QueryInterface(targetNode); nsAutoString selType; multiSel->GetSelType(selType); if (selType.IsEmpty() || !selType.EqualsLiteral("single")) { // XXX: We need to fire EVENT_SELECTION_ADD and EVENT_SELECTION_REMOVE
--- a/accessible/src/html/nsHTMLSelectAccessible.cpp +++ b/accessible/src/html/nsHTMLSelectAccessible.cpp @@ -411,69 +411,17 @@ nsHTMLSelectOptionAccessible::SetSelecte } //////////////////////////////////////////////////////////////////////////////// // nsHTMLSelectOptionAccessible: Widgets nsAccessible* nsHTMLSelectOptionAccessible::ContainerWidget() const { - if (mParent && mParent->IsListControl()) { - nsAccessible* grandParent = mParent->Parent(); - if (grandParent && grandParent->IsCombobox()) - return grandParent; - - return mParent; - } - return nsnull; -} - -//////////////////////////////////////////////////////////////////////////////// -// nsHTMLSelectOptionAccessible: static methods - -void -nsHTMLSelectOptionAccessible::SelectionChangedIfOption(nsIContent *aPossibleOptionNode) -{ - if (!aPossibleOptionNode || - aPossibleOptionNode->Tag() != nsGkAtoms::option || - !aPossibleOptionNode->IsHTML()) { - return; - } - - nsAccessible *multiSelect = - nsAccUtils::GetMultiSelectableContainer(aPossibleOptionNode); - if (!multiSelect) - return; - - nsAccessible *option = GetAccService()->GetAccessible(aPossibleOptionNode); - if (!option) - return; - - - nsRefPtr<AccEvent> selWithinEvent = - new AccEvent(nsIAccessibleEvent::EVENT_SELECTION_WITHIN, multiSelect); - - if (!selWithinEvent) - return; - - option->GetDocAccessible()->FireDelayedAccessibleEvent(selWithinEvent); - - PRUint64 state = option->State(); - PRUint32 eventType; - if (state & states::SELECTED) { - eventType = nsIAccessibleEvent::EVENT_SELECTION_ADD; - } - else { - eventType = nsIAccessibleEvent::EVENT_SELECTION_REMOVE; - } - - nsRefPtr<AccEvent> selAddRemoveEvent = new AccEvent(eventType, option); - - if (selAddRemoveEvent) - option->GetDocAccessible()->FireDelayedAccessibleEvent(selAddRemoveEvent); + return mParent && mParent->IsListControl() ? mParent : nsnull; } //////////////////////////////////////////////////////////////////////////////// // nsHTMLSelectOptionAccessible: private methods nsIContent* nsHTMLSelectOptionAccessible::GetSelectState(PRUint64* aState) { @@ -733,36 +681,32 @@ nsHTMLComboboxAccessible::AreItemsOperab { nsIComboboxControlFrame* comboboxFrame = do_QueryFrame(GetFrame()); return comboboxFrame && comboboxFrame->IsDroppedDown(); } nsAccessible* nsHTMLComboboxAccessible::CurrentItem() { - // No current item for collapsed combobox. - return SelectedOption(true); + return AreItemsOperable() ? mListAccessible->CurrentItem() : nsnull; } //////////////////////////////////////////////////////////////////////////////// // nsHTMLComboboxAccessible: protected nsAccessible* -nsHTMLComboboxAccessible::SelectedOption(bool aIgnoreIfCollapsed) const +nsHTMLComboboxAccessible::SelectedOption() const { nsIFrame* frame = GetFrame(); nsIComboboxControlFrame* comboboxFrame = do_QueryFrame(frame); - if (comboboxFrame) { - if (aIgnoreIfCollapsed && !comboboxFrame->IsDroppedDown()) - return nsnull; + if (!comboboxFrame) + return nsnull; - frame = comboboxFrame->GetDropDown(); - } - - nsIListControlFrame* listControlFrame = do_QueryFrame(frame); + nsIListControlFrame* listControlFrame = + do_QueryFrame(comboboxFrame->GetDropDown()); if (listControlFrame) { nsCOMPtr<nsIContent> activeOptionNode = listControlFrame->GetCurrentOption(); if (activeOptionNode) { nsDocAccessible* document = GetDocAccessible(); if (document) return document->GetAccessible(activeOptionNode); } } @@ -853,8 +797,24 @@ void nsHTMLComboboxListAccessible::GetBo if (!frame) { *aBoundingFrame = nsnull; return; } *aBoundingFrame = frame->GetParent(); aBounds = (*aBoundingFrame)->GetRect(); } + +//////////////////////////////////////////////////////////////////////////////// +// nsHTMLComboboxListAccessible: Widgets + +bool +nsHTMLComboboxListAccessible::IsActiveWidget() const +{ + return mParent && mParent->IsActiveWidget(); +} + +bool +nsHTMLComboboxListAccessible::AreItemsOperable() const +{ + return mParent && mParent->AreItemsOperable(); +} +
--- a/accessible/src/html/nsHTMLSelectAccessible.h +++ b/accessible/src/html/nsHTMLSelectAccessible.h @@ -125,18 +125,16 @@ public: PRInt32 *aSetSize); // ActionAccessible virtual PRUint8 ActionCount(); // Widgets virtual nsAccessible* ContainerWidget() const; - static void SelectionChangedIfOption(nsIContent *aPossibleOption); - protected: // nsAccessible virtual nsIFrame* GetBoundsFrame(); private: /** * Get Select element's accessible state @@ -214,17 +212,17 @@ public: protected: // nsAccessible virtual void CacheChildren(); /** * Return selected option. */ - nsAccessible* SelectedOption(bool aIgnoreIfCollapsed = false) const; + nsAccessible* SelectedOption() const; private: nsRefPtr<nsHTMLComboboxListAccessible> mListAccessible; }; /* * A class that represents the window that lives to the right * of the drop down button inside the Select. This is the window @@ -241,11 +239,15 @@ public: // nsAccessNode virtual nsIFrame* GetFrame() const; virtual bool IsPrimaryForNode() const; // nsAccessible virtual PRUint64 NativeState(); virtual void GetBoundsRect(nsRect& aBounds, nsIFrame** aBoundingFrame); + + // Widgets + virtual bool IsActiveWidget() const; + virtual bool AreItemsOperable() const; }; #endif
--- a/accessible/src/mac/mozAccessible.mm +++ b/accessible/src/mac/mozAccessible.mm @@ -185,19 +185,17 @@ GetNativeFromGeckoAccessible(nsIAccessib if (!generalAttributes) { // standard attributes that are shared and supported by all generic elements. generalAttributes = [[NSArray alloc] initWithObjects: NSAccessibilityChildrenAttribute, NSAccessibilityParentAttribute, NSAccessibilityRoleAttribute, NSAccessibilityTitleAttribute, NSAccessibilityValueAttribute, NSAccessibilitySubroleAttribute, -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 NSAccessibilityRoleDescriptionAttribute, -#endif NSAccessibilityPositionAttribute, NSAccessibilityEnabledAttribute, NSAccessibilitySizeAttribute, NSAccessibilityWindowAttribute, NSAccessibilityFocusedAttribute, NSAccessibilityHelpAttribute, NSAccessibilityTitleUIElementAttribute, kTopLevelUIElementAttribute, @@ -231,20 +229,18 @@ GetNativeFromGeckoAccessible(nsIAccessib if ([attribute isEqualToString:NSAccessibilityPositionAttribute]) return [self position]; if ([attribute isEqualToString:NSAccessibilitySubroleAttribute]) return [self subrole]; if ([attribute isEqualToString:NSAccessibilityEnabledAttribute]) return [NSNumber numberWithBool:[self isEnabled]]; if ([attribute isEqualToString:NSAccessibilityValueAttribute]) return [self value]; -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 if ([attribute isEqualToString:NSAccessibilityRoleDescriptionAttribute]) return NSAccessibilityRoleDescription([self role], nil); -#endif if ([attribute isEqualToString: (NSString*) kInstanceDescriptionAttribute]) return [self customDescription]; if ([attribute isEqualToString:NSAccessibilityFocusedAttribute]) return [NSNumber numberWithBool:[self isFocused]]; if ([attribute isEqualToString:NSAccessibilitySizeAttribute]) return [self size]; if ([attribute isEqualToString:NSAccessibilityWindowAttribute]) return [self window];
--- a/accessible/src/mac/mozActionElements.mm +++ b/accessible/src/mac/mozActionElements.mm @@ -221,34 +221,30 @@ enum CheckboxValue { } - (NSArray *)accessibilityActionNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if ([self isEnabled]) { return [NSArray arrayWithObjects:NSAccessibilityPressAction, -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 NSAccessibilityShowMenuAction, -#endif nil]; } return nil; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (NSString *)accessibilityActionDescription:(NSString *)action { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 if ([action isEqualToString:NSAccessibilityShowMenuAction]) return @"show menu"; -#endif return [super accessibilityActionDescription:action]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (void)accessibilityPerformAction:(NSString *)action { NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
--- a/accessible/src/mac/mozTextAccessible.mm +++ b/accessible/src/mac/mozTextAccessible.mm @@ -44,19 +44,17 @@ extern const NSString *kTopLevelUIElemen static NSArray *supportedAttributes = nil; if (!supportedAttributes) { // standard attributes that are shared and supported by all generic elements. supportedAttributes = [[NSArray alloc] initWithObjects:NSAccessibilityParentAttribute, // required NSAccessibilityRoleAttribute, // required NSAccessibilityTitleAttribute, NSAccessibilityValueAttribute, // required NSAccessibilitySubroleAttribute, -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 NSAccessibilityRoleDescriptionAttribute, -#endif NSAccessibilityPositionAttribute, // required NSAccessibilitySizeAttribute, // required NSAccessibilityWindowAttribute, // required NSAccessibilityFocusedAttribute, // required NSAccessibilityEnabledAttribute, // required kTopLevelUIElementAttribute, // required (on OS X 10.4+) kInstanceDescriptionAttribute, // required (on OS X 10.4+) /* text-specific attributes */ @@ -244,19 +242,17 @@ extern const NSString *kTopLevelUIElemen static NSArray *supportedAttributes = nil; if (!supportedAttributes) { // standard attributes that are shared and supported by all generic elements. supportedAttributes = [[NSArray alloc] initWithObjects:NSAccessibilityParentAttribute, // required NSAccessibilityRoleAttribute, // required NSAccessibilityTitleAttribute, NSAccessibilityValueAttribute, // required NSAccessibilityHelpAttribute, -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 NSAccessibilityRoleDescriptionAttribute, -#endif NSAccessibilityPositionAttribute, // required NSAccessibilitySizeAttribute, // required NSAccessibilityWindowAttribute, // required NSAccessibilityFocusedAttribute, // required NSAccessibilityEnabledAttribute, // required NSAccessibilityChildrenAttribute, // required NSAccessibilityHelpAttribute, // NSAccessibilityExpandedAttribute, // required @@ -276,52 +272,46 @@ extern const NSString *kTopLevelUIElemen } - (NSArray *)accessibilityActionNames { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; if ([self isEnabled]) { return [NSArray arrayWithObjects:NSAccessibilityConfirmAction, -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 NSAccessibilityShowMenuAction, -#endif nil]; } return nil; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (NSString *)accessibilityActionDescription:(NSString *)action { NS_OBJC_BEGIN_TRY_ABORT_BLOCK_NIL; -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 if ([action isEqualToString:NSAccessibilityShowMenuAction]) return @"show menu"; -#endif if ([action isEqualToString:NSAccessibilityConfirmAction]) return @"confirm"; return [super accessibilityActionDescription:action]; NS_OBJC_END_TRY_ABORT_BLOCK_NIL; } - (void)accessibilityPerformAction:(NSString *)action { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; // both the ShowMenu and Click action do the same thing. if ([self isEnabled]) { -#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_4 if ([action isEqualToString:NSAccessibilityShowMenuAction]) [self showMenu]; -#endif if ([action isEqualToString:NSAccessibilityConfirmAction]) [self confirm]; } NS_OBJC_END_TRY_ABORT_BLOCK; } - (void)showMenu
--- a/accessible/src/msaa/nsEventMap.h +++ b/accessible/src/msaa/nsEventMap.h @@ -85,17 +85,17 @@ static const PRUint32 gWinEventMap[] = { kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_MINIMIZE_START kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_MINIMIZE_END IA2_EVENT_DOCUMENT_LOAD_COMPLETE, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE IA2_EVENT_DOCUMENT_RELOAD, // nsIAccessibleEvent::EVENT_DOCUMENT_RELOAD IA2_EVENT_DOCUMENT_LOAD_STOPPED, // nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED IA2_EVENT_DOCUMENT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_DOCUMENT_ATTRIBUTES_CHANGED IA2_EVENT_DOCUMENT_CONTENT_CHANGED, // nsIAccessibleEvent::EVENT_DOCUMENT_CONTENT_CHANGED kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_PROPERTY_CHANGED - kEVENT_WIN_UNKNOWN, // nsIAccessibleEvent::EVENT_SELECTION_CHANGED + IA2_EVENT_PAGE_CHANGED, // nsIAccessibleEvent::IA2_EVENT_PAGE_CHANGED IA2_EVENT_TEXT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_ATTRIBUTE_CHANGED IA2_EVENT_TEXT_CARET_MOVED, // nsIAccessibleEvent::EVENT_TEXT_CARET_MOVED IA2_EVENT_TEXT_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_CHANGED IA2_EVENT_TEXT_INSERTED, // nsIAccessibleEvent::EVENT_TEXT_INSERTED IA2_EVENT_TEXT_REMOVED, // nsIAccessibleEvent::EVENT_TEXT_REMOVED IA2_EVENT_TEXT_UPDATED, // nsIAccessibleEvent::EVENT_TEXT_UPDATED IA2_EVENT_TEXT_SELECTION_CHANGED, // nsIAccessibleEvent::EVENT_TEXT_SELECTION_CHANGED IA2_EVENT_VISIBLE_DATA_CHANGED, // nsIAccessibleEvent::EVENT_VISIBLE_DATA_CHANGED @@ -126,12 +126,11 @@ static const PRUint32 gWinEventMap[] = { IA2_EVENT_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED, // nsIAccessibleEvent::EVENT_HYPERLINK_NUMBER_OF_ANCHORS_CHANGED IA2_EVENT_HYPERLINK_SELECTED_LINK_CHANGED, // nsIAccessibleEvent::EVENT_HYPERLINK_SELECTED_LINK_CHANGED IA2_EVENT_HYPERTEXT_LINK_ACTIVATED, // nsIAccessibleEvent::EVENT_HYPERTEXT_LINK_ACTIVATED IA2_EVENT_HYPERTEXT_LINK_SELECTED, // nsIAccessibleEvent::EVENT_HYPERTEXT_LINK_SELECTED IA2_EVENT_HYPERLINK_START_INDEX_CHANGED, // nsIAccessibleEvent::EVENT_HYPERLINK_START_INDEX_CHANGED IA2_EVENT_HYPERTEXT_CHANGED, // nsIAccessibleEvent::EVENT_HYPERTEXT_CHANGED IA2_EVENT_HYPERTEXT_NLINKS_CHANGED, // nsIAccessibleEvent::EVENT_HYPERTEXT_NLINKS_CHANGED IA2_EVENT_OBJECT_ATTRIBUTE_CHANGED, // nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED - IA2_EVENT_PAGE_CHANGED, // nsIAccessibleEvent::EVENT_PAGE_CHANGED kEVENT_LAST_ENTRY // nsIAccessibleEvent::EVENT_LAST_ENTRY };
--- a/accessible/tests/mochitest/events.js +++ b/accessible/tests/mochitest/events.js @@ -10,17 +10,19 @@ const EVENT_FOCUS = nsIAccessibleEvent.E const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; const EVENT_OBJECT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; +const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; +const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; const EVENT_TEXT_ATTRIBUTE_CHANGED = nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; const EVENT_TEXT_SELECTION_CHANGED = nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED;
--- a/accessible/tests/mochitest/events/Makefile.in +++ b/accessible/tests/mochitest/events/Makefile.in @@ -77,17 +77,19 @@ include $(topsrcdir)/config/rules.mk test_focus_name.html \ test_focus_selects.html \ test_focus_tabbox.xul \ test_focus_tree.xul \ test_menu.xul \ test_mutation.html \ test_mutation.xhtml \ test_scroll.xul \ + test_selection_aria.html \ test_selection.html \ + test_selection.xul \ test_statechange.html \ test_text_alg.html \ test_text.html \ test_textattrchange.html \ test_tree.xul \ test_valuechange.html \ $(NULL)
--- a/accessible/tests/mochitest/events/test_selection.html +++ b/accessible/tests/mochitest/events/test_selection.html @@ -17,78 +17,97 @@ src="../events.js"></script> <script type="application/javascript" src="../states.js"></script> <script type="application/javascript"> //////////////////////////////////////////////////////////////////////////// // Invokers - function addSelection(aNode, aOption) - { - this.DOMNode = aNode; - this.optionNode = aOption; - - this.eventSeq = [ - new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.DOMNode)), - new invokerChecker(EVENT_SELECTION_ADD, getAccessible(this.optionNode)) - ]; - - this.invoke = function addselection_invoke() { - synthesizeMouse(this.optionNode, 1, 1, {}); - }; - - this.getID = function addselection_getID() { - return prettyName(this.optionNode) + " added to selection"; - }; - } - //////////////////////////////////////////////////////////////////////////// // Do tests - var gQueue = null; + //gA11yEventDumpToConsole = true; // debuggin - //var gA11yEventDumpID = "eventdump"; // debug stuff - + var gQueue = null; function doTests() { gQueue = new eventQueue(); - var select = document.getElementById("toppings"); - var option = document.getElementById("onions"); - gQueue.push(new addSelection(select, option)); + // open combobox + gQueue.push(new synthClick("combobox", + new invokerChecker(EVENT_FOCUS, "cb1_item1"))); + gQueue.push(new synthDownKey("cb1_item1", + new invokerChecker(EVENT_SELECTION, "cb1_item2"))); + + // closed combobox + gQueue.push(new synthEscapeKey("combobox", + new invokerChecker(EVENT_FOCUS, "combobox"))); + gQueue.push(new synthDownKey("cb1_item2", + new invokerChecker(EVENT_SELECTION, "cb1_item3"))); + + // listbox + gQueue.push(new synthClick("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); gQueue.invoke(); // Will call SimpleTest.finish(); } SimpleTest.waitForExplicitFinish(); addA11yLoadEvent(doTests); </script> </head> <body> <a target="_blank" - href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653" - title="Make selection events async"> - Mozilla Bug 569653 + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Mozilla Bug 414302 </a> <p id="display"></p> <div id="content" style="display: none"></div> <pre id="test"> </pre> - <p>Pizza</p> - <select id="toppings" name="toppings" multiple size=5> - <option value="mushrooms">mushrooms - <option value="greenpeppers">green peppers - <option value="onions" id="onions">onions - <option value="tomatoes">tomatoes - <option value="olives">olives + <select id="combobox"> + <option id="cb1_item1" value="mushrooms">mushrooms + <option id="cb1_item2" value="greenpeppers">green peppers + <option id="cb1_item3" value="onions" id="onions">onions + <option id="cb1_item4" value="tomatoes">tomatoes + <option id="cb1_item5" value="olives">olives </select> - <div id="testContainer"> - <iframe id="iframe"></iframe> - </div> + <select id="listbox" size=5> + <option id="lb1_item1" value="mushrooms">mushrooms + <option id="lb1_item2" value="greenpeppers">green peppers + <option id="lb1_item3" value="onions" id="onions">onions + <option id="lb1_item4" value="tomatoes">tomatoes + <option id="lb1_item5" value="olives">olives + </select> + + <p>Pizza</p> + <select id="listbox2" multiple size=5> + <option id="lb2_item1" value="mushrooms">mushrooms + <option id="lb2_item2" value="greenpeppers">green peppers + <option id="lb2_item3" value="onions" id="onions">onions + <option id="lb2_item4" value="tomatoes">tomatoes + <option id="lb2_item5" value="olives">olives + </select> + <div id="eventdump"></div> </body> </html>
new file mode 100644 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.xul @@ -0,0 +1,244 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Selection event tests"> + + <script type="application/javascript" + src="chrome://mochikit/content/MochiKit/packed.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + function advanceTab(aTabsID, aDirection, aNextTabID) + { + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION, aNextTabID) + ]; + + this.invoke = function advanceTab_invoke() + { + getNode(aTabsID).advanceSelectedTab(aDirection, true); + } + + this.getID = function synthFocus_getID() + { + return "advanceTab on " + prettyName(aTabsID) + " to " + prettyName(aNextTabID); + } + } + + function select4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(3)) + ]; + + this.invoke = function select4FirstItems_invoke() + { + synthesizeKey("VK_DOWN", { shiftKey: true }); // selects two items + synthesizeKey("VK_DOWN", { shiftKey: true }); + synthesizeKey("VK_DOWN", { shiftKey: true }); + } + + this.getID = function select4FirstItems_getID() + { + return "select 4 first items for " + prettyName(aID); + } + } + + function unselect4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(3)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselect4FirstItems_invoke() + { + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey(" ", { ctrlKey: true }); // unselect first item + } + + this.getID = function unselect4FirstItems_getID() + { + return "unselect 4 first items for " + prettyName(aID); + } + } + + function selectAllItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function selectAllItems_invoke() + { + synthesizeKey("VK_END", { shiftKey: true }); + } + + this.getID = function selectAllItems_getID() + { + return "select all items for " + prettyName(aID); + } + } + + function unselectAllItemsButFirst(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function unselectAllItemsButFirst_invoke() + { + synthesizeKey("VK_HOME", { shiftKey: true }); + } + + this.getID = function unselectAllItemsButFirst_getID() + { + return "unselect all items for " + prettyName(aID); + } + } + + function unselectSelectItem(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselectSelectItem_invoke() + { + synthesizeKey(" ", { ctrlKey: true }); // select item + synthesizeKey(" ", { ctrlKey: true }); // unselect item + } + + this.getID = function unselectSelectItem_getID() + { + return "unselect and then select first item for " + prettyName(aID); + } + } + + /** + * Do tests. + */ + var gQueue = null; + + //gA11yEventDumpToConsole = true; // debuggin + + function doTests() + { + gQueue = new eventQueue(); + + ////////////////////////////////////////////////////////////////////////// + // tabbox + gQueue.push(new advanceTab("tabs", 1, "tab3")); + + ////////////////////////////////////////////////////////////////////////// + // listbox + gQueue.push(new synthClick("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + + ////////////////////////////////////////////////////////////////////////// + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); + + ////////////////////////////////////////////////////////////////////////// + // selection event coalescence + + // fire 4 selection_add events + gQueue.push(new select4FirstItems("listbox2")); + // fire 4 selection_remove events + gQueue.push(new unselect4FirstItems("listbox2")); + // fire selection_within event + gQueue.push(new selectAllItems("listbox2")); + // fire selection_within event + gQueue.push(new unselectAllItemsButFirst("listbox2")); + // fire selection_remove/add events + gQueue.push(new unselectSelectItem("listbox2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Mozilla Bug 414302 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <tabbox id="tabbox" selectedIndex="1"> + <tabs id="tabs"> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab2"/> + <tab id="tab3" label="tab3"/> + <tab id="tab4" label="tab4"/> + </tabs> + <tabpanels> + <tabpanel><!-- tabpanel First elements go here --></tabpanel> + <tabpanel><button id="b1" label="b1"/></tabpanel> + <tabpanel><button id="b2" label="b2"/></tabpanel> + <tabpanel></tabpanel> + </tabpanels> + </tabbox> + + <listbox id="listbox"> + <listitem id="lb1_item1" label="item1"/> + <listitem id="lb1_item2" label="item2"/> + </listbox> + + <listbox id="listbox2" seltype="multiple"> + <listitem id="lb2_item1" label="item1"/> + <listitem id="lb2_item2" label="item2"/> + <listitem id="lb2_item3" label="item3"/> + <listitem id="lb2_item4" label="item4"/> + <listitem id="lb2_item5" label="item5"/> + <listitem id="lb2_item6" label="item6"/> + <listitem id="lb2_item7" label="item7"/> + </listbox> + + </hbox> +</window>
new file mode 100644 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection_aria.html @@ -0,0 +1,112 @@ +<html> + +<head> + <title>ARIA selection event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function selectTreeItem(aTreeID, aItemID) + { + this.treeNode = getNode(aTreeID); + this.itemNode = getNode(aItemID); + + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION, aItemID) + ]; + + this.invoke = function selectTreeItem_invoke() { + var itemNode = this.treeNode.querySelector("*[aria-selected='true']"); + if (itemNode) + itemNode.removeAttribute("aria-selected", "true"); + + this.itemNode.setAttribute("aria-selected", "true"); + } + + this.getID = function selectTreeItem_getID() + { + return "selectTreeItem " + prettyName(aItemID); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + + //var gA11yEventDumpID = "eventdump"; // debug stuff + + function doTests() + { + gQueue = new eventQueue(); + + gQueue.push(new selectTreeItem("tree", "treeitem1")); + gQueue.push(new selectTreeItem("tree", "treeitem1a")); + gQueue.push(new selectTreeItem("tree", "treeitem1a1")); + + gQueue.push(new selectTreeItem("tree2", "tree2item1")); + gQueue.push(new selectTreeItem("tree2", "tree2item1a")); + gQueue.push(new selectTreeItem("tree2", "tree2item1a1")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653" + title="Make selection events async"> + Mozilla Bug 569653 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="tree" role="tree"> + <div id="treeitem1" role="treeitem">Canada + <div id="treeitem1a" role="treeitem">- Ontario + <div id="treeitem1a1" role="treeitem">-- Toronto</div> + </div> + <div id="treeitem1b" role="treeitem">- Manitoba</div> + </div> + <div id="treeitem2" role="treeitem">Germany</div> + <div id="treeitem3" role="treeitem">Russia</div> + </div> + + <div id="tree2" role="tree" aria-multiselectable="true"> + <div id="tree2item1" role="treeitem">Canada + <div id="tree2item1a" role="treeitem">- Ontario + <div id="tree2item1a1" role="treeitem">-- Toronto</div> + </div> + <div id="tree2item1b" role="treeitem">- Manitoba</div> + </div> + <div id="tree2item2" role="treeitem">Germany</div> + <div id="tree2item3" role="treeitem">Russia</div> + </div> + + <div id="eventdump"></div> +</body> +</html>
--- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -49,16 +49,20 @@ #endif pref("browser.chromeURL","chrome://browser/content/"); pref("browser.hiddenWindowChromeURL", "chrome://browser/content/hiddenWindow.xul"); // Enables some extra Extension System Logging (can reduce performance) pref("extensions.logging.enabled", false); +// Enables strict compatibility. To be toggled in bug 698653, to make addons +// compatibile by default. +pref("extensions.strictCompatibility", true); + // Preferences for AMO integration pref("extensions.getAddons.cache.enabled", true); pref("extensions.getAddons.maxResults", 15); pref("extensions.getAddons.get.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/guid:%IDS%?src=firefox&appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%"); pref("extensions.getAddons.search.browseURL", "https://addons.mozilla.org/%LOCALE%/firefox/search?q=%TERMS%"); pref("extensions.getAddons.search.url", "https://services.addons.mozilla.org/%LOCALE%/firefox/api/%API_VERSION%/search/%TERMS%/all/%MAX_RESULTS%/%OS%/%VERSION%?src=firefox"); pref("extensions.webservice.discoverURL", "https://services.addons.mozilla.org/%LOCALE%/firefox/discovery/pane/%VERSION%/%OS%");
--- a/browser/base/content/aboutHome.css +++ b/browser/base/content/aboutHome.css @@ -364,13 +364,21 @@ body[dir=rtl] #restorePreviousSession::b position: absolute; color: rgb(150,150,150); font-size: .8em; width: 100%; text-align: center; bottom: 2%; } +#syncLinksContainer { + padding-top: 1em; +} + +.sync-link { + padding: 1em; +} + @media all and (max-height: 370px) { #bottomSection { visibility: hidden; } }
--- a/browser/base/content/aboutHome.xhtml +++ b/browser/base/content/aboutHome.xhtml @@ -97,11 +97,15 @@ <button id="restorePreviousSession">&historyRestoreLastSession.label;</button> </div> </div> <div id="bottomSection"> <div id="aboutMozilla"> <a href="http://www.mozilla.com/about/">&abouthome.aboutMozilla;</a> </div> + <div id="syncLinksContainer"> + <a href="javascript:void(0);" class="sync-link" id="setupSyncLink">&abouthome.syncSetup.label;</a> + <a href="javascript:void(0);" class="sync-link" id="pairDeviceLink">&abouthome.pairDevice.label;</a> + </div> </div> </body> </html>
--- a/browser/base/content/browser-sets.inc +++ b/browser/base/content/browser-sets.inc @@ -140,22 +140,18 @@ oncommand="PlacesCommandHook.showPlacesOrganizer('AllBookmarks');"/> <command id="Browser:ShowAllHistory" oncommand="PlacesCommandHook.showPlacesOrganizer('History');"/> </commandset> <commandset id="inspectorCommands"> <command id="Inspector:Inspect" oncommand="InspectorUI.toggleInspection();"/> - <command id="Inspector:Previous" - oncommand="InspectorUI.inspectPrevious();" - disabled="true"/> - <command id="Inspector:Next" - oncommand="InspectorUI.inspectNext();" - disabled="true"/> + <command id="Inspector:Sidebar" + oncommand="InspectorUI.toggleSidebar();"/> </commandset> <broadcasterset id="mainBroadcasterSet"> <broadcaster id="viewBookmarksSidebar" autoCheck="false" label="&bookmarksButton.label;" type="checkbox" group="sidebar" sidebarurl="chrome://browser/content/bookmarks/bookmarksPanel.xul" oncommand="toggleSidebar('viewBookmarksSidebar');"/> <!-- for both places and non-places, the sidebar lives at
--- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -206,16 +206,34 @@ let gSyncUI = { }, onLoginFinish: function SUI_onLoginFinish() { // Clear out any login failure notifications let title = this._stringBundle.GetStringFromName("error.login.title"); this.clearError(title); }, + // Set visibility of "Setup Sync" link + showSetupSyncAboutHome: function SUI_showSetupSyncAboutHome(toShow) { + let browsers = gBrowser.browsers; + for (let i = 0; i < browsers.length; i++) { + let b = browsers[i]; + if ("about:home" == b.currentURI.spec) { + b.contentDocument.getElementById("setupSyncLink").hidden = !toShow; + } + } + }, + + onSetupComplete: function SUI_onSetupComplete() { + // Remove "setup sync" link in about:home if it is open. + this.showSetupSyncAboutHome(false); + + onLoginFinish(); + }, + onLoginError: function SUI_onLoginError() { // if login fails, any other notifications are essentially moot Weave.Notifications.removeAll(); // if we haven't set up the client, don't show errors if (this._needsSetup()) { this.updateUI(); return; @@ -250,16 +268,18 @@ let gSyncUI = { }, onLogout: function SUI_onLogout() { this.updateUI(); }, onStartOver: function SUI_onStartOver() { this.clearError(); + // Make "setup sync" link visible in about:home if it is open. + this.showSetupSyncAboutHome(true); }, onQuotaNotice: function onQuotaNotice(subject, data) { let title = this._stringBundle.GetStringFromName("warning.sync.quota.label"); let description = this._stringBundle.GetStringFromName("warning.sync.quota.description"); let buttons = []; buttons.push(new Weave.NotificationButton( this._stringBundle.GetStringFromName("error.sync.viewQuotaButton.label"), @@ -286,26 +306,50 @@ let gSyncUI = { if (this._needsSetup()) this.openSetup(); else this.doSync(); }, //XXXzpao should be part of syncCommon.js - which we might want to make a module... // To be fixed in a followup (bug 583366) - openSetup: function SUI_openSetup() { + + /** + * Invoke the Sync setup wizard. + * + * @param wizardType + * Indicates type of wizard to launch: + * null -- regular set up wizard + * "pair" -- pair a device first + * "reset" -- reset sync + */ + + openSetup: function SUI_openSetup(wizardType) { let win = Services.wm.getMostRecentWindow("Weave:AccountSetup"); if (win) win.focus(); else { window.openDialog("chrome://browser/content/syncSetup.xul", - "weaveSetup", "centerscreen,chrome,resizable=no"); + "weaveSetup", "centerscreen,chrome,resizable=no", + wizardType); } }, + openAddDevice: function () { + if (!Weave.Utils.ensureMPUnlocked()) + return; + + let win = Services.wm.getMostRecentWindow("Sync:AddDevice"); + if (win) + win.focus(); + else + window.openDialog("chrome://browser/content/syncAddDevice.xul", + "syncAddDevice", "centerscreen,chrome,resizable=no"); + }, + openQuotaDialog: function SUI_openQuotaDialog() { let win = Services.wm.getMostRecentWindow("Sync:ViewQuota"); if (win) win.focus(); else Services.ww.activeWindow.openDialog( "chrome://browser/content/syncQuota.xul", "", "centerscreen,chrome,dialog,modal"); @@ -457,17 +501,17 @@ let gSyncUI = { break; case "weave:service:sync:delayed": this.onSyncDelay(); break; case "weave:service:quota:remaining": this.onQuotaNotice(); break; case "weave:service:setup-complete": - this.onLoginFinish(); + this.onSetupComplete(); break; case "weave:service:login:start": this.onActivityStart(); break; case "weave:service:login:finish": this.onLoginFinish(); break; case "weave:ui:login:error":
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -2670,25 +2670,31 @@ function PageProxyClickHandler(aEvent) * to the DOM for unprivileged pages. */ function BrowserOnAboutPageLoad(document) { if (/^about:home$/i.test(document.documentURI)) { let ss = Components.classes["@mozilla.org/browser/sessionstore;1"]. getService(Components.interfaces.nsISessionStore); if (!ss.canRestoreLastSession) document.getElementById("sessionRestoreContainer").hidden = true; + // Sync-related links + if (Services.prefs.prefHasUserValue("services.sync.username")) { + document.getElementById("setupSyncLink").hidden = true; + } } } /** * Handle command events bubbling up from error page content */ function BrowserOnClick(event) { // Don't trust synthetic events - if (!event.isTrusted || event.target.localName != "button") + if (!event.isTrusted || + (event.target.localName != "button" && + event.target.className != "sync-link")) return; var ot = event.originalTarget; var errorDoc = ot.ownerDocument; // If the event came from an ssl error page, it is probably either the "Add // Exception…" or "Get me out of here!" button if (/^about:certerror/.test(errorDoc.documentURI)) { @@ -2808,16 +2814,26 @@ function BrowserOnClick(event) { else if (/^about:home$/i.test(errorDoc.documentURI)) { if (ot == errorDoc.getElementById("restorePreviousSession")) { let ss = Cc["@mozilla.org/browser/sessionstore;1"]. getService(Ci.nsISessionStore); if (ss.canRestoreLastSession) ss.restoreLastSession(); errorDoc.getElementById("sessionRestoreContainer").hidden = true; } + else if (ot == errorDoc.getElementById("pairDeviceLink")) { + if (Services.prefs.prefHasUserValue("services.sync.username")) { + gSyncUI.openAddDevice(); + } else { + gSyncUI.openSetup("pair"); + } + } + else if (ot == errorDoc.getElementById("setupSyncLink")) { + gSyncUI.openSetup(null); + } } } /** * Re-direct the browser to a known-safe page. This function is * used when, for example, the user browses to a known malware page * and is presented with about:blocked. The "Get me out of here!" * button should take the user to the default start page so that even @@ -3887,22 +3903,39 @@ var FullScreen = { this._isAnimating = false; // This is needed if they use the context menu to quit fullscreen this._isPopupOpen = false; this.cleanup(); } }, + exitDomFullScreen : function(e) { + document.mozCancelFullScreen(); + }, + enterDomFullScreen : function(event) { - if (!document.mozFullScreen) { + // We receive "mozfullscreenchange" events for each subdocument which + // is an ancestor of the document containing the element which requested + // full-screen. Only add listeners and show warning etc when the event we + // receive is targeted at the chrome document, i.e. only once every time + // we enter DOM full-screen mode. + if (!document.mozFullScreen || event.target.ownerDocument != document) { return; } this.showWarning(true); + // Exit DOM full-screen mode upon open, close, or change tab. + gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); + + // Exit DOM full-screen mode when the browser window loses focus (ALT+TAB, etc). + window.addEventListener("deactivate", this.exitDomFullScreen, true); + // Cancel any "hide the toolbar" animation which is in progress, and make // the toolbar hide immediately. clearInterval(this._animationInterval); clearTimeout(this._animationTimeout); this._isAnimating = false; this._shouldAnimate = false; this.mouseoverToggle(false); @@ -3925,16 +3958,20 @@ var FullScreen = { gPrefService.removeObserver("browser.fullscreen", this); let fullScrToggler = document.getElementById("fullscr-toggler"); if (fullScrToggler) { fullScrToggler.removeEventListener("mouseover", this._expandCallback, false); fullScrToggler.removeEventListener("dragenter", this._expandCallback, false); } this.cancelWarning(); + gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen); + gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen); + window.removeEventListener("deactivate", this.exitDomFullScreen, true); } }, observe: function(aSubject, aTopic, aData) { if (aData == "browser.fullscreen.autohide") { if (gPrefService.getBoolPref("browser.fullscreen.autohide")) { gBrowser.mPanelContainer.addEventListener("mousemove", @@ -8895,24 +8932,26 @@ function toggleAddonBar() { let addonBar = document.getElementById("addon-bar"); setToolbarVisibility(addonBar, addonBar.collapsed); } var Scratchpad = { prefEnabledName: "devtools.scratchpad.enabled", openScratchpad: function SP_openScratchpad() { - const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; - const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; - - return Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", - SCRATCHPAD_WINDOW_FEATURES, null); - }, + return this.ScratchpadManager.openScratchpad(); + } }; +XPCOMUtils.defineLazyGetter(Scratchpad, "ScratchpadManager", function() { + let tmp = {}; + Cu.import("resource:///modules/devtools/scratchpad-manager.jsm", tmp); + return tmp.ScratchpadManager; +}); + XPCOMUtils.defineLazyGetter(window, "gShowPageResizers", function () { #ifdef XP_WIN // Only show resizers on Windows 2000 and XP let sysInfo = Components.classes["@mozilla.org/system-info;1"] .getService(Components.interfaces.nsIPropertyBag2); return parseFloat(sysInfo.getProperty("version")) < 6; #else
--- a/browser/base/content/browser.xul +++ b/browser/base/content/browser.xul @@ -961,16 +961,22 @@ <tabbrowser id="content" disablehistory="true" flex="1" contenttooltip="aHTMLTooltip" tabcontainer="tabbrowser-tabs" contentcontextmenu="contentAreaContextMenu" autocompletepopup="PopupAutoComplete" onclick="return contentAreaClick(event, false);"/> <statuspanel id="statusbar-display" inactive="true"/> </vbox> + <splitter id="devtools-side-splitter" hidden="true"/> + <vbox id="devtools-sidebar-box" hidden="true" flex="1" + style="min-width: 18em; width: 22em; max-width: 42em;" persist="width"> + <toolbar id="devtools-sidebar-toolbar" nowindowdrag="true"/> + <deck id="devtools-sidebar-deck" flex="1"/> + </vbox> <vbox id="browser-border-end" hidden="true" layer="true"/> </hbox> <hbox id="full-screen-warning-container" hidden="true" fadeout="true"> <hbox style="min-width: 100%;" pack="center"> <!-- Inner hbox needed due to bug 579776. --> <hbox id="full-screen-warning-message"> <description id="full-screen-warning-text" value="&domFullScreenWarning.label;"></description> </hbox> @@ -982,26 +988,40 @@ nowindowdrag="true" hidden="true"> <vbox flex="1"> <resizer id="inspector-top-resizer" flex="1" class="inspector-resizer" dir="top" disabled="true" element="inspector-tree-box"/> <hbox> +#ifdef XP_MACOSX + <toolbarbutton id="highlighter-closebutton" + oncommand="InspectorUI.closeInspectorUI(false);" + tooltiptext="&inspectCloseButton.tooltiptext;"/> +#endif <toolbarbutton id="inspector-inspect-toolbutton" label="&inspectButton.label;" accesskey="&inspectButton.accesskey;" command="Inspector:Inspect"/> <arrowscrollbox id="inspector-breadcrumbs" flex="1" orient="horizontal" clicktoscroll="true"/> <hbox id="inspector-tools"> + <toolbarbutton id="inspector-style-button" + label="&inspectStyleButton.label;" + accesskey="&inspectStyleButton.accesskey;" + command="Inspector:Sidebar"/> <!-- registered tools go here --> </hbox> +#ifndef XP_MACOSX + <toolbarbutton id="highlighter-closebutton" + oncommand="InspectorUI.closeInspectorUI(false);" + tooltiptext="&inspectCloseButton.tooltiptext;"/> +#endif <resizer id="inspector-end-resizer" class="inspector-resizer" dir="top" disabled="true" element="inspector-tree-box"/> </hbox> </vbox> </toolbar> <toolbar id="addon-bar"
--- a/browser/base/content/highlighter.css +++ b/browser/base/content/highlighter.css @@ -25,24 +25,27 @@ #highlighter-veil-rightbox { -moz-box-flex: 1; } #highlighter-veil-middlebox:-moz-locale-dir(rtl) { -moz-box-direction: reverse; } -#highlighter-close-button { - position: absolute; - pointer-events: auto; - z-index: 1; +.inspector-breadcrumbs-button { + direction: ltr; } -.inspector-breadcrumbs-button { - direction: ltr; +.inspector-resizer { + display: none; +} + +#inspector-toolbar[treepanel-open] > vbox > #inspector-top-resizer, +#inspector-toolbar[treepanel-open] > vbox > hbox > #inspector-end-resizer { + display: -moz-box; } /* * Node Infobar */ #highlighter-nodeinfobar-container { position: absolute;
--- a/browser/base/content/nsContextMenu.js +++ b/browser/base/content/nsContextMenu.js @@ -272,19 +272,20 @@ nsContextMenu.prototype = { } // Reload image depends on an image that's not fully loaded this.showItem("context-reloadimage", (this.onImage && !this.onCompletedImage)); // View image depends on having an image that's not standalone // (or is in a frame), or a canvas. this.showItem("context-viewimage", (this.onImage && - (!this.onStandaloneImage || this.inFrame)) || this.onCanvas); + (!this.inSyntheticDoc || this.inFrame)) || this.onCanvas); - this.showItem("context-viewvideo", this.onVideo); + // View video depends on not having a standalone video. + this.showItem("context-viewvideo", this.onVideo && (!this.inSyntheticDoc || this.inFrame)); this.setItemAttr("context-viewvideo", "disabled", !this.mediaURL); // View background image depends on whether there is one. this.showItem("context-viewbgimage", shouldShow && !this._hasMultipleBGImages); this.showItem("context-sep-viewbgimage", shouldShow && !this._hasMultipleBGImages); document.getElementById("context-viewbgimage") .disabled = !this.hasBGImage; @@ -461,32 +462,32 @@ nsContextMenu.prototype = { this.shouldDisplay = false; return; } // Initialize contextual info. this.onImage = false; this.onLoadedImage = false; this.onCompletedImage = false; - this.onStandaloneImage = false; this.onCanvas = false; this.onVideo = false; this.onAudio = false; this.onTextInput = false; this.onKeywordField = false; this.mediaURL = ""; this.onLink = false; this.onMailtoLink = false; this.onSaveableLink = false; this.link = null; this.linkURL = ""; this.linkURI = null; this.linkProtocol = ""; this.onMathML = false; this.inFrame = false; + this.inSyntheticDoc = false; this.hasBGImage = false; this.bgImageURL = ""; this.onEditableArea = false; this.isDesignMode = false; // Clear any old spellchecking items from the menu, this used to // be in the menu hiding code but wasn't getting called in all // situations. Here, we can ensure it gets cleaned up any time the @@ -495,33 +496,33 @@ nsContextMenu.prototype = { InlineSpellCheckerUI.clearSuggestionsFromMenu(); InlineSpellCheckerUI.clearDictionaryListFromMenu(); InlineSpellCheckerUI.uninit(); // Remember the node that was clicked. this.target = aNode; + // Check if we are in a synthetic document (stand alone image, video, etc.). + this.inSyntheticDoc = this.target.ownerDocument.mozSyntheticDocument; // First, do checks for nodes that never have children. if (this.target.nodeType == Node.ELEMENT_NODE) { // See if the user clicked on an image. if (this.target instanceof Ci.nsIImageLoadingContent && this.target.currentURI) { this.onImage = true; var request = this.target.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)) this.onLoadedImage = true; if (request && (request.imageStatus & request.STATUS_LOAD_COMPLETE)) this.onCompletedImage = true; this.mediaURL = this.target.currentURI.spec; - if (this.target.ownerDocument instanceof ImageDocument) - this.onStandaloneImage = true; } else if (this.target instanceof HTMLCanvasElement) { this.onCanvas = true; } else if (this.target instanceof HTMLVideoElement) { this.onVideo = true; this.mediaURL = this.target.currentSrc || this.target.src; }
--- a/browser/base/content/syncProgress.js +++ b/browser/base/content/syncProgress.js @@ -39,34 +39,67 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://services-sync/main.js"); let gProgressBar; let gCounter = 0; function onLoad(event) { - Services.obs.addObserver(increaseProgressBar, "weave:engine:sync:finish", false); - Services.obs.addObserver(increaseProgressBar, "weave:engine:sync:error", false); + Services.obs.addObserver(onEngineSync, "weave:engine:sync:finish", false); + Services.obs.addObserver(onEngineSync, "weave:engine:sync:error", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:finish", false); + Services.obs.addObserver(onServiceSync, "weave:service:sync:error", false); + gProgressBar = document.getElementById('uploadProgressBar'); if (Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { - gProgressBar.max = Weave.Engines.getEnabled().length; gProgressBar.style.display = "inline"; } else { gProgressBar.style.display = "none"; } } function onUnload(event) { - Services.obs.removeObserver(increaseProgressBar, "weave:engine:sync:finish"); - Services.obs.removeObserver(increaseProgressBar, "weave:engine:sync:error"); + cleanUpObservers(); +} + +function cleanUpObservers() { + try { + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:finish", false); + Services.obs.removeObserver(onEngineSync, "weave:engine:sync:error", false); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:finish", false); + Services.obs.removeObserver(onServiceSync, "weave:service:sync:error", false); + } + catch (e) { + // may be double called by unload & exit. Ignore. + } } -function increaseProgressBar(){ +function onEngineSync(subject, topic, data) { + // The Clients engine syncs first. At this point we don't necessarily know + // yet how many engines will be enabled, so we'll ignore the Clients engine + // and evaluate how many engines are enabled when the first "real" engine + // syncs. + if (data == "clients") { + return; + } + + if (!gCounter && + Services.prefs.getPrefType("services.sync.firstSync") != Ci.nsIPrefBranch.PREF_INVALID) { + gProgressBar.max = Weave.Engines.getEnabled().length; + } + gCounter += 1; gProgressBar.setAttribute("value", gCounter); } +function onServiceSync(subject, topic, data) { + // To address the case where 0 engines are synced, we will fill the + // progress bar so the user knows that the sync has finished. + gProgressBar.setAttribute("value", gProgressBar.max); + cleanUpObservers(); +} + function closeTab() { window.close(); }
--- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -2427,17 +2427,17 @@ offset *= -1; this.tabContainer.advanceSelectedTab(offset, true); aEvent.stopPropagation(); aEvent.preventDefault(); } #else if (aEvent.ctrlKey && !aEvent.shiftKey && !aEvent.metaKey && aEvent.keyCode == KeyEvent.DOM_VK_F4 && - this.mTabBox.handleCtrlPageUpDown) { + !this.mCurrentTab.pinned) { this.removeCurrentTab({animate: true}); aEvent.stopPropagation(); aEvent.preventDefault(); } #endif ]]></body> </method>
--- a/browser/base/content/test/browser_aboutHome.js +++ b/browser/base/content/test/browser_aboutHome.js @@ -5,16 +5,19 @@ registerCleanupFunction(function() { // Ensure we don't pollute prefs for next tests. try { Services.prefs.clearUserPref("network.cookies.cookieBehavior"); } catch (ex) {} try { Services.prefs.clearUserPref("network.cookie.lifetimePolicy"); } catch (ex) {} + try { + Services.prefs.clearUserPref("services.sync.username"); + } catch (ex) {} }); let gTests = [ { desc: "Check that rejecting cookies does not prevent page from working", setup: function () { @@ -109,16 +112,126 @@ let gTests = [ ok(snippetsElt, "Found snippets element"); is(snippetsElt.getElementsByTagName("span").length, 1, "A default snippet is visible."); executeSoon(runNextTest); } }, +{ + desc: "Check sync links visibility before and after Sync setup", + setup: function () + { + try { + Services.prefs.clearUserPref("services.sync.username"); + } catch (ex) {} + Services.obs.notifyObservers(null, "weave:service:ready", null); + }, + run: function () + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let pairLink = doc.getElementById("pairDeviceLink"); + let setupLink = doc.getElementById("setupSyncLink"); + + ok(pairLink, "Found 'Pair Device' link"); + ok(setupLink, "Found 'Set Up Sync' link"); + ok(!pairLink.hidden, "'Pair' link is visible before setup"); + ok(!setupLink.hidden, "'Set Up' link is visible before setup"); + + Services.obs.notifyObservers(null, "weave:service:setup-complete", null); + + executeSoon(function () { + setupLink = doc.getElementById("setupSyncLink"); + ok(setupLink.hidden, "'Set Up' link is hidden after setup"); + ok(!pairLink.hidden, "'Pair' link is visible after setup"); + + executeSoon(runNextTest); + }); + } +}, + +{ + desc: "Check sync links visibility before and after Sync unlink", + setup: function () + { + Services.prefs.setCharPref("services.sync.username", "someuser@domain.com"); + Services.obs.notifyObservers(null, "weave:service:ready", null); + }, + run: function () + { + let doc = gBrowser.selectedTab.linkedBrowser.contentDocument; + let pairLink = doc.getElementById("pairDeviceLink"); + let setupLink = doc.getElementById("setupSyncLink"); + + ok(!pairLink.hidden, "'Pair' link is visible before unlink"); + ok(setupLink.hidden, "'Set Up' link is hidden before unlink"); + + Services.obs.notifyObservers(null, "weave:service:start-over", null); + + executeSoon(function () { + setupLink = doc.getElementById("setupSyncLink"); + ok(!setupLink.hidden, "'Set Up' link is visible after unlink"); + ok(!pairLink.hidden, "'Pair' link is visible after unlink"); + executeSoon(runNextTest); + }); + } +}, + +{ + desc: "Check Pair Device link opens correct dialog with Sync account ", + setup: function () + { + Services.prefs.setCharPref("services.sync.username", "someuser@domain.com"); + Services.obs.notifyObservers(null, "weave:service:ready", null); + }, + run: function () + { + expectDialogWindow("Sync:AddDevice"); + let browser = gBrowser.selectedTab.linkedBrowser; + let button = browser.contentDocument.getElementById("pairDeviceLink"); + EventUtils.sendMouseEvent({type: "click"}, button, browser.contentWindow); + } +}, + +{ + desc: "Check Pair Device link opens correct dialog without Sync account", + setup: function () + { + try { + Services.prefs.clearUserPref("services.sync.username"); + } catch (ex) {} + Services.obs.notifyObservers(null, "weave:service:ready", null); + }, + run: function () + { + expectDialogWindow("Weave:AccountSetup"); + let browser = gBrowser.selectedTab.linkedBrowser; + let button = browser.contentDocument.getElementById("pairDeviceLink"); + EventUtils.sendMouseEvent({type: "click"}, button, browser.contentWindow); + } +}, + +{ + desc: "Check Sync Setup link opens correct dialog (without Sync account)", + setup: function () + { + try { + Services.prefs.clearUserPref("services.sync.username"); + } catch (ex) {} + Services.obs.notifyObservers(null, "weave:service:ready", null); + }, + run: function () + { + expectDialogWindow("Weave:AccountSetup"); + let browser = gBrowser.selectedTab.linkedBrowser; + let button = browser.contentDocument.getElementById("setupSyncLink"); + EventUtils.sendMouseEvent({type: "click"}, button, browser.contentWindow); + } +}, ]; function test() { waitForExplicitFinish(); // browser-chrome test harness inits browser specifying an hardcoded page // and this causes nsIBrowserHandler.defaultArgs to not be evaluated since @@ -154,16 +267,32 @@ function runNextTest() executeSoon(test.run); }, true); } else { finish(); } } +function expectDialogWindow(expectedDialog) { + Services.ww.registerNotification(function onWindow(subject, topic) { + let win = subject.QueryInterface(Components.interfaces.nsIDOMWindow); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad, false); + let wintype = win.document.documentElement.getAttribute("windowtype"); + if (topic == "domwindowopened" && wintype == expectedDialog) { + Services.ww.unregisterNotification(onWindow); + // Clean up dialog. + win.close(); + executeSoon(runNextTest); + } + }, false); + }); +} + function getStorage() { let aboutHomeURI = Services.io.newURI("moz-safe-about:home", null, null); let principal = Components.classes["@mozilla.org/scriptsecuritymanager;1"]. getService(Components.interfaces.nsIScriptSecurityManager). getCodebasePrincipal(Services.io.newURI("about:home", null, null)); let dsm = Components.classes["@mozilla.org/dom/storagemanager;1"]. getService(Components.interfaces.nsIDOMStorageManager);
--- a/browser/base/content/test/browser_bug553455.js +++ b/browser/base/content/test/browser_bug553455.js @@ -861,16 +861,17 @@ var XPInstallObserver = { } }; function test() { requestLongerTimeout(4); waitForExplicitFinish(); Services.prefs.setBoolPref("extensions.logging.enabled", true); + Services.prefs.setBoolPref("extensions.strictCompatibility", true); Services.obs.addObserver(XPInstallObserver, "addon-install-started", false); Services.obs.addObserver(XPInstallObserver, "addon-install-blocked", false); Services.obs.addObserver(XPInstallObserver, "addon-install-failed", false); Services.obs.addObserver(XPInstallObserver, "addon-install-complete", false); registerCleanupFunction(function() { // Make sure no more test parts run in case we were timed out @@ -879,16 +880,17 @@ function test() { AddonManager.getAllInstalls(function(aInstalls) { aInstalls.forEach(function(aInstall) { aInstall.cancel(); }); }); Services.prefs.clearUserPref("extensions.logging.enabled"); + Services.prefs.clearUserPref("extensions.strictCompatibility"); Services.obs.removeObserver(XPInstallObserver, "addon-install-started"); Services.obs.removeObserver(XPInstallObserver, "addon-install-blocked"); Services.obs.removeObserver(XPInstallObserver, "addon-install-failed"); Services.obs.removeObserver(XPInstallObserver, "addon-install-complete"); }); runNextTest();
--- a/browser/base/content/test/browser_save_video.js +++ b/browser/base/content/test/browser_save_video.js @@ -1,17 +1,21 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +var MockFilePicker = SpecialPowers.MockFilePicker; +MockFilePicker.reset(); + /** * TestCase for bug 564387 * <https://bugzilla.mozilla.org/show_bug.cgi?id=564387> */ function test() { waitForExplicitFinish(); + var fileName; gBrowser.loadURI("http://mochi.test:8888/browser/browser/base/content/test/bug564387.html"); registerCleanupFunction(function () { gBrowser.addTab(); gBrowser.removeCurrentTab(); }); @@ -30,60 +34,57 @@ function test() { }); }); function contextMenuOpened(event) { event.currentTarget.removeEventListener("popupshown", contextMenuOpened); // Create the folder the video will be saved into. var destDir = createTemporarySaveDirectory(); + var destFile = destDir.clone(); - mockFilePickerSettings.destDir = destDir; - mockFilePickerSettings.filterIndex = 1; // kSaveAsType_URL - mockFilePickerRegisterer.register(); + MockFilePicker.displayDirectory = destDir; + MockFilePicker.showCallback = function(fp) { + fileName = fp.defaultString; + destFile.append (fileName); + MockFilePicker.returnFiles = [destFile]; + MockFilePicker.filterIndex = 1; // kSaveAsType_URL + }; mockTransferCallback = onTransferComplete; mockTransferRegisterer.register(); registerCleanupFunction(function () { mockTransferRegisterer.unregister(); - mockFilePickerRegisterer.unregister(); + MockFilePicker.reset(); destDir.remove(true); }); // Select "Save Video As" option from context menu var saveVideoCommand = document.getElementById("context-savevideo"); saveVideoCommand.doCommand(); event.target.hidePopup(); } function onTransferComplete(downloadSuccess) { ok(downloadSuccess, "Video file should have been downloaded successfully"); - // Read the name of the saved file. - var fileName = mockFilePickerResults.selectedFile.leafName; - is(fileName, "Bug564387-expectedName.ogv", "Video file name is correctly retrieved from Content-Disposition http header"); finish(); } } Cc["@mozilla.org/moz/jssubscript-loader;1"] .getService(Ci.mozIJSSubScriptLoader) .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockTransfer.js", this); -Cc["@mozilla.org/moz/jssubscript-loader;1"] - .getService(Ci.mozIJSSubScriptLoader) - .loadSubScript("chrome://mochitests/content/browser/toolkit/content/tests/browser/common/mockFilePicker.js", - this); - function createTemporarySaveDirectory() { var saveDir = Cc["@mozilla.org/file/directory_service;1"] .getService(Ci.nsIProperties) .get("TmpD", Ci.nsIFile); saveDir.append("testsavedir"); if (!saveDir.exists()) saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0755); return saveDir;
--- a/browser/base/content/test/subtst_contextmenu.html +++ b/browser/base/content/test/subtst_contextmenu.html @@ -13,16 +13,18 @@ Browser context menu subtest. <img id="test-image" src="ctxmenu-image.png"> <canvas id="test-canvas" width="100" height="100" style="background-color: blue"></canvas> <video controls id="test-video-ok" src="video.ogg" width="100" height="100" style="background-color: green"></video> <video controls id="test-video-bad" src="bogus.duh" width="100" height="100" style="background-color: orange"></video> <video controls id="test-video-bad2" width="100" height="100" style="background-color: yellow"> <source src="bogus.duh" type="video/durrrr;"> </video> <iframe id="test-iframe" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-video-in-iframe" src="video.ogg" width="98" height="98" style="border: 1px solid black"></iframe> +<iframe id="test-image-in-iframe" src="ctxmenu-image.png" width="98" height="98" style="border: 1px solid black"></iframe> <textarea id="test-textarea">chssseesbbbie</textarea> <!-- a weird word which generates only one suggestion --> <div id="test-contenteditable" contenteditable="true">chssseefsbbbie</div> <!-- a more weird word which generates no suggestions --> <input id="test-input-spellcheck" type="text" spellcheck="true" autofocus value="prodkjfgigrty"> <!-- this one also generates one suggestion --> <div contextmenu="myMenu"> <p id="test-pagemenu" hopeless="true">I've got a context menu!</p> <menu id="myMenu" type="context"> <menuitem label="Plain item" onclick="document.getElementById('test-pagemenu').removeAttribute('hopeless');"></menuitem> <menuitem label="Disabled item" disabled></menuitem>
--- a/browser/base/content/test/test_contextmenu.html +++ b/browser/base/content/test/test_contextmenu.html @@ -428,20 +428,78 @@ function runTest(testNum) { "---", null, "context-viewframesource", true, "context-viewframeinfo", true], null, "---", null, "context-viewsource", true, "context-viewinfo", true ].concat(inspectItems)); closeContextMenu(); + openContextMenuFor(video_in_iframe); // Invoke context menu for next test. + break; + + case 12: + // Context menu for a video in an iframe + checkContextMenu(["context-media-play", true, + "context-media-mute", true, + "context-media-hidecontrols", true, + "context-video-showstats", true, + "context-video-fullscreen", true, + "---", null, + "context-viewvideo", true, + "context-copyvideourl", true, + "---", null, + "context-savevideo", true, + "context-video-saveimage", true, + "context-sendvideo", true, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframeinfo", true], null].concat(inspectItems)); + closeContextMenu(); + openContextMenuFor(image_in_iframe); // Invoke context menu for next test. + break; + + case 13: + // Context menu for an image in an iframe + checkContextMenu(["context-viewimage", true, + "context-copyimage-contents", true, + "context-copyimage", true, + "---", null, + "context-saveimage", true, + "context-sendimage", true, + "context-setDesktopBackground", true, + "context-viewimageinfo", true, + "frame", null, + ["context-showonlythisframe", true, + "context-openframeintab", true, + "context-openframe", true, + "---", null, + "context-reloadframe", true, + "---", null, + "context-bookmarkframe", true, + "context-saveframe", true, + "---", null, + "context-printframe", true, + "---", null, + "context-viewframeinfo", true], null].concat(inspectItems)); + closeContextMenu(); openContextMenuFor(textarea, false, true); // Invoke context menu for next test, but wait for the spellcheck. break; - case 12: + case 14: // Context menu for textarea checkContextMenu(["*chubbiness", true, // spelling suggestion "spell-add-to-dictionary", true, "---", null, "context-undo", false, "---", null, "context-cut", false, "context-copy", false, @@ -456,17 +514,17 @@ function runTest(testNum) { "---", null, "spell-add-dictionaries", true], null, ].concat(inspectItems)); closeContextMenu(); openContextMenuFor(contenteditable); // Invoke context menu for next test. break; - case 13: + case 15: // Context menu for contenteditable checkContextMenu(["spell-no-suggestions", false, "spell-add-to-dictionary", true, "---", null, "context-undo", false, "---", null, "context-cut", false, "context-copy", false, @@ -481,17 +539,17 @@ function runTest(testNum) { "---", null, "spell-add-dictionaries", true], null ].concat(inspectItems)); closeContextMenu(); openContextMenuFor(inputspell); // Invoke context menu for next test. break; - case 14: + case 16: // Context menu for spell-check input checkContextMenu(["*prodigality", true, // spelling suggestion "spell-add-to-dictionary", true, "---", null, "context-undo", false, "---", null, "context-cut", false, "context-copy", false, @@ -506,23 +564,23 @@ function runTest(testNum) { "---", null, "spell-add-dictionaries", true], null ].concat(inspectItems)); closeContextMenu(); openContextMenuFor(link); // Invoke context menu for next test. break; - case 15: + case 17: executeCopyCommand("cmd_copyLink", "http://mozilla.com/"); closeContextMenu(); openContextMenuFor(pagemenu); // Invoke context menu for next test. break; - case 16: + case 18: // Context menu for element with assigned content context menu checkContextMenu(["+Plain item", {type: "", icon: "", checked: false, disabled: false}, "+Disabled item", {type: "", icon: "", checked: false, disabled: true}, "+Item w/ textContent", {type: "", icon: "", checked: false, disabled: false}, "---", null, "+Checkbox", {type: "checkbox", icon: "", checked: true, disabled: false}, "---", null, "+Radio1", {type: "checkbox", icon: "", checked: true, disabled: false}, @@ -555,17 +613,17 @@ function runTest(testNum) { "context-viewinfo", true ].concat(inspectItems)); invokeItemAction("0"); closeContextMenu(); openContextMenuFor(pagemenu, true); // Invoke context menu for next test. break; - case 17: + case 19: // 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, @@ -599,17 +657,17 @@ function runTest(testNum) { } } var testNum = 1; var subwindow, chromeWin, contextMenu, lastElement; var text, link, mailto, input, img, canvas, video_ok, video_bad, video_bad2, - iframe, textarea, contenteditable, inputspell, pagemenu; + iframe, video_in_iframe, image_in_iframe, textarea, contenteditable, inputspell, pagemenu; function startTest() { netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect'); chromeWin = subwindow .QueryInterface(Ci.nsIInterfaceRequestor) .getInterface(Ci.nsIWebNavigation) .QueryInterface(Ci.nsIDocShellTreeItem) .rootTreeItem @@ -631,16 +689,19 @@ function startTest() { mailto = subwindow.document.getElementById("test-mailto"); input = subwindow.document.getElementById("test-input"); img = subwindow.document.getElementById("test-image"); canvas = subwindow.document.getElementById("test-canvas"); video_ok = subwindow.document.getElementById("test-video-ok"); video_bad = subwindow.document.getElementById("test-video-bad"); video_bad2 = subwindow.document.getElementById("test-video-bad2"); iframe = subwindow.document.getElementById("test-iframe"); + video_in_iframe = subwindow.document.getElementById("test-video-in-iframe").contentDocument.getElementsByTagName("video")[0]; + video_in_iframe.pause(); + image_in_iframe = subwindow.document.getElementById("test-image-in-iframe").contentDocument.getElementsByTagName("img")[0]; textarea = subwindow.document.getElementById("test-textarea"); contenteditable = subwindow.document.getElementById("test-contenteditable"); contenteditable.focus(); // content editable needs to be focused to enable spellcheck inputspell = subwindow.document.getElementById("test-input-spellcheck"); pagemenu = subwindow.document.getElementById("test-pagemenu"); contextMenu.addEventListener("popupshown", function() { runTest(++testNum); }, false); runTest(1);
--- a/browser/components/dirprovider/Makefile.in +++ b/browser/components/dirprovider/Makefile.in @@ -40,17 +40,19 @@ topsrcdir = @top_srcdir@ srcdir = @srcdir@ VPATH = @srcdir@ include $(DEPTH)/config/autoconf.mk MODULE = browserdir LIBRARY_NAME = browserdir_s +ifdef ENABLE_TESTS DIRS = tests +endif FORCE_STATIC_LIB = 1 FORCE_USE_PIC = 1 # Because we are an application component, link against the CRT statically # (on Windows, but only if we're not building our own CRT for jemalloc) ifndef MOZ_MEMORY USE_STATIC_LIBS = 1
--- a/browser/components/sessionstore/src/nsSessionStore.js +++ b/browser/components/sessionstore/src/nsSessionStore.js @@ -135,16 +135,21 @@ Cu.import("resource://gre/modules/Servic // debug.js adds NS_ASSERT. cf. bug 669196 Cu.import("resource://gre/modules/debug.js"); XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { Cu.import("resource://gre/modules/NetUtil.jsm"); return NetUtil; }); +XPCOMUtils.defineLazyGetter(this, "ScratchpadManager", function() { + Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); + return ScratchpadManager; +}); + XPCOMUtils.defineLazyServiceGetter(this, "CookieSvc", "@mozilla.org/cookiemanager;1", "nsICookieManager2"); #ifdef MOZ_CRASHREPORTER XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", "@mozilla.org/xre/app-info;1", "nsICrashReporter"); #endif @@ -1577,16 +1582,20 @@ SessionStoreService.prototype = { } // Merge closed windows from this session with ones from last session if (lastSessionState._closedWindows) { this._closedWindows = this._closedWindows.concat(lastSessionState._closedWindows); this._capClosedWindows(); } + if (lastSessionState.scratchpads) { + ScratchpadManager.restoreSession(lastSessionState.scratchpads); + } + // Set data that persists between sessions this._recentCrashes = lastSessionState.session && lastSessionState.session.recentCrashes || 0; this._sessionStartTime = lastSessionState.session && lastSessionState.session.startTime || this._sessionStartTime; this._lastSessionState = null; @@ -2249,27 +2258,43 @@ SessionStoreService.prototype = { * should we check the privacy level for https * @param aIsPinned * is the entry we're evaluating for a pinned tab; used only if * aCheckPrivacy */ _extractHostsForCookies: function sss__extractHostsForCookies(aEntry, aHosts, aCheckPrivacy, aIsPinned) { - // _host and _scheme may not be set (for about: urls for example), in which - // case testing _scheme will be sufficient. - if (/https?/.test(aEntry._scheme) && !aHosts[aEntry._host] && + let host = aEntry._host, + scheme = aEntry._scheme; + + // If host & scheme aren't defined, then we are likely here in the startup + // process via _splitCookiesFromWindow. In that case, we'll turn aEntry.url + // into an nsIURI and get host/scheme from that. This will throw for about: + // urls in which case we don't need to do anything. + if (!host && !scheme) { + try { + let uri = this._getURIFromString(aEntry.url); + host = uri.host; + scheme = uri.scheme; + } + catch(ex) { } + } + + // host and scheme may not be set (for about: urls for example), in which + // case testing scheme will be sufficient. + if (/https?/.test(scheme) && !aHosts[host] && (!aCheckPrivacy || - this._checkPrivacyLevel(aEntry._scheme == "https", aIsPinned))) { + this._checkPrivacyLevel(scheme == "https", aIsPinned))) { // By setting this to true or false, we can determine when looking at // the host in _updateCookies if we should check for privacy. - aHosts[aEntry._host] = aIsPinned; + aHosts[host] = aIsPinned; } - else if (aEntry._scheme == "file") { - aHosts[aEntry._host] = true; + else if (scheme == "file") { + aHosts[host] = true; } if (aEntry.children) { aEntry.children.forEach(function(entry) { this._extractHostsForCookies(entry, aHosts, aCheckPrivacy, aIsPinned); }, this); } }, @@ -2482,22 +2507,26 @@ SessionStoreService.prototype = { ix = -1; let session = { state: this._loadState == STATE_RUNNING ? STATE_RUNNING_STR : STATE_STOPPED_STR, lastUpdate: Date.now(), startTime: this._sessionStartTime, recentCrashes: this._recentCrashes }; + + // get open Scratchpad window states too + var scratchpads = ScratchpadManager.getSessionState(); return { windows: total, selectedWindow: ix + 1, _closedWindows: lastClosedWindowsCopy, - session: session + session: session, + scratchpads: scratchpads }; }, /** * serialize session data for a window * @param aWindow * Window reference * @returns string @@ -2695,16 +2724,20 @@ SessionStoreService.prototype = { } if (aOverwriteTabs || root._firstTabs) { this._windows[aWindow.__SSi]._closedTabs = winData._closedTabs || []; } this.restoreHistoryPrecursor(aWindow, tabs, winData.tabs, (aOverwriteTabs ? (parseInt(winData.selected) || 1) : 0), 0, 0); + if (aState.scratchpads) { + ScratchpadManager.restoreSession(aState.scratchpads); + } + // This will force the keypress listener that Panorama has to attach if it // isn't already. This will be the case if tab view wasn't entered or there // were only visible tabs when TabView.init was first called. aWindow.TabView.init(); // set smoothScroll back to the original value tabstrip.smoothScroll = smoothScroll; @@ -3187,27 +3220,30 @@ SessionStoreService.prototype = { if (aEntry.postdata_b64) { var postdata = atob(aEntry.postdata_b64); var stream = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); stream.setData(postdata, postdata.length); shEntry.postData = stream; } + let childDocIdents = {}; if (aEntry.docIdentifier) { // If we have a serialized document identifier, try to find an SHEntry // which matches that doc identifier and adopt that SHEntry's // BFCacheEntry. If we don't find a match, insert shEntry as the match // for the document identifier. let matchingEntry = aDocIdentMap[aEntry.docIdentifier]; if (!matchingEntry) { - aDocIdentMap[aEntry.docIdentifier] = shEntry; + matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents}; + aDocIdentMap[aEntry.docIdentifier] = matchingEntry; } else { - shEntry.adoptBFCacheEntry(matchingEntry); + shEntry.adoptBFCacheEntry(matchingEntry.shEntry); + childDocIdents = matchingEntry.childDocIdents; } } if (aEntry.owner_b64) { var ownerInput = Cc["@mozilla.org/io/string-input-stream;1"]. createInstance(Ci.nsIStringInputStream); var binaryData = atob(aEntry.owner_b64); ownerInput.setData(binaryData, binaryData.length); @@ -3219,18 +3255,34 @@ SessionStoreService.prototype = { } catch (ex) { debug(ex); } } if (aEntry.children && shEntry instanceof Ci.nsISHContainer) { for (var i = 0; i < aEntry.children.length; i++) { //XXXzpao Wallpaper patch for bug 514751 if (!aEntry.children[i].url) continue; + + // We're getting sessionrestore.js files with a cycle in the + // doc-identifier graph, likely due to bug 698656. (That is, we have + // an entry where doc identifier A is an ancestor of doc identifier B, + // and another entry where doc identifier B is an ancestor of A.) + // + // If we were to respect these doc identifiers, we'd create a cycle in + // the SHEntries themselves, which causes the docshell to loop forever + // when it looks for the root SHEntry. + // + // So as a hack to fix this, we restrict the scope of a doc identifier + // to be a node's siblings and cousins, and pass childDocIdents, not + // aDocIdents, to _deserializeHistoryEntry. That is, we say that two + // SHEntries with the same doc identifier have the same document iff + // they have the same parent or their parents have the same document. + shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, - aDocIdentMap), i); + childDocIdents), i); } } return shEntry; }, /** * restores all sessionStorage "super cookies" @@ -3981,16 +4033,19 @@ SessionStoreService.prototype = { tab.entries.forEach(function(entry) { this._extractHostsForCookies(entry, cookieHosts, false) }, this); }, this); // By creating a regex we reduce overhead and there is only one loop pass // through either array (cookieHosts and aWinState.cookies). let hosts = Object.keys(cookieHosts).join("|").replace("\\.", "\\.", "g"); + // If we don't actually have any hosts, then we don't want to do anything. + if (!hosts.length) + return; let cookieRegex = new RegExp(".*(" + hosts + ")"); for (let cIndex = 0; cIndex < aWinState.cookies.length;) { if (cookieRegex.test(aWinState.cookies[cIndex].host)) { aTargetWinState.cookies = aTargetWinState.cookies.concat(aWinState.cookies.splice(cIndex, 1)); continue; } cIndex++;
--- a/browser/components/sessionstore/test/browser/Makefile.in +++ b/browser/components/sessionstore/test/browser/Makefile.in @@ -144,21 +144,24 @@ include $(topsrcdir)/config/rules.mk browser_615394-SSWindowState_events.js \ browser_618151.js \ browser_623779.js \ browser_624727.js \ browser_625257.js \ browser_628270.js \ browser_635418.js \ browser_636279.js \ + browser_644409-scratchpads.js \ browser_645428.js \ browser_659591.js \ browser_662812.js \ browser_665702-state_session.js \ browser_682507.js \ + browser_687710.js \ + browser_687710_2.js \ browser_694378.js \ $(NULL) ifneq ($(OS_ARCH),Darwin) _BROWSER_TEST_FILES += \ browser_597071.js \ browser_625016.js \ $(NULL)
new file mode 100644 --- /dev/null +++ b/browser/components/sessionstore/test/browser/browser_644409-scratchpads.js @@ -0,0 +1,59 @@ + /* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +const testState = { + windows: [{ + tabs: [ + { entries: [{ url: "about:blank" }] }, + ] + }], + scratchpads: [ + { text: "text1", executionContext: 1 }, + { text: "", executionContext: 2, filename: "test.js" } + ] +}; + +// only finish() when correct number of windows opened +var restored = []; +function addState(state) { + restored.push(state); + + if (restored.length == testState.scratchpads.length) { + ok(statesMatch(restored, testState.scratchpads), + "Two scratchpad windows restored"); + + Services.ww.unregisterNotification(windowObserver); + finish(); + } +} + +function test() { + waitForExplicitFinish(); + + Services.ww.registerNotification(windowObserver); + + ss.setBrowserState(JSON.stringify(testState)); +} + +function windowObserver(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + let win = aSubject.QueryInterface(Ci.nsIDOMWindow); + win.addEventListener("load", function() { + if (win.Scratchpad) { + let state = win.Scratchpad.getState(); + win.close(); + addState(state); + } + }, false); + } +} + +function statesMatch(restored, states) { + return states.every(function(state) { + return restored.some(function(restoredState) { + return state.filename == restoredState.filename && + state.text == restoredState.text && + state.executionContext == restoredState.executionContext; + }) + }); +} \ No newline at end of file
new file mode 100644 --- /dev/null +++ b/browser/components/sessionstore/test/browser/browser_687710.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that sessionrestore handles cycles in the shentry graph properly. +// +// These cycles shouldn't be there in the first place, but they cause hangs +// when they mysteriously appear (bug 687710). Docshell code assumes this +// graph is a tree and tires to walk to the root. But if there's a cycle, +// there is no root, and we loop forever. + +let stateBackup = ss.getBrowserState(); + +let state = {windows:[{tabs:[{entries:[ + { + docIdentifier: 1, + url: "http://example.com", + children: [ + { + docIdentifier: 2, + url: "http://example.com" + } + ] + }, + { + docIdentifier: 2, + url: "http://example.com", + children: [ + { + docIdentifier: 1, + url: "http://example.com" + } + ] + } +]}]}]} + +function test() { + registerCleanupFunction(function () { + ss.setBrowserState(stateBackup); + }); + + /* This test fails by hanging. */ + ss.setBrowserState(JSON.stringify(state)); + ok(true, "Didn't hang!"); +}
new file mode 100644 --- /dev/null +++ b/browser/components/sessionstore/test/browser/browser_687710_2.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test that the fix for bug 687710 isn't too aggressive -- shentries which are +// cousins should be able to share bfcache entries. + +let stateBackup = ss.getBrowserState(); + +let state = {entries:[ + { + docIdentifier: 1, + url: "http://example.com?1", + children: [{ docIdentifier: 10, + url: "http://example.com?10" }] + }, + { + docIdentifier: 1, + url: "http://example.com?1#a", + children: [{ docIdentifier: 10, + url: "http://example.com?10#aa" }] + } +]}; + +function test() +{ + registerCleanupFunction(function () { + ss.setBrowserState(stateBackup); + }); + + let tab = gBrowser.addTab("about:blank"); + ss.setTabState(tab, JSON.stringify(state)); + let history = tab.linkedBrowser.webNavigation.sessionHistory; + + is(history.count, 2, "history.count"); + for (let i = 0; i < history.count; i++) { + for (let j = 0; j < history.count; j++) { + compareEntries(i, j, history); + } + } +} + +function compareEntries(i, j, history) +{ + let e1 = history.getEntryAtIndex(i, false) + .QueryInterface(Ci.nsISHEntry) + .QueryInterface(Ci.nsISHContainer); + + let e2 = history.getEntryAtIndex(j, false) + .QueryInterface(Ci.nsISHEntry) + .QueryInterface(Ci.nsISHContainer); + + ok(e1.sharesDocumentWith(e2), + i + ' should share doc with ' + j); + is(e1.childCount, e2.childCount, + 'Child count mismatch (' + i + ', ' + j + ')'); + + for (let c = 0; c < e1.childCount; c++) { + let c1 = e1.GetChildAt(c); + let c2 = e2.GetChildAt(c); + + ok(c1.sharesDocumentWith(c2), + 'Cousins should share documents. (' + i + ', ' + j + ', ' + c + ')'); + } +}
--- a/browser/components/shell/src/nsWindowsShellService.cpp +++ b/browser/components/shell/src/nsWindowsShellService.cpp @@ -257,16 +257,19 @@ LaunchHelper(nsAutoString& aPath) return NS_OK; } NS_IMETHODIMP nsWindowsShellService::ShortcutMaintenance() { nsresult rv; + // XXX App ids were updated to a constant install path hash, + // XXX this code can be removed after a few upgrade cycles. + // Launch helper.exe so it can update the application user model ids on // shortcuts in the user's taskbar and start menu. This keeps older pinned // shortcuts grouped correctly after major updates. Note, we also do this // through the upgrade installer script, however, this is the only place we // have a chance to trap links created by users who do control the install/ // update process of the browser. nsCOMPtr<nsIWinTaskbar> taskbarInfo =
--- a/browser/config/mozconfigs/macosx32/debug +++ b/browser/config/mozconfigs/macosx32/debug @@ -1,12 +1,9 @@ -# Don't use the standard mozconfig. We don't want universal for a debug build. -#. $topsrcdir/build/macosx/universal/mozconfig - -ac_add_options --with-macos-sdk=/Developer/SDKs/MacOSX10.5.sdk +. $topsrcdir/build/macosx/mozconfig.leopard ac_add_options --enable-debug ac_add_options --enable-trace-malloc # Enable parallel compiling mk_add_options MOZ_MAKE_FLAGS="-j4" # Needed to enable breakpad in application.ini export MOZILLA_OFFICIAL=1
--- a/browser/devtools/Makefile.in +++ b/browser/devtools/Makefile.in @@ -46,16 +46,13 @@ include $(DEPTH)/config/autoconf.mk include $(topsrcdir)/config/config.mk DIRS = \ highlighter \ webconsole \ sourceeditor \ styleinspector \ + scratchpad \ shared \ $(NULL) -ifdef ENABLE_TESTS -DIRS += scratchpad/test -endif - include $(topsrcdir)/config/rules.mk
--- a/browser/devtools/highlighter/TreePanel.jsm +++ b/browser/devtools/highlighter/TreePanel.jsm @@ -226,22 +226,17 @@ TreePanel.prototype = { this.IUI.browser.ownerDocument.getElementById("browser-bottombox"); treeBox = this.document.createElement("vbox"); treeBox.id = "inspector-tree-box"; treeBox.state = "open"; // for the registerTools API. treeBox.minHeight = 10; treeBox.flex = 1; toolbarParent.insertBefore(treeBox, toolbar); - let resizerTop = - this.IUI.browser.ownerDocument.getElementById("inspector-top-resizer"); - let resizerEnd = - this.IUI.browser.ownerDocument.getElementById("inspector-end-resizer"); - resizerTop.removeAttribute("disabled"); - resizerEnd.removeAttribute("disabled"); + this.IUI.toolbar.setAttribute("treepanel-open", "true"); treeBox.appendChild(this.treeIFrame); let boundLoadedInitializeTreePanel = function loadedInitializeTreePanel() { this.treeIFrame.removeEventListener("load", boundLoadedInitializeTreePanel, true); this.initializeIFrame(); @@ -259,22 +254,17 @@ TreePanel.prototype = { }, /** * Close the TreePanel. */ close: function TP_close() { if (this.openInDock) { - let resizerTop = - this.IUI.browser.ownerDocument.getElementById("inspector-top-resizer"); - let resizerEnd = - this.IUI.browser.ownerDocument.getElementById("inspector-end-resizer"); - resizerTop.setAttribute("disabled", "true"); - resizerEnd.setAttribute("disabled", "true"); + this.IUI.toolbar.removeAttribute("treepanel-open"); let treeBox = this.container; let treeBoxParent = treeBox.parentNode; treeBoxParent.removeChild(treeBox); } else { this.container.hidePopup(); }
--- a/browser/devtools/highlighter/inspector.jsm +++ b/browser/devtools/highlighter/inspector.jsm @@ -119,30 +119,30 @@ Highlighter.prototype = { this._highlighting = false; this.highlighterContainer = this.chromeDoc.createElement("stack"); this.highlighterContainer.id = "highlighter-container"; this.veilContainer = this.chromeDoc.createElement("vbox"); this.veilContainer.id = "highlighter-veil-container"; + // The controlsBox will host the different interactive + // elements of the highlighter (buttons, toolbars, ...). let controlsBox = this.chromeDoc.createElement("box"); controlsBox.id = "highlighter-controls"; this.highlighterContainer.appendChild(this.veilContainer); this.highlighterContainer.appendChild(controlsBox); stack.appendChild(this.highlighterContainer); // The veil will make the whole page darker except // for the region of the selected box. this.buildVeil(this.veilContainer); - // The controlsBox will host the different interactive - // elements of the highlighter (buttons, toolbars, ...). - this.buildControls(controlsBox); + this.buildInfobar(controlsBox); this.browser.addEventListener("resize", this, true); this.browser.addEventListener("scroll", this, true); this.handleResize(); }, /** @@ -196,30 +196,16 @@ Highlighter.prototype = { this.veilMiddleBox.appendChild(veilRightBox); aParent.appendChild(this.veilTopBox); aParent.appendChild(this.veilMiddleBox); aParent.appendChild(veilBottomBox); }, /** - * Build the controls: - * - * <box id="highlighter-close-button"/> - * - * @param nsIDOMElement aParent - * The container of the controls elements. - */ - buildControls: function Highlighter_buildControls(aParent) - { - this.buildCloseButton(aParent); - this.buildInfobar(aParent); - }, - - /** * Build the node Infobar. * * <box id="highlighter-nodeinfobar-container"> * <box id="Highlighter-nodeinfobar-arrow-top"/> * <vbox id="highlighter-nodeinfobar"> * <label id="highlighter-nodeinfobar-tagname"/> * <label id="highlighter-nodeinfobar-id"/> * <vbox id="highlighter-nodeinfobar-classes"/> @@ -275,48 +261,23 @@ Highlighter.prototype = { idLabel: idLabel, classesBox: classesBox, container: container, barHeight: barHeight, }; }, /** - * Build the close button. - * - * @param nsIDOMElement aParent - * The container of the close-button. - */ - buildCloseButton: function Highlighter_buildCloseButton(aParent) - { - let closeButton = this.chromeDoc.createElement("box"); - closeButton.id = "highlighter-close-button"; - closeButton.appendChild(this.chromeDoc.createElement("image")); - - let boundCloseEventHandler = this.IUI.closeInspectorUI.bind(this.IUI, false); - - closeButton.addEventListener("click", boundCloseEventHandler, false); - - aParent.appendChild(closeButton); - - this.boundCloseEventHandler = boundCloseEventHandler; - this.closeButton = closeButton; - }, - - /** * Destroy the nodes. */ destroy: function Highlighter_destroy() { this.browser.removeEventListener("scroll", this, true); this.browser.removeEventListener("resize", this, true); - this.closeButton.removeEventListener("click", this.boundCloseEventHandler, false); this.boundCloseEventHandler = null; - this.closeButton.parentNode.removeChild(this.closeButton); - this.closeButton = null; this._contentRect = null; this._highlightRect = null; this._highlighting = false; this.veilTopBox = null; this.veilLeftBox = null; this.veilMiddleBox = null; this.veilTransparentBox = null; this.veilContainer = null; @@ -793,16 +754,67 @@ InspectorUI.prototype = { if (this.isInspectorOpen) { this.closeInspectorUI(); } else { this.openInspectorUI(); } }, /** + * Show the Sidebar. + */ + showSidebar: function IUI_showSidebar() + { + this.sidebarBox.removeAttribute("hidden"); + this.sidebarSplitter.removeAttribute("hidden"); + this.stylingButton.checked = true; + + // Activate the first tool in the sidebar, only if none previously- + // selected. We'll want to do a followup to remember selected tool-states. + if (!Array.some(this.sidebarToolbar.children, + function(btn) btn.hasAttribute("checked"))) { + let firstButtonId = this.getToolbarButtonId(this.sidebarTools[0].id); + this.chromeDoc.getElementById(firstButtonId).click(); + } + }, + + /** + * Hide the Sidebar. + */ + hideSidebar: function IUI_hideSidebar() + { + this.sidebarBox.setAttribute("hidden", "true"); + this.sidebarSplitter.setAttribute("hidden", "true"); + this.stylingButton.checked = false; + }, + + /** + * Show or hide the sidebar. Called from the Styling button on the + * highlighter toolbar. + */ + toggleSidebar: function IUI_toggleSidebar() + { + if (!this.isSidebarOpen) { + this.showSidebar(); + } else { + this.hideSidebar(); + } + }, + + /** + * Getter to test if the Sidebar is open or not. + */ + get isSidebarOpen() + { + return this.stylingButton.checked && + !this.sidebarBox.hidden && + !this.sidebarSplitter.hidden; + }, + + /** * Toggle the status of the inspector, starting or stopping it. Invoked * from the toolbar's Inspect button. */ toggleInspection: function IUI_toggleInspection() { if (this.inspecting) { this.stopInspecting(); } else { @@ -989,16 +1001,19 @@ InspectorUI.prototype = { this.stopInspecting(); this.browser.removeEventListener("keypress", this, true); this.saveToolState(this.winID); this.toolsDo(function IUI_toolsHide(aTool) { this.unregisterTool(aTool); }.bind(this)); + // close the sidebar + this.hideSidebar(); + if (this.highlighter) { this.highlighter.highlighterContainer.removeEventListener("keypress", this, true); this.highlighter.destroy(); this.highlighter = null; } @@ -1401,135 +1416,253 @@ InspectorUI.prototype = { * @returns String */ getToolbarButtonId: function IUI_createButtonId(anId) { return "inspector-" + anId + "-toolbutton"; }, /** + * Save a registered tool's callback for a specified event. + * @param aWidget xul:widget + * @param aEvent a DOM event name + * @param aCallback Function the click event handler for the button + */ + bindToolEvent: function IUI_bindToolEvent(aWidget, aEvent, aCallback) + { + this.toolEvents[aWidget.id + "_" + aEvent] = aCallback; + aWidget.addEventListener(aEvent, aCallback, false); + }, + + /** * Register an external tool with the inspector. * * aRegObj = { * id: "toolname", * context: myTool, - * label: "Button label", + * label: "Button or tab label", * icon: "chrome://somepath.png", * tooltiptext: "Button tooltip", * accesskey: "S", * isOpen: object.property, (getter) returning true if tool is open. * onSelect: object.method, * show: object.method, called to show the tool when button is pressed. * hide: object.method, called to hide the tool when button is pressed. * dim: object.method, called to disable a tool during highlighting. * unregister: object.method, called when tool should be destroyed. - * panel: myTool.panel + * panel: myTool.panel, set if tool is in a separate panel, null otherwise. + * sidebar: boolean, true if tool lives in sidebar tab. * } * * @param aRegObj Object * The Registration Object used to register this tool described * above. The tool should cache this object for later deregistration. */ registerTool: function IUI_registerTool(aRegObj) { if (this.toolRegistered(aRegObj.id)) { return; } this.tools[aRegObj.id] = aRegObj; let buttonContainer = this.chromeDoc.getElementById("inspector-tools"); - let btn = this.chromeDoc.createElement("toolbarbutton"); + let btn; + + // if this is a sidebar tool, create the sidebar features for it and bail. + if (aRegObj.sidebar) { + this.createSidebarTool(aRegObj); + return; + } + + btn = this.chromeDoc.createElement("toolbarbutton"); let buttonId = this.getToolbarButtonId(aRegObj.id); btn.setAttribute("id", buttonId); btn.setAttribute("label", aRegObj.label); btn.setAttribute("tooltiptext", aRegObj.tooltiptext); btn.setAttribute("accesskey", aRegObj.accesskey); btn.setAttribute("image", aRegObj.icon || ""); - buttonContainer.appendChild(btn); + buttonContainer.insertBefore(btn, this.stylingButton); - /** - * Save a registered tool's callback for a specified event. - * @param aWidget xul:widget - * @param aEvent a DOM event name - * @param aCallback Function the click event handler for the button - */ - let toolEvents = this.toolEvents; - function bindToolEvent(aWidget, aEvent, aCallback) { - toolEvents[aWidget.id + "_" + aEvent] = aCallback; - aWidget.addEventListener(aEvent, aCallback, false); - } - - bindToolEvent(btn, "click", + this.bindToolEvent(btn, "click", function IUI_toolButtonClick(aEvent) { if (btn.checked) { this.toolHide(aRegObj); } else { this.toolShow(aRegObj); } }.bind(this)); + // if the tool has a panel, register the popuphiding event if (aRegObj.panel) { - bindToolEvent(aRegObj.panel, "popuphiding", + this.bindToolEvent(aRegObj.panel, "popuphiding", function IUI_toolPanelHiding() { btn.checked = false; }); } }, + get sidebarBox() + { + return this.chromeDoc.getElementById("devtools-sidebar-box"); + }, + + get sidebarToolbar() + { + return this.chromeDoc.getElementById("devtools-sidebar-toolbar"); + }, + + get sidebarDeck() + { + return this.chromeDoc.getElementById("devtools-sidebar-deck"); + }, + + get sidebarSplitter() + { + return this.chromeDoc.getElementById("devtools-side-splitter"); + }, + + get stylingButton() + { + return this.chromeDoc.getElementById("inspector-style-button"); + }, + + /** + * Creates a tab and tabpanel for our tool to reside in. + * @param {Object} aRegObj the Registration Object for our tool. + */ + createSidebarTool: function IUI_createSidebarTab(aRegObj) + { + // toolbutton elements + let btn = this.chromeDoc.createElement("toolbarbutton"); + let buttonId = this.getToolbarButtonId(aRegObj.id); + + btn.id = buttonId; + btn.setAttribute("label", aRegObj.label); + btn.setAttribute("tooltiptext", aRegObj.tooltiptext); + btn.setAttribute("accesskey", aRegObj.accesskey); + btn.setAttribute("image", aRegObj.icon || ""); + btn.setAttribute("type", "radio"); + btn.setAttribute("group", "sidebar-tools"); + this.sidebarToolbar.appendChild(btn); + + // create tool iframe + let iframe = this.chromeDoc.createElement("iframe"); + iframe.id = "devtools-sidebar-iframe-" + aRegObj.id; + iframe.setAttribute("flex", "1"); + this.sidebarDeck.appendChild(iframe); + + // wire up button to show the iframe + this.bindToolEvent(btn, "click", function showIframe() { + let visible = this.sidebarDeck.selectedPanel == iframe; + if (!visible) { + sidebarDeck.selectedPanel = iframe; + } + this.toolShow(aRegObj); + }.bind(this)); + }, + + /** + * Return the registered object's iframe. + * @param aRegObj see registerTool function. + * @return iframe or null + */ + getToolIframe: function IUI_getToolIFrame(aRegObj) + { + return this.chromeDoc.getElementById("devtools-sidebar-iframe-" + aRegObj.id); + }, + /** * Show the specified tool. * @param aTool Object (see comment for IUI_registerTool) */ toolShow: function IUI_toolShow(aTool) { aTool.show.call(aTool.context, this.selection); - this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id)).checked = true; + + let btn = this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id)); + btn.setAttribute("checked", "true"); }, /** * Hide the specified tool. * @param aTool Object (see comment for IUI_registerTool) */ toolHide: function IUI_toolHide(aTool) { aTool.hide.call(aTool.context); - this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id)).checked = false; + + let btn = this.chromeDoc.getElementById(this.getToolbarButtonId(aTool.id)); + btn.removeAttribute("checked"); + }, + + /** + * Unregister the events associated with the registered tool's widget. + * @param aWidget XUL:widget (toolbarbutton|panel). + * @param aEvent a DOM event. + */ + unbindToolEvent: function IUI_unbindToolEvent(aWidget, aEvent) + { + let toolEvent = aWidget.id + "_" + aEvent; + aWidget.removeEventListener(aEvent, this.toolEvents[toolEvent], false); + delete this.toolEvents[toolEvent] }, /** * Unregister the registered tool, unbinding click events for the buttons * and showing and hiding events for the panel. * @param aRegObj Object * The registration object used to register the tool. */ unregisterTool: function IUI_unregisterTool(aRegObj) { + // if this is a sidebar tool, use the sidebar unregistration method + if (aRegObj.sidebar) { + this.unregisterSidebarTool(aRegObj); + return; + } + let button = this.chromeDoc.getElementById(this.getToolbarButtonId(aRegObj.id)); + let buttonContainer = this.chromeDoc.getElementById("inspector-tools"); - /** - * Unregister the events associated with the registered tool's widget. - * @param aWidget XUL:widget (toolbarbutton|panel). - * @param aEvent a DOM event. - */ - let toolEvents = this.toolEvents; - function unbindToolEvent(aWidget, aEvent) { - let toolEvent = aWidget.id + "_" + aEvent; - aWidget.removeEventListener(aEvent, toolEvents[toolEvent], false); - delete toolEvents[toolEvent] - }; + // unbind click events on button + this.unbindToolEvent(button, "click"); - let buttonContainer = this.chromeDoc.getElementById("inspector-tools"); - unbindToolEvent(button, "click"); + // unbind panel popuphiding events if present. + if (aRegObj.panel) + this.unbindToolEvent(aRegObj.panel, "popuphiding"); - if (aRegObj.panel) - unbindToolEvent(aRegObj.panel, "popuphiding"); - + // remove the button from its container buttonContainer.removeChild(button); + // call unregister callback and remove from collection + if (aRegObj.unregister) + aRegObj.unregister.call(aRegObj.context); + + delete this.tools[aRegObj.id]; + }, + + /** + * Unregister the registered sidebar tool, unbinding click events for the + * button. + * @param aRegObj Object + * The registration object used to register the tool. + */ + unregisterSidebarTool: function IUI_unregisterSidebarTool(aRegObj) + { + // unbind tool button click event + let buttonId = this.getToolbarButtonId(aRegObj.id); + let btn = this.chromeDoc.getElementById(buttonId); + this.unbindToolEvent(btn, "click"); + + // remove sidebar buttons and tools + this.sidebarToolbar.removeChild(btn); + + // call unregister callback and remove from collection, this also removes + // the iframe. if (aRegObj.unregister) aRegObj.unregister.call(aRegObj.context); delete this.tools[aRegObj.id]; }, /** * Save a list of open tools to the inspector store. @@ -1554,16 +1687,19 @@ InspectorUI.prototype = { * restored. */ restoreToolState: function IUI_restoreToolState(aWinID) { let openTools = this.store.getValue(aWinID, "openTools"); if (openTools) { this.toolsDo(function IUI_toolsOnShow(aTool) { if (aTool.id in openTools) { + if (aTool.sidebar && !this.isSidebarOpen) { + this.showSidebar(); + } this.toolShow(aTool); } }.bind(this)); } }, /** * For each tool in the tools collection select the current node that is @@ -1601,16 +1737,28 @@ InspectorUI.prototype = { toolsDo: function IUI_toolsDo(aFunction) { for each (let tool in this.tools) { aFunction(tool); } }, /** + * Convenience getter to retrieve only the sidebar tools. + */ + get sidebarTools() + { + let sidebarTools = []; + for each (let tool in this.tools) + if (tool.sidebar) + sidebarTools.push(tool); + return sidebarTools; + }, + + /** * Check if a tool is registered? * @param aId The id of the tool to check */ toolRegistered: function IUI_toolRegistered(aId) { return aId in this.tools; },
--- a/browser/devtools/highlighter/test/browser_inspector_bug_690361.js +++ b/browser/devtools/highlighter/test/browser_inspector_bug_690361.js @@ -77,17 +77,17 @@ function runInspectorTests() ok(!InspectorUI.toolbar.hidden, "toolbar is visible"); ok(InspectorUI.inspecting, "Inspector is inspecting"); ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open"); ok(InspectorUI.highlighter, "Highlighter is up"); salutation = doc.getElementById("salutation"); InspectorUI.inspectNode(salutation); - let button = document.getElementById("highlighter-close-button"); + let button = document.getElementById("highlighter-closebutton"); button.click(); } function closeInspectorTests() { Services.obs.removeObserver(closeInspectorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); Services.obs.addObserver(inspectorOpenedTrap,
--- a/browser/devtools/highlighter/test/browser_inspector_initialization.js +++ b/browser/devtools/highlighter/test/browser_inspector_initialization.js @@ -71,16 +71,17 @@ function runInspectorTests() InspectorUI.INSPECTOR_NOTIFICATIONS.OPENED); Services.obs.addObserver(treePanelTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY, false); ok(InspectorUI.toolbar, "we have the toolbar."); ok(!InspectorUI.toolbar.hidden, "toolbar is visible"); ok(InspectorUI.inspecting, "Inspector is inspecting"); ok(!InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is not open"); + ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is not open"); ok(InspectorUI.highlighter, "Highlighter is up"); InspectorUI.inspectNode(doc.body); InspectorUI.stopInspecting(); InspectorUI.treePanel.open(); } function treePanelTests() @@ -88,27 +89,27 @@ function treePanelTests() Services.obs.removeObserver(treePanelTests, InspectorUI.INSPECTOR_NOTIFICATIONS.TREEPANELREADY); Services.obs.addObserver(stylePanelTests, "StyleInspector-opened", false); ok(InspectorUI.treePanel.isOpen(), "Inspector Tree Panel is open"); executeSoon(function() { - InspectorUI.stylePanel.open(doc.body); + InspectorUI.showSidebar(); }); } function stylePanelTests() { Services.obs.removeObserver(stylePanelTests, "StyleInspector-opened"); Services.obs.addObserver(runContextMenuTest, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED, false); - ok(InspectorUI.stylePanel.isOpen(), "Style Panel is Open"); + ok(InspectorUI.isSidebarOpen, "Inspector Sidebar is open"); ok(InspectorUI.stylePanel.cssHtmlTree, "Style Panel has a cssHtmlTree"); executeSoon(function() { InspectorUI.closeInspectorUI(); }); } @@ -186,16 +187,17 @@ function inspectNodesFromContextTestTrap function finishInspectorTests() { Services.obs.removeObserver(finishInspectorTests, InspectorUI.INSPECTOR_NOTIFICATIONS.CLOSED); ok(!InspectorUI.highlighter, "Highlighter is gone"); ok(!InspectorUI.treePanel, "Inspector Tree Panel is closed"); ok(!InspectorUI.inspecting, "Inspector is not inspecting"); + ok(!InspectorUI.isSidebarOpen, "Inspector Sidebar is closed"); ok(!InspectorUI.toolbar, "toolbar is hidden"); gBrowser.removeCurrentTab(); finish(); } function test() {
--- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -1,10 +1,11 @@ browser.jar: * content/browser/inspector.html (highlighter/inspector.html) content/browser/NetworkPanel.xhtml (webconsole/NetworkPanel.xhtml) * content/browser/scratchpad.xul (scratchpad/scratchpad.xul) * content/browser/scratchpad.js (scratchpad/scratchpad.js) content/browser/csshtmltree.xhtml (styleinspector/csshtmltree.xhtml) + content/browser/devtools/cssruleview.xhtml (styleinspector/cssruleview.xhtml) + content/browser/devtools/styleinspector.css (styleinspector/styleinspector.css) content/browser/orion.js (sourceeditor/orion/orion.js) content/browser/orion.css (sourceeditor/orion/orion.css) content/browser/orion-mozilla.css (sourceeditor/orion/mozilla.css) -
new file mode 100644 --- /dev/null +++ b/browser/devtools/scratchpad/Makefile.in @@ -0,0 +1,53 @@ +# +# ***** BEGIN LICENSE BLOCK ***** +# Version: MPL 1.1/GPL 2.0/LGPL 2.1 +# +# The contents of this file are subject to the Mozilla Public License Version +# 1.1 (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.mozilla.org/MPL/ +# +# Software distributed under the License is distributed on an "AS IS" basis, +# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License +# for the specific language governing rights and limitations under the +# License. +# +# The Original Code is Scratchpad Build Code. +# +# The Initial Developer of the Original Code is The Mozilla Foundation. +# +# Portions created by the Initial Developer are Copyright (C) 2011 +# the Initial Developer. All Rights Reserved. +# +# Contributor(s): +# Rob Campbell <rcampbell@mozilla.com> +# +# Alternatively, the contents of this file may be used under the terms of +# either the GNU General Public License Version 2 or later (the "GPL"), or +# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), +# in which case the provisions of the GPL or the LGPL are applicable instead +# of those above. If you wish to allow use of your version of this file only +# under the terms of either the GPL or the LGPL, and not to allow others to +# use your version of this file under the terms of the MPL, indicate your +# decision by deleting the provisions above and replace them with the notice +# and other provisions required by the GPL or the LGPL. If you do not delete +# the provisions above, a recipient may use your version of this file under +# the terms of any one of the MPL, the GPL or the LGPL. +# +# ***** END LICENSE BLOCK ***** + +DEPTH = ../../.. +topsrcdir = @top_srcdir@ +srcdir = @srcdir@ +VPATH = @srcdir@ + +include $(DEPTH)/config/autoconf.mk + +ifdef ENABLE_TESTS + DIRS += test +endif + +include $(topsrcdir)/config/rules.mk + +libs:: + $(NSINSTALL) $(srcdir)/*.jsm $(FINAL_TARGET)/modules/devtools
new file mode 100644 --- /dev/null +++ b/browser/devtools/scratchpad/scratchpad-manager.jsm @@ -0,0 +1,174 @@ +/* vim:set ts=2 sw=2 sts=2 et tw=80: + * ***** BEGIN LICENSE BLOCK ***** + * Version: MPL 1.1/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Mozilla Public License Version + * 1.1 (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.mozilla.org/MPL/ + * + * Software distributed under the License is distributed on an "AS IS" basis, + * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License + * for the specific language governing rights and limitations under the + * License. + * + * The Original Code is Scratchpad + * + * The Initial Developer of the Original Code is + * The Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2011 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Heather Arthur <fayearthur@gmail.com> (original author) + * + * Alternatively, the contents of this file may be used under the terms of + * either the GNU General Public License Version 2 or later (the "GPL"), or + * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the MPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the MPL, the GPL or the LGPL. + * + * ***** END LICENSE BLOCK *****/ + +"use strict"; + +var EXPORTED_SYMBOLS = ["ScratchpadManager"]; + +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cu = Components.utils; + +const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; +const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; + +Cu.import("resource://gre/modules/Services.jsm"); + +/** + * The ScratchpadManager object opens new Scratchpad windows and manages the state + * of open scratchpads for session restore. There's only one ScratchpadManager in + * the life of the browser. + */ +var ScratchpadManager = { + + _scratchpads: [], + + /** + * Get the saved states of open scratchpad windows. Called by + * session restore. + * + * @return array + * The array of scratchpad states. + */ + getSessionState: function SPM_getSessionState() + { + return this._scratchpads; + }, + + /** + * Restore scratchpad windows from the scratchpad session store file. + * Called by session restore. + * + * @param function aSession + * The session object with scratchpad states. + * + * @return array + * The restored scratchpad windows. + */ + restoreSession: function SPM_restoreSession(aSession) + { + if (!Array.isArray(aSession)) { + return []; + } + + let wins = []; + aSession.forEach(function(state) { + let win = this.openScratchpad(state); + wins.push(win); + }, this); + + return wins; + }, + + /** + * Iterate through open scratchpad windows and save their states. + */ + saveOpenWindows: function SPM_saveOpenWindows() { + this._scratchpads = []; + + let enumerator = Services.wm.getEnumerator("devtools:scratchpad"); + while (enumerator.hasMoreElements()) { + let win = enumerator.getNext(); + if (!win.closed) { + this._scratchpads.push(win.Scratchpad.getState()); + } + } + }, + + /** + * Open a new scratchpad window with an optional initial state. + * + * @param object aState + * Optional. The initial state of the scratchpad, an object + * with properties filename, text, and executionContext. + * + * @return nsIDomWindow + * The opened scratchpad window. + */ + openScratchpad: function SPM_openScratchpad(aState) + { + let params = null; + if (aState) { + if (typeof aState != 'object') { + return; + } + params = Cc["@mozilla.org/embedcomp/dialogparam;1"] + .createInstance(Ci.nsIDialogParamBlock); + params.SetNumberStrings(1); + params.SetString(0, JSON.stringify(aState)); + } + let win = Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", + SCRATCHPAD_WINDOW_FEATURES, params); + // Only add shutdown observer if we've opened a scratchpad window + ShutdownObserver.init(); + + return win; + } +}; + + +/** + * The ShutdownObserver listens for app shutdown and saves the current state + * of the scratchpads for session restore. + */ +var ShutdownObserver = { + _initialized: false, + + init: function SDO_init() + { + if (this._initialized) { + return; + } + + Services.obs.addObserver(this, "quit-application-granted", false); + this._initialized = true; + }, + + observe: function SDO_observe(aMessage, aTopic, aData) + { + if (aTopic == "quit-application-granted") { + ScratchpadManager.saveOpenWindows(); + this.uninit(); + } + }, + + uninit: function SDO_uninit() + { + Services.obs.removeObserver(this, "quit-application-granted"); + } +};
--- a/browser/devtools/scratchpad/scratchpad.js +++ b/browser/devtools/scratchpad/scratchpad.js @@ -54,22 +54,22 @@ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/NetUtil.jsm"); Cu.import("resource:///modules/PropertyPanel.jsm"); Cu.import("resource:///modules/source-editor.jsm"); +Cu.import("resource:///modules/devtools/scratchpad-manager.jsm"); + const SCRATCHPAD_CONTEXT_CONTENT = 1; const SCRATCHPAD_CONTEXT_BROWSER = 2; -const SCRATCHPAD_WINDOW_URL = "chrome://browser/content/scratchpad.xul"; const SCRATCHPAD_L10N = "chrome://browser/locale/devtools/scratchpad.properties"; -const SCRATCHPAD_WINDOW_FEATURES = "chrome,titlebar,toolbar,centerscreen,resizable,dialog=no"; const DEVTOOLS_CHROME_ENABLED = "devtools.chrome.enabled"; /** * The scratchpad object handles the Scratchpad window functionality. */ var Scratchpad = { /** * The script execution context. This tells Scratchpad in which context the @@ -128,16 +128,65 @@ var Scratchpad = { * replacing text in the editor. */ setText: function SP_setText(aText, aStart, aEnd) { this.editor.setText(aText, aStart, aEnd); }, /** + * Set the filename in the scratchpad UI and object + * + * @param string aFilename + * The new filename + */ + setFilename: function SP_setFilename(aFilename) + { + document.title = this.filename = aFilename; + }, + + /** + * Get the current state of the scratchpad. Called by the + * Scratchpad Manager for session storing. + * + * @return object + * An object with 3 properties: filename, text, and + * executionContext. + */ + getState: function SP_getState() + { + return { + filename: this.filename, + text: this.getText(), + executionContext: this.executionContext + }; + }, + + /** + * Set the filename and execution context using the given state. Called + * when scratchpad is being restored from a previous session. + * + * @param object aState + * An object with filename and executionContext properties. + */ + setState: function SP_getState(aState) + { + if (aState.filename) { + this.setFilename(aState.filename); + } + + if (aState.executionContext == SCRATCHPAD_CONTEXT_BROWSER) { + this.setBrowserContext(); + } + else { + this.setContentContext(); + } + }, + + /** * Get the most recent chrome window of type navigator:browser. */ get browserWindow() Services.wm.getMostRecentWindow("navigator:browser"), /** * Reference to the last chrome window of type navigator:browser. We use this * to check if the chrome window changed since the last code evaluation. */ @@ -259,17 +308,17 @@ var Scratchpad = { * * @param string aString * The script you want evaluated. * @return mixed * The script evaluation result. */ evalInContentSandbox: function SP_evalInContentSandbox(aString) { - let result; + let error, result; try { result = Cu.evalInSandbox(aString, this.contentSandbox, "1.8", "Scratchpad", 1); } catch (ex) { this.openWebConsole(); let contentWindow = this.gBrowser.selectedBrowser.contentWindow; @@ -278,43 +327,47 @@ var Scratchpad = { createInstance(Ci.nsIScriptError2); scriptError.initWithWindowID(ex.message + "\n" + ex.stack, ex.fileName, "", ex.lineNumber, 0, scriptError.errorFlag, "content javascript", this.getInnerWindowId(contentWindow)); Services.console.logMessage(scriptError); + + error = true; } - return result; + return [error, result]; }, /** * Evaluate a string in the most recent navigator:browser chrome window. * * @param string aString * The script you want evaluated. * @return mixed * The script evaluation result. */ evalInChromeSandbox: function SP_evalInChromeSandbox(aString) { - let result; + let error, result; try { result = Cu.evalInSandbox(aString, this.chromeSandbox, "1.8", "Scratchpad", 1); } catch (ex) { Cu.reportError(ex); Cu.reportError(ex.stack); this.openErrorConsole(); + + error = true; } - return result; + return [error, result]; }, /** * Evaluate a string in the currently desired context, that is either the * chrome window or the tab content window object. * * @param string aString * The script you want to evaluate. @@ -330,31 +383,31 @@ var Scratchpad = { /** * Execute the selected text (if any) or the entire editor content in the * current context. */ run: function SP_run() { let selection = this.selectedText || this.getText(); - let result = this.evalForContext(selection); + let [error, result] = this.evalForContext(selection); this.deselect(); - return [selection, result]; + return [selection, error, result]; }, /** * Execute the selected text (if any) or the entire editor content in the * current context. The resulting object is opened up in the Property Panel * for inspection. */ inspect: function SP_inspect() { - let [selection, result] = this.run(); + let [selection, error, result] = this.run(); - if (result) { + if (!error) { this.openPropertyPanel(selection, result); } }, /** * Execute the selected text (if any) or the entire editor content in the * current context. The evaluation result is inserted into the editor after * the selected text, or at the end of the editor content if there is no @@ -362,22 +415,22 @@ var Scratchpad = { */ display: function SP_display() { let selection = this.getSelectionRange(); let insertionPoint = selection.start != selection.end ? selection.end : // after selected text this.editor.getCharCount(); // after text end - let [selectedText, result] = this.run(); - if (!result) { + let [selectedText, error, result] = this.run(); + if (error) { return; } - let newComment = "/*\n" + result.toString() + "\n*/"; + let newComment = "/*\n" + result + "\n*/"; this.setText(newComment, insertionPoint, insertionPoint); // Select the new comment. this.selectRange(insertionPoint, insertionPoint + newComment.length); }, /** @@ -405,24 +458,21 @@ var Scratchpad = { // the content of the panel. if (aEvalString !== null) { buttons.push({ label: this.strings. GetStringFromName("propertyPanel.updateButton.label"), accesskey: this.strings. GetStringFromName("propertyPanel.updateButton.accesskey"), oncommand: function () { - try { - let result = self.evalForContext(aEvalString); + let [error, result] = self.evalForContext(aEvalString); - if (result !== undefined) { - propPanel.treeView.data = result; - } + if (!error) { + propPanel.treeView.data = result; } - catch (ex) { } } }); } let doc = this.browserWindow.document; let parent = doc.getElementById("mainPopupSet"); let title = aOutputObject.toString(); propPanel = new PropertyPanel(parent, doc, title, aOutputObject, buttons); @@ -437,18 +487,17 @@ var Scratchpad = { // Menu Operations /** * Open a new Scratchpad window. */ openScratchpad: function SP_openScratchpad() { - Services.ww.openWindow(null, SCRATCHPAD_WINDOW_URL, "_blank", - SCRATCHPAD_WINDOW_FEATURES, null); + ScratchpadManager.openScratchpad(); }, /** * Export the textbox content to a file. * * @param nsILocalFile aFile * The file where you want to save the textbox content. * @param boolean aNoConfirmation @@ -536,17 +585,17 @@ var Scratchpad = { */ openFile: function SP_openFile() { let fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker); fp.init(window, this.strings.GetStringFromName("openFile.title"), Ci.nsIFilePicker.modeOpen); fp.defaultString = ""; if (fp.show() != Ci.nsIFilePicker.returnCancel) { - document.title = this.filename = fp.file.path; + this.setFilename(fp.file.path); this.importFromFile(fp.file); } }, /** * Save the textbox content to the currently open file. */ saveFile: function SP_saveFile() @@ -675,22 +724,30 @@ var Scratchpad = { let environmentMenu = document.getElementById("sp-environment-menu"); let errorConsoleCommand = document.getElementById("sp-cmd-errorConsole"); let chromeContextCommand = document.getElementById("sp-cmd-browserContext"); environmentMenu.removeAttribute("hidden"); chromeContextCommand.removeAttribute("disabled"); errorConsoleCommand.removeAttribute("disabled"); } + let initialText = this.strings.GetStringFromName("scratchpadIntro"); + if ("arguments" in window && + window.arguments[0] instanceof Ci.nsIDialogParamBlock) { + let state = JSON.parse(window.arguments[0].GetString(0)); + this.setState(state); + initialText = state.text; + } + this.editor = new SourceEditor(); let config = { mode: SourceEditor.MODES.JAVASCRIPT, showLineNumbers: true, - placeholderText: this.strings.GetStringFromName("scratchpadIntro"), + placeholderText: initialText }; let editorPlaceholder = document.getElementById("scratchpad-editor"); this.editor.init(editorPlaceholder, config, this.onEditorLoad.bind(this)); }, /** * The load event handler for the source editor. This method does post-load
--- a/browser/devtools/scratchpad/test/Makefile.in +++ b/browser/devtools/scratchpad/test/Makefile.in @@ -48,11 +48,14 @@ include $(topsrcdir)/config/rules.mk browser_scratchpad_contexts.js \ browser_scratchpad_tab_switch.js \ browser_scratchpad_execute_print.js \ browser_scratchpad_inspect.js \ browser_scratchpad_files.js \ browser_scratchpad_ui.js \ browser_scratchpad_bug_646070_chrome_context_pref.js \ browser_scratchpad_bug_660560_tab.js \ + browser_scratchpad_open.js \ + browser_scratchpad_restore.js \ + browser_scratchpad_bug_679467_falsy.js \ libs:: $(_BROWSER_TEST_FILES) $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644 --- /dev/null +++ b/browser/devtools/scratchpad/test/browser_scratchpad_bug_679467_falsy.js @@ -0,0 +1,64 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Reference to the Scratchpad chrome window object. +let gScratchpadWindow; + +function test() +{ + waitForExplicitFinish(); + + gBrowser.selectedTab = gBrowser.addTab(); + gBrowser.selectedBrowser.addEventListener("load", function() { + gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true); + + gScratchpadWindow = Scratchpad.openScratchpad(); + gScratchpadWindow.addEventListener("load", testFalsy, false); + }, true); + + content.location = "data:text/html,<p>test falsy display() values in Scratchpad"; +} + +function testFalsy(sp) +{ + gScratchpadWindow.removeEventListener("load", testFalsy, false); + + let sp = gScratchpadWindow.Scratchpad; + verifyFalsies(sp); + + sp.setBrowserContext(); + verifyFalsies(sp); + + gScratchpadWindow.close(); + gScratchpadWindow = null; + gBrowser.removeCurrentTab(); + finish(); +} + +function verifyFalsies(sp) +{ + sp.setText("undefined"); + sp.display(); + is(sp.selectedText, "/*\nundefined\n*/", "'undefined' is displayed"); + + sp.setText("false"); + sp.display(); + is(sp.selectedText, "/*\nfalse\n*/", "'false' is displayed"); + + sp.setText("0"); + sp.display(); + is(sp.selectedText, "/*\n0\n*/", "'0' is displayed"); + + sp.setText("null"); + sp.display(); + is(sp.selectedText, "/*\nnull\n*/", "'null' is displayed"); + + sp.setText("NaN"); + sp.display(); + is(sp.selectedText, "/*\nNaN\n*/", "'NaN' is displayed"); + + sp.setText("''"); + sp.display(); + is(sp.selectedText, "/*\n\n*/", "empty string is displayed"); +}
--- a/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_contexts.js @@ -83,48 +83,48 @@ function runTests() is(window.foobarBug636725, "aloha2", "window.foobarBug636725 has been set"); sp.setText("gBrowser", 7); ok(sp.getText(), "window.gBrowser", "setText() worked with no end for the replace range"); - is(typeof sp.run()[1].addTab, "function", + is(typeof sp.run()[2].addTab, "function", "chrome context has access to chrome objects"); // Check that the sandbox is cached. sp.setText("typeof foobarBug636725cache;"); - is(sp.run()[1], "undefined", "global variable does not exist"); + is(sp.run()[2], "undefined", "global variable does not exist"); sp.setText("var foobarBug636725cache = 'foo';"); sp.run(); sp.setText("typeof foobarBug636725cache;"); - is(sp.run()[1], "string", + is(sp.run()[2], "string", "global variable exists across two different executions"); sp.resetContext(); - is(sp.run()[1], "undefined", + is(sp.run()[2], "undefined", "global variable no longer exists after calling resetContext()"); sp.setText("var foobarBug636725cache2 = 'foo';"); sp.run(); sp.setText("typeof foobarBug636725cache2;"); - is(sp.run()[1], "string", + is(sp.run()[2], "string", "global variable exists across two different executions"); sp.setContentContext(); is(sp.executionContext, gScratchpadWindow.SCRATCHPAD_CONTEXT_CONTENT, "executionContext is content"); - is(sp.run()[1], "undefined", + is(sp.run()[2], "undefined", "global variable no longer exists after changing the context"); gScratchpadWindow.close(); gScratchpadWindow = null; gBrowser.removeCurrentTab(); finish(); }
--- a/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_execute_print.js @@ -27,18 +27,19 @@ function runTests() let sp = gScratchpadWindow.Scratchpad; content.wrappedJSObject.foobarBug636725 = 1; sp.setText("++window.foobarBug636725"); let exec = sp.run(); is(exec[0], sp.getText(), "run()[0] is correct"); - is(exec[1], content.wrappedJSObject.foobarBug636725, - "run()[1] is correct"); + ok(!exec[1], "run()[1] is correct"); + is(exec[2], content.wrappedJSObject.foobarBug636725, + "run()[2] is correct"); is(sp.getText(), "++window.foobarBug636725", "run() does not change the editor content"); is(content.wrappedJSObject.foobarBug636725, 2, "run() updated window.foobarBug636725"); sp.display(); @@ -72,18 +73,20 @@ function runTests() is(selection.start, 0, "selection.start is 0"); is(selection.end, 29, "selection.end is 29"); exec = sp.run(); is(exec[0], "window.foobarBug636725 = 'a';", "run()[0] is correct"); - is(exec[1], "a", + ok(!exec[1], "run()[1] is correct"); + is(exec[2], "a", + "run()[2] is correct"); is(sp.getText(), "window.foobarBug636725 = 'a';\n" + "window.foobarBug636725 = 'b';", "run() does not change the textbox value"); is(content.wrappedJSObject.foobarBug636725, "a", "run() worked for the selected range");
new file mode 100644 --- /dev/null +++ b/browser/devtools/scratchpad/test/browser_scratchpad_open.js @@ -0,0 +1,71 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var ScratchpadManager = Scratchpad.ScratchpadManager; + +// only finish() when correct number of tests are done +const expected = 3; +var count = 0; + +function done() +{ + if (++count == expected) { + finish(); + } +} + + +function test() +{ + waitForExplicitFinish(); + testOpen(); + testOpenWithState(); + testOpenInvalidState(); +} + +function testOpen() +{ + let win = ScratchpadManager.openScratchpad(); + + win.addEventListener("load", function() { + is(win.Scratchpad.filename, undefined, "Default filename is undefined"); + is(win.Scratchpad.getText(), + win.Scratchpad.strings.GetStringFromName("scratchpadIntro"), + "Default text is loaded") + is(win.Scratchpad.executionContext, win.SCRATCHPAD_CONTEXT_CONTENT, + "Default execution context is content"); + + win.close(); + done(); + }); +} + +function testOpenWithState() +{ + let state = { + filename: "testfile", + executionContext: 2, + text: "test text" + }; + + let win = ScratchpadManager.openScratchpad(state); + + win.addEventListener("load", function() { + is(win.Scratchpad.filename, state.filename, "Filename loaded from state"); + is(win.Scratchpad.executionContext, state.executionContext, "Execution context loaded from state"); + is(win.Scratchpad.getText(), state.text, "Content loaded from state"); + + win.close(); + done(); + }); +} + +function testOpenInvalidState() +{ + let state = 7; + + let win = ScratchpadManager.openScratchpad(state); + ok(!win, "no scratchpad opened if state is not an object"); + done(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/scratchpad/test/browser_scratchpad_restore.js @@ -0,0 +1,101 @@ +/* vim: set ts=2 et sw=2 tw=80: */ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +var ScratchpadManager = Scratchpad.ScratchpadManager; + +/* Call the iterator for each item in the list, + calling the final callback with all the results + after every iterator call has sent its result */ +function asyncMap(items, iterator, callback) +{ + let expected = items.length; + let results = []; + + items.forEach(function(item) { + iterator(item, function(result) { + results.push(result); + if (results.length == expected) { + callback(results); + } + }); + }); +} + +function test() +{ + waitForExplicitFinish(); + testRestore(); +} + +function testRestore() +{ + let states = [ + { + filename: "testfile", + text: "test1", + executionContext: 2 + }, + { + text: "text2", + executionContext: 1 + }, + { + text: "text3", + executionContext: 1 + } + ]; + + asyncMap(states, function(state, done) { + // Open some scratchpad windows + let win = ScratchpadManager.openScratchpad(state); + win.addEventListener("load", function() { + done(win); + }) + }, function(wins) { + // Then save the windows to session store + ScratchpadManager.saveOpenWindows(); + + // Then get their states + let session = ScratchpadManager.getSessionState(); + + // Then close them + wins.forEach(function(win) { + win.close(); + }); + + // Clear out session state for next tests + ScratchpadManager.saveOpenWindows(); + + // Then restore them + let restoredWins = ScratchpadManager.restoreSession(session); + + is(restoredWins.length, 3, "Three scratchad windows restored"); + + asyncMap(restoredWins, function(restoredWin, done) { + restoredWin.addEventListener("load", function() { + let state = restoredWin.Scratchpad.getState(); + restoredWin.close(); + done(state); + }); + }, function(restoredStates) { + // Then make sure they were restored with the right states + ok(statesMatch(restoredStates, states), + "All scratchpad window states restored correctly"); + + // Yay, we're done! + finish(); + }); + }); +} + +function statesMatch(restoredStates, states) +{ + return states.every(function(state) { + return restoredStates.some(function(restoredState) { + return state.filename == restoredState.filename + && state.text == restoredState.text + && state.executionContext == restoredState.executionContext; + }) + }); +}
--- a/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js +++ b/browser/devtools/scratchpad/test/browser_scratchpad_tab_switch.js @@ -92,17 +92,17 @@ function runTests2() { content.location = "data:text/html,test context switch in Scratchpad location 2"; } function runTests3() { gBrowser.selectedBrowser.removeEventListener("load", runTests3, true); // Check that the sandbox is not cached. sp.setText("typeof foosbug653108;"); - is(sp.run()[1], "undefined", "global variable does not exist"); + is(sp.run()[2], "undefined", "global variable does not exist"); gScratchpadWindow.close(); gScratchpadWindow = null; tab1 = null; tab2 = null; sp = null; gBrowser.removeCurrentTab(); gBrowser.removeCurrentTab();
--- a/browser/devtools/sourceeditor/orion/Makefile.dryice.js +++ b/browser/devtools/sourceeditor/orion/Makefile.dryice.js @@ -44,16 +44,17 @@ const ORION_EDITOR = "org.eclipse.orion. var js_src = copy.createDataObject(); copy({ source: [ ORION_EDITOR + "/orion/textview/keyBinding.js", ORION_EDITOR + "/orion/textview/rulers.js", ORION_EDITOR + "/orion/textview/undoStack.js", ORION_EDITOR + "/orion/textview/textModel.js", + ORION_EDITOR + "/orion/textview/tooltip.js", ORION_EDITOR + "/orion/textview/textView.js", ORION_EDITOR + "/orion/editor/htmlGrammar.js", ORION_EDITOR + "/orion/editor/textMateStyler.js", ORION_EDITOR + "/examples/textview/textStyler.js", ], dest: js_src, });
--- a/browser/devtools/sourceeditor/orion/README +++ b/browser/devtools/sourceeditor/orion/README @@ -3,18 +3,30 @@ This is the Orion editor packaged for Mozilla. The Orion editor web site: http://www.eclipse.org/orion # Upgrade To upgrade Orion to a newer version see the UPGRADE file. -Orion version: git clone from 2011-10-07 - commit hash eedba6403b6dff4536bc0469d31126c3485deb56 +Orion version: git clone from 2011-10-26 + commit hash 0ab295660e1f7d33ca2bfb8558b3b7492d2c5aa5 + + patch for Eclipse Bug 358623 - Drag and Drop support: + https://github.com/mihaisucan/orion.client/tree/bug-358623 + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=358623 + + patch for Eclipse Bug 362286 - Monaco font line height: + https://github.com/mihaisucan/orion.client/tree/bug-362286 + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=362286 + + patch for Eclipse Bug 362107 - Ctrl-Up/Down failure on Linux: + https://github.com/mihaisucan/orion.client/tree/bug-362107 + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=362107 + + patch for Eclipse Bug 362428 - _getXToOffset() throws: + https://github.com/mihaisucan/orion.client/tree/bug-362428 + see https://bugs.eclipse.org/bugs/show_bug.cgi?id=362428 # License The following files are licensed according to the contents in the LICENSE file: orion.js orion.css
--- a/browser/devtools/sourceeditor/orion/mozilla.css +++ b/browser/devtools/sourceeditor/orion/mozilla.css @@ -1,11 +1,16 @@ /* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ +.viewContainer { + font-size: inherit; /* inherit browser's default monospace font size */ +} .rulerLines { background: -moz-Dialog; color: -moz-DialogText; min-width: 1.4em; padding-left: 4px; padding-right: 4px; + text-align: end; } +
--- a/browser/devtools/sourceeditor/orion/orion.css +++ b/browser/devtools/sourceeditor/orion/orion.css @@ -29,35 +29,16 @@ } /* Styles for the line number ruler */ .rulerLines { background-color: white; } .rulerLines.even .rulerLines.odd { -} - -/* Styles for the ruler tooltips */ -.rulerTooltip { - font-family: monospace; - font-size: 10pt; - background-color: InfoBackground; - color: InfoText; - padding: 2px; - border-radius: 4px; - border: 1px solid black; - z-index: 100; - position: absolute; - overflow: hidden; - white-space: pre; -} -.rulerTooltip em { - font-style: normal; - font-weight: bold; }.token_singleline_comment { color: green; } .token_multiline_comment { color: green; } @@ -133,9 +114,9 @@ .string-quoted { color: #2a00ff; font-style: italic; } .invalid { color: red; font-weight: bold; -} +} \ No newline at end of file
--- a/browser/devtools/sourceeditor/orion/orion.js +++ b/browser/devtools/sourceeditor/orion/orion.js @@ -103,17 +103,17 @@ if (typeof window !== "undefined" && typ * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). * * Contributors: IBM Corporation - initial API and implementation ******************************************************************************/ -/*global window define setTimeout clearTimeout setInterval clearInterval */ +/*global window define setTimeout clearTimeout setInterval clearInterval Node */ /** * @namespace The global container for Orion APIs. */ var orion = orion || {}; /** * @namespace The container for textview APIs. */ @@ -121,17 +121,17 @@ orion.textview = orion.textview || {}; /** * Constructs a new ruler. * <p> * The default implementation does not implement all the methods in the interface * and is useful only for objects implementing rulers. * <p/> * - * @param {orion.textview.AnnotationModel} [annotationModel] the annotation model for the ruler. + * @param {orion.textview.AnnotationModel} annotationModel the annotation model for the ruler. * @param {String} [rulerLocation="left"] the location for the ruler. * @param {String} [rulerOverview="page"] the overview for the ruler. * @param {orion.textview.Style} [rulerStyle] the style for the ruler. * * @class This interface represents a ruler for the text view. * <p> * A Ruler is a graphical element that is placed either on the left or on the right side of * the view. It can be used to provide the view with per line decoration such as line numbering, @@ -169,29 +169,33 @@ orion.textview.Ruler = (function() { /** * Adds an annotation type to the ruler. * <p> * Only annotations of the specified types will be shown by * this ruler. * </p> * * @param type {Object} the annotation type to be shown + * + * @see #removeAnnotationType + * @see #isAnnotationTypeVisible */ addAnnotationType: function(type) { this._types.push(type); }, /** - * Returns the annotations for a given line range. + * Returns the annotations for a given line range merging multiple + * annotations when necessary. * <p> - * This method is called the the text view when the ruler is redrawn. + * This method is called by the text view when the ruler is redrawn. * </p> * - * @param {Number} startLine the line index - * @param {Number} endLine the line index - * @return {orion.textview.LineAnnotation} the annotations for the line range. + * @param {Number} startLine the start line index + * @param {Number} endLine the end line index + * @return {orion.textview.Annotation[]} the annotations for the line range. The array might be sparse. */ getAnnotations: function(startLine, endLine) { var annotationModel = this._annotationModel; if (!annotationModel) { return []; } var model = this._view.getModel(); var start = model.getLineStart(startLine); var end = model.getLineEnd(endLine - 1); var baseModel = model; @@ -220,28 +224,28 @@ orion.textview.Ruler = (function() { if (rulerAnnotation) { result[visualLineIndex] = rulerAnnotation; } } } if (!this._multiAnnotation && this._multiAnnotationOverlay) { for (var k in result) { if (result[k]._multiple) { - result[k].html = result[k].html + this._multiAnnotationOverlay.rulerHTML; + result[k].html = result[k].html + this._multiAnnotationOverlay.html; } } } return result; }, /** - * Returns the ruler annotation model. + * Returns the annotation model. * * @returns {orion.textview.AnnotationModel} the ruler annotation model. * - * @see #getOverview + * @see #setAnnotationModel */ getAnnotationModel: function() { return this._annotationModel; }, /** * Returns the ruler location. * * @returns {String} the ruler location, which is either "left" or "right". @@ -257,233 +261,238 @@ orion.textview.Ruler = (function() { * @returns {String} the overview type, which is either "page" or "document". * * @see #getLocation */ getOverview: function() { return this._overview; }, /** - * Returns the CSS styling information for the ruler. - * - * @returns {orion.textview.Style} the CSS styling for ruler. + * Returns the style information for the ruler. + * + * @returns {orion.textview.Style} the style information. */ getRulerStyle: function() { return this._rulerStyle; }, /** * Returns the widest annotation which determines the width of the ruler. * <p> * If the ruler does not have a fixed width it should provide the widest * annotation to avoid the ruler from changing size as the view scrolls. * </p> * <p> - * This method is called the the text view when the ruler is redrawn. + * This method is called by the text view when the ruler is redrawn. * </p> * - * @returns {orion.textview.Annotation} the annotation for the generic line. + * @returns {orion.textview.Annotation} the widest annotation. * * @see #getAnnotations */ getWidestAnnotation: function() { return null; }, /** * Returns whether the ruler shows annotations of the specified type. * - * @param {Object} the annotation type - * @returns {Boolean} whether the specified is shown + * @param {Object} type the annotation type + * @returns {Boolean} whether the specified annotation type is shown + * + * @see #addAnnotationType + * @see #removeAnnotationType */ isAnnotationTypeVisible: function(type) { for (var i = 0; i < this._types.length; i++) { if (this._types[i] === type) { return true; } } return false; }, /** * Removes an annotation type from the ruler. * - * @param type {Object} the annotation type to be shown + * @param {Object} type the annotation type to be removed + * + * @see #addAnnotationType + * @see #isAnnotationTypeVisible */ removeAnnotationType: function(type) { for (var i = 0; i < this._types.length; i++) { if (this._types[i] === type) { this._types.splice(i, 1); break; } } }, /** * Sets the annotation model for the ruler. * * @param {orion.textview.AnnotationModel} annotationModel the annotation model. + * + * @see #getAnnotationModel */ setAnnotationModel: function (annotationModel) { if (this._annotationModel) { this._annotationModel.removeListener(this._annotationModelListener); } this._annotationModel = annotationModel; if (this._annotationModel) { this._annotationModel.addListener(this._annotationModelListener); } }, /** * Sets the annotation that is displayed when a given line contains multiple - * annotations. - * - * @param {orion.textview.Annotation} the annotation for lines with multiple annotations. + * annotations. This annotation is used when there are different types of + * annotations in a given line. + * + * @param {orion.textview.Annotation} annotation the annotation for lines with multiple annotations. + * + * @see #setMultiAnnotationOverlay */ setMultiAnnotation: function(annotation) { this._multiAnnotation = annotation; }, /** - * Sets the annotation that overlays a line with multiple annotations. - * - * @param {orion.textview.Annotation} the annotation overlay for lines with multiple annotations. + * Sets the annotation that overlays a line with multiple annotations. This annotation is displayed on + * top of the computed annotation for a given line when there are multiple annotations of the same type + * in the line. It is also used when the multiple annotation is not set. + * + * @param {orion.textview.Annotation} annotation the annotation overlay for lines with multiple annotations. + * + * @see #setMultiAnnotation */ setMultiAnnotationOverlay: function(annotation) { this._multiAnnotationOverlay = annotation; }, /** * Sets the view for the ruler. * <p> - * This method is called the the text view when the ruler + * This method is called by the text view when the ruler * is added to the view. * </p> * * @param {orion.textview.TextView} view the text view. */ setView: function (view) { if (this._onTextModelChanged && this._view) { this._view.removeEventListener("ModelChanged", this, this._onTextModelChanged); } this._view = view; if (this._onTextModelChanged && this._view) { this._view.addEventListener("ModelChanged", this, this._onTextModelChanged); } }, /** - * This event is sent when the user clicks a line decoration. + * This event is sent when the user clicks a line annotation. * * @event - * @param {Number} lineIndex the line index of the clicked decoration. + * @param {Number} lineIndex the line index of the annotation under the pointer. * @param {DOMEvent} e the click event. */ onClick: function(lineIndex, e) { }, /** - * This event is sent when the user double clicks a line decoration. + * This event is sent when the user double clicks a line annotation. * * @event - * @param {Number} lineIndex the line index of the double clicked decoration. + * @param {Number} lineIndex the line index of the annotation under the pointer. * @param {DOMEvent} e the double click event. */ onDblClick: function(lineIndex, e) { }, + /** + * This event is sent when the user moves the mouse over a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse move event. + */ onMouseMove: function(lineIndex, e) { - if (this._tooltip && this._tooltipLineIndex === lineIndex) { return; } + var tooltip = orion.textview.Tooltip.getTooltip(this._view); + if (!tooltip) { return; } + if (tooltip.isVisible() && this._tooltipLineIndex === lineIndex) { return; } + this._tooltipLineIndex = lineIndex; var self = this; - self._hideTooltip(); - self._tooltipLineIndex = lineIndex; - self._tooltipClientY = e.clientY; - self._tooltipShowTimeout = setTimeout(function() { - self._showTooltip(); - if (self._tooltip) { - self._tooltipHideTimeout = setTimeout(function() { - var opacity = parseFloat(self._getNodeStyle(self._tooltip, "opacity", "1")); - self._tooltipFadeTimeout = setInterval(function() { - if (self._tooltip && opacity > 0) { - opacity -= 0.1; - self._tooltip.style.opacity = opacity; - return; - } - self._hideTooltip(); - }, 50); - }, 5000); - } - }, 1000); - }, + tooltip.setTarget({ + y: e.clientY, + getTooltipInfo: function() { + return self._getTooltipInfo(self._tooltipLineIndex, this.y); + } + }); + }, + /** + * This event is sent when the mouse pointer enters a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse over event. + */ onMouseOver: this._onMouseMove, + /** + * This event is sent when the mouse pointer exits a line annotation. + * + * @event + * @param {Number} lineIndex the line index of the annotation under the pointer. + * @param {DOMEvent} e the mouse out event. + */ onMouseOut: function(lineIndex, e) { - this._hideTooltip(); - }, - _getNodeStyle: function(node, prop, defaultValue) { - var value; - if (node) { - value = node.style[prop]; - if (!value) { - if (node.currentStyle) { - var index = 0, p = prop; - while ((index = p.indexOf("-", index)) !== -1) { - p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2); - } - value = node.currentStyle[p]; - } else { - var css = node.ownerDocument.defaultView.getComputedStyle(node, null); - value = css ? css.getPropertyValue(prop) : null; - } - } - } - return value || defaultValue; - }, - _getTooltip: function(document, lineIndex, annotations) { - if (annotations.length === 0) { return null; } - var model = this._view.getModel(), annotation; - function getText(start, end) { - var m = model.getBaseModel ? model.getBaseModel() : model; - var textStart = m.getLineStart(m.getLineAtOffset(start)); - var textEnd = m.getLineEnd(m.getLineAtOffset(end), true); - return m.getText(textStart, textEnd); - } - var title; - if (annotations.length === 1) { - annotation = annotations[0]; - if (annotation.rulerTitle) { - title = annotation.rulerTitle.replace(/</g, "<").replace(/>/g, ">"); - return annotation.rulerHTML + " " + title; - } else { - //TODO show a projection textview to get coloring - return document.createTextNode(getText(annotation.start, annotation.end)); - } + var tooltip = orion.textview.Tooltip.getTooltip(this._view); + if (!tooltip) { return; } + tooltip.setTarget(null); + }, + /** @ignore */ + _getTooltipInfo: function(lineIndex, y) { + if (lineIndex === undefined) { return; } + var view = this._view; + var model = view.getModel(); + var annotationModel = this._annotationModel; + var annotations = []; + if (annotationModel) { + var start = model.getLineStart(lineIndex); + var end = model.getLineEnd(lineIndex); + if (model.getBaseModel) { + start = model.mapOffset(start); + end = model.mapOffset(end); + } + var iter = annotationModel.getAnnotations(start, end); + var annotation; + while (iter.hasNext()) { + annotation = iter.next(); + if (!this.isAnnotationTypeVisible(annotation.type)) { continue; } + annotations.push(annotation); + } + } + var contents = this._getTooltipContents(lineIndex, annotations); + if (!contents) { return null; } + var info = { + contents: contents, + anchor: this.getLocation() + }; + var rect = view.getClientArea(); + if (this.getOverview() === "document") { + rect.y = view.convert({y: y}, "view", "document").y; } else { - var tooltipHTML = "<em>Multiple annotations:</em><br>"; - for (var i = 0; i < annotations.length; i++) { - annotation = annotations[i]; - title = annotation.rulerTitle; - if (!title) { - title = getText(annotation.start, annotation.end); - } - title = title.replace(/</g, "<").replace(/>/g, ">"); - tooltipHTML += annotation.rulerHTML + " " + title + "<br>"; - } - return tooltipHTML; - } - }, - _hideTooltip: function() { - this._tooltipLineIndex = this._tooltipEvent = undefined; - if (this._tooltip) { - var parent = this._tooltip.parentNode; - if (parent) { parent.removeChild(this._tooltip); } - this._tooltip = null; - } - if (this._tooltipShowTimeout) { - clearTimeout(this._tooltipShowTimeout); - this._tooltipShowTimeout = null; - } - if (this._tooltipHideTimeout) { - clearTimeout(this._tooltipHideTimeout); - this._tooltipHideTimeout = null; - } - if (this._tooltipFadeTimeout) { - clearInterval(this._tooltipFadeTimeout); - this._tooltipFadeTimeout = null; - } - }, + rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y; + } + view.convert(rect, "document", "page"); + info.x = rect.x; + info.y = rect.y; + if (info.anchor === "right") { + info.x += rect.width; + } + info.maxWidth = rect.width; + info.maxHeight = rect.height - (rect.y - view._parent.getBoundingClientRect().top); + return info; + }, + /** @ignore */ + _getTooltipContents: function(lineIndex, annotations) { + return annotations; + }, + /** @ignore */ _onAnnotationModelChanged: function(e) { var view = this._view; if (!view) { return; } var model = view.getModel(), self = this; var lineCount = model.getLineCount(); if (e.textModelChangedEvent) { var start = e.textModelChangedEvent.start; if (model.getBaseModel) { start = model.mapOffset(start, true); } @@ -504,33 +513,35 @@ orion.textview.Ruler = (function() { view.redrawLines(model.getLineAtOffset(start), model.getLineAtOffset(Math.max(start, end - 1)) + 1, self); } } } redraw(e.added); redraw(e.removed); redraw(e.changed); }, + /** @ignore */ _mergeAnnotation: function(result, annotation, annotationLineIndex, annotationLineCount) { if (!result) { result = {}; } if (annotationLineIndex === 0) { - if (result.html && annotation.rulerHTML) { - if (annotation.rulerHTML !== result.html) { + if (result.html && annotation.html) { + if (annotation.html !== result.html) { if (!result._multiple && this._multiAnnotation) { - result.html = this._multiAnnotation.rulerHTML; + result.html = this._multiAnnotation.html; } } result._multiple = true; } else { - result.html = annotation.rulerHTML; - } - } - result.style = this._mergeStyle(result.style, annotation.rulerStyle); + result.html = annotation.html; + } + } + result.style = this._mergeStyle(result.style, annotation.style); return result; }, + /** @ignore */ _mergeStyle: function(result, style) { if (style) { if (!result) { result = {}; } if (result.styleClass && style.styleClass && result.styleClass !== style.styleClass) { result.styleClass += " " + style.styleClass; } else { result.styleClass = style.styleClass; } @@ -548,74 +559,16 @@ orion.textview.Ruler = (function() { for (prop in style.attributes) { if (!result.attributes[prop]) { result.attributes[prop] = style.attributes[prop]; } } } } return result; - }, - _showTooltip: function() { - var lineIndex = this._tooltipLineIndex; - if (lineIndex === undefined) { return; } - var view = this._view; - var model = view.getModel(); - var annotationModel = this._annotationModel; - var annotations = []; - if (annotationModel) { - var start = model.getLineStart(lineIndex); - var end = model.getLineEnd(lineIndex); - if (model.getBaseModel) { - start = model.mapOffset(start); - end = model.mapOffset(end); - } - var iter = annotationModel.getAnnotations(start, end); - var annotation; - while (iter.hasNext()) { - annotation = iter.next(); - if (!this.isAnnotationTypeVisible(annotation.type)) { continue; } - annotations.push(annotation); - } - } - var document = this._view._parentDocument;//TODO bad not API - var tooltipContent = this._getTooltip(document, lineIndex, annotations); - if (!tooltipContent) { return; } - var tooltip = this._tooltip = document.createElement("DIV"); - tooltip.className = "rulerTooltip"; - if (typeof tooltipContent === "string") { - tooltip.innerHTML = tooltipContent; - } else { - tooltip.appendChild(tooltipContent); - } - var rect = view.getClientArea(); - if (this.getOverview() === "document") { - rect.y = view.convert({y: this._tooltipClientY}, "view", "document").y; - } else { - rect.y = view.getLocationAtOffset(model.getLineStart(lineIndex)).y; - } - view.convert(rect, "document", "page"); - tooltip.style.visibility = "hidden"; - document.body.appendChild(tooltip); - var left = parseInt(this._getNodeStyle(tooltip, "padding-left", "0"), 10); - left += parseInt(this._getNodeStyle(tooltip, "border-left-width", "0"), 10); - var top = parseInt(this._getNodeStyle(tooltip, "padding-top", "0"), 10); - top += parseInt(this._getNodeStyle(tooltip, "border-top-width", "0"), 10); - rect.y -= top; - if (this.getLocation() === "right") { - var right = parseInt(this._getNodeStyle(tooltip, "padding-right", "0"), 10); - right += parseInt(this._getNodeStyle(tooltip, "border-right-width", "0"), 10); - tooltip.style.right = (document.body.getBoundingClientRect().right - (rect.x + rect.width) + left + right) + "px"; - } else { - tooltip.style.left = (rect.x - left) + "px"; - } - tooltip.style.top = rect.y + "px"; - tooltip.style.maxWidth = rect.width + "px"; - tooltip.style.maxHeight = (rect.height - (rect.y - view._parent.getBoundingClientRect().top)) + "px"; - tooltip.style.visibility = "visible"; } }; return Ruler; }()); /** * Constructs a new line numbering ruler. * @@ -751,27 +704,27 @@ orion.textview.OverviewRuler = (function return result; }; /** @ignore */ OverviewRuler.prototype.onClick = function(lineIndex, e) { if (lineIndex === undefined) { return; } this._view.setTopIndex(lineIndex); }; /** @ignore */ - OverviewRuler.prototype._getTooltip = function(document, lineIndex, annotations) { + OverviewRuler.prototype._getTooltipContents = function(lineIndex, annotations) { if (annotations.length === 0) { var model = this._view.getModel(); var mapLine = lineIndex; if (model.getBaseModel) { var lineStart = model.getLineStart(mapLine); mapLine = model.getBaseModel().getLineAtOffset(model.mapOffset(lineStart)); } return "Line: " + (mapLine + 1); } - return orion.textview.Ruler.prototype._getTooltip.call(this, document, lineIndex, annotations); + return orion.textview.Ruler.prototype._getTooltipContents.call(this, lineIndex, annotations); }; /** @ignore */ OverviewRuler.prototype._mergeAnnotation = function(previousAnnotation, annotation, annotationLineIndex, annotationLineCount) { if (annotationLineIndex !== 0) { return undefined; } var result = previousAnnotation; if (!result) { //TODO annotationLineCount does not work when there are folded lines var height = 3 * annotationLineCount; @@ -805,33 +758,36 @@ orion.textview.FoldingRuler = (function( } var annotation, iter = annotationModel.getAnnotations(start, end); while (!annotation && iter.hasNext()) { var a = iter.next(); if (!this.isAnnotationTypeVisible(a.type)) { continue; } annotation = a; } if (annotation) { - this._hideTooltip(); + var tooltip = orion.textview.Tooltip.getTooltip(this._view); + if (tooltip) { + tooltip.setTarget(null); + } if (annotation.expanded) { annotation.collapse(); } else { annotation.expand(); } this._annotationModel.modifyAnnotation(annotation); } }; /** @ignore */ - FoldingRuler.prototype._getTooltip = function(document, lineIndex, annotations) { + FoldingRuler.prototype._getTooltipContents = function(lineIndex, annotations) { if (annotations.length === 1) { if (annotations[0].expanded) { return null; } } - return orion.textview.AnnotationRuler.prototype._getTooltip.call(this, document, lineIndex, annotations); + return orion.textview.AnnotationRuler.prototype._getTooltipContents.call(this, lineIndex, annotations); }; /** @ignore */ FoldingRuler.prototype._onAnnotationModelChanged = function(e) { if (e.textModelChangedEvent) { orion.textview.AnnotationRuler.prototype._onAnnotationModelChanged.call(this, e); return; } var view = this._view; @@ -858,17 +814,17 @@ orion.textview.FoldingRuler = (function( view.redrawLines(lineIndex, lineCount, rulers[i]); } }; return FoldingRuler; }()); if (typeof window !== "undefined" && typeof window.define !== "undefined") { - define([], function() { + define(['orion/textview/tooltip'], function() { return orion.textview; }); } /******************************************************************************* * Copyright (c) 2010, 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution @@ -1010,29 +966,22 @@ orion.textview.UndoStack = (function() { this.reset(); var model = view.getModel(); if (model.getBaseModel) { model = model.getBaseModel(); } this.model = model; var self = this; this._modelListener = { - onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { - var e = { - text: text, - start: start, - removedCharCount: removedCharCount, - addedCharCount: addedCharCount, - removedLineCount: removedLineCount, - addedLineCount: addedLineCount - }; + onChanging: function(e) { self._onModelChanging(e); } }; model.addListener(this._modelListener); + view._undoStack = this; view.addEventListener("Destroy", this, this._onDestroy); } UndoStack.prototype = /** @lends orion.textview.UndoStack.prototype */ { /** * Adds a change to the stack. * * @param change the change to add. * @param {Number} change.offset the offset of the change @@ -1208,16 +1157,17 @@ orion.textview.UndoStack = (function() { } this._undoStart = undefined; this._undoText = ""; } }, _onDestroy: function() { this.model.removeListener(this._modelListener); this.view.removeEventListener("Destroy", this, this._onDestroy); + this.view._undoStack = null; }, _onModelChanging: function(e) { var newText = e.text; var start = e.start; var removedCharCount = e.removedCharCount; var addedCharCount = e.addedCharCount; if (this._ignoreUndo) { return; @@ -1554,54 +1504,45 @@ orion.textview.TextModel = (function() { * use {@link orion.textview.TextView#event:onModelChanging}. * </p> * <p> * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel * as part of the implementation of {@link #setText}. This method is included in the public API for documentation * purposes and to allow integration with other toolkit frameworks. * </p> * - * @param {String} text the text that is about to be inserted in the model. - * @param {Number} start the character offset in the model where the change will occur. - * @param {Number} removedCharCount the number of characters being removed from the model. - * @param {Number} addedCharCount the number of characters being added to the model. - * @param {Number} removedLineCount the number of lines being removed from the model. - * @param {Number} addedLineCount the number of lines being added to the model. - */ - onChanging: function(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + * @param {orion.textview.ModelChangingEvent} modelChangingEvent the changing event + */ + onChanging: function(modelChangingEvent) { for (var i = 0; i < this._listeners.length; i++) { var l = this._listeners[i]; if (l && l.onChanging) { - l.onChanging(text, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + l.onChanging(modelChangingEvent); } } }, /** * Notifies all listeners that the text has changed. * <p> * This notification is intended to be used only by the view. Application clients should * use {@link orion.textview.TextView#event:onModelChanged}. * </p> * <p> * NOTE: This method is not meant to called directly by application code. It is called internally by the TextModel * as part of the implementation of {@link #setText}. This method is included in the public API for documentation * purposes and to allow integration with other toolkit frameworks. * </p> * - * @param {Number} start the character offset in the model where the change occurred. - * @param {Number} removedCharCount the number of characters removed from the model. - * @param {Number} addedCharCount the number of characters added to the model. - * @param {Number} removedLineCount the number of lines removed from the model. - * @param {Number} addedLineCount the number of lines added to the model. - */ - onChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { + * @param {orion.textview.ModelChangedEvent} modelChangedEvent the changed event + */ + onChanged: function(modelChangedEvent) { for (var i = 0; i < this._listeners.length; i++) { var l = this._listeners[i]; if (l && l.onChanged) { - l.onChanged(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + l.onChanged(modelChangedEvent); } } }, /** * Sets the line delimiter that is used by the view * when new lines are inserted in the model due to key * strokes and paste operations. * <p> @@ -1669,17 +1610,25 @@ orion.textview.TextModel = (function() { index = cr + 1; } else { index = lf + 1; } newLineOffsets.push(start + index); addedLineCount++; } - this.onChanging(text, eventStart, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + var modelChangingEvent = { + text: text, + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanging(modelChangingEvent); //TODO this should be done the loops below to avoid getText() if (newLineOffsets.length === 0) { var startLineOffset = this.getLineStart(startLine), endLineOffset; if (endLine + 1 < lineCount) { endLineOffset = this.getLineStart(endLine + 1); } else { endLineOffset = this.getCharCount(); @@ -1724,17 +1673,24 @@ orion.textview.TextModel = (function() { var afterText = lastText.substring(end - lastOffset); var params = [firstChunk, lastChunk - firstChunk + 1]; if (beforeText) { params.push(beforeText); } if (text) { params.push(text); } if (afterText) { params.push(afterText); } Array.prototype.splice.apply(this._text, params); if (this._text.length === 0) { this._text = [""]; } - this.onChanged(eventStart, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + var modelChangedEvent = { + start: eventStart, + removedCharCount: removedCharCount, + addedCharCount: addedCharCount, + removedLineCount: removedLineCount, + addedLineCount: addedLineCount + }; + this.onChanged(modelChangedEvent); } }; return TextModel; }()); if (typeof window !== "undefined" && typeof window.define !== "undefined") { define([], function() { @@ -1743,20 +1699,265 @@ if (typeof window !== "undefined" && typ } /******************************************************************************* * Copyright (c) 2010, 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials are made * available under the terms of the Eclipse Public License v1.0 * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). * + * Contributors: IBM Corporation - initial API and implementation + ******************************************************************************/ + +/*global window define setTimeout clearTimeout setInterval clearInterval Node */ + +/** + * @namespace The global container for Orion APIs. + */ +var orion = orion || {}; +/** + * @namespace The container for textview APIs. + */ +orion.textview = orion.textview || {}; + +/** @ignore */ +orion.textview.Tooltip = (function() { + /** @private */ + function Tooltip (view) { + this._view = view; + //TODO add API to get the parent of the view + this._create(view._parent.ownerDocument); + view.addEventListener("Destroy", this, this.destroy); + } + Tooltip.getTooltip = function(view) { + if (!view._tooltip) { + view._tooltip = new Tooltip(view); + } + return view._tooltip; + }; + Tooltip.prototype = /** @lends orion.textview.Tooltip.prototype */ { + _create: function(document) { + if (this._domNode) { return; } + this._document = document; + var domNode = this._domNode = document.createElement("DIV"); + domNode.className = "viewTooltip"; + var viewParent = this._viewParent = document.createElement("DIV"); + domNode.appendChild(viewParent); + var htmlParent = this._htmlParent = document.createElement("DIV"); + domNode.appendChild(htmlParent); + document.body.appendChild(domNode); + this.hide(); + }, + destroy: function() { + if (!this._domNode) { return; } + if (this._contentsView) { + this._contentsView.destroy(); + this._contentsView = null; + this._emptyModel = null; + } + var parent = this._domNode.parentNode; + if (parent) { parent.removeChild(this._domNode); } + this._domNode = null; + }, + hide: function() { + if (this._contentsView) { + this._contentsView.setModel(this._emptyModel); + } + if (this._viewParent) { + this._viewParent.style.left = "-10000px"; + this._viewParent.style.position = "fixed"; + this._viewParent.style.visibility = "hidden"; + } + if (this._htmlParent) { + this._htmlParent.style.left = "-10000px"; + this._htmlParent.style.position = "fixed"; + this._htmlParent.style.visibility = "hidden"; + this._htmlParent.innerHTML = ""; + } + if (this._domNode) { + this._domNode.style.visibility = "hidden"; + } + if (this._showTimeout) { + clearTimeout(this._showTimeout); + this._showTimeout = null; + } + if (this._hideTimeout) { + clearTimeout(this._hideTimeout); + this._hideTimeout = null; + } + if (this._fadeTimeout) { + clearInterval(this._fadeTimeout); + this._fadeTimeout = null; + } + }, + isVisible: function() { + return this._domNode && this._domNode.style.visibility === "visible"; + }, + setTarget: function(target) { + if (this.target === target) { return; } + this._target = target; + this.hide(); + if (target) { + var self = this; + self._showTimeout = setTimeout(function() { + self.show(true); + }, 1000); + } + }, + show: function(autoHide) { + if (!this._target) { return; } + var info = this._target.getTooltipInfo(); + if (!info) { return; } + var domNode = this._domNode; + domNode.style.left = domNode.style.right = domNode.style.width = domNode.style.height = "auto"; + var contents = info.contents, contentsDiv; + if (contents instanceof Array) { + contents = this._getAnnotationContents(contents); + } + if (typeof contents === "string") { + (contentsDiv = this._htmlParent).innerHTML = contents; + } else if (contents instanceof Node) { + (contentsDiv = this._htmlParent).appendChild(contents); + } else if (contents instanceof orion.textview.ProjectionTextModel) { + if (!this._contentsView) { + this._emptyModel = new orion.textview.TextModel(""); + //TODO need hook into setup.js (or editor.js) to create a text view (and styler) + var newView = this._contentsView = new orion.textview.TextView({ + model: this._emptyModel, + parent: this._viewParent, + tabSize: 4, + stylesheet: ["/orion/textview/tooltip.css", "/orion/textview/rulers.css", + "/examples/textview/textstyler.css", "/css/default-theme.css"] + }); + //TODO this is need to avoid IE from getting focus + newView._clientDiv.contentEditable = false; + //TODO need to find a better way of sharing the styler for multiple views + var view = this._view; + newView.addEventListener("LineStyle", view, view.onLineStyle); + } + var contentsView = this._contentsView; + contentsView.setModel(contents); + var size = contentsView.computeSize(); + contentsDiv = this._viewParent; + //TODO always make the width larger than the size of the scrollbar to avoid bug in updatePage + contentsDiv.style.width = (size.width + 20) + "px"; + contentsDiv.style.height = size.height + "px"; + } else { + return; + } + contentsDiv.style.left = "auto"; + contentsDiv.style.position = "static"; + contentsDiv.style.visibility = "visible"; + var left = parseInt(this._getNodeStyle(domNode, "padding-left", "0"), 10); + left += parseInt(this._getNodeStyle(domNode, "border-left-width", "0"), 10); + if (info.anchor === "right") { + var right = parseInt(this._getNodeStyle(domNode, "padding-right", "0"), 10); + right += parseInt(this._getNodeStyle(domNode, "border-right-width", "0"), 10); + domNode.style.right = (domNode.ownerDocument.body.getBoundingClientRect().right - info.x + left + right) + "px"; + } else { + domNode.style.left = (info.x - left) + "px"; + } + var top = parseInt(this._getNodeStyle(domNode, "padding-top", "0"), 10); + top += parseInt(this._getNodeStyle(domNode, "border-top-width", "0"), 10); + domNode.style.top = (info.y - top) + "px"; + domNode.style.maxWidth = info.maxWidth + "px"; + domNode.style.maxHeight = info.maxHeight + "px"; + domNode.style.opacity = "1"; + domNode.style.visibility = "visible"; + if (autoHide) { + var self = this; + self._hideTimeout = setTimeout(function() { + var opacity = parseFloat(self._getNodeStyle(domNode, "opacity", "1")); + self._fadeTimeout = setInterval(function() { + if (domNode.style.visibility === "visible" && opacity > 0) { + opacity -= 0.1; + domNode.style.opacity = opacity; + return; + } + self.hide(); + }, 50); + }, 5000); + } + }, + _getAnnotationContents: function(annotations) { + if (annotations.length === 0) { + return null; + } + var model = this._view.getModel(), annotation; + var baseModel = model.getBaseModel ? model.getBaseModel() : model; + function getText(start, end) { + var textStart = baseModel.getLineStart(baseModel.getLineAtOffset(start)); + var textEnd = baseModel.getLineEnd(baseModel.getLineAtOffset(end), true); + return baseModel.getText(textStart, textEnd); + } + var title; + if (annotations.length === 1) { + annotation = annotations[0]; + if (annotation.title) { + title = annotation.title.replace(/</g, "<").replace(/>/g, ">"); + return annotation.html + " " + title; + } else { + var newModel = new orion.textview.ProjectionTextModel(baseModel); + var lineStart = baseModel.getLineStart(baseModel.getLineAtOffset(annotation.start)); + newModel.addProjection({start: annotation.end, end: newModel.getCharCount()}); + newModel.addProjection({start: 0, end: lineStart}); + return newModel; + } + } else { + var tooltipHTML = "<em>Multiple annotations:</em><br>"; + for (var i = 0; i < annotations.length; i++) { + annotation = annotations[i]; + title = annotation.title; + if (!title) { + title = getText(annotation.start, annotation.end); + } + title = title.replace(/</g, "<").replace(/>/g, ">"); + tooltipHTML += annotation.html + " " + title + "<br>"; + } + return tooltipHTML; + } + }, + _getNodeStyle: function(node, prop, defaultValue) { + var value; + if (node) { + value = node.style[prop]; + if (!value) { + if (node.currentStyle) { + var index = 0, p = prop; + while ((index = p.indexOf("-", index)) !== -1) { + p = p.substring(0, index) + p.substring(index + 1, index + 2).toUpperCase() + p.substring(index + 2); + } + value = node.currentStyle[p]; + } else { + var css = node.ownerDocument.defaultView.getComputedStyle(node, null); + value = css ? css.getPropertyValue(prop) : null; + } + } + } + return value || defaultValue; + } + }; + return Tooltip; +}()); + +if (typeof window !== "undefined" && typeof window.define !== "undefined") { + define([], function() { + return orion.textview; + }); +}/******************************************************************************* + * Copyright (c) 2010, 2011 IBM Corporation and others. + * All rights reserved. This program and the accompanying materials are made + * available under the terms of the Eclipse Public License v1.0 + * (http://www.eclipse.org/legal/epl-v10.html), and the Eclipse Distribution + * License v1.0 (http://www.eclipse.org/org/documents/edl-v10.html). + * * Contributors: * Felipe Heidrich (IBM Corporation) - initial API and implementation * Silenio Quarti (IBM Corporation) - initial API and implementation - * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 + * Mihai Sucan (Mozilla Foundation) - fix for Bug#334583 Bug#348471 Bug#349485 Bug#350595 Bug#360726 Bug#361180 Bug#358623 Bug#362286 Bug#362107 Bug#362428 ******************************************************************************/ /*global window document navigator setTimeout clearTimeout XMLHttpRequest define */ /** * @namespace The global container for Orion APIs. */ var orion = orion || {}; @@ -1804,16 +2005,17 @@ orion.textview.TextView = (function() { var isSafari = navigator.userAgent.indexOf("Safari") !== -1; var isWebkit = navigator.userAgent.indexOf("WebKit") !== -1; var isPad = navigator.userAgent.indexOf("iPad") !== -1; var isMac = navigator.platform.indexOf("Mac") !== -1; var isWindows = navigator.platform.indexOf("Win") !== -1; var isLinux = navigator.platform.indexOf("Linux") !== -1; var isW3CEvents = typeof window.document.documentElement.addEventListener === "function"; var isRangeRects = (!isIE || isIE >= 9) && typeof window.document.createRange().getBoundingClientRect === "function"; + var isDnD = isFirefox || isWebkit; // drag and drop support var platformDelimiter = isWindows ? "\r\n" : "\n"; /** * Constructs a new Selection object. * * @class A Selection represents a range of selected text in the view. * @name orion.textview.Selection */ @@ -2002,16 +2204,51 @@ orion.textview.TextView = (function() { * @param {orion.textview.Ruler} ruler the ruler. */ addRuler: function (ruler) { this._rulers.push(ruler); ruler.setView(this); this._createRuler(ruler); this._updatePage(); }, + computeSize: function() { + var w = 0, h = 0; + var model = this._model, clientDiv = this._clientDiv; + var clientWidth = clientDiv.style.width; + /* + * Feature in WekKit. Webkit limits the width of the lines + * computed below to the width of the client div. This causes + * the lines to be wrapped even though "pre" is set. The fix + * is to set the width of the client div to a larger number + * before computing the lines width. Note that this value is + * reset to the appropriate value further down. + */ + if (isWebkit) { + clientDiv.style.width = (0x7FFFF).toString() + "px"; + } + var lineCount = model.getLineCount(); + var document = this._frameDocument; + for (var lineIndex=0; lineIndex<lineCount; lineIndex++) { + var child = this._getLineNode(lineIndex), dummy = null; + if (!child || child.lineChanged) { + child = dummy = this._createLine(clientDiv, null, document, lineIndex, model); + } + var rect = this._getLineBoundingClientRect(child); + w = Math.max(w, rect.right - rect.left); + h += rect.bottom - rect.top; + if (dummy) { clientDiv.removeChild(dummy); } + } + if (isWebkit) { + clientDiv.style.width = clientWidth; + } + var viewPadding = this._getViewPadding(); + w += viewPadding.right - viewPadding.left; + h += viewPadding.bottom - viewPadding.top; + return {width: w, height: h}; + }, /** * Converts the given rectangle from one coordinate spaces to another. * <p>The supported coordinate spaces are: * <ul> * <li>"document" - relative to document, the origin is the top-left corner of first line</li> * <li>"page" - relative to html page that contains the text view</li> * <li>"view" - relative to text view, the origin is the top-left corner of the view container</li> * </ul> @@ -2097,16 +2334,18 @@ orion.textview.TextView = (function() { * and the view contents and handlers is released properly by * destroyView(). */ this._destroyFrame(); var e = {}; this.onDestroy(e); + this._dragStartSelection = null; + this._dropDestination = null; this._parent = null; this._parentDocument = null; this._model = null; this._selection = null; this._doubleClickSelection = null; this._eventTable = null; this._keyBindings = null; this._actions = null; @@ -2576,16 +2815,17 @@ orion.textview.TextView = (function() { * @class This is the event sent when the text view needs the style information for a line. * <p> * <b>See:</b><br/> * {@link orion.textview.TextView}<br/> * {@link orion.textview.TextView#event:onLineStyle} * </p> * @name orion.textview.LineStyleEvent * + * @property {orion.textview.TextView} textView The text view. * @property {Number} lineIndex The line index. * @property {String} lineText The line text. * @property {Number} lineStart The character offset, relative to document, of the first character in the line. * @property {orion.textview.Style} style The style for the entire line (output argument). * @property {orion.textview.StyleRange[]} ranges An array of style ranges for the line (output argument). */ /** * This event is sent when the text view needs the style information for a line. @@ -2611,17 +2851,17 @@ orion.textview.TextView = (function() { * @property {Number} addedCharCount The number of characters added to the model. * @property {Number} removedLineCount The number of lines removed from the model. * @property {Number} addedLineCount The number of lines added to the model. */ /** * This event is sent when the text in the model has changed. * * @event - * @param {orion.textview.ModelChangingEvent} modelChangingEvent the event + * @param {orion.textview.ModelChangedEvent} modelChangedEvent the event */ onModelChanged: function(modelChangedEvent) { this._eventTable.sendEvent("ModelChanged", modelChangedEvent); }, /** * @class This is the event sent when the text in the model is about to change. * <p> * <b>See:</b><br/> @@ -2778,16 +3018,17 @@ orion.textview.TextView = (function() { if (startLine <= lineIndex && lineIndex < endLine) { child.lineChanged = true; } child = child.nextSibling; } } if (!ruler) { if (startLine <= this._maxLineIndex && this._maxLineIndex < endLine) { + this._checkMaxLineIndex = this._maxLineIndex; this._maxLineIndex = -1; this._maxLineWidth = 0; } } this._queueUpdatePage(); }, /** * Redraws the text in the given range. @@ -2966,43 +3207,44 @@ orion.textview.TextView = (function() { }, /** * Sets the text model of the text view. * * @param {orion.textview.TextModel} model the text model of the view. */ setModel: function(model) { if (!model) { return; } + if (model === this._model) { return; } this._model.removeListener(this._modelListener); var oldLineCount = this._model.getLineCount(); var oldCharCount = this._model.getCharCount(); var newLineCount = model.getLineCount(); var newCharCount = model.getCharCount(); var newText = model.getText(); var e = { text: newText, start: 0, removedCharCount: oldCharCount, addedCharCount: newCharCount, removedLineCount: oldLineCount, addedLineCount: newLineCount }; - this.onModelChanging(e); - this.redrawRange(); + this.onModelChanging(e); this._model = model; e = { start: 0, removedCharCount: oldCharCount, addedCharCount: newCharCount, removedLineCount: oldLineCount, addedLineCount: newLineCount }; this.onModelChanged(e); this._model.addListener(this._modelListener); - this.redrawRange(); + this._reset(); + this._updatePage(); }, /** * Sets the text view selection. * <p> * The selection is defined by a start and end character offset relative to the * document. The character at end offset is not included in the selection. * </p> * <p> @@ -3247,29 +3489,139 @@ orion.textview.TextView = (function() { this._lastMouseTime = time; if (this._clickCount !== 2) { this._clickCount = 2; this._handleMouse(e); } }, _handleDragStart: function (e) { if (!e) { e = window.event; } + if (isDnD) { + var sel = this._getSelection(); + var text = !sel.isEmpty() ? this._getBaseText(sel.start, sel.end) : ""; + if (text) { + e.dataTransfer.effectAllowed = "copyMove"; + e.dataTransfer.setData("text/plain", text); + // TODO: generate a drag image to be a better visual indicatator of the drag operation. + this._dragStartSelection = {start: sel.start, end: sel.end}; + this.focus(); + return; + } + } if (e.preventDefault) { e.preventDefault(); } return false; }, + _handleDragEnd: function (e) { + if (!e) { e = window.event; } + if (e.preventDefault) { e.preventDefault(); } + var startSel = this._dragStartSelection; + var drop = this._dropDestination; + if (startSel && e.dataTransfer.dropEffect === "move") { + var offset = 0; + if (drop && drop.offset < Math.min(startSel.start, startSel.end)) { + offset = drop.length; + } + var change = { + text: "", + start: startSel.start + offset, + end: startSel.end + offset + }; + this._modifyContent(change, false); + } + if (this._undoStack && drop) { + this._undoStack.endCompoundChange(); + } + this._dragNode.draggable = false; + this._dragStartSelection = null; + this._dropDestination = null; + return false; + }, + _handleDragEnter: function (e) { + if (!e) { e = window.event; } + if (e.preventDefault) { e.preventDefault(); } + var types = e.dataTransfer.types; + var allowed = false; + var types = isDnD ? e.dataTransfer.types : null; + if (types) { + // Firefox gives a .types of type StringList, while Webkit gives us an actual string. + allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain"); + } + if (allowed) { + e.dataTransfer.dropEffect = "copyMove"; + this.focus(); + return true; + } + e.dataTransfer.dropEffect = "none"; + return false; + }, _handleDragOver: function (e) { if (!e) { e = window.event; } - e.dataTransfer.dropEffect = "none"; if (e.preventDefault) { e.preventDefault(); } - return false; + var allowed = false; + var types = isDnD ? e.dataTransfer.types : null; + if (types) { + allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain"); + } + if (!allowed) { + e.dataTransfer.dropEffect = "none"; + return false; + } + + var destLine = this._getYToLine(e.clientY); + var destOffset = this._getXToOffset(destLine, e.clientX); + + var startSel = this._dragStartSelection; + if (startSel && startSel.start <= destOffset && destOffset <= startSel.end) { + e.dataTransfer.dropEffect = "none"; + return false; + } + + if (!startSel) { + // Hide the selection when the user drags something coming from the outside. + // TODO: make sure the cursor is actually visible. It's not visible in Firefox during drag, only in Chrome... + this.setSelection(destOffset, destOffset, true); + } + + return true; }, _handleDrop: function (e) { if (!e) { e = window.event; } if (e.preventDefault) { e.preventDefault(); } - return false; + var allowed = false; + var types = isDnD ? e.dataTransfer.types : null; + if (types) { + allowed = types.contains ? types.contains("text/plain") : types.indexOf("text/plain"); + } + if (!allowed) { + return false; + } + + var destLine = this._getYToLine(e.clientY); + var destOffset = this._getXToOffset(destLine, e.clientX); + var startSel = this._dragStartSelection; + + if (startSel && startSel.start <= destOffset && destOffset <= startSel.end) { + return false; + } + + var text = e.dataTransfer.getData("text/plain"); + this.setSelection(destOffset, destOffset, true); + + if (startSel) { + this._dropDestination = {offset: destOffset, length: text.length}; + if (this._undoStack) { + this._undoStack.startCompoundChange(); + } + } else { + this._dragNode.draggable = false; + } + + this._doContent(text); + this.focus(); + return true; }, _handleDocFocus: function (e) { if (!e) { e = window.event; } this._clientDiv.focus(); }, _handleFocus: function (e) { if (!e) { e = window.event; } this._hasFocus = true; @@ -3314,31 +3666,16 @@ orion.textview.TextView = (function() { if (e.preventDefault) { e.preventDefault(); } return false; } this._startIME(); } else { this._commitIME(); } /* - * Bug in Firefox. The paste operation on Firefox is done by switching - * focus into a textarea, let the user agent paste the text into the - * textarea and retrieve the text pasted from it. This works as expected - * in Firefox 3.x, but fails in Firefox 4 and greater. The fix is to - * switch focus to the textarea during the key down event that triggers - * the paste operation. - */ - if (isFirefox) { - var ctrlKey = isMac ? e.metaKey : e.ctrlKey; - if (ctrlKey && e.keyCode === 86 /*Ctrl+v*/) { - this._textArea.value = ""; - this._textArea.focus(); - } - } - /* * Feature in Firefox. When a key is held down the browser sends * right number of keypress events but only one keydown. This is * unexpected and causes the view to only execute an action * just one time. The fix is to ignore the keydown event and * execute the actions from the keypress handler. * Note: This only happens on the Mac and Linux (Firefox 3.6). * * Feature in Opera. Opera sends keypress events even for non-printable @@ -3482,28 +3819,51 @@ orion.textview.TextView = (function() { this._setLinksVisible(false); } else { return; } } var left = e.which ? e.button === 0 : e.button === 1; this._commitIME(); if (left) { - this._isMouseDown = true; var deltaX = Math.abs(this._lastMouseX - e.clientX); var deltaY = Math.abs(this._lastMouseY - e.clientY); var time = e.timeStamp ? e.timeStamp : new Date().getTime(); if ((time - this._lastMouseTime) <= this._clickTime && deltaX <= this._clickDist && deltaY <= this._clickDist) { this._clickCount++; } else { this._clickCount = 1; } this._lastMouseX = e.clientX; this._lastMouseY = e.clientY; this._lastMouseTime = time; + + // Selection drag support + if (isDnD && this._clickCount === 1) { + var inSelection = false; + var selection = this._getSelection(); + if (!selection.isEmpty()) { + var clickLine = this._getYToLine(e.clientY); + var clickOffset = this._getXToOffset(clickLine, e.clientX); + inSelection = selection.start < clickOffset && clickOffset < selection.end; + } + + // Webkit fails to allow dragging if .draggable is set to true during mousedown. + // But Firefox makes it a requirement to set .draggable to true. + this._dragNode.draggable = !isWebkit && inSelection; + + if (inSelection) { + return; // allow the dragstart event + } + } + if (this._dragNode && this._dragNode.draggable) { + this._dragNode.draggable = false; + } + + this._isMouseDown = true; this._handleMouse(e); if (isOpera || isChrome) { if (!this._hasFocus) { this.focus(); } e.preventDefault(); } } @@ -3586,16 +3946,24 @@ orion.textview.TextView = (function() { }, _handleMouseUp: function (e) { if (!e) { e = window.event; } if (this._linksVisible) { return; } var left = e.which ? e.button === 0 : e.button === 1; if (left) { + if (this._dragNode && this._dragNode.draggable) { + this._dragNode.draggable = false; + if (!this._dragStartSelection) { + this._setSelectionTo(e.clientX, e.clientY, false); + } + this.focus(); + } + this._isMouseDown = false; this._endAutoScroll(); /* * Feature in IE8 and older, the sequence of events in the IE8 event model * for a doule-click is: * * down @@ -4114,105 +4482,97 @@ orion.textview.TextView = (function() { return true; }, _doLineDown: function (args) { var model = this._model; var selection = this._getSelection(); var caret = selection.getCaret(); var lineIndex = model.getLineAtOffset(caret); if (lineIndex + 1 < model.getLineCount()) { + var scrollX = this._getScroll().x; var x = this._columnX; - if (x === -1 || args.select) { - x = this._getOffsetToX(caret); - } - selection.extend(this._getXToOffset(lineIndex + 1, x)); + if (x === -1 || args.select || args.wholeLine) { + var offset = args.wholeLine ? model.getLineEnd(lineIndex + 1) : caret; + x = this._getOffsetToX(offset) + scrollX; + } + selection.extend(this._getXToOffset(lineIndex + 1, x - scrollX)); if (!args.select) { selection.collapse(); } this._setSelection(selection, true, true); - this._columnX = x;//fix x by scrolling + this._columnX = x; } return true; }, _doLineUp: function (args) { var model = this._model; var selection = this._getSelection(); var caret = selection.getCaret(); var lineIndex = model.getLineAtOffset(caret); if (lineIndex > 0) { + var scrollX = this._getScroll().x; var x = this._columnX; - if (x === -1 || args.select) { - x = this._getOffsetToX(caret); - } - selection.extend(this._getXToOffset(lineIndex - 1, x)); + if (x === -1 || args.select || args.wholeLine) { + var offset = args.wholeLine ? model.getLineStart(lineIndex - 1) : caret; + x = this._getOffsetToX(offset) + scrollX; + } + selection.extend(this._getXToOffset(lineIndex - 1, x - scrollX)); if (!args.select) { selection.collapse(); } this._setSelection(selection, true, true); - this._columnX = x;//fix x by scrolling + this._columnX = x; } return true; }, _doPageDown: function (args) { var model = this._model; var selection = this._getSelection(); var caret = selection.getCaret(); var caretLine = model.getLineAtOffset(caret); var lineCount = model.getLineCount(); if (caretLine < lineCount - 1) { + var scroll = this._getScroll(); var clientHeight = this._getClientHeight(); var lineHeight = this._getLineHeight(); var lines = Math.floor(clientHeight / lineHeight); var scrollLines = Math.min(lineCount - caretLine - 1, lines); scrollLines = Math.max(1, scrollLines); var x = this._columnX; if (x === -1 || args.select) { - x = this._getOffsetToX(caret); - } - selection.extend(this._getXToOffset(caretLine + scrollLines, x)); + x = this._getOffsetToX(caret) + scroll.x; + } + selection.extend(this._getXToOffset(caretLine + scrollLines, x - scroll.x)); if (!args.select) { selection.collapse(); } - this._setSelection(selection, false, false); - var verticalMaximum = lineCount * lineHeight; - var verticalScrollOffset = this._getScroll().y; - var scrollOffset = verticalScrollOffset + scrollLines * lineHeight; + var scrollOffset = scroll.y + scrollLines * lineHeight; if (scrollOffset + clientHeight > verticalMaximum) { scrollOffset = verticalMaximum - clientHeight; - } - if (scrollOffset > verticalScrollOffset) { - this._scrollView(0, scrollOffset - verticalScrollOffset); - } else { - this._updateDOMSelection(); - } - this._columnX = x;//fix x by scrolling + } + this._setSelection(selection, true, true, scrollOffset - scroll.y); + this._columnX = x; } return true; }, _doPageUp: function (args) { var model = this._model; var selection = this._getSelection(); var caret = selection.getCaret(); var caretLine = model.getLineAtOffset(caret); if (caretLine > 0) { + var scroll = this._getScroll(); var clientHeight = this._getClientHeight(); var lineHeight = this._getLineHeight(); var lines = Math.floor(clientHeight / lineHeight); var scrollLines = Math.max(1, Math.min(caretLine, lines)); var x = this._columnX; if (x === -1 || args.select) { - x = this._getOffsetToX(caret); - } - selection.extend(this._getXToOffset(caretLine - scrollLines, x)); + x = this._getOffsetToX(caret) + scroll.x; + } + selection.extend(this._getXToOffset(caretLine - scrollLines, x - scroll.x)); if (!args.select) { selection.collapse(); } - this._setSelection(selection, false, false); - - var verticalScrollOffset = this._getScroll().y; - var scrollOffset = Math.max(0, verticalScrollOffset - scrollLines * lineHeight); - if (scrollOffset < verticalScrollOffset) { - this._scrollView(0, scrollOffset - verticalScrollOffset); - } else { - this._updateDOMSelection(); - } - this._columnX = x;//fix x by scrolling + var scrollOffset = Math.max(0, scroll.y - scrollLines * lineHeight); + this._setSelection(selection, true, true, scrollOffset - scroll.y); + this._columnX = x; } return true; }, _doPaste: function(e) { var text = this._getClipboardText(e); if (text) { this._doContent(text); } @@ -4323,36 +4683,34 @@ orion.textview.TextView = (function() { span3.appendChild(document.createTextNode(c)); line.appendChild(span3); var span4 = document.createElement("SPAN"); span4.style.fontWeight = "bold"; span4.style.fontStyle = "italic"; span4.appendChild(document.createTextNode(c)); line.appendChild(span4); parent.appendChild(line); + var lineRect = line.getBoundingClientRect(); var spanRect1 = span1.getBoundingClientRect(); var spanRect2 = span2.getBoundingClientRect(); var spanRect3 = span3.getBoundingClientRect(); var spanRect4 = span4.getBoundingClientRect(); var h1 = spanRect1.bottom - spanRect1.top; var h2 = spanRect2.bottom - spanRect2.top; var h3 = spanRect3.bottom - spanRect3.top; var h4 = spanRect4.bottom - spanRect4.top; var fontStyle = 0; - var lineHeight = h1; + var lineHeight = lineRect.bottom - lineRect.top; if (h2 > h1) { - lineHeight = h2; fontStyle = 1; } if (h3 > h2) { - lineHeight = h3; fontStyle = 2; } if (h4 > h3) { - lineHeight = h4; fontStyle = 3; } this._largestFontStyle = fontStyle; parent.removeChild(line); return lineHeight; }, _calculatePadding: function() { var document = this._frameDocument; @@ -4467,16 +4825,20 @@ orion.textview.TextView = (function() { bindings.push({name: "scrollTextEnd", keyBinding: new KeyBinding(35), predefined: true}); bindings.push({name: "textStart", keyBinding: new KeyBinding(38, true), predefined: true}); bindings.push({name: "textEnd", keyBinding: new KeyBinding(40, true), predefined: true}); } else { bindings.push({name: "pageUp", keyBinding: new KeyBinding(33), predefined: true}); bindings.push({name: "pageDown", keyBinding: new KeyBinding(34), predefined: true}); bindings.push({name: "lineStart", keyBinding: new KeyBinding(36), predefined: true}); bindings.push({name: "lineEnd", keyBinding: new KeyBinding(35), predefined: true}); + if (isLinux) { + bindings.push({name: "lineStartUp", keyBinding: new KeyBinding(38, true), predefined: true}); + bindings.push({name: "lineEndDown", keyBinding: new KeyBinding(40, true), predefined: true}); + } bindings.push({name: "wordPrevious", keyBinding: new KeyBinding(37, true), predefined: true}); bindings.push({name: "wordNext", keyBinding: new KeyBinding(39, true), predefined: true}); bindings.push({name: "textStart", keyBinding: new KeyBinding(36, true), predefined: true}); bindings.push({name: "textEnd", keyBinding: new KeyBinding(35, true), predefined: true}); } // Select Cursor Navigation bindings.push({name: "selectLineUp", keyBinding: new KeyBinding(38, null, true), predefined: true}); @@ -4490,16 +4852,20 @@ orion.textview.TextView = (function() { bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(39, true, true), predefined: true}); bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, null, true, true), predefined: true}); bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, null, true, true), predefined: true}); bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(38, true, true), predefined: true}); bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(40, true, true), predefined: true}); } else { + if (isLinux) { + bindings.push({name: "selectWholeLineUp", keyBinding: new KeyBinding(38, true, true), predefined: true}); + bindings.push({name: "selectWholeLineDown", keyBinding: new KeyBinding(40, true, true), predefined: true}); + } bindings.push({name: "selectLineStart", keyBinding: new KeyBinding(36, null, true), predefined: true}); bindings.push({name: "selectLineEnd", keyBinding: new KeyBinding(35, null, true), predefined: true}); bindings.push({name: "selectWordPrevious", keyBinding: new KeyBinding(37, true, true), predefined: true}); bindings.push({name: "selectWordNext", keyBinding: new KeyBinding(39, true, true), predefined: true}); bindings.push({name: "selectTextStart", keyBinding: new KeyBinding(36, true, true), predefined: true}); bindings.push({name: "selectTextEnd", keyBinding: new KeyBinding(35, true, true), predefined: true}); } @@ -4563,32 +4929,36 @@ orion.textview.TextView = (function() { //1 to 1, no duplicates var self = this; this._actions = [ {name: "lineUp", defaultHandler: function() {return self._doLineUp({select: false});}}, {name: "lineDown", defaultHandler: function() {return self._doLineDown({select: false});}}, {name: "lineStart", defaultHandler: function() {return self._doHome({select: false, ctrl:false});}}, {name: "lineEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:false});}}, + {name: "lineStartUp", defaultHandler: function() {return self._doLineUp({select: false, wholeLine:true});}}, + {name: "lineEndDown", defaultHandler: function() {return self._doLineDown({select: false, wholeLine:true});}}, {name: "charPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"character"});}}, {name: "charNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"character"});}}, {name: "pageUp", defaultHandler: function() {return self._doPageUp({select: false});}}, {name: "pageDown", defaultHandler: function() {return self._doPageDown({select: false});}}, {name: "scrollPageUp", defaultHandler: function() {return self._doScroll({type: "pageUp"});}}, {name: "scrollPageDown", defaultHandler: function() {return self._doScroll({type: "pageDown"});}}, {name: "wordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: false, unit:"word"});}}, {name: "wordNext", defaultHandler: function() {return self._doCursorNext({select: false, unit:"word"});}}, {name: "textStart", defaultHandler: function() {return self._doHome({select: false, ctrl:true});}}, {name: "textEnd", defaultHandler: function() {return self._doEnd({select: false, ctrl:true});}}, {name: "scrollTextStart", defaultHandler: function() {return self._doScroll({type: "textStart"});}}, {name: "scrollTextEnd", defaultHandler: function() {return self._doScroll({type: "textEnd"});}}, {name: "centerLine", defaultHandler: function() {return self._doScroll({type: "centerLine"});}}, {name: "selectLineUp", defaultHandler: function() {return self._doLineUp({select: true});}}, {name: "selectLineDown", defaultHandler: function() {return self._doLineDown({select: true});}}, + {name: "selectWholeLineUp", defaultHandler: function() {return self._doLineUp({select: true, wholeLine: true});}}, + {name: "selectWholeLineDown", defaultHandler: function() {return self._doLineDown({select: true, wholeLine: true});}}, {name: "selectLineStart", defaultHandler: function() {return self._doHome({select: true, ctrl:false});}}, {name: "selectLineEnd", defaultHandler: function() {return self._doEnd({select: true, ctrl:false});}}, {name: "selectCharPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"character"});}}, {name: "selectCharNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"character"});}}, {name: "selectPageUp", defaultHandler: function() {return self._doPageUp({select: true});}}, {name: "selectPageDown", defaultHandler: function() {return self._doPageDown({select: true});}}, {name: "selectWordPrevious", defaultHandler: function() {return self._doCursorPrevious({select: true, unit:"word"});}}, {name: "selectWordNext", defaultHandler: function() {return self._doCursorNext({select: true, unit:"word"});}}, @@ -4608,17 +4978,17 @@ orion.textview.TextView = (function() { {name: "copy", defaultHandler: function() {return self._doCopy();}}, {name: "cut", defaultHandler: function() {return self._doCut();}}, {name: "paste", defaultHandler: function() {return self._doPaste();}} ]; }, _createLine: function(parent, sibling, document, lineIndex, model) { var lineText = model.getLine(lineIndex); var lineStart = model.getLineStart(lineIndex); - var e = {lineIndex: lineIndex, lineText: lineText, lineStart: lineStart}; + var e = {textView: this, lineIndex: lineIndex, lineText: lineText, lineStart: lineStart}; this.onLineStyle(e); var child = document.createElement("DIV"); child.lineIndex = lineIndex; this._applyStyle(e.style, child); if (lineText.length !== 0) { var start = 0; var tabSize = this._customTabSize; if (tabSize && tabSize !== 8) { @@ -4910,24 +5280,22 @@ orion.textview.TextView = (function() { textArea.style.padding = "0px"; textArea.style.margin = "0px"; textArea.style.borderRadius = "0px"; textArea.style.WebkitAppearance = "none"; textArea.style.WebkitTapHighlightColor = "transparent"; touchDiv.appendChild(textArea); } if (isFirefox) { - textArea = frameDocument.createElement("TEXTAREA"); - this._textArea = textArea; - textArea.id = "textArea"; - textArea.style.position = "fixed"; - textArea.style.whiteSpace = "pre"; - textArea.style.left = "-1000px"; - textArea.tabIndex = -1; - body.appendChild(textArea); + var clipboardDiv = frameDocument.createElement("DIV"); + this._clipboardDiv = clipboardDiv; + clipboardDiv.style.position = "fixed"; + clipboardDiv.style.whiteSpace = "pre"; + clipboardDiv.style.left = "-1000px"; + body.appendChild(clipboardDiv); } var viewDiv = frameDocument.createElement("DIV"); viewDiv.className = "view"; this._viewDiv = viewDiv; viewDiv.id = "viewDiv"; viewDiv.tabIndex = -1; viewDiv.style.overflow = "auto"; @@ -5042,16 +5410,19 @@ orion.textview.TextView = (function() { overlayDiv.style.padding = clientDiv.style.padding; overlayDiv.style.cursor = "text"; overlayDiv.style.zIndex = "1"; scrollDiv.appendChild(overlayDiv); } if (!isPad) { clientDiv.contentEditable = "true"; } + if (isDnD) { + this._dragNode = this._overlayDiv || this._clientDiv; + } this._lineHeight = this._calculateLineHeight(); this._viewPadding = this._calculatePadding(); if (isIE) { body.style.lineHeight = this._lineHeight + "px"; } if (this._tabSize) { if (isOpera) { clientDiv.style.OTabSize = this._tabSize+""; @@ -5134,20 +5505,22 @@ orion.textview.TextView = (function() { if (this._touchDiv) { this._parent.removeChild(this._touchDiv); this._touchDiv = null; } this._selDiv1 = null; this._selDiv2 = null; this._selDiv3 = null; this._textArea = null; + this._clipboardDiv = null; this._scrollDiv = null; this._viewDiv = null; this._clientDiv = null; this._overlayDiv = null; + this._dragNode = null; this._leftDiv = null; this._rightDiv = null; }, _doAutoScroll: function (direction, x, y) { this._autoScrollDir = direction; this._autoScrollX = x; this._autoScrollY = y; if (!this._autoScrollTimerID) { @@ -5278,36 +5651,23 @@ orion.textview.TextView = (function() { //IE clipboadText = []; text = this._frameWindow.clipboardData.getData("Text"); this._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);}); return clipboadText.join(""); } if (isFirefox) { var document = this._frameDocument; - var textArea = this._textArea; - textArea.innerHTML = ""; - textArea.focus(); + var clipboardDiv = this._clipboardDiv; + clipboardDiv.innerHTML = "<pre contenteditable=''></pre>"; + clipboardDiv.firstChild.focus(); var self = this; var _getText = function() { - var text; - if (textArea.firstChild) { - text = ""; - var child = textArea.firstChild; - while (child) { - if (child.nodeType === child.TEXT_NODE) { - text += child.data; - } else if (child.tagName === "BR") { - text += delimiter; - } - child = child.nextSibling; - } - } else { - text = textArea.value; - } + var text = self._getTextFromElement(clipboardDiv); + clipboardDiv.innerHTML = ""; clipboadText = []; self._convertDelimiter(text, function(t) {clipboadText.push(t);}, function() {clipboadText.push(delimiter);}); return clipboadText.join(""); }; /* Try execCommand first. Works on firefox with clipboard permission. */ var result = false; this._ignorePaste = true; @@ -5368,16 +5728,46 @@ orion.textview.TextView = (function() { text += textNode.data; } textNode = textNode.nextSibling; } lineChild = lineChild.nextSibling; } return text; }, + _getTextFromElement: function(element) { + var document = element.ownerDocument; + var window = document.defaultView; + if (!window.getSelection) { + return element.innerText || element.textContent; + } + + var newRange = document.createRange(); + newRange.selectNode(element); + + var selection = window.getSelection(); + var oldRanges = []; + for (var i = 0; i < selection.rangeCount; i++) { + oldRanges.push(selection.getRangeAt(i)); + } + + this._ignoreSelect = true; + selection.removeAllRanges(); + selection.addRange(newRange); + + var text = selection.toString(); + + selection.removeAllRanges(); + for (var i = 0; i < oldRanges.length; i++) { + selection.addRange(oldRanges[i]); + } + + this._ignoreSelect = false; + return text; + }, _getViewPadding: function() { return this._viewPadding; }, _getLineBoundingClientRect: function (child) { var rect = child.getBoundingClientRect(); var lastChild = child.lastChild; //Remove any artificial trailing whitespace in the line while (lastChild && lastChild.ignoreChars === lastChild.firstChild.length) { @@ -5690,17 +6080,17 @@ orion.textview.TextView = (function() { if (found) { high = mid; } else { low = mid; } } offset += high; start = high; - end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : high + 1; + end = high === nodeLength - 1 && lineChild.ignoreChars ? textNode.length : Math.min(high + 1, textNode.length); if (isRangeRects) { range.setStart(textNode, start); range.setEnd(textNode, end); } else { range.moveToElementText(lineChild); range.move("character", start); range.moveEnd("character", end - start); } @@ -5809,22 +6199,22 @@ orion.textview.TextView = (function() { right += area; bottom += area; return (left <= x && x <= right && top <= y && y <= bottom); }, _hookEvents: function() { var self = this; this._modelListener = { /** @private */ - onChanging: function(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { - self._onModelChanging(newText, start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + onChanging: function(modelChangingEvent) { + self._onModelChanging(modelChangingEvent); }, /** @private */ - onChanged: function(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount) { - self._onModelChanged(start, removedCharCount, addedCharCount, removedLineCount, addedLineCount); + onChanged: function(modelChangedEvent) { + self._onModelChanged(modelChangedEvent); } }; this._model.addListener(this._modelListener); var clientDiv = this._clientDiv; var viewDiv = this._viewDiv; var body = this._frameDocument.body; var handlers = this._handlers = []; @@ -5842,44 +6232,46 @@ orion.textview.TextView = (function() { handlers.push({target: textArea, type: "input", handler: function(e) { return self._handleInput(e); }}); handlers.push({target: textArea, type: "textInput", handler: function(e) { return self._handleTextInput(e); }}); handlers.push({target: textArea, type: "click", handler: function(e) { return self._handleTextAreaClick(e); }}); handlers.push({target: touchDiv, type: "touchstart", handler: function(e) { return self._handleTouchStart(e); }}); handlers.push({target: touchDiv, type: "touchmove", handler: function(e) { return self._handleTouchMove(e); }}); handlers.push({target: touchDiv, type: "touchend", handler: function(e) { return self._handleTouchEnd(e); }}); } else { var topNode = this._overlayDiv || this._clientDiv; + var dragNode = this._dragNode || topNode; var grabNode = isIE ? clientDiv : this._frameWindow; handlers.push({target: clientDiv, type: "keydown", handler: function(e) { return self._handleKeyDown(e);}}); handlers.push({target: clientDiv, type: "keypress", handler: function(e) { return self._handleKeyPress(e);}}); handlers.push({target: clientDiv, type: "keyup", handler: function(e) { return self._handleKeyUp(e);}}); handlers.push({target: clientDiv, type: "selectstart", handler: function(e) { return self._handleSelectStart(e);}}); handlers.push({target: clientDiv, type: "contextmenu", handler: function(e) { return self._handleContextMenu(e);}}); handlers.push({target: clientDiv, type: "copy", handler: function(e) { return self._handleCopy(e);}}); handlers.push({target: clientDiv, type: "cut", handler: function(e) { return self._handleCut(e);}}); - if (!isFirefox) { - handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}}); - } + handlers.push({target: clientDiv, type: "paste", handler: function(e) { return self._handlePaste(e);}}); handlers.push({target: clientDiv, type: "mousedown", handler: function(e) { return self._handleMouseDown(e);}}); handlers.push({target: grabNode, type: "mouseup", handler: function(e) { return self._handleMouseUp(e);}}); handlers.push({target: grabNode, type: "mousemove", handler: function(e) { return self._handleMouseMove(e);}}); handlers.push({target: body, type: "mousedown", handler: function(e) { return self._handleBodyMouseDown(e);}}); - handlers.push({target: topNode, type: "dragstart", handler: function(e) { return self._handleDragStart(e);}}); - handlers.push({target: topNode, type: "dragover", handler: function(e) { return self._handleDragOver(e);}}); - handlers.push({ta