# $Id: macros.py,v 1.26 2003/12/26 02:52:04 pervago Exp $

import kahakai
from events import *
from events import _modifiers
from actions import *
from actions import _processFunction, init, finish

###### directional focusing

def directionalFocus(currentWindow, event, action):
    _directionalSearchAndFocus(currentWindow, action.param)

directionalFocusOnlyVisibleToggle = True

def _directionalSearchAndFocus(currentWindow, direction):
    # thanks to fvwm and tore for this code

    if currentWindow.id == screen().id: return #root window

    bestScore = -1
    bestClient = None

    diagonals   = ( "UpRight", "UpLeft", "DownRight", "DownLeft" )
    verticals   = ( "Up",      "Down",   "UpRight",   "DownLeft" )
    horizontals = ( "Right",   "Left",   "DownRight", "UpLeft"   )

    if not direction in verticals + horizontals + diagonals:
        raise ValueError, "%s not valid direction" % direction

    # c is for center
    my_cx = currentWindow.GetFrameLeft() + (currentWindow.GetFrameWidth() / 2)
    my_cy = currentWindow.GetFrameTop() + (currentWindow.GetFrameHeight() / 2)

    for win in screen().kawindow_list:
        if win.id == currentWindow.id: continue # we want a *different* window
        # don't display windows not on screen
        if directionalFocusOnlyVisibleToggle and not win.IsVisible(): continue
        if not win.OnCurrentDesktop(): continue
        if not win.flags.tasklist: continue # skip windows in the dock, etc

        # center of $win, relative to current window's center
        his_cx = (win.GetFrameLeft() - my_cx) + (win.GetFrameWidth() / 2)
        his_cy = (win.GetFrameTop() - my_cy) + (win.GetFrameHeight() / 2)

        if direction in diagonals:
            # rotate 45 degrees
            tx = his_cx + his_cy
            his_cy = -his_cx + his_cy
            his_cx = tx

        if direction in verticals:
            offset = abs(his_cx)
            distance = direction.find("Up") > -1 and -his_cy or his_cy
        elif direction in horizontals:
            offset = abs(his_cy)
            distance = direction.find("Left") > -1 and -his_cx or his_cx

        if distance <= 0: continue # target must be in the requested direction

        # Calculate score for this window.  The smaller the better.
        score = distance + offset

        # windows more than 45 degrees off the direction are heavily penalized
        # and will only be chosen if nothing else is within a million pixels
        if offset > distance: score += 1000000

        if bestScore == -1 or score < bestScore:
            bestClient = win
            bestScore = score

    if bestClient: bestClient.FocusVis()

###### end directional focusing

###### directional snapping/growing/shrinking

def directionalSnapToEdge(currentWindow, event, action):
    if currentWindow.id == screen().id: return #root window

    direction = action.param
    edgeFound = directionalEdgeSearch(currentWindow, direction)

    if direction == "Up": currentWindow.SetFrameTop(edgeFound)
    elif direction == "Down": currentWindow.SetFrameBottom(edgeFound)
    elif direction == "Left": currentWindow.SetFrameLeft(edgeFound)
    elif direction == "Right": currentWindow.SetFrameRight(edgeFound)

    currentWindow.RedrawWindow()


def directionalResizeToEdge(currentWindow, event, action):
    if currentWindow.id == screen().id: return #root window

    direction = action.param

    directions = ( "Up", "Down", "Left", "Right" )
    horizontals = ( "Left", "Right" )
    verticals = ( "Up", "Down" )

    if not direction in directions:
        raise ValueError, "`%s' not a valid direction" % direction

    screenEdges = {
        "Up": 0, "Down": screen().height, "Left": 0, "Right": screen().width }

    windowEdges = {
        "Up": currentWindow.GetFrameTop(),
        "Down": currentWindow.GetFrameBottom(),
        "Left": currentWindow.GetFrameLeft(),
        "Right": currentWindow.GetFrameRight() }

    windowEdge = windowEdges[direction]
    screenEdge = screenEdges[direction]

    # if we're not to the edge yet, grow
    if windowEdge != screenEdge:
        destination = directionalEdgeSearch(currentWindow, direction)

        if direction in horizontals:
            oldWidth = currentWindow.GetFrameWidth()
        elif direction in verticals:
            oldHeight = currentWindow.GetFrameHeight()

        if direction == "Up":
            currentWindow.SetFrameHeight(
                oldHeight + currentWindow.GetFrameTop() - destination)
            currentWindow.SetFrameTop(destination)

        elif direction == "Down":
            currentWindow.SetFrameHeight(
                oldHeight + destination - currentWindow.GetFrameBottom())
            currentWindow.SetFrameBottom(destination)

        elif direction == "Left":
            currentWindow.SetFrameWidth(
                oldWidth + currentWindow.GetFrameLeft() - destination)
            currentWindow.SetFrameLeft(destination)

        elif direction == "Right":
            currentWindow.SetFrameWidth(
                oldWidth + destination - currentWindow.GetFrameRight())
            currentWindow.SetFrameRight(destination)

    # we are at the edge already, so shrink
    else:

        if direction == "Up":
            destination = currentWindow.GetFrameHeight() / 2
            currentWindow.SetFrameHeight(destination)
            currentWindow.SetFrameBottom(destination)
        elif direction == "Down":
            destination = screen().height - (currentWindow.GetFrameHeight() /2)
            currentWindow.SetFrameHeight(currentWindow.GetFrameHeight() / 2)
            currentWindow.SetFrameTop(destination)
        elif direction == "Left":
            destination = currentWindow.GetFrameWidth() / 2
            currentWindow.SetFrameWidth(destination)
            currentWindow.SetFrameRight(destination)
        elif direction == "Right":
            destination = screen().width - (currentWindow.GetFrameWidth() / 2)
            currentWindow.SetFrameWidth(currentWindow.GetFrameWidth() / 2)
            currentWindow.SetFrameLeft(destination)

    currentWindow.RedrawWindow()



def directionalEdgeSearch(currentWindow, direction):
    sides = { "Up":   ("Left", "Right"), "Down":  ("Left", "Right"),
              "Left": ("Top", "Bottom"), "Right": ("Top", "Bottom") }

    edges = { "Up": "Top", "Down": "Bottom", "Left": "Left", "Right": "Right" }
    theirEdges = { "Up":"Bottom", "Down":"Top", "Left":"Right", "Right":"Left" }
    defaults = { "Up":0, "Down":screen().height, "Left":0, "Right":screen().width }

    mySides = [ getattr(currentWindow, "GetFrame"+side)()
                    for side in sides[direction] ]

    myEdge = getattr(currentWindow, "GetFrame"+edges[direction])()

    closestEdge = defaults[direction]

    for win in screen().kawindow_list:
        hisSides = [
            getattr(win, "GetFrame"+side)() for side in sides[direction] ]
        hisEdge = getattr(win, "GetFrame"+theirEdges[direction])()

        if direction in [ "Up", "Left" ]:
            if myEdge < hisEdge + 1 or closestEdge > hisEdge: continue
        elif direction in [ "Down", "Right" ]:
            if myEdge > hisEdge - 1 or closestEdge < hisEdge: continue

        if not (hisSides[1] < mySides[0] or hisSides[0] > mySides[1]):
            closestEdge = hisEdge

    return closestEdge

###### end

### helper macros


_Focus = kahakai.WindowEvFocus()
_Raise = kahakai.WindowEvRaise()
_Lower = kahakai.WindowEvLower()
_MenuFocus = kahakai.MenuEvFocus()
_MenuRaise = kahakai.MenuEvRaise()
_MenuLower = kahakai.MenuEvLower()
_WindowMenuRemapFocused = kahakai.WindowEvMenuRemapFocused()
_WindowMenuUnmap = kahakai.WindowEvMenuUnmap()
_ScreenMenuRemapFocused = kahakai.ScreenEvMenuRemapFocused()
_ScreenMenuUnmap = kahakai.ScreenEvMenuUnmap()
_Move = kahakai.WindowEvMove()
_MoveOpaque = kahakai.WindowEvMoveOpaque()
_SmartResize = kahakai.WindowEvSmartResize()
_SmartResizeOpaque = kahakai.WindowEvSmartResizeOpaque()
_EndMoveResize = _processFunction("EndMoveResize", "window")
_MenuEndMoveResize = _processFunction("EndMoveResize", "menu")

_NextItem = kahakai.MenuEvNextItem()
_UnmapTree = kahakai.MenuEvUnmapTree()
_UnmapSubmenus = kahakai.MenuEvUnmapSubmenus()
_RemapSubmenuFocused = kahakai.MenuEvRemapSubmenuFocused()
_Exec = kahakai.MenuEvExec()
_Func = kahakai.MenuEvFunc()


def clickFocus(passthru=True):
    allWindowActions( [ (ButtonPress('Button1', replay=passthru), _Focus) ] )

def sloppyFocus(delay=0):
    windowActions('window.frame',
                  [ (EnterNotify(delay=delay), _Focus) ])

def mouseFocus():
    sloppyFocus()
    screenActions( [ (EnterNotify(), _Focus) ] )

def focusNewWindows():
    windowActions('window.passiveclient', [ (MapRequest(), _Focus) ])

def wheelStacking(raiseButton=5, lowerButton=4, modifier=Alt):
    raiseButton = "Button%d" % raiseButton
    lowerButton = "Button%d" % lowerButton

    allWindowActions([ (ButtonPress(lowerButton), _Lower),
                       (ButtonPress(raiseButton), _Raise) ], ["title","label"])

    allWindowActions([ (ButtonPress(modifier, lowerButton), _Lower),
                       (ButtonPress(modifier, raiseButton), _Raise) ])

def clickToRaise(passthru=True):
    allWindowActions([ (ButtonPress('Button1', replay=passthru), _Raise) ])

def mouseRaise(delay=0):
    """
    Raises window when mouse passes over them.
    """
    windowActions('window.frame', [ (EnterNotify(delay=delay), _Raise) ])

_edgeActions = {
    'n': 'MoveViewportUp',
    's': 'MoveViewportDown',
    'e': 'MoveViewportRight',
    'w': 'MoveViewportLeft'
}

def viewportSurf(modifier=-Ctrl+Alt, wrap=False):
    """
    Installs MoveViewport{Up,Down,Left,Right} actions for edges, activated by
    the specified modifier(s).  This also allows you to drag windows around if
    you're holding your appropriate modifier(s) while doing so.
    """
    for edge, action in _edgeActions.items():
        if wrap: action = action.replace("Viewport", "ViewportWrap")
        edgeActions([(EnterNotify(modifier), action)], edge)

def viewportKnock(button=1, modifier=-Ctrl-Alt, wrap=False):
    """
    Allows you to "knock" on screen edges with a mouse button to flip
    viewports.
    """

    for edge, action in _edgeActions.items():
        if wrap: action = action.replace("Viewport", "ViewportWrap")
        edgeActions([(ButtonPress(modifier, "Button%d"%button), action)], edge)

def windowSnapping(pixels=15):
    """
    Enables window snapping with specified snap distance.
    """
    screen().config.snapping = True
    screen().config.snapDistance = pixels


def desktopKeys(modifier=Ctrl+Alt-AltGr, keys="%d"):
    """
    Keys for switching desktops.  Parameter "modifier" should be the modifiers you
    wish to use, keys should either be a string containing a %d so that the
    desktop number will be filled in for each (for e.g. F1,F2,etc), or a string
    containing a list of keys seperated by commas that will switch to each
    respective viewport.  This string-list of keys should have the same or more
    keys than the number of desktops.
    """
    numDesktops = len(screen().desktop_list)

    if keys.find("%d") != -1:
        ourKeys = [ keys % (int(n)+1) for n in range(numDesktops) ]

    elif "," in keys:
        ourKeys = keys.split(",")
        
        if len(ourKeys) < numDesktops:
            warn(("desktopKeys(): only %d keys defined, " +
                  "%d needed using defaults.") % (len(ourKeys), numDesktops))
            
            ourKeys = map(str, range(numDesktops))

    GoToDesktop = lambda i: _processFunction(("GoToDesktop", str(i)), "screen")

    actions = [ (KeyPress(modifier, ourKeys[i]), GoToDesktop(i))
                for i in range(numDesktops) ]

    globalActions(actions)

def altTab(action="TaskSwitcher"):
    """
    Installs an action for Alt+Tab.  Default is TaskSwitcher, you can change
    this by passing an argument of a different action.
    """
    globalActions([ (KeyPress(-Ctrl-AltGr+Alt, "Tab"), action) ])

_b = lambda num: ButtonPress(-Ctrl-Alt, "Button%d"%num)

def rootMenuClick(buttons):
    for button in buttons:
        screenActions([ (_b(button), (_ScreenMenuRemapFocused, "rootmenu")) ])

def rootMenuUnmap(buttons):
    for button in buttons:
        screenActions([ (_b(button), (_ScreenMenuUnmap, "rootmenu")) ])

def windowMenuClick(buttons):
    for button in buttons:
        allWindowActions([ (_b(button), (_WindowMenuRemapFocused, "window")) ],
                         ["label", "title", "handle", "leftgrip", "rightgrip"])

def windowMenuKeyPress(modifier, key):
    allWindowActions([ (KeyPress(modifier, key),
                        (_WindowMenuRemapFocused, "window")) ])

def windowMenuUnmap(buttons):
    for button in buttons:
        allWindowActions([ (_b(button), (_WindowMenuUnmap, "window")) ],
                         ["label", "title", "handle", "leftgrip", "rightgrip"])


def desktopWheelScroll(upaction="PreviousDesktop", downaction="NextDesktop", modifier=None):
    if modifier:
        up = (modifier, "Button4")
        down = (modifier, "Button5")
    else:
        up = ("Button4",)
        down = ("Button5",)

    screenActions( [(ButtonPress(*up), upaction),
                    (ButtonPress(*down), downaction)] )

def smartWindowPlacement():
    allWindowActions([ (MapRequest(), 'MoveWindowToSmartPlace') ])

def moveWindowWithModifier(modifier=Alt, opaque=True, button=1):
    if opaque:
        move = _MoveOpaque
    else:
        move = _Move
    allWindowActions(
      [ (ButtonPress(modifier, "Button%d"%button), move),
        (ButtonRelease(MoveResizeMask, "Button%d"%button), _EndMoveResize) ])

def resizeWindowWithModifier(modifier=Alt, opaque=False, button=3, smart=True):
    if opaque:
        if smart: resize = _SmartResizeOpaque
        else: resize = "ResizeRightOpaque"
    else:
        if smart: resize = _SmartResize
        else: resize = "ResizeRight"

    allWindowActions( [ (ButtonPress(modifier, "Button%d"%button), resize),
        (ButtonRelease(MoveResizeMask, "Button%d"%button), _EndMoveResize) ])

def lowerWindowWithModifier(modifier=Alt, button=2):
    allWindowActions([ (ButtonPress(modifier, "Button%d"%button), _Lower) ])

def moveByTitlebar(buttons=[1], opaque=True):
    if opaque:
        move = 'MoveOpaque'
    else:
        move = 'Move'
    for button in buttons:
        allWindowActions(
          [ (_b(button), move),
            (ButtonRelease(MoveResizeMask,'Button%d'%button),_EndMoveResize) ],
          ["label", "title", "handle"] )

def doubleClickTitlebarShade(buttons=[1]):
    doubleClickTitlebarAction(buttons, -Ctrl-Alt, "ToggleShade")

def doubleClickTitlebarMaximize(buttons=[1]):
    doubleClickTitlebarAction(buttons, -Ctrl-Alt, "ToggleMaximize")

def doubleClickTitlebarAction(buttons, modifier, action):
    for button in buttons:
        allWindowActions([ (DoubleClick(modifier, "Button%d"%button),
                            action) ], ["label", "title"] )

def resizeWithGrips(buttons=[1], opaque=False):
    if opaque:
        left = "ResizeLeftOpaque"
        right = "ResizeRightOpaque"
    else:
        left = "ResizeLeft"
        right = "ResizeRight"
    for button in buttons:
        windowActions("window.leftgrip",  [ (_b(button), left ) ] )
        windowActions("window.rightgrip", [ (_b(button), right) ] )

def shadeButton(buttons=[1]):
    for button in buttons:
        windowActions("window.button_shade",
            [ (ButtonRelease(-Ctrl-Alt, 'Button%d'%button), 'ToggleShade') ] )
                  
def minimizeButton(buttons=[1]):
    for button in buttons:
        windowActions("window.button_minimize",
            [ (ButtonRelease(-Ctrl-Alt, 'Button%d'%button), 'ToggleMinimize')] )

def closeButton(buttons=[1]):
    for button in buttons:
        windowActions("window.button_close",
            [ (ButtonRelease(-Ctrl-Alt, 'Button%d'%button), 'Close') ] )

def maximizeButton(both="Button1", horiz="Button3", vert="Button2"):
    windowActions("window.button_maximize",
        [ (ButtonRelease(-Ctrl-Alt, both ), 'ToggleMaximize'), 
          (ButtonRelease(-Ctrl-Alt, vert ), 'ToggleMaximizeVertically'),
          (ButtonRelease(-Ctrl-Alt, horiz), 'ToggleMaximizeHorizontally') ] )


def vMaximizeButton(buttons=[1]):
    for button in buttons:
        windowActions("window.button_vmaximize",
            [ (ButtonRelease(-Ctrl-Alt, "Button%d"%button), 'ToggleMaximizeVertically') ] )

def hMaximizeButton(buttons=[1]):
    for button in buttons:
        windowActions("window.button_hmaximize",
            [ (ButtonRelease(-Ctrl-Alt, "Button%d"%button), 'ToggleMaximizeHorizontally') ] )

def windowButtons():
    shadeButton()
    minimizeButton()
    closeButton()
    maximizeButton()
    hMaximizeButton()
    vMaximizeButton()

def keyboardMenuNavigation(closeOnExec=True):

    allMenuActions([ (KeyPress('Tab'),    _NextItem),
                     (KeyPress('Down'),   _NextItem),
                     (KeyPress('Up'),     'PreviousItem'),
                     (KeyPress('Left'),   _UnmapSubmenus),
                     (KeyPress('Escape'), _UnmapTree),
                     (KeyPress('Return'), _Exec),
                     (KeyPress('Return'), _Func) ],
                   ["item", "sub", "checkbox"])

    if closeOnExec:
        allMenuActions([ (KeyPress('Return'), _UnmapTree) ],
                       ["item", "sub", "checkbox"])

    allMenuActions([ (KeyPress('space'), _Exec),
                     (KeyPress('space'), _Func) ],
                   ["checkbox"])

    allMenuActions([ (KeyPress('Right'),  _RemapSubmenuFocused),
                     (KeyPress('Return'), _RemapSubmenuFocused) ],
                   ["sub"])


def mouseMenuNavigation(closeOnExec=True, moveWithModifier=Alt, closeButtons=[3]):

    if moveWithModifier:
        allMenuActions([
                (ButtonPress(moveWithModifier, 'Button1'), _MenuRaise),
                (ButtonPress(moveWithModifier, 'Button1'), 'MoveOpaque'),
                (ButtonPress(moveWithModifier, 'Button1'), 'UnLinkMenu'),
                (ButtonRelease(MoveResizeMask, 'Button1'), _MenuEndMoveResize)
            ])

    for button in closeButtons:
        allMenuActions([(ButtonPress(-Ctrl-Alt, "Button%d"%button),_UnmapTree)])

        allMenuActions([ (ButtonPress('Button%d'%button), _UnmapSubmenus),
                         (ButtonPress('Button%d'%button), 'UnmapMenu') ],
                       ["title"])


    allMenuActions([ (ButtonPress(-Ctrl-Alt, 'Button1'), _Exec),
                     (ButtonPress(-Ctrl-Alt, 'Button1'), _Func) ],
                   ["item", "sub", "checkbox"])

    if closeOnExec:
        allMenuActions([ (ButtonPress(-Ctrl-Alt, 'Button1'), _UnmapTree) ],
                       ["item", "sub", "checkbox"])

    
    allMenuActions([ (ButtonPress('Button1'), _MenuRaise),
                     (ButtonPress(-Alt, 'Button1'), 'UnLinkMenu'),
                     (ButtonPress(-Alt, 'Button1'), 'Move') ],
                   ["title"])

    allMenuActions([ (EnterNotify(), _UnmapSubmenus),
                     (ButtonRelease('AnyButton'), _UnmapTree) ],
                   ["item"])

    allMenuActions([ (EnterNotify(), _UnmapSubmenus) ], ["checkbox"])

    allMenuActions([ (EnterNotify(), 'MapSubmenuOnly'),
                     (ButtonPress('AnyButton'), 'RemapSubmenu') ],
                   ["sub"])


def ignoreModifiers(modifiers):
    mods = []
    for mod in modifiers:
        if _modifiers.has_key(mod):
            mods.append(_modifiers[mod])

    # weird code that builds masks for all combinations of modifiers
    # don't ask how.. don't ask why.. don't ask questions..
    masks = []
    for i in range(len(mods)):
        masks.append(mods[i])
        for j in mods[i:]:
            if mods[i] | j not in masks:
                masks.append(mods[i] | j)
            for k in masks[:]:
                if j | k not in masks:
                    masks.append(j|k)

    for mask in masks:
        screen().ImodAdd(mask)

CapsLock = "Caps_Lock"
NumLock = "Num_Lock"

def registerWindowFunction(name, function):
    if not isinstance(function, kahakai.WindowFunction):
        function = kahakai.createWindowFunction(function)

    screen().kahakai.registerWindowFunction(name, function)
    
def registerScreenFunction(name, function):
    if not isinstance(function, kahakai.ScreenFunction):
        function = kahakai.createScreenFunction(function)

    screen().kahakai.registerScreenFunction(name, function)

def registerMenuFunction(name, function):
    if not isinstance(function, kahakai.MenuFunction):
        function = kahakai.createMenuFunction(function)

    screen().kahakai.registerMenuFunction(name, function)

