Project

Profile

Help

Revision e1d2d0dc

IDe1d2d0dc4b668367d39b8de6cbc71756fbedd40c
Parent 481c2207
Child a5035561

Added by Vincent Le Goff over 3 years ago

Add the basic CocoMUD proof-of-concept

View differences:

settings/options.conf
1
# Text-to-speech configuration
2
[TTS]
3
    on = True
4
    outside = True
src/UniversalSpeech.py
1
import ctypes as __ctypes
2
__uspeech = __ctypes.cdll.UniversalSpeech
3

  
4
VOLUME, VOLUME_MAX, VOLUME_MIN, VOLUME_SUPPORTED, \
5
RATE, RATE_MAX, RATE_MIN, RATE_SUPPORTED, \
6
PITCH, PITCH_MAX, PITCH_MIN, PITCH_SUPPORTED, \
7
INFLEXION, INFLEXION_MAX, INFLEXION_MIN, INFLEXION_SUPPORTED, \
8
PAUSED, PAUSE_SUPPORTED, \
9
BUSY, BUSY_SUPPORTED, \
10
WAIT, WAIT_SUPPORTED \
11
	= range(0,22)
12

  
13
ENABLE_NATIVE_SPEECH = 0xFFFF
14
VOICE = 0x10000
15
LANGUAGE = 0x20000
16
SUBENGINE = 0x30000
17
ENGINE = 0x40000
18
ENGINE_AVAILABLE = 0x50000
19
AUTO_ENGINE = 0xFFFE
20
USER_PARAM = 0x1000000
21

  
22

  
23
def say (msg, interrupt=True):
24
	return __uspeech.speechSay(msg, interrupt)
25

  
26
def braille (msg):
27
	return __uspeech.brailleDisplay(msg)
28

  
29
def stop () :
30
	return __uspeech.speechStop()
31

  
32
def getValue (what) :
33
	return __uspeech.speechGetValue(what)
34

  
35
def setValue (what, value):
36
	return __uspeech.speechSetValue(what, value)
37

  
38
def getString (what):
39
	__uspeech.speechGetString.restype = __ctypes.c_wchar_p
40
	return __uspeech.speechGetString(what)	
41

  
42
def setString (what, value):
43
	return __uspeech.speechSetString(what, value)
src/client.py
1
"""This file contains the client that can connect to a MUD.
2

  
3
It is launched in a new thread, so as not to block the main thread.
4

  
5
"""
6

  
7
import re
8
from telnetlib import Telnet, WONT, WILL, ECHO
9
import threading
10
import time
11
import wx
12

  
13
try:
14
    from UniversalSpeech import say, braille
15
except ImportError:
16
    say = None
17
    braille = None
18

  
19
from ui.event import FocusEvent, myEVT_FOCUS
20

  
21
# Constants
22
ANSI_ESCAPE = re.compile(r'\x1b[^m]*m')
23

  
24
class Client(threading.Thread):
25

  
26
    """Class to receive data from the MUD."""
27

  
28
    def __init__(self, host, port=4000, timeout=0.1, settings=None):
29
        """Connects to the MUD."""
30
        threading.Thread.__init__(self)
31
        self.client = None
32
        self.timeout = timeout
33
        self.settings = settings
34
        self.running = False
35

  
36
        # Try to connect to the specified host and port
37
        self.client = Telnet(host, port)
38
        self.running = True
39

  
40
    def run(self):
41
        """Run the thread."""
42
        while self.running:
43
            time.sleep(self.timeout)
44
            msg = self.client.read_very_eager()
45
            if msg:
46
                self.handle_message(msg)
47

  
48
    def handle_message(self, msg):
49
        """When the client receives a message."""
50
        pass
51

  
52

  
53
class GUIClient(Client):
54

  
55
    """Client specifically linked to a GUI window.
56

  
57
    This client proceeds to send the text it receives to the frame.
58

  
59
    """
60

  
61
    def __init__(self, host, port=4000, timeout=0.1, panel=None,
62
            settings=None):
63
        Client.__init__(self, host, port, timeout, settings)
64
        self.panel = panel
65
        if self.client:
66
            self.client.set_option_negotiation_callback(self.handle_option)
67

  
68
    def handle_message(self, msg):
69
        """When the client receives a message."""
70
        pos = self.panel.output.GetInsertionPoint()
71
        msg = msg.decode("utf-8", "replace")
72
        msg = ANSI_ESCAPE.sub('', msg)
73
        self.panel.output.write(msg)
74
        self.panel.output.SetInsertionPoint(pos)
75
        if self.settings["options.TTS.on"]:
76
            if say and braille:
77
                say(msg, interrupt=False)
78
                braille(msg)
79

  
80
    def handle_option(self, socket, command, option):
81
        """Handle a received option."""
82
        if command == WILL and option == ECHO:
83
            evt = FocusEvent(myEVT_FOCUS, -1, "password")
84
            wx.PostEvent(self.panel, evt)
85
        elif command == WONT and option == ECHO:
86
            evt = FocusEvent(myEVT_FOCUS, -1, "input")
87
            wx.PostEvent(self.panel, evt)
src/cocomud.py
1
"""This demo file creates a simple client with TTS support."""
2

  
3
import wx
4

  
5
from client import GUIClient
6
from config import Settings
7
from ui.window import MainWindow
8

  
9
settings = Settings()
10
settings.load()
11
app = wx.App()
12
window = MainWindow(settings)
13
client = GUIClient("vanciamud.fr", 4000, 0.1, window.panel, settings)
14
window.panel.client = client.client
15
client.start()
16
app.MainLoop()
src/config.py
1
"""This module defines the default configuration."""
2

  
3
import os
4
import os.path
5
from textwrap import dedent
6

  
7
from configobj import ConfigObj
8
from validate import Validator
9

  
10
class Configuration:
11

  
12
    """Class describing CocoMUD's configuration.
13

  
14
    Each configuration file is loaded here. Each file is loaded and validated with ConfigObj.  If everything goes smoothly, the ConfigObj objects can be found in _getitem__-ing these values, specifying the directory structure.
15

  
16
    Example:
17

  
18
        >>> config = Configuration()
19
        >>> config.load()
20
        >>> # In the settings/global.conf file is the following line:
21
        >>> #     name = CocoMUD
22
        >>> # You can access this configuration through:
23
        >>> configuration["options"]["global"]["name"]
24
        >>> # Or, more readable
25
        >>> configuration["options.global.name"]
26

  
27
    """
28

  
29
    def __init__(self, root_dir):
30
        self.root_dir = root_dir
31
        self.values = {}
32

  
33
    def __getitem__(self, key):
34
        """Return the configured value at the specified key.
35

  
36
        The key is a string.  It can be an identifier without periods
37
        (.).  In which case, the top-level data at this key is returned
38
        or a KeyError exception is raised.
39
        The 'key' can also be a list of identifiers in a string separated
40
        by a period.  In this case, the data is looked for in the
41
        directory/file/ConfigObj hierarchy.  A KeyError exception
42
        is raised if the expected key cannot be found.
43

  
44
        Thus, the two following lines are identical:
45
            >>> configuration["settings"]["global"]["name"]
46
            >>> configuration["settings.global.name"]
47

  
48
        """
49
        if "." in key:
50
            keys = key.split(".")
51
            value = self.values
52
            for sub_key in keys:
53
                if sub_key not in value:
54
                    raise KeyError("the key {} cannot be found, cannot " \
55
                            "find {}".format(repr(key), repr(sub_key)))
56

  
57
                value = value[sub_key]
58

  
59
        else:
60
            value = self.values[key]
61

  
62
        return value
63

  
64
    def __setitem__(self, key, value):
65
        """Change the value of 'key'.
66

  
67
        'key' is a string, and can be specified in the two ways supported
68
        by '__getitem__'.
69

  
70
        """
71
        if "." in key:
72
            keys = key.split(".")
73
            last = keys[-1]
74
            del keys[-1]
75
            dictionary = self.values
76
            for sub_key in keys:
77
                if sub_key not in dictionary:
78
                    raise KeyError("the key {} cannot be found, cannot " \
79
                            "find {}".format(repr(key), repr(sub_key)))
80

  
81
                dictionary = dictionary[sub_key]
82

  
83
            dictionary[last] = value
84
        else:
85
            self.values[key] = value
86

  
87
    def load(self):
88
        """Load the configuration."""
89
        raise NotImplementedError
90

  
91
    def load_file(self, filename, spec):
92
        """Load the specified file using ConfigObj."""
93
        fullpath = self.root_dir + os.sep + filename
94

  
95
        # Create the directory structure if necessary
96
        directories = os.path.dirname(fullpath).split("/")
97
        base = directories[0]
98
        if not os.path.exists(base):
99
            os.mkdir(base)
100

  
101
        for directory in directories[1:]:
102
            base += os.path + directory
103
            if not os.path.exists(base):
104
                os.mkdir(base)
105

  
106
        filename = os.path.basename(fullpath) + ".conf"
107

  
108
        # Create the ConfigObj
109
        config = ConfigObj(fullpath + ".conf", configspec=spec.split("\n"))
110

  
111
        # Validates the configuration
112
        validator = Validator()
113
        result = config.validate(validator)
114

  
115
        # Saves the ConfigObj
116
        values = self.values
117
        for path in os.path.dirname(fullpath).split("/")[1:]:
118
            if path not in values:
119
                values[path] = {}
120
            values = values[path]
121

  
122
        values[os.path.basename(fullpath)] = config
123

  
124

  
125
class Settings(Configuration):
126

  
127
    """Special configuration in the 'settings' directory."""
128

  
129
    def __init__(self):
130
        Configuration.__init__(self, "settings")
131

  
132
    def load(self):
133
        """Load all the files."""
134
        self.load_options()
135

  
136
    def load_options(self):
137
        """Load the file containing the options."""
138
        spec = dedent("""
139
            # Text-to-speech configuration
140
            [TTS]
141
                on = boolean(default=True)
142
                outside = boolean(default=True)
143
        """.strip("\n"))
144
        self.load_file("options", spec)
src/setup.py
1
from cx_Freeze import setup, Executable
2

  
3
exe = Executable(
4
    script="cocomud.py",
5
    base="Win32GUI",
6
)
7

  
8
includefiles = [
9
    "../dolapi.dll",
10
    "../jfwapi.dll",
11
    "../nvdaControllerClient.dll",
12
    "../SAAPI32.dll",
13
    "../UniversalSpeech.dll",
14
    "../settings",
15
]
16

  
17
setup(
18
    name = "CocoMUD client",
19
    version = "0.1",
20
    description = "The CocoMUD client.",
21
    options = {'build_exe': {'include_files': includefiles}},
22
    executables = [exe]
23
)
src/ui/dialogs/preferences.py
1
"""Module containing the preferences dialog."""
2

  
3
import wx
4

  
5
class TTSTab(wx.Panel):
6

  
7
    """TTS tab."""
8

  
9
    def __init__(self, parent, settings):
10
        super(TTSTab, self).__init__(parent)
11
        self.settings = settings
12

  
13
        self.InitUI()
14
        self.Fit()
15

  
16
    def InitUI(self):
17
        panel = wx.Panel(self)
18
        sizer = wx.GridBagSizer(15, 15)
19
        panel.SetSizer(sizer)
20

  
21
        # TTS preferendces
22
        self.TTS_on = wx.CheckBox(panel, label="Enable TTS (Text-To Speech)")
23
        self.TTS_on.SetValue(self.settings["options.TTS.on"])
24
        self.TTS_outside = wx.CheckBox(panel, label="Enable TTS when on a different window")
25
        self.TTS_outside.SetValue(self.settings["options.TTS.outside"])
26

  
27
        # Append to the sizer
28
        sizer.Add(self.TTS_on, pos=(0, 0))
29
        sizer.Add(self.TTS_outside, pos=(0, 1))
30

  
31

  
32
class PreferencesTabs(wx.Notebook):
33

  
34
    """Preference tabs."""
35

  
36
    def __init__(self, parent, settings):
37
        wx.Notebook.__init__(self, parent)
38

  
39
        TTS_tab = TTSTab(self, settings)
40
        self.AddPage(TTS_tab, "TTS")
41
        self.TTS = TTS_tab
42

  
43
class PreferencesDialog(wx.Dialog):
44

  
45
    """Preferences dialog."""
46

  
47
    def __init__(self, settings):
48
        super(PreferencesDialog, self).__init__(None, title="Preferences")
49
        self.settings = settings
50

  
51
        self.InitUI()
52
        self.Maximize()
53

  
54
    def InitUI(self):
55
        panel = wx.Panel(self)
56
        sizer = wx.GridBagSizer(15, 15)
57
        panel.SetSizer(sizer)
58

  
59
        # Add the tabs
60
        self.tabs = PreferencesTabs(panel, self.settings)
61
        buttons = self.CreateButtonSizer(wx.OK | wx.CANCEL)
62

  
63
        # Append to the sizer
64
        sizer.Add(self.tabs, pos=(1, 0), span=( 5, 5))
65
        sizer.Add(buttons, pos=(8, 0), span=(1, 2))
66

  
67
        # Event binding
68
        self.Bind(wx.EVT_BUTTON, self.OnOK, id=wx.ID_OK)
69
        self.Bind(wx.EVT_BUTTON, self.OnCancel, id=wx.ID_CANCEL)
70

  
71
    def OnOK(self, e):
72
        """Save the preferences."""
73
        tts = self.tabs.TTS
74
        self.settings["options.TTS.on"] = tts.TTS_on.GetValue()
75
        self.settings["options.TTS.outside"] = tts.TTS_outside.GetValue()
76
        self.settings["options"].write()
77
        self.Destroy()
78

  
79
    def OnCancel(self, e):
80
        """Simply exit the dialog."""
81
        self.Destroy()
src/ui/event.py
1
"""This file contains the specifial events for the CocoMUD client."""
2

  
3
import wx
4

  
5
# Constants
6
myEVT_FOCUS = wx.NewEventType()
7
EVT_FOCUS = wx.PyEventBinder(myEVT_FOCUS, 1)
8

  
9
class FocusEvent(wx.PyCommandEvent):
10

  
11
    """Change focus from the input field to the password field."""
12

  
13
    def __init__(self, etype, eid, value=None):
14
        wx.PyCommandEvent.__init__(self, etype, eid)
15
        self._value = value
16

  
17
    def GetValue(self):
18
        """Return the event's value."""
19
        return self._value
src/ui/window.py
1
"""This file contains the MainWindow class."""
2

  
3
import wx
4

  
5
from dialogs.preferences import PreferencesDialog
6
from event import EVT_FOCUS
7

  
8
class MainWindow(wx.Frame):
9

  
10
    def __init__(self, settings):
11
        super(MainWindow, self).__init__(None)
12
        self.settings = settings
13
        self.CreateMenuBar()
14
        self.InitUI()
15

  
16
    def CreateMenuBar(self):
17
        """Create the GUI menu bar and hierarchy of menus."""
18
        menubar = wx.MenuBar()
19
        fileMenu = wx.Menu()
20
        preferences = wx.MenuItem(fileMenu, -1, '&Preferences\tAlt+Enter')
21
        quit = wx.MenuItem(fileMenu, -1, '&Quit\tCtrl+Q')
22
        fileMenu.AppendItem(preferences)
23
        fileMenu.AppendItem(quit)
24

  
25
        self.Bind(wx.EVT_MENU, self.OnPreferences, preferences)
26
        self.Bind(wx.EVT_MENU, self.OnQuit, quit)
27

  
28
        menubar.Append(fileMenu, '&File')
29

  
30
        self.SetMenuBar(menubar)
31

  
32
    def InitUI(self):
33
        self.panel = MUDPanel(self)
34
        self.SetTitle("CocoMUD client")
35
        self.Centre()
36
        self.Show()
37

  
38
    def OnPreferences(self, e):
39
        """Open the preferences dialog box."""
40
        dialog = PreferencesDialog(self.settings)
41
        dialog.ShowModal()
42
        dialog.Destroy()
43
    def OnQuit(self, e):
44
        self.Close()
45

  
46

  
47
class MUDPanel(wx.Panel):
48

  
49
    def __init__(self, parent):
50
        wx.Panel.__init__(self, parent)
51
        self.client = None
52

  
53
        mainSizer = wx.GridBagSizer(5, 5)
54

  
55
        # Input
56
        l_input = wx.StaticText(self, -1, "Input")
57
        t_input = wx.TextCtrl(self, -1, "", size=(125, -1),
58
                style=wx.TE_PROCESS_ENTER)
59
        self.input = t_input
60

  
61
        # Password
62
        l_password = wx.StaticText(self, -1, "Password")
63
        t_password = wx.TextCtrl(self, -1, "", size=(20, -1),
64
                style=wx.TE_PROCESS_ENTER | wx.TE_PASSWORD)
65
        self.password = t_password
66
        t_password.Hide()
67

  
68
        # Add the input field in the sizer
69
        mainSizer.Add(l_input, pos=(0, 0))
70
        mainSizer.Add(t_input, pos=(0, 1), span=(1, 6))
71
        mainSizer.Add(l_password, pos=(0, 7))
72
        mainSizer.Add(t_password, pos=(0, 8), span=(1, 2))
73

  
74
        # Ouput
75
        l_output = wx.StaticText(self, -1, "Output")
76
        t_output = wx.TextCtrl(self, -1, "",
77
                size=(800, 800), style=wx.TE_MULTILINE|wx.TE_READONLY)
78
        self.output = t_output
79

  
80
        # Add the output fields in the sizer
81
        mainSizer.Add(l_output, pos=(1, 0))
82
        mainSizer.Add(t_output, pos=(10, 10))
83

  
84
        # Event handler
85
        t_input.Bind(wx.EVT_TEXT_ENTER, self.EvtText)
86
        t_password.Bind(wx.EVT_TEXT_ENTER, self.EvtText)
87
        self.Bind(EVT_FOCUS, self.OnFocus)
88
        t_output.Bind(wx.EVT_KEY_DOWN, self.OnKeyDownInOutput)
89

  
90
    def EvtText(self, event):
91
        """One of the input fields is sending text."""
92
        self.input.Clear()
93
        self.password.Clear()
94
        msg = event.GetString().encode("utf-8", "replace")
95
        self.client.write(msg + "\r\n")
96

  
97
    def OnFocus(self, evt):
98
        val = evt.GetValue()
99
        if val == "input":
100
            self.input.Show()
101
            self.input.SetFocus()
102
            self.password.Hide()
103
        elif val == "password":
104
            self.password.Show()
105
            self.password.SetFocus()
106
            self.input.Hide()
107

  
108
    def OnKeyDownInOutput(self, e):
109
        """A key is pressed while in the output."""
110
        key = e.GetUnicodeKey()
111
        print "pressing in output", key
112
        if key == 66:
113
            self.input.SetFocus()
114
            wx.PostEvent(self.input, e)
115
        else:
116
            e.Skip()

Also available in: Unified diff