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 kwargs["options"] = options
if text is not None:
if not (isinstance(text, basestring) or isinstance(text, tuple)): if not (isinstance(text, basestring) or isinstance(text, tuple)):
# sanitize text before sending across the wire # sanitize text before sending across the wire
try: try:
text = to_str(text, force_string=True) text = to_str(text, force_string=True)
except Exception: except Exception:
text = repr(text) text = repr(text)
kwargs['text'] = text
# session relay # session relay
sessions = make_iter(session) if session else self.sessions.all() sessions = make_iter(session) if session else self.sessions.all()
for session in sessions: for session in sessions:
session.data_out(text=text, **kwargs) session.data_out(**kwargs)
def execute_cmd(self, raw_string, session=None, **kwargs): def execute_cmd(self, raw_string, session=None, **kwargs):
""" """

View file

@ -2327,8 +2327,9 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
room - only look for rooms (location=None) room - only look for rooms (location=None)
exit - only look for exits (destination!=None) exit - only look for exits (destination!=None)
char - only look for characters (BASE_CHARACTER_TYPECLASS) char - only look for characters (BASE_CHARACTER_TYPECLASS)
exact- only exact matches are returned. exact - only exact matches are returned.
loc - display object location if exists and match has one result 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. Searches the database for an object of a particular name or exact #dbref.
Use *accountname to search for an account. The switches allows for Use *accountname to search for an account. The switches allows for
@ -2339,7 +2340,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
key = "@find" key = "@find"
aliases = "@search, @locate" 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)" locks = "cmd:perm(find) or perm(Builder)"
help_category = "Building" help_category = "Building"
@ -2413,10 +2414,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
aliasquery = Q(db_tags__db_key__iexact=searchstring, aliasquery = Q(db_tags__db_key__iexact=searchstring,
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) 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) keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
aliasquery = Q(db_tags__db_key__istartswith=searchstring, aliasquery = Q(db_tags__db_key__istartswith=searchstring,
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) 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() results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
nresults = results.count() nresults = results.count()

View file

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

View file

@ -71,7 +71,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
target = caller.search(self.args) target = caller.search(self.args)
if not target: if not target:
return return
self.msg(caller.at_look(target)) self.msg((caller.at_look(target), {'type':'look'}), options=None)
class CmdNick(COMMAND_DEFAULT_CLASS): class CmdNick(COMMAND_DEFAULT_CLASS):

View file

@ -14,16 +14,17 @@ main test suite started with
import re import re
import types import types
import datetime
from django.conf import settings from django.conf import settings
from mock import Mock, mock from mock import Mock, mock
from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import EvenniaTest 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.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand 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.server.sessionhandler import SESSIONS
from evennia import search_object from evennia import search_object
from evennia import DefaultObject, DefaultCharacter from evennia import DefaultObject, DefaultCharacter
@ -76,7 +77,8 @@ class CommandTest(EvenniaTest):
old_msg = receiver.msg old_msg = receiver.msg
try: try:
receiver.msg = Mock() receiver.msg = Mock()
cmdobj.at_pre_cmd() if cmdobj.at_pre_cmd():
return
cmdobj.parse() cmdobj.parse()
ret = cmdobj.func() ret = cmdobj.func()
if isinstance(ret, types.GeneratorType): 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.") self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
def test_find(self): 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 " +\ expect = "One Match(#1#7, loc):\n " +\
"Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))" "Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))"
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate") 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(), "Char2", expect, cmdstring="@locate")
self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc 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(), "Char2", "One Match", cmdstring="@find")
self.call(building.CmdFind(), "/startswith Room2", "One Match")
def test_script(self): def test_script(self):
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added") self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
@ -500,3 +503,12 @@ class TestInterruptCommand(CommandTest):
def test_interrupt_command(self): def test_interrupt_command(self):
ret = self.call(CmdInterrupt(), "") ret = self.call(CmdInterrupt(), "")
self.assertEqual(ret, "") 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 re
import time import time
import datetime
from collections import defaultdict from collections import defaultdict
from random import getrandbits from random import getrandbits
from django.conf import settings from django.conf import settings
@ -11,8 +12,9 @@ from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.comms.models import ChannelDB 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 from evennia.commands.cmdhandler import CMD_LOGINSTART
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) 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) 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): def _create_account(session, accountname, password, permissions, typeclass=None, email=None):
""" """
Helper function, creates an account of the specified typeclass. Helper function, creates an account of the specified typeclass.

View file

@ -253,7 +253,7 @@ class CmdMail(default_cmds.MuxCommand):
index += 1 index += 1
table.reformat_column(0, width=6) 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(2, width=34)
table.reformat_column(3, width=13) table.reformat_column(3, width=13)
table.reformat_column(4, width=7) table.reformat_column(4, width=7)

View file

@ -1088,7 +1088,7 @@ class CmdMask(RPCommand):
if self.cmdstring == "mask": if self.cmdstring == "mask":
# wear a mask # wear a mask
if not self.args: if not self.args:
caller.msg("Usage: (un)wearmask sdesc") caller.msg("Usage: (un)mask sdesc")
return return
if caller.db.unmasked_sdesc: if caller.db.unmasked_sdesc:
caller.msg("You are already wearing a mask.") caller.msg("You are already wearing a mask.")
@ -1111,7 +1111,7 @@ class CmdMask(RPCommand):
del caller.db.unmasked_sdesc del caller.db.unmasked_sdesc
caller.locks.remove("enable_recog") caller.locks.remove("enable_recog")
caller.sdesc.add(old_sdesc) 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): class RPSystemCmdSet(CmdSet):

View file

@ -798,7 +798,7 @@ from evennia.contrib import talking_npc
class TestTalkingNPC(CommandTest): class TestTalkingNPC(CommandTest):
def test_talkingnpc(self): def test_talkingnpc(self):
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) 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() npc.delete()

View file

@ -607,6 +607,46 @@ class LockHandler(object):
accessing_obj, locks, access_type) for access_type in locks) 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(): def _test():
# testing # testing

View file

@ -569,17 +569,19 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
except Exception: except Exception:
logger.log_trace() logger.log_trace()
if text is not None:
if not (isinstance(text, basestring) or isinstance(text, tuple)): if not (isinstance(text, basestring) or isinstance(text, tuple)):
# sanitize text before sending across the wire # sanitize text before sending across the wire
try: try:
text = to_str(text, force_string=True) text = to_str(text, force_string=True)
except Exception: except Exception:
text = repr(text) text = repr(text)
kwargs['text'] = text
# relay to session(s) # relay to session(s)
sessions = make_iter(session) if session else self.sessions.all() sessions = make_iter(session) if session else self.sessions.all()
for session in sessions: for session in sessions:
session.data_out(text=text, **kwargs) session.data_out(**kwargs)
def for_contents(self, func, exclude=None, **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("\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): def message(obj, from_obj):
obj.msg("%s has entered the game." % self.get_display_name(obj), from_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. 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")) 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")) 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: Args:
option_generator (callable or list): A list of strings indicating the options, or a callable 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. that is called as option_generator(caller) to produce such a list.
select (callable, option): Will be called as select(caller, menuchoice) select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
where menuchoice is the chosen option as a string. Should return the target node to contain the `available_choices` list and `selection` will hold one of the elements in
goto after this selection (or None to repeat the list-node). Note that if this is not that list. If a callable, it will be called as select(caller, menuchoice) where
given, the decorated node must itself provide a way to continue from the node! 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. pagesize (int): How many options to show per page.
Example: Example:
@ -1034,11 +1040,17 @@ def list_node(option_generator, select=None, pagesize=10):
except Exception: except Exception:
caller.msg("|rInvalid choice.|n") caller.msg("|rInvalid choice.|n")
else: else:
if select: if callable(select):
try: try:
return select(caller, selection) return select(caller, selection)
except Exception: except Exception:
logger.log_trace() 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 return None
def _list_node(caller, raw_string, **kwargs): def _list_node(caller, raw_string, **kwargs):

View file

@ -893,6 +893,9 @@ class EvColumn(object):
""" """
col = self.column 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) kwargs.update(self.options)
# use fixed width or adjust to the largest cell # use fixed width or adjust to the largest cell
if "width" not in kwargs: if "width" not in kwargs:
@ -1283,24 +1286,58 @@ class EvTable(object):
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable] cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
cwmin = sum(cwidths_min) cwmin = sum(cwidths_min)
if cwmin > width: # get which cols have separately set widths - these should be locked
# we cannot shrink any more # note that we need to remove cwidths_min for each lock to avoid counting
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin)) # 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: if self.evenwidth:
# make each column of equal width # 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 # flood-fill the minimum table starting with the smallest columns
ci = cwidths_min.index(min(cwidths_min)) 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 cwidths_min[ci] += 1
correction += 1
cwidths = cwidths_min cwidths = cwidths_min
else: else:
# make each column expand more proportional to their data size # 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 # fill wider columns first
ci = cwidths.index(max(cwidths)) ci = cwidths.index(max(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 cwidths_min[ci] += 1
correction += 1
# give a just changed col less prio next run
cwidths[ci] -= 3 cwidths[ci] -= 3
cwidths = cwidths_min cwidths = cwidths_min
@ -1323,28 +1360,46 @@ class EvTable(object):
for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)] for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
chmin = sum(cheights_min) 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: if chmin > self.height:
# we cannot shrink any more # 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. # 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 # We do this so that the tallest cells gets expanded first (and
# thus avoid getting cropped) # thus avoid getting cropped)
excess = self.height - chmin
even = self.height % 2 == 0 even = self.height % 2 == 0
for position in range(excess): correction = 0
while correction < excess:
# expand the cells with the most rows first # 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) # avoid adding to header first round (looks bad on very small tables)
ci = cheights[1:].index(max(cheights[1:])) + 1 ci = cheights[1:].index(max(cheights[1:])) + 1
else: else:
ci = cheights.index(max(cheights)) ci = cheights.index(max(cheights))
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 cheights_min[ci] += 1
# change balance
if ci == 0 and self.header: if ci == 0 and self.header:
# it doesn't look very good if header expands too fast # it doesn't look very good if header expands too fast
cheights[ci] -= 2 if even else 3 cheights[ci] -= 2 if even else 3
cheights[ci] -= 2 if even else 1 cheights[ci] -= 2 if even else 1
correction += 1
cheights = cheights_min cheights = cheights_min
# we must tell cells to crop instead of expanding # we must tell cells to crop instead of expanding
@ -1554,6 +1609,8 @@ class EvTable(object):
""" """
if index > len(self.table): if index > len(self.table):
raise Exception("Not a valid column index") 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].options.update(kwargs)
self.table[index].reformat(**kwargs) self.table[index].reformat(**kwargs)
@ -1569,6 +1626,7 @@ class EvTable(object):
def __str__(self): def __str__(self):
"""print table (this also balances it)""" """print table (this also balances it)"""
# h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()]))) return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()])))
def __unicode__(self): def __unicode__(self):

View file

@ -8,10 +8,11 @@
--- */ --- */
/* Overall element look */ /* Overall element look */
html, body, #clientwrapper { height: 100% } html, body {
height: 100%;
width: 100%;
}
body { body {
margin: 0;
padding: 0;
background: #000; background: #000;
color: #ccc; color: #ccc;
font-size: .9em; font-size: .9em;
@ -19,6 +20,12 @@ body {
line-height: 1.6em; line-height: 1.6em;
overflow: hidden; overflow: hidden;
} }
@media screen and (max-width: 480px) {
body {
font-size: .5rem;
line-height: .7rem;
}
}
a:link, a:visited { color: inherit; } a:link, a:visited { color: inherit; }
@ -74,93 +81,109 @@ div {margin:0px;}
} }
/* Style specific classes corresponding to formatted, narative text. */ /* Style specific classes corresponding to formatted, narative text. */
.wrapper {
height: 100%;
}
/* Container surrounding entire client */ /* Container surrounding entire client */
#wrapper { #clientwrapper {
position: relative; height: 100%;
height: 100%
} }
/* Main scrolling message area */ /* Main scrolling message area */
#messagewindow { #messagewindow {
position: absolute; overflow-y: auto;
overflow: auto; overflow-x: hidden;
padding: 1em; overflow-wrap: break-word;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
top: 0;
left: 0;
right: 0;
bottom: 70px;
} }
/* Input area containing input field and button */ #messagewindow {
#inputform { overflow-y: auto;
position: absolute; overflow-x: hidden;
width: 100%; overflow-wrap: break-word;
padding: 0;
bottom: 0;
margin: 0;
padding-bottom: 10px;
border-top: 1px solid #555;
}
#inputcontrol {
width: 100%;
padding: 0;
} }
/* Input field */ /* Input field */
#inputfield, #inputsend, #inputsizer { #inputfield, #inputsizer {
display: block; height: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
height: 50px;
background: #000; background: #000;
color: #fff; color: #fff;
padding: 0 .45em; padding: 0 .45rem;
font-size: 1.1em; font-size: 1.1rem;
font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace; font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace;
}
#inputfield, #inputsizer {
float: left;
width: 95%;
border: 0;
resize: none; resize: none;
line-height: normal; }
#inputsend {
height: 100%;
}
#inputcontrol {
height: 100%;
} }
#inputfield:focus { #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 area above input field */
#prompt { .prompt {
margin-top: 10px; max-height: 3rem;
padding: 0 .45em; }
#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 { #optionsbutton {
width: 40px; width: 2rem;
font-size: 20px; font-size: 2rem;
color: #a6a6a6; color: #a6a6a6;
background-color: transparent; background-color: transparent;
border: 0px; border: 0px;
@ -173,8 +196,8 @@ div {margin:0px;}
#toolbar { #toolbar {
position: fixed; position: fixed;
top: 0; top: .5rem;
right: 5px; right: .5rem;
z-index: 1; z-index: 1;
} }
@ -248,6 +271,52 @@ div {margin:0px;}
text-decoration: none; text-decoration: none;
cursor: pointer; 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 */ /* 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 () { (function () {
"use strict" "use strict"
var num_splits = 0; //unique id counter for default split-panel names
var options = {}; var options = {};
var known_types = new Array();
known_types.push('help');
// //
// GUI Elements // GUI Elements
// //
@ -106,6 +111,7 @@ function togglePopup(dialogname, content) {
// Grab text from inputline and send to Evennia // Grab text from inputline and send to Evennia
function doSendText() { function doSendText() {
console.log("sending text");
if (!Evennia.isConnected()) { if (!Evennia.isConnected()) {
var reconnect = confirm("Not currently connected. Reconnect?"); var reconnect = confirm("Not currently connected. Reconnect?");
if (reconnect) { if (reconnect) {
@ -158,7 +164,11 @@ function onKeydown (event) {
var code = event.which; var code = event.which;
var history_entry = null; var history_entry = null;
var inputfield = $("#inputfield"); var inputfield = $("#inputfield");
inputfield.focus(); if (code === 9) {
return;
}
//inputfield.focus();
if (code === 13) { // Enter key sends text if (code === 13) { // Enter key sends text
doSendText(); doSendText();
@ -205,74 +215,68 @@ function onKeyPress (event) {
} }
var resizeInputField = function () { var resizeInputField = function () {
var min_height = 50; return function() {
var max_height = 300; var wrapper = $("#inputform")
var prev_text_len = 0; var input = $("#inputcontrol")
var prompt = $("#prompt")
// Check to see if we should change the height of the input area input.height(wrapper.height() - (input.offset().top - wrapper.offset().top));
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;
} }
}(); }();
// Handle resizing of client // Handle resizing of client
function doWindowResize() { function doWindowResize() {
var formh = $('#inputform').outerHeight(true); resizeInputField();
var message_scrollh = $("#messagewindow").prop("scrollHeight"); var resizable = $("[data-update-append]");
$("#messagewindow") var parents = resizable.closest(".split")
.css({"bottom": formh}) // leave space for the input form parents.animate({
.scrollTop(message_scrollh); // keep the output window scrolled to the bottom scrollTop: parents.prop("scrollHeight")
}, 0);
} }
// Handle text coming from the server // Handle text coming from the server
function onText(args, kwargs) { function onText(args, kwargs) {
// append message to previous ones, then scroll so latest is at var use_default_pane = true;
// the bottom. Send 'cls' kwarg to modify the output class.
var renderto = "main"; if ( kwargs && 'type' in kwargs ) {
if (kwargs["type"] == "help") { var msgtype = kwargs['type'];
if (("helppopup" in options) && (options["helppopup"])) { if ( ! known_types.includes(msgtype) ) {
renderto = "#helpdialog"; // 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 mwin = $("#messagewindow");
var cls = kwargs == null ? 'out' : kwargs['cls']; var cls = kwargs == null ? 'out' : kwargs['cls'];
mwin.append("<div class='" + cls + "'>" + args[0] + "</div>"); mwin.append("<div class='" + cls + "'>" + args[0] + "</div>");
mwin.animate({ var scrollHeight = mwin.parent().parent().prop("scrollHeight");
scrollTop: document.getElementById("messagewindow").scrollHeight mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0);
}, 0);
onNewLine(args[0], null); onNewLine(args[0], null);
} else {
openPopup(renderto, args[0]);
} }
} }
@ -430,6 +434,105 @@ function doStartDragDialog(event) {
$(document).bind("mouseup", undrag); $(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 // Register Events
// //
@ -437,6 +540,18 @@ function doStartDragDialog(event) {
// Event when client finishes loading // Event when client finishes loading
$(document).ready(function() { $(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) { if ("Notification" in window) {
Notification.requestPermission(); Notification.requestPermission();
} }
@ -453,7 +568,7 @@ $(document).ready(function() {
//$(document).on("visibilitychange", onVisibilityChange); //$(document).on("visibilitychange", onVisibilityChange);
$("#inputfield").bind("resize", doWindowResize) $("[data-role-input]").bind("resize", doWindowResize)
.keypress(onKeyPress) .keypress(onKeyPress)
.bind("paste", resizeInputField) .bind("paste", resizeInputField)
.bind("cut", resizeInputField); .bind("cut", resizeInputField);
@ -506,6 +621,7 @@ $(document).ready(function() {
}, },
60000*3 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 http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" />
<meta name="author" content="Evennia" /> <meta name="author" content="Evennia" />
<meta name="generator" 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" %}> <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 --> <!-- Import JQuery and warn if there is a problem -->
{% block jquery_import %} {% 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 %} {% endblock %}
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
@ -29,6 +33,14 @@ JQuery available.
} }
</script> </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--> <!-- Set up Websocket url and load the evennia.js library-->
<script language="javascript" type="text/javascript"> <script language="javascript" type="text/javascript">
{% if websocket_enabled %} {% if websocket_enabled %}
@ -51,6 +63,12 @@ JQuery available.
</script> </script>
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></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 --> <!-- Load gui library -->
{% block guilib_import %} {% block guilib_import %}
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script> <script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
@ -63,7 +81,11 @@ JQuery available.
} }
</script> </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> </head>
<body> <body>
@ -86,10 +108,9 @@ JQuery available.
</div> </div>
<!-- main client --> <!-- main client -->
<div id=clientwrapper> <div id=clientwrapper class="d-none">
{% block client %} {% block client %}
{% endblock %} {% endblock %}
</div> </div>
</body> </body>
</html> </html>

View file

@ -8,20 +8,29 @@
{% block client %} {% 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"> <!-- The "Main" Content -->
<div id="toolbar"> <div id="main" class="split split-vertical" data-role-default>
<button id="optionsbutton" type="button" class="hidden">&#x2699;</button> <div id="main-sub" class="split-sub">
<div id="messagewindow"></div>
</div> </div>
<div id="messagewindow" role="log"></div> </div>
<div id="inputform"> <!-- The "Input" Pane -->
<div id="prompt"></div> <div id="input" class="split split-vertical" data-role-input data-update-append></div>
<div id="inputcontrol">
<textarea id="inputfield" type="text"></textarea> <!-- Basic UI Components -->
<input id="inputsend" type="button" value="&gt;"/> <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> </div>
<div id="inputsizer"></div>
</div> </div>
<div id="optionsdialog" class="dialog"> <div id="optionsdialog" class="dialog">
@ -47,4 +56,29 @@
</div> </div>
</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 %} {% endblock %}