mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 08:16:30 +01:00
Merge with develop branch, resolve conflicts
This commit is contained in:
commit
d71b281b56
18 changed files with 738 additions and 201 deletions
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet):
|
|||
self.add(unloggedin.CmdUnconnectedHelp())
|
||||
self.add(unloggedin.CmdUnconnectedEncoding())
|
||||
self.add(unloggedin.CmdUnconnectedScreenreader())
|
||||
self.add(unloggedin.CmdUnconnectedInfo())
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
||||
|
|
|
|||
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal file
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal 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,
|
||||
}
|
||||
})();
|
||||
|
|
@ -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");
|
||||
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -8,20 +8,29 @@
|
|||
|
||||
|
||||
{% block client %}
|
||||
<div id="toolbar">
|
||||
<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Settings</span></button>
|
||||
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⇹<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">↶<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||
</div>
|
||||
|
||||
<div id="wrapper">
|
||||
<div id="toolbar">
|
||||
<button id="optionsbutton" type="button" class="hidden">⚙</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=">"/>
|
||||
<!-- 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">×</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="">></button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue