Skip to content
Snippets Groups Projects
layout.js 21.93 KiB
/* $Id$ */
/* Window-style layout library for Synchronet 3.15+ 
 * 
 * NOTE: frame.js is required to use this library
 * if a Frame() object is not supplied to a Layout()
 * on creation, one will be created internally, however
 * it is recommended that you create an external frame object
 * as a basis for any other libraries that use frames.
 * 
 * NOTE: There is no real need to manually draw the contents of a layout
 * as long as either Frame.cycle() or Layout.cycle() are part of your
 * loop. If you use "blocking" operations with a layout or frame, 
 * then use Layout.draw() or Frame.draw() to update your views.
 * 
 * Layout methods:
 * 
 * 	addView(title,x,y,width,height) 
 * 	getViewByName(title)
 *	open()
 *	close()
 * 	draw()
 * 	cycle()
 * 	getcmd(cmd)
 * 
 * Layout properties:
 * 
 * 	x
 * 	y
 * 	width
 * 	height
 * 	frame
 * 	colors = {
 * 		border_bg,
 * 		border_fg,
 * 		title_bg,
 *		title_fg, 
 *		tab_bg,
 *		tab_fg,
 *		view_bg,
 *		view_fg
 *  }
 * 
 * LayoutView methods:
 * 
 * 	addTab(title,type,content)
 * 	getTabByTitle(title)
 *	open()
 *	close()
 * 	draw()
 * 	cycle()
 * 	getcmd(cmd)
 * 
 * LayoutView properties:
 * 
 * 	x
 * 	y
 * 	width
 * 	height
 * 	title
 * 	frame
 * 	colors (a reference to the parent layout's "colors" property)
 *  
 * LayoutView toggles: 
 * 
 * NOTE: changing these values will resize and reinitialize 
 * the view's components
 * 
 * 	show_border = (true|false)
 * 	show_title = (true|false)
 * 	show_tabs = (true|false)
 * 
 * NOTE: when adding a tab with the content parameter specified, 
 * the content must correspond to one of the predefined object types
 * listed below:
 *
 * 	content: Tree(), 	type: "tree"
 *	content: Frame(), 	type: "graphic"
 *	content: JSONChat(),type: "chat"
 *	content: Graphic(),	type: "graphic"
 * 
 * NOTE: default event handlers are included in the open, close, and activation
 * methods of most layout objects:
 *	
 *	<obj>.onKeyPress[key] - run if defined and specified key is pressed
 *
 *	NOTE: onKeyPress handler available for all layout objects. if a handler is defined
 *	it should return true (if the key was handled) or false (if the key was not handled) 
 *	in order to stop the layout from attempting to pass it to other objects.
 *
 *	Layout.onOpen - run when layout.open() is called
 *	Layout.onClose - run when layout.close() is called
 *	LayoutView.onOpen - run when view.open() is called
 *	LayoutView.onClose - run when view.close() is called
 *	LayoutView.onEnter - run when view.active is set to true
 *	LayoutView.onExit - run when view.active is set to false
 *	ViewTab.onEnter - run when tab.active is set to true
 *	ViewTab.onExit - run when tab.active is set to false
 *	
 */

if(js.global.getColor == undefined)
	js.global.load(js.global,"funclib.js");
if(js.global.Frame == undefined)
	js.global.load(js.global,"frame.js");

/* main layout object, intended to contain child layout view objects */ 
function Layout(frame) {
	
	/* private properties */
	var properties={
		views:[],
		index:0
	};
	var frames={
		main:undefined
	}
	
	/* read-only properties */
	this.__defineGetter__("x",function() {
		return frames.main.x;
	});
	this.__defineGetter__("y",function() {
		return frames.main.y;
	});
	this.__defineGetter__("width",function() {
		return frames.main.width;
	});
	this.__defineGetter__("height",function() {
		return frames.main.height;
	});
	this.__defineGetter__("frame",function() {
		return frames.main;
	});
	
	/* settings */
	this.__defineGetter__("current",function() {
		return properties.views[properties.index];
	});
	this.__defineSetter__("current",function(title_or_index) {
		if(title_or_index instanceof LayoutView)
			title_or_index = title_or_index.title;
		if(isNaN(title_or_index)) {
			for(var v=0;v<properties.views.length;v++) {
				if(properties.views[v].title.toUpperCase() == title_or_index.toUpperCase()) {
					properties.views[properties.index].active=false;
					properties.index = v;
					properties.views[properties.index].active=true;
					return true;
				}
			}
		}
		else if(properties.views[title_or_index]) {
			properties.views[properties.index].active=false;
			properties.index = title_or_index;
			properties.views[properties.index].active=true;
			return true;
		}
		return false;
	});
	
	/* public properties */
	this.colors={
		title_bg:BG_BLUE,
		title_fg:YELLOW,
		inactive_title_bg:BG_BLACK,
		inactive_title_fg:LIGHTGRAY,
		tab_bg:BG_GREEN,
		tab_fg:LIGHTGREEN,
		inactive_tab_bg:BG_LIGHTGRAY,
		inactive_tab_fg:BLACK,
		view_bg:BG_BLACK,
		view_fg:GREEN,
		border_bg:BG_BLACK,
		border_fg:LIGHTGRAY
	};

	/* public methods */
	this.open=function() {
		this.current=0;
		frames.main.open();
		for each(var v in properties.views)
			v.open();
		if(typeof this.onOpen == "function") 
			this.onOpen();
	}
	this.close=function() {
		frames.main.close();
		if(typeof this.onClose == "function") 
			this.onClose();
	}
	this.addView=function(title,x,y,w,h) {
		var f = new Frame(x,y,w,h,undefined,frames.main);
		var v = new LayoutView(title,f,this);
		properties.views.push(v);
		return v;
	}
	this.delView=function(title_or_index) {
		var view = false;
		if(title_or_index instanceof LayoutView)
			title_or_index = title_or_index.title;
		if(isNaN(title_or_index)) {
			for(var v=0;v<properties.views.length;v++) {
				if(properties.views[v].title.toUpperCase() == title_or_index.toUpperCase()) {
					view = properties.views[v];
					properties.views.splice(v,1);
				}
			}
		}
		else if(properties.views[title_or_index]) {
			tab = properties.views[title_or_index];
			properties.views.splice(title_or_index,1);
		}
		if(view) {
			view.frame.delete();
			while(!properties.views[properties.index] && properties.index > 0)
				properties.index--;
			if(this.current)
				this.current.active=true;
		}
		return view;
	}
	this.draw=function() {
		for each(var view in properties.views)
			view.draw();
	}
	this.cycle=function() {
		for each(var v in properties.views)
			v.cycle();
	}
	this.getViewByName=function(title) {
		for each(var v in properties.views) {
			if(v.title.toUpperCase() == title.toUpperCase())
				return v;
		}
	}
	
	/* default command handler for views */
	this.handleKeyEvent = function(cmd) {
		if(this.current) {
			if(this.current.handleKeyEvent(cmd))
				return true;
		}
		if(this.onKeyPress[cmd]) {
			return this.onKeyPress[cmd]();
		}
		return false;
	}
	this.getcmd=function(cmd) {
		if(!cmd) 
			return false;
		if(this.handleKeyEvent(cmd))
			return true;
		switch(cmd) {
		case '\x09': 
			if(properties.views.length > 1) 
				nextView();
			return true;
		default:
			if(properties.views.length > 0)
				return properties.views[properties.index].getcmd(cmd);
			break;
		}
		return false;
	}

	/* event handlers */
	this.onOpen;
	this.onClose;
	this.onKeyPress = {};
	
	/* constructor */
	function nextView() {
		var start = properties.index++;
		while(start !== properties.index) {
			if(properties.index >= properties.views.length)
				properties.index = 0;
			if(properties.views[properties.index].can_focus) {
				properties.views[start].active = false;
				properties.views[properties.index].active = true;
				break;
			}
			properties.index++;
		}
	}
	function init(frame) {
		if(frame instanceof Frame)
			frames.main = frame;
		else
			frames.main = new Frame();
	}
	init.call(this,frame);
}

/* layout view object, holds view tab objects
 * NOTE: this can be instantiated independently from Layout, 
 * but must be supplied a frame with the desired dimensions,
 * normally instantiated via Layout.addView() */
function LayoutView(title,frame,parent) {
	/* private properties */
	var properties={
		title:undefined,
		index:0,
		tabs:[],
	}
	var relations={
		parent:undefined
	}
	var settings={
		show_title:true,
		show_tabs:true,
		show_border:true,
		can_focus:true,
		active:false
	}
	var frames={
		main:undefined,
		border:undefined,
		title:undefined,
		tabs:undefined,
		content:undefined
	};
	
	/* read-only properties */
	this.__defineGetter__("x",function() {
		return frames.main.x;
	});
	this.__defineGetter__("y",function() {
		return frames.main.y;
	});
	this.__defineGetter__("width",function() {
		return frames.main.width;
	});
	this.__defineGetter__("height",function() {
		return frames.main.height;
	});
	this.__defineGetter__("colors",function() {
		return relations.parent.colors;
	});
	this.__defineGetter__("title",function() {
		return properties.title;
	});
	this.__defineGetter__("frame",function() {
		return frames.main;
	});
	this.__defineGetter__("tabs",function() {
		return properties.tabs;
	});
	
	/* settings */
	this.__defineGetter__("show_title",function() {
		return settings.show_title;
	});
	this.__defineSetter__("show_title",function(bool) {
		if(typeof bool !== "boolean")
			return false;
		settings.show_title = bool;
		updateViewFrames();
		return true;
	});
	this.__defineGetter__("show_tabs",function() {
		return settings.show_tabs;
	});
	this.__defineSetter__("show_tabs",function(bool) {
		if(typeof bool !== "boolean")
			return false;
		settings.show_tabs = bool;
		updateViewFrames();
		return true;
	});
	this.__defineGetter__("show_border",function() {
		return settings.show_border;
	});
	this.__defineSetter__("show_border",function(bool) {
		if(typeof bool !== "boolean")
			return false;
		settings.show_border = bool;
		updateViewFrames();
		return true;
	});
	this.__defineGetter__("current",function() {
		return properties.tabs[properties.index];
	});
	this.__defineSetter__("current",function(title_or_index) {
		if(title_or_index instanceof ViewTab)
			title_or_index = title_or_index.title;
		if(isNaN(title_or_index)) {
			for(var t=0;t<properties.tabs.length;t++) {
				if(properties.tabs[t].title.toUpperCase() == title_or_index.toUpperCase()) {
					properties.tabs[properties.index].active=false;
					properties.index = t;
					properties.tabs[properties.index].active=true;
					setTabs();
					return true;
				}
			}
		}
		else if(properties.tabs[title_or_index]) {
			properties.tabs[properties.index].active=false;
			properties.index = title_or_index;
			properties.tabs[properties.index].active=true;
			setTabs();
			return true;
		}
		return false;
	});
	this.__defineGetter__("can_focus",function() {
		return settings.can_focus;
	});
	this.__defineSetter__("can_focus",function(bool) {
		if(typeof bool !== "boolean")
			return false;
		settings.can_focus = bool;
		return true;
	});
	this.__defineSetter__("active",function(bool) {
		if(typeof bool !== "boolean" || settings.active == bool) 
			return false;
		settings.active = bool;
		if(settings.active) {
			frames.title.attr = this.colors.title_bg+this.colors.title_fg;
			setTitle();
			if(typeof this.onEnter == "function") 
				this.onEnter();
		}
		else {
			frames.title.attr = this.colors.inactive_title_bg + this.colors.inactive_title_fg;
			setTitle();
			if(typeof this.onExit == "function") 
				this.onExit();
		}
		return true;
	});
	
	/* public methods */
	this.open=function() {
		for each(var t in properties.tabs) {
			if(typeof t.open == "function")
				t.open();
		}
		setViewFrames();
		this.current=0;
		this.frame.open();
		if(typeof this.onOpen == "function") 
			this.onOpen();
	}
	this.close=function() {
		for each(var t in properties.tabs) {
			if(typeof t.close == "function")
				t.close();
		}
		if(typeof this.onClose == "function") 
			this.onClose();
	}
	this.draw=function() {
		frames.main.draw();
	}
	this.cycle=function() {
		for each(var t in properties.tabs)
			if(typeof t.cycle == "function")
				t.cycle();
	}
	this.addTab=function(title,type,content) {
		/* use this view's location and dimensions as 
		starting point for new tabs */
		var x = frames.content.x;
		var y = frames.content.y;
		var w = frames.content.width;
		var h = frames.content.height;
		var attr = this.colors.view_bg + this.colors.view_fg;
		var f = new Frame(x,y,w,h,attr,frames.content);
		var t = new ViewTab(title,f,this);	
		f.open();
		setContent(t,type,content);
		properties.tabs.push(t);
		if(this.current) {
			//this.current.active=true;
			this.current = properties.index;
		}
		setTabs();
		return t;
	}
	this.getTab=function(title_or_index) {
		if(isNaN(title_or_index)) {
			for each(var t in properties.tabs) {
				if(t.title.toUpperCase() == title_or_index.toUpperCase())
					return t;
			}
		}
		else {
			return properties.tabs[title_or_index];
		}
	}
	this.delTab=function(title_or_index) {
		var tab = false;
		if(title_or_index instanceof ViewTab)
			title_or_index = title_or_index.title;
		if(isNaN(title_or_index)) {
			for(var t=0;t<properties.tabs.length;t++) {
				if(properties.tabs[t].title.toUpperCase() == title_or_index.toUpperCase()) {
					tab = properties.tabs[t];
					properties.tabs.splice(t,1);
				}
			}
		}
		else if(properties.tabs[title_or_index]) {
			tab = properties.tabs[title_or_index];
			properties.tabs.splice(title_or_index,1);
		}
		if(tab) {
			tab.frame.delete();
			while(!properties.tabs[properties.index] && properties.index > 0)
				properties.index--;
			if(this.current)
				this.current.active=true;
			setTabs();
		}
		return tab;
	}
	this.handleKeyEvent = function(cmd) {
		if(this.current) {
			if(this.current.handleKeyEvent(cmd))
				return true;
		}
		if(this.onKeyPress[cmd]) {
			return this.onKeyPress[cmd]();
		}
		return false;
	}
	this.getcmd=function(cmd) {
		if(!cmd) 
			return false;
		if(this.handleKeyEvent(cmd))
			return true;
		switch(cmd) {
		case KEY_LEFT:
			if(properties.tabs.length > 1) {
				properties.tabs[properties.index].active = false;
				properties.index--;
				if(properties.index < 0)
					properties.index = properties.tabs.length-1;
				properties.tabs[properties.index].active = true;
				setTabs();
				return true;
			}
			break;
		case KEY_RIGHT:
			if(properties.tabs.length > 1) {
				properties.tabs[properties.index].active = false;
				properties.index++;
				if(properties.index >= properties.tabs.length)
					properties.index = 0;
				properties.tabs[properties.index].active = true;
				setTabs();
				return true;
			}
			break;
		default:
			if(properties.tabs.length > 0) {
				var t = properties.tabs[properties.index];
				if(t.onKeyPress[cmd] && t.onKeyPress[cmd]())
					return true;
				return properties.tabs[properties.index].getcmd(cmd);
			}
			break;
		}
	}
	
	/* event handlers */
	this.onEnter;
	this.onExit;
	this.onOpen;
	this.onClose;
	this.onKeyPress = {};
	
	/* private methods */
	function setContent(tab,type,content) {
		if(!type)
			return false;
		switch(type.toUpperCase()) {
		case "TREE":
			if(typeof Tree == "undefined")
				load("tree.js");
			if(content instanceof Tree)  {
				tab.tree = content;
				tab.tree.frame = tab.frame;
				tab.tree.refresh();
			}
			else {
				tab.tree = new Tree(tab.frame);
				tab.tree.refresh();
			}
			tab.getcmd = function(cmd) {
				return this.tree.getcmd.call(tab.tree,cmd);
			}
			tab.hotkeys = true;
			break;
		case "CHAT":
			if(typeof JSONChat == "undefined")
				load("json-chat.js");
			if(content instanceof JSONChat) 
				tab.chat = content;
			else
				tab.chat = new JSONChat();
			tab.getcmd = function(cmd) {
				switch(cmd) {
				case KEY_HOME:
					return this.frame.pageup();
				case KEY_END:
					return this.frame.pagedown();
				case KEY_UP:
					return this.frame.scroll(0,-1);
				case KEY_DOWN:
					return this.frame.scroll(0,1);
				default: 
					this.frame.scrollTo(1,this.frame.data_height - this.frame.height);
					return this.chat.submit.call(this.chat,this.title,cmd);
				}
			}
			tab.cycle = function() {
				var chan = this.chat.channels[this.title.toUpperCase()];
				if(!chan || chan.messages.length == 0) 
					return false;
				this.frame.scrollTo(1,this.frame.data_height - this.frame.height);
				while(chan.messages.length > 0) {
					var msg = chan.messages.shift();
					var str = "";
					if(msg.nick)
						var str = getColor(this.chat.settings.NICK_COLOR) + msg.nick.name + "\1n: " + 
						getColor(this.chat.settings.TEXT_COLOR) + msg.str;
					else
						var str = getColor(this.chat.settings.NOTICE_COLOR) + msg.str;
					this.frame.putmsg(str + "\r\n");
				}
			}
			//tab.chat.chatView = tab.parent;
			properties.chat = tab.chat;
			tab.frame.lf_strict = false;
			tab.frame.word_wrap = true;
			tab.hotkeys = false;
			break;
		case "FRAME":
			tab.getcmd = function(cmd) {
				switch(cmd) {
				case KEY_UP:
					return this.frame.scroll(0,-1);
				case KEY_DOWN:
					return this.frame.scroll(0,1);
				case KEY_LEFT:
					return this.frame.scroll(-1,1);
				case KEY_RIGHT:
					return this.frame.scroll(1,0);
				}
			}
			tab.hotkeys = true;
			tab.frame.v_scroll=true;
			tab.frame.h_scroll=true;
			tab.frame.open();
			break;
		case "GRAPHIC":
			//ToDo
			break;
		default:
			return false;
		}
		return true;
	}
	function updateViewFrames() {
		var x = frames.main.x;
		var y = frames.main.y;
		var w = frames.main.width;
		var h = frames.main.height;
		var colors = relations.parent.colors;
		
		/* delete all existing content frames */
		if(frames.border)
			frames.border.delete();
		if(frames.title) 
			frames.title.delete();
		if(frames.tabs) 
			frames.tabs.delete();
		if(frames.content) 
			frames.content.delete();
		/* recreate any frames set to be displayed */
		if(settings.show_border) {
			var attr = colors.border_bg + colors.border_fg;
			frames.border = new Frame(x,y,w,h,attr,frames.main);
			x++;
			y++;
			w-=2;
			h-=2;
		}
		if(settings.show_title) {
			var attr = colors.inactive_title_bg + colors.inactive_title_fg;
			frames.title = new Frame(x,y,w,1,attr,frames.main);
			y++;
			h--;
		}
		if(settings.show_tabs) {
			var attr = colors.inactive_tab_bg + colors.inactive_tab_fg;
			frames.tabs = new Frame(x,y,w,1,attr,frames.main);
			y++;
			h--;
		}

		frames.content = new Frame(x,y,w,h,undefined,frames.main); 
	}
	function setViewFrames() {
		if(settings.show_border)
			setBorder();
		if(settings.show_title)
			setTitle();
		if(settings.show_tabs) 
			setTabs();
	}
	function setBorder() {
		var f = frames.border;
		var h = "\xC4";
		var v = "\xB3";
		var tl = "\xDA";
		var tr = "\xBF";
		var bl = "\xC0";
		var br = "\xD9";
		var hline = "";
		for(var x=0;x<frames.main.width-2;x++)
			hline+=h;
		var vline = format(v + "%*s" + v,frames.main.width-2,"");
		f.home();
		f.putmsg(tl + hline + tr + "\r\n");
		for(var y=0;y<frames.main.height-2;y++)
			f.putmsg(vline + "\r\n");
		f.putmsg(bl + hline + br);
	}
	function setTabs() {
		var f = frames.tabs;
		var w = f.width;
		var max_width=w-2;
		var left_arrow = "<";
		var right_arrow = ">";
		var colors = relations.parent.colors;
		var inact_color = "\1n" + getColor(colors.inactive_tab_bg) + getColor(colors.inactive_tab_fg);
		var act_color = "\1n" + getColor(colors.tab_bg) + getColor(colors.tab_fg);
		var arrow_color = "\1r\1h";

		/* populate string with current tab highlight */
		var tab_str="";
		if(properties.tabs.length > 0) {
			tab_str+=act_color + properties.tabs[properties.index].title + inact_color;
			
			/* if there are items below the current index */
			var t=properties.index-1;
			while(t >= 0 && console.strlen(tab_str) < max_width) {
				if(console.strlen(tab_str)+properties.tabs[t].title.length+1 > max_width) 
					break;
				tab_str=properties.tabs[t].title + " " + tab_str;
				t--;
			}
			if(t >= 0) 
				left_arrow = arrow_color + left_arrow;
				
			/* if there are items above the current index */
			t=properties.index+1;
			while(t < properties.tabs.length && console.strlen(tab_str) < max_width) {
				if(console.strlen(tab_str)+properties.tabs[t].title.length+1 > max_width) 
					break;
				tab_str=
					tab_str + " " +	properties.tabs[t].title;
				t++;
			}
			if(t < properties.tabs.length) 
				right_arrow = arrow_color + right_arrow;
		}
		tab_str+=format("%-*s",max_width-console.strlen(tab_str),"");
		f.home();
		f.putmsg(left_arrow + "\1n" + tab_str + "\1n" + right_arrow);
	}
	function setTitle() {
		frames.title.clear();
		frames.title.center(properties.title);
	}
	
	/* constructor */
	function init(title,frame,parent) {
		properties.title = title;
		if(frame instanceof Frame)
			frames.main = frame;
		else
			frames.main = new Frame();
		relations.parent = parent;
		updateViewFrames();
		setViewFrames();
	}
	init.call(this,title,frame,parent);
}

/* view tab object, meant to inhabit a layout view.
 * will generally occupy the same space as other view tabs
 * within a given view, cannot be effectively instantiated
 * on its own, but rather through LayoutView.addTab() */
function ViewTab(title,frame,parent) {
	/* private properties */
	var properties={
		title:undefined,
		content:undefined
	}
	var settings={
		active:false,
		hotkeys:true
	}
	var frames={
		main:undefined
	}
	var relations={
		parent:undefined
	}

	/* read-only properties */
	this.__defineGetter__("x",function() {
		return frames.main.x;
	});
	this.__defineGetter__("y",function() {
		return frames.main.y;
	});
	this.__defineGetter__("width",function() {
		return frames.main.width;
	});
	this.__defineGetter__("height",function() {
		return frames.main.height;
	});
	this.__defineGetter__("colors",function() {
		return relations.parent.colors;
	});
	this.__defineGetter__("title",function() {
		return properties.title;
	});
	this.__defineGetter__("frame",function() {
		return frames.main;
	});
	this.__defineGetter__("parent",function() {	
		return relations.parent;
	});
	
	/* settings */
	this.__defineSetter__("active",function(bool) {
		if(typeof bool !== "boolean" || settings.active == bool) 
			return false;
		settings.active = bool;
		if(settings.active) {
			frames.main.top();
			if(typeof this.onEnter == "function") 
				this.onEnter();
		}
		else {
			if(typeof this.onExit == "function") 
				this.onExit();
		}
		return true;
	});
	this.__defineGetter__("hotkeys",function() {
		return settings.hotkeys;
	});
	this.__defineSetter__("hotkeys",function(hotkeys) {
		settings.hotkeys = hotkeys;
	});

	/* default command handler 
	 * (can be overridden for specialized tabs) */
	this.handleKeyEvent = function(cmd) {
		if(this.onKeyPress[cmd]) {
			return this.onKeyPress[cmd]();
		}
		return false;
	}
	this.getcmd = function(cmd) {
		if(!cmd)
			return false;
		frames.main.putmsg(cmd);
		return true;
	}
	
	/* event handlers */
	this.onEnter;
	this.onExit;
	this.onKeyPress = {};
	
	/* constructor */
	function init(title,frame,parent) {
		properties.title = title;
		frames.main = frame;
		relations.parent = parent;
	}
	init.call(this,title,frame,parent);
}