#!/usr/bin/python
#clickity
#Tray icon application to simulate difficult clicks and drags

## Author: Eric Bohlman

VERSION='0.2.0'
license='''
Copyright (c) 2010 Eric Bohlman

Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
'''

import pygtk
pygtk.require('2.0')
import gtk,gobject
import os,sys
import ConfigParser
from optparse import OptionParser

from clickitypix import *

class Clickity(object):
    
    def __init__(self):
        self.modes={
                'idle':(pix_mouse,self.do_idle),
                'right click':(pix_right,self.do_right),
                'middle click':(pix_middle,self.do_middle),
                'up':(pix_up,self.do_up),
                'down':(pix_down,self.do_down),
                'left double click':(pix_leftdouble,self.do_left_double),
                'right double click':(pix_rightdouble,self.do_right_double),
                'middle double click':(pix_middledouble,self.do_middle_double),
                'left drag':(pix_leftdrag,self.do_left_drag),
                'right drag':(pix_rightdrag,self.do_right_drag),
                'middle drag':(pix_middledrag,self.do_middle_drag),
                'left click':(pix_left,self.do_left),
                'menu':(gtk.STOCK_PREFERENCES,self.do_menu),
                'back':(gtk.STOCK_GO_BACK,self.do_back),
            }
        self.cfgpath='clickity/clickity.cfg'
        self.icon = gtk.StatusIcon()
        self.icon.connect('popup-menu', self.on_right_click)
        self.icon.connect('activate',self.on_activate)
        if sys.platform!='win32':
            self.display=X11.XOpenDisplay(None)
        self.gdkdisplay=gtk.gdk.display_get_default()
        self.holdtimer=self.dwelltimer=self.reptimer=self.autoscantimer=None
        self.awaiting_release=self.warning_dwell=False
        self.stop_autoscan=False
        self.dragpending=0
        self.mask=0
        self.holdx=self.holdy=0
        gobject.timeout_add(100,self.on_idle)
        (screen,self.holdx,self.holdy,mask)=self.gdkdisplay.get_pointer()
        self.get_options()

    def get_options(self):
        defaults={'delay':1000,'step_delay':2000,'step_rate':750,'revert':False,'threshold':20,
            'drag_release':1000,'profile':'ALL','dwell':0,'dwell_warning':500,'jump':False,
            'up_down_rate':500,'up_down_delay':0,'autoscan':False,}
        self.profiles={}
        cfgpath=self.get_config_path()
        if cfgpath:
            cp=ConfigParser.ConfigParser()
            if cp.read([cfgpath]):
                if cp.has_section('settings'):
                    for k in defaults.keys():
                        k1=k.replace('_','-')
                        if cp.has_option('settings',k1):
                            if isinstance(defaults[k],bool):
                                defaults[k]=cp.getboolean('settings',k1)
                            elif isinstance(defaults[k],int):
                                defaults[k]=cp.getint('settings',k1)
                            else:
                                defaults[k]=cp.get('settings',k1)
                if cp.has_section('profiles'):
                    self.profiles=dict(cp.items('profiles'))
        desc='''All times are in milliseconds. Releasing the left button when activated (icon is blinking or stepping) triggers action.
Pressing the control key terminates drags, and is the only way to do so if drag-release is set to 0.
A step-delay of 0 prevents stepping through actions.
Actions can be idle (activation does nothing), right-click, middle-click, up, down, left-double-click, right-double-click,
middle-double-click, left-drag, right-drag, middle-drag, left-click, back and menu. If no actions are given, all will be included in the
order listed. Left-clicking the icon steps through actions without activating them.'''
        parser=OptionParser('%prog [options] [actions]',
            description=desc,version='%prog '+VERSION)
        parser.add_option('-d','--delay',type='int',
            help='time to hold left button before activation')
        parser.add_option('-s','--step-delay',type='int',
            help='time to hold after activation to begin stepping')
        parser.add_option('-r','--step-rate',type='int',
            help='time to display action while stepping')
        parser.add_option('-l','--drag-release',type='int',
            help='time to release drag after no motion')
        parser.add_option('-e','--dwell',type='int',
            help='time to dwell before initiating action')
        parser.add_option('-w','--dwell_warning',type='int',
            help='time to blink before dwell action')
        parser.add_option('-u','--up-down-rate',type='int',
            help='time between repeats of up or down clicks')
        parser.add_option('-o','--up-down-delay',type='int',
            help='time to wait after up or down click before repeating')
        parser.add_option('-t','--threshold',type='int',
            help='pixels of movement to prevent activation')
        parser.add_option('-a','--autoscan',action='store_true',
            help='Step through actions when idle')
        parser.add_option('-j','--jump',action='store_true',
            help='jump forward one action when using button if dwell enabled')
        parser.add_option('-v','--revert',action='store_true',
            help='revert to showing original action after stepping')
        parser.add_option('-p','--profile',
            help='action profile to load from config file')
        parser.set_defaults(**defaults)
        (options,args)=parser.parse_args()
        self.hold_delay=options.delay
        self.advance_delay=options.step_delay
        self.advance_rate=options.step_rate
        self.drag_delay=options.drag_release
        self.dwell=options.dwell
        self.dwell_warning_delay=options.dwell_warning
        self.up_down_rate=options.up_down_rate
        self.up_down_delay=options.up_down_delay
        self.autoscan=options.autoscan
        self.jump=options.jump
        self.revert=options.revert
        self.bail_threshold=options.threshold
        self.profiles['ALL']='idle right-click middle-click up down left-double-click'+\
            ' right-double-click middle-double-click left-drag right-drag middle-drag left-click menu'
        if len(args):
            self.profiles['ARGS']=' '.join(args)
            profile='ARGS'
        else:
            profile=options.profile
        self.profile_stack=[]
        self.set_profile(None,profile)
        if len(self.actions)==0:
            self.profile_stack=[]
            self.set_profile(None,'ALL')
     
    def get_config_path(self):
        if sys.platform=='win32':
            config_home=os.path.expandvars('$USERPROFILE/Application Data')
            config_dirs=''
        else:
            config_home=os.environ.get('XDG_CONFIG_HOME') or os.path.expandvars('$HOME/.config')
            config_dirs=os.environ.get('XDG_CONFIG_DIRS') or '/etc'
        path=os.path.join(config_home,self.cfgpath)
        if not os.path.isdir(path):
            path=None
            for cfd in config_dirs.split(':'):
                path=os.path.join(cfd,self.cfgpath)
                if os.path.isdir(path):
                    break
        return path
        
    def set_profile(self,widget,profile):
        self.actions=[]
        for arg in self.profiles[profile].split():
            arg=arg.replace('-',' ').lower()
            targ=arg
            if arg[-1] in '+%':
                targ=arg[:-1]
            if targ in self.modes:
                self.actions.append(arg)
            else:
                sys.stderr.write('Unknown action: %s\n'% arg)
        self.mode=self.home_mode=0
        if len(self.actions)>0:
            self.profile_stack.append(profile)
            self.setmode(self.mode)

    def on_right_click(self,icon, event_button, event_time):
        menu = gtk.Menu()
        if len(self.profiles)>1:
            item=gtk.MenuItem('Profiles')
            item.set_sensitive(False)
            menu.append(item)
            for i,v in enumerate(sorted(self.profiles.keys())):
                if i<10:
                    itext='_%s. %s' % (i,v)
                else:
                    itext=v
                item=gtk.MenuItem(itext)
                item.connect('activate',self.set_profile,v)
                menu.append(item)
        menu.append(gtk.SeparatorMenuItem())
        item=gtk.MenuItem('_Preferences')
        item.connect('activate',self.on_preferences)
        menu.append(item)
        item=gtk.MenuItem('_About')
        item.connect('activate',self.on_about)
        menu.append(item)
        item=gtk.MenuItem('_Quit')
        item.connect('activate',self.on_quit)
        menu.append(item)
        menu.show_all()
        menu.popup(None, None,None,event_button,event_time,icon)

    def on_quit(self,widget):
        if self.dragpending:
            self.release_drag()
        if sys.platform!='win32':
            X11.XCloseDisplay(self.display)
        gtk.main_quit()
        
    def on_about(self,widget):
        dlg=gtk.AboutDialog()
        dlg.set_name('Clickity')
        dlg.set_license(license)
        dlg.set_authors(['Eric Bohlman <ericbohlman@gmail.com>'])
        dlg.set_version(VERSION)
        dlg.set_website('http://clickity.sourceforge.net')
        dlg.set_comments('Simulate difficult mouse clicks and drags')
        dlg.show()
        dlg.run()
        dlg.destroy()
        
    def on_preferences(self,widget):
        dlg=gtk.Dialog(title='Preferences',buttons=
            (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
            gtk.STOCK_OK, gtk.RESPONSE_OK))
        dlg.set_resizable(False)
        dlg.set_default_response(gtk.RESPONSE_OK)
        nb=gtk.Notebook()
        dlg.vbox.pack_start(nb,False,False,0)
        vbox=gtk.VBox()
        nb.append_page(vbox,gtk.Label('General'))
        frame=gtk.Frame('Hold')
        vbox.pack_start(frame,False,False,5)
        table=gtk.Table(rows=3,columns=3)
        frame.add(table)
        delay=self.labspin(table,0,0,'Delay',self.hold_delay)
        stepdelay=self.labspin(table,1,0,'Step delay',self.advance_delay)
        steprate=self.labspin(table,2,0,'Step rate',self.advance_rate)
        jump=gtk.CheckButton('Jump')
        jump.set_active(self.jump)
        table.attach(jump,0,1,2,3)
        revert=gtk.CheckButton('Revert')
        revert.set_active(self.revert)
        table.attach(revert,1,2,2,3)
        frame=gtk.Frame('Dwell')
        table=gtk.Table(rows=2,columns=2)
        frame.add(table)
        dwell=self.labspin(table,0,0,'Delay',self.dwell)
        warning=self.labspin(table,1,0,'Warning',self.dwell_warning_delay)
        vbox.pack_start(frame,False,False,5)
        frame=gtk.Frame('Scroll')
        table=gtk.Table(rows=2,columns=2)
        frame.add(table)
        scrolldelay=self.labspin(table,0,0,'Delay',self.up_down_delay)
        scrollrate=self.labspin(table,1,0,'Rate',self.up_down_rate)
        vbox.pack_start(frame,False,False,5)
        table=gtk.Table(rows=4,columns=2)
        autoscan=gtk.CheckButton('Autoscan')
        autoscan.set_active(self.autoscan)
        table.attach(autoscan,0,1,0,1)
        table.attach(gtk.Label('Drag release time: '),0,1,1,2)
        dragrelease=gtk.SpinButton(gtk.Adjustment(value=self.drag_delay, lower=0, upper=9999, step_incr=1, page_incr=100))
        table.attach(dragrelease,1,2,1,2)
        table.attach(gtk.Label('Threshold: '),0,1,2,3)
        threshold=gtk.SpinButton(gtk.Adjustment(value=self.bail_threshold, lower=0, upper=100, step_incr=1, page_incr=10))
        table.attach(threshold,1,2,2,3)
        table.attach(gtk.Label('Profile: '),0,1,3,4)
        self.pref_profile=gtk.combo_box_new_text()
        for i,v in enumerate(sorted(self.profiles.keys())):
            self.pref_profile.append_text(v)
            if v==self.profile_stack[-1]:
                self.pref_profile.set_active(i)
        table.attach(self.pref_profile,1,2,3,4)
        vbox.pack_start(table,False,False,5)
        vbox=gtk.VBox()
        nb.append_page(vbox,gtk.Label('Profiles'))
        hbox=gtk.HBox()
        hbox.pack_start(gtk.Label('Profile to edit: '),False,False,5)
        profile_edit=gtk.combo_box_entry_new_text()
        profile_edit.connect('changed',self.on_edit_profile_selected)
        self.tprofiles=dict(self.profiles)
        for v in sorted(self.tprofiles.keys()):
            profile_edit.append_text(v)
        hbox.pack_start(profile_edit,False,False,5)
        vbox.pack_start(hbox,False,False,5)
        hbox=gtk.HButtonBox()
        hbox.set_layout(gtk.BUTTONBOX_SPREAD)
        newprofile=gtk.Button(stock=gtk.STOCK_ADD)
        newprofile.connect('clicked',self.on_new_profile,profile_edit)
        hbox.add(newprofile)
        delprofile=gtk.Button(stock=gtk.STOCK_REMOVE)
        delprofile.connect('clicked',self.on_delete_profile,profile_edit)
        hbox.add(delprofile)
        vbox.pack_start(hbox,False,False,5)
        vbox.pack_start(gtk.HSeparator(),False,False,5)
        self.ep_list=gtk.ListStore(str,'gboolean','gboolean')
        self.ep_view=gtk.TreeView(self.ep_list)
        self.ep_view.set_reorderable(True)
        vbox.pack_start(self.ep_view,False,False,5)
        act_model=gtk.ListStore(str)
        for k in sorted(self.modes.keys()):
            act_model.append((k.replace(' ','-'),))
        cell=gtk.CellRendererCombo()
        cell.set_property('model',act_model)
        cell.set_property('text-column',0)
        #cell.set_property('has-entry',False)
        cell.set_property('editable',True)
        cell.connect('edited',self.on_action_edited)
        ep_tvc=gtk.TreeViewColumn('Action',cell,text=0)
        self.ep_view.append_column(ep_tvc)
        cell=gtk.CellRendererToggle()
        cell.set_property('activatable',True)
        cell.connect('toggled',self.on_action_toggled,1)
        ep_tvc=gtk.TreeViewColumn('Advance',cell,active=1)
        self.ep_view.append_column(ep_tvc)
        cell=gtk.CellRendererToggle()
        cell.set_property('activatable',True)
        cell.connect('toggled',self.on_action_toggled,2)
        ep_tvc=gtk.TreeViewColumn('Back',cell,active=2)
        self.ep_view.append_column(ep_tvc)
        hbox=gtk.HButtonBox()
        newaction=gtk.Button(stock=gtk.STOCK_ADD)
        newaction.connect('clicked',self.on_new_action)
        hbox.add(newaction)
        delaction=gtk.Button(stock=gtk.STOCK_REMOVE)
        delaction.connect('clicked',self.on_delete_action)
        hbox.add(delaction)
        btn=gtk.Button(stock=gtk.STOCK_GO_UP)
        btn.connect('clicked',self.on_move_action_up)
        hbox.add(btn)
        btn=gtk.Button(stock=gtk.STOCK_GO_DOWN)
        btn.connect('clicked',self.on_move_action_down)
        hbox.add(btn)
        vbox.pack_start(hbox,False,False,5)
        self.deleting_profile=False
        self.profile_edited=''
        dlg.show_all()
        self.ep_view.columns_autosize()
        if dlg.run()==gtk.RESPONSE_OK:
            self.rebuild_profile() #save last profile being edited, if any
            self.hold_delay=delay.get_value_as_int()
            self.advance_delay=stepdelay.get_value_as_int()
            self.advance_rate=steprate.get_value_as_int()
            self.drag_delay=dragrelease.get_value_as_int()
            self.dwell=dwell.get_value_as_int()
            self.dwell_warning_delay=warning.get_value_as_int()
            self.up_down_rate=scrollrate.get_value_as_int()
            self.up_down_delay=scrolldelay.get_value_as_int()
            self.autoscan=autoscan.get_active()
            self.jump=jump.get_active()
            self.revert=revert.get_active()
            self.bail_threshold=threshold.get_value_as_int()
            self.set_profile(None,self.pref_profile.get_active_text())
            self.profiles=dict(self.tprofiles)
            self.save_config()
        dlg.destroy()
        
    def labspin(self,table,col,row,label,initval):
        table.attach(gtk.Label(label),col,col+1,row,row+1,xpadding=5)
        res=gtk.SpinButton(gtk.Adjustment(value=initval, lower=0, upper=9999, step_incr=1, page_incr=100))
        table.attach(res,col,col+1,row+1,row+2)
        return res
    
    def on_edit_profile_selected(self,cb):
        if not self.deleting_profile:
            t=cb.get_active_text()
            if t in self.tprofiles:
                self.setup_edit_profile(t)
        
    def on_new_profile(self,btn,cb):
        t=cb.child.get_text()
        if t:
            cb.prepend_text(t)
            cb.set_active(0)
            self.tprofiles[t]=''
            self.setup_edit_profile(t)
            self.pref_profile.append_text(t)
        
    def on_delete_profile(self,btn,cb):
        t=cb.get_active()
        if t>0:
            self.deleting_profile=True
            del self.tprofiles[cb.get_active_text()]
            cb.remove_text(t)
            cb.child.set_text('')
            cb.set_active(-1)
            self.deleting_profile=False
            self.setup_edit_profile('')
            self.profile_edited=''
            
    def on_action_edited(self,cell,path,text):
        self.ep_list[path][0]=text
        self.rebuild_profile()
        
    def on_action_toggled(self,cell,path,col):
        self.ep_list[path][col]=not self.ep_list[path][col]
        self.rebuild_profile()
        
    def on_new_action(self,btn):
        self.ep_list.append(('',False,False))
        self.rebuild_profile()
        
    def on_delete_action(self,btn):
        (model,iter)=self.ep_view.get_selection().get_selected()
        if iter:
            model.remove(iter)
        self.rebuild_profile()
        
    def on_move_action_up(self,btn):
        (model,iter)=self.ep_view.get_selection().get_selected()
        if iter:
            path=model.get_path(iter)[0]
            if path>0:
                prev=model.get_iter((path-1,))
                model.swap(iter,prev)
        self.rebuild_profile()
        
    def on_move_action_down(self,btn):
        (model,iter)=self.ep_view.get_selection().get_selected()
        if iter:
            next=model.iter_next(iter)
            if next:
                model.swap(iter,next)
        self.rebuild_profile()
        
    def setup_edit_profile(self,profile):
        self.rebuild_profile() #make sure previous profile was saved
        self.ep_list.clear()
        if profile in self.tprofiles:
            self.profile_edited=profile
            for a in self.tprofiles[profile].split():
                self.ep_list.append(self.split_action(a))
        
    def rebuild_profile(self):
        if self.profile_edited:
            t=[]
            for (action,advance,back) in self.ep_list:
                if advance:
                    action+='+'
                elif back:
                    action+='%'
                t.append(action)
            self.tprofiles[self.profile_edited]=' '.join(t)
    
    def save_config(self):
        cp=ConfigParser.ConfigParser()
        cp.add_section('settings')
        cp.set('settings','delay',self.hold_delay)
        cp.set('settings','step-delay',self.advance_delay)
        cp.set('settings','step-rate',self.advance_rate)
        cp.set('settings','drag-release',self.drag_delay)
        cp.set('settings','dwell',self.dwell)
        cp.set('settings','dwell-warning',self.dwell_warning_delay)
        cp.set('settings','up-down-rate',self.up_down_rate)
        cp.set('settings','up-down-delay',self.up_down_delay)
        cp.set('settings','autoscan',self.autoscan)
        cp.set('settings','jump',self.jump)
        cp.set('settings','revert',self.revert)
        cp.set('settings','threshold',self.bail_threshold)
        cp.set('settings','profile',self.profile_stack[-1])
        cp.add_section('profiles')
        for k,v in self.profiles.items():
            cp.set('profiles',k,v)
        if sys.platform=='win32':
            config_home=os.path.expandvars('$USERPROFILE/Application Data')
        else:
            config_home=os.environ.get('XDG_CONFIG_HOME') or os.path.expandvars('$HOME/.config')
        path=os.path.join(config_home,self.cfgpath)
        if not os.path.exists(os.path.dirname(path)):
            try:
                os.makedirs(os.path.dirname(path))
            except IOError,ex:
                self.show_error(str(ex))
        try:
            f=open(path,'w')
            cp.write(f)
            f.close()
        except IOError,ex:
            self.show_error(str(ex))

    def show_error(message):
        error_dialog = gtk.MessageDialog(None,0,
                        gtk.MESSAGE_ERROR,gtk.BUTTONS_CLOSE,
                        message)
        error_dialog.run()
        error_dialog.destroy()
            
    def on_activate(self,icon):
        self.advancemode()
        self.home_mode=self.mode
        
    def advancemode(self):
        self.mode=(self.mode+1)%len(self.actions)
        self.setmode(self.mode)

    def setmode(self,action):
        (mode,self.autoadvance,self.autoback)=self.split_action(self.actions[action])
        self.icon.set_blinking(False)
        icon=self.modes[mode][0]
        if isinstance(icon,list):
            self.icon.set_from_pixbuf(gtk.gdk.pixbuf_new_from_xpm_data(icon))
        else:
            self.icon.set_from_stock(icon)
        self.icon.set_tooltip('Clickity: %s' % mode)
        self.action=self.modes[mode][1]
        
    def split_action(self,action):
        advance=back=False
        t=action[-1]
        if t in '+%':
            action=action[:-1]
            advance=(t=='+')
            back=(t=='%')
        return (action,advance,back)

    def on_idle(self):
        (screen,x,y,mask)=self.gdkdisplay.get_pointer()
        if sys.platform=='win32':
            if User32.GetAsyncKeyState(1):
                mask|=gtk.gdk.BUTTON1_MASK
            if User32.GetAsyncKeyState(17):
                mask|=gtk.gdk.CONTROL_MASK
        if self.autoscan and self.advance_rate>0:
            if self.autoscantimer is None:
                self.autoscantimer=gobject.timeout_add(self.advance_rate,self.on_autoscan_timeout)
        else:
                if self.autoscantimer is not None:
                    gobject.source_remove(self.autoscantimer)
                    self.autoscantimer=None
        if self.dragpending:
            self.post_drag(x,y,mask)
        else:
            if abs(x-self.holdx)>self.bail_threshold or abs(y-self.holdy)>self.bail_threshold:
                if self.holdtimer:
                    gobject.source_remove(self.holdtimer)
                    self.holdtimer=None
                if self.awaiting_release:
                    self.awaiting_release=False
                    if self.revert or (self.jump and self.dwell):
                        self.mode=self.home_mode
                    self.setmode(self.mode)
                if self.dwell:
                    self.holdx=x
                    self.holdy=y
                    self.stop_autoscan=True
                    if self.warning_dwell:
                        self.cancel_dwell()
                    else:
                        self.cancel_dwell()
                        self.dwelltimer=gobject.timeout_add(self.dwell,self.on_dwell_timeout)
                        self.home_mode=self.mode
            mask&=gtk.gdk.BUTTON1_MASK
            if mask!=self.mask:
                self.mask=mask
                if mask:
                    self.cancel_dwell()
                if self.hold_delay>0:
                    if mask:
                        self.holdtimer=gobject.timeout_add(self.hold_delay,self.on_hold_timeout)
                        self.home_mode=self.mode
                        self.holdx=x
                        self.holdy=y
                        if self.jump and self.dwell:
                            self.advancemode()
                    else:
                        if self.holdtimer:
                            gobject.source_remove(self.holdtimer)
                            self.holdtimer=None
                        if self.awaiting_release:
                            self.awaiting_release=False
                            self.do_action(False)
                            self.stop_autoscan=False
                            if self.revert or (self.jump and self.dwell):
                                self.mode=self.home_mode
                                self.setmode(self.mode)
                        else:
                            if self.jump and self.dwell:
                                self.mode=self.home_mode
                                self.setmode(self.mode)
        return True
        
    def on_hold_timeout(self):
        if self.awaiting_release:
            self.advancemode()
            self.holdtimer=gobject.timeout_add(self.advance_rate,self.on_hold_timeout)
        else:
            self.awaiting_release=True
            self.icon.set_blinking(True)
            if self.advance_delay>0 and self.advance_rate>0 and len(self.actions)>1:
                self.holdtimer=gobject.timeout_add(self.advance_delay,self.on_hold_timeout)
        return False
    
    def on_dwell_timeout(self):
        if self.warning_dwell or self.dwell_warning_delay<=0:
            self.warning_dwell=False
            self.do_action(True)
            self.stop_autoscan=False
        else:
            self.warning_dwell=True
            self.icon.set_blinking(True)
            self.holdtimer=gobject.timeout_add(self.dwell_warning_delay,self.on_dwell_timeout)
        return False
    
    def on_repeat_timeout(self):
        if self.reptimer:
            self.simulate_click(self.dragpending,False)
            gobject.source_remove(self.reptimer)
            self.reptimer=gobject.timeout_add(self.up_down_rate,self.on_repeat_timeout)
        return False
    
    def on_autoscan_timeout(self):
        if (not self.dragpending and not (self.mask&gtk.gdk.BUTTON1_MASK)
                and not self.warning_dwell and not self.stop_autoscan):
            self.advancemode()
        return True

    def post_drag(self,x,y,mask):
        if (mask&gtk.gdk.CONTROL_MASK):
            self.release_drag()
        else:
            if abs(x-self.holdx)>self.bail_threshold or abs(y-self.holdy)>self.bail_threshold:
                self.holdx=x
                self.holdy=y
                if self.reptimer:
                    self.release_drag()
                if self.holdtimer:
                    gobject.source_remove(self.holdtimer)
                self.holdtimer=gobject.timeout_add(self.drag_delay,self.release_drag)

    def release_drag(self):
        self.fake_button(self.dragpending,False)
        self.dragpending=0
        if self.reptimer:
            gobject.source_remove(self.reptimer)
            self.reptimer=None

    def cancel_dwell(self):
        if self.dwelltimer:
            gobject.source_remove(self.dwelltimer)
            self.dwelltimer=None
        if self.warning_dwell:
            self.warning_dwell=False
            self.setmode(self.mode)
            
    def do_action(self,dwelling):
        self.setmode(self.mode)
        (screen,x,y,mask)=self.gdkdisplay.get_pointer()
        t=self.icon.get_geometry()
        if t and gtk.gdk.region_rectangle(t[1]).point_in(x,y):
            if dwelling:
                # if dwelling over icon, force left click to select action
                self.do_left()
            else:
                # if holding over icon, force right click to get menu
                self.do_right()
        else:
            self.action()
            if self.autoadvance:
                self.advancemode()
            elif self.autoback:
                self.do_back()
#
# Actions
#

    def do_idle(self):
        pass
        
    def do_left(self):
        self.simulate_click(1,False)
        
    def do_right(self):
        self.simulate_click(3,False)
        
    def do_middle(self):
        self.simulate_click(2,False)

    def do_up(self):
        self.start_repeat(4)
        
    def do_down(self):
        self.start_repeat(5)
        
    def do_left_double(self):
        self.simulate_click(1,True)
        
    def do_right_double(self):
        self.simulate_click(3,True)
        
    def do_middle_double(self):
        self.simulate_click(2,True)
        
    def do_left_drag(self):
        self.start_drag(1)
        
    def do_right_drag(self):
        self.start_drag(3)
        
    def do_middle_drag(self):
        self.start_drag(2)
        
    def do_menu(self):
        self.on_right_click(self.icon,0,0)
        
    def do_back(self):
        if len(self.profile_stack)>1:
            self.profile_stack.pop()
            self.set_profile(None,self.profile_stack[-1])
            
    def start_drag(self,button):
        self.dragpending=button
        self.fake_button(button,True)
        
    def start_repeat(self,button):
        self.simulate_click(button,False)
        if self.up_down_rate>0:
            self.dragpending=button
            if self.up_down_delay>0:
                self.reptimer=gobject.timeout_add(self.up_down_delay,self.on_repeat_timeout)
            else:
                self.reptimer=gobject.timeout_add(self.up_down_rate,self.on_repeat_timeout)
                 
    def simulate_click(self,button,double):
        self.fake_button(button,True)
        self.fake_button(button,False)
        if double:
            self.fake_button(button,True)
            self.fake_button(button,False)
            
    def fake_button(self,button,down):
        if sys.platform=='win32':
            if (button<4):
                flags=[[4,2],[16,8],[64,32]][button-1][down]
                data=0
            else:
                if not down:
                    return
                flags=2048
                if button==4:
                    data=1
                else:
                    data=-1
            User32.mouse_event(flags,0,0,data,None)
        else:
            XTest.XTestFakeButtonEvent(self.display,button,down,0)
            X11.XFlush(self.display)
 
if __name__ == '__main__':
    if sys.platform=='win32':
        from ctypes import windll
        User32=windll.user32
    else:
        from ctypes import cdll
        from ctypes.util import find_library
        X11=cdll.LoadLibrary(find_library('X11'))
        XTest=cdll.LoadLibrary(find_library('Xtst'))
    app=Clickity()
    gtk.main()
