1294 lines
No EOL
47 KiB
TypeScript
1294 lines
No EOL
47 KiB
TypeScript
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;
|
|
private _icon_image: HTMLImageElement | undefined;
|
|
|
|
on_icon_select?: (current_id: number) => Promise<number>;
|
|
on_context_menu?: (x: number, y: number) => any;
|
|
on_grant_change?: () => any;
|
|
on_change?: () => any;
|
|
|
|
constructor(permission: PermissionInfo) {
|
|
super();
|
|
this._permission = permission;
|
|
}
|
|
|
|
set_icon_id_image(image: HTMLImageElement | undefined) {
|
|
if(this._icon_image === image)
|
|
return;
|
|
this._icon_image = image;
|
|
if(image) {
|
|
image.height = 16;
|
|
image.width = 16;
|
|
}
|
|
}
|
|
|
|
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.width = PermissionEntry.CHECKBOX_HEIGHT;
|
|
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 if(this._permission.name === "i_icon_id" && this._icon_image) {
|
|
this._listener_value.region.x = original_x + w;
|
|
this._listener_value.region.y = original_y;
|
|
this._listener_value.region.width = PermissionEntry.CHECKBOX_HEIGHT;
|
|
|
|
this._draw_icon_field(ctx, this.colors.permission.value_b, w, 0, PermissionEntry.COLUMN_VALUE, this.flag_value_hovered, this._icon_image);
|
|
} else {
|
|
this._listener_value.region.width = PermissionEntry.COLUMN_VALUE;
|
|
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_icon_field(ctx: CanvasRenderingContext2D, scheme: scheme.CheckBox, x: number, y: number, width: number, hovered: boolean, image: HTMLImageElement) {
|
|
const line = ctx.lineWidth;
|
|
ctx.lineWidth = 2;
|
|
ctx.fillStyle = scheme.border;
|
|
ctx.strokeRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2);
|
|
ctx.lineWidth = line;
|
|
|
|
ctx.fillStyle = hovered ? scheme.background_hovered : scheme.background;
|
|
ctx.fillRect(x + 1, y + 1, PermissionEntry.HEIGHT - 2, PermissionEntry.HEIGHT - 2);
|
|
|
|
const center_y = y + PermissionEntry.HEIGHT / 2;
|
|
const center_x = x + PermissionEntry.HEIGHT / 2;
|
|
ctx.drawImage(image, center_x - image.width / 2, center_y - image.height / 2);
|
|
}
|
|
|
|
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 if(this._permission.name === "i_icon_id") {
|
|
this.on_icon_select(this.value).then(value => {
|
|
this.value = value;
|
|
if(this.on_change)
|
|
this.on_change();
|
|
}).catch(error => {
|
|
console.warn(tr("Failed to select icon: %o"), error);
|
|
})
|
|
} 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 == KeyCode.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);
|
|
}
|
|
}
|
|
} |