From 09cee986f206325b194db72150d3a56145bf70ea Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 10 May 2024 13:46:05 +0100 Subject: [PATCH 1/5] Add a Table component, based on the TaskManager table For now, this is quite basic. The main feature is the sticky header. --- src/UI/Components/Table.js | 83 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 src/UI/Components/Table.js diff --git a/src/UI/Components/Table.js b/src/UI/Components/Table.js new file mode 100644 index 000000000..865aa7c4c --- /dev/null +++ b/src/UI/Components/Table.js @@ -0,0 +1,83 @@ +import { Component, defineComponent } from "../../util/Component.js"; + +/** + * A table with a sticky header + */ +export default class Table extends Component { + static PROPERTIES = { + headings: { value: [] }, + scale: { value: '2pt' }, + rows: { value: [] }, + } + + static CSS = /*css*/` + table { + box-sizing: border-box; + border-collapse: collapse; + width: 100%; + } + + thead th { + box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2); + backdrop-filter: blur(2px); + position: sticky; + z-index: 100; + padding: + calc(10 * var(--scale)) + calc(2.5 * var(--scale)) + calc(5 * var(--scale)) + calc(2.5 * var(--scale)); + top: 0; + background-color: hsla(0, 0%, 100%, 0.8); + text-align: left; + border-bottom: 1px solid #e0e0e0; + } + + thead th:not(:last-of-type) { + /* we set borders on this span because */ + /* borders fly away from sticky headers */ + border-right: 1px solid #e0e0e0; + } + + tbody > * { + border-bottom: 1px solid #e0e0e0; + padding: 0 calc(2.5 * var(--scale)); + vertical-align: middle; + } + `; + + create_template ({ template }) { + $(template).html(` + + + + + + + +
+ `); + } + + on_ready ({ listen }) { + listen('headings', headings => { + $(this.dom_).find('.headings') + .html(headings.map(heading => `${heading}`).join('')) + }); + + listen('scale', scale => { + $(this.dom_).css('--scale', scale); + }); + + listen('rows', rows => { + const tbody = $(this.dom_).find('tbody')[0]; + $(tbody).find('[slot=rows]').detach(); + for (const row of rows) { + row.setAttribute('slot', 'rows'); + row.attach(tbody); + } + }); + } +} + +defineComponent('c-table', Table); From e304f6fc3a07fdd248d800cd0cb762cfc36bdf34 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 10 May 2024 13:46:25 +0100 Subject: [PATCH 2/5] Add ability to look up a process by its uuid --- src/services/ProcessService.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/services/ProcessService.js b/src/services/ProcessService.js index 8047083c2..410217c42 100644 --- a/src/services/ProcessService.js +++ b/src/services/ProcessService.js @@ -19,6 +19,10 @@ export class ProcessService extends Service { return this.processes_map.get(NULL_UUID); } + get_by_uuid (uuid) { + return this.processes_map.get(uuid); + } + get_children_of (uuid) { if ( ! uuid ) { uuid = NULL_UUID; From cf605c8a38d13fdbe22b40791e8e4da614659437 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 10 May 2024 13:52:42 +0100 Subject: [PATCH 3/5] refactor: Convert Task Manager to use Components This currently behaves the same as it did before: It still recreates the table contents every half a second. It should also look identical, though it's possible I missed some small differences. The component structure is: TaskManagerTable - Table - TaskManagerRow - TaskManagerRow - TaskManagerRow - ... TaskManagerRow is implemented so that we can later move to modifying them in place as the process tree changes, instead of having to replace them all. Otherwise, most of the code is just moved around, and not changed much. --- src/UI/UIWindowTaskManager.js | 506 ++++++++++++++++++++-------------- src/css/style.css | 110 -------- 2 files changed, 292 insertions(+), 324 deletions(-) diff --git a/src/UI/UIWindowTaskManager.js b/src/UI/UIWindowTaskManager.js index 3b4ab3011..636f11321 100644 --- a/src/UI/UIWindowTaskManager.js +++ b/src/UI/UIWindowTaskManager.js @@ -2,11 +2,291 @@ import { END_HARD, END_SOFT } from "../definitions.js"; import UIAlert from "./UIAlert.js"; import UIContextMenu from "./UIContextMenu.js"; import UIWindow from "./UIWindow.js"; +import { Component, defineComponent } from '../util/Component.js'; +import UIComponentWindow from './UIComponentWindow.js'; +import Table from './Components/Table.js'; +import Placeholder from '../util/Placeholder.js'; +import TestView from './Components/TestView.js'; + +const end_process = async (uuid, force) => { + const svc_process = globalThis.services.get('process'); + const process = svc_process.get_by_uuid(uuid); + if (!process) { + console.warn(`Can't end process with uuid='${uuid}': does not exist`); + return; + } + + let confirmation; + if ( process.is_init() ) { + if ( ! force ) { + confirmation = i18n('close_all_windows_confirm'); + } else { + confirmation = i18n('restart_puter_confirm'); + } + } else if ( force ) { + confirmation = i18n('end_process_force_confirm'); + } + + if ( confirmation ) { + const alert_resp = await UIAlert({ + message: confirmation, + buttons:[ + { + label: i18n('yes'), + value: true, + type: 'primary', + }, + { + label: i18n('no'), + value: false, + }, + ] + }) + if ( ! alert_resp ) return; + } + + process.signal(force ? END_HARD : END_SOFT); +}; + +class TaskManagerTable extends Component { + static PROPERTIES = { + tasks: { value: [] }, + }; + + static CSS = /*css*/` + :host { + flex-grow: 1; + display: flex; + flex-direction: column; + background-color: rgba(255,255,255,0.8); + border: 2px inset rgba(127, 127, 127, 0.3); + overflow: auto; + } + `; + + #svc_process = globalThis.services.get('process'); + + create_template ({ template }) { + $(template).html(` +
+ `); + } + + on_ready ({ listen }) { + this.table = new Table({ + headings: [ + i18n('taskmgr_header_name'), + i18n('taskmgr_header_type'), + i18n('taskmgr_header_status'), + ] + }); + this.table.attach(this.dom_.querySelector('.taskmgr-taskarea')); + + listen('tasks', tasks => { + // TODO: Update DOM instead of replacing the entire table + this.table.set('rows', this.#iter_tasks(tasks, { indent_level: 0, is_last_item_stack: [] })); + }); + } + + #calculate_indent_string (indent_level, is_last_item_stack, is_last_item) { + // Returns a string of '| ├└' + let result = ''; + + for ( let i=0; i < indent_level; i++ ) { + const last_cell = i === indent_level - 1; + const has_trunk = (last_cell && ( ! is_last_item )) || + (!last_cell && !is_last_item_stack[i+1]); + const has_branch = last_cell; + + if (has_trunk && has_branch) { + result += '├'; + } else if (has_trunk) { + result += '|'; + } else if (has_branch) { + result += '└'; + } else { + result += ' '; + } + } + + return result; + } + + #iter_tasks (items, { indent_level, is_last_item_stack }) { + const rows = []; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + const is_last_item = i === items.length - 1; + rows.push(new TaskManagerRow({ + name: item.name, + uuid: item.uuid, + process_type: item.type, + process_status: item.status.i18n_key, + indentation: this.#calculate_indent_string(indent_level, is_last_item_stack, is_last_item), + })); + + const children = this.#svc_process.get_children_of(item.uuid); + if (children) { + rows.push(...this.#iter_tasks(children, { + indent_level: indent_level + 1, + is_last_item_stack: + [ ...is_last_item_stack, is_last_item ], + })); + } + } + return rows; + }; +} +defineComponent('c-task-manager-table', TaskManagerTable); + +class TaskManagerRow extends Component { + static PROPERTIES = { + name: {}, + uuid: {}, + process_type: {}, + process_status: {}, + indentation: { value: '' }, + }; + + static CSS = /*css*/` + :host { + display: table-row; + } + + td > span { + padding: 0 calc(2.5 * var(--scale)); + } + + .task { + display: flex; + height: calc(10 * var(--scale)); + line-height: calc(10 * var(--scale)); + } + + .task-name { + flex-grow: 1; + padding-left: calc(2.5 * var(--scale)); + } + + .task-indentation { + display: flex; + } + + .indentcell { + position: relative; + align-items: right; + width: calc(10 * var(--scale)); + height: calc(10 * var(--scale)); + } + + .indentcell-trunk { + position: absolute; + top: 0; + left: calc(5 * var(--scale)); + width: calc(5 * var(--scale)); + height: calc(10 * var(--scale)); + border-left: 2px solid var(--line-color); + } + + .indentcell-branch { + position: absolute; + top: 0; + left: calc(5 * var(--scale)); + width: calc(5 * var(--scale)); + height: calc(5 * var(--scale)); + border-left: 2px solid var(--line-color); + border-bottom: 2px solid var(--line-color); + border-radius: 0 0 0 calc(2.5 * var(--scale)); + } + `; + + create_template ({ template }) { + template.innerHTML = ` + +
+
+
+
+ + + + `; + } + + on_ready ({ listen }) { + listen('name', name => { + $(this.dom_).find('.task-name').text(name); + }); + listen('uuid', uuid => { + this.setAttribute('data-uuid', uuid); + }); + listen('process_type', type => { + $(this.dom_).find('.process-type').text(i18n('process_type_' + type)); + }); + listen('process_status', status => { + $(this.dom_).find('.process-status').text(i18n('process_status_' + status)); + }); + listen('indentation', indentation => { + const el = $(this.dom_).find('.task-indentation'); + let h = ''; + for (const c of indentation) { + h += `
`; + switch (c) { + case ' ': + break; + case '|': + h += `
`; + break; + case '└': + h += `
`; + break; + case '├': + h += `
`; + h += `
`; + break; + } + h += `
`; + } + el.html(h); + }); + + $(this).on('contextmenu', () => { + const uuid = this.get('uuid'); + UIContextMenu({ + items: [ + { + html: i18n('close'), + onClick: () => { + end_process(uuid); + }, + }, + { + html: i18n('force_quit'), + onClick: () => { + end_process(uuid, true); + }, + }, + ], + }); + }); + } +} +defineComponent('c-task-manager-row', TaskManagerRow); const UIWindowTaskManager = async function UIWindowTaskManager () { const svc_process = globalThis.services.get('process'); - const w = await UIWindow({ + let task_manager_table = new TaskManagerTable({ + tasks: [svc_process.get_init()], + }); + + const interval = setInterval(() => { + const processes = [svc_process.get_init()]; + task_manager_table.set('tasks', processes); + }, 500); + + const w = await UIComponentWindow({ + component: task_manager_table, title: i18n('task_manager'), icon: globalThis.icons['cog.svg'], uid: null, @@ -40,220 +320,18 @@ const UIWindowTaskManager = async function UIWindowTaskManager () { var(--primary-lightness), var(--primary-alpha))`, 'backdrop-filter': 'blur(3px)', - - } + 'box-sizing': 'border-box', + // could have been avoided with box-sizing: border-box + height: 'calc(100% - 30px)', + display: 'flex', + 'flex-direction': 'column', + '--scale': '2pt', + '--line-color': '#6e6e6ebd', + }, + on_close: () => { + clearInterval(interval); + }, }); - const w_body = w.querySelector('.window-body'); - w_body.classList.add('taskmgr'); - - const Indent = ({ has_trunk, has_branch }) => { - const el = document.createElement('div'); - el.classList.add('taskmgr-indentcell'); - if ( has_trunk ) { - // Add new child element - const el_indentcell_child = document.createElement('div'); - el_indentcell_child.classList.add('taskmgr-indentcell-trunk'); - el.appendChild(el_indentcell_child); - } - if ( has_branch ) { - const el_indentcell_child = document.createElement('div'); - el_indentcell_child.classList.add('taskmgr-indentcell-branch'); - el.appendChild(el_indentcell_child); - } - - return { - appendTo (parent) { - parent.appendChild(el); - return this; - } - }; - }; - - const Task = ({ placement, name }) => { - const { - indent_level, last_item, - parent_last_item, - } = placement; - - const el = document.createElement('div'); - el.classList.add('taskmgr-task'); - - for ( let i=0; i < indent_level; i++ ) { - const last_cell = i === indent_level - 1; - Indent({ - has_trunk: (last_cell && ( ! last_item )) || - (!last_cell && !parent_last_item[i+1]), - has_branch: last_cell - }).appendTo(el); - } - - const el_title = document.createElement('div'); - el_title.classList.add('taskmgr-task-title'); - el_title.innerText = name; - el.appendChild(el_title); - - return { - el () { return el; }, - appendTo (parent) { - parent.appendChild(el); - return this; - } - }; - } - - // https://codepen.io/fomkin/pen/gOgoBVy - const Table = ({ headings }) => { - const el_table = $(` - - - - ${headings.map(heading => - ``).join('')} - - - -
${heading}
- `)[0]; - - const el_tbody = el_table.querySelector('tbody'); - - return { - el () { return el_table; }, - add (el) { - if ( typeof el.el === 'function' ) el = el.el(); - el_tbody.appendChild(el); - return this; - }, - clear () { - el_tbody.innerHTML = ''; - } - }; - }; - - const Row = () => { - const el_tr = document.createElement('tr'); - return { - attach (parent) { - parent.appendChild(el_tr); - return this; - }, - el () { return el_tr; }, - add (el) { - if ( typeof el.el === 'function' ) el = el.el(); - const el_td = document.createElement('td'); - el_td.appendChild(el); - el_tr.appendChild(el_td); - return this; - } - }; - }; - - const el_taskarea = document.createElement('div'); - el_taskarea.classList.add('taskmgr-taskarea'); - - const tasktable = Table({ - headings: [ - i18n('taskmgr_header_name'), - i18n('taskmgr_header_type'), - i18n('taskmgr_header_status'), - ] - }); - - el_taskarea.appendChild(tasktable.el()); - - const end_process_ = async (process, force) => { - let confirmation; - - if ( process.is_init() ) { - if ( ! force ) { - confirmation = i18n('close_all_windows_confirm'); - } else { - confirmation = i18n('restart_puter_confirm'); - } - } else if ( force ) { - confirmation = i18n('end_process_force_confirm'); - } - - if ( confirmation ) { - const alert_resp = await UIAlert({ - message: confirmation, - buttons:[ - { - label: i18n('yes'), - value: true, - type: 'primary', - }, - { - label: i18n('no'), - value: false, - }, - ] - }) - if ( ! alert_resp ) return; - } - - process.signal(force ? END_HARD : END_SOFT); - } - - const iter_tasks = (items, { indent_level, parent_last_item }) => { - for ( let i=0 ; i < items.length; i++ ) { - const row = Row(); - const item = items[i]; - const last_item = i === items.length - 1; - row.add(Task({ - placement: { - parent_last_item, - indent_level, - last_item, - }, - name: item.name - })); - row.add($(`${i18n('process_type_' + item.type)}`)[0]) - row.add($(`${i18n('process_status_' + item.status.i18n_key)}`)[0]) - tasktable.add(row); - - $(row.el()).on('contextmenu', () => { - UIContextMenu({ - parent_element: $(el_taskarea), - items: [ - { - html: i18n('close'), - onClick: () => { - end_process_(item); - } - }, - { - html: i18n('force_quit'), - onClick: () => { - end_process_(item, true); - } - } - ] - }); - }) - - const children = svc_process.get_children_of(item.uuid); - if ( children ) { - iter_tasks(children, { - indent_level: indent_level + 1, - parent_last_item: - [...parent_last_item, last_item], - }); - } - } - }; - - const interval = setInterval(() => { - tasktable.clear(); - const processes = [svc_process.get_init()]; - iter_tasks(processes, { indent_level: 0, parent_last_item: [] }); - }, 500) - - w.on_close = () => { - clearInterval(interval); - } - - w_body.appendChild(el_taskarea); } export default UIWindowTaskManager; diff --git a/src/css/style.css b/src/css/style.css index a2657158c..2c2726635 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -3830,113 +3830,3 @@ fieldset[name=number-code] { gap: 10px; justify-content: flex-end; } - -.taskmgr { - box-sizing: border-box; - /* could have been avoided with box-sizing: border-box */ - height: calc(100% - 30px); - display: flex; - flex-direction: column; - - --scale: 2pt; - --line-color: #6e6e6ebd; -} - -.taskmgr * { - box-sizing: border-box; -} - -.taskmgr table { - border-collapse: collapse; -} - -.taskmgr-taskarea { - flex-grow: 1; - display: flex; - flex-direction: column; - background-color: rgba(255,255,255,0.8); - border: 2px inset rgba(127, 127, 127, 0.3); - overflow: auto; -} - -.taskmgr-taskarea table thead { -} - -.taskmgr-taskarea table th { - -webkit-box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2); - box-shadow: 0 1px 4px -2px rgba(0,0,0,0.2); - backdrop-filter: blur(2px); - position: sticky; - z-index: 100; - padding: 0; - top: 0; - background-color: hsla(0, 0%, 100%, 0.8); - text-align: left; -} - -.taskmgr-taskarea table th > span { - display: inline-block; - width: 100%; - /* we set borders on this span because */ - /* borders fly away from sticky headers */ - border-bottom: 1px solid #e0e0e0; - - /* padding order: top right bottom left */ - padding: - calc(10 * var(--scale)) - calc(2.5 * var(--scale)) - calc(5 * var(--scale)) - calc(2.5 * var(--scale)); -} - -.taskmgr-taskarea table th:not(:last-of-type) > span { - /* we set borders on this span because */ - /* borders fly away from sticky headers */ - border-right: 1px solid #e0e0e0; -} - -.taskmgr-taskarea table td { - border-bottom: 1px solid #e0e0e0; -} - -.taskmgr-taskarea table td > span { - padding: 0 calc(2.5 * var(--scale)); -} - -.taskmgr-indentcell { - position: relative; - align-items: right; - width: calc(10 * var(--scale)); - height: calc(10 * var(--scale)); -} - -.taskmgr-indentcell-trunk { - position: absolute; - top: 0; - left: calc(5 * var(--scale)); - width: calc(5 * var(--scale)); - height: calc(10 * var(--scale)); - border-left: 2px solid var(--line-color); -} - -.taskmgr-indentcell-branch { - position: absolute; - top: 0; - left: calc(5 * var(--scale)); - width: calc(5 * var(--scale)); - height: calc(5 * var(--scale)); - border-left: 2px solid var(--line-color); - border-bottom: 2px solid var(--line-color); - border-radius: 0 0 0 calc(2.5 * var(--scale)); -} - -.taskmgr-task { - display: flex; - height: calc(10 * var(--scale)); - line-height: calc(10 * var(--scale)); -} - -.taskmgr-task-title { - flex-grow: 1; - padding-left: calc(2.5 * var(--scale)); -} From 5b43358219402bee3eadf4a0f184a4b924d3293b Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 10 May 2024 14:48:26 +0100 Subject: [PATCH 4/5] fix: Only run Component initialization functions once If the Component gets removed from the DOM and then re-added, it already has contents, and we don't need to create them again. It also has already had on_ready called, so that doesn't need to happen again either. This fix stops Components duplicating their content elements and listener callbacks whenever they're moved around the document. --- src/util/Component.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/util/Component.js b/src/util/Component.js index 4439e3ce2..40839c86a 100644 --- a/src/util/Component.js +++ b/src/util/Component.js @@ -1,6 +1,9 @@ import ValueHolder from "./ValueHolder.js"; export class Component extends HTMLElement { + #has_created_element = false; + #has_called_on_ready = false; + // Render modes static NO_SHADOW = Symbol('no-shadow'); @@ -86,12 +89,18 @@ export class Component extends HTMLElement { } connectedCallback () { - this.on_ready && this.on_ready(this.get_api_()); + if (!this.#has_called_on_ready) { + this.on_ready && this.on_ready(this.get_api_()); + this.#has_called_on_ready = true; + } } attach (destination) { - const el = this.create_element_(); - this.dom_.appendChild(el); + if (!this.#has_created_element) { + const el = this.create_element_(); + this.dom_.appendChild(el); + this.#has_created_element = true; + } if ( destination instanceof HTMLElement ) { destination.appendChild(this); From 9bb4570126f05024e112361cb70ec3b5901cb6a6 Mon Sep 17 00:00:00 2001 From: Sam Atkins Date: Fri, 10 May 2024 15:12:53 +0100 Subject: [PATCH 5/5] Re-use existing Task Manager rows if possible instead of recreating them Most of the time, we'll already have a TaskManagerRow for the given process, so we can just update its properties. Iterating the results from #iter_tasks means we also insert the rows in the correct order, regardless of their previous order. --- src/UI/UIWindowTaskManager.js | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/UI/UIWindowTaskManager.js b/src/UI/UIWindowTaskManager.js index 636f11321..7cb2b392a 100644 --- a/src/UI/UIWindowTaskManager.js +++ b/src/UI/UIWindowTaskManager.js @@ -83,8 +83,28 @@ class TaskManagerTable extends Component { this.table.attach(this.dom_.querySelector('.taskmgr-taskarea')); listen('tasks', tasks => { - // TODO: Update DOM instead of replacing the entire table - this.table.set('rows', this.#iter_tasks(tasks, { indent_level: 0, is_last_item_stack: [] })); + const row_data = this.#iter_tasks(tasks, { indent_level: 0, is_last_item_stack: [] }); + const new_uuids = row_data.map(it => it.uuid); + + const old_rows = this.table.get('rows'); + + const rows = []; + for (const data of row_data) { + // Try to reuse old row + const old_row = old_rows.find(it => data.uuid === it.get('uuid')); + if (old_row) { + for (const property in data) { + old_row.set(property, data[property]); + } + rows.push(old_row); + continue; + } + + // Create a new row + rows.push(new TaskManagerRow(data)); + } + + this.table.set('rows', rows); }); } @@ -117,13 +137,13 @@ class TaskManagerTable extends Component { for (let i = 0; i < items.length; i++) { const item = items[i]; const is_last_item = i === items.length - 1; - rows.push(new TaskManagerRow({ + rows.push({ name: item.name, uuid: item.uuid, process_type: item.type, process_status: item.status.i18n_key, indentation: this.#calculate_indent_string(indent_level, is_last_item_stack, is_last_item), - })); + }); const children = this.#svc_process.get_children_of(item.uuid); if (children) {