Merge with develop branch, resolve conflicts

This commit is contained in:
Griatch 2018-05-09 17:39:10 +02:00
commit d71b281b56
18 changed files with 738 additions and 201 deletions

View file

@ -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):
"""

View file

@ -2324,11 +2324,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
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
@ -2339,7 +2340,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", "startswith")
locks = "cmd:perm(find) or perm(Builder)"
help_category = "Building"
@ -2413,10 +2414,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)
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()

View file

@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet):
self.add(unloggedin.CmdUnconnectedHelp())
self.add(unloggedin.CmdUnconnectedEncoding())
self.add(unloggedin.CmdUnconnectedScreenreader())
self.add(unloggedin.CmdUnconnectedInfo())

View file

@ -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):

View file

@ -14,16 +14,17 @@ 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.default.muxcommand import MuxCommand
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
@ -76,7 +77,8 @@ class CommandTest(EvenniaTest):
old_msg = receiver.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):
@ -328,7 +330,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")
@ -338,6 +340,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(), "/startswith Room2", "One Match")
def test_script(self):
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
@ -500,3 +503,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().replace("-", ""))
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)

View file

@ -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,24 @@ 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. 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\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):
"""
Helper function, creates an account of the specified typeclass.

View file

@ -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)

View file

@ -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):

View file

@ -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
@ -697,7 +697,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)
@ -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)

View file

@ -607,6 +607,46 @@ class LockHandler(object):
accessing_obj, locks, access_type) for access_type in locks)
# convenience access function
# dummy to be able to call check_lockstring from the outside
class _ObjDummy:
lock_storage = ''
_LOCK_HANDLER = LockHandler(_ObjDummy())
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

View file

@ -569,17 +569,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):
@ -1873,7 +1875,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)

View file

@ -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"))
@ -1001,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:
@ -1034,11 +1040,17 @@ 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()
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):

View file

@ -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):

View file

@ -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,109 @@ 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 {
width: 2rem;
font-size: 2rem;
color: #a6a6a6;
background-color: transparent;
border: 0px;
}
#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;
}
#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;
color: black;
border: 1px solid black;
background-color: darkgray;
margin: 0 auto;
}
.splitbutton:hover {
cursor: pointer;
}
#optionsbutton {
width: 40px;
font-size: 20px;
width: 2rem;
font-size: 2rem;
color: #a6a6a6;
background-color: transparent;
border: 0px;
@ -173,8 +196,8 @@ div {margin:0px;}
#toolbar {
position: fixed;
top: 0;
right: 5px;
top: .5rem;
right: .5rem;
z-index: 1;
}
@ -248,6 +271,52 @@ 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;
}
.split-sub {
padding: .5rem;
}
.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 */

View file

@ -0,0 +1,145 @@
// 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;
}
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';
// create the new div stack to replace the sub-div with.
var first_div = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-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-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 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.
$('#'+splitpane).append(first_div);
$('#'+splitpane).append(second_div);
$('#'+splitpane+'-sub').remove();
// And split
Split(['#'+pane_name1,'#'+pane_name2], {
direction: direction,
sizes: sizes,
gutterSize: 4,
minSize: [50,50],
});
// 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 = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-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'];
}
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],
});
split_panes['main'] = { 'types': [], 'update_method': 'append' };
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,
undo_split: undo_split,
}
})();

View file

@ -15,8 +15,13 @@
(function () {
"use strict"
var num_splits = 0; //unique id counter for default split-panel names
var options = {};
var known_types = new Array();
known_types.push('help');
//
// GUI Elements
//
@ -106,6 +111,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,7 +164,11 @@ function onKeydown (event) {
var code = event.which;
var history_entry = null;
var inputfield = $("#inputfield");
inputfield.focus();
if (code === 9) {
return;
}
//inputfield.focus();
if (code === 13) { // Enter key sends text
doSendText();
@ -205,74 +215,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);
}
// 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;
}
}
}
}
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("<div class='" + cls + "'>" + args[0] + "</div>");
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);
} else {
openPopup(renderto, args[0]);
}
}
@ -430,6 +434,105 @@ 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");
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();
dialog.append("<h3>Split?</h3>");
dialog.append('<input type="radio" name="direction" value="vertical" checked> top/bottom<br />');
dialog.append('<input type="radio" name="direction" value="horizontal"> side-by-side<br />');
dialog.append("<h3>Split Which Pane?</h3>");
for ( var pane in SplitHandler.split_panes ) {
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
}
dialog.append("<h3>New Pane Names</h3>");
dialog.append('<input type="text" name="new_pane1" value="" />');
dialog.append('<input type="text" name="new_pane2" value="" />');
dialog.append("<h3>New First Pane</h3>");
dialog.append('<input type="radio" name="flow1" value="append" checked>append new incoming messages<br />');
dialog.append('<input type="radio" name="flow1" value="replace">replace old messages with new ones<br />');
dialog.append("<h3>New Second Pane</h3>");
dialog.append('<input type="radio" name="flow2" value="append" checked>append new incoming messages<br />');
dialog.append('<input type="radio" name="flow2" value="replace">replace old messages with new ones<br />');
dialog.append('<div id="splitclose" class="button">Split It</div>');
$("#splitclose").bind("click", onSplitDialogClose);
togglePopup("#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("<h3>Set Which Pane?</h3>");
for ( var pane in SplitHandler.split_panes ) {
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
}
dialog.append("<h3>Which content types?</h3>");
for ( var type in known_types ) {
dialog.append('<input type="checkbox" value="'+ known_types[type] +'">'+ known_types[type] +'<br />');
}
dialog.append('<div id="paneclose" class="button">Make It So</div>');
$("#paneclose").bind("click", onPaneControlDialogClose);
togglePopup("#splitdialog");
}
//
// Register Events
//
@ -437,6 +540,18 @@ function doStartDragDialog(event) {
// Event when client finishes loading
$(document).ready(function() {
if( SplitHandler ) {
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) {
Notification.requestPermission();
}
@ -453,7 +568,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);
@ -506,6 +621,7 @@ $(document).ready(function() {
},
60000*3
);
console.log("Completed GUI setup");
});

View file

@ -13,6 +13,10 @@ JQuery available.
<meta http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" />
<meta name="author" content="Evennia" />
<meta name="generator" content="Evennia" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link rel='stylesheet' type="text/css" media="screen" href={% static "webclient/css/webclient.css" %}>
@ -20,7 +24,7 @@ JQuery available.
<!-- Import JQuery and warn if there is a problem -->
{% block jquery_import %}
<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript" charset="utf-8"></script>
<script src="https://code.jquery.com/jquery-3.2.1.min.js" type="text/javascript" charset="utf-8"></script>
{% endblock %}
<script type="text/javascript" charset="utf-8">
@ -29,6 +33,14 @@ JQuery available.
}
</script>
<!-- This is will only fire if javascript is actually active -->
<script language="javascript" type="text/javascript">
$(document).ready(function() {
$('#noscript').remove();
$('#clientwrapper').removeClass('d-none');
})
</script>
<!-- Set up Websocket url and load the evennia.js library-->
<script language="javascript" type="text/javascript">
{% if websocket_enabled %}
@ -51,6 +63,12 @@ JQuery available.
</script>
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
<!-- set up splits before loading the GUI -->
<script src="https://unpkg.com/split.js/split.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<script src={% static "webclient/js/splithandler.js" %} language="javascript"></script>
<!-- Load gui library -->
{% block guilib_import %}
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
@ -63,7 +81,11 @@ JQuery available.
}
</script>
<!-- jQuery first, then Tether, then Bootstrap JS. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
{% block scripts %}
{% endblock %}
</head>
<body>
@ -86,10 +108,9 @@ JQuery available.
</div>
<!-- main client -->
<div id=clientwrapper>
<div id=clientwrapper class="d-none">
{% block client %}
{% endblock %}
</div>
</body>
</html>

View file

@ -8,20 +8,29 @@
{% block client %}
<div id="toolbar">
<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x2699;<span class="sr-only sr-only-focusable">Settings</span></button>
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x21f9;<span class="sr-only sr-only-focusable">Splits</span></button>
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x2699;<span class="sr-only sr-only-focusable">Splits</span></button>
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x21B6;<span class="sr-only sr-only-focusable">Splits</span></button>
</div>
<div id="wrapper">
<div id="toolbar">
<button id="optionsbutton" type="button" class="hidden">&#x2699;</button>
</div>
<div id="messagewindow" role="log"></div>
<div id="inputform">
<div id="prompt"></div>
<div id="inputcontrol">
<textarea id="inputfield" type="text"></textarea>
<input id="inputsend" type="button" value="&gt;"/>
<!-- The "Main" Content -->
<div id="main" class="split split-vertical" data-role-default>
<div id="main-sub" class="split-sub">
<div id="messagewindow"></div>
</div>
</div>
<!-- The "Input" Pane -->
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
<!-- Basic UI Components -->
<div id="splitdialog" class="dialog">
<div class="dialogtitle">Split Pane<span class="dialogclose">&times;</span></div>
<div class="dialogcontentparent">
<div id="splitdialogcontent" class="dialogcontent">
</div>
</div>
<div id="inputsizer"></div>
</div>
<div id="optionsdialog" class="dialog">
@ -47,4 +56,29 @@
</div>
</div>
<script type="text/html" id="split-template">
<div class="split content<%#horizontal%> split-horizontal<%/horizontal%>" id='<%id%>'>
</div>
</script>
<script type="text/html" id="output-template">
<div id="<%id%>" role="log" data-role-output data-update-append data-tags='[<%#tags%>"<%.%>", <%/tags%>]'></div>
</script>
<script type="text/html" id="input-template">
<div id="inputform" class="wrapper">
<div id="prompt" class="prompt">
</div>
<div id="inputcontrol" class="input-group">
<textarea id="inputfield" type="text" class="form-control"></textarea>
<span class="input-group-btn">
<button class="btn btn-large btn-outline-primary" id="inputsend" type="button" value="">&gt;</button>
</span>
</div>
</div>
</script>
{% endblock %}
{% block scripts %}
{% endblock %}