namespace ui { export namespace scheme { export interface CheckBox { border: string; checkmark: string; checkmark_font: string; background_checked: string; background_checked_hovered: string; background: string; background_hovered: string; } export interface TextField { color: string; font: string; background: string; background_hovered: string; } export interface ColorScheme { permission: { background: string; background_selected: string; name: string; name_unset: string; name_font: string; value: TextField; value_b: CheckBox; granted: TextField; negate: CheckBox; skip: CheckBox; } group: { name: string; name_font: string; } } } export enum RepaintMode { NONE, REPAINT, REPAINT_OBJECT_FULL, REPAINT_FULL } export interface AxisAlignedBoundingBox { x: number; y: number; width: number; height: number; } export enum ClickEventType { SIGNLE, DOUBLE, CONTEXT_MENU } export interface InteractionClickEvent { type: ClickEventType; consumed: boolean; offset_x: number; offset_y: number; } export interface InteractionListener { region: AxisAlignedBoundingBox; region_weight: number; /** * @return true if a redraw is required */ on_mouse_enter?: () => RepaintMode; /** * @return true if a redraw is required */ on_mouse_leave?: () => RepaintMode; /** * @return true if a redraw is required */ on_click?: (event: InteractionClickEvent) => RepaintMode; mouse_cursor?: string; set_full_draw?: () => any; disabled?: boolean; } abstract class DrawableObject { abstract draw(context: CanvasRenderingContext2D, full: boolean); private _object_full_draw = false; private _width: number = 0; set_width(value: number) { this._width = value; } request_full_draw() { this._object_full_draw = true; } pop_full_draw() { const result = this._object_full_draw; this._object_full_draw = false; return result; } width() { return this._width; } abstract height(); private _transforms: DOMMatrix[] = []; protected push_transform(context: CanvasRenderingContext2D) { this._transforms.push(context.getTransform()); } protected pop_transform(context: CanvasRenderingContext2D) { const transform = this._transforms.pop(); context.setTransform( transform.a, transform.b, transform.c, transform.d, transform.e, transform.f ); } protected original_x(context: CanvasRenderingContext2D, x: number) { return context.getTransform().e + x; } protected original_y(context: CanvasRenderingContext2D, y: number) { return context.getTransform().f + y; } protected colors: scheme.ColorScheme = {} as any; set_color_scheme(scheme: scheme.ColorScheme) { this.colors = scheme; } protected manager: PermissionEditor; set_manager(manager: PermissionEditor) { this.manager = manager; } abstract initialize(); abstract finalize(); } class PermissionGroup extends DrawableObject { public static readonly HEIGHT = 24; public static readonly ARROW_SIZE = 12; group: GroupedPermissions; _sub_elements: PermissionGroup[] = []; _element_permissions: PermissionList; collapsed = false; private _listener_colaps: InteractionListener; constructor(group: GroupedPermissions) { super(); this.group = group; this._element_permissions = new PermissionList(this.group.permissions); for(const sub of this.group.children) this._sub_elements.push(new PermissionGroup(sub)); } draw(context: CanvasRenderingContext2D, full: boolean) { const _full = this.pop_full_draw() || full; this.push_transform(context); context.translate(PermissionGroup.ARROW_SIZE + 20, PermissionGroup.HEIGHT); let sum_height = 0; /* let first draw the elements, because if the sum height is zero then we could hide ourselves */ if(!this.collapsed) { /* draw the next groups */ for(const group of this._sub_elements) { group.draw(context, full); const height = group.height(); sum_height += height; context.translate(0, height); } this._element_permissions.draw(context, full); if(sum_height == 0) sum_height += this._element_permissions.height(); } else { const process_group = (group: PermissionGroup) => { for(const g of group._sub_elements) process_group(g); group._element_permissions.handle_hide(); if(sum_height == 0 && group._element_permissions.height() > 0){ sum_height = 1; } }; process_group(this); } this.pop_transform(context); if(_full && sum_height > 0) { const arrow_stretch = 2/3; if(!full) { context.clearRect(0, 0, this.width(), PermissionGroup.HEIGHT); } context.fillStyle = this.colors.group.name; /* arrow */ { const x1 = this.collapsed ? PermissionGroup.ARROW_SIZE * arrow_stretch / 2 : 0; const y1 = (PermissionGroup.HEIGHT - PermissionGroup.ARROW_SIZE) / 2 + (this.collapsed ? 0 : PermissionGroup.ARROW_SIZE * arrow_stretch / 2); /* center arrow */ const x2 = this.collapsed ? x1 + PermissionGroup.ARROW_SIZE * arrow_stretch : x1 + PermissionGroup.ARROW_SIZE / 2; const y2 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE / 2 : y1 + PermissionGroup.ARROW_SIZE * arrow_stretch; const x3 = this.collapsed ? x1 : x1 + PermissionGroup.ARROW_SIZE; const y3 = this.collapsed ? y1 + PermissionGroup.ARROW_SIZE : y1; context.beginPath(); context.moveTo(x1, y1); context.lineTo(x2, y2); context.lineTo(x3, y3); context.moveTo(x2, y2); context.lineTo(x3, y3); context.fill(); this._listener_colaps.region.x = this.original_x(context, 0); this._listener_colaps.region.y = this.original_y(context, y1); } /* text */ { context.font = this.colors.group.name_font; context.textBaseline = "middle"; context.textAlign = "start"; context.fillText(this.group.group.name, PermissionGroup.ARROW_SIZE + 5, PermissionGroup.HEIGHT / 2); } } } set_width(value: number) { super.set_width(value); for(const element of this._sub_elements) element.set_width(value - PermissionGroup.ARROW_SIZE - 20); this._element_permissions.set_width(value - PermissionGroup.ARROW_SIZE - 20); } set_color_scheme(scheme: scheme.ColorScheme) { super.set_color_scheme(scheme); for(const child of this._sub_elements) child.set_color_scheme(scheme); this._element_permissions.set_color_scheme(scheme); } set_manager(manager: PermissionEditor) { super.set_manager(manager); for(const child of this._sub_elements) child.set_manager(manager); this._element_permissions.set_manager(manager); } height() { let result = 0; if(!this.collapsed) { for(const element of this._sub_elements) result += element.height(); result += this._element_permissions.height(); } else { //We've to figure out if we have permissions const process_group = (group: PermissionGroup) => { if(result == 0 && group._element_permissions.height() > 0){ result = 1; } else { for(const g of group._sub_elements) process_group(g); } }; process_group(this); if(result > 0) return PermissionGroup.HEIGHT; return 0; } if(result > 0) { result += PermissionGroup.HEIGHT; return result; } else { return 0; } } initialize() { for(const child of this._sub_elements) child.initialize(); this._element_permissions.initialize(); this._listener_colaps = { region: { x: 0, y: 0, height: PermissionGroup.ARROW_SIZE, width: PermissionGroup.ARROW_SIZE }, region_weight: 10, /* on_mouse_enter: () => { this.collapsed_hovered = true; return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { this.collapsed_hovered = false; return RepaintMode.REPAINT_OBJECT_FULL; }, */ on_click: () => { this.collapsed = !this.collapsed; return RepaintMode.REPAINT_FULL; }, set_full_draw: () => this.request_full_draw(), mouse_cursor: "pointer" }; this.manager.intercept_manager().register_listener(this._listener_colaps); } finalize() { for(const child of this._sub_elements) child.finalize(); this._element_permissions.finalize(); } collapse_group() { for(const child of this._sub_elements) child.collapse_group(); this.collapsed = true; } expend_group() { for(const child of this._sub_elements) child.expend_group(); this.collapsed = false; } } class PermissionList extends DrawableObject { permissions: PermissionEntry[] = []; constructor(permissions: PermissionInfo[]) { super(); for(const permission of permissions) this.permissions.push(new PermissionEntry(permission)); } set_width(value: number) { super.set_width(value); for(const entry of this.permissions) entry.set_width(value); } draw(context: CanvasRenderingContext2D, full: boolean) { this.push_transform(context); for(const permission of this.permissions) { permission.draw(context, full); context.translate(0, permission.height()); } this.pop_transform(context); } height() { let height = 0; for(const permission of this.permissions) height += permission.height(); return height; } set_color_scheme(scheme: scheme.ColorScheme) { super.set_color_scheme(scheme); for(const entry of this.permissions) entry.set_color_scheme(scheme); } set_manager(manager: PermissionEditor) { super.set_manager(manager); for(const entry of this.permissions) entry.set_manager(manager); } initialize() { for(const entry of this.permissions) entry.initialize(); } finalize() { for(const entry of this.permissions) entry.finalize(); } handle_hide() { for(const entry of this.permissions) entry.handle_hide(); } } class PermissionEntry extends DrawableObject { public static readonly HEIGHT = 24; public static readonly HALF_HEIGHT = PermissionEntry.HEIGHT / 2; public static readonly CHECKBOX_HEIGHT = PermissionEntry.HEIGHT - 2; public static readonly COLUMN_PADDING = 2; public static readonly COLUMN_VALUE = 75; public static readonly COLUMN_GRANTED = 75; //public static readonly COLUMN_NEGATE = 25; //public static readonly COLUMN_SKIP = 25; public static readonly COLUMN_NEGATE = 75; public static readonly COLUMN_SKIP = 75; private _permission: PermissionInfo; hidden: boolean; granted: number = 22; value: number; flag_skip: boolean = true; flag_negate: boolean; private _prev_selected = false; selected: boolean; flag_skip_hovered = false; flag_negate_hovered = false; flag_value_hovered = false; flag_grant_hovered = false; private _listener_checkbox_skip: InteractionListener; private _listener_checkbox_negate: InteractionListener; private _listener_value: InteractionListener; private _listener_grant: InteractionListener; private _listener_general: InteractionListener; on_context_menu?: (x: number, y: number) => any; on_grant_change?: () => any; on_change?: () => any; constructor(permission: PermissionInfo) { super(); this._permission = permission; } permission() { return this._permission; } draw(ctx: CanvasRenderingContext2D, full: boolean) { if(!this.pop_full_draw() && !full) { /* Note: do not change this order! */ /* test for update! */ return; } if(this.hidden) { this.handle_hide(); return; } ctx.lineWidth = 1; /* debug box */ if(false) { ctx.fillStyle = "#FF0000"; ctx.fillRect(0, 0, this.width(), PermissionEntry.HEIGHT); ctx.fillStyle = "#000000"; ctx.strokeRect(0, 0, this.width(), PermissionEntry.HEIGHT); } if(!full) { const off = this.selected || this._prev_selected ? ctx.getTransform().e : 0; ctx.clearRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); } if(this.selected) ctx.fillStyle = this.colors.permission.background_selected; else ctx.fillStyle = this.colors.permission.background; const off = this.selected ? ctx.getTransform().e : 0; ctx.fillRect(-off, 0, this.width() + off, PermissionEntry.HEIGHT); this._prev_selected = this.selected; /* permission name */ { ctx.fillStyle = typeof(this.value) !== "undefined" ? this.colors.permission.name : this.colors.permission.name_unset; ctx.textBaseline = "middle"; ctx.textAlign = "start"; ctx.font = this.colors.permission.name_font; ctx.fillText(this._permission.name, 0, PermissionEntry.HALF_HEIGHT); } const original_y = this.original_y(ctx, 0); const original_x = this.original_x(ctx, 0); const width = this.width(); /* draw granted */ let w = width - PermissionEntry.COLUMN_GRANTED; if(typeof(this.granted) === "number") { this._listener_grant.region.x = original_x + w; this._listener_grant.region.y = original_y; this._draw_number_field(ctx, this.colors.permission.granted, w, 0, PermissionEntry.COLUMN_VALUE, this.granted, this.flag_grant_hovered); } else { this._listener_grant.region.y = original_y; this._listener_grant.region.x = original_x + width - PermissionEntry.COLUMN_GRANTED; } /* draw value and the skip stuff */ if(typeof(this.value) === "number") { w -= PermissionEntry.COLUMN_SKIP + PermissionEntry.COLUMN_PADDING; { const x = w + (PermissionEntry.COLUMN_SKIP - PermissionEntry.CHECKBOX_HEIGHT) / 2; const y = 1; this._listener_checkbox_skip.region.x = original_x + x; this._listener_checkbox_skip.region.y = original_y + y; this._draw_checkbox_field(ctx, this.colors.permission.skip, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_skip, this.flag_skip_hovered); } w -= PermissionEntry.COLUMN_NEGATE + PermissionEntry.COLUMN_PADDING; { const x = w + (PermissionEntry.COLUMN_NEGATE - PermissionEntry.CHECKBOX_HEIGHT) / 2; const y = 1; this._listener_checkbox_negate.region.x = original_x + x; this._listener_checkbox_negate.region.y = original_y + y; this._draw_checkbox_field(ctx, this.colors.permission.negate, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.flag_negate, this.flag_negate_hovered); } w -= PermissionEntry.COLUMN_VALUE + PermissionEntry.COLUMN_PADDING; if(this._permission.is_boolean()) { const x = w + PermissionEntry.COLUMN_VALUE - PermissionEntry.CHECKBOX_HEIGHT; const y = 1; this._listener_value.region.x = original_x + x; this._listener_value.region.y = original_y + y; this._draw_checkbox_field(ctx, this.colors.permission.value_b, x, y, PermissionEntry.CHECKBOX_HEIGHT, this.value > 0, this.flag_value_hovered); } else { this._listener_value.region.x = original_x + w; this._listener_value.region.y = original_y; this._draw_number_field(ctx, this.colors.permission.value, w, 0, PermissionEntry.COLUMN_VALUE, this.value, this.flag_value_hovered); } this._listener_value.disabled = false; } else { this._listener_checkbox_skip.region.y = -1e8; this._listener_checkbox_negate.region.y = -1e8; this._listener_value.region.y = original_y; this._listener_value.region.x = original_x + width - PermissionEntry.COLUMN_GRANTED - PermissionEntry.COLUMN_NEGATE - PermissionEntry.COLUMN_VALUE - PermissionEntry.COLUMN_PADDING * 4; this._listener_value.disabled = true; } this._listener_general.region.y = original_y; this._listener_general.region.x = original_x; } handle_hide() { /* so the listener wound get triggered */ this._listener_value.region.x = -1e8; this._listener_grant.region.x = -1e8; this._listener_checkbox_negate.region.x = -1e8; this._listener_checkbox_skip.region.x = -1e8; this._listener_general.region.x = -1e8; } private _draw_number_field(ctx: CanvasRenderingContext2D, scheme: scheme.TextField, x: number, y: number, width: number, value: number, hovered: boolean) { ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background; ctx.fillRect(x, y, width, PermissionEntry.HEIGHT); ctx.fillStyle = scheme.color; ctx.font = scheme.font; //Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; ctx.textAlign = "start"; ctx.fillText(value + "", x, y + PermissionEntry.HALF_HEIGHT, width); ctx.strokeStyle = "#6e6e6e"; const line = ctx.lineWidth; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(x, y + PermissionEntry.HEIGHT - 2); ctx.lineTo(x + width, y + PermissionEntry.HEIGHT - 2); ctx.stroke(); ctx.lineWidth = line; } private _draw_checkbox_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, height: number, checked: boolean, hovered: boolean) { ctx.fillStyle = scheme.border; ctx.strokeRect(x, y, height, height); ctx.fillStyle = checked ? (hovered ? scheme.background_checked_hovered : scheme.background_checked) : (hovered ? scheme.background_hovered : scheme.background); ctx.fillRect(x + 1, y + 1, height - 2, height - 2); if(checked) { ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillStyle = scheme.checkmark; ctx.font = scheme.checkmark_font; //Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; ctx.fillText("✓", x + height / 2, y + height / 2); } } height() { return this.hidden ? 0 : PermissionEntry.HEIGHT; } set_width(value: number) { super.set_width(value); this._listener_general.region.width = value; } initialize() { this._listener_checkbox_skip = { region: { x: -1e8, y: -1e8, height: PermissionEntry.CHECKBOX_HEIGHT, width: PermissionEntry.CHECKBOX_HEIGHT }, region_weight: 10, on_mouse_enter: () => { this.flag_skip_hovered = true; return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { this.flag_skip_hovered = false; return RepaintMode.REPAINT_OBJECT_FULL; }, on_click: () => { this.flag_skip = !this.flag_skip; if(this.on_change) this.on_change(); return RepaintMode.REPAINT_OBJECT_FULL; }, set_full_draw: () => this.request_full_draw(), mouse_cursor: "pointer" }; this._listener_checkbox_negate = { region: { x: -1e8, y: -1e8, height: PermissionEntry.CHECKBOX_HEIGHT, width: PermissionEntry.CHECKBOX_HEIGHT }, region_weight: 10, on_mouse_enter: () => { this.flag_negate_hovered = true; return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { this.flag_negate_hovered = false; return RepaintMode.REPAINT_OBJECT_FULL; }, on_click: () => { this.flag_negate = !this.flag_negate; if(this.on_change) this.on_change(); return RepaintMode.REPAINT_OBJECT_FULL; }, set_full_draw: () => this.request_full_draw(), mouse_cursor: "pointer" }; this._listener_value = { region: { x: -1e8, y: -1e8, height: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.HEIGHT, width: this._permission.is_boolean() ? PermissionEntry.CHECKBOX_HEIGHT : PermissionEntry.COLUMN_VALUE }, region_weight: 10, on_mouse_enter: () => { this.flag_value_hovered = true; return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { this.flag_value_hovered = false; return RepaintMode.REPAINT_OBJECT_FULL; }, on_click: () => { if(this._permission.is_boolean()) { this.value = this.value > 0 ? 0 : 1; if(this.on_change) this.on_change(); return RepaintMode.REPAINT_OBJECT_FULL; } else { this._spawn_number_edit( this._listener_value.region.x, this._listener_value.region.y, this._listener_value.region.width, this._listener_value.region.height, this.colors.permission.value, this.value || 0, value => { if(typeof(value) === "number") { this.value = value; this.request_full_draw(); this.manager.request_draw(false); if(this.on_change) this.on_change(); } } ) } return RepaintMode.REPAINT_OBJECT_FULL; }, set_full_draw: () => this.request_full_draw(), mouse_cursor: "pointer" }; this._listener_grant = { region: { x: -1e8, y: -1e8, height: PermissionEntry.HEIGHT, width: PermissionEntry.COLUMN_VALUE }, region_weight: 10, on_mouse_enter: () => { this.flag_grant_hovered = true; return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { this.flag_grant_hovered = false; return RepaintMode.REPAINT_OBJECT_FULL; }, on_click: () => { this._spawn_number_edit( this._listener_grant.region.x, this._listener_grant.region.y, this._listener_grant.region.width, this._listener_grant.region.height, this.colors.permission.granted, this.granted || 0, //TODO use max assignable value? value => { if(typeof(value) === "number") { this.granted = value; this.request_full_draw(); this.manager.request_draw(false); if(this.on_grant_change) this.on_grant_change(); } } ); return RepaintMode.REPAINT_OBJECT_FULL; }, set_full_draw: () => this.request_full_draw(), mouse_cursor: "pointer" }; this._listener_general = { region: { x: -1e8, y: -1e8, height: PermissionEntry.HEIGHT, width: 0 }, region_weight: 0, /* on_mouse_enter: () => { return RepaintMode.REPAINT_OBJECT_FULL; }, on_mouse_leave: () => { return RepaintMode.REPAINT_OBJECT_FULL; }, */ on_click: (event: InteractionClickEvent) => { this.manager.set_selected_entry(this); if(event.type == ClickEventType.DOUBLE && typeof(this.value) === "undefined") return this._listener_value.on_click(event); else if(event.type == ClickEventType.CONTEXT_MENU) { const mouse = this.manager.mouse; if(this.on_context_menu) { this.on_context_menu(mouse.x, mouse.y); event.consumed = true; } } return RepaintMode.NONE; }, set_full_draw: () => this.request_full_draw(), }; this.manager.intercept_manager().register_listener(this._listener_checkbox_negate); this.manager.intercept_manager().register_listener(this._listener_checkbox_skip); this.manager.intercept_manager().register_listener(this._listener_value); this.manager.intercept_manager().register_listener(this._listener_grant); this.manager.intercept_manager().register_listener(this._listener_general); } finalize() { } private _spawn_number_edit(x: number, y: number, width: number, height: number, color: scheme.TextField, value: number, callback: (new_value?: number) => any) { const element = $.spawn("div"); element.prop("contentEditable", true); element .css("pointer-events", "none") .css("background", color.background) .css("display", "block") .css("position", "absolute") .css("top", y) .css("left", x) .css("width", width) .css("height", height) .css("z-index", 1e6); element.text(value); element.appendTo(this.manager.canvas_container); element.focus(); element.on('focusout', event => { console.log("permission changed to " + element.text()); if(!isNaN(parseInt(element.text()))) { callback(parseInt(element.text())); } else { callback(undefined); } element.remove(); }); element.on('keypress', event => { if(event.which == JQuery.Key.Enter) element.trigger('focusout'); const text = String.fromCharCode(event.which); if (isNaN(parseInt(text)) && text != "-") event.preventDefault(); if(element.text().length > 7) event.preventDefault(); }); if (window.getSelection) { const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(element[0]); selection.removeAllRanges(); selection.addRange(range); } } trigger_value_assign() { this._listener_value.on_click(undefined); } trigger_grant_assign() { this._listener_grant.on_click(undefined); } } export class InteractionManager { private _listeners: InteractionListener[] = []; private _entered_listeners: InteractionListener[] = []; register_listener(listener: InteractionListener) { this._listeners.push(listener); } remove_listener(listener: InteractionListener) { this._listeners.remove(listener); } process_mouse_move(new_x: number, new_y: number) : { repaint: RepaintMode, cursor: string } { let _entered_listeners: InteractionListener[] = []; for(const listener of this._listeners) { const aabb = listener.region; if(listener.disabled) continue; if(new_x < aabb.x || new_x > aabb.x + aabb.width) continue; if(new_y < aabb.y || new_y > aabb.y + aabb.height) continue; _entered_listeners.push(listener); } let repaint: RepaintMode = RepaintMode.NONE; _entered_listeners.sort((a, b) => (a.region_weight || 0) - (b.region_weight || 0)); for(const listener of this._entered_listeners) { if(listener.on_mouse_leave && _entered_listeners.indexOf(listener) == -1) { let mode = listener.on_mouse_leave(); if(mode == RepaintMode.REPAINT_OBJECT_FULL) { mode = RepaintMode.REPAINT; if(listener.set_full_draw) listener.set_full_draw(); } if(mode > repaint) repaint = mode; } } for(const listener of _entered_listeners) { if(listener.on_mouse_enter && this._entered_listeners.indexOf(listener) == -1) { let mode = listener.on_mouse_enter(); if(mode == RepaintMode.REPAINT_OBJECT_FULL) { mode = RepaintMode.REPAINT; if(listener.set_full_draw) listener.set_full_draw(); } if(mode > repaint) repaint = mode; } } this._entered_listeners = _entered_listeners; let cursor; for(const listener of _entered_listeners) if(typeof(listener.mouse_cursor) === "string") { cursor = listener.mouse_cursor; } return { repaint: repaint, cursor: cursor }; } private process_click_event(x: number, y: number, event: InteractionClickEvent) : RepaintMode { const move_result = this.process_mouse_move(x, y); let repaint: RepaintMode = move_result.repaint; for(const listener of this._entered_listeners) if(listener.on_click) { let mode = listener.on_click(event); if(mode == RepaintMode.REPAINT_OBJECT_FULL){ mode = RepaintMode.REPAINT; if(listener.set_full_draw) listener.set_full_draw(); } if(mode > repaint) repaint = mode; } return repaint; } process_click(x: number, y: number) : RepaintMode { const event: InteractionClickEvent = { consumed: false, type: ClickEventType.SIGNLE, offset_x: x, offset_y: y }; return this.process_click_event(x, y, event); } process_dblclick(x: number, y: number) : RepaintMode { const event: InteractionClickEvent = { consumed: false, type: ClickEventType.DOUBLE, offset_x: x, offset_y: y }; return this.process_click_event(x, y, event); } process_context_menu(js_event: MouseEvent, x: number, y: number) : RepaintMode { const event: InteractionClickEvent = { consumed: js_event.defaultPrevented, type: ClickEventType.CONTEXT_MENU, offset_x: x, offset_y: y }; const result = this.process_click_event(x, y, event); if(event.consumed) js_event.preventDefault(); return result; } } export class PermissionEditor { private static readonly PERMISSION_HEIGHT = 32; private static readonly PERMISSION_GROUP_HEIGHT = 32; readonly grouped_permissions: GroupedPermissions[]; readonly canvas: HTMLCanvasElement; readonly canvas_container: HTMLDivElement; private _max_height: number = 0; private _permission_count: number = 0; private _permission_group_count: number = 0; private _canvas_context: CanvasRenderingContext2D; private _selected_entry: PermissionEntry; private _draw_requested: boolean = false; private _draw_requested_full: boolean = false; private _elements: PermissionGroup[] = []; private _intersect_manager: InteractionManager; private _permission_entry_map: {[key: number]:PermissionEntry} = {}; mouse: { x: number, y: number } = { x: 0, y: 0 }; constructor(permissions: GroupedPermissions[]) { this.grouped_permissions = permissions; this.canvas_container = $.spawn("div") .css("position", "relative") .css("user-select", "none") [0]; this.canvas = $.spawn("canvas")[0]; this.canvas_container.appendChild(this.canvas); this._intersect_manager = new InteractionManager(); this.canvas_container.onmousemove = event => { this.mouse.x = event.pageX; this.mouse.y = event.pageY; const draw = this._intersect_manager.process_mouse_move(event.offsetX, event.offsetY); this.canvas_container.style.cursor = draw.cursor || ""; this._handle_repaint(draw.repaint); }; this.canvas_container.onclick = event => { this._handle_repaint(this._intersect_manager.process_click(event.offsetX, event.offsetY)); }; this.canvas_container.ondblclick = event => { this._handle_repaint(this._intersect_manager.process_dblclick(event.offsetX, event.offsetY)); }; this.canvas_container.oncontextmenu = (event: MouseEvent) => { this._handle_repaint(this._intersect_manager.process_context_menu(event, event.offsetX, event.offsetY)); }; this.initialize(); } private _handle_repaint(mode: RepaintMode) { if(mode == RepaintMode.REPAINT || mode == RepaintMode.REPAINT_FULL) this.request_draw(mode == RepaintMode.REPAINT_FULL); } request_draw(full?: boolean) { this._draw_requested_full = this._draw_requested_full || full; if(this._draw_requested) return; this._draw_requested = true; requestAnimationFrame(() => { this.draw(this._draw_requested_full); }); } draw(full?: boolean) { this._draw_requested = false; this._draw_requested_full = false; /* clear max height */ this.canvas_container.style.overflowY = "shown"; this.canvas_container.style.height = undefined; const max_height = this._max_height; const max_width = this.canvas_container.clientWidth; const update_width = this.canvas.width != max_width; const full_draw = typeof(full) !== "boolean" || full || update_width; if(update_width) { this.canvas.width = max_width; for(const element of this._elements) element.set_width(max_width); } console.log("Drawing%s on %dx%d", full_draw ? " full" : "", max_width, max_height); if(full_draw) this.canvas.height = max_height; const ctx = this._canvas_context; ctx.resetTransform(); if(full_draw) ctx.clearRect(0, 0, max_width, max_height); let sum_height = 0; for(const element of this._elements) { element.draw(ctx, full_draw); const height = element.height(); sum_height += height; ctx.translate(0, height); } this.canvas_container.style.overflowY = "hidden"; this.canvas_container.style.height = sum_height + "px"; } private initialize() { /* setup the canvas */ { const apply_group = (group: GroupedPermissions) => { for(const g of group.children || []) apply_group(g); this._permission_group_count++; this._permission_count += group.permissions.length; }; for(const group of this.grouped_permissions) apply_group(group); this._max_height = this._permission_count * PermissionEditor.PERMISSION_HEIGHT + this._permission_group_count * PermissionEditor.PERMISSION_GROUP_HEIGHT; console.log("%d permissions and %d groups required %d height", this._permission_count, this._permission_group_count, this._max_height); this.canvas.style.width = "100%"; this.canvas.style.flexShrink = "0"; this.canvas_container.style.flexShrink = "0"; this._canvas_context = this.canvas.getContext("2d"); } const font = Math.floor(2/3 * PermissionEntry.HEIGHT) + "px Arial"; const font_checkmark = Math.floor((5/4) * PermissionEntry.HEIGHT) + "px Arial"; const checkbox = { background: "#FFFFFF", background_hovered: "#CCCCCC", background_checked: "#0000AA", background_checked_hovered: "#0000AA77", border: "#000000", checkmark: "#FFFFFF", checkmark_font: font_checkmark }; const input: scheme.TextField = { color: "#000000", font: font, background_hovered: "#CCCCCCCC", background: "#FFFFFF00" }; const color_scheme: scheme.ColorScheme = { group: { name: "black", name_font: font }, permission: { name: "black", name_unset: "#888888", name_font: font, background: "#FFFFFF", background_selected: "#00007788", value: input, value_b: checkbox, granted: input, negate: checkbox, skip: checkbox } }; (window as any).scheme = color_scheme; /* setup elements to draw */ { const process_group = (group: PermissionGroup) => { for(const permission of group._element_permissions.permissions) this._permission_entry_map[permission.permission().id] = permission; for(const g of group._sub_elements) process_group(g); }; for(const group of this.grouped_permissions) { const element = new PermissionGroup(group); element.set_color_scheme(color_scheme); element.set_manager(this); process_group(element); this._elements.push(element); } for(const element of this._elements) { element.initialize(); } } } intercept_manager() { return this._intersect_manager; } set_selected_entry(entry?: PermissionEntry) { if(this._selected_entry === entry) return; if(this._selected_entry) { this._selected_entry.selected = false; this._selected_entry.request_full_draw(); } this._selected_entry = entry; if(this._selected_entry) { this._selected_entry.selected = true; this._selected_entry.request_full_draw(); } this.request_draw(false); } permission_entries() : PermissionEntry[] { return Object.keys(this._permission_entry_map).map(e => this._permission_entry_map[e]); } collapse_all() { for(const group of this._elements) group.collapse_group(); this.request_draw(true); } expend_all() { for(const group of this._elements) group.expend_group(); this.request_draw(true); } } }