You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
7.4 KiB

#! /usr/bin/env python3
dev = False
profiling = False
import tkinter as tk
from multiprocessing import Process
import sys, threading, queue, time
if not dev:
import imp
imp.load_source('pacgraph', '/usr/bin/pacgraph')
import pacgraph
colors = {'sel' : '#000',
'uns' : '#888',
'dep' : '#008',
'req' : '#00F',
'bg' : '#FFF',
'line' : '#DDD',
'line2': '#777',
'dot' : '#F00',
# UI thoughts:
# hold right plays detailed history
# right drag fast moves ahead/back in history
class Motion(object):
def __init__(self):
self.mouse = None # prev canvas coord
self.scale = 1.0
self.typed = '' # keyboard search buffer
self.offset = (0,0) # unzoomed coords
self.history = [] # list of (package, [coords])
self.size = canvas.winfo_width(), canvas.winfo_height()
def button_up(self, event):
self.mouse = None
def drag(self, event):
if self.mouse is None:
self.mouse = (event.x, event.y)
mdx = event.x - self.mouse[0]
mdy = event.y - self.mouse[1]
self.mouse = (event.x, event.y)
self.moveall(mdx, mdy)
def moveall(self, dx, dy):
canvas.move(tk.ALL, dx, dy)
scaled = xy_add((dx,dy), (0,0), self.scale)
self.offset = xy_add(scaled, self.offset, 1.0)
def zoom(self, event, factor):
# buggy?
self.scale *= factor
#self.offset = xy_add(self.offset, (0,0), factor)
new_p = lambda p: str(max(1, int(p * 0.4 * self.scale)))
cx,cy = origin()
canvas.scale(tk.ALL, cx, cy, factor, factor)
for n,v in list(cant.items()):
canvas.itemconfig(, font=('Monospace', new_p(v.font_pt)))
def zoom_in(self, event):
if self.scale*1.1 > 2:
self.zoom(event, 1.1)
def zoom_out(self, event):
if self.scale*0.8 < 0.1:
self.zoom(event, 0.8)
def resize(self, event):
dx = event.width - self.size[0]
dy = event.height - self.size[1]
self.moveall(dx//2, dy//2)
self.size = event.width, event.height
def search(self, event):
# prototype, eventually zoom-to-fit typed matches
#print event.char, event.keysym, event.keycode
ks = event.keysym
c = event.char
matches = []
if ks in ['space', 'BackSpace', 'Escape', 'Delete', 'Return']:
self.typed = ''
if c.isalpha():
self.typed += c
matches = [n for n in cant if self.typed in n]
for name in list(cant):
if name in matches:
color_text(name, 'sel')
color_text(name, 'uns')
class Container(object):
def origin():
"center of the canvas"
return canvas.winfo_width()//2, canvas.winfo_height()//2
def xy_add(p1, p2, scale=1.0):
"add two and scale points"
return (p1[0]+p2[0]) * scale, (p1[1]+p2[1]) * scale
def zoom_shift(p):
"real coords -> canvas coords"
# buggy?
px,py = p
ox,oy = motion.offset
cx,cy = origin()
z = motion.scale
return z * (px - cx + ox) + cx, z * (py - cy + oy) + cy
def color_text(name, color):
canvas.itemconfig(cant[name].tk, fill=colors[color])
def hilite(event, name, selected):
loaded = set(cant)
ci = canvas.itemconfig
if not name:
[color_text(l, 'uns') for l in cant]
ci('dot', fill='', outline='')
ci('line', fill=colors['line'])
if selected:
[ci(i, fill=colors['dot'], outline=colors['dot']) for i in cant[name].dots_tk]
[ci(, fill=colors['line2']) for i in cant[name].lines_tk]
color_text(name, 'sel')
for l in cant[name].links & loaded:
color_text(l, 'dep')
for l in cant[name].inverse & loaded:
color_text(l, 'req')
color_text(name, 'uns')
for l in cant[name].all & loaded:
color_text(l, 'uns')
[ci(i, fill='', outline='') for i in cant[name].dots_tk]
[ci(, fill=colors['line']) for i in cant[name].lines_tk]
def sync_place():
"requires an existing place_iter"
global cant
frame_delay = 10 # milliseconds
hilite(None, None, False)
name, centers = next(place_iter)
except StopIteration:
# zoom is broken during the animation
# so be lame and don't enable until now
canvas.bind('<Button-4>', motion.zoom_in)
canvas.bind('<Button-5>', motion.zoom_out)
if profiling:
node = tree[name]
center = centers[-1]
motion.history.append((name, centers))
lines = [(center, cant[l].center, l) for l in set(cant) & node.all]
node.lines_tk = []
node.dots_tk = []
for line in lines:
l = Container()
n = line[2]
l.p = line[0] + line[1]
l.c = colors['line']
lp2 = zoom_shift(line[0]) + zoom_shift(line[1]) = canvas.create_line(lp2, tag='line', fill=l.c)
for c in centers:
tkc = zoom_shift(c)
d = canvas.create_oval(tkc+tkc, tags='dot', fill=colors['dot'], outline=colors['dot'])
p = node.font_pt = center
tkcenter = zoom_shift((center[0], center[1]+p//4)) = canvas.create_text(
tkcenter[0], tkcenter[1],
text=name, anchor=tk.S, fill=colors['sel'],
font=('Monospace', str(max(1, int(p*0.4*motion.scale)))))
cant[name] = node
hilite(None, name, True)
cant[name].center = center
n = name
canvas.tag_bind(cant[n].tk, '<Enter>', lambda e, n=n: hilite(e, n, True))
canvas.tag_bind(cant[n].tk, '<Leave>', lambda e, n=n: hilite(e, n, False))
canvas.after(frame_delay, sync_place)
def main():
global canvas, tree, motion, cant, place_iter
loaders = {'arch': pacgraph.Arch, 'debian': pacgraph.Debian,
'redhat': pacgraph.Redhat, 'gentoo': pacgraph.Gentoo,
'frugalware': pacgraph.Frugal}
distro = pacgraph.distro_detect2()
if not distro:
loader = loaders[distro]()
if len(sys.argv) == 1:
print('Loading local repo.')
tree = loader.local_load()
print('Loading repository.')
tree = loader.repo_load()
print('Preparing %i nodes.' % len(tree))
tree = pacgraph.pt_sizes(tree, 10, 100)
print('Hover, drag, scroll and type to control.')
print('Red dots are the path taken to find empty space.')
root = tk.Tk()
canvas = tk.Canvas(root, bg=colors['bg'])
canvas.pack(expand=1, fill=tk.BOTH)
motion = Motion()
cant = {}
canvas.bind('<B1-Motion>', motion.drag)
canvas.bind('<ButtonRelease-1>', motion.button_up)
canvas.bind('<B2-Motion>', motion.drag)
canvas.bind('<ButtonRelease-2>', motion.button_up)
canvas.bind('<B3-Motion>', motion.drag)
canvas.bind('<ButtonRelease-3>', motion.button_up)
#canvas.bind('<Button-4>', motion.zoom_in)
#canvas.bind('<Button-5>', motion.zoom_out)
canvas.bind('<Configure>', motion.resize)
place_iter =, detail=True)
canvas.after(500, sync_place)
if not profiling:
import cProfile'main()', sort=1)