|
#!/usr/bin/python |
|
# -*- coding: utf-8 -*- |
|
|
|
import os |
|
import sys |
|
import re |
|
import time |
|
from PySide2 import QtWidgets, QtGui, QtCore |
|
|
|
BASE_DIR = '.' |
|
|
|
|
|
XPM_ARROW_RIGHT = [ |
|
# "/* XPM */", |
|
# "static char *_7[] = {", |
|
# "/* columns rows colors chars-per-pixel */", |
|
"24 24 109 2 ", |
|
" c #00318A", |
|
". c #00308B", |
|
"X c #01318D", |
|
"o c #01338F", |
|
"O c #013491", |
|
"+ c #023593", |
|
"@ c #023793", |
|
"# c #023897", |
|
"$ c #043B97", |
|
"% c #053F9D", |
|
"& c #043C9E", |
|
"* c #053C9E", |
|
"= c #043D9E", |
|
"- c #043C9F", |
|
"; c #043D9F", |
|
": c #0744A3", |
|
"> c #0541A4", |
|
", c #0847AC", |
|
"< c #0948AA", |
|
"1 c #0C4CAC", |
|
"2 c #0B4EB1", |
|
"3 c #0B4EB3", |
|
"4 c #0D53B9", |
|
"5 c #0D54BC", |
|
"6 c #1154B5", |
|
"7 c #0F59C1", |
|
"8 c #0F5AC4", |
|
"9 c #115FC8", |
|
"0 c #1963C3", |
|
"q c #1D68C5", |
|
"w c #1160CB", |
|
"e c #1F6BC8", |
|
"r c #1364D0", |
|
"t c #1365D1", |
|
"y c #1469D6", |
|
"u c #1569D6", |
|
"i c #166DDC", |
|
"p c #166EDC", |
|
"a c #2371CD", |
|
"s c #2475D3", |
|
"d c #287BD6", |
|
"f c #287BD7", |
|
"g c #297BD7", |
|
"h c #2A7BD6", |
|
"j c #2A7ED8", |
|
"k c #2B7ED8", |
|
"l c #2A7FD8", |
|
"z c #2B7FD8", |
|
"x c #2A7ED9", |
|
"c c #2B7ED9", |
|
"v c #2D7FD8", |
|
"b c #1872E1", |
|
"n c #1872E2", |
|
"m c #1975E7", |
|
"M c #1976E6", |
|
"N c #1A78EA", |
|
"B c #2C80DA", |
|
"V c #2D80DA", |
|
"C c #2E83DC", |
|
"Z c #2F83DC", |
|
"A c #2E84DC", |
|
"S c #2F84DC", |
|
"D c #3184DC", |
|
"F c #3185DD", |
|
"G c #3185DE", |
|
"H c #3186DE", |
|
"J c #3288DF", |
|
"K c #3388DF", |
|
"L c #3289DF", |
|
"P c #3389DF", |
|
"I c #3288E0", |
|
"U c #3388E0", |
|
"Y c #3289E0", |
|
"T c #3389E0", |
|
"R c #368BE2", |
|
"E c #368CE1", |
|
"W c #358CE2", |
|
"Q c #368CE2", |
|
"! c #4195EB", |
|
"~ c #4998EA", |
|
"^ c #4E98E8", |
|
"/ c #579CE6", |
|
"( c #549EE6", |
|
") c #569FE6", |
|
"_ c #569FE7", |
|
"` c #5D9CE3", |
|
"' c #469CF0", |
|
"] c #6BA5E4", |
|
"[ c #63A6E8", |
|
"{ c #64A6E8", |
|
"} c #64A6E9", |
|
"| c #6EB6F5", |
|
" . c #78BAF3", |
|
".. c #84B9ED", |
|
"X. c #86BAED", |
|
"o. c #85BAEE", |
|
"O. c #86BAEE", |
|
"+. c #82BDF2", |
|
"@. c #8CC0F2", |
|
"#. c #8CC1F2", |
|
"$. c #94C3F1", |
|
"%. c #95C3F1", |
|
"&. c #91C8F7", |
|
"*. c #95C8F6", |
|
"=. c #BEDCF7", |
|
"-. c #BADAF8", |
|
";. c #D5E7F9", |
|
":. c #ECF4FC", |
|
">. c None", |
|
# "/* pixels */", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.N >.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.N N >.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.M ' M >.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.n &.! n >.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.p *.| ~ p >.>.>.>.>.", |
|
"y y y y y y y y y y y y y y y -. . .^ y >.>.>.>.", |
|
"r _ _ _ _ _ _ _ _ _ _ _ _ _ _ =.+.+.+._ r >.>.>.", |
|
"w [ [ [ [ [ [ [ { { [ [ [ [ { ;.@.@.@.@.` 9 >.>.", |
|
"8 O.O.O.O.O.O.O.O.O.........O.:.%.$.%.%.%.] 8 >.", |
|
"5 ( ( ( ( _ ( ( _ ( ( ( ( ( _ Z E E R R R R h 4 ", |
|
"3 J J J L L L J U U U U J J J C D G G G G a 3 >.", |
|
", A S A A A C C A A A A C C C z C z z C e < >.>.", |
|
"> k z k B l k k k B k k k k k d d d k q : >.>.>.", |
|
"- - - = & & & & & & = * = = = s s 0 6 = >.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.# 0 0 6 $ >.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.O 6 6 @ >.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.O 1 O >.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>. X >.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>. >.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.", |
|
">.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.>.", |
|
# "};" |
|
] |
|
|
|
|
|
class Properties(object): |
|
""" |
|
A Python replacement for java.util.Properties |
|
NOTE: Copied from https://bitbucket.org/jnoller/pyjavaproperties |
|
""" |
|
|
|
def __init__(self, props=None): |
|
|
|
# Note: We don't take a default properties object |
|
# as argument yet |
|
|
|
# Dictionary of properties. |
|
self._props = {} |
|
# Dictionary of properties with 'pristine' keys |
|
# This is used for dumping the properties to a file |
|
# using the 'store' method |
|
self._origprops = {} |
|
self._keyorder = [] |
|
# Dictionary mapping keys from property |
|
# dictionary to pristine dictionary |
|
self._keymap = {} |
|
|
|
self.othercharre = re.compile(r'(?<!\\)(\s*\=)|(?<!\\)(\s*\:)') |
|
self.othercharre2 = re.compile(r'(\s*\=)|(\s*\:)') |
|
self.bspacere = re.compile(r'\\(?!\s$)') |
|
|
|
def __str__(self): |
|
s = '{' |
|
for key, value in self._props.items(): |
|
s = ''.join((s, key, '=', value, ', ')) |
|
|
|
s = ''.join((s[:-2], '}')) |
|
return s |
|
|
|
def __parse(self, lines): |
|
""" Parse a list of lines and create |
|
an internal property dictionary """ |
|
|
|
# Every line in the file must consist of either a comment |
|
# or a key-value pair. A key-value pair is a line consisting |
|
# of a key which is a combination of non-white space characters |
|
# The separator character between key-value pairs is a '=', |
|
# ':' or a whitespace character not including the newline. |
|
# If the '=' or ':' characters are found, in the line, even |
|
# keys containing whitespace chars are allowed. |
|
|
|
# A line with only a key according to the rules above is also |
|
# fine. In such case, the value is considered as the empty string. |
|
# In order to include characters '=' or ':' in a key or value, |
|
# they have to be properly escaped using the backslash character. |
|
|
|
# Some examples of valid key-value pairs: |
|
# |
|
# key value |
|
# key=value |
|
# key:value |
|
# key value1,value2,value3 |
|
# key value1,value2,value3 \ |
|
# value4, value5 |
|
# key |
|
# This key= this value |
|
# key = value1 value2 value3 |
|
|
|
# Any line that starts with a '#' or '!' is considerered a comment |
|
# and skipped. Also any trailing or preceding whitespaces |
|
# are removed from the key/value. |
|
|
|
# This is a line parser. It parses the |
|
# contents like by line. |
|
|
|
lineno = 0 |
|
i = iter(lines) |
|
|
|
for line in i: |
|
lineno += 1 |
|
line = line.strip() |
|
# Skip null lines |
|
if not line: |
|
continue |
|
# Skip lines which are comments |
|
if line[0] in ('#', '!'): |
|
continue |
|
# Position of first separation char |
|
sepidx = -1 |
|
# Check for valid space separation |
|
# First obtain the max index to which we |
|
# can search. |
|
m = self.othercharre.search(line) |
|
if m: |
|
first, last = m.span() |
|
start, end = 0, first |
|
wspacere = re.compile(r'(?<![\\\=\:])(\s)') |
|
else: |
|
if self.othercharre2.search(line): |
|
# Check if either '=' or ':' is present |
|
# in the line. If they are then it means |
|
# they are preceded by a backslash. |
|
|
|
# This means, we need to modify the |
|
# wspacere a bit, not to look for |
|
# : or = characters. |
|
wspacere = re.compile(r'(?<![\\])(\s)') |
|
start, end = 0, len(line) |
|
|
|
m2 = wspacere.search(line, start, end) |
|
if m2: |
|
# print 'Space match=>',line |
|
# Means we need to split by space. |
|
first, last = m2.span() |
|
sepidx = first |
|
elif m: |
|
# print 'Other match=>',line |
|
# No matching wspace char found, need |
|
# to split by either '=' or ':' |
|
first, last = m.span() |
|
sepidx = last - 1 |
|
# print line[sepidx] |
|
|
|
# If the last character is a backslash |
|
# it has to be preceded by a space in which |
|
# case the next line is read as part of the |
|
# same property |
|
while line[-1] == '\\': |
|
# Read next line |
|
nextline = i.next() |
|
nextline = nextline.strip() |
|
lineno += 1 |
|
# This line will become part of the value |
|
line = line[:-1] + nextline |
|
|
|
# Now split to key,value according to separation char |
|
if sepidx != -1: |
|
key, value = line[:sepidx], line[sepidx + 1:] |
|
else: |
|
key, value = line, '' |
|
self._keyorder.append(key) |
|
self.processPair(key, value) |
|
|
|
def processPair(self, key, value): |
|
""" Process a (key, value) pair """ |
|
|
|
oldkey = key |
|
oldvalue = value |
|
|
|
# Create key intelligently |
|
keyparts = self.bspacere.split(key) |
|
# print keyparts |
|
|
|
strippable = False |
|
lastpart = keyparts[-1] |
|
|
|
if lastpart.find('\\ ') != -1: |
|
keyparts[-1] = lastpart.replace('\\', '') |
|
|
|
# If no backspace is found at the end, but empty |
|
# space is found, strip it |
|
elif lastpart and lastpart[-1] == ' ': |
|
strippable = True |
|
|
|
key = ''.join(keyparts) |
|
if strippable: |
|
key = key.strip() |
|
oldkey = oldkey.strip() |
|
|
|
oldvalue = self.unescape(oldvalue) |
|
value = self.unescape(value) |
|
|
|
# Patch from N B @ ActiveState |
|
curlies = re.compile("{.+?}") |
|
found = curlies.findall(value) |
|
|
|
for f in found: |
|
srcKey = f[1:-1] |
|
if srcKey in self._props: |
|
value = value.replace(f, self._props[srcKey], 1) |
|
|
|
self._props[key] = value.strip() |
|
|
|
# Check if an entry exists in pristine keys |
|
if key in self._keymap: |
|
oldkey = self._keymap.get(key) |
|
self._origprops[oldkey] = oldvalue.strip() |
|
else: |
|
self._origprops[oldkey] = oldvalue.strip() |
|
# Store entry in keymap |
|
self._keymap[key] = oldkey |
|
|
|
if key not in self._keyorder: |
|
self._keyorder.append(key) |
|
|
|
def escape(self, value): |
|
|
|
# Java escapes the '=' and ':' in the value |
|
# string with backslashes in the store method. |
|
# So let us do the same. |
|
newvalue = value.replace(':', '\:') |
|
newvalue = newvalue.replace('=', '\=') |
|
|
|
return newvalue |
|
|
|
def unescape(self, value): |
|
|
|
# Reverse of escape |
|
newvalue = value.replace('\:', ':') |
|
newvalue = newvalue.replace('\=', '=') |
|
|
|
return newvalue |
|
|
|
def load(self, stream): |
|
""" Load properties from an open file stream """ |
|
|
|
# For the time being only accept file input streams |
|
if type(stream) is not file: |
|
raise TypeError('Argument should be a file object!') |
|
# Check for the opened mode |
|
if stream.mode != 'r': |
|
raise ValueError('Stream should be opened in read-only mode!') |
|
|
|
try: |
|
lines = stream.readlines() |
|
self.__parse(lines) |
|
except IOError: |
|
raise |
|
|
|
def getProperty(self, key): |
|
""" Return a property for the given key """ |
|
|
|
return self._props.get(key, '') |
|
|
|
def setProperty(self, key, value): |
|
""" Set the property for the given key """ |
|
|
|
if type(key) is str and type(value) is str: |
|
self.processPair(key, value) |
|
else: |
|
raise TypeError('both key and value should be strings!') |
|
|
|
def propertyNames(self): |
|
""" Return an iterator over all the keys of the property |
|
dictionary, i.e the names of the properties """ |
|
|
|
return self._props.keys() |
|
|
|
def list(self, out=sys.stdout): |
|
""" Prints a listing of the properties to the |
|
stream 'out' which defaults to the standard output """ |
|
|
|
out.write('-- listing properties --\n') |
|
for key, value in self._props.items(): |
|
out.write(''.join((key, '=', value, '\n'))) |
|
|
|
def store(self, out, header=""): |
|
""" Write the properties list to the stream 'out' along |
|
with the optional 'header' """ |
|
|
|
if out.mode[0] != 'w': |
|
raise ValueError('Steam should be opened in write mode!') |
|
|
|
try: |
|
out.write(''.join(('#', header, '\n'))) |
|
# Write timestamp |
|
tstamp = time.strftime('%a %b %d %H:%M:%S %Z %Y', time.localtime()) |
|
out.write(''.join(('#', tstamp, '\n'))) |
|
# Write properties from the pristine dictionary |
|
for prop in self._keyorder: |
|
if prop in self._origprops: |
|
val = self._origprops[prop] |
|
out.write(''.join((prop, '=', self.escape(val), '\n'))) |
|
|
|
out.close() |
|
except IOError: |
|
raise |
|
|
|
def getPropertyDict(self): |
|
return self._props |
|
|
|
def __getitem__(self, name): |
|
""" To support direct dictionary like access """ |
|
|
|
return self.getProperty(name) |
|
|
|
def __setitem__(self, name, value): |
|
""" To support direct dictionary like access """ |
|
|
|
self.setProperty(name, value) |
|
|
|
def __getattr__(self, name): |
|
""" For attributes not found in self, redirect |
|
to the properties dictionary """ |
|
|
|
try: |
|
return self.__dict__[name] |
|
except KeyError: |
|
if hasattr(self._props, name): |
|
return getattr(self._props, name) |
|
|
|
|
|
class LevelManager(): |
|
def __init__(self, baseDirectory='.'): |
|
self.baseDirectory = baseDirectory |
|
|
|
def defineServerPropsFile(self): |
|
if not hasattr(self, 'serverPropsFile'): |
|
self.serverPropsFile = os.path.join(self.baseDirectory, 'server.properties') |
|
|
|
def getCurrentLevel(self): |
|
self.defineServerPropsFile() |
|
current = '' |
|
props = Properties() |
|
with open(self.serverPropsFile) as f: |
|
props.load(f) |
|
current = props.get('level-name') |
|
return current |
|
|
|
def setCurrentLevel(self, level): |
|
self.defineServerPropsFile() |
|
props = Properties() |
|
with open(self.serverPropsFile) as fin: |
|
props.load(fin) |
|
with open(self.serverPropsFile, 'w') as fout: |
|
props['level-name'] = str(level) |
|
props.store(fout) |
|
|
|
def availableLevels(self): |
|
available = [] |
|
for entry in os.listdir(self.baseDirectory): |
|
dirname = os.path.join(self.baseDirectory, entry) |
|
level_dat = os.path.join(dirname, 'level.dat') |
|
if os.path.isdir(dirname) and os.path.exists(level_dat): |
|
available.append(entry) |
|
return available |
|
|
|
|
|
class MainWidget(QtWidgets.QWidget): |
|
@QtCore.Slot() |
|
def on_click(self): |
|
selected_level = None |
|
new_level = self.text.strip() |
|
if new_level: |
|
selected_level = new_level |
|
self.edit.clear() |
|
else: |
|
for item in self.listWidget.selectedItems(): |
|
selected_level = item.text() |
|
break |
|
if selected_level: |
|
self.levelManager.setCurrentLevel(selected_level) |
|
self.reload_list() |
|
|
|
@QtCore.Slot() |
|
def on_edit(self, text): |
|
self.text = text |
|
|
|
def reload_list(self): |
|
found = False |
|
self.listWidget.clear() |
|
current = self.levelManager.getCurrentLevel() |
|
for entry in self.levelManager.availableLevels(): |
|
item = QtWidgets.QListWidgetItem(entry) |
|
if entry == current: |
|
found = True |
|
item.setIcon(self.icon) |
|
self.listWidget.addItem(item) |
|
if not found: |
|
item = QtWidgets.QListWidgetItem(current) |
|
item.setIcon(self.icon) |
|
self.listWidget.addItem(item) |
|
|
|
def __init__(self): |
|
QtWidgets.QWidget.__init__(self) |
|
self.levelManager = LevelManager(BASE_DIR) |
|
self.text = '' |
|
# main list |
|
self.verticalLayout = QtWidgets.QVBoxLayout(self) |
|
self.listWidget = QtWidgets.QListWidget() |
|
self.icon = QtGui.QIcon() |
|
self.icon.addPixmap(QtGui.QPixmap(XPM_ARROW_RIGHT), QtGui.QIcon.Normal, QtGui.QIcon.Off) |
|
self.verticalLayout.addWidget(self.listWidget) |
|
# new level / button layout |
|
self.horizontalLayout = QtWidgets.QHBoxLayout(self) |
|
self.edit = QtWidgets.QLineEdit() |
|
self.edit.textChanged.connect(self.on_edit) |
|
self.button = QtWidgets.QPushButton('Add / Set', self) |
|
self.button.clicked.connect(self.on_click) |
|
self.horizontalLayout.addWidget(self.edit) |
|
self.horizontalLayout.addWidget(self.button) |
|
self.verticalLayout.addLayout(self.horizontalLayout) |
|
self.reload_list() |
|
|
|
|
|
class WorldSelector(QtWidgets.QMainWindow): |
|
def __init__(self, parent=None): |
|
super(WorldSelector, self).__init__(parent) |
|
self.setWindowTitle('World Selector') |
|
self.resize(640, 480) |
|
self.setCentralWidget(MainWidget()) |
|
|
|
|
|
app = QtWidgets.QApplication(sys.argv) |
|
ws = WorldSelector() |
|
ws.show() |
|
sys.exit(app.exec_()) |