From b55638b622ec00fbd480cb01726271dec96f88fd Mon Sep 17 00:00:00 2001 From: friarzen Date: Sat, 17 Mar 2018 20:50:51 +0000 Subject: [PATCH 01/29] Alpha webclient split interface support --- .../static/webclient/css/webclient.css | 167 +++++++++------ .../static/webclient/js/splithandler.js | 77 +++++++ .../static/webclient/js/webclient_gui.js | 198 +++++++++++++----- .../webclient/templates/webclient/base.html | 113 +++++----- .../templates/webclient/webclient.html | 57 ++++- 5 files changed, 434 insertions(+), 178 deletions(-) create mode 100644 evennia/web/webclient/static/webclient/js/splithandler.js diff --git a/evennia/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css index 1c94a1f9fd..94344386a1 100644 --- a/evennia/web/webclient/static/webclient/css/webclient.css +++ b/evennia/web/webclient/static/webclient/css/webclient.css @@ -8,10 +8,11 @@ --- */ /* Overall element look */ -html, body, #clientwrapper { height: 100% } +html, body { + height: 100%; + width: 100%; +} body { - margin: 0; - padding: 0; background: #000; color: #ccc; font-size: .9em; @@ -19,6 +20,12 @@ body { line-height: 1.6em; overflow: hidden; } +@media screen and (max-width: 480px) { + body { + font-size: .5rem; + line-height: .7rem; + } +} a:link, a:visited { color: inherit; } @@ -74,93 +81,75 @@ div {margin:0px;} } /* Style specific classes corresponding to formatted, narative text. */ - +.wrapper { + height: 100%; +} /* Container surrounding entire client */ -#wrapper { - position: relative; - height: 100% +#clientwrapper { + height: 100%; } /* Main scrolling message area */ + #messagewindow { - position: absolute; - overflow: auto; - padding: 1em; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - top: 0; - left: 0; - right: 0; - bottom: 70px; + overflow-y: auto; + overflow-x: hidden; + overflow-wrap: break-word; } -/* Input area containing input field and button */ -#inputform { - position: absolute; - width: 100%; - padding: 0; - bottom: 0; - margin: 0; - padding-bottom: 10px; - border-top: 1px solid #555; -} - -#inputcontrol { - width: 100%; - padding: 0; +#messagewindow { + overflow-y: auto; + overflow-x: hidden; + overflow-wrap: break-word; } /* Input field */ -#inputfield, #inputsend, #inputsizer { - display: block; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - height: 50px; +#inputfield, #inputsizer { + height: 100%; background: #000; color: #fff; - padding: 0 .45em; - font-size: 1.1em; + padding: 0 .45rem; + font-size: 1.1rem; font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace; -} - -#inputfield, #inputsizer { - float: left; - width: 95%; - border: 0; resize: none; - line-height: normal; +} +#inputsend { + height: 100%; +} +#inputcontrol { + height: 100%; } #inputfield:focus { - outline: 0; -} - -#inputsizer { - margin-left: -9999px; -} - -/* Input 'send' button */ -#inputsend { - float: right; - width: 3%; - max-width: 25px; - margin-right: 10px; - border: 0; - background: #555; } /* prompt area above input field */ -#prompt { - margin-top: 10px; - padding: 0 .45em; +.prompt { + max-height: 3rem; +} + +.splitbutton { + position: absolute; + right: 1%; + top: 1%; + z-index: 1; + width: 2rem; + height: 2rem; + font-size: 2rem; + color: #a6a6a6; + background-color: transparent; + border: 0px; +} + +.splitbutton:hover { + color: white; + cursor: pointer; } #optionsbutton { - width: 40px; - font-size: 20px; + width: 2rem; + font-size: 2rem; color: #a6a6a6; background-color: transparent; border: 0px; @@ -173,8 +162,8 @@ div {margin:0px;} #toolbar { position: fixed; - top: 0; - right: 5px; + top: .5rem; + right: .5rem; z-index: 1; } @@ -248,6 +237,48 @@ div {margin:0px;} text-decoration: none; cursor: pointer; } +.gutter.gutter-vertical { + cursor: row-resize; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=') +} + +.gutter.gutter-horizontal { + cursor: col-resize; + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==') +} + +.split { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + + overflow-y: auto; + overflow-x: hidden; +} + +.content { + border: 1px solid #C0C0C0; + box-shadow: inset 0 1px 2px #e4e4e4; + background-color: black; + padding: 1rem; +} +@media screen and (max-width: 480px) { + .content { + padding: .5rem; + } +} + +.gutter { + background-color: grey; + + background-repeat: no-repeat; + background-position: 50%; +} + +.split.split-horizontal, .gutter.gutter-horizontal { + height: 100%; + float: left; +} /* XTERM256 colors */ diff --git a/evennia/web/webclient/static/webclient/js/splithandler.js b/evennia/web/webclient/static/webclient/js/splithandler.js new file mode 100644 index 0000000000..56890009e5 --- /dev/null +++ b/evennia/web/webclient/static/webclient/js/splithandler.js @@ -0,0 +1,77 @@ +// Use split.js to create a basic ui +var SplitHandler = (function () { + var num_splits = 0; + var split_panes = {}; + + var set_pane_types = function(splitpane, types) { + split_panes[splitpane]['types'] = types; + } + + var dynamic_split = function(splitpane, direction, update_method1, update_method2) { + var first = ++num_splits; + var second = ++num_splits; + + var first_div = $( '
' ) + var first_sub = $( '
' ) + var second_div = $( '
' ) + var second_sub = $( '
' ) + + // check to see if this pane contains the primary message window. + contents = $('#'+splitpane).contents(); + if( contents ) { + // it does, so move it to the first new div (TODO -- selectable between first/second?) + contents.appendTo(first_sub); + } + + first_div.append( first_sub ); + second_div.append( second_sub ); + + // update the split_panes array to remove this split + delete( split_panes[splitpane] ); + + // now vaporize the current split_N-sub placeholder and create two new panes. + $('#'+splitpane).parent().append(first_div); + $('#'+splitpane).parent().append(second_div); + $('#'+splitpane).remove(); + + // And split + Split(['#split_'+first,'#split_'+second], { + direction: direction, + sizes: [50,50], + gutterSize: 4, + minSize: [50,50], + }); + + // store our new splits for future splits/uses by the main UI. + split_panes['split_'+first +'-sub'] = { 'types': [], 'update_method': update_method1 }; + split_panes['split_'+second+'-sub'] = { 'types': [], 'update_method': update_method2 }; + } + + + var init = function(settings) { + //change Mustache tags to ruby-style (Django gets mad otherwise) + var customTags = [ '<%', '%>' ]; + Mustache.tags = customTags; + + var input_template = $('#input-template').html(); + Mustache.parse(input_template); + + Split(['#main','#input'], { + direction: 'vertical', + sizes: [90,10], + gutterSize: 4, + minSize: [50,50], + }); + + var input_render = Mustache.render(input_template); + $('[data-role-input]').html(input_render); + console.log("SplitHandler initialized"); + } + + return { + init: init, + set_pane_types: set_pane_types, + dynamic_split: dynamic_split, + split_panes: split_panes, + } +})(); diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index 57d9b0b7c0..b4e5168769 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -17,6 +17,10 @@ var options = {}; + +var known_types = new Array(); + known_types.push('help'); + // // GUI Elements // @@ -106,6 +110,7 @@ function togglePopup(dialogname, content) { // Grab text from inputline and send to Evennia function doSendText() { + console.log("sending text"); if (!Evennia.isConnected()) { var reconnect = confirm("Not currently connected. Reconnect?"); if (reconnect) { @@ -158,6 +163,10 @@ function onKeydown (event) { var code = event.which; var history_entry = null; var inputfield = $("#inputfield"); + if (code === 9) { + return; + } + inputfield.focus(); if (code === 13) { // Enter key sends text @@ -205,64 +214,68 @@ function onKeyPress (event) { } var resizeInputField = function () { - var min_height = 50; - var max_height = 300; - var prev_text_len = 0; + return function() { + var wrapper = $("#inputform") + var input = $("#inputcontrol") + var prompt = $("#prompt") - // Check to see if we should change the height of the input area - return function () { - var inputfield = $("#inputfield"); - var scrollh = inputfield.prop("scrollHeight"); - var clienth = inputfield.prop("clientHeight"); - var newh = 0; - var curr_text_len = inputfield.val().length; - - if (scrollh > clienth && scrollh <= max_height) { - // Need to make it bigger - newh = scrollh; - } - else if (curr_text_len < prev_text_len) { - // There is less text in the field; try to make it smaller - // To avoid repaints, we draw the text in an offscreen element and - // determine its dimensions. - var sizer = $('#inputsizer') - .css("width", inputfield.prop("clientWidth")) - .text(inputfield.val()); - newh = sizer.prop("scrollHeight"); - } - - if (newh != 0) { - newh = Math.min(newh, max_height); - if (clienth != newh) { - inputfield.css("height", newh + "px"); - doWindowResize(); - } - } - prev_text_len = curr_text_len; + input.height(wrapper.height() - (input.offset().top - wrapper.offset().top)); } }(); // Handle resizing of client function doWindowResize() { - var formh = $('#inputform').outerHeight(true); - var message_scrollh = $("#messagewindow").prop("scrollHeight"); - $("#messagewindow") - .css({"bottom": formh}) // leave space for the input form - .scrollTop(message_scrollh); // keep the output window scrolled to the bottom + resizeInputField(); + var resizable = $("[data-update-append]"); + var parents = resizable.closest(".split") + parents.animate({ + scrollTop: parents.prop("scrollHeight") + }, 0); } // Handle text coming from the server function onText(args, kwargs) { - // append message to previous ones, then scroll so latest is at - // the bottom. Send 'cls' kwarg to modify the output class. - var renderto = "main"; - if (kwargs["type"] == "help") { - if (("helppopup" in options) && (options["helppopup"])) { - renderto = "#helpdialog"; + var use_default_pane = true; + + if ( kwargs && 'type' in kwargs ) { + var msgtype = kwargs['type']; + if ( ! known_types.includes(msgtype) ) { + // this is a new output type that can be mapped to panes + console.log('detected new output type: ' + msgtype) + known_types.push(msgtype); + } + + if ( msgtype == 'help' ) { + if (("helppopup" in options) && (options["helppopup"])) { + openPopup("#helpdialog", args[0]); + return; + } + // fall through to the default output + + } else { + // pass this message to each pane that has this msgtype mapped + if( SplitHandler ) { + for ( var key in SplitHandler.split_panes) { + var pane = SplitHandler.split_panes[key]; + console.log(pane); + // is this message type mapped to this pane? + if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { + // yes, so append/replace this pane's inner div with this message + if ( pane['update_method'] == 'replace' ) { + $('#'+key).html(args[0]) + } else { + $('#'+key).append(args[0]).animate({ scrollTop: document.getElementById("#"+key).scrollHeight }, 0); + } + // record sending this message to a pane, no need to update the default div + use_default_pane = false; + } + } + } } } - if (renderto == "main") { + // append message to default pane, then scroll so latest is at the bottom. + if(use_default_pane) { var mwin = $("#messagewindow"); var cls = kwargs == null ? 'out' : kwargs['cls']; mwin.append("
" + args[0] + "
"); @@ -271,8 +284,6 @@ function onText(args, kwargs) { }, 0); onNewLine(args[0], null); - } else { - openPopup(renderto, args[0]); } } @@ -377,7 +388,10 @@ function onNewLine(text, originator) { document.title = "(" + unread + ") " + originalTitle; if ("Notification" in window){ if (("notification_popup" in options) && (options["notification_popup"])) { - Notification.requestPermission().then(function(result) { + // There is a Promise-based API for this, but it’s not supported + // in Safari and some older browsers: + // https://developer.mozilla.org/en-US/docs/Web/API/Notification/requestPermission#Browser_compatibility + Notification.requestPermission(function(result) { if(result === "granted") { var title = originalTitle === "" ? "Evennia" : originalTitle; var options = { @@ -427,6 +441,81 @@ function doStartDragDialog(event) { $(document).bind("mouseup", undrag); } + +function onSplitDialogClose() { + var pane = $("input[name=pane]:checked").attr("value"); + var direction = $("input[name=direction]:checked").attr("value"); + var flow1 = $("input[name=flow1]:checked").attr("value"); + var flow2 = $("input[name=flow2]:checked").attr("value"); + + SplitHandler.dynamic_split( pane, direction, flow1, flow2 ); + + closePopup("#splitdialog"); +} + + +function onSplitDialog() { + var dialog = $("#splitdialogcontent"); + dialog.empty(); + + dialog.append("

Split?

"); + dialog.append(' top/bottom
'); + dialog.append(' side-by-side
'); + + dialog.append("

Split Which Pane?

"); + for ( var pane in SplitHandler.split_panes ) { + dialog.append(''+ pane +'
'); + } + + dialog.append("

New First Pane Flow

"); + dialog.append('append
'); + dialog.append('replace
'); + + dialog.append("

New Second Pane Flow

"); + dialog.append('append
'); + dialog.append('replace
'); + + dialog.append('
Split It
'); + + $("#splitclose").bind("click", onSplitDialogClose); + + openPopup("#splitdialog"); +} + +function onPaneControlDialogClose() { + var pane = $("input[name=pane]:checked").attr("value"); + + var types = new Array; + $('#splitdialogcontent input[type=checkbox]:checked').each(function() { + types.push( $(this).attr('value') ); + }); + + SplitHandler.set_pane_types( pane, types ); + + closePopup("#splitdialog"); +} + +function onPaneControlDialog() { + var dialog = $("#splitdialogcontent"); + dialog.empty(); + + dialog.append("

Set Which Pane?

"); + for ( var pane in SplitHandler.split_panes ) { + dialog.append(''+ pane +'
'); + } + + dialog.append("

Which content types?

"); + for ( var type in known_types ) { + dialog.append(''+ known_types[type] +'
'); + } + + dialog.append('
Make It So
'); + + $("#paneclose").bind("click", onPaneControlDialogClose); + + openPopup("#splitdialog"); +} + // // Register Events // @@ -434,6 +523,16 @@ function doStartDragDialog(event) { // Event when client finishes loading $(document).ready(function() { + if( SplitHandler ) { + SplitHandler.init(); + SplitHandler.split_panes['main-sub'] = {'types': ['help'], 'update_method': 'replace'}; + $("#splitbutton").bind("click", onSplitDialog); + $("#panebutton").bind("click", onPaneControlDialog); + } else { + $("#splitbutton").hide(); + $("#panebutton").hide(); + } + if ("Notification" in window) { Notification.requestPermission(); } @@ -450,7 +549,7 @@ $(document).ready(function() { //$(document).on("visibilitychange", onVisibilityChange); - $("#inputfield").bind("resize", doWindowResize) + $("[data-role-input]").bind("resize", doWindowResize) .keypress(onKeyPress) .bind("paste", resizeInputField) .bind("cut", resizeInputField); @@ -503,6 +602,7 @@ $(document).ready(function() { }, 60000*3 ); + console.log("Completed GUI setup"); }); diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index f5f47b230f..f31e4c89f1 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -14,55 +14,16 @@ JQuery available. + + + + + - - - - {% block jquery_import %} - - {% endblock %} - - - - - - - - - {% block guilib_import %} - - {% endblock %} - - + @@ -79,17 +40,69 @@ JQuery available. web browser supporting javascript.

This error could also be due to not being able to access the online jQuery javascript library.

- -
-
+
{% block client %} {% endblock %}
+ + + {% block jquery_import %} + + {% endblock %} + + + + + + + + + + + + + + + + + + {% block guilib_import %} + + {% endblock %} + + + + + {% block scripts %} + {% endblock %} diff --git a/evennia/web/webclient/templates/webclient/webclient.html b/evennia/web/webclient/templates/webclient/webclient.html index 1c641bffb0..2b138cb8bd 100644 --- a/evennia/web/webclient/templates/webclient/webclient.html +++ b/evennia/web/webclient/templates/webclient/webclient.html @@ -8,20 +8,30 @@ {% block client %} +
+ + + +
-
-
- -
-
-
-
-
- - + +
+
+
+
+
+ + +
+
+ + +
+
Split Pane×
+
+
-
@@ -47,4 +57,29 @@
+ + + + + + +{% endblock %} +{% block scripts %} {% endblock %} From f95f66633dc28f307441c3ff8ff20746f1836a63 Mon Sep 17 00:00:00 2001 From: friarzen Date: Sat, 17 Mar 2018 21:50:11 +0000 Subject: [PATCH 02/29] Example of how to tag msg() with a type --- evennia/commands/default/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 2f4c51a227..aef5309d32 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -71,7 +71,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS): target = caller.search(self.args) if not target: return - self.msg(caller.at_look(target)) + self.msg((caller.at_look(target), {'type':'look'}), options=None) class CmdNick(COMMAND_DEFAULT_CLASS): From e003bac7452676a6d1023e1f8ed42f85bde88445 Mon Sep 17 00:00:00 2001 From: friarzen Date: Sun, 18 Mar 2018 00:33:08 +0000 Subject: [PATCH 03/29] move the initial settings for the main split where it belongs --- evennia/web/webclient/static/webclient/js/splithandler.js | 2 ++ evennia/web/webclient/static/webclient/js/webclient_gui.js | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/web/webclient/static/webclient/js/splithandler.js b/evennia/web/webclient/static/webclient/js/splithandler.js index 56890009e5..7ba8e45e17 100644 --- a/evennia/web/webclient/static/webclient/js/splithandler.js +++ b/evennia/web/webclient/static/webclient/js/splithandler.js @@ -63,6 +63,8 @@ var SplitHandler = (function () { minSize: [50,50], }); + split_panes['main-sub'] = {'types': [], 'update_method': 'append'}; + var input_render = Mustache.render(input_template); $('[data-role-input]').html(input_render); console.log("SplitHandler initialized"); diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index b4e5168769..d07239039f 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -525,7 +525,6 @@ $(document).ready(function() { if( SplitHandler ) { SplitHandler.init(); - SplitHandler.split_panes['main-sub'] = {'types': ['help'], 'update_method': 'replace'}; $("#splitbutton").bind("click", onSplitDialog); $("#panebutton").bind("click", onPaneControlDialog); } else { From b364807b4bb5d8af2d25aca7d49d919492d0e015 Mon Sep 17 00:00:00 2001 From: friarzen Date: Sun, 18 Mar 2018 00:45:23 +0000 Subject: [PATCH 04/29] Make the initial login 'look' match CmdLook --- evennia/objects/objects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f583570707..a588af0000 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -1762,7 +1762,7 @@ class DefaultCharacter(DefaultObject): """ self.msg("\nYou become |c%s|n.\n" % self.name) - self.msg(self.at_look(self.location)) + self.msg((self.at_look(self.location), {'type':'look'}), options = None) def message(obj, from_obj): obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj) From b1ab4dd667bbf6e05a8a05be88c99c24a1a65eeb Mon Sep 17 00:00:00 2001 From: friarzen Date: Mon, 19 Mar 2018 00:59:28 +0000 Subject: [PATCH 05/29] Fix append scrolling -- needs more testing --- .../web/webclient/static/webclient/js/webclient_gui.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index d07239039f..4189e026fb 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -257,14 +257,15 @@ function onText(args, kwargs) { if( SplitHandler ) { for ( var key in SplitHandler.split_panes) { var pane = SplitHandler.split_panes[key]; - console.log(pane); // is this message type mapped to this pane? if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { // yes, so append/replace this pane's inner div with this message if ( pane['update_method'] == 'replace' ) { $('#'+key).html(args[0]) } else { - $('#'+key).append(args[0]).animate({ scrollTop: document.getElementById("#"+key).scrollHeight }, 0); + $('#'+key).append(args[0]); + var scrollHeight = $('#'+key).parent().prop("scrollHeight"); + $('#'+key).parent().animate({ scrollTop: scrollHeight }, 0); } // record sending this message to a pane, no need to update the default div use_default_pane = false; @@ -279,9 +280,8 @@ function onText(args, kwargs) { var mwin = $("#messagewindow"); var cls = kwargs == null ? 'out' : kwargs['cls']; mwin.append("
" + args[0] + "
"); - mwin.animate({ - scrollTop: document.getElementById("messagewindow").scrollHeight - }, 0); + var scrollHeight = mwin.parent().parent().prop("scrollHeight"); + mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0); onNewLine(args[0], null); } From ceefd1eef134c2ebda78591e0ffcbbef8c45c81d Mon Sep 17 00:00:00 2001 From: Nicholas Matlaga Date: Mon, 19 Mar 2018 11:20:08 -0400 Subject: [PATCH 06/29] fix webclient/base.html (500 error) --- .../webclient/templates/webclient/base.html | 100 +++++++----------- 1 file changed, 37 insertions(+), 63 deletions(-) diff --git a/evennia/web/webclient/templates/webclient/base.html b/evennia/web/webclient/templates/webclient/base.html index 3852b45273..a5c65fad2c 100644 --- a/evennia/web/webclient/templates/webclient/base.html +++ b/evennia/web/webclient/templates/webclient/base.html @@ -13,16 +13,18 @@ JQuery available. - - + + + + {% block jquery_import %} - + {% endblock %} + + + - + + + + + + + + {% block guilib_import %} + + {% endblock %} + - - + + + + {% block scripts %} + {% endblock %} @@ -74,6 +101,10 @@ JQuery available. web browser supporting javascript.

This error could also be due to not being able to access the online jQuery javascript library.

+ +
@@ -81,62 +112,5 @@ JQuery available. {% block client %} {% endblock %}
- - - - {% block jquery_import %} - - {% endblock %} - - - - - - - - - - - - - - - - - - {% block guilib_import %} - - {% endblock %} - - - - - {% block scripts %} - {% endblock %} From 7f8c5ea839dd50465fc02cf253abbb45d1a3d687 Mon Sep 17 00:00:00 2001 From: friarzen Date: Wed, 21 Mar 2018 18:35:48 +0000 Subject: [PATCH 07/29] Add user selected names to each new pane and some CSS --- .../static/webclient/css/webclient.css | 17 +++++ .../static/webclient/js/splithandler.js | 44 +++++------ .../static/webclient/js/webclient_gui.js | 73 ++++++++++++------- .../templates/webclient/webclient.html | 6 +- 4 files changed, 86 insertions(+), 54 deletions(-) diff --git a/evennia/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css index 94344386a1..1e9adb283b 100644 --- a/evennia/web/webclient/static/webclient/css/webclient.css +++ b/evennia/web/webclient/static/webclient/css/webclient.css @@ -147,6 +147,19 @@ div {margin:0px;} cursor: pointer; } +.button { + width: fit-content; + padding: 1em; + color: black; + border: 1px solid black; + background-color: darkgray; + margin: 0 auto; +} + +.splitbutton:hover { + cursor: pointer; +} + #optionsbutton { width: 2rem; font-size: 2rem; @@ -256,6 +269,10 @@ div {margin:0px;} overflow-x: hidden; } +.split-sub { + padding: .5rem; +} + .content { border: 1px solid #C0C0C0; box-shadow: inset 0 1px 2px #e4e4e4; diff --git a/evennia/web/webclient/static/webclient/js/splithandler.js b/evennia/web/webclient/static/webclient/js/splithandler.js index 7ba8e45e17..aa6ea4364a 100644 --- a/evennia/web/webclient/static/webclient/js/splithandler.js +++ b/evennia/web/webclient/static/webclient/js/splithandler.js @@ -1,50 +1,50 @@ // Use split.js to create a basic ui var SplitHandler = (function () { - var num_splits = 0; var split_panes = {}; var set_pane_types = function(splitpane, types) { split_panes[splitpane]['types'] = types; } - var dynamic_split = function(splitpane, direction, update_method1, update_method2) { - var first = ++num_splits; - var second = ++num_splits; - var first_div = $( '
' ) - var first_sub = $( '
' ) - var second_div = $( '
' ) - var second_sub = $( '
' ) + var dynamic_split = function(splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) { + // find the sub-div of the pane we are being asked to split + splitpanesub = splitpane + '-sub'; - // check to see if this pane contains the primary message window. - contents = $('#'+splitpane).contents(); + // create the new div stack to replace the sub-div with. + var first_div = $( '
' ) + var first_sub = $( '
' ) + var second_div = $( '
' ) + var second_sub = $( '
' ) + + // check to see if this sub-pane contains anything + contents = $('#'+splitpanesub).contents(); if( contents ) { - // it does, so move it to the first new div (TODO -- selectable between first/second?) + // it does, so move it to the first new div-sub (TODO -- selectable between first/second?) contents.appendTo(first_sub); } - first_div.append( first_sub ); second_div.append( second_sub ); - // update the split_panes array to remove this split + // update the split_panes array to remove this pane name delete( split_panes[splitpane] ); // now vaporize the current split_N-sub placeholder and create two new panes. - $('#'+splitpane).parent().append(first_div); - $('#'+splitpane).parent().append(second_div); - $('#'+splitpane).remove(); + $('#'+splitpane).append(first_div); + $('#'+splitpane).append(second_div); + $('#'+splitpane+'-sub').remove(); // And split - Split(['#split_'+first,'#split_'+second], { + Split(['#'+pane_name1,'#'+pane_name2], { direction: direction, - sizes: [50,50], + sizes: sizes, gutterSize: 4, minSize: [50,50], }); - // store our new splits for future splits/uses by the main UI. - split_panes['split_'+first +'-sub'] = { 'types': [], 'update_method': update_method1 }; - split_panes['split_'+second+'-sub'] = { 'types': [], 'update_method': update_method2 }; + // store our new split sub-divs for future splits/uses by the main UI. + split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 }; + split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 }; } @@ -63,7 +63,7 @@ var SplitHandler = (function () { minSize: [50,50], }); - split_panes['main-sub'] = {'types': [], 'update_method': 'append'}; + split_panes['main'] = { 'types': [], 'update_method': 'append' }; var input_render = Mustache.render(input_template); $('[data-role-input]').html(input_render); diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index 4189e026fb..7a7c218921 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -15,6 +15,7 @@ (function () { "use strict" +var num_splits = 0; var options = {}; @@ -167,7 +168,7 @@ function onKeydown (event) { return; } - inputfield.focus(); + //inputfield.focus(); if (code === 13) { // Enter key sends text doSendText(); @@ -245,31 +246,23 @@ function onText(args, kwargs) { known_types.push(msgtype); } - if ( msgtype == 'help' ) { - if (("helppopup" in options) && (options["helppopup"])) { - openPopup("#helpdialog", args[0]); - return; - } - // fall through to the default output - - } else { - // pass this message to each pane that has this msgtype mapped - if( SplitHandler ) { - for ( var key in SplitHandler.split_panes) { - var pane = SplitHandler.split_panes[key]; - // is this message type mapped to this pane? - if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { - // yes, so append/replace this pane's inner div with this message - if ( pane['update_method'] == 'replace' ) { - $('#'+key).html(args[0]) - } else { - $('#'+key).append(args[0]); - var scrollHeight = $('#'+key).parent().prop("scrollHeight"); - $('#'+key).parent().animate({ scrollTop: scrollHeight }, 0); - } - // record sending this message to a pane, no need to update the default div - use_default_pane = false; + // pass this message to each pane that has this msgtype mapped + if( SplitHandler ) { + for ( var key in SplitHandler.split_panes) { + var pane = SplitHandler.split_panes[key]; + // is this message type mapped to this pane? + if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) { + // yes, so append/replace this pane's inner div with this message + var text_div = $('#'+key+'-sub'); + if ( pane['update_method'] == 'replace' ) { + text_div.html(args[0]) + } else { + text_div.append(args[0]); + var scrollHeight = text_div.parent().prop("scrollHeight"); + text_div.parent().animate({ scrollTop: scrollHeight }, 0); } + // record sending this message to a pane, no need to update the default div + use_default_pane = false; } } } @@ -441,19 +434,39 @@ function doStartDragDialog(event) { $(document).bind("mouseup", undrag); } - function onSplitDialogClose() { var pane = $("input[name=pane]:checked").attr("value"); var direction = $("input[name=direction]:checked").attr("value"); + var new_pane1 = $("input[name=new_pane1]").val(); + var new_pane2 = $("input[name=new_pane2]").val(); var flow1 = $("input[name=flow1]:checked").attr("value"); var flow2 = $("input[name=flow2]:checked").attr("value"); - SplitHandler.dynamic_split( pane, direction, flow1, flow2 ); + if( new_pane1 == "" ) { + new_pane1 = 'pane_'+num_splits; + num_splits++; + } + + if( new_pane2 == "" ) { + new_pane2 = 'pane_'+num_splits; + num_splits++; + } + + if( document.getElementById(new_pane1) ) { + alert('An element: "' + new_pane1 + '" already exists'); + return; + } + + if( document.getElementById(new_pane2) ) { + alert('An element: "' + new_pane2 + '" already exists'); + return; + } + + SplitHandler.dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] ); closePopup("#splitdialog"); } - function onSplitDialog() { var dialog = $("#splitdialogcontent"); dialog.empty(); @@ -467,6 +480,10 @@ function onSplitDialog() { dialog.append(''+ pane +'
'); } + dialog.append("

New Pane Names

"); + dialog.append(''); + dialog.append(''); + dialog.append("

New First Pane Flow

"); dialog.append('append
'); dialog.append('replace
'); diff --git a/evennia/web/webclient/templates/webclient/webclient.html b/evennia/web/webclient/templates/webclient/webclient.html index 2b138cb8bd..b750257048 100644 --- a/evennia/web/webclient/templates/webclient/webclient.html +++ b/evennia/web/webclient/templates/webclient/webclient.html @@ -16,14 +16,12 @@
-
+
- -
-
+
From 75582f23f98b60fbc21bb9b5ed0f15a4b5deb276 Mon Sep 17 00:00:00 2001 From: friarzen Date: Thu, 22 Mar 2018 00:52:19 +0000 Subject: [PATCH 08/29] adjust css to match existing toolbar and toggle split/pane popup --- .../static/webclient/css/webclient.css | 22 +++++++++++++------ .../static/webclient/js/webclient_gui.js | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/evennia/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css index 1e9adb283b..75dd91ce2a 100644 --- a/evennia/web/webclient/static/webclient/css/webclient.css +++ b/evennia/web/webclient/static/webclient/css/webclient.css @@ -129,20 +129,28 @@ div {margin:0px;} max-height: 3rem; } -.splitbutton { - position: absolute; - right: 1%; - top: 1%; - z-index: 1; +#splitbutton { width: 2rem; - height: 2rem; font-size: 2rem; color: #a6a6a6; background-color: transparent; border: 0px; } -.splitbutton:hover { +#splitbutton:hover { + color: white; + cursor: pointer; +} + +#panebutton { + width: 2rem; + font-size: 2rem; + color: #a6a6a6; + background-color: transparent; + border: 0px; +} + +#panebutton:hover { color: white; cursor: pointer; } diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index 7a7c218921..b975ae7044 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -496,7 +496,7 @@ function onSplitDialog() { $("#splitclose").bind("click", onSplitDialogClose); - openPopup("#splitdialog"); + togglePopup("#splitdialog"); } function onPaneControlDialogClose() { @@ -530,7 +530,7 @@ function onPaneControlDialog() { $("#paneclose").bind("click", onPaneControlDialogClose); - openPopup("#splitdialog"); + togglePopup("#splitdialog"); } // From 57b0c03ce501125e724393d9d09fbb07833759f4 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Thu, 29 Mar 2018 18:02:45 -0700 Subject: [PATCH 09/29] Add 'INFO' command to unlogged-in command set, so that we can be polled by Mudconnector and Mudstats. --- evennia/commands/default/cmdset_unloggedin.py | 1 + evennia/commands/default/unloggedin.py | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/cmdset_unloggedin.py b/evennia/commands/default/cmdset_unloggedin.py index 5e43d5e8c8..1c45d908aa 100644 --- a/evennia/commands/default/cmdset_unloggedin.py +++ b/evennia/commands/default/cmdset_unloggedin.py @@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet): self.add(unloggedin.CmdUnconnectedHelp()) self.add(unloggedin.CmdUnconnectedEncoding()) self.add(unloggedin.CmdUnconnectedScreenreader()) + self.add(unloggedin.CmdUnconnectedInfo()) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 429948b0fb..28c126e6d8 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -3,6 +3,7 @@ Commands that are available from the connect screen. """ import re import time +import datetime from collections import defaultdict from random import getrandbits from django.conf import settings @@ -11,8 +12,9 @@ from evennia.accounts.models import AccountDB from evennia.objects.models import ObjectDB from evennia.server.models import ServerConfig from evennia.comms.models import ChannelDB +from evennia.server.sessionhandler import SESSIONS -from evennia.utils import create, logger, utils +from evennia.utils import create, logger, utils, gametime from evennia.commands.cmdhandler import CMD_LOGINSTART COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) @@ -516,6 +518,23 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS): self.session.sessionhandler.session_portal_sync(self.session) +class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS): + """ + Provides MUDINFO output, so that Evennia games can be added to Mudconnector + and Mudstats. + """ + key = "info" + locks = "cmd:all()" + + def func(self): + self.caller.msg("## BEGIN INFO 1.1") + self.caller.msg("Name: %s" % settings.SERVERNAME) + self.caller.msg("Uptime: %s" % datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime()) + self.caller.msg("Connected: %d" % SESSIONS.account_count()) + self.caller.msg("Version: Evennia %s" % utils.get_evennia_version()) + self.caller.msg("## END INFO") + + def _create_account(session, accountname, password, permissions, typeclass=None, email=None): """ Helper function, creates an account of the specified typeclass. From 60606898f880c6132fedc0c91315f585535e9137 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 31 Mar 2018 12:40:57 +0200 Subject: [PATCH 10/29] Fix of output handling in msg() when text is None --- evennia/accounts/accounts.py | 16 +++++++++------- evennia/objects/objects.py | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 48d03d8dc8..fe7693cce0 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -421,17 +421,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): kwargs["options"] = options - if not (isinstance(text, basestring) or isinstance(text, tuple)): - # sanitize text before sending across the wire - try: - text = to_str(text, force_string=True) - except Exception: - text = repr(text) + if text is not None: + if not (isinstance(text, basestring) or isinstance(text, tuple)): + # sanitize text before sending across the wire + try: + text = to_str(text, force_string=True) + except Exception: + text = repr(text) + kwargs['text'] = text # session relay sessions = make_iter(session) if session else self.sessions.all() for session in sessions: - session.data_out(text=text, **kwargs) + session.data_out(**kwargs) def execute_cmd(self, raw_string, session=None, **kwargs): """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index f583570707..8cdf546706 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -535,17 +535,19 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)): except Exception: logger.log_trace() - if not (isinstance(text, basestring) or isinstance(text, tuple)): - # sanitize text before sending across the wire - try: - text = to_str(text, force_string=True) - except Exception: - text = repr(text) + if text is not None: + if not (isinstance(text, basestring) or isinstance(text, tuple)): + # sanitize text before sending across the wire + try: + text = to_str(text, force_string=True) + except Exception: + text = repr(text) + kwargs['text'] = text # relay to session(s) sessions = make_iter(session) if session else self.sessions.all() for session in sessions: - session.data_out(text=text, **kwargs) + session.data_out(**kwargs) def for_contents(self, func, exclude=None, **kwargs): From b525e135530ddc8f29a6b0bbd823390b40112038 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sat, 31 Mar 2018 23:15:28 -0700 Subject: [PATCH 11/29] Update INFO command to take a single msg() call, and add better docstring. --- evennia/commands/default/unloggedin.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/evennia/commands/default/unloggedin.py b/evennia/commands/default/unloggedin.py index 28c126e6d8..293e022a19 100644 --- a/evennia/commands/default/unloggedin.py +++ b/evennia/commands/default/unloggedin.py @@ -521,18 +521,19 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS): class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS): """ Provides MUDINFO output, so that Evennia games can be added to Mudconnector - and Mudstats. + and Mudstats. Sadly, the MUDINFO specification seems to have dropped off the + face of the net, but it is still used by some crawlers. This implementation + was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost, + and PennMUSH. """ key = "info" locks = "cmd:all()" def func(self): - self.caller.msg("## BEGIN INFO 1.1") - self.caller.msg("Name: %s" % settings.SERVERNAME) - self.caller.msg("Uptime: %s" % datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime()) - self.caller.msg("Connected: %d" % SESSIONS.account_count()) - self.caller.msg("Version: Evennia %s" % utils.get_evennia_version()) - self.caller.msg("## END INFO") + self.caller.msg("## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % ( + settings.SERVERNAME, + datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), + SESSIONS.account_count(), utils.get_evennia_version())) def _create_account(session, accountname, password, permissions, typeclass=None, email=None): From 5d37f54b083ef3f89efa7b4f3ec846d28417110a Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sun, 1 Apr 2018 00:39:16 -0700 Subject: [PATCH 12/29] Add test for unconnected commands, add INFO command to test set. --- evennia/commands/default/tests.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 41654077a4..a645014482 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -14,15 +14,16 @@ main test suite started with import re import types +import datetime from django.conf import settings from mock import Mock, mock from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.utils.test_resources import EvenniaTest -from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms +from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin from evennia.commands.command import Command, InterruptCommand -from evennia.utils import ansi, utils +from evennia.utils import ansi, utils, gametime from evennia.server.sessionhandler import SESSIONS from evennia import search_object from evennia import DefaultObject, DefaultCharacter @@ -433,3 +434,12 @@ class TestInterruptCommand(CommandTest): def test_interrupt_command(self): ret = self.call(CmdInterrupt(), "") self.assertEqual(ret, "") + + +class TestUnconnectedCommand(CommandTest): + def test_info_command(self): + expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % ( + settings.SERVERNAME, + datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), + SESSIONS.account_count(), utils.get_evennia_version()) + self.call(unloggedin.CmdUnconnectedInfo(), "", expected) \ No newline at end of file From 9205fcced8a7e413fb8c6bbe555b76585ca4a46a Mon Sep 17 00:00:00 2001 From: Brenden Tuck Date: Sun, 8 Apr 2018 12:33:38 -0400 Subject: [PATCH 13/29] Added an undo button for multi-level undo of splits --- .../static/webclient/css/webclient.css | 13 ++++ .../static/webclient/js/splithandler.js | 68 ++++++++++++++++++- .../static/webclient/js/webclient_gui.js | 5 +- .../templates/webclient/webclient.html | 1 + 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/evennia/web/webclient/static/webclient/css/webclient.css b/evennia/web/webclient/static/webclient/css/webclient.css index 75dd91ce2a..7a33cfa207 100644 --- a/evennia/web/webclient/static/webclient/css/webclient.css +++ b/evennia/web/webclient/static/webclient/css/webclient.css @@ -155,6 +155,19 @@ div {margin:0px;} cursor: pointer; } +#undobutton { + width: 2rem; + font-size: 2rem; + color: #a6a6a6; + background-color: transparent; + border: 0px; +} + +#undobutton:hover { + color: white; + cursor: pointer; +} + .button { width: fit-content; padding: 1em; diff --git a/evennia/web/webclient/static/webclient/js/splithandler.js b/evennia/web/webclient/static/webclient/js/splithandler.js index aa6ea4364a..81210df854 100644 --- a/evennia/web/webclient/static/webclient/js/splithandler.js +++ b/evennia/web/webclient/static/webclient/js/splithandler.js @@ -1,6 +1,7 @@ // Use split.js to create a basic ui var SplitHandler = (function () { var split_panes = {}; + var backout_list = new Array; var set_pane_types = function(splitpane, types) { split_panes[splitpane]['types'] = types; @@ -26,7 +27,8 @@ var SplitHandler = (function () { first_div.append( first_sub ); second_div.append( second_sub ); - // update the split_panes array to remove this pane name + // update the split_panes array to remove this pane name, but store it for the backout stack + var backout_settings = split_panes[splitpane]; delete( split_panes[splitpane] ); // now vaporize the current split_N-sub placeholder and create two new panes. @@ -45,6 +47,69 @@ var SplitHandler = (function () { // store our new split sub-divs for future splits/uses by the main UI. split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 }; split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 }; + + // add our new split to the backout stack + backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} ); + } + + + var undo_split = function() { + // pop off the last split pair + var back = backout_list.pop(); + if( !back ) { + return; + } + + // Collect all the divs/subs in play + var pane1 = back['pane1']; + var pane2 = back['pane2']; + var pane1_sub = $('#'+pane1+'-sub'); + var pane2_sub = $('#'+pane2+'-sub'); + var pane1_parent = $('#'+pane1).parent(); + var pane2_parent = $('#'+pane2).parent(); + + if( pane1_parent.attr('id') != pane2_parent.attr('id') ) { + // sanity check failed...somebody did something weird...bail out + console.log( pane1 ); + console.log( pane2 ); + console.log( pane1_parent ); + console.log( pane2_parent ); + return; + } + + // create a new sub-pane in the panes parent + var parent_sub = $( '
' ) + + // check to see if the special #messagewindow is in either of our sub-panes. + var msgwindow = pane1_sub.find('#messagewindow') + if( !msgwindow ) { + //didn't find it in pane 1, try pane 2 + msgwindow = pane2_sub.find('#messagewindow') + } + if( msgwindow ) { + // It is, so collect all contents into it instead of our parent_sub div + // then move it to parent sub div, this allows future #messagewindow divs to flow properly + msgwindow.append( pane1_sub.contents() ); + msgwindow.append( pane2_sub.contents() ); + parent_sub.append( msgwindow ); + } else { + //didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane + parent_sub.append( pane1_sub.contents() ); + parent_sub.append( pane2_sub.contents() ); + } + + // clear the parent + pane1_parent.empty(); + + // add the new sub-pane back to the parent div + pane1_parent.append(parent_sub); + + // pull the sub-div's from split_panes + delete split_panes[pane1]; + delete split_panes[pane2]; + + // add our parent pane back into the split_panes list for future splitting + split_panes[pane1_parent.attr('id')] = back['undo']; } @@ -75,5 +140,6 @@ var SplitHandler = (function () { set_pane_types: set_pane_types, dynamic_split: dynamic_split, split_panes: split_panes, + undo_split: undo_split, } })(); diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index b975ae7044..8929a7529c 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -15,7 +15,7 @@ (function () { "use strict" -var num_splits = 0; +var num_splits = 0; //unique id counter for default split-panel names var options = {}; @@ -544,9 +544,12 @@ $(document).ready(function() { SplitHandler.init(); $("#splitbutton").bind("click", onSplitDialog); $("#panebutton").bind("click", onPaneControlDialog); + $("#undobutton").bind("click", SplitHandler.undo_split); + $("#optionsbutton").hide(); } else { $("#splitbutton").hide(); $("#panebutton").hide(); + $("#undobutton").hide(); } if ("Notification" in window) { diff --git a/evennia/web/webclient/templates/webclient/webclient.html b/evennia/web/webclient/templates/webclient/webclient.html index b750257048..74bef631cf 100644 --- a/evennia/web/webclient/templates/webclient/webclient.html +++ b/evennia/web/webclient/templates/webclient/webclient.html @@ -12,6 +12,7 @@ +
From 739680a014bdccca096b6a03aa20e03b8dd1b15d Mon Sep 17 00:00:00 2001 From: CloudKeeper1 Date: Sat, 14 Apr 2018 00:23:52 +1000 Subject: [PATCH 14/29] Wrong symbol on line 499 Wrong symbol on line 499 --- evennia/commands/default/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 2f4c51a227..f9634ed675 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -496,7 +496,7 @@ class CmdWhisper(COMMAND_DEFAULT_CLASS): Usage: whisper = - whisper , = , = Talk privately to one or more characters in your current location, without others in the room being informed. From 99b91111f1d887b9e5505899255a51078ff111f7 Mon Sep 17 00:00:00 2001 From: Aditya Arora Date: Mon, 16 Apr 2018 18:41:02 +0530 Subject: [PATCH 15/29] Update rpsystem.py --- evennia/contrib/rpsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/rpsystem.py b/evennia/contrib/rpsystem.py index a56d2de731..efba0fe7fd 100644 --- a/evennia/contrib/rpsystem.py +++ b/evennia/contrib/rpsystem.py @@ -1088,7 +1088,7 @@ class CmdMask(RPCommand): if self.cmdstring == "mask": # wear a mask if not self.args: - caller.msg("Usage: (un)wearmask sdesc") + caller.msg("Usage: (un)mask sdesc") return if caller.db.unmasked_sdesc: caller.msg("You are already wearing a mask.") @@ -1111,7 +1111,7 @@ class CmdMask(RPCommand): del caller.db.unmasked_sdesc caller.locks.remove("enable_recog") caller.sdesc.add(old_sdesc) - caller.msg("You remove your mask and is again '%s'." % old_sdesc) + caller.msg("You remove your mask and are again '%s'." % old_sdesc) class RPSystemCmdSet(CmdSet): From 6fd72f573bd5f5c6c988a98653afff744c0889fd Mon Sep 17 00:00:00 2001 From: Griatch Date: Mon, 16 Apr 2018 20:34:52 +0200 Subject: [PATCH 16/29] Fix unittest --- evennia/commands/default/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 6847afe8c4..7f0ed17924 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -502,5 +502,5 @@ class TestUnconnectedCommand(CommandTest): expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % ( settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), - SESSIONS.account_count(), utils.get_evennia_version()) + SESSIONS.account_count(), utils.get_evennia_version().replace("-","")) self.call(unloggedin.CmdUnconnectedInfo(), "", expected) From 0350b6c3c6228ff5b0a8f6781d1fcad276e30579 Mon Sep 17 00:00:00 2001 From: Griatch Date: Tue, 17 Apr 2018 22:49:01 +0200 Subject: [PATCH 17/29] style fix --- evennia/commands/default/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 7f0ed17924..b2d0f58870 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -502,5 +502,5 @@ class TestUnconnectedCommand(CommandTest): expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % ( settings.SERVERNAME, datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(), - SESSIONS.account_count(), utils.get_evennia_version().replace("-","")) + SESSIONS.account_count(), utils.get_evennia_version().replace("-", "")) self.call(unloggedin.CmdUnconnectedInfo(), "", expected) From a1ab7425879d36d75b9f0b44768d8cb2aefce013 Mon Sep 17 00:00:00 2001 From: Tehom Date: Wed, 18 Apr 2018 02:26:01 -0400 Subject: [PATCH 18/29] Fix CommandTest to stop if at_pre_cmd should stop execution. --- evennia/commands/default/tests.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 41654077a4..8ddcee78b0 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -75,7 +75,8 @@ class CommandTest(EvenniaTest): returned_msg = "" try: receiver.msg = Mock() - cmdobj.at_pre_cmd() + if cmdobj.at_pre_cmd(): + return cmdobj.parse() ret = cmdobj.func() if isinstance(ret, types.GeneratorType): From bee7fa174d1df01c1f541168f39b8e846787bbbb Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 19 Apr 2018 22:47:13 +0200 Subject: [PATCH 19/29] Cherry-pick EvMenu list_node decorator from olc branch --- evennia/utils/evmenu.py | 241 +++++++++++++++++++++++++++++++++++----- 1 file changed, 211 insertions(+), 30 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 3d8fb6b789..a2a92f5e34 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -43,13 +43,18 @@ command definition too) with function definitions: def node_with_other_name(caller, input_string): # code return text, options + + def another_node(caller, input_string, **kwargs): + # code + return text, options ``` Where caller is the object using the menu and input_string is the command entered by the user on the *previous* node (the command entered to get to this node). The node function code will only be executed once per node-visit and the system will accept nodes with -both one or two arguments interchangeably. +both one or two arguments interchangeably. It also accepts nodes +that takes **kwargs. The menu tree itself is available on the caller as `caller.ndb._menutree`. This makes it a convenient place to store @@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called. the callable. Those kwargs will also be passed into the next node if possible. Such a callable should return either a str or a (str, dict), where the string is the name of the next node to go to and the dict is the new, - (possibly modified) kwarg to pass into the next node. + (possibly modified) kwarg to pass into the next node. If the callable returns + None or the empty string, the current node will be revisited. - `exec` (str, callable or tuple, optional): This takes the same input as `goto` above and runs before it. If given a node name, the node will be executed but will not be considered the next node. If node/callback returns str or (str, dict), these will replace the `goto` step (`goto` callbacks will not fire), with the string being the next node name and the optional dict acting as the kwargs-input for the next node. + If an exec callable returns the empty string (only), the current node is re-run. If key is not given, the option will automatically be identified by its number 1..N. @@ -167,7 +174,7 @@ from evennia import Command, CmdSet from evennia.utils import logger from evennia.utils.evtable import EvTable from evennia.utils.ansi import strip_ansi -from evennia.utils.utils import mod_import, make_iter, pad, m_len +from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter from evennia.commands import cmdhandler # read from protocol NAWS later? @@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT # i18n from django.utils.translation import ugettext as _ -_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.") +_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or " + "caused an error. Make another choice.") _ERR_GENERAL = _("Error in menu node '{nodename}'.") _ERR_NO_OPTION_DESC = _("No description.") _HELP_FULL = _("Commands: , help, quit") @@ -573,6 +581,7 @@ class EvMenu(object): except EvMenuError: errmsg = _ERR_GENERAL.format(nodename=callback) self.caller.msg(errmsg, self._session) + logger.log_trace() raise return ret @@ -606,9 +615,11 @@ class EvMenu(object): nodetext, options = ret, None except KeyError: self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) + logger.log_trace() raise EvMenuError except Exception: self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session) + logger.log_trace() raise # store options to make them easier to test @@ -665,9 +676,49 @@ class EvMenu(object): if isinstance(ret, basestring): # only return a value if a string (a goto target), ignore all other returns + if not ret: + # an empty string - rerun the same node + return self.nodename return ret, kwargs return None + def extract_goto_exec(self, nodename, option_dict): + """ + Helper: Get callables and their eventual kwargs. + + Args: + nodename (str): The current node name (used for error reporting). + option_dict (dict): The seleted option's dict. + + Returns: + goto (str, callable or None): The goto directive in the option. + goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty. + execute (callable or None): Executable given by the `exec` directive. + exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty. + + """ + goto_kwargs, exec_kwargs = {}, {} + goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) + if goto and isinstance(goto, (tuple, list)): + if len(goto) > 1: + goto, goto_kwargs = goto[:2] # ignore any extra arguments + if not hasattr(goto_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format( + nodename, goto_kwargs)) + else: + goto = goto[0] + if execute and isinstance(execute, (tuple, list)): + if len(execute) > 1: + execute, exec_kwargs = execute[:2] # ignore any extra arguments + if not hasattr(exec_kwargs, "__getitem__"): + # not a dict-like structure + raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format( + nodename, goto_kwargs)) + else: + execute = execute[0] + return goto, goto_kwargs, execute, exec_kwargs + def goto(self, nodename, raw_string, **kwargs): """ Run a node by name, optionally dynamically generating that name first. @@ -681,29 +732,6 @@ class EvMenu(object): argument) """ - def _extract_goto_exec(option_dict): - "Helper: Get callables and their eventual kwargs" - goto_kwargs, exec_kwargs = {}, {} - goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) - if goto and isinstance(goto, (tuple, list)): - if len(goto) > 1: - goto, goto_kwargs = goto[:2] # ignore any extra arguments - if not hasattr(goto_kwargs, "__getitem__"): - # not a dict-like structure - raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format( - nodename, goto_kwargs)) - else: - goto = goto[0] - if execute and isinstance(execute, (tuple, list)): - if len(execute) > 1: - execute, exec_kwargs = execute[:2] # ignore any extra arguments - if not hasattr(exec_kwargs, "__getitem__"): - # not a dict-like structure - raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format( - nodename, goto_kwargs)) - else: - execute = execute[0] - return goto, goto_kwargs, execute, exec_kwargs if callable(nodename): # run the "goto" callable, if possible @@ -714,6 +742,9 @@ class EvMenu(object): raise EvMenuError( "{}: goto callable must return str or (str, dict)".format(inp_nodename)) nodename, kwargs = nodename[:2] + if not nodename: + # no nodename return. Re-run current node + nodename = self.nodename try: # execute the found node, make use of the returns. nodetext, options = self._execute_node(nodename, raw_string, **kwargs) @@ -746,12 +777,12 @@ class EvMenu(object): desc = dic.get("desc", dic.get("text", None)) if "_default" in keys: keys = [key for key in keys if key != "_default"] - goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) + goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic) self.default = (goto, goto_kwargs, execute, exec_kwargs) else: # use the key (only) if set, otherwise use the running number keys = list(make_iter(dic.get("key", str(inum + 1).strip()))) - goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) + goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic) if keys: display_options.append((keys[0], desc)) for key in keys: @@ -945,14 +976,164 @@ class EvMenu(object): node (str): The formatted node to display. """ + screen_width = self._session.protocol_flags.get("SCREENWIDTH", {0: 78})[0] + nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) options_width_max = max(m_len(line) for line in optionstext.split("\n")) - total_width = max(options_width_max, nodetext_width_max) + total_width = min(screen_width, max(options_width_max, nodetext_width_max)) separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext +# ----------------------------------------------------------- +# +# List node (decorator turning a node into a list with +# look/edit/add functionality for the elements) +# +# ----------------------------------------------------------- + +def list_node(option_generator, select=None, pagesize=10): + """ + Decorator for making an EvMenu node into a multi-page list node. Will add new options, + prepending those options added in the node. + + Args: + option_generator (callable or list): A list of strings indicating the options, or a callable + that is called as option_generator(caller) to produce such a list. + select (callable, option): Will be called as select(caller, menuchoice) + where menuchoice is the chosen option as a string. Should return the target node to + goto after this selection (or None to repeat the list-node). Note that if this is not + given, the decorated node must itself provide a way to continue from the node! + pagesize (int): How many options to show per page. + + Example: + @list_node(['foo', 'bar'], select) + def node_index(caller): + text = "describing the list" + return text, [] + + Notes: + All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept + **kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named + options (descs) visible on the current node page. + + """ + + def decorator(func): + + def _select_parser(caller, raw_string, **kwargs): + """ + Parse the select action + """ + available_choices = kwargs.get("available_choices", []) + + try: + index = int(raw_string.strip()) - 1 + selection = available_choices[index] + except Exception: + caller.msg("|rInvalid choice.|n") + else: + if select: + try: + return select(caller, selection) + except Exception: + logger.log_trace() + return None + + def _list_node(caller, raw_string, **kwargs): + + option_list = option_generator(caller) \ + if callable(option_generator) else option_generator + + npages = 0 + page_index = 0 + page = [] + options = [] + + if option_list: + nall_options = len(option_list) + pages = [option_list[ind:ind + pagesize] + for ind in range(0, nall_options, pagesize)] + npages = len(pages) + + page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0))) + page = pages[page_index] + + text = "" + extra_text = None + + # dynamic, multi-page option list. Each selection leads to the `select` + # callback being called with a result from the available choices + options.extend([{"desc": opt, + "goto": (_select_parser, + {"available_choices": page})} for opt in page]) + + if npages > 1: + # if the goto callable returns None, the same node is rerun, and + # kwargs not used by the callable are passed on to the node. This + # allows us to call ourselves over and over, using different kwargs. + options.append({"key": ("|Wcurrent|n", "c"), + "desc": "|W({}/{})|n".format(page_index + 1, npages), + "goto": (lambda caller: None, + {"optionpage_index": page_index})}) + if page_index > 0: + options.append({"key": ("|wp|Wrevious page|n", "p"), + "goto": (lambda caller: None, + {"optionpage_index": page_index - 1})}) + if page_index < npages - 1: + options.append({"key": ("|wn|Wext page|n", "n"), + "goto": (lambda caller: None, + {"optionpage_index": page_index + 1})}) + + # add data from the decorated node + + decorated_options = [] + try: + text, decorated_options = func(caller, raw_string) + except TypeError: + try: + text, decorated_options = func(caller) + except Exception: + raise + except Exception: + logger.log_trace() + else: + if isinstance(decorated_options, {}): + decorated_options = [decorated_options] + else: + decorated_options = make_iter(decorated_options) + + extra_options = [] + for eopt in decorated_options: + cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None + if cback: + signature = eopt[cback] + if callable(signature): + # callable with no kwargs defined + eopt[cback] = (signature, {"available_choices": page}) + elif is_iter(signature): + if len(signature) > 1 and isinstance(signature[1], dict): + signature[1]["available_choices"] = page + eopt[cback] = signature + elif signature: + # a callable alone in a tuple (i.e. no previous kwargs) + eopt[cback] = (signature[0], {"available_choices": page}) + else: + # malformed input. + logger.log_err("EvMenu @list_node decorator found " + "malformed option to decorate: {}".format(eopt)) + extra_options.append(eopt) + + options.extend(extra_options) + text = text + "\n\n" + extra_text if extra_text else text + + return text, options + + return _list_node + return decorator + + # ------------------------------------------------------------------------------------------------- # # Simple input shortcuts From b571d6fdd46dc3ccfcf99b17fa9f03e9fd53e45c Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 20 Apr 2018 19:51:12 +0200 Subject: [PATCH 20/29] Fix unittests --- evennia/contrib/tests.py | 18 +++++++++--------- evennia/utils/evmenu.py | 6 +++++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 30bf71dcc8..be5921ff67 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -670,7 +670,7 @@ class TestGenderSub(CommandTest): char = create_object(gendersub.GenderCharacter, key="Gendered", location=self.room1) txt = "Test |p gender" self.assertEqual(gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender") - + # test health bar contrib from evennia.contrib import health_bar @@ -798,7 +798,7 @@ from evennia.contrib import talking_npc class TestTalkingNPC(CommandTest): def test_talkingnpc(self): npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) - self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)|") + self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)") npc.delete() @@ -966,7 +966,7 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_basic.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") - + # Test equipment commands def test_turnbattleequipcmd(self): # Start with equip module specific commands. @@ -984,7 +984,7 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_equip.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") - + # Test range commands def test_turnbattlerangecmd(self): # Start with range module specific commands. @@ -998,7 +998,7 @@ class TestTurnBattleCmd(CommandTest): self.call(tb_range.CmdPass(), "", "You can only do that in combat. (see: help fight)") self.call(tb_range.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") - + class TestTurnBattleFunc(EvenniaTest): @@ -1080,7 +1080,7 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) # Remove the script at the end turnhandler.stop() - + # Test the combat functions in tb_equip too. They work mostly the same. def test_tbequipfunc(self): attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker") @@ -1159,7 +1159,7 @@ class TestTurnBattleFunc(EvenniaTest): self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) # Remove the script at the end turnhandler.stop() - + # Test combat functions in tb_range too. def test_tbrangefunc(self): testroom = create_object(DefaultRoom, key="Test Room") @@ -1264,7 +1264,7 @@ Bar -Qux""" class TestTreeSelectFunc(EvenniaTest): - + def test_tree_functions(self): # Dash counter self.assertTrue(tree_select.dashcount("--test") == 2) @@ -1279,7 +1279,7 @@ class TestTreeSelectFunc(EvenniaTest): # Option list to menu options test_optlist = tree_select.parse_opts(TREE_MENU_TESTSTR, category_index=2) optlist_to_menu_expected_result = [{'goto': ['menunode_treeselect', {'newindex': 3}], 'key': 'Baz 1'}, - {'goto': ['menunode_treeselect', {'newindex': 4}], 'key': 'Baz 2'}, + {'goto': ['menunode_treeselect', {'newindex': 4}], 'key': 'Baz 2'}, {'goto': ['menunode_treeselect', {'newindex': 1}], 'key': ['<< Go Back', 'go back', 'back'], 'desc': 'Return to the previous menu.'}] self.assertTrue(tree_select.optlist_to_menuoptions(TREE_MENU_TESTSTR, test_optlist, 2, True, True) == optlist_to_menu_expected_result) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index a2a92f5e34..0e494ca3e0 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -976,7 +976,11 @@ class EvMenu(object): node (str): The formatted node to display. """ - screen_width = self._session.protocol_flags.get("SCREENWIDTH", {0: 78})[0] + if self._session: + screen_width = self._session.protocol_flags.get( + "SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0] + else: + screen_width = _MAX_TEXT_WIDTH nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) options_width_max = max(m_len(line) for line in optionstext.split("\n")) From acc651b2feb744f6a77b169bb60038c51e64fb33 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 21 Apr 2018 15:31:30 +0200 Subject: [PATCH 21/29] Inject selection in list_node decorator if select kwarg is a string --- evennia/utils/evmenu.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index 0e494ca3e0..f6806c06b8 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1005,10 +1005,12 @@ def list_node(option_generator, select=None, pagesize=10): Args: option_generator (callable or list): A list of strings indicating the options, or a callable that is called as option_generator(caller) to produce such a list. - select (callable, option): Will be called as select(caller, menuchoice) - where menuchoice is the chosen option as a string. Should return the target node to - goto after this selection (or None to repeat the list-node). Note that if this is not - given, the decorated node must itself provide a way to continue from the node! + select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will + contain the `available_choices` list and `selection` will hold one of the elements in + that list. If a callable, it will be called as select(caller, menuchoice) where + menuchoice is the chosen option as a string. Should return the target node to goto after + this selection (or None to repeat the list-node). Note that if this is not given, the + decorated node must itself provide a way to continue from the node! pagesize (int): How many options to show per page. Example: @@ -1038,11 +1040,16 @@ def list_node(option_generator, select=None, pagesize=10): except Exception: caller.msg("|rInvalid choice.|n") else: - if select: + if callable(select): try: return select(caller, selection) except Exception: logger.log_trace() + else: + # we assume a string was given, we inject the result into the kwargs + # to pass on to the next node + kwargs['selection'] = selection + return str(select) return None def _list_node(caller, raw_string, **kwargs): From a99c1ed74c98dc93fb173f7b01ba0aa883c7fe1b Mon Sep 17 00:00:00 2001 From: friarzen Date: Sat, 21 Apr 2018 17:03:01 +0000 Subject: [PATCH 22/29] Attempt to make append/replace dialog text more clear --- .../webclient/static/webclient/js/webclient_gui.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/evennia/web/webclient/static/webclient/js/webclient_gui.js b/evennia/web/webclient/static/webclient/js/webclient_gui.js index 8929a7529c..e1ed4d31fd 100644 --- a/evennia/web/webclient/static/webclient/js/webclient_gui.js +++ b/evennia/web/webclient/static/webclient/js/webclient_gui.js @@ -484,13 +484,13 @@ function onSplitDialog() { dialog.append(''); dialog.append(''); - dialog.append("

New First Pane Flow

"); - dialog.append('append
'); - dialog.append('replace
'); + dialog.append("

New First Pane

"); + dialog.append('append new incoming messages
'); + dialog.append('replace old messages with new ones
'); - dialog.append("

New Second Pane Flow

"); - dialog.append('append
'); - dialog.append('replace
'); + dialog.append("

New Second Pane

"); + dialog.append('append new incoming messages
'); + dialog.append('replace old messages with new ones
'); dialog.append('
Split It
'); From f57192f9f1ce81eda9059335bfd56e0877ea071a Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 22 Apr 2018 13:39:47 +0200 Subject: [PATCH 23/29] Fix error if sending string to list_node select callback --- evennia/utils/evmenu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evennia/utils/evmenu.py b/evennia/utils/evmenu.py index f6806c06b8..0de33de348 100644 --- a/evennia/utils/evmenu.py +++ b/evennia/utils/evmenu.py @@ -1045,11 +1045,12 @@ def list_node(option_generator, select=None, pagesize=10): return select(caller, selection) except Exception: logger.log_trace() - else: + elif select: # we assume a string was given, we inject the result into the kwargs # to pass on to the next node kwargs['selection'] = selection return str(select) + # this means the previous node will be re-run with these same kwargs return None def _list_node(caller, raw_string, **kwargs): From 9bbfc422ee4c7b4642380d2c211d4b05b77ba9ac Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 22 Apr 2018 14:48:55 +0200 Subject: [PATCH 24/29] Make EvTable respect col-widths while retaining total table width. Resolves #1614. --- evennia/contrib/mail.py | 2 +- evennia/contrib/tests.py | 2 +- evennia/utils/evtable.py | 96 ++++++++++++++++++++++++++++++++-------- 3 files changed, 79 insertions(+), 21 deletions(-) diff --git a/evennia/contrib/mail.py b/evennia/contrib/mail.py index 6e8585136d..dbecb62fcd 100644 --- a/evennia/contrib/mail.py +++ b/evennia/contrib/mail.py @@ -253,7 +253,7 @@ class CmdMail(default_cmds.MuxCommand): index += 1 table.reformat_column(0, width=6) - table.reformat_column(1, width=17) + table.reformat_column(1, width=18) table.reformat_column(2, width=34) table.reformat_column(3, width=13) table.reformat_column(4, width=7) diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index ff6b01d5cf..1e526bd9cb 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -688,7 +688,7 @@ class TestMail(CommandTest): "You have received a new @mail from TestAccount2(account 2)|You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2) self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2) - self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account) + self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account) self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account) self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account) self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account) diff --git a/evennia/utils/evtable.py b/evennia/utils/evtable.py index 31218189ab..ffb29873c4 100644 --- a/evennia/utils/evtable.py +++ b/evennia/utils/evtable.py @@ -893,6 +893,9 @@ class EvColumn(object): """ col = self.column + # fixed options for the column will override those requested in the call! + # this is particularly relevant to things like width/height, to avoid + # fixed-widths columns from being auto-balanced kwargs.update(self.options) # use fixed width or adjust to the largest cell if "width" not in kwargs: @@ -1283,25 +1286,59 @@ class EvTable(object): cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable] cwmin = sum(cwidths_min) - if cwmin > width: - # we cannot shrink any more - raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin)) + # get which cols have separately set widths - these should be locked + # note that we need to remove cwidths_min for each lock to avoid counting + # it twice (in cwmin and in locked_cols) + locked_cols = {icol: col.options['width'] - cwidths_min[icol] + for icol, col in enumerate(self.worktable) if 'width' in col.options} + locked_width = sum(locked_cols.values()) + + excess = width - cwmin - locked_width + + if len(locked_cols) >= ncols and excess: + # we can't adjust the width at all - all columns are locked + raise Exception("Cannot balance table to width %s - " + "all columns have a set, fixed width summing to %s!" % ( + self.width, sum(cwidths))) + + if excess < 0: + # the locked cols makes it impossible + raise Exception("Cannot shrink table width to %s. " + "Minimum size (and/or fixed-width columns) " + "sets minimum at %s." % (self.width, cwmin + locked_width)) - excess = width - cwmin if self.evenwidth: # make each column of equal width - for _ in range(excess): + # use cwidths as a work-array to track weights + cwidths = copy(cwidths_min) + correction = 0 + while correction < excess: # flood-fill the minimum table starting with the smallest columns - ci = cwidths_min.index(min(cwidths_min)) - cwidths_min[ci] += 1 + ci = cwidths.index(min(cwidths)) + if ci in locked_cols: + # locked column, make sure it's not picked again + cwidths[ci] += 9999 + cwidths_min[ci] = locked_cols[ci] + else: + cwidths_min[ci] += 1 + correction += 1 cwidths = cwidths_min else: # make each column expand more proportional to their data size - for _ in range(excess): + # we use cwidth as a work-array to track weights + correction = 0 + while correction < excess: # fill wider columns first ci = cwidths.index(max(cwidths)) - cwidths_min[ci] += 1 - cwidths[ci] -= 3 + if ci in locked_cols: + # locked column, make sure it's not picked again + cwidths[ci] -= 9999 + cwidths_min[ci] = locked_cols[ci] + else: + cwidths_min[ci] += 1 + correction += 1 + # give a just changed col less prio next run + cwidths[ci] -= 3 cwidths = cwidths_min # reformat worktable (for width align) @@ -1323,28 +1360,46 @@ class EvTable(object): for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)] chmin = sum(cheights_min) + # get which cols have separately set heights - these should be locked + # note that we need to remove cheights_min for each lock to avoid counting + # it twice (in chmin and in locked_cols) + locked_cols = {icol: col.options['height'] - cheights_min[icol] + for icol, col in enumerate(self.worktable) if 'height' in col.options} + locked_height = sum(locked_cols.values()) + + excess = self.height - chmin - locked_height + if chmin > self.height: # we cannot shrink any more - raise Exception("Cannot shrink table height to %s. Minimum size is %s." % (self.height, chmin)) + raise Exception("Cannot shrink table height to %s. Minimum " + "size (and/or fixed-height rows) sets minimum at %s." % ( + self.height, chmin + locked_height)) # now we add all the extra height up to the desired table-height. # We do this so that the tallest cells gets expanded first (and # thus avoid getting cropped) - excess = self.height - chmin even = self.height % 2 == 0 - for position in range(excess): + correction = 0 + while correction < excess: # expand the cells with the most rows first - if 0 <= position < nrowmax and nrowmax > 1: + if 0 <= correction < nrowmax and nrowmax > 1: # avoid adding to header first round (looks bad on very small tables) ci = cheights[1:].index(max(cheights[1:])) + 1 else: ci = cheights.index(max(cheights)) - cheights_min[ci] += 1 - if ci == 0 and self.header: - # it doesn't look very good if header expands too fast - cheights[ci] -= 2 if even else 3 - cheights[ci] -= 2 if even else 1 + if ci in locked_cols: + # locked row, make sure it's not picked again + cheights[ci] -= 9999 + cheights_min[ci] = locked_cols[ci] + else: + cheights_min[ci] += 1 + # change balance + if ci == 0 and self.header: + # it doesn't look very good if header expands too fast + cheights[ci] -= 2 if even else 3 + cheights[ci] -= 2 if even else 1 + correction += 1 cheights = cheights_min # we must tell cells to crop instead of expanding @@ -1554,6 +1609,8 @@ class EvTable(object): """ if index > len(self.table): raise Exception("Not a valid column index") + # we update the columns' options which means eventual width/height + # will be 'locked in' and withstand auto-balancing width/height from the table later self.table[index].options.update(kwargs) self.table[index].reformat(**kwargs) @@ -1569,6 +1626,7 @@ class EvTable(object): def __str__(self): """print table (this also balances it)""" + # h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890" return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()]))) def __unicode__(self): From 04db9292ce10d430f3fc3cd8281cdf1c0aea10cf Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sun, 8 Apr 2018 13:11:35 -0700 Subject: [PATCH 25/29] Add /contains switch to find. --- evennia/commands/default/building.py | 15 ++++++++++----- evennia/commands/default/tests.py | 1 + 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 702f2e241a..a111e2c878 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2270,11 +2270,12 @@ class CmdFind(COMMAND_DEFAULT_CLASS): @locate - this is a shorthand for using the /loc switch. Switches: - room - only look for rooms (location=None) - exit - only look for exits (destination!=None) - char - only look for characters (BASE_CHARACTER_TYPECLASS) - exact- only exact matches are returned. - loc - display object location if exists and match has one result + room - only look for rooms (location=None) + exit - only look for exits (destination!=None) + char - only look for characters (BASE_CHARACTER_TYPECLASS) + exact - only exact matches are returned. + loc - display object location if exists and match has one result + contains- search for names containing the string, rather than starting with. Searches the database for an object of a particular name or exact #dbref. Use *accountname to search for an account. The switches allows for @@ -2359,6 +2360,10 @@ class CmdFind(COMMAND_DEFAULT_CLASS): keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__iexact=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) + elif "contains" in switches: + keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high) + aliasquery = Q(db_tags__db_key__icontains=searchstring, + db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) else: keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__istartswith=searchstring, diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 8e73a8bf5d..37e4b07b03 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -339,6 +339,7 @@ class TestBuilding(CommandTest): self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate") self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find") + self.call(building.CmdFind(), "/contains om2", "One Match") def test_script(self): self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added") From 32ea0075e6bd631164225a28e6bdbb640dbd15c3 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sun, 8 Apr 2018 13:30:01 -0700 Subject: [PATCH 26/29] 0.8 has switches defined in the command, need to make the change from the 0.7 changeset this originated from. --- evennia/commands/default/building.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index a111e2c878..dee9cd737e 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2286,7 +2286,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS): key = "@find" aliases = "@search, @locate" - switch_options = ("room", "exit", "char", "exact", "loc") + switch_options = ("room", "exit", "char", "exact", "loc", "contains") locks = "cmd:perm(find) or perm(Builder)" help_category = "Building" From bde11edaf0e3f4e5380915b1bb3ec447ceac34b7 Mon Sep 17 00:00:00 2001 From: Rachel Blackman Date: Sat, 21 Apr 2018 17:00:38 -0700 Subject: [PATCH 27/29] Switch /contains to default, add /startswith switch instead. --- evennia/commands/default/building.py | 24 ++++++++++++------------ evennia/commands/default/tests.py | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index dee9cd737e..3cda726881 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -2270,12 +2270,12 @@ class CmdFind(COMMAND_DEFAULT_CLASS): @locate - this is a shorthand for using the /loc switch. Switches: - room - only look for rooms (location=None) - exit - only look for exits (destination!=None) - char - only look for characters (BASE_CHARACTER_TYPECLASS) - exact - only exact matches are returned. - loc - display object location if exists and match has one result - contains- search for names containing the string, rather than starting with. + room - only look for rooms (location=None) + exit - only look for exits (destination!=None) + char - only look for characters (BASE_CHARACTER_TYPECLASS) + exact - only exact matches are returned. + loc - display object location if exists and match has one result + startswith - search for names starting with the string, rather than containing Searches the database for an object of a particular name or exact #dbref. Use *accountname to search for an account. The switches allows for @@ -2286,7 +2286,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS): key = "@find" aliases = "@search, @locate" - switch_options = ("room", "exit", "char", "exact", "loc", "contains") + switch_options = ("room", "exit", "char", "exact", "loc", "startswith") locks = "cmd:perm(find) or perm(Builder)" help_category = "Building" @@ -2360,14 +2360,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS): keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__iexact=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) - elif "contains" in switches: - keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high) - aliasquery = Q(db_tags__db_key__icontains=searchstring, - db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) - else: + elif "startswith" in switches: keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high) aliasquery = Q(db_tags__db_key__istartswith=searchstring, db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) + else: + keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high) + aliasquery = Q(db_tags__db_key__icontains=searchstring, + db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) results = ObjectDB.objects.filter(keyquery | aliasquery).distinct() nresults = results.count() diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 37e4b07b03..f296ca61b6 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -329,7 +329,7 @@ class TestBuilding(CommandTest): self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.") def test_find(self): - self.call(building.CmdFind(), "Room2", "One Match") + self.call(building.CmdFind(), "oom2", "One Match") expect = "One Match(#1#7, loc):\n " +\ "Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))" self.call(building.CmdFind(), "Char2", expect, cmdstring="locate") @@ -339,7 +339,7 @@ class TestBuilding(CommandTest): self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate") self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find") - self.call(building.CmdFind(), "/contains om2", "One Match") + self.call(building.CmdFind(), "/startswith Room2", "One Match") def test_script(self): self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added") From c9ecd3b9970566f3ace8c1b1efbaa34107e93f17 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 22 Apr 2018 22:28:43 +0200 Subject: [PATCH 28/29] Add check_lockstring as a function in locks/lockhandler.py --- evennia/locks/lockhandler.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index b8801f9655..14ac34a989 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -541,6 +541,42 @@ class LockHandler(object): return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks) +# convenience access function + +# dummy to be able to call check_lockstring from the outside +_LOCK_HANDLER = LockHandler() + + +def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False, + default=False, access_type=None): + """ + Do a direct check against a lockstring ('atype:func()..'), + without any intermediary storage on the accessed object. + + Args: + accessing_obj (object or None): The object seeking access. + Importantly, this can be left unset if the lock functions + don't access it, no updating or storage of locks are made + against this object in this method. + lockstring (str): Lock string to check, on the form + `"access_type:lock_definition"` where the `access_type` + part can potentially be set to a dummy value to just check + a lock condition. + no_superuser_bypass (bool, optional): Force superusers to heed lock. + default (bool, optional): Fallback result to use if `access_type` is set + but no such `access_type` is found in the given `lockstring`. + access_type (str, bool): If set, only this access_type will be looked up + among the locks defined by `lockstring`. + + Return: + access (bool): If check is passed or not. + + """ + return _LOCK_HANDLER.check_lockstring( + accessing_obj, lockstring, no_superuser_bypass=no_superuser_bypass, + default=default, access_type=access_type) + + def _test(): # testing From 2f985c882b926ddda2d9ac8ef1e915e38b771c60 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 25 Apr 2018 22:34:33 +0200 Subject: [PATCH 29/29] Fix lockhandler singleton implmentation with a dummyobj --- evennia/locks/lockhandler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 14ac34a989..14556579d7 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -544,7 +544,11 @@ class LockHandler(object): # convenience access function # dummy to be able to call check_lockstring from the outside -_LOCK_HANDLER = LockHandler() + +class _ObjDummy: + lock_storage = '' + +_LOCK_HANDLER = LockHandler(_ObjDummy()) def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False,