View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0017311 | MMW 5 | Tracklist | public | 2020-12-29 20:22 | 2021-01-15 00:31 |
Reporter | drakinite | Assigned To | |||
Priority | urgent | Severity | minor | Reproducibility | sometimes |
Status | closed | Resolution | reopened | ||
Product Version | 5.0 | ||||
Target Version | 5.0 | Fixed in Version | 5.0 | ||
Summary | 0017311: Tracklist sometimes goes invisible when the headers extend past the horizontal width of the listview | ||||
Description | It's difficult to reproduce reliably, but it happens more often the wider the listview headers are. The vertical positioning of each lvItem appears incorrect when it occurs. | ||||
Steps To Reproduce | 1. Add many fields to the tracklist header so that it's significantly wider than the listview itself 2. On a screen with many tracks, scroll up/down and left/right The items sometimes disappear entirely, but sometimes a small portion of the listview is visible. It can sometimes be alleviated by pressing F5. Both Drakinite and user video is uploaded to FTP. | ||||
Tags | No tags attached. | ||||
Fixed in build | 2294 | ||||
|
I think that Drakinite's issue is different than the user's issue (according to the videos), but I cannot replicate either of these whatever I do :-( Assigned back to Draki for additional feedback: 1) I see that you are in Music node, List view , with 'Column Filter' enabled, is this important to replicate? Or what triggers the issue ? 2) If the issue is triggered anytime you access the view, could you please share your persistent.json , MM5.DB and scaling settings (i.e. the Make Text Bigger, Make everything bigger, resolution) ? I it related to the specific scaling or resolution settings? Or does this happen with any settings? 3) Could you edit /controls/listview.js and change the var fullLVDebug = false; line to var fullLVDebug = true; and generate new debug log ? |
|
1) It looks like the column filter is not necessary to replicate. It can happen with column filter disabled. 2) It's not triggered every time I access the view; I have to scroll the LV somewhat randomly in order for it to appear. Attached persistent.json. - Strangely, it appears that it actually IS caused by scaling. When my main display is set to 125%, it's replicable on both my main display AND my secondary display (which is always at 100%). But when my main display is set to 100%, it's not replicable on either display. 3) Attached a debug log from app startup all the way to the issue starting to appear. |
|
2) I tried, but so far unsuccessful to replicate the issue (even with "Make everything bigger" set to 125%) Maybe a longer screencast would help so that I can see exactly what you are doing before the bug appears. 3) The persistent.json and the debug log are not attached -- so please share (and hopefully the log will show a clue). |
|
2) How exactly you are scrolling for the bug to appear? Using mouse wheel or scrollbars? Horizontal or vertical? Could you catch this in a screencast ? Could you please do two additional tests: 4) Try whether the issue is replicable with smooth scrolling disabled ? 5) Try whether the issue is replicable with this version of scroller.js scroller.js (14,584 bytes)
/** @module UI controls */ requirejs('controls/control'); /** Scroller - a generic scrolling control, that's mainly supposed to host 1 or more listviews, that are virtualized (i.e. can handle even millions of items). @class Scroller @constructor @extends Control */ inheritClass('Scroller', Control, { initialize: function () { Scroller.$super.initialize.apply(this, arguments); this.enableDragNDrop(); this.contextMenu = []; // LS: for the context menu forwarding below to work (#15884) this.reloadSettings(); // set smooth/animated scroll this.smoothScrollTimeLimit = 1000 * animTools.animationTime * 1.02 /* a reserve in order to finish scrolling after e.g. pop-up animation starts */ ; // ms this.scrolled = false; this.lassoParentElementScroller = true; this.container.classList.add('scrollable'); this.registerEventHandler('wheel'); this.registerEventHandler('keydown'); this.registerEventHandler('mousedown'); this.registerEventHandler('mouseup'); this.localListen(app, 'settingschange', function () { this.reloadSettings(); }.bind(this)); this.localListen(document.body, 'keydown', (e) => { var key = friendlyKeyName(e); if (key == 'Space') { // ignore scrolling on space (#15917) this._ignoreScroll = true; this.requestTimeout(() => { this._ignoreScroll = false; }, 500); var focusedElement = document.activeElement; if (focusedElement && ((focusedElement.nodeName == 'INPUT') || (focusedElement.hasAttribute('contenteditable')))) { // editing - we cannot prevent default for spacebar (#16185) } else { // not editing - prevent default to ignore scrolling on space (#15917) e.preventDefault(); } } }); this.localListen(this.container, 'scroll', function (evt) { if (evt.target == this.container && !this._ignoreScroll) { this.scrolled = true; this.notifyChildren(true /* to make deferred draw, otherwise freeze can happen - #16794 */ ); } }.bind(this)); }, reloadSettings: function () { var sett = settings.get('Appearance'); this.smoothScroll = sett.Appearance.SmoothScroll; }, handle_mouseup: function (e) { if (this._nearestLV && this._nearestLV.controlClass && this._nearestLV.controlClass.lassoSelectionEnabled) { this._nearestLV.controlClass._cleanUpLasso(); } this._nearestLV = null; }, handle_mousedown: function (e) { this._nearestLV = this.getFirstUp(e.offsetY, this.container.offsetHeight, true /* allow empty LVs */ ); if (this._nearestLV && this._nearestLV.controlClass && this._nearestLV.controlClass.lassoSelectionEnabled) { this._nearestLV.controlClass.handleLassoStart(null, e); } }, handle_wheel: function (e) { if (e.deltaY !== 0 && !e.shiftKey /* #16406 - item 4 */ ) { if (e.stopPropagation) e.stopPropagation(); e.preventDefault(); var delta = e.deltaY; if (e.deltaMode === DOM_DELTA_LINE) { delta *= fontLineSizePx(); } else if (e.deltaMode === DOM_DELTA_PAGE) { delta *= this.container.clientHeight; } this.setSmoothScrollOffset(this.getScrollOffset() + delta); } }, cleanUp: function () { Scroller.$super.cleanUp.apply(this, arguments); }, getScrollOffset: function () { if (!this.scrolled) return 0; if (this.smoothScrollTarget >= 0) return this.smoothScrollTarget; else return this.container.scrollTop; }, getSmoothScrollOffset: function () { var scrollTop = this.getScrollOffset(); if (this.smoothScrollOrigin >= 0) { var newTime = window.performance.now(); if (newTime - this.smoothScrollTime >= this.smoothScrollTimeLimit) { var res = this.smoothScrollTarget; this.setScrollOffset(res); this.smoothScrollOrigin = -1; this.smoothScrollTarget = -1; return res; } else return this.smoothScrollOrigin + Math.round((scrollTop - this.smoothScrollOrigin) * Math.pow((newTime - this.smoothScrollTime) / this.smoothScrollTimeLimit, 0.6)); } else return scrollTop; }, notifyChildren: function (deferred) { var ctrls = qes(this.container, '[data-control-class]'); for (var i = 0; i < ctrls.length; i++) { var el = ctrls[i]; if (el.controlClass && el.controlClass.parentScrollFrame) el.controlClass.parentScrollFrame(deferred); } }, handleSmoothScroll: function () { var scrollFn = function () { this.container.scrollTop = this.getSmoothScrollOffset(); if (this.smoothScrollOrigin >= 0) this.requestFrame(scrollFn.bind(this), 'scrollFn'); else this.frameQueued = false; this.notifyChildren(); // to prevent incorrect listview header movement during scrolling } if (!this.frameQueued) { this.frameQueued = true; this.requestFrame(scrollFn.bind(this), 'scrollFn'); } }, setSmoothScrollOffset: function (newValue, canScrollBeyond /*To allow scrolling lower than is the current viewport height*/ ) { if (isNaN(newValue)) return; newvalue = Math.max(canScrollBeyond ? newValue : Math.min(newValue, this.container.scrollHeight - this.container.clientHeight), 0); if (this.smoothScroll) { this.scrolled = true; this.smoothScrollOrigin = this.getSmoothScrollOffset(); this.smoothScrollTime = window.performance.now(); this.smoothScrollTarget = newvalue; this.handleSmoothScroll(); } else { this.setScrollOffset(newValue); } }, setScrollOffset: function (newValue) { if (isNaN(newValue)) return; this.container.scrollTop = newValue; this.scrolled = true; }, focusFirstDown: function (fromPoint, maxLen, allowEmpty) { var ctrls = qes(this.container, '[data-control-class]'); var bestctrl = undefined; var bestmatch = Number.MAX_SAFE_INTEGER; forEach(ctrls, function (ctrl) { if (!ctrl.controlClass || !ctrl.controlClass.visible || !(ctrl.tabIndex >= 0) || (ctrl.controlClass.itemCount == 0 && !allowEmpty) /*empty listview*/ ) return; if (fromPoint <= ctrl.offsetTop && ctrl.offsetTop < bestmatch) { bestmatch = ctrl.offsetTop; bestctrl = ctrl; } }); if (bestctrl && bestctrl.offsetTop < fromPoint + maxLen) { // De-focus the old element var el = document.activeElement; if (el) { var ctrl = elementControl(el); if (ctrl && ctrl.setFocusedAndSelectedIndex) ctrl.setFocusedAndSelectedIndex(-1); } // Focus the newly found control bestctrl.focus({ preventScroll: true }); if (bestctrl.controlClass.setFocusedAndSelectedIndex) bestctrl.controlClass.setFocusedAndSelectedIndex(0); return true; } return false; }, getFirstUp: function (fromPoint, maxLen, allowEmpty) { var ctrls = qes(this.container, '[data-control-class]'); var bestctrl = undefined; var bestmatch = -1; forEach(ctrls, function (ctrl) { if (!ctrl.controlClass || !ctrl.controlClass.visible || !(ctrl.tabIndex >= 0) || (ctrl.controlClass.itemCount == 0 && !allowEmpty) /*empty listview*/ ) return; var bottom = ctrl.offsetTop + ctrl.offsetHeight; if (bottom <= fromPoint && bottom > bestmatch) { bestmatch = bottom; bestctrl = ctrl; } }); if (bestctrl && bestctrl.offsetTop + bestctrl.offsetHeight >= fromPoint - maxLen) { return bestctrl; } return null; }, focusFirstUp: function (fromPoint, maxLen, allowEmpty) { var bestctrl = this.getFirstUp(fromPoint, maxLen, allowEmpty); if (bestctrl) { // De-focus the old element var el = document.activeElement; if (el) { var ctrl = elementControl(el); if (ctrl && ctrl.setFocusedAndSelectedIndex) ctrl.setFocusedAndSelectedIndex(-1); } // Focus the newly found control bestctrl.focus({ preventScroll: true }); if (bestctrl.controlClass.setFocusedAndSelectedIndex) bestctrl.controlClass.setFocusedAndSelectedIndex(bestctrl.controlClass.itemCount - 1); return true; } return false; }, handle_keydown: function (e) { var handled = false; switch (friendlyKeyName(e)) { case 'Home': this.focusFirstDown(0, this.container.offsetHeight); this.setSmoothScrollOffset(0); handled = true; break; case 'End': this.focusFirstUp(this.container.scrollHeight, this.container.offsetHeight); this.setSmoothScrollOffset(this.container.scrollHeight); handled = true; break; case 'Up': var el = document.activeElement; if (!el) break; var bottom = el.offsetTop + el.offsetHeight; handled = this.focusFirstUp(el.offsetTop, el.offsetTop - this.container.scrollTop); break; case 'Down': var el = document.activeElement; if (!el) break; var bottom = el.offsetTop + el.offsetHeight; handled = this.focusFirstDown(bottom, this.container.scrollTop + this.container.offsetHeight - bottom); break; } if (handled) { e.stopPropagation(); e.preventDefault(); } }, handle_layoutchange: function (evt) { if (this._initialScrollTop !== undefined) { if (this.container.scrollTop > 0) { // was already manually scrolled, do not change this._initialScrollTop = undefined; } else if (this._initialScrollTop <= (this.container.scrollHeight - this.container.clientHeight)) { // we already have sufficient height of the main control, scroll to initial position this.setScrollOffset(this._initialScrollTop); this._initialScrollTop = undefined; this.notifyChildren(); } if (this._initialScrollTop === undefined) { // unregister layoutchange event, no longer needed this.unregisterEventHandler('layoutchange'); } } Scroller.$super.handle_layoutchange.call(this, evt); }, _forward: function (e, action, forContextMenu) { var _this = this; var getParentClass = function (ctrl) { while (ctrl.parentNode) { ctrl = ctrl.parentNode; if (ctrl.controlClass && ctrl.parentNode == _this.container) { return ctrl; } } } var ctrl = getParentClass(e.target); if (!ctrl) { // it is drop to the "empty" area of this scroller // find the last usable control for the drop var ctrl = this.container.firstChild; var lastCtrl; while (ctrl) { if (ctrl.controlClass && ctrl.controlClass.dndEventsRegistered) lastCtrl = ctrl; ctrl = ctrl.nextSibling; } ctrl = lastCtrl; } if (ctrl) { var cls = ctrl.controlClass; if (cls.dndEventsRegistered && cls[action]) { if (forContextMenu) { if (cls.cancelSelection) cls.cancelSelection(); ctrl.focus(); } e._target = ctrl; dnd.setDropTargetControl(e, ctrl); // define correct drop target (so isSameControl can be enumerated correctly) return cls[action].call(cls, e); } } }, getDropMode: function (e) { return this._forward(e, 'getDropMode'); }, canDrop: function (e) { return this._forward(e, 'canDrop'); }, drop: function (e) { this._forward(e, 'drop'); }, getDraggedObject: function (e) { return this._forward(e, 'getDraggedObject'); }, contextMenuHandler: function (e) { e.stopPropagation(); this._forward(e, 'contextMenuHandler', true); }, storeState: function () { var state = Scroller.$super.storeState.call(this); state.scrollTop = this.container.scrollTop; return state; }, restoreState: function (state, isJustViewModeChange) { Scroller.$super.restoreState.apply(this, arguments); this._initialScrollTop = state.scrollTop || 0; this.container.scrollTop = 0; // reset to original null value, to be able to detect scroll change if (this._initialScrollTop > 0) { // register layoutchange event, so we can check, if we already can scroll to desired position this.registerEventHandler('layoutchange'); } else this._initialScrollTop = undefined; } }, { }); |
|
Figured out how to consistently reproduce it: https://www.youtube.com/watch?v=0rKnKPW80NY 2) Either vertical or horizontal scrolling causes it. When using the mousewheel, if a tooltip appears while it is smooth-scrolling, if the horizontal bar is not all the way to the left, that is when the issue appears. 4) The issue is replicable with the smooth scrolling setting disabled. If you do the same actions (forcing a tooltip to appear immediately after scrolling the mousewheel, even if it is not actively smooth-scrolling) 5) It is replicable with that version of scroller.js. |
|
Fixed in 2293 Thanks to Jordan for cooperation and great catch that it was related to tooltips! |
|
The fix also fixed 0017118 (and we could add the animation back). Just re-opened as Jordan indicated that the issue still appears when Toast Message is shown, so there will be a similar issue with toasts |
|
Fixed |
|
Found the real culprit. We were attempting to minimize layout changes, because layout changes in my screen configuration were causing the issue. But it's actually a screen-position calculation problem, as indicated in this recording: https://www.youtube.com/watch?v=NL5ugdwRkHI It needs a very particular monitor configuration to take effect. It only occurs when: - Main display has a scaling factor greater than secondary (e.g. 125% and 100%) - and the secondary display is positioned to the left of the main display Edit: In the recording, I didn't mention that it's also replicable on that secondary display. The same strange behavior occurs when the lvViewport's left position reaches past the leftmost bounds of the display. |
|
Great catch again, this makes perfect sense now (findScreenPos of LV.viewport on the left monitor versus findScreenPos of LV on the right monitor). And also the reason why I never ever replicated the issue as I simply use a single monitor ;-) We need a solution that would find the position of LV.viewport absolutely to the right monitor (on which the MM5 window is situated). Per IM discussion assigned to Petr (as he already prepared some stuff related to this in the past) and has two monitors to test EDIT: It seems that LV.viewport is already calculated absolutely to the right monitor (where MM5 window resides) otherwise it wouldn't work when the two monitors have both the 100% scaling. |
|
Fixed |
|
Had to make a tweak to fix it on my end. When "Make text bigger" is set to an odd value (e.g. 101%), window.devicePixelRatio becomes very slightly different from screenInfo.ratio, due to some imprecision e.g.: window.devicePixelRatio became 1.2625000476837 while screenInfo.ratio was 1.2625 exactly. Fixed by changing the comparison from an exact equals to using Math.round(x * 1000). |
|
Verified 2295 Looks OK now. |