Various small changes
[galtack.git] / galtack_client.py
blobe9f543e1f5c89c6852e9e38d201086f780988357
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 """
4 GalTacK - An unofficial PyGTK client to the Galcon multiplayer game.
5 """
6 # Copyright (C) 2007 Michael Carter
7 # Copyright (C) 2007 Felix Rabe <public@felixrabe.textdriven.com>
9 # Permission is hereby granted, free of charge, to any person obtaining a
10 # copy of this software and associated documentation files (the
11 # "Software"), to deal in the Software without restriction, including
12 # without limitation the rights to use, copy, modify, merge, publish,
13 # distribute, sublicense, and/or sell copies of the Software, and to permit
14 # persons to whom the Software is furnished to do so, subject to the
15 # following conditions:
17 # The above copyright notice and this permission notice shall be included
18 # in all copies or substantial portions of the Software.
20 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
21 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23 # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
24 # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
25 # OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR
26 # THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 # Recommended line length or text width: 75 characters.
30 galtack_version = (0,2,0)
31 galtack_version_str = ".".join(map(str, galtack_version))
33 import optparse
34 import math
35 PI2 = math.pi * 2
36 import time
38 from PyGTKShell.Config import _config
39 _config["main-loop-integrate-twisted"] = True
40 from PyGTKShell.RawConsole import *
41 arrow_cursor = gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_ARROW)
42 watch_cursor = gtk.gdk.Cursor(gtk.gdk.WATCH)
44 from twisted.internet import reactor
46 import galtack.net
47 class GaltackClient(
48 galtack.net.GaltackClientRecvCmdLoggerMixin,
49 galtack.net.GaltackClientSendCmdLoggerMixin,
50 galtack.net.GaltackClientUniverseTrackerMixin,
51 galtack.net.GaltackClientHousekeeperMixin,
52 galtack.net.GaltackClientBase,
53 ): pass
56 ### CUSTOMIZED STUFF FROM PYGTK SHELL [START]
59 ## RawConsole
62 class RawConsoleWithIntroMixin(RawConsoleBase):
63 """
64 Raw console running an example script on initialization.
66 Customized for GalTacK.
67 """
69 def __init__(self, *a, **kw):
70 super(RawConsoleWithIntroMixin, self).__init__(*a, **kw)
71 buf = self.code_view.get_buffer()
72 msg = a_("Press F5 or Ctrl+E to execute this code.")
73 buf('# -*- coding: utf-8 -*-\n' +
74 '# %s\n' % msg +
75 'from galtack_client import *\n' +
76 'o = console.output_view.get_buffer()\n' +
77 'o.set_text("")\n' +
78 'o("GaltackClient instance: %r\\n" % window.galtack_client)\n' +
79 'o("Loadable return value: %r\\n" % reload_loadable())\n'
83 class RawConsoleWithIntro(
84 RawConsoleCenterInitMixin,
85 RawConsoleWithIntroMixin,
86 RawConsole,
87 ): pass
90 class WindowWithRawConsole(Window):
91 """
92 Window with a RawConsole in it and starting at a reasonable size.
93 """
95 def __init__(self, *a, **kw):
96 super(WindowWithRawConsole, self).__init__(*a, **kw)
97 self.set_title("PyGTK Shell RawConsole")
98 self.set_default_size(550, 400)
99 self.raw_console = self(RawConsoleWithIntro())
102 class WindowF12RawConsoleMixin(Window):
104 Window opening a Window containing a RawConsole when F12 is pressed.
107 def __init__(self, *a, **kw):
108 super(WindowF12RawConsoleMixin, self).__init__()
109 self.connect("key-press-event", self.__cb_key_press_event)
111 def __cb_key_press_event(self, window, event):
112 if (KeyPressEval("F12"))(event):
113 rc = WindowWithRawConsole().raw_console
114 rc.code_view.textview_userexec_namespace["window"] = self
115 return True
116 return False
119 class WindowF12RawConsole(
120 WindowF12RawConsoleMixin,
121 Window,
122 ): pass
125 ### CUSTOMIZED STUFF FROM PYGTK SHELL [END]
128 def print_errback(failure):
129 failure.printTraceback()
130 return None
133 def reload_loadable(*a, **kw):
134 m = sys.modules.get("galtack_loadable", None)
135 if m:
136 reload(m)
137 else:
138 import galtack_loadable as m
139 return m.run_loadable(*a, **kw)
142 class GaltackChatWindow(WindowF12RawConsoleMixin):
144 A Window to allow chatting in Galcon, following the Galcon client
145 protocol.
148 def __init__(self, prev_window, options, user_info, server_info):
149 self.__class__._instance = self
150 self.__prev_window = prev_window
151 self.__options = options
152 self.__user_info = user_info
153 self.__server_info = server_info
155 super(GaltackChatWindow, self).__init__()
156 self.set_default_size(550, 300)
157 n, o = server_info["name"], server_info["owner"]
158 if n: n += " - "
159 self.set_title("Chat (%s%s) - GalTacK" % (n, o))
160 self.connect("delete-event", self.__cb_delete_event)
162 outer_vbox, inner_vbox = gnome_hig(self)
164 sw = inner_vbox(Frame())(ScrolledWindow())
165 self.output_view = sw(TextView())
166 self.__buf = self.output_view.get_buffer()
167 self.__buf("Hint: Press <F12> to execute arbitrary Python code.\n")
168 self.__buf.create_mark("end", self.__buf.get_end_iter(), False)
169 self.__buf.connect("insert-text", self.__cb_insert_text)
170 self.output_view.set_editable(False)
171 hbox = gnome_hig(inner_vbox(HBox(), False, False))
172 self.input_entry = hbox(Entry())
173 self.send_button = hbox(Button("_Send"), False, False)
174 self.send_button.connect("clicked", self.__cb_send_clicked)
175 self.leave_button = hbox(Button("_Leave"), False, False)
176 self.leave_button.connect("clicked", self.__cb_leave_clicked)
177 self.quit_button = hbox(Button("_Quit"), False, False)
178 self.quit_button.connect("clicked", self.__cb_quit_clicked)
180 self.send_button.set_property("can-default", True)
181 gobject.idle_add(self.send_button.grab_default)
182 gobject.idle_add(self.input_entry.grab_focus)
184 C = GaltackClient
185 self.galtack_client = C(self.__options,
186 self.__user_info, self.__server_info)
187 r = self.galtack_client.register_command_callback
188 r("close", self.__cmd_close)
189 r("message", self.__cmd_message)
190 r("start", self.__cmd_start)
191 r("stop", self.__cmd_stop)
192 reactor.listenUDP(0, self.galtack_client)
194 def f(): # TODO: implement this properly after login
195 self.galtack_client.send_commands((1, "status", "away"))
196 return False
197 gobject.timeout_add(500, f)
199 def __cb_delete_event(self, widget, event):
200 self.leave_button.clicked()
201 return True
203 def __cb_insert_text(self, textbuffer, iter, text, length):
204 mark = textbuffer.get_mark("end")
205 if not mark:
206 return False
207 self.output_view.scroll_to_mark(mark, 0.0)
208 return False
210 def __cb_leave_clicked(self, button):
211 self.set_sensitive(False)
212 deferred = self.galtack_client.logout()
213 deferred.addCallback(self.__cb_left)
214 deferred.addErrback(self.__eb_left)
216 def __cb_left(self, ignored_arg):
217 # GaltackServerListWindow._instance.show()
218 self.__prev_window.show()
219 self.destroy()
221 def __eb_left(self, ignored_arg):
222 self.set_sensitive(True)
224 def __cb_quit_clicked(self, button):
225 self.set_sensitive(False)
226 deferred = self.galtack_client.logout()
227 deferred.addCallback(self.__cb_left_quit)
228 deferred.addErrback(self.__cb_left_quit)
230 def __cb_left_quit(self, ignored_arg):
231 self.destroy()
233 def __cb_send_clicked(self, button):
234 msg = self.input_entry.get_text()
235 self.galtack_client.send_commands((1, "message", msg))
236 self.input_entry.set_text("")
238 def __cmd_message(self, command):
239 sender, message = command[3:]
240 snd = ""
241 if sender: snd = " " + sender
242 self.__buf("%s%s %s\n" % (time.strftime("%X"), snd, message))
244 def __cmd_close(self, command):
245 self.galtack_client.send_commands((1, "[CLOSE]"))
246 gobject.timeout_add(500, main_loop_quit) # TODO: await ACK
248 def __cmd_stop(self, command):
249 self.__buf("(stop)\n")
251 def __cmd_start(self, command):
252 self.__buf("(start)\n")
255 class GaltackServerListWindow(Window):
257 Window displaying the list of Galcon servers.
260 NEXT_CLASS = GaltackChatWindow
262 def __init__(self, prev_window, options, user_info):
263 self.__class__._instance = self
264 self.__prev_window = prev_window
265 self.__options = options
266 super(GaltackServerListWindow, self).__init__()
267 self.__user_info = user_info
268 self.set_title("Galcon Server List - GalTacK")
269 self.set_default_size(600, 400)
270 self.set_position(gtk.WIN_POS_CENTER)
272 outer_vbox, self.__inner_vbox = gnome_hig(self)
274 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
275 self.__version_label = hbox(LeftLabel(""), False, False)
276 self.set_current_version()
277 self.__version_label.set_sensitive(False)
279 self.__refresh_button = hbox(Button("_Refresh List"), False, False,
280 pack_end = True)
281 self.__refresh_button.connect("clicked", self.__cb_refresh)
283 sw = self.__inner_vbox(Frame())(ScrolledWindow())
284 self.__treeview = sw(TreeView())
285 self.__treeview.set_sensitive(False)
286 self.__treeview.set_property("rules-hint", True)
287 self.__treeview.connect("row-activated", self.__cb_row_activated)
288 cb = self.__cb_selection_changed
289 self.__treeview.get_selection().connect("changed", cb)
291 for i, spec in enumerate(galtack.net.ServerInfo.COL_SPEC):
292 if not spec["visible"]: continue
293 col = gtk.TreeViewColumn(spec["caption"])
294 col.set_reorderable(True)
295 col.set_sort_column_id(i)
296 self.__treeview.append_column(col)
297 cell = gtk.CellRendererText()
298 col.pack_start(cell, True)
299 col.add_attribute(cell, "text", i)
300 col.add_attribute(cell, "sensitive", 0)
302 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
303 self.__join_button = hbox(Button("_Join"))
304 self.__join_button.set_sensitive(False)
305 self.__join_button.connect("clicked", self.__cb_join)
307 self.back_button = hbox(Button("_Back"), False, False)
308 self.back_button.connect("clicked", self.__cb_back)
310 self.__statusbar = outer_vbox(Statusbar(), False, False)
311 self.__statusbar_cid = cid = self.__statusbar.get_context_id("msg")
313 self.__server_password_prompt = dlg = Dialog(
314 "Server Password Required", self, gtk.DIALOG_MODAL,
315 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
316 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
318 dlg.set_default_response(gtk.RESPONSE_ACCEPT)
319 dlg.set_default_size(300, -1)
320 vbox = gnome_hig(VBox()) # I want my own VBox here ;-)
321 vbox.set_border_width(6)
322 dlg.vbox.pack_start(vbox)
323 self.__server_password_prompt_label = vbox(LeftLabel())
324 self.__server_password_prompt_entry = vbox(Entry())
325 self.__server_password_prompt_entry.set_visibility(False)
327 def set_current_version(self, current_version = None):
328 self.__version_label.set_sensitive(False)
329 if current_version is None:
330 self.__version_label.set_text("Current version: ???")
331 else:
332 self.__version_label.set_text("Current version: %s" %
333 ".".join(current_version))
334 self.__version_label.set_sensitive(True)
336 def set_server_info_list(self, server_info_list = None):
337 types = galtack.net.ServerInfo.get_col_types()
338 model = gtk.ListStore(*types)
339 self.__treeview.set_sensitive(False)
340 if server_info_list is not None:
341 for server_info in server_info_list:
342 model.append(server_info.get_data_tuple())
343 self.__treeview.set_sensitive(True)
344 self.__treeview.set_model(model)
346 def __cb_refresh(self, button):
347 self.window.set_cursor(watch_cursor)
348 self.__inner_vbox.set_sensitive(False)
349 self.__statusbar.push(self.__statusbar_cid,
350 "Retrieving server list...")
351 deferred = galtack.net.get_server_list(self.__user_info)
352 deferred.addCallback(self.__server_list_callback)
353 deferred.addErrback(self.__server_list_errback)
355 def __server_list_callback(self, (current_version, server_info_list)):
356 self.set_current_version(current_version)
357 self.set_server_info_list(server_info_list)
358 self.__statusbar.pop(self.__statusbar_cid)
359 self.__inner_vbox.set_sensitive(True)
360 self.window.set_cursor(arrow_cursor)
362 def __server_list_errback(self, failure):
363 self.set_sensitive(True)
364 self.window.set_cursor(arrow_cursor)
365 cid = self.__statusbar_cid
366 sbar = self.__statusbar
367 sbar.pop(cid)
368 gtk.gdk.beep()
369 mid = sbar.push(cid, "Retrieving server list failed")
370 gobject.timeout_add(4000, lambda: sbar.remove(cid, mid))
371 return failure
373 def __cb_selection_changed(self, treesel):
374 model, iter = treesel.get_selected()
375 if iter is None:
376 ok_to_join = False
377 else:
378 data = galtack.net.ServerInfo(*model[iter])
379 ok_to_join = data.bots_ok
380 self.__join_button.set_sensitive(ok_to_join)
382 def __cb_row_activated(self, treeview, path, view_column):
383 self.__cb_join(self.__join_button)
385 def __cb_join(self, button):
386 treesel = self.__treeview.get_selection()
387 model, iter = treesel.get_selected()
388 server_info = galtack.net.ServerInfo(*model[iter])
389 if server_info.pwd_protected:
390 self.__run_server_password_prompt(server_info)
391 return None
392 self.join_with_server_info(server_info)
394 def __cb_back(self, button):
395 # self.GaltackLoginWindow._instance.show()
396 self.__prev_window.show()
397 self.destroy()
399 def __run_server_password_prompt(self, server_info):
400 n, o = server_info["name"], server_info["owner"]
401 if n: n += " - "
402 s = "Password for %s%s:" % (n, o)
403 self.__server_password_prompt_label.set_text(s)
404 response_id = self.__server_password_prompt.run()
405 if response_id != gtk.RESPONSE_ACCEPT: return None
406 passwd = self.__server_password_prompt_entry.get_text()
407 server_info["passwd"] = passwd
408 self.join_with_server_info(server_info)
410 def join_with_server_info(self, server_info):
411 chat_window = self.NEXT_CLASS(self, self.__options,
412 self.__user_info, server_info)
413 gobject.idle_add(self.hide)
416 class GaltackLoginWindow(Window):
418 A Window asking the user for Galcon login details and log in.
421 NEXT_CLASS = GaltackServerListWindow
423 def __init__(self, options):
424 self.__class__._instance = self
425 self.__options = options
426 super(GaltackLoginWindow, self).__init__()
427 self.set_title("GalTacK Login")
428 self.set_default_size(400, -1)
429 self.set_position(gtk.WIN_POS_CENTER)
431 outer_vbox, self.inner_vbox = gnome_hig(self)
433 table = gnome_hig(self.inner_vbox(Table(), False, False))
435 xop = {"xoptions": gtk.FILL}
436 table.add_rows()
437 label = table.attach_cell(LeftLabel("_Email Address:"), **xop)
438 self.email_entry = table.attach_cell(Entry())
439 label.set_mnemonic_widget(self.email_entry)
440 if self.__options.email is not None:
441 self.email_entry.set_text(self.__options.email)
443 table.add_rows()
444 label = table.attach_cell(LeftLabel("_Username:"), **xop)
445 self.name_entry = table.attach_cell(Entry())
446 label.set_mnemonic_widget(self.name_entry)
447 if self.__options.user is not None:
448 self.name_entry.set_text(self.__options.user)
450 table.add_rows()
451 label = table.attach_cell(LeftLabel("_Password:"), **xop)
452 self.passwd_entry = table.attach_cell(Entry())
453 label.set_mnemonic_widget(self.passwd_entry)
454 self.passwd_entry.set_visibility(False)
455 if self.__options.password is not None:
456 self.passwd_entry.set_text(self.__options.password)
458 table.add_rows()
459 hbox = gnome_hig(table.attach_row(HBox()))
460 self.__login_button = hbox(Button("_Sign In"))
461 self.__login_button.connect("clicked", self.__cb_login)
462 self.__login_button.set_property("can-default", True)
463 gobject.idle_add(self.__login_button.grab_default)
465 self.__quit_button = hbox(Button("_Quit"), False, False)
466 self.__quit_button.connect("clicked", self.__cb_quit)
468 self.statusbar = outer_vbox(Statusbar(), False, False)
469 self.statusbar_cid = cid = self.statusbar.get_context_id("msg")
470 mid = self.statusbar.push(cid, "Enter your login details")
471 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
473 def __get_user_info(self):
474 email = self.email_entry.get_text()
475 name = self.name_entry.get_text()
476 passwd = self.passwd_entry.get_text()
477 platform = "linux2"
478 version = "1.2.1"
479 return galtack.net.UserInfo(email, name, passwd, platform, version)
481 def __cb_login(self, button):
482 self.window.set_cursor(watch_cursor)
483 self.inner_vbox.set_sensitive(False)
484 self.statusbar.push(self.statusbar_cid,
485 "Retrieving server list...")
486 user_info = self.__get_user_info()
487 deferred = galtack.net.get_server_list(user_info)
488 deferred.addCallbacks(self.__server_list_callback,
489 self.__server_list_errback)
490 deferred.addErrback(print_errback)
492 def __cb_quit(self, button):
493 main_loop_quit()
495 def __server_list_callback(self, (current_version, server_info_list)):
496 w = self.NEXT_CLASS(self, self.__options, self.__get_user_info())
497 w.set_current_version(current_version)
498 w.set_server_info_list(server_info_list)
499 gobject.idle_add(self.hide)
500 self.inner_vbox.set_sensitive(True)
501 self.window.set_cursor(arrow_cursor)
502 cid = self.statusbar_cid
503 self.statusbar.pop(cid)
505 def __server_list_errback(self, failure):
506 failure.trap(galtack.net.ServerListException)
507 self.inner_vbox.set_sensitive(True)
508 self.window.set_cursor(arrow_cursor)
509 cid = self.statusbar_cid
510 self.statusbar.pop(cid)
511 gtk.gdk.beep()
512 mid = self.statusbar.push(cid, "Failure: %s" %
513 failure.getErrorMessage())
514 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
515 return None
517 @classmethod # for potential inheritance
518 def _modify_option_parser(cls, option_parser):
519 option_parser.add_option("-e", "--email", metavar = "ADDRESS",
520 help = "login email address")
521 option_parser.add_option("-u", "--user",
522 help = "login username")
523 option_parser.add_option("-p", "--password",
524 help = "login password")
525 return option_parser
528 def main(argv):
529 option_parser = optparse.OptionParser(prog = "GalTacK",
530 version = "%%prog %s" %
531 galtack_version_str)
532 option_parser = GaltackLoginWindow._modify_option_parser(option_parser)
533 option_parser = GaltackClient._modify_option_parser(option_parser)
534 options, arguments = option_parser.parse_args(argv[1:])
535 GaltackLoginWindow(options)
536 main_loop_run()
537 return 0
539 if __name__ == "__main__":
540 import sys
541 sys.exit(main(sys.argv))