View Issue Details

IDProjectCategoryView StatusLast Update
0017311MMW 5Tracklistpublic2021-01-15 00:31
Reporterdrakinite Assigned To 
PriorityurgentSeverityminorReproducibilitysometimes
Status closedResolutionreopened 
Product Version5.0 
Target Version5.0Fixed in Version5.0 
Summary0017311: Tracklist sometimes goes invisible when the headers extend past the horizontal width of the listview
DescriptionIt'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 Reproduce1. 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.
TagsNo tags attached.
Fixed in build2294

Relationships

related to 0017118 closedpetr Performance issue - Extreme layout thrashing when tooltips fade in and out 

Activities

Ludek

2020-12-30 18:01

developer   ~0061125

Last edited: 2020-12-30 18:05

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 ?

drakinite

2020-12-30 21:25

developer   ~0061130

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.

Ludek

2021-01-04 20:29

developer   ~0061152

Last edited: 2021-01-04 20:47

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).

Ludek

2021-01-05 15:35

developer   ~0061160

Last edited: 2021-01-05 15:46

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;
    }
}, {

});
scroller.js (14,584 bytes)   

drakinite

2021-01-06 18:51

developer   ~0061179

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.

Ludek

2021-01-07 22:34

developer   ~0061198

Fixed in 2293

Thanks to Jordan for cooperation and great catch that it was related to tooltips!

Ludek

2021-01-07 22:51

developer   ~0061199

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

Ludek

2021-01-08 00:01

developer   ~0061204

Fixed

drakinite

2021-01-08 00:46

developer   ~0061212

Last edited: 2021-01-08 00:52

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.

Ludek

2021-01-08 11:30

developer   ~0061217

Last edited: 2021-01-08 11:43

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.

petr

2021-01-08 12:51

developer   ~0061220

Fixed

drakinite

2021-01-08 21:10

developer   ~0061249

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).

peke

2021-01-15 00:31

developer   ~0061350

Verified 2295

Looks OK now.