From 9772129f30a14a94bf806f9ea288ec817f5952a5 Mon Sep 17 00:00:00 2001 From: Eratosthenes Date: Tue, 7 Sep 2010 09:50:29 -0400 Subject: [PATCH] Rewrite of XTables to use Xlib. Considerable cleanup in TableWindow. --- pyfpdb/HandHistoryConverter.py | 13 ++- pyfpdb/TableWindow.py | 179 +++++++++++++++++++++++++-------- pyfpdb/Tables_Demo.py | 32 +++--- pyfpdb/XTables.py | 96 +++++++++--------- 4 files changed, 212 insertions(+), 108 deletions(-) diff --git a/pyfpdb/HandHistoryConverter.py b/pyfpdb/HandHistoryConverter.py index 2053eba9..4dfc3a80 100644 --- a/pyfpdb/HandHistoryConverter.py +++ b/pyfpdb/HandHistoryConverter.py @@ -501,12 +501,23 @@ or None if we fail to get the info """ else: return table_name - + @staticmethod + def getTableNoRe(tournament): + "Returns string to search window title for tournament table no." +# Full Tilt: $30 + $3 Tournament (181398949), Table 1 - 600/1200 Ante 100 - Limit Razz +# PokerStars: WCOOP 2nd Chance 02: $1,050 NLHE - Tournament 307521826 Table 1 - Blinds $30/$60 + return "%s.+Table (\d+)" % (tournament, ) def getTableTitleRe(config, sitename, *args, **kwargs): "Returns string to search in windows titles for current site" return getSiteHhc(config, sitename).getTableTitleRe(*args, **kwargs) +def getTableNoRe(config, sitename, *args, **kwargs): + "Returns string to search window titles for tournament table no." + return getSiteHhc(config, sitename).getTableNoRe(*args, **kwargs) + + + def getSiteHhc(config, sitename): "Returns HHC class for current site" hhcName = config.supported_sites[sitename].converter diff --git a/pyfpdb/TableWindow.py b/pyfpdb/TableWindow.py index aa994bfe..a77a9aaa 100644 --- a/pyfpdb/TableWindow.py +++ b/pyfpdb/TableWindow.py @@ -1,11 +1,13 @@ #!/usr/bin/env python -"""Discover_TableWindow.py +"""Base class for interacting with poker client windows. -Inspects the currently open windows and finds those of interest to us--that is -poker table windows from supported sites. Returns a list -of Table_Window objects representing the windows found. +There are currently subclasses for X and Windows. + +The class queries the poker client window for data of interest, such as +size and location. It also controls the signals to alert the HUD when the +client has been resized, destroyed, etc. """ -# Copyright 2008 - 2009, Ray E. Barker +# Copyright 2008 - 2010, Ray E. Barker # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -24,35 +26,36 @@ of Table_Window objects representing the windows found. ######################################################################## # Standard Library modules -import os -import sys +import re # pyGTK modules -import pygtk import gtk import gobject # FreePokerTools modules -import Configuration -#if os.name == "posix": -# import XTables -#elif os.name == "nt": -# import WinTables +from HandHistoryConverter import getTableTitleRe +from HandHistoryConverter import getTableNoRe -# Global used for figuring out the current game being played from the title -# The dict key is the fpdb name for the game +# Global used for figuring out the current game being played from the title. +# The dict key is a tuple of (limit type, category) for the game. # The list is the names for those games used by the supported poker sites -# This is currently only used for HORSE, so it only needs to support those +# This is currently only used for mixed games, so it only needs to support those # games on PokerStars and Full Tilt. -game_names = { #fpdb name Stars Name FTP Name - "holdem" : ("Hold\'em" , ), - "omahahilo" : ("Omaha H/L" , ), - "studhilo" : ("Stud H/L" , ), - "razz" : ("Razz" , ), - "studhi" : ("Stud" , "Stud Hi") +nlpl_game_names = { #fpdb name Stars Name FTP Name (if different) + ("nl", "holdem" ) : ("No Limit Hold\'em" , ), + ("pl", "holdem" ) : ("Pot Limit Hold\'em" , ), + ("pl", "omahahi" ) : ("Pot Limit Omaha" ,"Pot Limit Omaha Hi" ), + } +limit_game_names = { #fpdb name Stars Name FTP Name + ("fl", "holdem" ) : ("Limit Hold\'em" , ), + ("fl", "omahahilo" ) : ("Limit Omaha H/L" , ), + ("fl", "studhilo" ) : ("Limit Stud H/L" , ), + ("fl", "razz" ) : ("Limit Razz" , ), + ("fl", "studhi" ) : ("Limit Stud" , "Stud Hi"), + ("fl", "27_3draw" ) : ("Limit Triple Draw 2-7 Lowball", ) } -# A window title might have our table name + one of theses words/ +# A window title might have our table name + one of these words/ # phrases. If it has this word in the title, it is not a table. bad_words = ('History for table:', 'HUD:', 'Chat:') @@ -75,11 +78,19 @@ gobject.signal_new("client_destroyed", gtk.Window, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)) +gobject.signal_new("game_changed", gtk.Window, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)) + +gobject.signal_new("table_changed", gtk.Window, + gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)) + # Each TableWindow object must have the following attributes correctly populated: # tw.name = the table name from the title bar, which must to match the table name -# from the corresponding hand history. -# tw.site = the site name, e.g. PokerStars, FullTilt. This must match the site -# name specified in the config file. +# from the corresponding hand record in the db. # tw.number = This is the system id number for the client table window in the # format that the system presents it. This is Xid in Xwindows and # hwnd in Microsoft Windows. @@ -91,61 +102,143 @@ gobject.signal_new("client_destroyed", gtk.Window, # to the top left of the display screen. This also does not include the # title bar and window borders. To put it another way, this is the # screen location of (0, 0) in the working window. +# tournament = Tournament number for a tournament or None for a cash game. +# table = Table number for a tournament. +# gdkhandle = +# window = +# parent = +# game = +# search_string = class Table_Window(object): - def __init__(self, search_string, table_name = None, tournament = None, table_number = None): + def __init__(self, config, site, table_name = None, tournament = None, table_number = None): + self.config = config + self.site = site if tournament is not None and table_number is not None: - print "tournament %s, table %s" % (tournament, table_number) self.tournament = int(tournament) self.table = int(table_number) self.name = "%s - %s" % (self.tournament, self.table) + self.type = "tour" + table_kwargs = dict(tournament = self.tournament, table_number = self.table) + self.tableno_re = getTableNoRe(self.config, self.site, tournament = self.tournament) elif table_name is not None: - # search_string = table_name self.name = table_name + self.type = "cash" self.tournament = None + table_kwargs = dict(table_name = table_name) + else: return None - self.find_table_parameters(search_string) + self.search_string = getTableTitleRe(self.config, self.site, self.type, **table_kwargs) + self.find_table_parameters() def __str__(self): # __str__ method for testing - likely_attrs = ("site", "number", "title", "width", "height", "x", "y", - "tournament", "table", "gdkhandle") + likely_attrs = ("number", "title", "site", "width", "height", "x", "y", + "tournament", "table", "gdkhandle", "window", "parent", + "game", "search_string", "tableno_re") temp = 'TableWindow object\n' for a in likely_attrs: if getattr(self, a, 0): temp += " %s = %s\n" % (a, getattr(self, a)) return temp +#################################################################### +# "get" methods. These query the table and return the info to get. +# They don't change the data in the table and are generally used +# by the "check" methods. Most of the get methods are in the +# subclass because they are specific to X, Windows, etc. def get_game(self): title = self.get_window_title() - print title - for game, names in game_names.iteritems(): + if title is None: + return False + +# check for nl and pl games first, to avoid bad matches + for game, names in nlpl_game_names.iteritems(): for name in names: if name in title: return game - return None + for game, names in limit_game_names.iteritems(): + for name in names: + if name in title: + return game + return False - def check_geometry(self): + def get_table_no(self): + new_title = self.get_window_title() + if new_title is None: + return False + + mo = re.search(self.tableno_re, new_title) + if mo is not None: + return mo[1] + return False + +#################################################################### +# check_table() is meant to be called by the hud periodically to +# determine if the client has been moved or resized. check_table() +# also checks and signals if the client has been closed. + def check_table(self, hud): + result = self.check_size() + if result != False: + hud.main_window.emit(result, hud) + if result == "client_destroyed": + return True + + result = self.check_loc() + if result != False: + hud.main_window.emit(result, hud) + if result == "client_destroyed": + return True + return True + +#################################################################### +# "check" methods. They use the corresponding get method, update the +# table object and return the name of the signal to be emitted or +# False if unchanged. These do not signal for destroyed +# clients to prevent a race condition. + +# These might be called by a Window.timeout, so they must not +# return False, or the timeout will be cancelled. + def check_game(self, hud): + new_game = self.get_game() + if new_game is not None and self.game != new_game: + self.game = new_game + hud.main_window.emit("game_changed", hud) + return "game_changed" + return True + + def check_size(self): new_geo = self.get_geometry() - if new_geo is None: # window destroyed return "client_destroyed" - elif self.x != new_geo['x'] or self.y != new_geo['y']: # window moved - self.x = new_geo['x'] - self.y = new_geo['y'] - return "client_moved" - elif self.width != new_geo['width'] or self.height != new_geo['height']: # window resized self.width = new_geo['width'] self.height = new_geo['height'] return "client_resized" + return False # no change - else: return False # window not changed + def check_loc(self): + new_geo = self.get_geometry() + if new_geo is None: # window destroyed + return "client_destroyed" + if self.x != new_geo['x'] or self.y != new_geo['y']: # window moved + self.x = new_geo['x'] + self.y = new_geo['y'] + return "client_moved" + return False # no change + + def check_table_no(self, hud): + result = self.get_table_no() + if result != False and result != self.table: + self.table = result + hud.main_window.emit("table_changed", hud) + return True + def check_bad_words(self, title): for word in bad_words: if word in title: return True diff --git a/pyfpdb/Tables_Demo.py b/pyfpdb/Tables_Demo.py index 199e81c2..aff1d006 100755 --- a/pyfpdb/Tables_Demo.py +++ b/pyfpdb/Tables_Demo.py @@ -31,7 +31,6 @@ import gobject # fpdb/free poker tools modules import Configuration -from HandHistoryConverter import getTableTitleRe # get the correct module for the current os if os.name == 'posix': @@ -58,24 +57,31 @@ if __name__=="__main__": self.main_window.move(table.x + dx, table.y + dy) self.main_window.show_all() table.topify(self) + +# These are the currently defined signals. Do this in the HUD. self.main_window.connect("client_moved", self.client_moved) self.main_window.connect("client_resized", self.client_resized) self.main_window.connect("client_destroyed", self.client_destroyed) + self.main_window.connect("game_changed", self.game_changed) + self.main_window.connect("table_changed", self.table_changed) +# And these of the handlers that go with those signals. +# These would live inside the HUD code. def client_moved(self, widget, hud): self.main_window.move(self.table.x + self.dx, self.table.y + self.dy) def client_resized(self, *args): - print "client resized" + print "Client resized" def client_destroyed(self, *args): # call back for terminating the main eventloop + print "Client destroyed." gtk.main_quit() - def check_on_table(table, hud): - result = table.check_geometry() - if result != False: - hud.main_window.emit(result, hud) - return True + def game_changed(self, *args): + print "Game Changed." + + def table_changed(self, *args): + print "Table Changed." print "enter table name to find: ", table_name = sys.stdin.readline() @@ -92,16 +98,12 @@ if __name__=="__main__": type = "cash" table_kwargs = dict(table_name = table_name) - search_string = getTableTitleRe(config, "Full Tilt Poker", type, **table_kwargs) - table = Tables.Table(search_string, **table_kwargs) - table.gdk_handle = gtk.gdk.window_foreign_new(table.number) - - print "table =", table -# print "game =", table.get_game() + table = Tables.Table(config, "Full Tilt Poker", **table_kwargs) + print table fake = fake_hud(table) - print "fake =", fake - gobject.timeout_add(100, check_on_table, table, fake) + gobject.timeout_add(1000, table.check_game, fake) + gobject.timeout_add(100, table.check_table, fake) print "calling main" gtk.main() diff --git a/pyfpdb/XTables.py b/pyfpdb/XTables.py index 03b18073..10626950 100644 --- a/pyfpdb/XTables.py +++ b/pyfpdb/XTables.py @@ -1,7 +1,5 @@ #!/usr/bin/env python -"""XTables.py - -XWindows specific methods for TableWindows Class. +"""XWindows specific methods for TableWindows Class. """ # Copyright 2008 - 2010, Ray E. Barker @@ -23,73 +21,67 @@ XWindows specific methods for TableWindows Class. # Standard Library modules import re +import os +import subprocess # pyGTK modules import gtk # Other Library modules import Xlib.display -from Xlib import Xatom -# FreePokerTools modules +# FPDB modules from TableWindow import Table_Window # We might as well do this once and make them globals disp = Xlib.display.Display() root = disp.screen().root -name_atom = disp.get_atom("WM_NAME", 1) -icon_atom = disp.get_atom("WM_ICON_NAME", 1) class Table(Table_Window): - def find_table_parameters(self, search_string): - self.window = None - done_looping = False - for outside in root.query_tree().children: - for inside in outside.query_tree().children: - if done_looping: break -# if inside.get_wm_name() and re.search(search_string, inside.get_wm_name()): - if inside.get_wm_name() and re.search(search_string, inside.get_wm_icon_name()): - print "inside = ", inside - print "\n".join(dir(inside)) - for atom in inside.list_properties(): print atom, disp.get_atom_name(atom), inside.get_full_property(atom, Xatom.STRING).value - if hasattr(inside, "window"): print "window = ", str(inside.window) - if self.check_bad_words(inside.get_wm_name()): continue - self.window = inside - self.parent = outside - done_looping = True - break + def find_table_parameters(self): - if self.window == None or self.parent == None: - print "Window %s not found. Skipping." % search_string - return None - - my_geo = self.window.get_geometry() - pa_geo = self.parent.get_geometry() - - self.x = pa_geo.x + my_geo.x - self.y = pa_geo.y + my_geo.y - self.width = my_geo.width - self.height = my_geo.height - self.exe = self.window.get_wm_class()[0] - self.title = self.window.get_wm_name() - self.site = "" - self.hud = None - self.number = self.get_xid() + self.number = None + for listing in os.popen('xwininfo -root -tree').readlines(): + if re.search(self.search_string, listing): + mo = re.match('\s+([\dxabcdef]+) (.+):\s\(\"([a-zA-Z0-9\-.]+)\".+ (\d+)x(\d+)\+\d+\+\d+ \+(\d+)\+(\d+)', listing) + self.number = int( mo.group(1), 0) + self.title = re.sub('\"', '', mo.group(2)) + self.exe = "" # not used? + self.hud = None # specified later + break - def get_xid(self): - mo = re.match('Xlib\.display\.Window\(([\dxabcdef]+)', str(self.window)) - if not mo: + if self.number is None: return None - else: - return int( mo.group(1), 0) + + self.window = self.get_window_from_xid(self.number) + self.parent = self.window.query_tree().parent + + geo = self.get_geometry() + if geo is None: return None + self.width = geo['width'] + self.height = geo['height'] + self.x = geo['x'] + self.y = geo['y'] + + self.game = self.get_game() + self.gdk_handle = gtk.gdk.window_foreign_new(self.number) + + def get_window_from_xid(self, id): + for outside in root.query_tree().children: + if outside.id == id: + return outside + for inside in outside.query_tree().children: + if inside.id == id: + return inside + return None def get_geometry(self): try: my_geo = self.window.get_geometry() pa_geo = self.parent.get_geometry() - return {'x' : pa_geo.x + my_geo.x, - 'y' : pa_geo.y + my_geo.y, + return {'x' : my_geo.x + pa_geo.x, + 'y' : my_geo.y + pa_geo.y, 'width' : my_geo.width, 'height' : my_geo.height } @@ -97,8 +89,14 @@ class Table(Table_Window): return None def get_window_title(self): - return self.window.get_wm_name() - + proc = subprocess.Popen("xwininfo -wm -id %d" % self.number, shell = True, stdout = subprocess.PIPE) + s = proc.stdout.read() + mo = re.search('"(.+)"', s) + try: + return mo.group(1) + except AttributeError: + return None + def topify(self, hud): hud.main_window.gdkhandle = gtk.gdk.window_foreign_new(hud.main_window.window.xid) hud.main_window.gdkhandle.set_transient_for(self.gdk_handle)