blob: d4f5a8035d2e3528ab643648d7770d5df2789148 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const MDCMenu = mdc.menu.MDCMenu;
const MDCMenuFoundation = mdc.menu.MDCMenuFoundation;
const maxZIndex = 999;
class FilterUI extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.innerHTML = `
<style>
.row {
display: flex;
justify-content: space-between;
padding-bottom: 10px;
}
#filter-container {
max-width: 400px;
min-width: 350px;
font-family: Roboto;
font-size: 10pt;
background-color: white;
padding: 10px;
}
#saveFilter {
margin: 20px 10px;
margin-right: 0px;
}
</style>
<div id='filter-container'>
<div class='sectionTitle' id='filter-ui-title'>Add Filter</div>
<div class='section'>
<div class='row'>
<div class='label' title='Filter annotation to match.
For example frame.root.damage' >Annotation </div>
<div class='input'>
<input list="knownAnnotations" placeholder='Substring to match'
id='annotation' size=40>
<!-- TODO: A fancy drop-down here would be nice. -->
</div>
</div>
<div class='row'>
<div class='label'>File name</div>
<div class='input'>
<input placeholder='Substring to match (usually empty!)'
id='filename' size=40>
<!-- TODO: A fancy drop-down here would be nice. -->
</div>
</div>
<div class='row'>
<div class='label'>Function</div>
<div class='input'>
<input placeholder='Substring to match (usually empty!)'
id='functionname' size=40>
<!-- TODO: A fancy drop-down here would be nice. -->
</div>
</div>
</div>
<div style='display: flex'>
<div style='flex-grow: 1'>
<div class='sectionTitle'>Action</div>
<div class='section'>
<div>
<form id="actionform">
<input type='radio' id='drawfilter' name='dowhat' value='drawfilter'/>Override
<input type='color' id='drawcolor' name='drawcolor' value='#000000'/>
opacity <input type='range' name='fillalpha' min='0' max='100'
step='10' value='50' list='alphastep'/>
<datalist id='alphastep'>
<option>0</option><option>10</option><option>20</option>
<option>30</option><option>40</option><option>50</option>
<option>60</option> <option>70</option><option>80</option>
<option>90</option><option>100</option>
</datalist>
<input checked type='radio' name='dowhat' value='drawcaller'/>
Draw with caller color and opacity
<br/>
<input type='radio' name='dowhat' value='skip'
title='Used as a discard filter'/>Do not draw
<br/>
</form>
</div>
</div>
</div>
<button id='saveFilter'>Save</button>
</div>
<div>
`;
this.setUpButtons_();
// Event listener to check if custom color selected
// to automatically select override button
document.getElementById('drawcolor').addEventListener("change",function(){
document.getElementById('drawfilter').checked = true;
});
}
setUpButtons_() {
const SaveFilter = () => {
let input = this.querySelector('#filename');
const filename = input.value || undefined;
input = this.querySelector('#functionname');
const func = input.value || undefined;
input = this.querySelector('#annotation');
const anno = input.value || undefined;
input = this.querySelector('input[name="dowhat"]:checked');
const action = { skipDraw: input.value === 'skip' };
if (input.value === 'drawfilter') {
action.color = this.querySelector('input[name="drawcolor"]').value;
action.alpha = this.querySelector('input[name="fillalpha"]').value;
}
this.dispatchEvent(new CustomEvent('saveFilter', {
detail: { selector: { filename, func, anno }, action }
}));
};
this.querySelector('#saveFilter').addEventListener('click', SaveFilter);
this.querySelector('#filter-container').addEventListener('keypress',
(e) => { if (e.key === 'Enter') SaveFilter(); });
}
};
window.customElements.define('filter-ui', FilterUI);
// A <datalist> element containing values of previously seen filter annotations.
let dataListElement = undefined;
let knownAnnotations = new Set();
// Called when processing new sources from a frame.
function notifyUiOfNewSource(source) {
if (dataListElement === undefined) {
dataListElement = document.createElement("datalist");
dataListElement.id = "knownAnnotations";
document.body.appendChild(dataListElement);
}
if (!knownAnnotations.has(source.anno)) {
let option = document.createElement("option");
option.value = source.anno;
dataListElement.appendChild(option);
knownAnnotations.add(source.anno);
}
}
function createFilterChip(filter) {
const chip = document.createElement('div');
chip.className = "mdc-chip";
chip.setAttribute("role", "row");
chip.style.margin = '5px';
chip.style.borderRadius = '0px';
chip.style.zIndex = maxZIndex;
if (filter.shouldDraw) {
chip.style.border = filter.drawColor ?
`2px solid ${filter.drawColor}` : `1px solid black`;
if (filter.fillAlpha > 0) {
var alpha = Math.min(.6, parseFloat(filter.fillAlpha) / 100);
var rgba = filter.drawColor + DrawCall.alphaFloatToHex(alpha);
chip.style.backgroundColor = rgba;
chip.style.fontWeight = 'bold';
}
else {
chip.style.backgroundColor = `white`;
}
}
else {
chip.style.border = `1px dashed grey`;
chip.style.backgroundColor = `white`;
}
chip.innerHTML =
`
<input checked type="checkbox"/>
<span role="gridcell">
<span class="mdc-chip__text" id="filterstring"></span>
</span>
<span role="gridcell">
<div class="mdc-menu-surface--anchor">
<i class="material-icons-outlined mdc-chip__icon mdc-chip__icon--trailing"
tabindex="-1" role="button" id="filtermenu">more_vert</i>
<div id="filterchipmenu" class="mdc-menu mdc-menu-surface">
<ul class="mdc-list" role="menu" aria-hidden="true"
aria-orientation="vertical" tabindex="-1">
<li class="mdc-list-item" role="menuitem"
id="filter-moveprev" onclick="movePrev(this)">
<span class="mdc-list-item__ripple"></span>
<span class="mdc-list-item__text">
<i class="material-icons-outlined">arrow_back_ios</i>Move Previous
</span>
</li>
<li class="mdc-list-item" role="menuitem"
id="filter-movenext" onclick="moveNext(this)">
<span class="mdc-list-item__ripple"></span>
<span class="mdc-list-item__text">
<i class="material-icons-outlined">arrow_forward_ios</i> Move Next
</span>
</li>
<li class="mdc-list-item"
role="menuitem" id="filter-edit" onclick="showEditFilterPopup(this)">
<span class="mdc-list-item__ripple"></span>
<span class="mdc-list-item__text">
<i class="material-icons-outlined">edit</i> Edit</span>
</li>
<li class="mdc-list-item" role="menuitem"
id="filter-delete" onclick="deleteFilter(this)">
<span class="mdc-list-item__ripple"></span>
<span class="mdc-list-item__text">
<i class="material-icons-outlined">delete</i> Delete</span>
</li>
</ul>
</div>
</div>
</span>
`;
chip.querySelector('#filterstring').innerHTML = filter.createUIString();
chip.querySelector('#filtermenu').addEventListener('click', () => {
const menu = new MDCMenu(chip.querySelector('#filterchipmenu'));
menu.setAnchorMargin({ top: 25 });
menu.open = true;
});
const check = chip.querySelector('input');
check.checked = filter.enabled;
check.addEventListener('change', () => {
filter.enabled = !!check.checked;
locallyStoreFilters();
Player.instance.refresh();
Filter.sendStreamFilters();
});
return chip;
}
function showCreateFilterPopup(anchor) {
const filterUi = document.createElement('filter-ui');
filterUi.addEventListener('saveFilter', (event) => {
if (event.detail.selector && event.detail.action) {
const filter =
new Filter(true, event.detail.selector, event.detail.action);
const chip = createFilterChip(filter);
const list = document.querySelector('#filters');
list.appendChild(chip);
hideModal();
refreshFilterSet();
}
});
filterUi.style.position = 'absolute';
filterUi.style.top = (anchor.offsetTop + anchor.offsetHeight) + 'px';
filterUi.style.left = (anchor.offsetLeft + 20) + 'px';
filterUi.style.zIndex = maxZIndex;
showModal(filterUi, '#annotation');
}
// Traverses through all the filter chips and enables/disables
// "move next/prev" button clickability depending on if
// the chip is at the beginning/middle/end of list
function refreshFilterSet() {
const list = document.querySelector('#filters');
for (var chip = list.firstElementChild;
chip !== null; chip = chip.nextSibling) {
if (chip === list.firstElementChild) {
chip.querySelector('#filter-moveprev')
.classList.add("mdc-list-item--disabled");
}
else {
chip.querySelector('#filter-moveprev')
.classList.remove("mdc-list-item--disabled");
}
if (chip.nextSibling === null) {
chip.querySelector('#filter-movenext')
.classList.add("mdc-list-item--disabled");
}
else {
chip.querySelector('#filter-movenext')
.classList.remove("mdc-list-item--disabled");
}
}
locallyStoreFilters();
Player.instance.refresh();
Filter.sendStreamFilters();
}
function movePrev(item) {
if (item.classList.contains("mdc-list-item--disabled")) {
return;
}
var chip = item.closest(".mdc-chip");
const menu = new MDCMenu(chip.querySelector('#filterchipmenu'));
menu.open = false;
var index = Array.prototype.indexOf.call(chip.parentNode.children, chip);
Filter.swapFilters(index - 1, index);
var prevChip = chip.previousElementSibling;
chip.parentNode.insertBefore(chip, prevChip);
refreshFilterSet();
}
function moveNext(item) {
if (item.classList.contains("mdc-list-item--disabled")) {
return;
}
var chip = item.closest(".mdc-chip");
const menu = new MDCMenu(chip.querySelector('#filterchipmenu'));
menu.open = false;
var index = Array.prototype.indexOf.call(chip.parentNode.children, chip);
Filter.swapFilters(index, index + 1);
var nextChip = chip.nextElementSibling;
chip.parentNode.insertBefore(nextChip, chip);
refreshFilterSet();
}
function createFilterComplete(enabled, selector, action, index) {
var newFilter =
new Filter(enabled, selector, action, index);
const newChip = createFilterChip(newFilter);
const list = document.querySelector('#filters');
list.appendChild(newChip);
hideModal();
refreshFilterSet();
return newChip;
}
function showEditFilterPopup(item) {
var chip = item.closest(".mdc-chip");
var index = Array.prototype.indexOf.call(chip.parentNode.children, chip);
var filter = Filter.getFilter(index);
const menu = new MDCMenu(chip.querySelector('#filterchipmenu'));
menu.open = false;
const filterUi = document.createElement('filter-ui');
const isEnabled = filter.enabled;
filterUi.addEventListener('saveFilter', (event) => {
if (event.detail.selector && event.detail.action) {
var newChip = createFilterComplete(isEnabled, event.detail.selector,
event.detail.action, index);
chip.replaceWith(newChip);
}
});
filterUi.style.position = 'absolute';
filterUi.style.top = (chip.offsetTop + chip.offsetHeight) + 'px';
filterUi.style.left = (chip.offsetLeft + 20) + 'px';
filterUi.style.zIndex = maxZIndex;
showModal(filterUi, '#annotation');
// fill form from filter data
filterUi.querySelector('#filter-ui-title').innerHTML = "Edit Filter";
filterUi.querySelector('#filename').value = filter.file;
filterUi.querySelector('#functionname').value = filter.func;
filterUi.querySelector('#annotation').value = filter.anno;
var actionform = filterUi.querySelector('#actionform');
actionform.dowhat.value = !filter.shouldDraw ? 'skip' :
filter.drawColor ? 'drawfilter' : 'drawcaller';
actionform.drawcolor.value = filter.drawColor;
actionform.fillalpha.value = filter.fillAlpha;
}
function deleteFilter(item) {
var chip = item.closest(".mdc-chip");
const menu = new MDCMenu(chip.querySelector('#filterchipmenu'));
menu.open = false;
var index = Array.prototype.indexOf.call(chip.parentNode.children, chip);
Filter.deleteFilter(index);
chip.parentNode.removeChild(chip);
refreshFilterSet();
}
// Stores filter instances in local storage
function locallyStoreFilters() {
var filterInstances = Filter.instances;
// Store string representation of filterInstances list in local storage.
localStorage.setItem('filterInstances', JSON.stringify(filterInstances));
}
// Restores filter instances from local storage.
function restoreFilters() {
const retrievedFilterString = localStorage.getItem('filterInstances');
console.log(" Filter string=" + retrievedFilterString);
// Add default filters to the instances list.
FilterUIDefault.initialize();
if (!retrievedFilterString) {
return;
}
var retrievedFilterInstances = JSON.parse(retrievedFilterString);
// Remove any default filter duplicates from restored filters.
for (const defaultFilter of defaultFilters) {
retrievedFilterInstances = retrievedFilterInstances.filter
(instance => !isDuplicate(instance, defaultFilter));
}
// Re-create non-default filter chips from local storage.
// Pre-existing filters are appended behind the default ones.
retrievedFilterInstances.forEach((instance) =>
createFilterComplete(instance.enabled_,
instance.selector_, instance.action_));
}
// Checks if one filter is a duplicate of another.
// NOTE: Custom equality check needed to avoid marking same style
// of filters with different user states and indices as non-duplicates.
function isDuplicate(filter1, filter2) {
if (!filter1 || !filter2) {
return false;
}
if (filter1.selector_.filename !== filter2.selector_.filename) {
return false;
}
if (filter1.selector_.func !== filter2.selector_.func) {
return false;
}
if (filter1.selector_.anno !== filter2.selector_.anno) {
return false;
}
return true;
}
const defaultFilters = [
{
selector_: { filename: "", func: "", anno: "frame.render_pass.meta" },
action_: { skipDraw: false },
enabled_: false
},
{
selector_: { filename: "", func: "", anno: "frame.render_pass.quad" },
action_: { skipDraw: false, color: '#000000', alpha: "10" },
enabled_: true
},
{
selector_: { filename: "", func: "", anno: "frame.render_pass.damage" },
action_: { skipDraw: false, color: '#FF0000', alpha: "20" },
enabled_: true
},
{
selector_: {
filename: "",
func: "",
anno: "frame.render_pass.output_rect",
},
action_: { skipDraw: false },
enabled_: false,
},
{
selector_: { filename: "", func: "", anno: "overlay.selected.rect" },
action_: { skipDraw: false, color: '#22FF22', alpha: "20" },
enabled_: true
},
{
selector_: { filename: "", func: "", anno: "overlay.outgoing.damage" },
action_: { skipDraw: false, color: '#AA00AA', alpha: "20" },
enabled_: true
},
{
selector_: { filename: "", func: "", anno: "frame.render_pass.material" },
action_: { skipDraw: false },
enabled_: false
}
];
// Default filters should probably load off disk.
const FilterUIDefault = {
initialize() {
defaultFilters.forEach((instance) =>
createFilterComplete(instance.enabled_,
instance.selector_, instance.action_))
}
};