cc14717930
Separate the window titlebar from the main background area, so the transparency of each can be set independently. From Henning Stalkhe.
506 lines
13 KiB
Text
506 lines
13 KiB
Text
var gui = {
|
|
widgets: {},
|
|
focused_window: nil,
|
|
region_highlight: nil,
|
|
|
|
# Window/dialog stacking order
|
|
STACK_INDEX: {
|
|
"default": 0,
|
|
"always-on-top": 1,
|
|
"tooltip": 2
|
|
}
|
|
};
|
|
|
|
var gui_dir = getprop("/sim/fg-root") ~ "/Nasal/canvas/gui/";
|
|
var loadGUIFile = func(file) io.load_nasal(gui_dir ~ file, "canvas");
|
|
var loadWidget = func(name) loadGUIFile("widgets/" ~ name ~ ".nas");
|
|
var loadDialog = func(name) loadGUIFile("dialogs/" ~ name ~ ".nas");
|
|
|
|
loadGUIFile("Config.nas");
|
|
loadGUIFile("Style.nas");
|
|
loadGUIFile("Widget.nas");
|
|
loadGUIFile("styles/DefaultStyle.nas");
|
|
loadWidget("Button");
|
|
loadWidget("CheckBox");
|
|
loadWidget("Label");
|
|
loadWidget("LineEdit");
|
|
loadWidget("ScrollArea");
|
|
loadDialog("InputDialog");
|
|
loadDialog("MessageBox");
|
|
|
|
var style = DefaultStyle.new("AmbianceClassic", "Humanity");
|
|
var WindowButton = {
|
|
new: func(parent, name)
|
|
{
|
|
var m = {
|
|
parents: [WindowButton, gui.widgets.Button.new(parent, nil, {"flat": 1})],
|
|
_name: name
|
|
};
|
|
m._focus_policy = m.NoFocus;
|
|
m._setView({_root: parent.createChild("image", "WindowButton-" ~ name)});
|
|
return m;
|
|
},
|
|
# protected:
|
|
_onStateChange: func
|
|
{
|
|
var file = style._dir_decoration ~ "/" ~ me._name;
|
|
var window_focus = me._windowFocus();
|
|
file ~= window_focus ? "_focused" : "_unfocused";
|
|
|
|
if( me._down )
|
|
file ~= "_pressed";
|
|
else if( me._hover )
|
|
file ~= "_prelight";
|
|
else if( window_focus )
|
|
file ~= "_normal";
|
|
|
|
me._view._root.set("src", file ~ ".png");
|
|
}
|
|
};
|
|
|
|
var Window = {
|
|
# Constructor
|
|
#
|
|
# @param size ([width, height])
|
|
new: func(size, type = nil, id = nil)
|
|
{
|
|
var ghost = _newWindowGhost(id);
|
|
var m = {
|
|
parents: [Window, PropertyElement, ghost],
|
|
_ghost: ghost,
|
|
_node: props.wrapNode(ghost._node_ghost),
|
|
_focused: 0,
|
|
_widgets: [],
|
|
_frame_width: 4,
|
|
_title_bar_height: 25,
|
|
};
|
|
|
|
m.setInt("content-size[0]", size[0]);
|
|
m.setInt("content-size[1]", size[1]);
|
|
|
|
# TODO better default position
|
|
m.move(0,0);
|
|
m.setFocus();
|
|
|
|
# arg = [child, listener_node, mode, is_child_event]
|
|
setlistener(m._node, func m._propCallback(arg[0], arg[2]), 0, 2);
|
|
if( type )
|
|
m.set("type", type);
|
|
|
|
return m;
|
|
},
|
|
# Destructor
|
|
del: func
|
|
{
|
|
me.clearFocus();
|
|
|
|
if( me["_canvas"] != nil )
|
|
{
|
|
var placements = me._canvas._node.getChildren("placement");
|
|
# Do not remove canvas if other placements exist
|
|
if( size(placements) > 1 )
|
|
foreach(var p; placements)
|
|
{
|
|
if( p.getValue("type") == "window"
|
|
and p.getValue("id") == me.get("id") )
|
|
p.remove();
|
|
}
|
|
else
|
|
me._canvas.del();
|
|
me._canvas = nil;
|
|
}
|
|
|
|
me._node.remove();
|
|
me._node = nil;
|
|
},
|
|
setTitle: func(title)
|
|
{
|
|
return me.set("title", title);
|
|
},
|
|
# Create the canvas to be used for this Window
|
|
#
|
|
# @return The new canvas
|
|
createCanvas: func()
|
|
{
|
|
var size = [
|
|
me.get("content-size[0]"),
|
|
me.get("content-size[1]")
|
|
];
|
|
|
|
me._canvas = new({
|
|
size: [size[0], size[1]],
|
|
view: size,
|
|
placement: {
|
|
type: "window",
|
|
id: me.get("id")
|
|
},
|
|
|
|
# Standard alpha blending
|
|
"blend-source-rgb": "src-alpha",
|
|
"blend-destination-rgb": "one-minus-src-alpha",
|
|
|
|
# Just keep current alpha (TODO allow using rgb textures instead of rgba?)
|
|
"blend-source-alpha": "zero",
|
|
"blend-destination-alpha": "one"
|
|
});
|
|
|
|
me._canvas._focused_widget = nil;
|
|
me._canvas.data("focused", me._focused);
|
|
me._canvas.addEventListener("mousedown", func me.raise());
|
|
|
|
return me._canvas;
|
|
},
|
|
# Set an existing canvas to be used for this Window
|
|
setCanvas: func(canvas_)
|
|
{
|
|
if( ghosttype(canvas_) != "Canvas" )
|
|
return debug.warn("Not a Canvas");
|
|
|
|
canvas_.addPlacement({type: "window", "id": me.get("id")});
|
|
me['_canvas'] = canvas_;
|
|
|
|
canvas_._focused_widget = nil;
|
|
canvas_.data("focused", me._focused);
|
|
|
|
# prevent resizing if canvas is placed from somewhere else
|
|
me.onResize = nil;
|
|
},
|
|
# Get the displayed canvas
|
|
getCanvas: func(create = 0)
|
|
{
|
|
if( me['_canvas'] == nil and create )
|
|
me.createCanvas();
|
|
|
|
return me['_canvas'];
|
|
},
|
|
getCanvasDecoration: func()
|
|
{
|
|
return wrapCanvas(me._getCanvasDecoration());
|
|
},
|
|
setLayout: func(l)
|
|
{
|
|
if( me['_canvas'] == nil )
|
|
me.createCanvas();
|
|
|
|
me._canvas.update(); # Ensure placement is applied
|
|
me._ghost.setLayout(l);
|
|
return me;
|
|
},
|
|
#
|
|
setFocus: func
|
|
{
|
|
if( me._focused )
|
|
return me;
|
|
|
|
if( gui.focused_window != nil )
|
|
gui.focused_window.clearFocus();
|
|
|
|
me._focused = 1;
|
|
# me.onFocusIn();
|
|
me._onStateChange();
|
|
gui.focused_window = me;
|
|
setInputFocus(me);
|
|
return me;
|
|
},
|
|
#
|
|
clearFocus: func
|
|
{
|
|
if( !me._focused )
|
|
return me;
|
|
|
|
me._focused = 0;
|
|
# me.onFocusOut();
|
|
me._onStateChange();
|
|
gui.focused_window = nil;
|
|
setInputFocus(nil);
|
|
return me;
|
|
},
|
|
setPosition: func
|
|
{
|
|
if( size(arg) == 1 )
|
|
var arg = arg[0];
|
|
var (x, y) = arg;
|
|
|
|
me.setInt("tf/t[0]", x);
|
|
me.setInt("tf/t[1]", y);
|
|
},
|
|
setSize: func
|
|
{
|
|
if( size(arg) == 1 )
|
|
var arg = arg[0];
|
|
var (w, h) = arg;
|
|
|
|
me.set("content-size[0]", w);
|
|
me.set("content-size[1]", h);
|
|
|
|
if( me.onResize != nil )
|
|
me.onResize();
|
|
|
|
return me;
|
|
},
|
|
move: func
|
|
{
|
|
if( size(arg) == 1 )
|
|
var arg = arg[0];
|
|
var (x, y) = arg;
|
|
|
|
me.setInt("tf/t[0]", me.get("tf/t[0]", 10) + x);
|
|
me.setInt("tf/t[1]", me.get("tf/t[1]", 30) + y);
|
|
},
|
|
# Raise to top of window stack
|
|
raise: func()
|
|
{
|
|
# on writing the z-index the window always is moved to the top of all other
|
|
# windows with the same z-index.
|
|
me.setInt("z-index", me.get("z-index", gui.STACK_INDEX["default"]));
|
|
|
|
me.setFocus();
|
|
},
|
|
onResize: func()
|
|
{
|
|
if( me['_canvas'] == nil )
|
|
return;
|
|
|
|
for(var i = 0; i < 2; i += 1)
|
|
{
|
|
var size = me.get("content-size[" ~ i ~ "]");
|
|
me._canvas.set("size[" ~ i ~ "]", size);
|
|
me._canvas.set("view[" ~ i ~ "]", size);
|
|
}
|
|
},
|
|
# protected:
|
|
_onStateChange: func
|
|
{
|
|
var event = canvas.CustomEvent.new("wm.focus-" ~ (me._focused ? "in" : "out"));
|
|
|
|
if( me._getCanvasDecoration() != nil )
|
|
{
|
|
# Stronger shadow for focused windows
|
|
me.getCanvasDecoration()
|
|
.set("image[1]/fill", me._focused ? "#000000" : "rgba(0,0,0,0.5)");
|
|
|
|
var suffix = me._focused ? "" : "-unfocused";
|
|
me._title_bar_bg.set("fill", style.getColor("title" ~ suffix));
|
|
me._frame.set("stroke", style.getColor("title" ~ suffix));
|
|
me._title.set( "fill", style.getColor("title-text" ~ suffix));
|
|
me._top_line.set( "stroke", style.getColor("title-highlight" ~ suffix));
|
|
|
|
me.getCanvasDecoration()
|
|
.data("focused", me._focused)
|
|
.dispatchEvent(event);
|
|
}
|
|
|
|
if( me.getCanvas() != nil )
|
|
me.getCanvas()
|
|
.data("focused", me._focused)
|
|
.dispatchEvent(event);
|
|
},
|
|
# private:
|
|
_propCallback: func(child, mode)
|
|
{
|
|
if( !me._node.equals(child.getParent()) )
|
|
return;
|
|
var name = child.getName();
|
|
|
|
# support for CSS like position: absolute; with right and/or bottom margin
|
|
if( name == "right" )
|
|
me._handlePositionAbsolute(child, mode, name, 0);
|
|
else if( name == "bottom" )
|
|
me._handlePositionAbsolute(child, mode, name, 1);
|
|
|
|
# update decoration on type change
|
|
else if( name == "type" )
|
|
{
|
|
if( mode == 0 )
|
|
settimer(func me._updateDecoration(), 0, 1);
|
|
}
|
|
|
|
else if( name.starts_with("resize-") )
|
|
{
|
|
if( mode == 0 )
|
|
me._handleResize(child, name);
|
|
}
|
|
else if( name == "size" )
|
|
{
|
|
if( mode == 0 )
|
|
me._resizeDecoration();
|
|
}
|
|
},
|
|
_handlePositionAbsolute: func(child, mode, name, index)
|
|
{
|
|
# mode
|
|
# -1 child removed
|
|
# 0 value changed
|
|
# 1 child added
|
|
|
|
if( mode == 0 )
|
|
me._updatePos(index, name);
|
|
else if( mode == 1 )
|
|
me["_listener_" ~ name] = [
|
|
setlistener
|
|
(
|
|
"/sim/gui/canvas/size[" ~ index ~ "]",
|
|
func me._updatePos(index, name)
|
|
),
|
|
setlistener
|
|
(
|
|
me._node.getNode("content-size[" ~ index ~ "]"),
|
|
func me._updatePos(index, name)
|
|
)
|
|
];
|
|
else if( mode == -1 )
|
|
for(var i = 0; i < 2; i += 1)
|
|
removelistener(me["_listener_" ~ name][i]);
|
|
},
|
|
_updatePos: func(index, name)
|
|
{
|
|
me.setInt
|
|
(
|
|
"tf/t[" ~ index ~ "]",
|
|
getprop("/sim/gui/canvas/size[" ~ index ~ "]")
|
|
- me.get(name)
|
|
- me.get("content-size[" ~ index ~ "]")
|
|
);
|
|
},
|
|
_handleResize: func(child, name)
|
|
{
|
|
var is_status = name == "resize-status";
|
|
if( !is_status and !me["_resize"] )
|
|
return;
|
|
|
|
var min_size = [75, 100];
|
|
|
|
var x = me.get("tf/t[0]");
|
|
var y = me.get("tf/t[1]");
|
|
var old_size = [me.get("size[0]"), me.get("size[1]")];
|
|
|
|
var l = x + math.min(me.get("resize-left"), old_size[0] - min_size[0]);
|
|
var t = y + math.min(me.get("resize-top"), old_size[1] - min_size[1]);
|
|
var r = x + math.max(me.get("resize-right"), min_size[0]);
|
|
var b = y + math.max(me.get("resize-bottom"), min_size[1]);
|
|
|
|
if( is_status )
|
|
{
|
|
me._resize = child.getValue();
|
|
|
|
if( me._resize and gui.region_highlight == nil )
|
|
gui.region_highlight =
|
|
getDesktop().createChild("path", "highlight")
|
|
.set("stroke", "#ffa500")
|
|
.set("stroke-width", 2)
|
|
.set("fill", "rgba(255, 165, 0, 0.15)")
|
|
.set("z-index", 100);
|
|
else if( !me._resize and gui.region_highlight != nil )
|
|
{
|
|
gui.region_highlight.hide();
|
|
me.setPosition(l, t);
|
|
me.setSize
|
|
(
|
|
me.get("content-size[0]") + (r - l) - old_size[0],
|
|
me.get("content-size[1]") + (b - t) - old_size[1],
|
|
);
|
|
if( me.onResize != nil )
|
|
me.onResize();
|
|
return;
|
|
}
|
|
}
|
|
else if( !me["_resize"] )
|
|
return;
|
|
|
|
gui.region_highlight.reset()
|
|
.moveTo(l, t)
|
|
.horizTo(r)
|
|
.vertTo(b)
|
|
.horizTo(l)
|
|
.close()
|
|
.update()
|
|
.show();
|
|
},
|
|
_updateDecoration: func()
|
|
{
|
|
var border_radius = 9;
|
|
me.set("decoration-border", "25 1 1");
|
|
me.set("shadow-inset", int((1 - math.cos(45 * D2R)) * border_radius + 0.5));
|
|
me.set("shadow-radius", 5);
|
|
me.setBool("update", 1);
|
|
|
|
var canvas_deco = me.getCanvasDecoration();
|
|
canvas_deco.addEventListener("mousedown", func me.raise());
|
|
canvas_deco.set("blend-source-rgb", "src-alpha");
|
|
canvas_deco.set("blend-destination-rgb", "one-minus-src-alpha");
|
|
canvas_deco.set("blend-source-alpha", "one");
|
|
canvas_deco.set("blend-destination-alpha", "one");
|
|
|
|
var group_deco = canvas_deco.getGroup("decoration");
|
|
var title_bar = group_deco.createChild("group", "title_bar");
|
|
me._title_bar_bg = title_bar.createChild("path");
|
|
me._top_line = title_bar.createChild("path", "top-line");
|
|
me._frame = title_bar.createChild("path");
|
|
me._frame.set("fill", "none");
|
|
me._frame.set("stroke-width", me._frame_width);
|
|
|
|
# close icon
|
|
var x = 10;
|
|
var y = 3;
|
|
var w = 19;
|
|
var h = 19;
|
|
|
|
var button_close = WindowButton.new(title_bar, "close")
|
|
.move(x, y);
|
|
button_close.listen("clicked", func me.del());
|
|
|
|
# title
|
|
me._title = title_bar.createChild("text", "title")
|
|
.set("alignment", "left-center")
|
|
.set("character-size", 14)
|
|
.set("font", "LiberationFonts/LiberationSans-Bold.ttf")
|
|
.setTranslation( int(x + 1.5 * w + 0.5),
|
|
int(y + 0.5 * h + 0.5) );
|
|
|
|
var title = me.get("title", "Canvas Dialog");
|
|
me._node.getNode("title", 1).alias(me._title._node.getPath() ~ "/text");
|
|
me.set("title", title);
|
|
|
|
title_bar.addEventListener("drag", func(e) me.move(e.deltaX, e.deltaY));
|
|
|
|
me._resizeDecoration();
|
|
me._onStateChange();
|
|
},
|
|
_resizeDecoration: func()
|
|
{
|
|
if( me["_title_bar_bg"] == nil )
|
|
return;
|
|
|
|
var border_radius = 9;
|
|
me._title_bar_bg
|
|
.reset()
|
|
.rect( 0, 0,
|
|
me.get("size[0]"), me._title_bar_height,
|
|
{"border-top-radius": border_radius} );
|
|
me._frame
|
|
.reset()
|
|
.rect( 0, 0,
|
|
me.get("size[0]"), me.get("size[1]"),
|
|
{"border-top-radius": border_radius} );
|
|
|
|
me._top_line
|
|
.reset()
|
|
.moveTo(border_radius - 2, 2)
|
|
.lineTo(me.get("size[0]") - border_radius + 2, 2);
|
|
}
|
|
};
|
|
|
|
# Clear focus on click outside any window
|
|
getDesktop().addEventListener("mousedown", func {
|
|
if( gui.focused_window != nil )
|
|
gui.focused_window.clearFocus();
|
|
});
|
|
|
|
# Provide old 'Dialog' for backwards compatiblity (should be removed for 3.0)
|
|
var Dialog = {
|
|
new: func(size, type = nil, id = nil)
|
|
{
|
|
debug.warn("'canvas.Dialog' is deprectated! (use canvas.Window instead)");
|
|
return Window.new(size, type, id);
|
|
}
|
|
};
|