/* Do not remove the headers from this file! see /USAGE for more info. */ /* ** Stanadard Menuing facilities. ** Jul-4-95. John Viega (rust@virginia.edu) -- Created ** ** ** To-do for this: ** make it so you can have a single item that you can choose that ** doesn't get displayed in the menu. ** Convert strings like titles, etc... to allow functionals. ** move completion callback into a closure. ** finish new_prompt() support / dont_complete... remove old completion ** code. ** add a security check in the input callback. */ #include <menu.h> #include <mudlib.h> inherit M_INPUT; class menu_item { string description; mixed action; int disabled; mixed choice_name; // should be a string if the user sets it. int prompt_after_action; function constraint; // A seperator is just a line of text, and not something that // can be chosen. Therefore, action, choice_name, and // prompt_after_selection are meaningless if this is non-zero. int seperator; } class menu { // Items should only ever be MENU_ITEM*'s or string*'s, and // should be of uniform type. If this is of type string, // The menu won't display options. // And if items is a string*, you'd better have a no_match_function, // because there won't be any MENU_ITEMS to match against. mixed *items; mixed title; mixed prompt; int allow_enter; function no_match_function; int num_columns; int dont_complete; string *current_choices; } protected void goto_menu(MENU); protected void display_current_menu(); protected void prompt_then_return(); MENU current_menu, previous_menu; int need_refreshing; protected void remove() { destruct(); } varargs protected MENU new_menu(string title, string prompt, int allow_enter, function no_match_function) { MENU new_menu; new_menu = new(MENU); new_menu->items = ({}); new_menu->title = title; new_menu->prompt = prompt; new_menu->allow_enter = allow_enter; new_menu->no_match_function = no_match_function; return new_menu; } varargs protected MENU new_prompt(string prompt, function callback, string* completions) { MENU new_menu; new_menu = new(MENU); new_menu->prompt = prompt; new_menu->no_match_function = callback; new_menu->items = completions ? completions : ({}); return new_menu; } varargs protected MENU_ITEM new_seperator (string description, function constraint) { MENU_ITEM new_menu_item; new_menu_item = new(MENU_ITEM); new_menu_item->description = description; new_menu_item->constraint = constraint; new_menu_item->seperator = 1; return new_menu_item; } varargs protected MENU_ITEM new_menu_item(string description, mixed action, string choice_name, int prompt, function constraint) { MENU_ITEM new_menu_item; new_menu_item = new(MENU_ITEM); new_menu_item->description = description; new_menu_item->action = action; new_menu_item->choice_name = choice_name; new_menu_item->prompt_after_action = prompt; new_menu_item->constraint = constraint; return new_menu_item; } protected void add_menu_item(MENU menu, MENU_ITEM menu_item) { menu->items += ({ menu_item }); } protected void set_menu_items(MENU menu, MENU_ITEM* menu_items) { menu->items = menu_items; } protected void set_menu_title(MENU menu, string title) { menu->title = title; } protected void set_menu_prompt(MENU menu, mixed prompt) { if(!(stringp(prompt) || functionp(prompt))) { error("Bad type arg 2 to set_menu_prompt"); return; } menu->prompt = prompt; } protected void allow_empty_selection(MENU menu) { menu->allow_enter = 1; } protected void disallow_empty_selection(MENU menu) { menu->allow_enter = 0; } protected void set_no_match_function(MENU menu, function f) { menu->no_match_function = f; } protected void set_number_of_columns(MENU menu, int n) { menu->num_columns = n; } protected void disable_menu_item(MENU_ITEM item) { item->disabled = 1; } protected void enable_menu_item(MENU_ITEM item) { item->disabled = 0; } protected void set_menu_item_description(MENU_ITEM item, string description) { item->description = description; } protected void set_menu_item_action(MENU_ITEM item, mixed action) { // Should type check here, but I can't figure out how // to typecheck the class. item->action = action; } protected void set_menu_item_choice_name (MENU_ITEM item, string choice_name) { item->choice_name = choice_name; } protected void constrain_menu_item (MENU_ITEM item, function f) { item->constraint = f; } // This variable is kind of a hack... when an action is taken // (assuming that action is a function) this menu becomes the // current menu. This was done so that I could integrate completion // menus as a real menu without having to go through and modify // every single action private MENU menu_after_selection; protected void new_parse_menu_input(string input) { string* matches; int i; MENU_ITEM matched_item; MENU completion_menu; input = trim_spaces(input); if(input == "" && !current_menu->allow_enter) { return; } if((i=member_array(input, current_menu->current_choices)) != -1) { matches = ({ input }); } else { if(current_menu->dont_complete) { if(functionp(current_menu->no_match_function)) { evaluate (current_menu->no_match_function, input); } else { write("Invalid selection.\n"); } return; } matches = M_REGEX->insensitive_regexp(current_menu->current_choices, M_GLOB->translate(input, 1)); } switch(sizeof(matches)){ case 0: write("Invalid selection.\n"); return; case 1: if(!sizeof(current_menu->items) || stringp(current_menu->items[0])) { evaluate(current_menu->no_match_function, matches[0]); if(menu_after_selection) { goto_menu(menu_after_selection); menu_after_selection = 0; } return; } matched_item = filter_array(current_menu->items, (: intp(((MENU_ITEM)$1)->choice_name) ? sprintf("%d",((MENU_ITEM)$1)->choice_name) == $2 : ((MENU_ITEM)$1)->choice_name == $2 :), matches[0])[0]; if (functionp(matched_item->action)) { if(menu_after_selection) { goto_menu(menu_after_selection); menu_after_selection = 0; } evaluate(matched_item->action,input); need_refreshing = 1; if(matched_item->prompt_after_action) prompt_then_return(); return; } goto_menu (matched_item->action); return; default: completion_menu = new_menu("Choose one by number:\n" "---------------------\n"); set_menu_items(completion_menu, filter_array(current_menu->items, (: intp(((MENU_ITEM)$1)->choice_name) ? member_array(sprintf("%d", ((MENU_ITEM)$1)->choice_name),$2) != -1 : member_array(((MENU_ITEM)$1)->choice_name, $2) != -1 :), matches)); add_menu_item(completion_menu, new_menu_item("Return to previous menu", current_menu)); goto_menu(completion_menu); menu_after_selection = current_menu; } } protected void parse_menu_input(mixed input) { int counter; MENU_ITEM item; mixed action; if(input == -1) remove(); input = trim_spaces(input); if(input == "" && !current_menu->allow_enter) return; foreach (item in current_menu->items) { // Quick and sleazy way of knowing we're a prompt... if(stringp(item)) break; if(item->disabled || item->seperator || (item->constraint && !evaluate(item->constraint))) continue; if ((!stringp(item->choice_name) && sprintf("%d",++counter) == input) || input == item->choice_name) { action = item->action; if(functionp(action)) { evaluate(action, input); need_refreshing = 1; if(item->prompt_after_action) prompt_then_return(); return; } goto_menu (action); return; } } if( functionp(current_menu->no_match_function) ) evaluate (current_menu->no_match_function, input); else write("Invalid selection.\n"); } protected string get_current_prompt() { mixed prompt; prompt = current_menu->prompt; if(need_refreshing) display_current_menu(); if(!prompt) { // Build a smart default prompt. This info and constraint info // could easily be cached.... // the only thing not smart about this prompt is that it assumes // your choices are one character if you provide them yourself. // I did that because comma seperating choices is ugly. string s = ""; string c; MENU_ITEM item; int counter; foreach(item in current_menu->items) { if(item->disabled || item->seperator || (item->constraint && !evaluate(item->constraint))) continue; if(!c=item->choice_name) { counter++; continue; } s += c; } switch(counter) { case 0: s = sprintf("[%s] ",s); break; case 1: s = (s == "" ? "[1] " : sprintf("[1,%s] ", s)); break; default: s = (s == "" ? sprintf("[1-%d] ", counter) : sprintf("[1-%d,%s] ", counter, s)); break; } return s; } return stringp(prompt) ? prompt : evaluate(prompt); } protected void init_menu_application(MENU toplevel) { modal_push((: parse_menu_input :), (: get_current_prompt :)); current_menu = toplevel; goto_menu(toplevel); } protected void quit_menu_application() { modal_pop(); destruct(this_object()); } protected void goto_menu(MENU m) { previous_menu = current_menu; current_menu = m; display_current_menu(); } protected void goto_menu_silently(MENU m) { previous_menu = current_menu; current_menu = m; } protected void goto_previous_menu() { MENU swap; swap = current_menu; current_menu = previous_menu; previous_menu = swap; } protected void display_current_menu() { int leftwidth; int rightwidth; int num_columns, i, j; int counter; string output; MENU_ITEM this_item; need_refreshing = 0; if(!sizeof(current_menu->items) && !current_menu->no_match_function) { write("###Not implemented yet.\n"); current_menu = previous_menu; need_refreshing = 1; prompt_then_return(); return; } if(!sizeof(current_menu->items) || stringp((current_menu->items)[0])) { current_menu->current_choices = current_menu->items; return; } rightwidth = max(map(filter_array(current_menu->items, (: !(((MENU_ITEM)$1)->seperator) :)), (: strlen(((MENU_ITEM)$1)->description) :))); // This stuff is getting as ugly as Amylaar closures =P leftwidth = max(map(filter_array(current_menu->items, (: stringp(((MENU_ITEM)$1) -> choice_name) :)), (: strlen(((MENU_ITEM)$1)->choice_name) :)) + ({3})); output = current_menu->title + "\n"; if(!(num_columns=current_menu->num_columns)) num_columns = 78 / (leftwidth + rightwidth + 6); if(!num_columns) num_columns = 1; // Build this each time, and pass it on to the input handler, // because the menu may change.... current_menu->current_choices = ({}); for(i = 0, j = 0; i < sizeof(current_menu->items); i++, j++) { this_item = current_menu->items[i]; if(this_item->disabled || (this_item->constraint && !evaluate(this_item->constraint))) continue; if(this_item->seperator) { j = -1; // we want it to be 0 on the next loop, and it's // going to get incremented at the start of the // next loop output += sprintf("%s\n", this_item->description); continue; } if(!stringp(this_item->choice_name)) { output += sprintf("%="+leftwidth+"d) %-"+rightwidth+"s", ++counter, this_item->description); current_menu->current_choices += ({ sprintf("%d",counter) }); // Note, this will still get recalculated every time, since // we're setting it to an int, and not a string, but that's // what we want since we want the menus to be dynamic... // We set this for convenience of finding this menu item // without having to recalculate all this crap when it comes // time to process the choice. this_item->choice_name = counter; } else { output += sprintf("%="+leftwidth+"s) %-"+rightwidth+"s", this_item->choice_name, this_item->description); current_menu->current_choices += ({ this_item->choice_name }); } if(j%num_columns == (num_columns-1)) output += "\n"; else output += " "; } output += "\n"; more(output); } private void finish_completion( function completion_callback, string *cur_choices, string input ) { int i; if(input == "") return; if(input == "r") { goto_menu(current_menu); return; } if((i = to_int(input)) < 1 || i > sizeof(cur_choices)) { write("Invalid choice.\n"); return; } evaluate(completion_callback, cur_choices[i-1]); need_refreshing = 1; } varargs protected void complete_choice(string input, string* choices, function f) { string* matches; int i; string output; if(input) matches = M_REGEX->insensitive_regexp(choices, M_GLOB->translate(input, 1)); else matches = choices; ZBUG(sizeof(matches)); switch(sizeof(matches)) { case 0: write("Invalid selection.\n"); break; case 1: evaluate(f, matches[0]); break; default: output = "Select choice by number\n" "-----------------------\n"; for(i=1; i<= sizeof(matches); i++) output += sprintf("%=3d) %s\n", i, matches[i-1]); modal_simple((: finish_completion, f, matches :), "[Enter number or r to return to menu] "); more(output); break; } } protected void get_input_then_call(function thencall, string prompt) { input_one_arg(prompt, thencall); } protected void prompt_then_return() { /* just ignore the input... */ modal_simple((: 0 :), "[Hit enter to return to menu] "); } //### probably should be protected too void quit_if_cr(string input) { if(input == "") { quit_menu_application(); return; } }