#! /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) return 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(v.tk, font=('Monospace', new_p(v.font_pt))) def zoom_in(self, event): if self.scale*1.1 > 2: return self.zoom(event, 1.1) def zoom_out(self, event): if self.scale*0.8 < 0.1: return 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') else: color_text(name, 'uns') class Container(object): pass 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']) return if selected: [ci(i, fill=colors['dot'], outline=colors['dot']) for i in cant[name].dots_tk] [ci(i.tk, 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') else: 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(i.tk, 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) try: name, centers = next(place_iter) except StopIteration: # zoom is broken during the animation # so be lame and don't enable until now canvas.bind('', motion.zoom_in) canvas.bind('', motion.zoom_out) if profiling: sys.exit() return 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]) l.tk = canvas.create_line(lp2, tag='line', fill=l.c) canvas.tag_lower(l.tk) node.lines_tk.append(l) cant[n].lines_tk.append(l) for c in centers: tkc = zoom_shift(c) d = canvas.create_oval(tkc+tkc, tags='dot', fill=colors['dot'], outline=colors['dot']) node.dots_tk.append(d) p = node.font_pt node.center = center tkcenter = zoom_shift((center[0], center[1]+p//4)) node.tk = 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, '', lambda e, n=n: hilite(e, n, True)) canvas.tag_bind(cant[n].tk, '', 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: pacgraph.distro_detect() loader = loaders[distro]() if len(sys.argv) == 1: print('Loading local repo.') tree = loader.local_load() else: 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) canvas.tk_focusFollowsMouse() motion = Motion() cant = {} canvas.bind('', motion.drag) canvas.bind('', motion.button_up) canvas.bind('', motion.drag) canvas.bind('', motion.button_up) canvas.bind('', motion.drag) canvas.bind('', motion.button_up) #canvas.bind('', motion.zoom_in) #canvas.bind('', motion.zoom_out) canvas.bind('', motion.search) canvas.bind('', motion.resize) place_iter = pacgraph.place(tree, detail=True) canvas.after(500, sync_place) root.mainloop() if not profiling: main() else: import cProfile cProfile.run('main()', sort=1)