#!/usr/bin/python3
#
# pyscreenruler -- a rotatable screen ruler.
#
# Derived from examples/shapewin.py coming with python3-xlib, which is
#
#    Copyright (C) 2002 Peter Liljenberg <petli@ctrl-c.liu.se>
#
# licensed under GPL 2 or later.
#
# Screen ruler code: Public Domain.
#
# python3 only

import contextlib
import math
import sys
import os

from Xlib import X, display, Xutil
from Xlib.keysymdef import latin1
from Xlib.ext import shape

@contextlib.contextmanager
def gc_on(drawable, **kwargs):
    """a context manager for a graphics context on some drawable.
    """
    gc = drawable.create_gc(**kwargs)
    try:
        yield gc
    finally:
        gc.free()


def bomb_out(*args):
    """a default implementation for the onerror handlers of xlib.
    """
    raise Exception("X error: %s"%repr(args))


class Window(object):
    def __init__(self, display):
        self.d = display
        self.r_width, self.r_height = 500, 40
        self.filled_ruler = False
        self.win_dim = self.r_width+2*self.r_height
        

        if not self.d.has_extension('SHAPE'):
            sys.exit('%s: server does not have SHAPE extension\n'
                             % sys.argv[1])
            sys.exit(1)

        self.screen = self.d.screen()

        self._compute_shapes()

        # this is where the ruler is going to live later
        self.ruler_pixmap = self.screen.root.create_pixmap(
            self.win_dim, self.win_dim,
            self.screen.root_depth)

        self.window = self.screen.root.create_window(
            0, 0, self.win_dim, self.win_dim, 0,
            self.screen.root_depth,
            X.InputOutput,
            X.CopyFromParent,
            override_redirect=True,
            event_mask = X.StructureNotifyMask | X.KeyPressMask
                | X.ExposureMask | X.ButtonMotionMask
                | X.ButtonPressMask | X.EnterWindowMask,
            colormap = X.CopyFromParent)

        # and this is where we'll paint the mask
        self.mask_pixmap = self.window.create_pixmap(
            self.win_dim, self.win_dim, 1)

        self.WM_DELETE_WINDOW = self.d.intern_atom('WM_DELETE_WINDOW')
        self.WM_PROTOCOLS = self.d.intern_atom('WM_PROTOCOLS')

        self.window.set_wm_name('Python Screen Ruler')
        self.window.set_wm_icon_name('pyscreenruler')
        self.window.set_wm_class('shapewin', 'PyScreenRuler')

        self.window.set_wm_protocols([self.WM_DELETE_WINDOW])
        self.window.set_wm_hints(flags = Xutil.StateHint,
                                 initial_state = Xutil.NormalState)

        self.window.set_wm_normal_hints(
            flags = Xutil.PPosition|Xutil.PSize|Xutil.USSize,
            width = self.win_dim,
            height = self.win_dim)

        self._update_ruler(0, False)

        self.window.shape_select_input(1)
        self.window.map()

    def _compute_shapes(self):
        """computes the un-rotated shapes of the ruler elements.
        """
        self.ruler_poly = [(-self.r_width/2, self.r_height),
            (self.r_width/2, self.r_height),
            (self.r_width/2, 0),
            (-self.r_width/2, 0)]

        self.tics = [((-self.r_width/2, 0), (self.r_width/2, 0))]
        for x in range(0, self.r_width, 20):
	        if x%100:
		        self.tics.append((
		            (-self.r_width/2+x, 0),
		            (-self.r_width/2+x, self.r_height/3)))
	        else:
		        self.tics.append((
		            (-self.r_width/2+x, 0),
		            (-self.r_width/2+x, 2*self.r_height/3)))

    def get_transform(self):
        """returns a function transforming point lists from axis-parallel to
        the current angle.

        (0,0) of this coordinate system (and hence the pivot point)
        is the center of the window.
        """
        ca = math.cos(self.angle/180*math.pi)
        sa = math.sin(self.angle/180*math.pi)
        def _(points, sa=sa, ca=ca, c=self.win_dim/2):
            res = []
            for p in points:
                res.append((
                    int(round(p[0]*ca+p[1]*sa)+c), 
                    int(-p[0]*sa+p[1]*ca+c)))
            return res
        return _

    def _draw_tics(self, drawable, gc, transform):
        """renders our current rulers on drawable through gc.

        Transform is what get_transform returns for the current angle.
        """
        for tic in self.tics:
           coords = transform(tic)
           drawable.line(gc, 
               coords[0][0], coords[0][1], 
               coords[1][0], coords[1][1], bomb_out)

    def _update_ruler(self, new_angle, on_screen=True):
        """computes new ruler and mask pixmaps, and sets
        the new angle (in degrees).
        """
        self.angle = (new_angle+180)%360-180
        t = self.get_transform()

        with gc_on(self.mask_pixmap, foreground=0, background=1) as gc:
            self.mask_pixmap.fill_rectangle(
                gc, 0, 0, self.win_dim, self.win_dim, bomb_out)
            gc.change(foreground=1)
            if self.filled_ruler:
                self.mask_pixmap.fill_poly(
                    gc, X.Convex, X.CoordModeOrigin,
                    t(self.ruler_poly), bomb_out)

            gc.change(foreground=1, line_width=8)
            self._draw_tics(self.mask_pixmap, gc, t)

        self.window.shape_mask(shape.SO.Set, shape.SK.Bounding,
             0, 0, self.mask_pixmap)

        with gc_on(self.ruler_pixmap, 
                foreground=self.screen.white_pixel) as gc:
            if self.filled_ruler:
                self.ruler_pixmap.fill_poly(
                    gc, X.Convex, X.CoordModeOrigin,
                    t(self.ruler_poly), bomb_out)

            gc.change(foreground=self.screen.white_pixel,
                line_width=8)
            self._draw_tics(self.ruler_pixmap, gc, t)
            gc.change(foreground=self.screen.black_pixel,
                line_width=2)
            self._draw_tics(self.ruler_pixmap, gc, t)

        if on_screen:
            with gc_on(self.window) as gc:
                self.window.copy_area(gc, self.ruler_pixmap, 
                    0, 0, self.win_dim, self.win_dim,
                    0, 0, bomb_out)

    def _handle_key(self, keycode, state):
        angle_delta = 0
        if keycode==latin1.XK_plus:
            angle_delta = 1
        elif keycode==latin1.XK_minus:
            angle_delta = -1
        elif keycode==latin1.XK_q:
            sys.exit(1)

        if state&1:
            angle_delta *= 10

        if angle_delta:
            self._update_ruler(self.angle+angle_delta)

    def loop(self):
        drag_start = False
        while 1:
            e = self.d.next_event()

            if e.type==X.DestroyNotify:
                sys.exit(0)

            elif e.type==X.Expose:
                with gc_on(self.window) as gc:
                    self.window.copy_area(gc, self.ruler_pixmap, 
                        e.x, e.y, e.width, e.height,
                        e.x, e.y, bomb_out)

            elif e.type==X.KeyPress:
                self._handle_key(
                    self.d.keycode_to_keysym(e.detail, 0),
                    e.state)
            
            elif e.type==X.ButtonPress:
                drag_start = True
                from_center = math.sqrt((e.event_x-self.win_dim/2)**2
                    +(e.event_y-self.win_dim/2)**2)
                drag_rotate = from_center>self.r_width/3
                    

            elif e.type==X.MotionNotify:
                if drag_start:
                    init_root = (e.root_x, e.root_y)
                    window_offset = (e.root_x-e.event_x, e.root_y-e.event_y)
                    drag_start = False
                
                if drag_rotate:
                    angle = math.atan2(
                        -e.event_y+self.win_dim/2,
                        e.event_x-self.win_dim/2)
                    self._update_ruler(angle/math.pi*180)
                else:
                    dx = init_root[0]-e.root_x
                    dy = init_root[1]-e.root_y
                    self.window.configure(
                        x=window_offset[0]-dx, 
                        y=window_offset[1]-dy)

            elif e.type==X.ButtonRelease:
                if e.detail == 1:
                    # left click
                    pass
                elif e.detail == 3:
                    # right click
                    pass

            elif e.type==X.ClientMessage:
                if e.client_type==self.WM_PROTOCOLS:
                    fmt, data = e.data
                    if fmt==32 and data[0]==self.WM_DELETE_WINDOW:
                        sys.exit(0)

            elif e.type==X.EnterNotify:
                self.window.set_input_focus(
                    X.RevertToPointerRoot, X.CurrentTime)


if __name__ == '__main__':
    w = Window(display.Display())
    w.loop()

# vim:et:sw=4:sta
