OLD | NEW |
1 <!DOCTYPE html> | 1 <!DOCTYPE html> |
2 <!-- | 2 <!-- |
3 Copyright (c) 2012 The Chromium Authors. All rights reserved. | 3 Copyright (c) 2012 The Chromium Authors. All rights reserved. |
4 Use of this source code is governed by a BSD-style license that can be | 4 Use of this source code is governed by a BSD-style license that can be |
5 found in the LICENSE file. | 5 found in the LICENSE file. |
6 --> | 6 --> |
7 | 7 |
8 <link rel="import" href="/tracing/base/event.html"> | 8 <link rel="import" href="/tracing/base/event.html"> |
9 <link rel="import" href="/tracing/base/iteration_helpers.html"> | 9 <link rel="import" href="/tracing/base/iteration_helpers.html"> |
10 <link rel="import" href="/tracing/base/settings.html"> | 10 <link rel="import" href="/tracing/base/settings.html"> |
11 <link rel="import" href="/tracing/base/task.html"> | 11 <link rel="import" href="/tracing/base/task.html"> |
12 <link rel="import" href="/tracing/base/unit.html"> | 12 <link rel="import" href="/tracing/base/unit.html"> |
13 <link rel="import" href="/tracing/core/filter.html"> | 13 <link rel="import" href="/tracing/core/filter.html"> |
14 <link rel="import" href="/tracing/model/event.html"> | 14 <link rel="import" href="/tracing/model/event.html"> |
15 <link rel="import" href="/tracing/model/event_set.html"> | 15 <link rel="import" href="/tracing/model/event_set.html"> |
16 <link rel="import" href="/tracing/model/x_marker_annotation.html"> | 16 <link rel="import" href="/tracing/model/x_marker_annotation.html"> |
17 <link rel="import" href="/tracing/ui/base/hotkey_controller.html"> | 17 <link rel="import" href="/tracing/ui/base/hotkey_controller.html"> |
18 <link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> | 18 <link rel="import" href="/tracing/ui/base/mouse_tracker.html"> |
19 <link rel="import" href="/tracing/ui/base/timing_tool.html"> | 19 <link rel="import" href="/tracing/ui/base/timing_tool.html"> |
20 <link rel="import" href="/tracing/ui/base/ui.html"> | 20 <link rel="import" href="/tracing/ui/base/ui.html"> |
21 <link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"
> | 21 <link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"
> |
22 <link rel="import" href="/tracing/ui/timeline_viewport.html"> | 22 <link rel="import" href="/tracing/ui/timeline_viewport.html"> |
23 <link rel="import" href="/tracing/ui/tracks/drawing_container.html"> | 23 <link rel="import" href="/tracing/ui/tracks/drawing_container.html"> |
24 <link rel="import" href="/tracing/ui/tracks/model_track.html"> | 24 <link rel="import" href="/tracing/ui/tracks/model_track.html"> |
25 <link rel="import" href="/tracing/ui/tracks/x_axis_track.html"> | 25 <link rel="import" href="/tracing/ui/tracks/x_axis_track.html"> |
26 | 26 |
27 <!-- | 27 <!-- |
28 Interactive visualizaiton of Model objects based loosely on gantt charts. | 28 Interactive visualizaiton of Model objects based loosely on gantt charts. |
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
72 <tv-ui-b-hotkey-controller id='hotkey_controller'> | 72 <tv-ui-b-hotkey-controller id='hotkey_controller'> |
73 </tv-ui-b-hotkey-controller> | 73 </tv-ui-b-hotkey-controller> |
74 </template> | 74 </template> |
75 </dom-module> | 75 </dom-module> |
76 <script> | 76 <script> |
77 'use strict'; | 77 'use strict'; |
78 | 78 |
79 Polymer({ | 79 Polymer({ |
80 is: 'tr-ui-timeline-track-view', | 80 is: 'tr-ui-timeline-track-view', |
81 | 81 |
| 82 created() { |
| 83 this.mouseMode_ = tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION; |
| 84 }, |
| 85 |
82 ready() { | 86 ready() { |
83 this.displayTransform_ = new tr.ui.TimelineDisplayTransform(); | 87 this.displayTransform_ = new tr.ui.TimelineDisplayTransform(); |
84 this.model_ = undefined; | 88 this.model_ = undefined; |
85 | 89 |
86 this.timelineView_ = undefined; | 90 this.timelineView_ = undefined; |
87 this.pollIfViewportAttachedInterval_ = undefined; | 91 this.pollIfViewportAttachedInterval_ = undefined; |
88 | 92 |
89 this.viewport_ = new tr.ui.TimelineViewport(this); | 93 this.viewport_ = new tr.ui.TimelineViewport(this); |
90 this.viewportDisplayTransformAtMouseDown_ = undefined; | 94 this.viewportDisplayTransformAtMouseDown_ = undefined; |
91 this.brushingStateController_ = undefined; | 95 this.brushingStateController_ = undefined; |
(...skipping 16 matching lines...) Expand all Loading... |
108 this.modelTrackContainer_.style.display = 'block'; | 112 this.modelTrackContainer_.style.display = 'block'; |
109 this.modelTrackContainer_.invalidate(); | 113 this.modelTrackContainer_.invalidate(); |
110 | 114 |
111 this.viewport_.modelTrackContainer = this.modelTrackContainer_; | 115 this.viewport_.modelTrackContainer = this.modelTrackContainer_; |
112 | 116 |
113 this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); | 117 this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); |
114 Polymer.dom(this.modelTrackContainer_).appendChild(this.modelTrack_); | 118 Polymer.dom(this.modelTrackContainer_).appendChild(this.modelTrack_); |
115 | 119 |
116 this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this); | 120 this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this); |
117 | 121 |
118 this.initMouseModeSelector(); | |
119 | |
120 this.hideDragBox_(); | 122 this.hideDragBox_(); |
121 | 123 |
122 this.initHintText_(); | 124 this.initHintText_(); |
123 | 125 |
124 this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); | 126 this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); |
125 | 127 |
126 this.onDblClick_ = this.onDblClick_.bind(this); | 128 this.onDblClick_ = this.onDblClick_.bind(this); |
127 this.addEventListener('dblclick', this.onDblClick_); | 129 this.addEventListener('dblclick', this.onDblClick_); |
128 | 130 |
129 this.onMouseWheel_ = this.onMouseWheel_.bind(this); | 131 this.onMouseWheel_ = this.onMouseWheel_.bind(this); |
130 this.addEventListener('mousewheel', this.onMouseWheel_); | 132 this.addEventListener('mousewheel', this.onMouseWheel_); |
131 | 133 |
132 this.onMouseDown_ = this.onMouseDown_.bind(this); | 134 this.onMouseDown_ = this.onMouseDown_.bind(this); |
133 this.addEventListener('mousedown', this.onMouseDown_); | 135 this.addEventListener('mousedown', this.onMouseDown_); |
134 | 136 |
| 137 this.onMouseUp_ = this.onMouseUp_.bind(this); |
| 138 this.addEventListener('mousedown', this.onMouseUp_); |
| 139 |
135 this.onMouseMove_ = this.onMouseMove_.bind(this); | 140 this.onMouseMove_ = this.onMouseMove_.bind(this); |
136 this.addEventListener('mousemove', this.onMouseMove_); | |
137 | 141 |
138 this.onTouchStart_ = this.onTouchStart_.bind(this); | 142 this.onTouchStart_ = this.onTouchStart_.bind(this); |
139 this.addEventListener('touchstart', this.onTouchStart_); | 143 this.addEventListener('touchstart', this.onTouchStart_); |
140 | 144 |
141 this.onTouchMove_ = this.onTouchMove_.bind(this); | 145 this.onTouchMove_ = this.onTouchMove_.bind(this); |
142 this.addEventListener('touchmove', this.onTouchMove_); | 146 this.addEventListener('touchmove', this.onTouchMove_); |
143 | 147 |
144 this.onTouchEnd_ = this.onTouchEnd_.bind(this); | 148 this.onTouchEnd_ = this.onTouchEnd_.bind(this); |
145 this.addEventListener('touchend', this.onTouchEnd_); | 149 this.addEventListener('touchend', this.onTouchEnd_); |
146 | 150 |
147 | 151 |
148 this.addHotKeys_(); | 152 this.addHotKeys_(); |
149 | 153 |
150 this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; | 154 this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; |
151 this.lastMouseViewPos_ = {x: 0, y: 0}; | 155 this.lastMouseViewPos_ = {x: 0, y: 0}; |
152 | 156 |
153 this.lastTouchViewPositions_ = []; | 157 this.lastTouchViewPositions_ = []; |
154 | 158 |
155 this.alert_ = undefined; | 159 this.alert_ = undefined; |
156 | 160 |
157 this.isPanningAndScanning_ = false; | 161 this.isPanningAndScanning_ = false; |
158 this.isZooming_ = false; | 162 this.isZooming_ = false; |
159 }, | 163 }, |
160 | 164 |
161 initMouseModeSelector() { | 165 get mouseMode() { |
162 this.mouseModeSelector_ = document.createElement( | 166 return this.mouseMode_; |
163 'tr-ui-b-mouse-mode-selector'); | 167 }, |
164 this.mouseModeSelector_.targetElement = this; | |
165 Polymer.dom(this).appendChild(this.mouseModeSelector_); | |
166 | 168 |
167 this.mouseModeSelector_.addEventListener('beginpan', | 169 set mouseMode(m) { |
168 this.onBeginPanScan_.bind(this)); | 170 if (m === this.mouseMode_) return; |
169 this.mouseModeSelector_.addEventListener('updatepan', | 171 if (this.mouseMode_ === tr.ui.b.MOUSE_SELECTOR_MODE.TIMING) { |
170 this.onUpdatePanScan_.bind(this)); | 172 this.timingTool_.onExitTiming(); |
171 this.mouseModeSelector_.addEventListener('endpan', | 173 } |
172 this.onEndPanScan_.bind(this)); | 174 this.mouseMode_ = m; |
173 | 175 if (this.mouseMode_ === tr.ui.b.MOUSE_SELECTOR_MODE.TIMING) { |
174 this.mouseModeSelector_.addEventListener('beginselection', | 176 this.timingTool_.onEnterTiming(); |
175 this.onBeginSelection_.bind(this)); | 177 } |
176 this.mouseModeSelector_.addEventListener('updateselection', | |
177 this.onUpdateSelection_.bind(this)); | |
178 this.mouseModeSelector_.addEventListener('endselection', | |
179 this.onEndSelection_.bind(this)); | |
180 | |
181 this.mouseModeSelector_.addEventListener('beginzoom', | |
182 this.onBeginZoom_.bind(this)); | |
183 this.mouseModeSelector_.addEventListener('updatezoom', | |
184 this.onUpdateZoom_.bind(this)); | |
185 this.mouseModeSelector_.addEventListener('endzoom', | |
186 this.onEndZoom_.bind(this)); | |
187 | |
188 this.mouseModeSelector_.addEventListener('entertiming', | |
189 this.timingTool_.onEnterTiming.bind(this.timingTool_)); | |
190 this.mouseModeSelector_.addEventListener('begintiming', | |
191 this.timingTool_.onBeginTiming.bind(this.timingTool_)); | |
192 this.mouseModeSelector_.addEventListener('updatetiming', | |
193 this.timingTool_.onUpdateTiming.bind(this.timingTool_)); | |
194 this.mouseModeSelector_.addEventListener('endtiming', | |
195 this.timingTool_.onEndTiming.bind(this.timingTool_)); | |
196 this.mouseModeSelector_.addEventListener('exittiming', | |
197 this.timingTool_.onExitTiming.bind(this.timingTool_)); | |
198 | |
199 const m = tr.ui.b.MOUSE_SELECTOR_MODE; | |
200 this.mouseModeSelector_.supportedModeMask = | |
201 m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING; | |
202 this.mouseModeSelector_.settingsKey = | |
203 'timelineTrackView.mouseModeSelector'; | |
204 this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0)); | |
205 this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0)); | |
206 this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0)); | |
207 this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0)); | |
208 | |
209 this.mouseModeSelector_.setModifierForAlternateMode( | |
210 m.SELECTION, tr.ui.b.MODIFIER.SHIFT); | |
211 this.mouseModeSelector_.setModifierForAlternateMode( | |
212 m.PANSCAN, tr.ui.b.MODIFIER.SPACE); | |
213 }, | 178 }, |
214 | 179 |
215 get brushingStateController() { | 180 get brushingStateController() { |
216 return this.brushingStateController_; | 181 return this.brushingStateController_; |
217 }, | 182 }, |
218 | 183 |
219 set brushingStateController(brushingStateController) { | 184 set brushingStateController(brushingStateController) { |
220 if (this.brushingStateController_) { | 185 if (this.brushingStateController_) { |
221 this.brushingStateController_.removeEventListener('change', | 186 this.brushingStateController_.removeEventListener('change', |
222 this.onSelectionChanged_); | 187 this.onSelectionChanged_); |
(...skipping 124 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
347 const lastT = firstT.after(function() { | 312 const lastT = firstT.after(function() { |
348 this.upperModelTrack_.addAllEventsMatchingFilterToSelection( | 313 this.upperModelTrack_.addAllEventsMatchingFilterToSelection( |
349 filter, selection); | 314 filter, selection); |
350 }, this); | 315 }, this); |
351 return firstT; | 316 return firstT; |
352 }, | 317 }, |
353 | 318 |
354 onMouseMove_(e) { | 319 onMouseMove_(e) { |
355 // Zooming requires the delta since the last mousemove so we need to avoid | 320 // Zooming requires the delta since the last mousemove so we need to avoid |
356 // tracking it when the zoom interaction is active. | 321 // tracking it when the zoom interaction is active. |
357 if (this.isZooming_) return; | 322 if (!this.isZooming_) this.storeLastMousePos_(e); |
358 | 323 |
359 this.storeLastMousePos_(e); | 324 switch (this.mouseMode) { |
| 325 case tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION: |
| 326 this.onUpdateSelection_(e); |
| 327 break; |
| 328 case tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN: |
| 329 this.onUpdatePanScan_(e); |
| 330 break; |
| 331 case tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM: |
| 332 this.onUpdateZoom_(e); |
| 333 break; |
| 334 case tr.ui.b.MOUSE_SELECTOR_MODE.TIMING: |
| 335 this.timingTool_.onUpdateTiming(e); |
| 336 break; |
| 337 } |
360 }, | 338 }, |
361 | 339 |
362 onTouchStart_(e) { | 340 onTouchStart_(e) { |
363 this.storeLastTouchPositions_(e); | 341 this.storeLastTouchPositions_(e); |
364 this.focusElements_(); | 342 this.focusElements_(); |
365 }, | 343 }, |
366 | 344 |
367 onTouchMove_(e) { | 345 onTouchMove_(e) { |
368 e.preventDefault(); | 346 e.preventDefault(); |
369 this.onUpdateTransformForTouch_(e); | 347 this.onUpdateTransformForTouch_(e); |
(...skipping 180 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
550 } else { | 528 } else { |
551 this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0); | 529 this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0); |
552 } | 530 } |
553 e.preventDefault(); | 531 e.preventDefault(); |
554 e.stopPropagation(); | 532 e.stopPropagation(); |
555 } | 533 } |
556 }); | 534 }); |
557 }, | 535 }, |
558 | 536 |
559 onDblClick_(e) { | 537 onDblClick_(e) { |
560 if (this.mouseModeSelector_.mode !== | 538 if (this.mouseMode !== tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) { |
561 tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) { | |
562 return; | 539 return; |
563 } | 540 } |
564 | 541 |
565 const curSelection = this.brushingStateController_.selection; | 542 const curSelection = this.brushingStateController_.selection; |
566 if (!curSelection.length || !tr.b.getOnlyElement(curSelection).title) { | 543 if (!curSelection.length || !tr.b.getOnlyElement(curSelection).title) { |
567 return; | 544 return; |
568 } | 545 } |
569 | 546 |
570 const selection = new tr.model.EventSet(); | 547 const selection = new tr.model.EventSet(); |
571 const filter = new tr.c.ExactTitleFilter( | 548 const filter = new tr.c.ExactTitleFilter( |
572 tr.b.getOnlyElement(curSelection).title); | 549 tr.b.getOnlyElement(curSelection).title); |
573 this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, | 550 this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, |
574 selection); | 551 selection); |
575 | 552 |
576 this.brushingStateController.changeSelectionFromTimeline(selection); | 553 this.brushingStateController.changeSelectionFromTimeline(selection); |
577 }, | 554 }, |
578 | 555 |
579 onMouseWheel_(e) { | 556 onMouseWheel_(e) { |
580 if (!e.altKey) return; | 557 if (!e.altKey) return; |
581 | 558 |
582 const delta = e.wheelDelta / 120; | 559 const delta = e.wheelDelta / 120; |
583 const zoomScale = Math.pow(1.5, delta); | 560 const zoomScale = Math.pow(1.5, delta); |
584 this.zoomBy_(zoomScale); | 561 this.zoomBy_(zoomScale); |
585 e.preventDefault(); | 562 e.preventDefault(); |
586 }, | 563 }, |
587 | 564 |
588 onMouseDown_(e) { | 565 onMouseDown_(e) { |
589 if (this.mouseModeSelector_.mode !== | 566 switch (this.mouseMode) { |
590 tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) { | 567 case tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION: |
591 return; | 568 this.onBeginSelection_(e); |
| 569 break; |
| 570 case tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN: |
| 571 this.onBeginPanScan_(e); |
| 572 break; |
| 573 case tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM: |
| 574 this.onBeginZoom_(e); |
| 575 break; |
| 576 case tr.ui.b.MOUSE_SELECTOR_MODE.TIMING: |
| 577 this.timingTool_.onBeginTiming(e); |
| 578 break; |
592 } | 579 } |
| 580 tr.ui.b.trackMouseMovesUntilMouseUp(this.onMouseMove_, this.onMouseUp_); |
| 581 }, |
593 | 582 |
594 // Mouse down must start on ruler track for crosshair guide lines to draw. | 583 onMouseUp_(e) { |
595 if (e.target !== this.rulerTrack_) return; | 584 if (e.type === 'mousedown') return; |
596 | 585 switch (this.mouseMode) { |
597 // Make sure we don't start a selection drag event here. | 586 case tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION: |
598 this.dragBeginEvent_ = undefined; | 587 this.onEndSelection_(e); |
599 | 588 break; |
600 // Remove nav string marker if it exists, since we're clearing the | 589 case tr.ui.b.MOUSE_SELECTOR_MODE.PANSCAN: |
601 // find control box. | 590 this.onEndPanScan_(e); |
602 if (this.xNavStringMarker_) { | 591 break; |
603 this.model.removeAnnotation(this.xNavStringMarker_); | 592 case tr.ui.b.MOUSE_SELECTOR_MODE.ZOOM: |
604 this.xNavStringMarker_ = undefined; | 593 this.onEndZoom_(e); |
| 594 break; |
| 595 case tr.ui.b.MOUSE_SELECTOR_MODE.TIMING: |
| 596 this.timingTool_.onEndTiming(e); |
| 597 break; |
605 } | 598 } |
606 | |
607 const dt = this.viewport_.currentDisplayTransform; | |
608 tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler. | |
609 // If mouse event is on ruler, don't do anything. | |
610 if (e.target === this.rulerTrack_) return; | |
611 | |
612 const relativePosition = this.extractRelativeMousePosition_(e); | |
613 const loc = tr.model.Location.fromViewCoordinates( | |
614 this.viewport_, relativePosition.x, relativePosition.y); | |
615 // Not all points on the timeline represents a valid location. | |
616 // ex. process header tracks, letter dot tracks. | |
617 if (!loc) return; | |
618 | |
619 if (this.guideLineAnnotation_ === undefined) { | |
620 this.guideLineAnnotation_ = | |
621 new tr.model.XMarkerAnnotation(loc.xWorld); | |
622 this.model.addAnnotation(this.guideLineAnnotation_); | |
623 } else { | |
624 this.guideLineAnnotation_.timestamp = loc.xWorld; | |
625 this.modelTrackContainer_.invalidate(); | |
626 } | |
627 | |
628 // Set the findcontrol's text to nav string of current state. | |
629 const state = new tr.ui.b.UIState(loc, | |
630 this.viewport_.currentDisplayTransform.scaleX); | |
631 this.timelineView_.setFindCtlText( | |
632 state.toUserFriendlyString(this.viewport_)); | |
633 }.bind(this), | |
634 undefined, // Mouse up handler. | |
635 function onKeyUpDuringDrag() { | |
636 if (this.dragBeginEvent_) { | |
637 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, | |
638 this.dragBoxXEnd_, this.dragBoxYEnd_); | |
639 } | |
640 }.bind(this)); | |
641 }, | 599 }, |
642 | 600 |
643 queueSmoothPan_(viewDeltaX, deltaY) { | 601 queueSmoothPan_(viewDeltaX, deltaY) { |
644 const deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( | 602 const deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( |
645 viewDeltaX); | 603 viewDeltaX); |
646 const animation = new tr.ui.TimelineDisplayTransformPanAnimation( | 604 const animation = new tr.ui.TimelineDisplayTransformPanAnimation( |
647 deltaX, deltaY); | 605 deltaX, deltaY); |
648 this.viewport_.queueDisplayTransformAnimation(animation); | 606 this.viewport_.queueDisplayTransformAnimation(animation); |
649 }, | 607 }, |
650 | 608 |
(...skipping 512 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1163 Polymer.dom(this.$.hint_text).textContent = text; | 1121 Polymer.dom(this.$.hint_text).textContent = text; |
1164 this.$.hint_text.style.display = ''; | 1122 this.$.hint_text.style.display = ''; |
1165 }, | 1123 }, |
1166 | 1124 |
1167 hideHintText_() { | 1125 hideHintText_() { |
1168 this.pendingHintTextClearTimeout_ = undefined; | 1126 this.pendingHintTextClearTimeout_ = undefined; |
1169 this.$.hint_text.style.display = 'none'; | 1127 this.$.hint_text.style.display = 'none'; |
1170 } | 1128 } |
1171 }); | 1129 }); |
1172 </script> | 1130 </script> |
OLD | NEW |