A lot of logout work
[galtack.git] / galtack_client.py
blob7e44096314966869db2a5ecc5f5e75d0214c9d96
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.GaltackClientHousekeeperMixin,
50 galtack.net.GaltackClientUniverseTrackerMixin,
51 galtack.net.GaltackClientBase,
52 ): pass
55 ### CUSTOMIZED STUFF FROM PYGTK SHELL [START]
58 ## RawConsole
61 class RawConsoleWithIntroMixin(RawConsoleBase):
62 """
63 Raw console running an example script on initialization.
65 Customized for GalTacK.
66 """
68 def __init__(self, *a, **kw):
69 super(RawConsoleWithIntroMixin, self).__init__(*a, **kw)
70 buf = self.code_view.get_buffer()
71 msg = a_("Press F5 or Ctrl+E to execute this code.")
72 buf('# -*- coding: utf-8 -*-\n' +
73 '# %s\n' % msg +
74 'from galtack_client import *\n' +
75 'o = console.output_view.get_buffer()\n' +
76 'o.set_text("")\n' +
77 'c = window.galtack_client\n' +
78 'c.send_commands((1, "message", "Hello, World!"))\n' +
79 'o(str(dir(c)))\n')
82 class RawConsoleWithIntro(
83 RawConsoleCenterInitMixin,
84 RawConsoleWithIntroMixin,
85 RawConsole,
86 ): pass
89 class WindowWithRawConsole(Window):
90 """
91 Window with a RawConsole in it and starting at a reasonable size.
92 """
94 def __init__(self, *a, **kw):
95 super(WindowWithRawConsole, self).__init__(*a, **kw)
96 self.set_title("PyGTK Shell RawConsole")
97 self.set_default_size(400, 400)
98 self.raw_console = self(RawConsoleWithIntro())
101 class WindowF12RawConsoleMixin(Window):
103 Window opening a Window containing a RawConsole when F12 is pressed.
106 def __init__(self, *a, **kw):
107 super(WindowF12RawConsoleMixin, self).__init__(*a, **kw)
108 self.connect("key-press-event", self.__cb_key_press_event)
110 def __cb_key_press_event(self, window, event):
111 if (KeyPressEval("F12"))(event):
112 rc = WindowWithRawConsole().raw_console
113 rc.code_view.textview_userexec_namespace["window"] = self
114 return True
115 return False
118 class WindowF12RawConsole(
119 WindowF12RawConsoleMixin,
120 Window,
121 ): pass
124 ### CUSTOMIZED STUFF FROM PYGTK SHELL [END]
127 def print_errback(failure):
128 failure.printTraceback()
129 return None
132 class UniverseView(DrawingArea):
134 Universe canvas.
137 def __init__(self):
138 super(UniverseView, self).__init__()
139 self.reset()
140 self.connect("expose-event", self.__cb_expose_event)
141 self.set_size_request(640, 480)
143 def reset(self):
144 self.planet_info_list = []
146 def __cb_expose_event(self, widget, event):
147 x, y, w, h = self.allocation
148 cr = self.window.cairo_create()
150 # background
151 cr.rectangle(0, 0, w, h)
152 cr.set_source_rgba(0, 0, 0, 1)
153 cr.fill()
155 # planets
156 cr.set_source_rgba(1, 1, 1, 1)
157 for planet_info in self.planet_info_list:
158 cr.arc(planet_info.x, planet_info.y, 10, 0, PI2)
159 cr.fill()
161 def redraw(self):
162 if self.window is not None:
163 self.window.invalidate_rect(self.allocation, False)
164 return False
167 class GaltackUniverseWindow(WindowF12RawConsoleMixin):
169 A Window displaying a view of the Galcon universe.
172 def __init__(self):
173 super(GaltackUniverseWindow, self).__init__()
174 self.set_title("Universe Window - GalTacK")
175 self.universe_view = self(UniverseView())
178 class GaltackChatWindow(WindowF12RawConsoleMixin):
180 A Window to allow chatting in Galcon, following the Galcon client
181 protocol.
184 def __init__(self, options, user_info, server_info):
185 self.__options = options
186 self.__user_info = user_info
187 self.__server_info = server_info
189 super(GaltackChatWindow, self).__init__()
190 self.set_default_size(400, 300)
191 n, o = server_info["name"], server_info["owner"]
192 if n: n += " - "
193 self.set_title("Chat (%s%s) - GalTacK" % (n, o))
194 self.connect("delete-event", self.__cb_delete_event)
196 outer_vbox, inner_vbox = gnome_hig(self)
198 sw = inner_vbox(Frame())(ScrolledWindow())
199 self.output_view = sw(TextView())
200 self.__buf = self.output_view.get_buffer()
201 self.output_view.set_editable(False)
202 hbox = gnome_hig(inner_vbox(HBox(), False, False))
203 self.input_entry = hbox(Entry())
204 self.send_button = hbox(Button("_Send"), False, False)
205 self.send_button.connect("clicked", self.__cb_send_clicked)
206 self.leave_button = hbox(Button("_Leave"), False, False)
207 self.leave_button.connect("clicked", self.__cb_leave_clicked)
208 self.quit_button = hbox(Button("_Quit"), False, False)
209 self.quit_button.connect("clicked", self.__cb_quit_clicked)
211 self.send_button.set_property("can-default", True)
212 gobject.idle_add(self.send_button.grab_default)
213 gobject.idle_add(self.input_entry.grab_focus)
215 C = GaltackClient
216 self.galtack_client = C(self.__options,
217 self.__user_info, self.__server_info)
218 r = self.galtack_client.register_command_callback
219 r("close", self.__cmd_close)
220 r("message", self.__cmd_message)
221 r("start", self.__cmd_start)
222 r("stop", self.__cmd_stop)
223 reactor.listenUDP(0, self.galtack_client)
225 def f(): # TODO: implement this properly after login
226 self.galtack_client.send_commands((1, "status", "away"))
227 return False
228 gobject.timeout_add(500, f)
230 def __cb_delete_event(self, widget, event):
231 self.leave_button.clicked()
232 return True
234 def __cb_leave_clicked(self, button):
235 self.set_sensitive(False)
236 deferred = self.galtack_client.logout()
237 deferred.addCallback(self.__cb_left)
238 deferred.addErrback(self.__eb_left)
240 def __cb_left(self, ignored_arg):
241 # GaltackLoginWindow._instance.show()
242 GaltackServerListWindow._instance.show()
243 self.destroy()
245 def __eb_left(self, ignored_arg):
246 self.set_sensitive(True)
248 def __cb_quit_clicked(self, button):
249 self.set_sensitive(False)
250 deferred = self.galtack_client.logout()
251 deferred.addCallback(self.__cb_left_quit)
252 deferred.addErrback(self.__cb_left_quit)
254 def __cb_left_quit(self, ignored_arg):
255 self.destroy()
257 def __cb_send_clicked(self, button):
258 msg = self.input_entry.get_text()
259 self.galtack_client.send_commands((1, "message", msg))
260 self.input_entry.set_text("")
262 def __cmd_message(self, command):
263 sender, message = command[3:]
264 snd = ""
265 if sender: snd = " " + sender
266 self.__buf("%s%s %s\n" % (time.strftime("%X"), snd, message))
267 if "BOTQUIT" in message:
268 self.galtack_client.logout()
269 w = Window()
270 w.set_position(gtk.WIN_POS_CENTER)
271 w.present()
272 o, i = gnome_hig(w)
273 i(Label("Bye!")).modify_font(pango.FontDescription("sans 32"))
274 gobject.timeout_add(2000, main_loop_quit)
276 def __cmd_close(self, command):
277 self.galtack_client.send_commands((1, "[CLOSE]"))
278 gobject.timeout_add(500, main_loop_quit) # TODO: await ACK
280 def __cmd_stop(self, command):
281 self.__buf("(stop)\n")
283 def __cmd_start(self, command):
284 self.__buf("(start)\n")
287 class GaltackServerListWindow(WindowF12RawConsoleMixin):
289 Window displaying the list of Galcon servers.
292 def __init__(self, options, user_info):
293 self.__options = options
294 super(GaltackServerListWindow, self).__init__()
295 self.__user_info = user_info
296 self.set_title("Galcon Server List - GalTacK")
297 self.set_default_size(600, 400)
298 self.set_position(gtk.WIN_POS_CENTER)
300 outer_vbox, self.__inner_vbox = gnome_hig(self)
302 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
303 self.__version_label = hbox(LeftLabel(""), False, False)
304 self.set_current_version()
305 self.__version_label.set_sensitive(False)
307 self.__refresh_button = hbox(Button("_Refresh List"), False, False,
308 pack_end = True)
309 self.__refresh_button.connect("clicked", self.__cb_refresh)
311 sw = self.__inner_vbox(Frame())(ScrolledWindow())
312 self.__treeview = sw(TreeView())
313 self.__treeview.set_sensitive(False)
314 self.__treeview.set_property("rules-hint", True)
315 cb = self.__cb_selection_changed
316 self.__treeview.get_selection().connect("changed", cb)
318 for i, spec in enumerate(galtack.net.ServerInfo.COL_SPEC):
319 if not spec["visible"]: continue
320 col = gtk.TreeViewColumn(spec["caption"])
321 col.set_reorderable(True)
322 col.set_sort_column_id(i)
323 self.__treeview.append_column(col)
324 cell = gtk.CellRendererText()
325 col.pack_start(cell, True)
326 col.add_attribute(cell, "text", i)
327 col.add_attribute(cell, "sensitive", 0)
329 hbox = gnome_hig(self.__inner_vbox(HBox(), False, False))
330 self.__join_button = hbox(Button("_Join"))
331 self.__join_button.set_sensitive(False)
332 self.__join_button.connect("clicked", self.__cb_join)
334 self.back_button = hbox(Button("_Back"), False, False)
335 self.back_button.connect("clicked", self.__cb_back)
337 self.__statusbar = outer_vbox(Statusbar(), False, False)
338 self.__statusbar_cid = cid = self.__statusbar.get_context_id("msg")
340 self.__server_password_prompt = dlg = Dialog(
341 "Server Password Required", self, gtk.DIALOG_MODAL,
342 (gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT,
343 gtk.STOCK_OK, gtk.RESPONSE_ACCEPT)
345 dlg.set_default_response(gtk.RESPONSE_ACCEPT)
346 dlg.set_default_size(300, -1)
347 vbox = gnome_hig(VBox()) # I want my own VBox here ;-)
348 vbox.set_border_width(6)
349 dlg.vbox.pack_start(vbox)
350 self.__server_password_prompt_label = vbox(LeftLabel())
351 self.__server_password_prompt_entry = vbox(Entry())
352 self.__server_password_prompt_entry.set_visibility(False)
354 def set_current_version(self, current_version = None):
355 self.__version_label.set_sensitive(False)
356 if current_version is None:
357 self.__version_label.set_text("Current version: ???")
358 else:
359 self.__version_label.set_text("Current version: %s" %
360 ".".join(current_version))
361 self.__version_label.set_sensitive(True)
363 def set_server_info_list(self, server_info_list = None):
364 types = galtack.net.ServerInfo.get_col_types()
365 model = gtk.ListStore(*types)
366 self.__treeview.set_sensitive(False)
367 if server_info_list is not None:
368 for server_info in server_info_list:
369 model.append(server_info.get_data_tuple())
370 self.__treeview.set_sensitive(True)
371 self.__treeview.set_model(model)
373 def __cb_refresh(self, button):
374 self.window.set_cursor(watch_cursor)
375 self.__inner_vbox.set_sensitive(False)
376 self.__statusbar.push(self.__statusbar_cid,
377 "Retrieving server list...")
378 deferred = galtack.net.get_server_list(self.__user_info)
379 deferred.addCallback(self.__server_list_callback)
380 deferred.addErrback(self.__server_list_errback)
382 def __server_list_callback(self, (current_version, server_info_list)):
383 self.set_current_version(current_version)
384 self.set_server_info_list(server_info_list)
385 self.__statusbar.pop(self.__statusbar_cid)
386 self.__inner_vbox.set_sensitive(True)
387 self.window.set_cursor(arrow_cursor)
389 def __server_list_errback(self, failure):
390 self.set_sensitive(True)
391 self.window.set_cursor(arrow_cursor)
392 cid = self.__statusbar_cid
393 sbar = self.__statusbar
394 sbar.pop(cid)
395 gtk.gdk.beep()
396 mid = sbar.push(cid, "Retrieving server list failed")
397 gobject.timeout_add(4000, lambda: sbar.remove(cid, mid))
398 return failure
400 def __cb_selection_changed(self, treesel):
401 model, iter = treesel.get_selected()
402 if iter is None:
403 ok_to_join = False
404 else:
405 data = galtack.net.ServerInfo(*model[iter])
406 ok_to_join = data.bots_ok
407 self.__join_button.set_sensitive(ok_to_join)
409 def __cb_join(self, button):
410 treesel = self.__treeview.get_selection()
411 model, iter = treesel.get_selected()
412 server_info = galtack.net.ServerInfo(*model[iter])
413 if server_info.pwd_protected:
414 self.__run_server_password_prompt(server_info)
415 return None
416 self.join_with_server_info(server_info)
418 def __cb_back(self, button):
419 GaltackLoginWindow._instance.show()
420 self.destroy()
422 def __run_server_password_prompt(self, server_info):
423 n, o = server_info["name"], server_info["owner"]
424 if n: n += " - "
425 s = "Password for %s%s:" % (n, o)
426 self.__server_password_prompt_label.set_text(s)
427 response_id = self.__server_password_prompt.run()
428 if response_id != gtk.RESPONSE_ACCEPT: return None
429 passwd = self.__server_password_prompt_entry.get_text()
430 server_info["passwd"] = passwd
431 self.join_with_server_info(server_info)
433 def join_with_server_info(self, server_info):
434 chat_window = GaltackChatWindow(self.__options,
435 self.__user_info, server_info)
436 gobject.idle_add(self.hide)
439 class GaltackLoginWindow(WindowF12RawConsoleMixin):
441 A Window asking the user for Galcon login details and log in.
444 def __init__(self, options):
445 self.__options = options
446 super(GaltackLoginWindow, self).__init__()
447 self.set_title("GalTacK Login")
448 self.set_default_size(400, -1)
449 self.set_position(gtk.WIN_POS_CENTER)
451 outer_vbox, self.inner_vbox = gnome_hig(self)
453 table = gnome_hig(self.inner_vbox(Table(), False, False))
455 xop = {"xoptions": gtk.FILL}
456 table.add_rows()
457 label = table.attach_cell(LeftLabel("_Email Address:"), **xop)
458 self.email_entry = table.attach_cell(Entry())
459 label.set_mnemonic_widget(self.email_entry)
460 if self.__options.email is not None:
461 self.email_entry.set_text(self.__options.email)
463 table.add_rows()
464 label = table.attach_cell(LeftLabel("_Username:"), **xop)
465 self.name_entry = table.attach_cell(Entry())
466 label.set_mnemonic_widget(self.name_entry)
467 if self.__options.user is not None:
468 self.name_entry.set_text(self.__options.user)
470 table.add_rows()
471 label = table.attach_cell(LeftLabel("_Password:"), **xop)
472 self.passwd_entry = table.attach_cell(Entry())
473 label.set_mnemonic_widget(self.passwd_entry)
474 self.passwd_entry.set_visibility(False)
475 if self.__options.password is not None:
476 self.passwd_entry.set_text(self.__options.password)
478 table.add_rows()
479 hbox = gnome_hig(table.attach_row(HBox()))
480 self.__login_button = hbox(Button("_Sign In"))
481 self.__login_button.connect("clicked", self.__cb_login)
482 self.__login_button.set_property("can-default", True)
483 gobject.idle_add(self.__login_button.grab_default)
485 self.__quit_button = hbox(Button("_Quit"), False, False)
486 self.__quit_button.connect("clicked", self.__cb_quit)
488 self.statusbar = outer_vbox(Statusbar(), False, False)
489 self.statusbar_cid = cid = self.statusbar.get_context_id("msg")
490 mid = self.statusbar.push(cid, "Enter your login details")
491 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
493 def __get_user_info(self):
494 email = self.email_entry.get_text()
495 name = self.name_entry.get_text()
496 passwd = self.passwd_entry.get_text()
497 platform = "linux2"
498 version = "1.2.1"
499 return galtack.net.UserInfo(email, name, passwd, platform, version)
501 def __cb_login(self, button):
502 self.window.set_cursor(watch_cursor)
503 self.inner_vbox.set_sensitive(False)
504 self.statusbar.push(self.statusbar_cid,
505 "Retrieving server list...")
506 user_info = self.__get_user_info()
507 deferred = galtack.net.get_server_list(user_info)
508 deferred.addCallbacks(self.__server_list_callback,
509 self.__server_list_errback)
510 deferred.addErrback(print_errback)
512 def __cb_quit(self, button):
513 main_loop_quit()
515 def __server_list_callback(self, (current_version, server_info_list)):
516 w = GaltackServerListWindow(self.__options, self.__get_user_info())
517 GaltackServerListWindow._instance = w
518 w.set_current_version(current_version)
519 w.set_server_info_list(server_info_list)
520 gobject.idle_add(self.hide)
521 self.inner_vbox.set_sensitive(True)
522 self.window.set_cursor(arrow_cursor)
523 cid = self.statusbar_cid
524 self.statusbar.pop(cid)
526 def __server_list_errback(self, failure):
527 failure.trap(galtack.net.ServerListException)
528 self.inner_vbox.set_sensitive(True)
529 self.window.set_cursor(arrow_cursor)
530 cid = self.statusbar_cid
531 self.statusbar.pop(cid)
532 gtk.gdk.beep()
533 mid = self.statusbar.push(cid, "Failure: %s" %
534 failure.getErrorMessage())
535 gobject.timeout_add(4000, lambda: self.statusbar.remove(cid, mid))
536 return None
538 @classmethod # for potential inheritance
539 def _modify_option_parser(cls, option_parser):
540 option_parser.add_option("-e", "--email", metavar = "ADDRESS",
541 help = "login email address")
542 option_parser.add_option("-u", "--user",
543 help = "login username")
544 option_parser.add_option("-p", "--password",
545 help = "login password")
546 return option_parser
549 def main(argv):
550 option_parser = optparse.OptionParser(prog = "GalTacK",
551 version = "%%prog %s" %
552 galtack_version_str)
553 option_parser = GaltackLoginWindow._modify_option_parser(option_parser)
554 option_parser = GaltackClient._modify_option_parser(option_parser)
555 options, arguments = option_parser.parse_args(argv[1:])
556 GaltackLoginWindow._instance = GaltackLoginWindow(options)
557 main_loop_run()
558 return 0
560 if __name__ == "__main__":
561 import sys
562 sys.exit(main(sys.argv))