123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831 |
- #####################################################################
- # #
- # THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
- # LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
- # FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
- # WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
- # IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
- # OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
- # #
- # ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
- # (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
- # YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
- # THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
- # VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
- # WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
- # #
- # THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
- # NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
- # THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
- # SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
- # THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
- # #
- # THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
- # FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
- # THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
- # TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
- # #
- #####################################################################
- # Since LBRY wallet is just a long stream of increasing and decreasing
- # numbers. I call this the analytics.
- import os
- import math
- import json
- import time
- import random
- import colorsys
- import threading
- from gi.repository import Gtk
- from gi.repository import Gdk
- from gi.repository import GLib
- from gi.repository import cairo
- from flbry import fetch
- from flbry import settings
- def make_colors(amount=4, color=(0.1,0.5,0.8)):
- # TODO: Figure out how to got the themes prefered active color
- # and use it here.
-
- colors = []
- for i in range(amount):
- nc = colorsys.rgb_to_hsv(*color)
- nc = list(nc)
-
- if i != 0:
- nc[1] = 0
- nc[2] = (i)*(1/amount)
-
- colors.append(colorsys.hsv_to_rgb(*nc))
- return colors
- def window(win):
-
-
- awin = Gtk.Window()
- awin.set_title("FastLBRY GTK: Wallet / Analytics")
- awin.set_size_request(500, 500)
- notebook = Gtk.Notebook()
- notebook.set_scrollable(True)
- awin.add(notebook)
- ############################ ANALYTICS SECTION #############################
- box = Gtk.VBox()
- notebook.append_page(box, Gtk.Label("Totals / Analytics"))
-
- # Let's get...
- raw_balance = fetch.lbrynet("wallet_balance")
- # ... and format the balance information.
- balance = {
- "Spendable":raw_balance["available"],
- "Claim Bids":raw_balance["reserved_subtotals"]["claims"],
- "Supports":raw_balance["reserved_subtotals"]["supports"],
- "Tips":raw_balance["reserved_subtotals"]["tips"],
- }
- # Pie chart for balance
- def conv(t):
- return str(t) + " LBC"
- box.pack_start(pie_chart(win, balance, "Totals", converter=conv), 0, 0 , 0)
- minus_30_days = 60*60*24*30*-1
-
- transactions = {"items":[], # The list of items
- "zoom":[0,0], #minus_30_days,0], # The range of the selection ( by timestamp )
- "allow_negative":True
- }
-
- def getting_all_transactions(data, win):
-
- # This is a small function funnin in a separate thread, loading
- # all transactions so the graph could draw them.
- # TODO: this commented code works perfectly fine to get the kind of
- # data that I want. But it's hell'a heavy on my computer. Last time
- # I tried, it froze. So I need to figure out something about this.
- # cash_file = settings.get_settings_folder()+"GTK-transactions-graph.json"
-
- # try:
- # with open(cash_file) as json_file:
- # data["items"] = json.load(json_file)
- # except Exception as e:
- # print(e)
-
-
- # def add(out, addpage):
- # ret = False
- # added = 0
- # count = 0
- # for i in out["items"]:
- # # Removing unnesesary data
-
- # a = i.copy()
- # # a["amount"] = i["amount"]
- # # a["timestamp"] = i["timestamp"]
- # if a["timestamp"] == None: # Too recent
- # a["timestamp"] = int(time.time())
- # # a["txid"] = i["txid"]
- # # a["name"] = i.get("name", "")
- # if a not in data["items"]:
- # added += 1
- # data["items"].append(a)
- # elif not addpage:
- # count += 1
- # ret = True
- # return ret
-
- # Fetching
-
-
- # out = fetch.lbrynet("transaction_list", { "page_size":50})
- # all_of_them = out["total_items"]
- # addpage = 0
- # add(out, 0)
- # for i in range(out["total_pages"]):
- # out = fetch.lbrynet("transaction_list", {"page":addpage+i+2,
- # #"exclude_internal_transfers": True,
- # #"no_totals":True,
- # "page_size":50})
-
- # if add(out, addpage):
- # addpage = int((len(data["items"]))/50) - 2
-
- # # Save to cash
- # with open(cash_file, 'w') as fp:
- # json.dump(data["items"], fp)
- # if not win.keep_loading_the_wallet_graph or all_of_them == len(data["items"]):
- # return
- # THE MEANWHILE SOLUTION
- out = fetch.lbrynet("txo_plot", { "days_back":1000, # Fetch 100 days of txo
- "exclude_internal_transfers":True, # Without crap
- "is_not_my_input":True, # Not from me ( as in support only )
- })
-
- for i in out:
- a = {}
- a["amount"] = i["total"]
- a["timestamp"] = int(time.mktime(time.strptime(i["day"],"%Y-%m-%d")))
- data["items"].append(a)
-
-
- win.keep_loading_the_wallet_graph = True
- t = threading.Thread(target=getting_all_transactions, args=(transactions, win))
- t.setDaemon(True)
- t.start()
- def kill_graph(w):
- win.keep_loading_the_wallet_graph = False
- awin.connect("destroy", kill_graph)
-
- # Graph with the history of all transactions
- the_graph = graph(win, transactions, "Totals")
-
- box.pack_start(the_graph, 1, 1 , 0)
-
-
-
- awin.show_all()
- def box_of_color(color):
- def on_draw(widget, cr, data):
-
- width = widget.get_allocated_width()
- height = widget.get_allocated_height()
-
- cr.set_source_rgb(*color)
- cr.rectangle(0, 0, width, height)
- cr.fill()
-
- area = Gtk.DrawingArea()
- area.set_size_request(40, 40)
- area.connect("draw", on_draw, color)
- return area
-
- def pie_chart_draw(d, main_layer, win, data, colors):
-
- # Need to know how big is our chart
- w = d.get_allocated_width()
- h = d.get_allocated_height()
- # We want our circle to fit, so we find which one is
- # smaller.
- smaller = min(w, h)
-
- last = 0
- whole = float(math.pi*2)
- sum_of_data = 0
-
- for i in data:
- sum_of_data += float(data[i])
-
- for n, i in enumerate(data):
-
- this = whole* ( float(data[i]) / sum_of_data )
-
- main_layer.move_to(w/2, h/2)
- main_layer.arc(w/2, h/2, smaller/2, last, last+this)
- main_layer.close_path()
- main_layer.set_source_rgb(*colors[n%len(colors)])
- main_layer.fill()
-
- last = last+this
- def pie_chart(win, data, title="", converter=False):
- ret = Gtk.HBox(True)
- colors = make_colors(len(data))
-
- da = Gtk.DrawingArea()
- da.connect("draw", pie_chart_draw, win, data, colors)
- ret.pack_start(da, 1,1,5)
- lbox = Gtk.VBox()
- ret.pack_start(lbox, 1,1,5)
- sum_of_data = 0
-
- for i in data:
- sum_of_data += float(data[i])
- if converter:
- sum_of_data = converter(sum_of_data)
-
- lbox.pack_start(Gtk.Label(" Total : "+str(sum_of_data)+" "), 0,0,1)
-
- for n, i in enumerate(data):
- ibox = Gtk.HBox()
- lbox.pack_start(ibox, 0,0,3)
- ibox.pack_start(box_of_color(colors[n%len(colors)]), 0,0,0)
- show_size = data[i]
- if converter:
- show_size = converter(show_size)
- ibox.pack_start(Gtk.Label(" "+i+": "+str(show_size)+" "), 0,0,1)
-
-
-
- return ret
- def graph_draw(d, main_layer, win, data, currancy, graph_addition):
-
- data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
-
- # Need to know how big is our graph
- w = d.get_allocated_width()
- h = d.get_allocated_height()
- if data.get("allow_negative", False):
- zero_at = h / 2
- else:
- zero_at = h
- # The mouse position of a given frame
- mx = d.get_pointer()[0]
- my = d.get_pointer()[1]
- # Test of the mouse position
- # main_layer.move_to(0,0)
- # main_layer.line_to(mx, my)
- # main_layer.stroke()
-
- len_day = 60*60*24 # Seconds in a day
- len_hour = 60*60 # Seconds in an hour
- len_minute = 60 # Seconds in a minute
-
- # Here we are getting the latest and the earliest
- # timestamp, so we could calculate the step of the
- # graph. ( So the data will be readable )
- latest = 0
- try:
- earliest = data["items"][0]["timestamp"]
- except:
- earliest = 0
- for i in data["items"]:
- if i.get("timestamp", 0) > latest:
- latest = i.get("timestamp", 0)
- if i.get("timestamp", 0) < earliest:
- earliest = i.get("timestamp", 0)
- # Now let's look at our zoom value
- to_zoom = data["zoom"][0] != earliest or data["zoom"][1] != latest
- if data["zoom"][0] < earliest or data["zoom"][0] == 0:
- data["zoom"][0] = earliest
- if data["zoom"][1] > latest or data["zoom"][1] == 0:
- data["zoom"][1] = latest
-
- earliest, latest = data["zoom"]
- # Now I want to make a scale of dates from left
- # to right.
- main_layer.select_font_face("Monospace")
- main_layer.set_font_size(10)
-
- full_date = "%Y-%m-%d %H:%M:%S"
- only_date = "%Y-%m-%d"
- if latest - earliest > 10 * len_day:
- show_format = only_date
- count = int( w / (len("xxxx-xx-xx")*6+12) )
- else:
- show_format = full_date
- count = int( w / (len("xxxx-xx-xx xx:xx:xx")*6+12) )
- # Now I want to show the current date / time for
- # the area where the user is hovering.
- suglen = len("xxxx-xx-xx xx:xx:xx")*6+12
-
- thexm = mx-suglen/2
- if thexm < 2:
- thexm = 2
- elif thexm > w - suglen - 2:
- thexm = w - suglen - 2
- try:
- res_date = int( ( latest - earliest ) / w * mx + earliest )
- show_date = time.strftime(full_date, time.gmtime(res_date))
- except:
- show_date = "0000-00-00"
-
- main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
- main_layer.rectangle(2+thexm,2,len(show_date)*6+2, 14)
- main_layer.fill()
- main_layer.move_to(3+thexm,12)
- main_layer.set_source_rgba(1,1,1,1)
- main_layer.show_text(str(show_date))
-
- # main_layer.set_source_rgba(0.7,0.7,0.7,1)
- # main_layer.move_to( mx, 20 )
- # main_layer.line_to( mx, h )
- # main_layer.stroke()
- # main_layer.set_dash([10,10])
- # main_layer.set_source_rgba(0.2,0.2,0.2,1)
- # main_layer.move_to( mx, 20 )
- # main_layer.line_to( mx, h )
- # main_layer.stroke()
- # main_layer.set_dash([1])
- # And the rest of the dates
-
- for date in range(count):
- try:
- res_date = int( ( latest - earliest ) / count * date + earliest )
- show_date = time.strftime(show_format, time.gmtime(res_date))
- except:
- show_date = "0000-00-00"
- thex = w / count * date
-
- # If not in range of the mouse ( so I could show the current day
- # for that specific area ).
- if int(thex) not in range(int(thexm-suglen/2), int(thexm+suglen)):
-
- main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
- main_layer.rectangle(2+thex,2,len(show_date)*6+2, 14)
- main_layer.fill()
- main_layer.move_to(3+thex,12)
- main_layer.set_source_rgba(1,1,1,1)
- main_layer.show_text(str(show_date))
-
-
- # A step is how often will there be a data point
- # of the graph. Step of one minute, means every
- # point on the graph will consist all the data
- # happened in this minute.
-
-
- step = (latest - earliest) / (w / 2) # A second
-
-
- # Now we need the smallest and biggest value in a
- # given step
- values = []
- times = []
-
- pstep = earliest
- s = 0
- av = []
-
- for n, i in enumerate(data["items"]):
- if i.get("timestamp", 0) < earliest:
- continue
- if graph_addition == "add":
- s += float(i.get("amount", i.get("value", 0)))
- elif graph_addition == "average":
- av.append( float(i.get("amount", i.get("value", 0))) )
- elif graph_addition == "last":
- s = float(i.get("amount", i.get("value", 0)))
- if i.get("timestamp", 0) > pstep + step-1:
- pstep = i.get("timestamp", n)
-
- if graph_addition == "average":
- try:
- values.append(sum(av)/len(av))
- except:
- values.append(0)
- else:
- values.append(s)
- times.append(pstep)
- s = 0
- av = []
- if i.get("timestamp", 0) > latest:
- break
-
-
-
- # Finding the farthest point from the center
- # center being the 0 (zero)
- try:
- biggest = max(values)
- if min(values) * -1 > biggest:
- biggest = min(values) * -1 # Multuply by -1 reverses the - to a +
- except Exception as e:
- biggest = 1
-
-
- # Now let's draw it
- main_layer.set_line_cap(cairo.LineCap.ROUND)
-
- # POSITIVE VALUE
- try:
- toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
- except:
- toy = zero_at
- #toy = min(toy, zero_at)
- main_layer.rectangle(0,0,w,zero_at)
- main_layer.clip()
-
- main_layer.move_to(0, toy)
-
- prex = 0
- prey = toy
- toxes = []
- toyes = []
-
- for n, i in enumerate(values):
-
- tox = w / (latest - earliest) * (times[n]-earliest)
- try:
- toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
- except:
- toy = zero_at
- toxes.append(tox)
- toyes.append(toy)
-
- #toy = min(toy, zero_at)
-
-
- main_layer.curve_to(
- tox - (tox - prex)/2,
- prey,
-
- prex + (tox - prex)/2,
- toy,
- tox,
- toy)
- prex = tox
- prey = toy
- main_layer.line_to( w, zero_at)
- main_layer.line_to( 0, zero_at)
- main_layer.set_source_rgba(0.2,0.8,0.2,0.5)
- main_layer.fill_preserve()
- main_layer.set_source_rgba(0.2,0.8,0.2,1)
- main_layer.stroke()
- # NEGATIVE VALUE
- try:
- toy = ( zero_at ) - ( ( zero_at ) / biggest * values[0] ) *0.9
- except:
- toy = zero_at
- #toy = max(toy, zero_at)
-
- main_layer.reset_clip()
- main_layer.rectangle(0,zero_at,w,h)
- main_layer.clip()
-
- main_layer.move_to(0, toy)
- prex = 0
- prey = toy
-
- for n, i in enumerate(values):
-
- tox = w / (latest - earliest) * (times[n]-earliest)
- try:
- toy = ( zero_at ) - ( ( zero_at ) / biggest * i ) *0.9
- except:
- toy = zero_at
- #toy = max(toy, zero_at)
-
- main_layer.curve_to(
-
- tox - (tox - prex)/2,
- prey,
-
- prex + (tox - prex)/2,
- toy,
-
- tox,
- toy)
- prex = tox
- prey = toy
- main_layer.line_to( w, zero_at)
- main_layer.line_to( 0, zero_at)
- main_layer.set_source_rgba(0.8,0.2,0.2,0.5)
- main_layer.fill_preserve()
- main_layer.set_source_rgba(0.8,0.2,0.2,1)
- main_layer.stroke()
- main_layer.reset_clip()
-
-
- # Reference line
- main_layer.set_source_rgba(0.7,0.7,0.7,1)
- main_layer.move_to( 0, zero_at )
- main_layer.line_to( w, zero_at )
- main_layer.stroke()
- main_layer.set_dash([10,10])
- main_layer.set_source_rgba(0.2,0.2,0.2,1)
- main_layer.move_to( 0, zero_at )
- main_layer.line_to( w, zero_at )
- main_layer.stroke()
- main_layer.set_dash([1])
- # MOUSE OVER SELECTOR
- def closest(l, v):
- distances = []
- for i in l:
- distances.append(max(i-v, v-i))
- try:
- return l[distances.index(min(distances))]
- except:
- return 0
- selectx = closest(toxes, mx)
- if selectx:
- selecty = toyes[toxes.index(selectx)]
- # Litte circle
-
- main_layer.arc(selectx, selecty, 8, 0, math.pi*2)
-
- main_layer.set_source_rgba(0.2,0.8,0.2,1)
- if selecty > zero_at:
- main_layer.set_source_rgba(0.8,0.2,0.2,1)
- main_layer.fill()
- # Line from that circle downwards
-
- main_layer.move_to(selectx, selecty)
- main_layer.line_to(selectx, zero_at)
- main_layer.stroke()
- # Data about this time frame
- to_data = times[toxes.index(selectx)]
- from_data = to_data - step
- try:
- from_data = time.strftime(show_format, time.gmtime(from_data))
- except:
- from_data = "0000-00-00"
- try:
- to_data = time.strftime(show_format, time.gmtime(to_data))
- except:
- to_data = "0000-00-00"
-
-
- # Counting the largest thing
- plist = ["From: "+from_data,
- "To: "+to_data,
- "Total: "+currancy+" "+str(round(values[toxes.index(selectx)], 2)) ]
-
- leng = 0
- for thing in plist:
- if len(str(thing))*6+2 > leng:
- leng = len(str(thing))*6+2
- if selectx > w/2:
- recx = selectx - leng - 10
- else:
- recx = selectx + 10
- if selecty + len(plist)*15 > h:
- recy = selecty - len(plist)*15
- else:
- recy = selecty
-
- main_layer.set_source_rgba(0.1,0.1,0.1,0.7)
- main_layer.rectangle(recx, recy, leng, len(plist)*15)
- main_layer.fill()
- for n, thing in enumerate(plist):
- main_layer.move_to(recx+2, recy+12+(15*n))
- main_layer.set_source_rgba(1,1,1,1)
- main_layer.show_text(thing)
-
- # Now let's get the values ( to the side of the graph )
- for i in range(int(h/20)):
- # TODO: This has to be tuned a bit. It's not perfect. But it's
- # very close.
-
- they = i*20+20
- try:
- value_is = round( biggest / zero_at * (zero_at - they), 2)
- except Exception as e:
- print("what", e)
- value_is = 0
-
- show_value = currancy + " " + str(value_is)
- if mx > w / 2:
- main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
- main_layer.rectangle(2, 2+they,len(show_value)*6+4, 14)
- main_layer.fill()
- main_layer.move_to(3,12+they)
- main_layer.set_source_rgba(1,1,1,1)
- main_layer.show_text(show_value)
- else:
- main_layer.set_source_rgba(0.1,0.1,0.1,0.5)
- main_layer.rectangle(w-len(show_value)*6-4, 2+they,len(show_value)*6+4, 14)
- main_layer.fill()
- main_layer.move_to(w-len(show_value)*6-3,12+they)
- main_layer.set_source_rgba(1,1,1,1)
- main_layer.show_text(show_value)
- # Render a little pressed selector
- if "pressed" in data:
- for i in [data["pressed"], mx]:
- main_layer.set_source_rgba(0.7,0.7,0.7,1)
- main_layer.move_to( i, 0 )
- main_layer.line_to( i, h )
- main_layer.stroke()
- main_layer.set_dash([10,10])
- main_layer.set_source_rgba(0.2,0.2,0.2,1)
- main_layer.move_to( i, 0 )
- main_layer.line_to( i, h )
- main_layer.stroke()
- main_layer.set_dash([1])
-
- # Keep redrawing the graph
- d.queue_draw()
-
- def graph_button_press(w, e, data, da):
- data["pressed"] = e.x
-
- print(data["zoom"])
- print(e.x, e.y)
- def graph_button_release(w, e, data, da):
- if "pressed" in data:
- x = data["pressed"]
- # If there was no motion
- if x-2 < e.x < x+2:
- data["zoom"] = [0,0]
- else:
- w = da.get_allocated_width()
- zoom0 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * min(x, e.x))
- zoom1 = data["zoom"][0] + ((data["zoom"][1] - data["zoom"][0]) / w * max(x, e.x))
- data["zoom"] = [zoom0, zoom1]
- print(data["zoom"])
- del data["pressed"]
- def graph(win, data, title="", currancy="$", add_now=True, add_value="Same", graph_addition="add"):
- # adding one more data point for "now"
- if add_now:
- try:
- data["items"] = sorted(data["items"], key=lambda k: k["timestamp"])
- last = data["items"][-1].copy()
- last["timestamp"] = int(time.time())
- if not add_value == "Same":
- last["amount"] = add_value
- data["items"].append(last)
- except:
- pass
-
- event_box = Gtk.EventBox()
- da = Gtk.DrawingArea()
- da.set_size_request(100,100)
- da.connect("draw", graph_draw, win, data, currancy, graph_addition)
- event_box.connect("button-press-event", graph_button_press, data, da)
- event_box.connect("button-release-event", graph_button_release, data, da)
- event_box.add(da)
- return event_box
|