Changeset 277414 in webkit
- Timestamp:
- May 12, 2021, 5:31:44 PM (5 years ago)
- Location:
- trunk/Source/WebKit
- Files:
-
- 2 deleted
- 19 edited
-
ChangeLog (modified) (1 diff)
-
Sources.txt (modified) (1 diff)
-
UIProcess/API/APIUIClient.h (modified) (2 diffs)
-
UIProcess/API/C/WKNotificationPermissionRequest.cpp (modified) (1 diff)
-
UIProcess/API/C/WKPage.cpp (modified) (2 diffs)
-
UIProcess/API/glib/WebKitNotificationPermissionRequest.cpp (modified) (2 diffs)
-
UIProcess/API/glib/WebKitUIClient.cpp (modified) (2 diffs)
-
UIProcess/Cocoa/UIDelegate.h (modified) (1 diff)
-
UIProcess/Cocoa/UIDelegate.mm (modified) (1 diff)
-
UIProcess/Notifications/NotificationPermissionRequest.h (modified) (1 diff)
-
UIProcess/Notifications/NotificationPermissionRequestManagerProxy.cpp (deleted)
-
UIProcess/Notifications/NotificationPermissionRequestManagerProxy.h (deleted)
-
UIProcess/WebPageProxy.cpp (modified) (3 diffs)
-
UIProcess/WebPageProxy.h (modified) (3 diffs)
-
UIProcess/WebPageProxy.messages.in (modified) (1 diff)
-
WebKit.xcodeproj/project.pbxproj (modified) (4 diffs)
-
WebProcess/Notifications/NotificationPermissionRequestManager.cpp (modified) (3 diffs)
-
WebProcess/Notifications/NotificationPermissionRequestManager.h (modified) (2 diffs)
-
WebProcess/WebPage/WebPage.cpp (modified) (1 diff)
-
WebProcess/WebPage/WebPage.h (modified) (1 diff)
-
WebProcess/WebPage/WebPage.messages.in (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
trunk/Source/WebKit/ChangeLog
r277413 r277414 1 2021-05-12 Chris Dumez <cdumez@apple.com> 2 3 Simplify / Modernize Notification permission implementation 4 https://bugs.webkit.org/show_bug.cgi?id=225711 5 6 Reviewed by Alex Christensen. 7 8 Simplify / Modernize Notification permission implementation. The main change was 9 adopting sendWithAsyncReply() for the IPC instead of using 2 separate IPC messages 10 for the request and the decision. As a result, we don't have to deal with request 11 identifiers. 12 13 No new tests, no behavior change and covered by existing API tests. 14 15 * Sources.txt: 16 * UIProcess/API/APIUIClient.h: 17 (API::UIClient::decidePolicyForNotificationPermissionRequest): 18 * UIProcess/API/C/WKNotificationPermissionRequest.cpp: 19 (WKNotificationPermissionRequestAllow): 20 (WKNotificationPermissionRequestDeny): 21 * UIProcess/API/C/WKPage.cpp: 22 (WKPageSetPageUIClient): 23 * UIProcess/API/glib/WebKitNotificationPermissionRequest.cpp: 24 (webkitNotificationPermissionRequestAllow): 25 (webkitNotificationPermissionRequestDeny): 26 * UIProcess/API/glib/WebKitUIClient.cpp: 27 * UIProcess/Cocoa/UIDelegate.h: 28 * UIProcess/Cocoa/UIDelegate.mm: 29 (WebKit::UIDelegate::UIClient::decidePolicyForNotificationPermissionRequest): 30 * UIProcess/Notifications/NotificationPermissionRequest.h: 31 (WebKit::NotificationPermissionRequest::create): 32 (WebKit::NotificationPermissionRequest::~NotificationPermissionRequest): 33 (WebKit::NotificationPermissionRequest::didReceiveDecision): 34 (WebKit::NotificationPermissionRequest::NotificationPermissionRequest): 35 * UIProcess/Notifications/NotificationPermissionRequestManagerProxy.cpp: Removed. 36 * UIProcess/Notifications/NotificationPermissionRequestManagerProxy.h: Removed. 37 * UIProcess/WebPageProxy.cpp: 38 (WebKit::WebPageProxy::resetState): 39 (WebKit::WebPageProxy::requestNotificationPermission): 40 * UIProcess/WebPageProxy.h: 41 * UIProcess/WebPageProxy.messages.in: 42 * WebKit.xcodeproj/project.pbxproj: 43 * WebProcess/Notifications/NotificationPermissionRequestManager.cpp: 44 (WebKit::NotificationPermissionRequestManager::startRequest): 45 * WebProcess/Notifications/NotificationPermissionRequestManager.h: 46 * WebProcess/WebPage/WebPage.cpp: 47 * WebProcess/WebPage/WebPage.h: 48 * WebProcess/WebPage/WebPage.messages.in: 49 1 50 2021-05-12 Ada Chan <ada.chan@apple.com> 2 51 -
trunk/Source/WebKit/Sources.txt
r277264 r277414 470 470 UIProcess/Media/RemoteMediaSessionCoordinatorProxy.cpp 471 471 472 UIProcess/Notifications/NotificationPermissionRequestManagerProxy.cpp473 472 UIProcess/Notifications/WebNotification.cpp 474 473 UIProcess/Notifications/WebNotificationManagerProxy.cpp -
trunk/Source/WebKit/UIProcess/API/APIUIClient.h
r276853 r277414 55 55 class NativeWebKeyboardEvent; 56 56 class NativeWebWheelEvent; 57 class NotificationPermissionRequest;58 57 class UserMediaPermissionRequestProxy; 59 58 class WebColorPickerResultListenerProxy; … … 138 137 virtual void decidePolicyForUserMediaPermissionRequest(WebKit::WebPageProxy&, WebKit::WebFrameProxy&, SecurityOrigin&, SecurityOrigin&, WebKit::UserMediaPermissionRequestProxy& request) { request.doDefaultAction(); } 139 138 virtual void checkUserMediaPermissionForOrigin(WebKit::WebPageProxy&, WebKit::WebFrameProxy&, SecurityOrigin&, SecurityOrigin&, WebKit::UserMediaPermissionCheckProxy& request) { request.deny(); } 140 virtual void decidePolicyForNotificationPermissionRequest(WebKit::WebPageProxy&, SecurityOrigin&, Function<void(bool)>&& completionHandler) { completionHandler(false); }139 virtual void decidePolicyForNotificationPermissionRequest(WebKit::WebPageProxy&, SecurityOrigin&, CompletionHandler<void(bool allowed)>&& completionHandler) { completionHandler(false); } 141 140 virtual void requestStorageAccessConfirm(WebKit::WebPageProxy&, WebKit::WebFrameProxy*, const WebCore::RegistrableDomain& requestingDomain, const WebCore::RegistrableDomain& currentDomain, CompletionHandler<void(bool)>&& completionHandler) { completionHandler(true); } 142 141 -
trunk/Source/WebKit/UIProcess/API/C/WKNotificationPermissionRequest.cpp
r102352 r277414 39 39 void WKNotificationPermissionRequestAllow(WKNotificationPermissionRequestRef notificationPermissionRequest) 40 40 { 41 return toImpl(notificationPermissionRequest)-> allow();41 return toImpl(notificationPermissionRequest)->didReceiveDecision(true); 42 42 } 43 43 44 44 void WKNotificationPermissionRequestDeny(WKNotificationPermissionRequestRef notificationPermissionRequest) 45 45 { 46 return toImpl(notificationPermissionRequest)->d eny();46 return toImpl(notificationPermissionRequest)->didReceiveDecision(false); 47 47 } -
trunk/Source/WebKit/UIProcess/API/C/WKPage.cpp
r277356 r277414 63 63 #include "NativeWebWheelEvent.h" 64 64 #include "NavigationActionData.h" 65 #include "NotificationPermissionRequest.h" 65 66 #include "PageClient.h" 66 67 #include "PluginInformation.h" … … 1989 1990 } 1990 1991 1991 void decidePolicyForNotificationPermissionRequest(WebPageProxy& page, API::SecurityOrigin& origin, Function<void(bool)>&& completionHandler) final1992 void decidePolicyForNotificationPermissionRequest(WebPageProxy& page, API::SecurityOrigin& origin, CompletionHandler<void(bool allowed)>&& completionHandler) final 1992 1993 { 1993 1994 if (!m_client.decidePolicyForNotificationPermissionRequest) -
trunk/Source/WebKit/UIProcess/API/glib/WebKitNotificationPermissionRequest.cpp
r218487 r277414 65 65 return; 66 66 67 priv->request-> allow();67 priv->request->didReceiveDecision(true); 68 68 priv->madeDecision = true; 69 69 } … … 79 79 return; 80 80 81 priv->request->d eny();81 priv->request->didReceiveDecision(false); 82 82 priv->madeDecision = true; 83 83 } -
trunk/Source/WebKit/UIProcess/API/glib/WebKitUIClient.cpp
r276490 r277414 23 23 #include "APIUIClient.h" 24 24 #include "DrawingAreaProxy.h" 25 #include "NotificationPermissionRequest.h" 25 26 #include "WebKitDeviceInfoPermissionRequestPrivate.h" 26 27 #include "WebKitFileChooserRequestPrivate.h" … … 306 307 } 307 308 308 void decidePolicyForNotificationPermissionRequest(WebPageProxy&, API::SecurityOrigin&, Function<void(bool)>&& completionHandler) final309 void decidePolicyForNotificationPermissionRequest(WebPageProxy&, API::SecurityOrigin&, CompletionHandler<void(bool allowed)>&& completionHandler) final 309 310 { 310 311 GRefPtr<WebKitNotificationPermissionRequest> notificationPermissionRequest = adoptGRef(webkitNotificationPermissionRequestCreate(NotificationPermissionRequest::create(WTFMove(completionHandler)).ptr())); -
trunk/Source/WebKit/UIProcess/Cocoa/UIDelegate.h
r276853 r277414 118 118 void drawHeader(WebPageProxy&, WebFrameProxy&, WebCore::FloatRect&&) final; 119 119 void drawFooter(WebPageProxy&, WebFrameProxy&, WebCore::FloatRect&&) final; 120 void decidePolicyForNotificationPermissionRequest(WebPageProxy&, API::SecurityOrigin&, Function<void(bool)>&&) final;120 void decidePolicyForNotificationPermissionRequest(WebPageProxy&, API::SecurityOrigin&, CompletionHandler<void(bool allowed)>&&) final; 121 121 void unavailablePluginButtonClicked(WebPageProxy&, WKPluginUnavailabilityReason, API::Dictionary&) final; 122 122 void mouseDidMoveOverElement(WebPageProxy&, const WebHitTestResultData&, OptionSet<WebEvent::Modifier>, API::Object*); -
trunk/Source/WebKit/UIProcess/Cocoa/UIDelegate.mm
r276853 r277414 947 947 } 948 948 949 void UIDelegate::UIClient::decidePolicyForNotificationPermissionRequest(WebKit::WebPageProxy&, API::SecurityOrigin& securityOrigin, WTF::Function<void(bool)>&& completionHandler)949 void UIDelegate::UIClient::decidePolicyForNotificationPermissionRequest(WebKit::WebPageProxy&, API::SecurityOrigin& securityOrigin, CompletionHandler<void(bool allowed)>&& completionHandler) 950 950 { 951 951 if (!m_uiDelegate) -
trunk/Source/WebKit/UIProcess/Notifications/NotificationPermissionRequest.h
r221894 r277414 27 27 28 28 #include "APIObject.h" 29 #include <wtf/ Function.h>29 #include <wtf/CompletionHandler.h> 30 30 31 31 namespace WebKit { 32 32 33 class NotificationPermissionRequestManagerProxy;34 35 33 class NotificationPermissionRequest : public API::ObjectImpl<API::Object::Type::NotificationPermissionRequest> { 36 34 public: 37 static Ref<NotificationPermissionRequest> create( Function<void(bool)>&& completionHandler)35 static Ref<NotificationPermissionRequest> create(CompletionHandler<void(bool)>&& completionHandler) 38 36 { 39 37 return adoptRef(*new NotificationPermissionRequest(WTFMove(completionHandler))); 40 38 } 39 40 ~NotificationPermissionRequest() 41 { 42 didReceiveDecision(false); 43 } 41 44 42 void allow()45 void didReceiveDecision(bool allowed) 43 46 { 44 if (auto completionHandler = std::exchange(m_completionHandler, nullptr)) 45 completionHandler(true); 46 } 47 48 void deny() 49 { 50 if (auto completionHandler = std::exchange(m_completionHandler, nullptr)) 51 completionHandler(false); 47 if (m_completionHandler) 48 m_completionHandler(allowed); 52 49 } 53 50 54 51 private: 55 NotificationPermissionRequest(Function<void(bool)>&& completionHandler)52 explicit NotificationPermissionRequest(CompletionHandler<void(bool)>&& completionHandler) 56 53 : m_completionHandler(WTFMove(completionHandler)) 57 { } 54 { 55 } 58 56 59 Function<void(bool)> m_completionHandler;57 CompletionHandler<void(bool)> m_completionHandler; 60 58 }; 61 59 -
trunk/Source/WebKit/UIProcess/WebPageProxy.cpp
r277375 r277414 483 483 #endif 484 484 , m_geolocationPermissionRequestManager(*this) 485 , m_notificationPermissionRequestManager(*this)486 485 #if PLATFORM(IOS_FAMILY) 487 486 , m_audibleActivityTimer(RunLoop::main(), this, &WebPageProxy::clearAudibleActivity) … … 7632 7631 m_geolocationPermissionRequestManager.invalidateRequests(); 7633 7632 #endif 7634 7635 m_notificationPermissionRequestManager.invalidateRequests();7636 7633 7637 7634 setToolTip({ }); … … 8448 8445 8449 8446 8450 void WebPageProxy::requestNotificationPermission(uint64_t requestID, const String& originString) 8451 { 8452 if (!isRequestIDValid(requestID)) 8453 return; 8454 8447 void WebPageProxy::requestNotificationPermission(const String& originString, CompletionHandler<void(bool allowed)>&& completionHandler) 8448 { 8455 8449 auto origin = API::SecurityOrigin::createFromString(originString); 8456 auto request = m_notificationPermissionRequestManager.createRequest(requestID); 8457 8458 m_uiClient->decidePolicyForNotificationPermissionRequest(*this, origin.get(), [request = WTFMove(request)](bool allowed) { 8459 if (allowed) 8460 request->allow(); 8461 else 8462 request->deny(); 8463 }); 8450 m_uiClient->decidePolicyForNotificationPermissionRequest(*this, origin.get(), WTFMove(completionHandler)); 8464 8451 } 8465 8452 -
trunk/Source/WebKit/UIProcess/WebPageProxy.h
r277356 r277414 43 43 #include "MediaPlaybackState.h" 44 44 #include "MessageSender.h" 45 #include "NotificationPermissionRequestManagerProxy.h"46 45 #include "PDFPluginIdentifier.h" 47 46 #include "PageLoadState.h" … … 2130 2129 RefPtr<API::Navigation> launchProcessForReload(); 2131 2130 2132 void requestNotificationPermission( uint64_t notificationID, const String& originString);2131 void requestNotificationPermission(const String& originString, CompletionHandler<void(bool allowed)>&&); 2133 2132 void showNotification(const String& title, const String& body, const String& iconURL, const String& tag, const String& lang, WebCore::NotificationDirection, const String& originString, uint64_t notificationID); 2134 2133 void cancelNotification(uint64_t notificationID); … … 2601 2600 RefPtr<WebOpenPanelResultListenerProxy> m_openPanelResultListener; 2602 2601 GeolocationPermissionRequestManagerProxy m_geolocationPermissionRequestManager; 2603 NotificationPermissionRequestManagerProxy m_notificationPermissionRequestManager;2604 2602 2605 2603 #if ENABLE(MEDIA_STREAM) -
trunk/Source/WebKit/UIProcess/WebPageProxy.messages.in
r277196 r277414 282 282 283 283 # Notification messages 284 RequestNotificationPermission( uint64_t requestID, String originIdentifier)284 RequestNotificationPermission(String originIdentifier) -> (bool allowed) Async 285 285 ShowNotification(String title, String body, String iconURL, String tag, String lang, enum:uint8_t WebCore::NotificationDirection dir, String originIdentifier, uint64_t notificationID) 286 286 CancelNotification(uint64_t notificationID) -
trunk/Source/WebKit/WebKit.xcodeproj/project.pbxproj
r277264 r277414 823 823 31A2EC4B148997C200810D71 /* WebNotificationManagerProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 31A2EC44148997BF00810D71 /* WebNotificationManagerProxy.h */; }; 824 824 31A2EC4E148997C200810D71 /* WebNotificationProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 31A2EC47148997C100810D71 /* WebNotificationProvider.h */; }; 825 31A2EC521489981900810D71 /* NotificationPermissionRequestManagerProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 31A2EC511489981500810D71 /* NotificationPermissionRequestManagerProxy.h */; };826 825 31A2EC5614899C0900810D71 /* NotificationPermissionRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 31A2EC531489982500810D71 /* NotificationPermissionRequest.h */; }; 827 826 31A2EC74148D59CA00810D71 /* WKNotificationPermissionRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = 31A2EC72148D59BB00810D71 /* WKNotificationPermissionRequest.h */; settings = {ATTRIBUTES = (Private, ); }; }; … … 3577 3576 31A2EC46148997C000810D71 /* WebNotificationProvider.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = WebNotificationProvider.cpp; sourceTree = "<group>"; }; 3578 3577 31A2EC47148997C100810D71 /* WebNotificationProvider.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = WebNotificationProvider.h; sourceTree = "<group>"; }; 3579 31A2EC4F1489980500810D71 /* NotificationPermissionRequestManagerProxy.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = NotificationPermissionRequestManagerProxy.cpp; sourceTree = "<group>"; };3580 31A2EC511489981500810D71 /* NotificationPermissionRequestManagerProxy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationPermissionRequestManagerProxy.h; sourceTree = "<group>"; };3581 3578 31A2EC531489982500810D71 /* NotificationPermissionRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotificationPermissionRequest.h; sourceTree = "<group>"; }; 3582 3579 31A2EC72148D59BB00810D71 /* WKNotificationPermissionRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKNotificationPermissionRequest.h; sourceTree = "<group>"; }; … … 7838 7835 children = ( 7839 7836 31A2EC531489982500810D71 /* NotificationPermissionRequest.h */, 7840 31A2EC4F1489980500810D71 /* NotificationPermissionRequestManagerProxy.cpp */,7841 31A2EC511489981500810D71 /* NotificationPermissionRequestManagerProxy.h */,7842 7837 31A2EC41148997BE00810D71 /* WebNotification.cpp */, 7843 7838 31A2EC42148997BE00810D71 /* WebNotification.h */, … … 12111 12106 31A2EC5614899C0900810D71 /* NotificationPermissionRequest.h in Headers */, 12112 12107 3131261F148FF82C00BA2A39 /* NotificationPermissionRequestManager.h in Headers */, 12113 31A2EC521489981900810D71 /* NotificationPermissionRequestManagerProxy.h in Headers */,12114 12108 1A2D848B127F6A49001EB962 /* NPIdentifierData.h in Headers */, 12115 12109 1AE4976811FF658E0048B464 /* NPJSObject.h in Headers */, -
trunk/Source/WebKit/WebProcess/Notifications/NotificationPermissionRequestManager.cpp
r277380 r277414 44 44 using namespace WebCore; 45 45 46 #if ENABLE(NOTIFICATIONS)47 static uint64_t generateRequestID()48 {49 static uint64_t uniqueRequestID = 1;50 return uniqueRequestID++;51 }52 #endif53 54 46 Ref<NotificationPermissionRequestManager> NotificationPermissionRequestManager::create(WebPage* page) 55 47 { … … 83 75 return; 84 76 85 uint64_t requestID = generateRequestID();86 m_idToOriginMap.set(requestID, securityOrigin);77 m_page->sendWithAsyncReply(Messages::WebPageProxy::RequestNotificationPermission(securityOrigin.toString()), [this, protectedThis = makeRef(*this), callback = WTFMove(callback), securityOrigin](bool allowed) mutable { 78 WebProcess::singleton().supplement<WebNotificationManager>()->didUpdateNotificationDecision(securityOrigin.toString(), allowed); 87 79 88 // FIXME: This should use sendWithAsyncReply(). 89 m_page->send(Messages::WebPageProxy::RequestNotificationPermission(requestID, securityOrigin.toString())); 80 auto callbacks = m_requestsPerOrigin.take(securityOrigin); 81 for (auto& callback : callbacks) { 82 if (callback) 83 callback->handleEvent(allowed ? NotificationClient::Permission::Granted : NotificationClient::Permission::Denied); 84 } 85 }); 90 86 } 91 87 #endif … … 121 117 } 122 118 123 void NotificationPermissionRequestManager::didReceiveNotificationPermissionDecision(uint64_t requestID, bool allowed)124 {125 #if ENABLE(NOTIFICATIONS)126 if (!isRequestIDValid(requestID))127 return;128 129 auto origin = m_idToOriginMap.take(requestID);130 if (origin.isEmpty())131 return;132 133 WebProcess::singleton().supplement<WebNotificationManager>()->didUpdateNotificationDecision(origin.toString(), allowed);134 135 auto callbacks = m_requestsPerOrigin.take(origin);136 for (auto& callback : callbacks) {137 if (callback)138 callback->handleEvent(allowed ? NotificationClient::Permission::Granted : NotificationClient::Permission::Denied);139 }140 #else141 UNUSED_PARAM(requestID);142 UNUSED_PARAM(allowed);143 #endif144 }145 146 119 } // namespace WebKit -
trunk/Source/WebKit/WebProcess/Notifications/NotificationPermissionRequestManager.h
r277376 r277414 58 58 void removeAllPermissionsForTesting(); 59 59 60 void didReceiveNotificationPermissionDecision(uint64_t notificationID, bool allowed);61 62 60 private: 63 61 NotificationPermissionRequestManager(WebPage*); … … 65 63 #if ENABLE(NOTIFICATIONS) 66 64 HashMap<WebCore::SecurityOriginData, Vector<RefPtr<WebCore::NotificationPermissionCallback>>> m_requestsPerOrigin; 67 HashMap<uint64_t, WebCore::SecurityOriginData> m_idToOriginMap;68 65 WebPage* m_page; 69 66 #endif -
trunk/Source/WebKit/WebProcess/WebPage/WebPage.cpp
r277356 r277414 4655 4655 #endif 4656 4656 4657 void WebPage::didReceiveNotificationPermissionDecision(uint64_t notificationID, bool allowed)4658 {4659 notificationPermissionRequestManager()->didReceiveNotificationPermissionDecision(notificationID, allowed);4660 }4661 4662 4657 #if ENABLE(MEDIA_STREAM) 4663 4658 -
trunk/Source/WebKit/WebProcess/WebPage/WebPage.h
r277356 r277414 1737 1737 void didReceiveGeolocationPermissionDecision(GeolocationIdentifier, const String& authorizationToken); 1738 1738 1739 void didReceiveNotificationPermissionDecision(uint64_t notificationID, bool allowed);1740 1741 1739 #if ENABLE(MEDIA_STREAM) 1742 1740 void userMediaAccessWasGranted(uint64_t userMediaID, WebCore::CaptureDevice&& audioDeviceUID, WebCore::CaptureDevice&& videoDeviceUID, String&& mediaDeviceIdentifierHashSalt, SandboxExtension::Handle&&, CompletionHandler<void()>&&); -
trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
r277356 r277414 394 394 ResumeAllMediaPlayback() -> () Async 395 395 396 # Notification397 DidReceiveNotificationPermissionDecision(uint64_t notificationID, bool allowed)398 399 396 FreezeLayerTreeDueToSwipeAnimation() 400 397 UnfreezeLayerTreeDueToSwipeAnimation()
Note:
See TracChangeset
for help on using the changeset viewer.