Use debian 2.7 only
[fpbd-bostik.git] / pyfpdb / GuiHandViewer.py
blob1f4ce318b88aa7bd72718f04879139216fd322cc
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
4 #Copyright 2010-2011 Maxime Grandchamp
5 #This program is free software: you can redistribute it and/or modify
6 #it under the terms of the GNU Affero General Public License as published by
7 #the Free Software Foundation, version 3 of the License.
9 #This program is distributed in the hope that it will be useful,
10 #but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 #GNU General Public License for more details.
14 #You should have received a copy of the GNU Affero General Public License
15 #along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #In the "official" distribution you can find the license in agpl-3.0.txt.
20 # This code once was in GuiReplayer.py and was split up in this and the former by zarturo.
22 import L10n
23 _ = L10n.get_translation()
26 from Hand import *
27 import Configuration
28 import Database
29 import SQL
30 import fpdb_import
31 import Filters
32 import pygtk
33 pygtk.require('2.0')
34 import gtk
35 import math
36 import gobject
38 import copy
40 import GuiReplayer
42 import pprint
43 pp = pprint.PrettyPrinter(indent=4)
45 # The ListView renderer data function requires a function signature of
46 # renderer_cell_func(tree_column, cell, model, tree_iter, data)
47 # Placing the function into the Replayer object changes the call singature
48 # card_images has been made global to facilitate this.
50 global card_images
51 card_images = 53 * [0]
53 def card_renderer_cell_func(tree_column, cell, model, tree_iter, data):
54 card_width = 30
55 card_height = 42
56 col = data
57 coldata = model.get_value(tree_iter, col)
58 if coldata == None or coldata == '':
59 coldata = "0x"
60 coldata = coldata.replace("'","")
61 coldata = coldata.replace("[","")
62 coldata = coldata.replace("]","")
63 coldata = coldata.replace("'","")
64 coldata = coldata.replace(",","")
65 #print "DEBUG: coldata: %s" % (coldata)
66 cards = [Card.encodeCard(c) for c in coldata.split(' ')]
67 n_cards = len(cards)
69 #print "DEBUG: cards: %s" % cards
70 pixbuf = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, True, 8, card_width * n_cards, card_height)
71 if pixbuf:
72 x = 0 # x coord where the next card starts in scratch
73 for card in cards:
74 if card == None or card ==0:
75 card_images[0].copy_area(0, 0, card_width, card_height, pixbuf, x, 0)
77 card_images[card].copy_area(0, 0, card_width, card_height, pixbuf, x, 0)
78 x = x + card_width
79 cell.set_property('pixbuf', pixbuf)
82 # This function is a duplicate of 'ledger_style_render_func' in GuiRingPlayerStats
83 # TODO: Pull generic cell formatting functions into something common.
84 def cash_renderer_cell_func(tree_column, cell, model, tree_iter, data):
85 col = data
86 coldata = model.get_value(tree_iter, col)
87 if '-' in coldata:
88 coldata = coldata.replace("-", "")
89 coldata = "(%s)" %(coldata)
90 cell.set_property('foreground', 'red')
91 else:
92 cell.set_property('foreground', 'darkgreen')
93 cell.set_property('text', coldata)
95 def reset_style_render_func(tree_column, cell, model, iter, data):
96 cell.set_property('foreground', None)
97 cell.set_property('text', model.get_value(iter, data))
100 class GuiHandViewer:
101 def __init__(self, config, querylist, mainwin, options = None, debug=True):
102 self.debug = debug
103 self.config = config
104 self.main_window = mainwin
105 self.sql = querylist
106 self.replayer = None
107 self.date_from = None
108 self.date_to = None
110 # These are temporary variables until it becomes possible
111 # to select() a Hand object from the database
112 self.site="PokerStars"
114 self.db = Database.Database(self.config, sql=self.sql)
117 filters_display = { "Heroes" : True,
118 "Sites" : True,
119 "Games" : True,
120 "Currencies": False,
121 "Limits" : True,
122 "LimitSep" : True,
123 "LimitType" : True,
124 "Positions" : True,
125 "Type" : True,
126 "Seats" : False,
127 "SeatSep" : False,
128 "Dates" : True,
129 "Cards" : True,
130 "Groups" : False,
131 "GroupsAll" : False,
132 "Button1" : True,
133 "Button2" : False
136 self.filters = Filters.Filters(self.db, self.config, self.sql, display = filters_display)
137 self.filters.registerButton1Name(_("Load Hands"))
138 self.filters.registerButton1Callback(self.loadHands)
139 self.filters.registerCardsCallback(self.filter_cards_cb)
140 #self.filters.registerButton2Name(_("temp"))
141 #self.filters.registerButton2Callback(self.temp())
143 # hierarchy: self.mainHBox / self.hpane / self.handsVBox / self.area
145 self.mainHBox = gtk.HBox(False, 0)
146 self.mainHBox.show()
148 self.leftPanelBox = self.filters.get_vbox()
150 self.hpane = gtk.HPaned()
151 self.hpane.pack1(self.leftPanelBox)
152 self.mainHBox.add(self.hpane)
154 self.handsVBox = gtk.VBox(False, 0)
155 self.handsVBox.show()
157 self.hpane.pack2(self.handsVBox)
158 self.hpane.show()
160 self.playing = False
162 self.deck_image = "Cards01.png" #FIXME: read from config (requires deck to be defined somewhere appropriate
163 self.tableImage = None
164 self.playerBackdrop = None
165 self.cardImages = None
166 #NOTE: There are two caches of card images as I haven't found a way to
167 # replicate the copy_area() function from Pixbuf in the Pixmap class
168 # cardImages is used for the tables display card_images is used for the
169 # table display. Sooner or later we should probably use one or the other.
170 card_images = self.init_card_images(config)
172 def init_card_images(self, config):
173 suits = ('s', 'h', 'd', 'c')
174 ranks = (14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2)
175 pb = gtk.gdk.pixbuf_new_from_file(config.execution_path(self.deck_image))
177 for j in range(0, 13):
178 for i in range(0, 4):
179 loc = Card.cardFromValueSuit(ranks[j], suits[i])
180 card_images[loc] = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, pb.get_has_alpha(), pb.get_bits_per_sample(), 30, 42)
181 pb.copy_area(30*j, 42*i, 30, 42, card_images[loc], 0, 0)
182 card_images[0] = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, pb.get_has_alpha(), pb.get_bits_per_sample(), 30, 42)
183 pb.copy_area(30*13, 0, 30, 42, card_images[0], 0, 0)
184 return card_images
186 def loadHands(self, button, userdata):
187 hand_ids = self.get_hand_ids_from_date_range(self.filters.getDates()[0], self.filters.getDates()[1])
188 self.reload_hands(hand_ids)
190 def get_hand_ids_from_date_range(self, start, end, save_date = False):
191 """Returns the handids in the given date range and in the filters.
192 Set save_data to true if you want to keep the start and end date if no other date is specified through the filters by the user."""
194 if save_date:
195 self.date_from = start
196 self.date_to = end
197 else:
198 if start != self.filters.MIN_DATE: #if date is ever changed by the user previously saved dates are deleted
199 self.date_from = None
200 if end != self.filters.MAX_DATE:
201 self.date_to = None
203 if self.date_from != None and start == self.filters.MIN_DATE:
204 start = self.date_from
206 if self.date_to != None and end == self.filters.MAX_DATE:
207 end = self.date_to
209 q = self.db.sql.query['handsInRange']
210 q = q.replace('<datetest>', "between '" + start + "' and '" + end + "'")
211 q = self.filters.replace_placeholders_with_filter_values(q)
213 c = self.db.get_cursor()
215 c.execute(q)
216 return [r[0] for r in c.fetchall()]
218 def rankedhand(self, hand, game):
219 ranks = {'0':0, '2':2, '3':3, '4':4, '5':5, '6':6, '7':7, '8':8, '9':9, 'T':10, 'J':11, 'Q':12, 'K':13, 'A':14}
220 suits = {'x':0, 's':1, 'c':2, 'd':3, 'h':4}
222 if game == 'holdem':
223 card1 = ranks[hand[0]]
224 card2 = ranks[hand[3]]
225 suit1 = suits[hand[1]]
226 suit2 = suits[hand[4]]
227 if card1 < card2:
228 (card1, card2) = (card2, card1)
229 (suit1, suit2) = (suit2, suit1)
230 if suit1 == suit2:
231 suit1 += 4
232 return card1 * 14 * 14 + card2 * 14 + suit1
233 else:
234 return 0
236 def sorthand(self, model, iter1, iter2):
237 hand1 = self.hands[int(model.get_value(iter1, self.colnum['HandId']))]
238 hand2 = self.hands[int(model.get_value(iter2, self.colnum['HandId']))]
239 base1 = hand1.gametype['base']
240 base2 = hand2.gametype['base']
241 if base1 < base2:
242 return -1
243 elif base1 > base2:
244 return 1
246 cat1 = hand1.gametype['category']
247 cat2 = hand2.gametype['category']
248 if cat1 < cat2:
249 return -1
250 elif cat1 > cat2:
251 return 1
253 a = self.rankedhand(model.get_value(iter1, 0), hand1.gametype['category'])
254 b = self.rankedhand(model.get_value(iter2, 0), hand2.gametype['category'])
256 if a < b:
257 return -1
258 elif a > b:
259 return 1
261 return 0
263 def sort_float(self, model, iter1, iter2, col):
264 a = float(model.get_value(iter1, col))
265 b = float(model.get_value(iter2, col))
267 if a < b:
268 return -1
269 elif a > b:
270 return 1
272 return 0
274 def sort_pos(self, model, iter1, iter2, col):
275 a = self.__get_sortable_int_from_pos__(model.get_value(iter1, col))
276 b = self.__get_sortable_int_from_pos__(model.get_value(iter2, col))
278 if a < b:
279 return -1
280 elif a > b:
281 return 1
283 return 0
285 def __get_sortable_int_from_pos__(self, pos):
286 if pos == 'B':
287 return 8
288 if pos == 'S':
289 return 9
290 else:
291 return int(pos)
293 def reload_hands(self, handids):
294 self.hands = {}
295 for handid in handids:
296 self.hands[handid] = self.importhand(handid)
297 self.refreshHands()
299 def refreshHands(self):
300 try:
301 self.handsWindow.destroy()
302 except:
303 pass
304 self.handsWindow = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
305 self.handsWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
306 self.handsVBox.pack_end(self.handsWindow)
308 # Dict of colnames and their column idx in the model/ListStore
309 self.colnum = {
310 'Stakes' : 0,
311 'Pos' : 1,
312 'Street0' : 2,
313 'Action0' : 3,
314 'Street1-4' : 4,
315 'Action1-4' : 5,
316 'Won' : 6,
317 'Bet' : 7,
318 'Net' : 8,
319 'Game' : 9,
320 'HandId' : 10,
322 self.liststore = gtk.ListStore(*([str] * len(self.colnum)))
323 self.view = gtk.TreeView()
324 self.view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
325 self.handsWindow.add(self.view)
327 #self.viewfilter = self.liststore.filter_new() #if a filter is used, the sorting doesnt work anymore!! As GtkTreeModelFilter does NOT implement GtkTreeSortable
328 #self.view.set_model(self.viewfilter)
329 self.view.set_model(self.liststore)
330 textcell = gtk.CellRendererText()
331 numcell = gtk.CellRendererText()
332 numcell.set_property('xalign', 1.0)
333 pixbuf = gtk.CellRendererPixbuf()
334 pixbuf.set_property('xalign', 0.0)
336 self.view.insert_column_with_data_func(-1, 'Stakes', textcell, reset_style_render_func ,self.colnum['Stakes'])
337 self.view.insert_column_with_data_func(-1, 'Pos', textcell, reset_style_render_func ,self.colnum['Pos'])
338 self.view.insert_column_with_data_func(-1, 'Street 0', pixbuf, card_renderer_cell_func, self.colnum['Street0'])
339 self.view.insert_column_with_data_func(-1, 'Action 0', textcell, reset_style_render_func ,self.colnum['Action0'])
340 self.view.insert_column_with_data_func(-1, 'Street 1-4', pixbuf, card_renderer_cell_func, self.colnum['Street1-4'])
341 self.view.insert_column_with_data_func(-1, 'Action 1-4', textcell, reset_style_render_func ,self.colnum['Action1-4'])
342 self.view.insert_column_with_data_func(-1, 'Won', numcell, reset_style_render_func, self.colnum['Won'])
343 self.view.insert_column_with_data_func(-1, 'Bet', numcell, reset_style_render_func, self.colnum['Bet'])
344 self.view.insert_column_with_data_func(-1, 'Net', numcell, cash_renderer_cell_func, self.colnum['Net'])
345 self.view.insert_column_with_data_func(-1, 'Game', textcell, reset_style_render_func ,self.colnum['Game'])
347 self.liststore.set_sort_func(self.colnum['Street0'], self.sorthand)
348 self.liststore.set_sort_func(self.colnum['Pos'], self.sort_pos, self.colnum['Pos'])
349 self.liststore.set_sort_func(self.colnum['Net'], self.sort_float, self.colnum['Net'])
350 self.liststore.set_sort_func(self.colnum['Bet'], self.sort_float, self.colnum['Bet'])
351 self.view.get_column(self.colnum['Street0']).set_sort_column_id(self.colnum['Street0'])
352 self.view.get_column(self.colnum['Net']).set_sort_column_id(self.colnum['Net'])
353 self.view.get_column(self.colnum['Bet']).set_sort_column_id(self.colnum['Bet'])
354 self.view.get_column(self.colnum['Pos']).set_sort_column_id(self.colnum['Pos'])
356 #selection = self.view.get_selection()
357 #selection.set_select_function(self.select_hand, None, True) #listen on selection (single click)
358 self.view.connect('row-activated', self.row_activated) #listen to double klick
360 for handid, hand in self.hands.items():
361 hero = self.filters.getHeroes()[hand.sitename]
362 won = 0
363 if hero in hand.collectees.keys():
364 won = hand.collectees[hero]
365 bet = 0
366 if hero in hand.pot.committed.keys():
367 bet = hand.pot.committed[hero]
368 net = won - bet
369 pos = hand.get_player_position(hero)
370 gt = hand.gametype['category']
371 row = []
372 if hand.gametype['base'] == 'hold':
373 board = []
374 board.extend(hand.board['FLOP'])
375 board.extend(hand.board['TURN'])
376 board.extend(hand.board['RIVER'])
378 pre_actions = hand.get_actions_short(hero, 'PREFLOP')
379 post_actions = ''
380 if 'F' not in pre_actions: #if player hasen't folded preflop
381 post_actions = hand.get_actions_short_streets(hero, 'FLOP', 'TURN', 'RIVER')
383 row = [hand.getStakesAsString(), pos, hand.join_holecards(hero), pre_actions, ' '.join(board), post_actions, str(won), str(bet),
384 str(net), gt, handid]
386 elif hand.gametype['base'] == 'stud':
387 third = " ".join(hand.holecards['THIRD'][hero][0]) + " " + " ".join(hand.holecards['THIRD'][hero][1])
388 #ugh - fix the stud join_holecards function so we can retrieve sanely
389 later_streets= []
390 later_streets.extend(hand.holecards['FOURTH'] [hero][0])
391 later_streets.extend(hand.holecards['FIFTH'] [hero][0])
392 later_streets.extend(hand.holecards['SIXTH'] [hero][0])
393 later_streets.extend(hand.holecards['SEVENTH'][hero][0])
395 pre_actions = hand.get_actions_short(hero, 'THIRD')
396 post_actions = ''
397 if 'F' not in pre_actions:
398 post_actions = hand.get_actions_short_streets(hero, 'FOURTH', 'FIFTH', 'SIXTH', 'SEVENTH')
400 row = [hand.getStakesAsString(), pos, third, pre_actions, ' '.join(later_streets), post_actions, str(won), str(bet), str(net),
401 gt, handid]
403 elif hand.gametype['base'] == 'draw':
404 row = [hand.getStakesAsString(), pos, hand.join_holecards(hero,street='DEAL'), hand.get_actions_short(hero, 'DEAL'), None, None,
405 str(won), str(bet), str(net), gt, handid]
407 if self.is_row_in_card_filter(row):
408 self.liststore.append(row)
409 #self.viewfilter.set_visible_func(self.viewfilter_visible_cb)
410 self.handsWindow.show_all()
412 def filter_cards_cb(self, card):
413 self.refreshHands()
414 #self.viewfilter.refilter() #As the sorting doesnt work if this is used, a refresh is needed.
416 def is_row_in_card_filter(self, row):
417 """ Returns true if the cards of the given row are in the card filter """
418 #Does work but all cards that should NOT be displayed have to be clicked.
419 card_filter = self.filters.getCards()
420 hcs = row[self.colnum['Street0']].split(' ')
422 if '0x' in hcs: #if cards are unknown return True
423 return True
425 gt = row[self.colnum['Game']]
427 if gt not in ('holdem', 'omahahi', 'omahahilo'): return True
428 # Holdem: Compare the real start cards to the selected filter (ie. AhKh = AKs)
429 value1 = Card.card_map[hcs[0][0]]
430 value2 = Card.card_map[hcs[1][0]]
431 idx = Card.twoStartCards(value1, hcs[0][1], value2, hcs[1][1])
432 abbr = Card.twoStartCardString(idx)
433 return False if card_filter[abbr] == False else True
435 #def select_hand(self, selection, model, path, is_selected, userdata): #function head for single click event
436 def row_activated(self, view, path, column):
437 model = view.get_model()
438 hand = self.hands[int(model.get_value(model.get_iter(path), self.colnum['HandId']))]
439 if hand.gametype['currency']=="USD": #TODO: check if there are others ..
440 currency="$"
441 elif hand.gametype['currency']=="EUR":
442 currency="�"
443 else:
444 currency = hand.gametype['currency']
446 replayer = GuiReplayer.GuiReplayer(self.config, self.sql, self.main_window)
448 replayer.currency = currency
449 replayer.play_hand(hand)
450 return True
453 def get_vbox(self):
454 """returns the vbox of this thread"""
455 return self.mainHBox
458 def importhand(self, handid=1):
459 # Fetch hand info
460 # We need at least sitename, gametype, handid
461 # for the Hand.__init__
463 ####### Shift this section in Database.py for all to use ######
464 q = self.sql.query['get_gameinfo_from_hid']
465 q = q.replace('%s', self.sql.query['placeholder'])
467 c = self.db.get_cursor()
469 c.execute(q, (handid,))
470 res = c.fetchone()
471 gametype = {'category':res[1],'base':res[2],'type':res[3],'limitType':res[4],'hilo':res[5],'sb':res[6],'bb':res[7], 'currency':res[10]}
472 #FIXME: smallbet and bigbet are res[8] and res[9] respectively
473 ###### End section ########
474 if gametype['base'] == 'hold':
475 h = HoldemOmahaHand(config = self.config, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
476 elif gametype['base'] == 'stud':
477 h = StudHand(config = self.config, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
478 elif gametype['base'] == 'draw':
479 h = DrawHand(config = self.config, hhc = None, sitename=res[0], gametype = gametype, handText=None, builtFrom = "DB", handid=handid)
480 h.select(self.db, handid)
481 return h
484 #This code would use pango markup instead of pix for the cards and renderers
486 def refreshHands(self, handids):
487 self.hands = {}
488 for handid in handids:
489 self.hands[handid] = self.importhand(handid)
491 try:
492 self.handsWindow.destroy()
493 except:
494 pass
495 self.handsWindow = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
496 self.handsWindow.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
497 self.handsVBox.pack_end(self.handsWindow)
498 cols = [
499 str, # Street0 cards
500 str, # Street1 cards
501 str, # Street2 cards
502 str, # Street3 cards
503 str, # Street4 cards
504 str, # Net
505 str, # Gametype
506 str, # Hand Id
508 # Dict of colnames and their column idx in the model/ListStore
509 self.colnum = {
510 'Street0' : 0,
511 'Street1' : 1,
512 'Street2' : 2,
513 'Street3' : 3,
514 'Street4' : 4,
515 '+/-' : 5,
516 'Game' : 6,
517 'HID' : 7,
519 self.liststore = gtk.ListStore(*cols)
520 self.view = gtk.TreeView()
521 self.view.set_grid_lines(gtk.TREE_VIEW_GRID_LINES_BOTH)
522 self.handsWindow.add(self.view)
524 self.viewfilter = self.liststore.filter_new()
525 self.view.set_model(self.viewfilter)
526 text = gtk.CellRendererText()
528 self.view.insert_column_with_attributes(-1, 'Street 0', text, markup = self.colnum['Street0'])
529 self.view.insert_column_with_attributes(-1, 'Street 1', text, markup = self.colnum['Street1'])
530 self.view.insert_column_with_attributes(-1, 'Street 2', text, markup = self.colnum['Street2'])
531 self.view.insert_column_with_attributes(-1, 'Street 3', text, markup = self.colnum['Street3'])
532 self.view.insert_column_with_attributes(-1, 'Street 4', text, markup = self.colnum['Street4'])
533 self.view.insert_column_with_attributes(-1, '+/-', text, markup = self.colnum['+/-'])
534 self.view.insert_column_with_attributes(-1, 'Game', text, text = self.colnum['Game'])
536 self.liststore.set_sort_func(self.colnum['Street0'], self.sorthand)
537 self.liststore.set_sort_func(self.colnum['+/-'], self.sort_float)
538 self.view.get_column(self.colnum['Street0']).set_sort_column_id(self.colnum['Street0'])
539 self.view.get_column(self.colnum['+/-']).set_sort_column_id(self.colnum['+/-'])
541 selection = self.view.get_selection()
542 selection.set_select_function(self.select_hand, None, True)
544 for handid, hand in self.hands.items():
545 hero = self.filters.getHeroes()[hand.sitename]
546 won = 0
547 if hero in hand.collectees.keys():
548 won = hand.collectees[hero]
549 bet = 0
550 if hero in hand.pot.committed.keys():
551 bet = hand.pot.committed[hero]
552 net = self.get_net_pango_markup(won - bet)
554 gt = hand.gametype['category']
555 row = []
556 if hand.gametype['base'] == 'hold':
557 hole = hand.get_cards_pango_markup(hand.holecards['PREFLOP'][hero][1])
558 flop = hand.get_cards_pango_markup(hand.board["FLOP"])
559 turn = hand.get_cards_pango_markup(hand.board["TURN"])
560 river = hand.get_cards_pango_markup(hand.board["RIVER"])
561 row = [hole, flop, turn, river, None, net, gt, handid]
562 elif hand.gametype['base'] == 'stud':
563 third = hand.get_cards_pango_markup(hand.holecards['THIRD'][hero][0]) + " " + hand.get_cards_pango_markup(hand.holecards['THIRD'][hero][1])
564 #ugh - fix the stud join_holecards function so we can retrieve sanely
565 fourth = hand.get_cards_pango_markup(hand.holecards['FOURTH'] [hero][0])
566 fifth = hand.get_cards_pango_markup(hand.holecards['FIFTH'] [hero][0])
567 sixth = hand.get_cards_pango_markup(hand.holecards['SIXTH'] [hero][0])
568 seventh = hand.get_cards_pango_markup(hand.holecards['SEVENTH'][hero][0])
569 row = [third, fourth, fifth, sixth, seventh, net, gt, handid]
570 elif hand.gametype['base'] == 'draw':
571 row = [hand.get_cards_pango_markup(hand.holecards['DEAL'][hero][0]), None, None, None, None, net, gt, handid]
572 #print "DEBUG: row: %s" % row
573 self.liststore.append(row)
574 self.viewfilter.set_visible_func(self.viewfilter_visible_cb)
575 self.handsWindow.show_all()
577 def get_net_pango_markup(self, net):
578 """Pango marks up the +/- value ... putting negative values in () and coloring them red.
579 used instead of cash_renderer_cell_func because the render function renders the foreground of all columns and not just the one needed """
580 if net < 0:
581 ret = '<span foreground="red">(%s)</span>' %(net*-1)
582 else:
583 ret = str(net)
584 return ret