#!/usr/bin/python ############################################################################### # # Copyright 2006,2007 Danil Dotsenko # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA # ############################################################################### # Contributors: Trebol-a, info@trebol-a.com # Danil Dotsenko, dd@accentsolution.com from __future__ import division import karamba import os from commands import getoutput import glob import re from zipfile import is_zipfile from StringIO import StringIO from gettext import GNUTranslations import localconf # this is a custom wrapper for ConfigParser. Should be in local dir. from threading import Timer # used for delayed saving of settings. def debug(): return False ########################################################################### # All applet settings go here, and are accessible from everywhere. ########################################################################### class configClass: ''' An all-encoupasing, application-wide configuration storage location. This will be shared even between concurrently running themes. This is the equivalent of root directory of Windows registry. ''' def __init__(self): self.common={ 'name':'A-Foto', 'version':"v1.6, June 5, 2007", 'author':"Trebol-a (info@trebol-a.com)\nDanil (Suslik) Dotsenko (dd@accentsolution.com)", # incriment this every time there is update in /frames or /locale folders and you want to overwrite them in ~/.superkaramba 'temasversion':None, # loaded from INI 'localeversion':None, # loaded from INI 'pluginsversion':None, 'homepath':None, 'forever':60000000, # 1000 Minutes. we use this in "do not update the picture in single mode" as the update interval. 'temasPath':'', # automatically generated on every 1st start 'localePath':'', # automatically generated on every 1st start 'frameParts':["aa","ab","ac","ba","bc","ca","cb","cc"], # used for filenames. "s_" added for shadow, "t_" added for frame 'fileFilter':re.compile(".+\.(jpg|jpeg|png|gif|bmp)$",re.IGNORECASE), 'supported':[], # loaded from INI and converted to [] - should be an array 'order':[], # loaded from INI and converted to [] - should be an array containig INI file group names. 'active':[], # actually running instances using INI file group names. 'cleanStart':False, } self.ini = None # will be a holder of INI style config file contents and associated methods self.instance = {} # this will be populated by settings of each consecutive theme. See instanceClass class bellow self.plugins = {} self.using_ipc = False self.dcophelper = None def encode(self,string): ''' This is a helper function to convert pure unicode strings into locale-specific utf8 or utf16. This is needed for some python functions, like popen, that cannot handle pure unicode. ''' # it the future this may try to catch the actual locale and use that, # in the mean time, if it's unicode, you get utf8 if type(string) == type(u''): # unicode return string.encode('utf8') else: return string def pushSettingsDown(self): ''' This function packages and deposits COMMON afoto settings at INI object. We dont' save it. Issue something like appConfig.ini.save() when ready. ''' toSave = { 'temasversion':None, 'localeversion':None, 'order':None, } # creating a dict type, which our ini backend can take as group. for key in toSave.keys(): toSave[key]=self.common[key] self.ini.set('common',toSave) # will be saves as a group in section 'common' def pullSettingsUp(self): ''' This function pulls relevant entries from INI object. We don't load INI. Issue something like appConfig.ini.load(filename) before this. Note we pull ALL settings, push up only selected settings. ''' self.common.update(self.ini.get('common')) # if only one value was saved in an entry, it may return as string, not array, so toArray = [ 'supported', 'order', ] for key in toArray: if type(self.common[key]) == type(''): # string self.common[key] = [self.common[key]] # now, we are trying to catch our own mistakes and lack of thread-save config interface. # sometimes, when SUperKaramba is exiting, it may ask many afoto instances to close at the same time # in that case, simultaneous writing of configs may result in screwed up "order" entry in afiti.ini # we are going to pick up lost instance settings and add them to the end of line. - better last than lost. sections = self.ini.cfgObject.keys() section = sections.__len__() - 1 if debug(): print "pullSettingsUp: sections are ", sections while section > -1: if sections[section][:9] != 'instance_': sections.__delitem__(section) section = section - 1 if debug(): print "pullSettingsUp: sections part 2 are ", sections if sections.__len__() > self.common['order'].__len__(): for section in self.common['order']: try: sections.remove(section) except: pass if debug(): print "pullSettingsUp: sections part 3 are ", sections self.common['order'].extend(sections) def demoteInstance(self, name): ''' This function moves the Name of instance up or down the Order string. The new place will be right after all "Running" instances. This is usually called when we close this instance. (We also pushSettingsDown, but don't save INI file.) ''' remaining = self.common['order'] # lets remove all running out of the line. for iname in self.common['active']: try: remaining.remove(iname) except: pass self.common['active'].remove(name) self.common['order'] = self.common['active'] + [name] + remaining self.pushSettingsDown() self.ini.save() # returns instance's new place in the Order. # if it is 0 == it was last running instance. return self.common['order'].index(name) def newInstanceSettings(self): ''' This function looks at list of currently running instances and compares it to order list. - if all instance on order are already runing, we create a completely new one and put it on both: "order" and "running" list - if there are some instances on "order" list that are not used yet, we read that set of settings. In both cases we return the _bare minimum_ of settings. We add the instance NAME, not widget reference to the active, order, but don't push it up to INI object. Use *.instance[widget].pushSettingsDown() followed by *.ini.save() to actually save the settings in INI file. ''' # This part is THREAD-UNSAFE. I am hoping no one will find out. if self.common['active'].__len__() < self.common['order'].__len__(): # this means there are some old instance settings that we can use # lets remove what we already run from the order line. remaining = self.common['order'] # lets remove all running out of the line. for iname in self.common['active']: if remaining.__contains__(iname): remaining.__delitem__(remaining.index(iname)) nextInstance = remaining.pop(0) self.common['active'].append(nextInstance) self.common['order'] = self.common['active']+remaining self.pushSettingsDown() # but that doesn't save the INI file yet. # If the image is successfully drawn, we actually save it. nextSettings = self.ini.get(nextInstance) else: # if we are here - we used up all "old" instance settings and # have to create completely new set. nextInstance = "instance_" + str(self.common['active'].__len__()) self.common['active'].append(nextInstance) self.common['order'].append(nextInstance) # this needs to be deposited in INI object self.pushSettingsDown() # but that doesn't save the INI file yet. # If the image is successfully drawn, we actually save it. nextSettings = { 'associatedName':nextInstance, 'currentPath':self.common['temasPath']+'inicio.png', # should always be local 'resourcePath':'file://'+self.common['temasPath']+'inicio.png', # should always be URL-like } return nextSettings class instanceClass: ''' One copy of this class will be assigned to each consecutive theme instance. ''' def __init__(self): self.cfg = { 'associatedName':None, # Name of config group in INI file with which this entry is associated. 'currentPath':None, # we can't set path here because we don't know where default picture is in relation to us. 'resourcePath':None, # path to the folder or service containing the picture. These look like URL's with the type part dictating the type of resource # at this time, the following types are envisioned: file:// http:// flickr:// and each has a special handling method. 'update':0, # means "change picture or get new version of picture" - if true = slideshow or webcam mode, if false = single mode. 'updateinterval':120000, # 2 minutes. Used for slideshow mode. 'frame':"a-foto", # frame theme 'maxsize':180, # limits BOTH! height AND width of FOTO element. Frame will start OUTSIDE of maxsize 'shadowsize':18, 'limitlong':0, # we can't use True / False as our configparser turns them into strings. 'locationX':200, 'locationY':200, 'width':200, 'height':200, } self.ptr = { 'foto':None, 'sombra':[], 'estilo':[], 'maxsize':None, 'shadowsize':None, 'updateinterval':None, 'frame':None, 'editpath':None, } self.menu = {} self.flag = { 'drawFrame':True, # this is an accelleration flag. if false, skipping the drawing part. 'drawShadow':True, # this is an accelleration flag. if false, skipping the drawing part. 'delayed_action':None, 'lastorientation':1, 'locationXtemp':None, 'locationYtemp':None, } def pushSettingsDown(self): ''' pushSettingsDown() This function will organize and migrate the theme-related settings into ini-file object / repository. We do not save the INI file yet, just deposit the settings there. This is useful for making all instances save the settings to INI object and save it with last exiting instance. ''' appConfig.ini.set(self.cfg['associatedName'],self.cfg) def makeSettingsProper(self): ''' Our INI backend has a nasty habit of turning everything into strings, so we need to convert relevant entries back to needed format after each read from INI backend. ''' toInt = [ 'shadowsize', 'limitlong', 'locationY', 'update', 'width', 'maxsize', 'locationX', 'updateinterval', 'height', ] for key in toInt: self.cfg[key] = int(self.cfg[key]) def pause(self,widget,value=True): ''' this helper function will set a or remove pause in slideshow If Value is set to False, we UnPause. If it's set to true, or we call function without argument Value, the applet is "Paused" ''' if value: # if true pausing karamba.changeInterval(widget, appConfig.common['forever']) self.cfg['update'] = 0 #this takes care of play / pause button in the menu karamba.removeMenuItem(widget, self.menu['menu1'], self.menu['pause']) self.menu['pause'] = karamba.addMenuItem(widget, self.menu['menu1'], _("Resume slideshow") , "player_play") else: # unpausing karamba.changeInterval(widget, self.cfg['updateinterval']) self.cfg['update'] = 1 #this takes care of play / pause button in the menu karamba.removeMenuItem(widget, self.menu['menu1'], self.menu['pause']) self.menu['pause'] = karamba.addMenuItem(widget, self.menu['menu1'], _("Pause slideshow") , "player_pause") def unpause(self,widget, value=True): ''' this helper function will remove a pause in slideshow ''' self.pause(widget, not value) def setNewInterval(self,widget,newInterval=None): ''' setInterval(widget,[newInterval]) this helper function will set the interval for whole theme (applet) update. If no value is assigned to newInterval, it just uses what is already set in updateinterval var This is useful for cases when we are only starting the theme and just want to activate what is in the settings. ''' if newInterval: karamba.changeInterval(widget, newInterval) self.cfg['updateinterval'] = newInterval else: karamba.changeInterval(widget, self.cfg['updateinterval']) self.unpause(widget) ########################################################################### # A-Foto has 3 levels of start-up functions. # 1. "installUpdate()" # if skz package is new on this system, it prepares/unpacks all needed # files in ~/.superkaramba/afoto. # (at this point functionality of installUpdate() is rolled into commonConfig()) # 2. "commonConfig()" # Test and loads the config file and initiates variety of common classes. # This is done only with start of 1st instance of a-foto # 3. "instanceConfig()" # loads settings for a new a-foto instance. It doesn't and shouldn't care if it's first or not. # 3.5 "tryNextImage()" # It is a helper function that comes in play on widget start and updates. # def commonConfig(widget): global appConfig, _ cleanStart = False ########################################################################### # Here we determine if we run from *.skz file, and if true, extract it and use new extracted path. # Else, we use theme's folder for everything. ########################################################################### if is_zipfile(karamba.getThemePath(widget)): status(widget, "SKZ detected, extracting") # Note! It is important to understand this section for SKIN AUTHORS and TRANSLATORS!!! # Before extracting everything, we will check the config file # YOUR UPDATED SKINS / TRANSLATIONS WILL NOT BE EXTRACTED # if the variables above in this file are not updated to higher date/value. homepath = os.environ['HOME']+'/.superkaramba/afoto/' if debug(): print "commonConfig: this is a zipfile" if appConfig.using_ipc: cleanStart = True else: cleanStart = file_based_test(homepath) if cleanStart: if os.path.exists(homepath) == False: os.makedirs(homepath) toExtract = [] # this is where we load the ini-based config file from ~/.superkaramba/afoto folder # if exists, we check the version of installed temas and extract them if needed. # NEED_WORK this is a mess. will rework this when have time. things = ['temas','locale','plugins'] if appConfig.ini.load(homepath+'afoto.ini'): # True if file was read. # if it exists, we need to compare it to the one packaged in SKZ. defaults = karamba.readThemeFile(widget, 'afoto.ini') for thing in things: # now we compare the version number in skz to version number in user's folder. version = re.findall('(?<='+thing+'version = )[0-9]+', defaults) #print "%s's internal version is %s, while present is %s" % (thing, version[0], appConfig.ini.get('common', thing+'version')) if version[0] > appConfig.ini.get('common', thing+'version'): #print "Newer %s included. Extracting." % thing toExtract.append(thing+'/*') appConfig.ini.set('common',thing+'version', version[0]) appConfig.ini.delayed_action("save") else: # this means afoto.ini file was not found => completely new install toExtract.append('afoto.ini') for thing in things: toExtract.append(thing+'/*') # we delay it because it contains.... I forgot why I delay it. Let's hope for good reason. appConfig.ini.delayed_action("load", homepath+'afoto.ini') # now, we know what to extract if len(toExtract): myCommand = ['unzip', '-o', karamba.getThemePath(widget)] myCommand.extend(toExtract) myCommand.append('-d') myCommand.append(homepath) os.spawnvp(os.P_WAIT, 'unzip', myCommand) # this sommand holds the progress until extracting is done. appConfig.ini.delayed_action() appConfig.common['temasPath'] = homepath+"temas/" appConfig.common['localePath'] = homepath+"locale/" appConfig.common['homepath'] = homepath appConfig.pullSettingsUp() # retrieves common"Order" and common"Supported" out of INI object cleanStart = True else: # this means we do NOT run from SKZ file # this option uses the folder you run afoto from and sets that as the path for all items # NOTE! even config file is localised to the current folder! # This behavior is beneficial for troubleshooting and testing, as it will not mess up the "working" settings in ~/.superkaramba/afoto status(widget, "Folder detected, activating") homepath = karamba.getThemePath(widget) if debug(): print "commonConfig: this is an unpacked folder" if appConfig.using_ipc: cleanStart = True else: cleanStart = file_based_test(homepath) if cleanStart: if debug(): print "commonConfig: cleanStart" appConfig.common['temasPath'] = homepath+"temas/" appConfig.common['localePath'] = homepath+"locale/" appConfig.common['homepath'] = homepath appConfig.ini.load(homepath+'afoto.ini') appConfig.pullSettingsUp() # retrieves common"Order" and common"Supported" out of INI object if cleanStart and (not appConfig.using_ipc): # placing the "already running" flag on filesystem a = open(homepath+'running', 'w') a.close() appConfig.common['homepath'] = homepath if debug(): print "commonConfig: appConfig.common is ", appConfig.common ########################################################################### # This activates gettext-based translation services # from now on, use _('translatable text') instead of 'UN-translatable text' ########################################################################### if cleanStart: status(widget, "Setting up translations") try: lang = karamba.userLanguages(widget) # This should give us a nice list (array) with all preferred languages. # in middle ages userLanguage existed but returned a string with only one language. # we don't like that. See bellow except: lang = 'not supported' if type(lang) != type([]): # not array, need to scrub kdeglobals manually tempOut = getoutput('more ~/.kde/share/config/kdeglobals | grep Language') lang = re.split(':',tempOut[9:]) # this may give several languages as an array (ex: ['ru','en_US']) languageFile=None i = 0 while i < lang.__len__() and languageFile==None: if lang[i] == 'en_US': languageFile = 'en_US' elif os.path.exists(appConfig.common['localePath']+lang[i]+".po"): # OK, we will try to be nice and compile nonexisting or stale .mo's # This should be useful for translators - no need to compile .mo's, just translate the .po and rerun afoto. # DO NOT SHIP THE THEME WITHOUT .mo's. Users are unlikely to have msgfmt installed. if os.path.exists(appConfig.common['localePath']+lang[i]+".mo") == False: msgfmtPath = re.compile('/.*msgfmt$').match(getoutput('which msgfmt')) if msgfmtPath: myCommand = "msgfmt "+appConfig.common['localePath']+lang[i]+".po -o "+appConfig.common['localePath']+lang[i]+".mo" getoutput(myCommand) languageFile=appConfig.common['localePath']+lang[i]+".mo" i = i + 1 # thx to Matthew (dmbkiwi@yahoo.com) for showing the right way of doing this. # modified excerpt from LiquidWeather sources if languageFile in [None,'en_US']: _ = lambda(x): x else: # s = karamba.readThemeFile(widget, languageFile) # this function does not wotk on fully-formed URLs if os.path.exists(languageFile): filePointer = open(languageFile,'r') fileContents = filePointer.read() filePointer.close() if fileContents.__len__() > 0: gt = GNUTranslations(StringIO(fileContents)) _ = gt.ugettext else: _ = lambda(x): x # end of excerpt print "commonConfig: cleanStart is ", cleanStart return cleanStart ########################################################################### # SCANNING FOR PLUGINS FOR VARIOUS PATHS SUPPORT ########################################################################### def scanPlugins(widget, customPath=None): global appConfig ''' This function looks at the folder we decided to treat as home folder (appConfig.common['homepath']) and scans the plugins folder for available plugins. Plugins are identified by ini file. Initially we will support only accompanying files in the same directory. Later, we will think about adding some flag to ini file to point to a separate dir for plugin code. ''' # 1. Get listing of ini files in that dir. # 2. import the py's one by one, pulling things out of them in the process. # things to pull out # - "dropped path" string filter # - will need to have a detected > internal path conversion function in your code # - internal address storage URI moniker (file, http, flickr, picasa etc.) # - function that prepares next image # - provide config menu callback or find a way to add config menus # - somehow inform if we need to pause. status(widget, "Scanning Plugins") # looking for all files in ./plugins/ folder that end with .ini or .rc dirlist = os.listdir(appConfig.common['homepath']+'plugins/') srch = re.compile('.+\.(ini|rc)$',re.IGNORECASE) plugins = [] for item in dirlist: if srch.search(item): plugins.append(item) # now the plugins should be populated with all detected .ini and .rc files. if plugins: # if it is... # the next part requires understanding of what each key is used for. # see file.ini, or webimage.ini in plugins folder for explanation of each. # to sum it up, all we really care are 2 things: exec and protocol entries. # - exec is the py file that actually contains our plugin. # - protocol is an identifyer, a nickname for that plugin. # actual filenames don't have to match protocol name, but it is nice to have. # so you can have webimage-John-edition.ini, pointing to myftpaccess.py and registered as johnsimages protocol. # once a link in 'resourcePath' is assigned to johnsimages plugin, it will have a "johnsimages://asfd.asdf" url and # we will use that "johnsimages" as the catch-phrase to see when to use the plugin. os.chdir(appConfig.common['homepath']) # without this, import will not work for me. Even changing PYTHONPATH in mid-script doesn't help. cfg = localconf.cfgObject() for item in plugins: cfg.load(appConfig.common['homepath']+'plugins/'+item) protocol = cfg.get('init','protocol') # that's our made up protocol like "flickr" or "pixma", as well as "file" and "http" if debug(): print "in Plugins: item is ", item, '\nProtocol and Config are: ', protocol, cfg if protocol: # meaning, if there is an actual entry of "protocol" in section "init", otherwise it's None type module = cfg.get('init','exec') # this will be the python file which we will treat as module # cuttin out ".py" from the file name. Import can't handle those. if module[-3:] == '.py': module = module[:-3] exec "import plugins."+module # this should look like "import plugins.flickr" in the end exec "appConfig.plugins['"+protocol+"'] = plugins."+module+".pluginClass()" # I didn't want to make plugin authors import re module, so I am doing it here. appConfig.plugins[protocol].filterObj = re.compile(appConfig.plugins[protocol].filterStr) # now, we will do something rude # "http" urls are special, and if someone tries to be funny and registers # that as a plugin protocol, we kick them into gonads. if appConfig.plugins.has_key('http'): appConfig.plugins.__delitem__('http') return appConfig.plugins.keys() ########################################################################### # INSTANCE CONFIGURATION ########################################################################### def instanceConfig(widget): global appConfig, _ status(widget, "Setting up Instance") # this will be a temporary config instance for theme. instance = instanceClass() # mirroring instance settings into global registry, # You can continue to use "instance" until the end of this function, # and use appConfig.instance[widget] in all functions you call from here with widget as argument. # you don't need to pass theme settings themselves. appConfig.instance[widget] = instance # Creates empty images for shadow (sombra) framame (estilo) and photo (foto) # They are filled with real images later. Coordinates are not important instance.ptr['foto'] = karamba.createImage(widget,0,0,appConfig.common['temasPath']+"null.png") #this is just to show the right pointer when hovering over. karamba.attachClickArea(widget, instance.ptr['foto']) instance.ptr['sombra'] = [] instance.ptr['estilo'] = [] for i in range(8): # we get [0-7], no 8 instance.ptr['sombra'].append(karamba.createImage(widget,1,i,appConfig.common['temasPath']+"null.png")) instance.ptr['estilo'].append(karamba.createImage(widget,2,i,appConfig.common['temasPath']+"null.png")) instance.cfg.update(appConfig.newInstanceSettings()) # newInstanceSettings(widget) will read the setings from next available instance profile # or insert defaults if there is no available profile. # see theme class above for the list of all values. instance.makeSettingsProper() # see help for this function. Making relevant strings into int's. createMenus(widget) # move to the right location. karamba.moveWidget(widget, instance.cfg['locationX'], instance.cfg['locationY']) if debug(): print "in InstanceConfig: instance is ", instance.cfg # next function takes care of determining the source type, and assigning a new image. # If it returns True , we have an image. It exists and instance[widget].cfg['currentPath'] is set. # if not, we have to put in the default image and draw that. if not tryNextImage(widget): # hmmm... the image in theme.cfg['resourcePath'] is not available or nothing meaningfull is there. # we will overwrite "currentPath' with default image and keep resourcePath intact. # NEED_WORK show some indication that image was not accepted. instance.cfg['currentPath'] = appConfig.common['temasPath']+'inicio.png' # We do not DRAW the instance in this function, only configuring. ########################################################################### # TRY NEXT IMAGE ########################################################################### def tryNextImage(widget, customPath=None): ''' tryNextImage(widget, [customPath]): This function will test the validity of provided customPath, or if it's empty, will try to get a next image out of the resourcePath entry. If both fail, the nothing is changed and False is returned. Calling function should figure out what to do in that case. The new path, will be written into the related instance[widget].cfg[currentPath]. This function returns True if instance[widget].cfg[currentPath] WAS CHANGED. It returns False if customPath is rejected or there is a problem with pre-set path. RESULTING customPath MUST BE LOCAL PATH, NOT "file://" FORMAT, AS IT MAY CRASH SUPERKARAMBA. IF PATH IS NOT MADE LOCAL, YOUR IMAGES MAY BE !!!!! DELETED !!!!! IF SUPERKARAMBA CRASHES BEFORE TELLING THE SYSTEM D-N-D WAS A SUCCESS. ''' global appConfig, _ # for ease of use, lets make a shortcut to the settings dictionary instance = appConfig.instance[widget] if customPath: # if it is given, it is likely dropped or otherwise newlly entered RAW URL testPath = customPath else: testPath = instance.cfg['resourcePath'] # looking for string followed by :// if re.search('^\w+(?=://)', testPath): protocol = re.match('^\w+(?=://)', testPath).group(0) else: protocol = None # and we will be kicked out by the next IF because None is not a supported protocol. if debug(): print "tryNextImage: protocol and testpath: ", protocol, testPath # Normally we would need to convert dropped-in RAW, real urls into "internal" urls # so, in cases when customPath is given, we "convert" it, but, there are special cases # which are considered even before we start matching a url to a plugin. # special case scenarios: file://, or http:// ending in image extension. # these go directly to file and webcam plugins provided with afoto without asking any questions. # I chose these "priority" cases arbitrarily for full-proof operation, # fearing that bad filters of custom plugins would mistakenly detect these paths as theirs and greatly annoy the user. # Feel free to fold them into generic plugin detection agorythm... at your peril! Tam tam tam taaaammm! success = False if appConfig.plugins.has_key(protocol): # This is the right place to explain why we are # bothering with registering of custom url protocols like myfancyprotocol://dir/picture # If "Bad" plugin filters are used bellow, they may start detecting all # http://asdf/asdf URLs as theirs. # Asking user which of many aspiring plugins user wants to use is only nice when done # once per drop, not on every reload. if appConfig.plugins[protocol].tryNextImage(widget, instance, appConfig.common, customPath): success = True elif protocol == 'http' and appConfig.common['fileFilter'].search(testPath): # this is a special case, I am forcefully overstepping other plugins with this one. # for all paths with "http://" that end in image-like extension. # (see memo bellow) If custom plugin filters ARE sane, this special case will be removed. # or, I can implement some sort of priority ranking for plugins. customPath = appConfig.plugins['webimage'].translatePath(customPath) if appConfig.plugins['webimage'].tryNextImage(widget, instance, appConfig.common, customPath): success = True else: # In this part we try to use regular expression signatures provided by each plugin # to guess which plugin to use. # in other words, you will be here when user drops in a new path. # Lets see if we can detect the plugin with custom filters. # we can catch more than one plugin if the author gave a too simple filter. # so we will collect all of the matching ones, and if more than one, we ask user which he wants detected = [] for plugin in appConfig.plugins.keys(): if appConfig.plugins[plugin].filterObj.search(customPath): detected.append(plugin) if detected.__len__() >= 0: # TODO: here we should ask user which one of many cought protocols user wants. # but at this time just select the 1st. protocol = detected[0] customPath = appConfig.plugins[protocol].translatePath(customPath) if appConfig.plugins[protocol].tryNextImage(widget, instance, appConfig.common, customPath): success = True return success # ########################################################################### ########################################################################### # These just makes it easier to send messages through kdialog. # def notificationWindow(message, title="", mode=None): ''' This uses KDE kdialog app and asks for main body message and window title. App name is attached automatically ''' global appConfig myCommand = 'kdialog --caption "'+appConfig.common['name']+'" --title "'+title+'" --msgbox "'+message+'"' karamba.execute(myCommand) # ########################################################## ################## Config Menu Creations / Handling #################### # Config Menu: creation and handling of menu actions. # def createMenus(widget): ''' Creates menu that you get from right-clicking on the widget. This is part of layout rutine, include in karamba's init. ''' global appConfig, _ # lets make a shortcut for ease of use. menu = appConfig.instance[widget].menu # it's a dict type object now. # this is the old style menu karamba.addMenuConfigOption(widget, "about", _("About ")+appConfig.common['name']+" ...") karamba.setMenuConfigOption(widget, "about", 0) # These are alternatives to the traditional superkaramba "Configure Theme" menu. # These will work with Unicode in pre-0.40 versions of SuperKaramba (which has the unicode in menu fixed) menu['separator'] = [] menu['menu1'] = karamba.createMenu(widget) menu['view'] = karamba.addMenuItem(widget, menu['menu1'], _("Open image in viewer") , "viewmag") menu['explore'] = karamba.addMenuItem(widget, menu['menu1'], _("Show image location") , "bookmark_folder") menu['separator'].append(karamba.addMenuSeparator(widget, menu['menu1'])) menu['editpath'] = karamba.addMenuItem(widget, menu['menu1'], _("Change path...") , "editclear") menu['configure'] = karamba.addMenuItem(widget, menu['menu1'], _("Configure")+" "+appConfig.common['name'] , "configure") menu['separator'].append(karamba.addMenuSeparator(widget, menu['menu1'])) if appConfig.instance[widget].cfg['update']: menu['pause'] = karamba.addMenuItem(widget, menu['menu1'], _("Pause slideshow") , "player_pause") else: menu['pause'] = karamba.addMenuItem(widget, menu['menu1'], _("Resume slideshow") , "player_play") menu['menu2'] = karamba.createMenu(widget) menu['frame'] = karamba.addMenuItem(widget, menu['menu2'], _("Choose frame style...") , "frame_image") menu['shadowsize'] = karamba.addMenuItem(widget, menu['menu2'], _("Set shadow size...") , "transform") menu['maxsize'] = karamba.addMenuItem(widget, menu['menu2'], _("Set maximum photo size...") , "crop") menu['updateinterval'] = karamba.addMenuItem(widget, menu['menu2'], _("Set slideshow interval...") , "history") menu['save'] = karamba.addMenuItem(widget, menu['menu2'], _("Save settings"), "filesave") if appConfig.instance[widget].cfg['limitlong']: menu['limitlong'] = karamba.addMenuItem(widget, menu['menu2'], _("Limit by longer side") , "apply") else: menu['limitlong'] = karamba.addMenuItem(widget, menu['menu2'], _("Limit by longer side") , appConfig.common['temasPath']+"null.png") # Reading of old-style menu def menuOptionChanged(widget, key, value): "This function is called when right-click Karamba menu is clicked." global appConfig, _ if key=="about": notificationWindow(appConfig.common['name']+'\n'+appConfig.common['version']+'\n'+_('Created by')+':\n'+appConfig.common['author']+'\n'+_('For latest version search kde-look.org for ')+appConfig.common['name'], _("About ")+appConfig.common['name']) karamba.setMenuConfigOption(widget, "about", 0) # Reading of new-style menu def menuItemClicked(widget, menu, id): global appConfig, _ # creating a shortcut for ease of use. instance = appConfig.instance[widget] if (menu == instance.menu['menu1']): if(id == instance.menu['view']): # Open image in viewer karamba.execute('kfmclient exec "'+instance.cfg['currentPath']+'"') elif(id == instance.menu['explore']): # show location in file manager karamba.execute('konqueror --select "'+instance.cfg['currentPath']+'"') elif(id == instance.menu['configure']): # Configure Menu karamba.popupMenu(widget, instance.menu['menu2'], 5, 5) elif(id == instance.menu['editpath']): # Configure Menu ventana = ["kdialog", "--caption", appConfig.common['name'], "--title", _("Configure"), "--inputbox", _("Enter or paste in new path."),instance.cfg['resourcePath']] # there may be a unicode issue with the command above. instance.ptr['editpath'] = karamba.executeInteractive(widget, ventana) elif(id == instance.menu['pause']): if instance.cfg['update']: instance.pause(widget) elif (instance.cfg['update'] == 0) and (instance.cfg['currentPath'] != instance.cfg['resourcePath']): instance.unpause(widget) elif (menu == instance.menu['menu2']): if(id == instance.menu['frame']): # frame decor dialog ventana = ["kdialog", "--caption", appConfig.common['name'], "--title", _("Configure"), "--menu", _("Select new frame style")] # Creates a list of folders with temas inside. temasList = [] temp = os.listdir(appConfig.common['temasPath']) try: temp.sort() except: pass for i in range(len(temp)): if os.path.isdir(appConfig.common['temasPath']+temp[i]) and temp[i][0]!='.': temasList.append(temp[i]) temasList.append(temp[i]) # the 2nd time we do it for "kdialog --menu" which takes essentially a dict type as argument. instance.ptr['frame'] = karamba.executeInteractive(widget, ventana + temasList) elif(id == instance.menu['save']): # Save settings instance.cfg['locationX'], instance.cfg['locationY'] = karamba.getWidgetPosition(widget) instance.pushSettingsDown() appConfig.ini.save() elif(id == instance.menu['shadowsize']): # shadow width dialog ventana = ["kdialog", "--caption", appConfig.common['name'], "--title", _("Configure"), "--inputbox", _("Set the width of shadows (pixels)"),str(instance.cfg['shadowsize'])] instance.ptr['shadowsize'] = karamba.executeInteractive(widget, ventana) elif(id == instance.menu['maxsize']): # Maximum foto size dialog ventana = ["kdialog", "--caption", appConfig.common['name'], "--title", _("Configure"), "--inputbox", _("Set the maximum size for your picture (pixels)"),str(instance.cfg['maxsize'])] instance.ptr['maxsize'] = karamba.executeInteractive(widget, ventana) elif(id == instance.menu['updateinterval']): # update interval minutes = instance.cfg['updateinterval']/60000 if int(minutes) == float(minutes): minutes = str(int(minutes)) else: minutes = str(minutes) ventana = ["kdialog", "--caption", appConfig.common['name'], "--title", _("Configure"), "--inputbox", _("Set the refresh rate (minutes)"), minutes] instance.ptr['updateinterval'] = karamba.executeInteractive(widget, ventana) elif(id == instance.menu['limitlong']): # limit by longer / shorter side if instance.cfg['limitlong']: # if it's now True, change to False instance.cfg['limitlong'] = 0 karamba.removeMenuItem(widget, instance.menu['menu2'], instance.menu['limitlong']) instance.menu['limitlong'] = karamba.addMenuItem(widget, instance.menu['menu2'], _("Limit by longer side") , appConfig.common['temasPath']+"null.png") else: # if it's now False, change to True appConfig.instance[widget].cfg['limitlong'] = 1 karamba.removeMenuItem(widget, instance.menu['menu2'], instance.menu['limitlong']) instance.menu['limitlong'] = karamba.addMenuItem(widget, instance.menu['menu2'], _("Limit by longer side") , "apply") redrawObjects(widget) def commandOutput(widget, pid, output): ''' This gets called when a command you have executed with executeInteractive() outputs something to stdout. This way you can get the output of for example kdialog without freezing up the widget waiting for kdialog to end. widget = reference to your theme pid = process number of the program outputting output = the text the program outputted to stdout ''' global appConfig #print 'afoto.py commandOutput: output ', output # this portion is reqired for dcophelper thread processsor. try: if appConfig.dcophelper.__threads__.has_key(pid): appConfig.dcophelper.__threads__[pid].returned({'widget':widget, 'pid':pid, 'output':output}) except: pass # creating a shortcut for ease of use. if appConfig.common['cleanStart']: instance = appConfig.instance[widget] if (pid == instance.ptr['maxsize']) and (output != "\n"): instance.cfg['maxsize'] = int(output.replace('\n','')) redrawObjects(widget) instance.pushSettingsDown() appConfig.ini.save() instance.ptr['maxsize'] = None elif (pid == instance.ptr['shadowsize']) and (output != "\n"): instance.cfg['shadowsize'] = int(output.replace('\n','')) loadTema(widget) redrawObjects(widget) instance.pushSettingsDown() appConfig.ini.save() instance.ptr['shadowsize'] = None elif (pid == instance.ptr['editpath']) and (output != "\n"): itemDropped(widget, output.replace('\n','')) elif (pid == instance.ptr['updateinterval']) and (output != "\n"): instance.cfg['updateinterval'] = int(float(output.replace('\n',''))*60000) # we get answer in minutes instance.setNewInterval(widget) instance.pushSettingsDown() appConfig.ini.save() instance.ptr['updateinterval'] = None elif (pid == instance.ptr['frame']) and (output != "\n"): instance.cfg['frame'] = output.replace('\n','') loadTema(widget) redrawObjects(widget) instance.pushSettingsDown() appConfig.ini.save() instance.ptr['frame'] = None # ######################################################################## ################## LOAD TEMA IMAGES #################### # This function only loads the frame pieces. # This is done to separate (and not to waste cycles / harddrive time on) loading frame pieces every time a foto in slide show changes # This function need to have appConfig.tema already set to NEW theme. # This function also SETS "do not draw" flags to frame / shadow, when needed. def loadTema(widget): global appConfig lista = glob.glob( appConfig.common['temasPath'] + appConfig.instance[widget].cfg['frame'] + "/t_*.png") if len(lista) == 8: appConfig.instance[widget].flag['drawFrame'] = True for i in range(8): karamba.setImagePath(widget, appConfig.instance[widget].ptr['estilo'][i], appConfig.common['temasPath']+appConfig.instance[widget].cfg['frame']+"/t_"+appConfig.common['frameParts'][i]+".png") else: appConfig.instance[widget].flag['drawFrame'] = False for i in range(8): karamba.setImagePath(widget, appConfig.instance[widget].ptr['estilo'][i], appConfig.common['temasPath']+"null.png") lista = glob.glob( appConfig.common['temasPath'] + appConfig.instance[widget].cfg['frame'] + "/s_*.png") if appConfig.instance[widget].cfg['shadowsize'] > 0 and len(lista) == 8: appConfig.instance[widget].flag['drawShadow'] = True for i in range(8): karamba.setImagePath(widget, appConfig.instance[widget].ptr['sombra'][i], appConfig.common['temasPath']+appConfig.instance[widget].cfg['frame']+"/s_"+appConfig.common['frameParts'][i]+".png") else: appConfig.instance[widget].flag['drawShadow'] = False for i in range(8): karamba.setImagePath(widget, appConfig.instance[widget].ptr['sombra'][i], appConfig.common['temasPath']+"null.png") # #################################################### ################## LOAD FOTO AND CHANGE FRAME SIZE ACCORDIGNLY ############## # This function does NOT load the frame pieces or tests for their presence. # all it does is: changes foto, resizes it, and all ather EXISTING elements. # instance cfg should be set up and will be red from. # def redrawObjects(widget): global appConfig, _ # lets get our usuall shortcut going instance = appConfig.instance[widget] ######################################################################## # 1st step of smart relocation routine - getting the "center" stored # need to check if separate frame drowing will not brake this. # -0.25 is there to remove the rounding bias towards higher numbers # # widgetHeight and widgetWidth are hard to find out quickly, and impossible # to calculate once a new image is loaded. So, we are storing them in the # config over foto update cycles. We use that to calculate coordinates offset. ######################################################################## widgetX, widgetY = karamba.getWidgetPosition(widget) if debug(): print "redrawObjects: karamba reports x and y as ", widgetX, widgetY print "redrawObjects: temp x and y are ", instance.flag['locationXtemp'],instance.flag['locationYtemp'] print "redrawObjects: stored x and y are ", instance.cfg['locationX'],instance.cfg['locationY'] if widgetX == instance.flag['locationXtemp'] and widgetY == instance.flag['locationYtemp']: # this means edge-guard moved the position off center last time # and the user did NOT move the widget since then. # this means we need to fake saved, original, out-of-range coordinates widgetX = instance.cfg['locationX'] widgetY = instance.cfg['locationY'] # cleaning up. instance.flag['locationXtemp'] = None instance.flag['locationYtemp'] = None widgetX = widgetX + int(round((instance.cfg['width']/2)-0.25)) widgetY = widgetY + int(round((instance.cfg['height']/2)-0.25)) # Now we have coordinates of widget center ######################################################################## # Exif-based autorotation. # Part 1. Find out if new image needs other angle and pre-rotate the image. # Note, we are rotating IMAGE CANVAS, not particular image. # You can reload images in pre-rotated canvas all day, it will still be rotated. # in other words, New image does not reset the rotation angle. ######################################################################## # this is not used anywhere yet, but will be needed for translations eventually. menuoption = _("EXIF-based auto-rotation") newangle = 0 # rotate to 12 o'clock, if needed. flipDimensions = False if re.compile(".+\.(jpg|jpeg)$",re.IGNORECASE).search(instance.cfg['currentPath']): # ok, dealing with jpeg, lets see if we have EXIF info orientationFlag = getoutput('identify -format "orientation=%[EXIF:Orientation]" "'+ appConfig.encode(instance.cfg['currentPath'])+'"') if debug(): print "redrawObjects: this is Jpeg\nredrawObjects: rotation start, EXIF output is ", orientationFlag, type(instance.cfg['currentPath']) orientFilter = re.compile('(?<=orientation=)[68]{1}(?=\s*$)') if orientFilter.search(orientationFlag): # looking for 6, or 8 at the end of string, following "orientation=" orientationFlag = int(orientFilter.search(orientationFlag).group(0)) # now, it's just a number. 6, or 8. And we need it in degrees offset. newangle = (7-orientationFlag)*90 # 6 = 90 degree position, 8 = -90 (270) position flipDimensions = True if instance.flag['lastorientation'] != newangle: # we are going to load small image and rotate that to 0 degrees position, insted of large old image. # as of version 0.41 superkaramba used a real rotate algorythm which was very slow. # if superkaramba learns to "flip" images to 90, 180, 270, we will stop loading and rotating null. karamba.setImagePath(widget, instance.ptr['foto'], appConfig.common['temasPath']+'null.png') karamba.rotateImage(widget, instance.ptr['foto'], newangle) instance.flag['lastorientation'] = newangle if debug(): print "redrawObjects: rotation end" ######################################################################## # Loading anctual image ######################################################################## # NEED_WORK # implement test of SUCCESSFULL loading of image. # will be important when start supporting network images. karamba.setImagePath(widget, instance.ptr['foto'], instance.cfg['currentPath']) # getting actual image size. fotoWidth,fotoHeight = karamba.getImageSize(widget,instance.ptr['foto']) if debug(): print "redrawObjects here: path, width, height ", instance.cfg['currentPath'], fotoWidth,fotoHeight ######################################################################## # EXIF-Based rotation # Part 2. we need to flip the image dimentions # We couldn't do it before because we didn't know the dimensions ######################################################################## if flipDimensions: fotoWidth, fotoHeight = fotoHeight, fotoWidth # the end of rotation. if debug(): print "redrawObjects: resizing image start" ######################################################################## # Resizing ######################################################################## if fotoWidth and fotoHeight: # if there was a loading error sizes are 0s # for the following to work we need "from __future__ import division" ratio = fotoHeight / fotoWidth # if > 1, tall, is < 1 wide # if this is not logical, try simplifying all scenarios and you will get this. # For fun bonus point, it is possible simplify it further through use of bool(ratio) and limitlong as powers for ratio. # Try it. But, then, "free rides" are tougher to isolate. if bool(int(ratio)) == instance.cfg['limitlong']: # 0 = 0, and 1 = 1 cases if fotoHeight > instance.cfg['maxsize']: # if not, no need to resize - free ride. fotoHeight = instance.cfg['maxsize'] fotoWidth = int(instance.cfg['maxsize'] / ratio) karamba.resizeImageSmooth(widget, instance.ptr['foto'], fotoWidth, fotoHeight) else: # The free rides, 1 = 0, and 0 = 1 case if fotoWidth > instance.cfg['maxsize']: # if not, no need to resize - free ride. fotoHeight = int(instance.cfg['maxsize'] * ratio) fotoWidth = instance.cfg['maxsize'] karamba.resizeImageSmooth(widget, instance.ptr['foto'], fotoWidth, fotoHeight) if debug(): print "redrawObjects: resizing image end\nredrawObjects: new fotoWidth, fotoHeight, ratio ", fotoWidth, fotoHeight,ratio widgetWidth = fotoWidth widgetHeight = fotoHeight # Now we have "tweaked" fotoWidth and fotoHeight and can use them for other things. if debug(): print "redrawObjects: resizing frame start" # RESIZING FRAME PIECES width0, height0, width7, height7 = 0,0,0,0 # these relate to frame pieces 0 upper left, and 7 lower right if instance.flag['drawFrame'] == True: # we will use the corner pieces as guides for all other sizes. # (we need 2 corners because one will not work for "non-square" frames like "a-foto") # piece 0 width0,height0 = karamba.getImageSize(widget,instance.ptr['estilo'][0]) # upper left corner piece # and piece 7 width7,height7 = karamba.getImageSize(widget,instance.ptr['estilo'][7]) # lower right corner piece # piece 2 is unchanged # piece 5 is unchanged # we are only changing the long frame pieces karamba.resizeImageSmooth(widget, instance.ptr['estilo'][1], fotoWidth, height0) karamba.resizeImageSmooth(widget, instance.ptr['estilo'][3], width0, fotoHeight) karamba.resizeImageSmooth(widget, instance.ptr['estilo'][4], width7, fotoHeight) karamba.resizeImageSmooth(widget, instance.ptr['estilo'][6], fotoWidth, height7) widgetWidth = widgetWidth+width0+width7 widgetHeight = widgetHeight+height0+height7 # RESIZING SHADOW PIECES if instance.flag['drawShadow'] == True: karamba.resizeImageSmooth(widget, instance.ptr['sombra'][0], instance.cfg['shadowsize'], instance.cfg['shadowsize']) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][2], instance.cfg['shadowsize'], instance.cfg['shadowsize']) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][5], instance.cfg['shadowsize'], instance.cfg['shadowsize']) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][7], instance.cfg['shadowsize'], instance.cfg['shadowsize']) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][1], widgetWidth, instance.cfg['shadowsize']) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][3], instance.cfg['shadowsize'], widgetHeight) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][4], instance.cfg['shadowsize'], widgetHeight) karamba.resizeImageSmooth(widget, instance.ptr['sombra'][6], widgetWidth, instance.cfg['shadowsize']) widgetWidth = widgetWidth+instance.cfg['shadowsize']*2 widgetHeight = widgetHeight+instance.cfg['shadowsize']*2 if debug(): print "redrawObjects: resizing frame end" # RESIZING THE WIDGET karamba.resizeWidget(widget, widgetWidth, widgetHeight) ######################################################################## # MOVING ALL THE PIECES ######################################################################## if debug(): print "redrawObjects: moving pieces start" # we need this when tema contains NO files for shadow. if instance.flag['drawShadow'] == False: shadowsize = 0 else: shadowsize = instance.cfg['shadowsize'] # MOVING FOTO karamba.moveImage(widget,instance.ptr['foto'], width0+shadowsize, height0+shadowsize) # it has to be here because moving resets Tips karamba.addImageTooltip(widget, instance.ptr['foto'], _("- Drop image file or folder here.\n- (Double-)Click for settings.")) # MOVING FRAME PIECES if instance.flag['drawFrame']: karamba.moveImage(widget,instance.ptr['estilo'][0], 0+shadowsize, 0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][1], width0+shadowsize, 0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][2], fotoWidth+width0+shadowsize, 0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][3], 0+shadowsize, height0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][4], fotoWidth+width0+shadowsize, height0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][5], 0+shadowsize, fotoHeight+height0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][6], width0+shadowsize, fotoHeight+height0+shadowsize) karamba.moveImage(widget,instance.ptr['estilo'][7], fotoWidth+width0+shadowsize, fotoHeight+height0+shadowsize) # MOVING SHADOW PIECES if instance.flag['drawShadow']: karamba.moveImage(widget,instance.ptr['sombra'][0], 0, 0) karamba.moveImage(widget,instance.ptr['sombra'][1], shadowsize, 0) karamba.moveImage(widget,instance.ptr['sombra'][2], width7+fotoWidth+width0+shadowsize, 0) karamba.moveImage(widget,instance.ptr['sombra'][3], 0, shadowsize) karamba.moveImage(widget,instance.ptr['sombra'][4], width7+fotoWidth+width0+shadowsize, shadowsize) karamba.moveImage(widget,instance.ptr['sombra'][5], 0, height7+fotoHeight+height0+shadowsize) karamba.moveImage(widget,instance.ptr['sombra'][6], shadowsize, height7+fotoHeight+height0+shadowsize) karamba.moveImage(widget,instance.ptr['sombra'][7], width7+fotoWidth+width0+shadowsize, height7+fotoHeight+height0+shadowsize) if debug(): print "redrawObjects: moving pieces end" ######################################################################## # 2nd step of smart relocation routine - getting new "center" recalculated and applied ######################################################################## if instance.cfg['width'] and instance.cfg['height']: if debug(): print "redrawObjects: moving widget to new place" widgetX = widgetX - int(round((widgetWidth/2)-0.25)) widgetY = widgetY - int(round((widgetHeight/2)-0.25)) instance.cfg['locationX'] = widgetX # these two are saved here becase widgetClosed() gives me grief. instance.cfg['locationY'] = widgetY # so, now I am forced to save it every cycle. # EDGE-GUARD - staying away from edges. # now, we have what is supposed to be the new top, left corner. # but, we neet to adjust for the screen edges. maxX, maxY = screenDimensions(widget) x = limit(widgetX, (maxX-widgetWidth)) y = limit(widgetY, (maxY-widgetHeight)) if debug(): print "X: Max is %s, widget is %s, limited is %s" % (maxX, widgetX, x) print "Y: Max is %s, widget is %s, limited is %s" % (maxY, widgetY, y) if widgetX != x or widgetY != y: # this pair is adjusted, but NOT saved to settings. # we only use these here for moveWidget # the saved location contains "ideal" location. widgetX = x widgetY = y # this pair will behave as a flag for beginning of # next redrawObjects to compare to location. # this is where we "save" the "actual" location. instance.flag['locationXtemp'] = x instance.flag['locationYtemp'] = y karamba.moveWidget(widget, widgetX, widgetY) instance.cfg['width'] = widgetWidth instance.cfg['height'] = widgetHeight instance.pushSettingsDown() appConfig.ini.save() # finally, redraw karamba.redrawWidget(widget) if debug(): print "redrawObjects: checking location vars ", instance.cfg['locationX'], instance.cfg['locationY'], instance.cfg['width'], instance.cfg['height'] print "redrawObjects: actual location reported is ", karamba.getWidgetPosition(widget) print "redrawObjects: Finished loading foto.\n\n" ################################################################## # Testing & helper functions. ################################################################## def limit(x, maxim): ''' Used for limiting any integer number, even negative, to the range between 0 and maxim. Used by edge-guard algorythm. ''' # if you need nonzero start of the range, offset both x and maxim by range-start # then add that back in to the answer. step = maxim-((x+x.__abs__())/2) x = maxim - (step+step.__abs__())/2 return int(x) def screenDimensions(widget): global appConfig if appConfig.instance[widget].flag.has_key('dimensions'): maxX, maxY = appConfig.instance[widget].flag['dimensions'] else: ret = getoutput('xdpyinfo') schobj = re.compile('(?<=dimensions:)[ ]+\d+x\d+') maxX, maxY = schobj.search(ret).group().strip().split('x') maxX = int(maxX) maxY = int(maxY) appConfig.instance[widget].flag['dimensions'] = (maxX, maxY) return maxX, maxY def kde_uses_dcop(): #import re #from commands import getoutput a = "(?<=SuperKaramba:) *\d\.\d" ret = getoutput('superkaramba --version') try: skver = float(re.search(a, ret).group().strip()) except: skver = 100 if skver < 0.5: return True else: return False def file_based_test(homepath): cleanStart = False if not os.path.exists(homepath+'running'): cleanStart = True else: # another instance is already running - abort. message = "You may see this message for one of two reasons:\nA-Foto is already running and you are trying to start another A-Foto instance in an incompatible way, or\nSuperKaramba crashed and left some of AFoto pieces around.\nPress CANCEL to stop loading this copy of A-Foto. Use main SuperKaramba interface to start additional A-Foto instances.\nPress CONTINUE If you are sure there are no other running A-Foto instances." myCommand = 'kdialog --caption "'+appConfig.common['name']+'" --title "Error" --warningcontinuecancel "'+message+'" ; echo $?' if not int(getoutput(myCommand)): # Cancel Button = code 2. Continue Button = code 0 cleanStart = True return cleanStart def status(widget, text): try: meter = karamba.getThemeText(widget, "message") karamba.changeText(widget, meter, text) karamba.redrawWidget(widget) except: print "Status Report: ", text ################################################################## # KARAMBA's built-in functions ################################################################## #this is called when you widget is initialized def initWidget(widget): global appConfig try: a = appConfig.common['name'] # print "initWidget: appConfig already exists. Attaching." # if this works, we are already running in the same thread as # previous instance. We can talk to them just fine # through global vars and functions, no need for common setup except: # this is really 1st time, so... appConfig=configClass() if kde_uses_dcop() and not appConfig.common['cleanStart']: # supporting DCOP AND this is the 1st running instance. print "Using DCOP to test for presense of other AFoto instances." status(widget, "Using DCOP-based IPC") import dcophelper appConfig.using_ipc = True # this has to happen for commandOutput to identify us. # note that I pass the function name WITHOUT () appConfig.dcophelper = dcophelper.dcophelper(widget, initWidget2) elif not appConfig.common['cleanStart']: # no DCOP support and this is 1st runnging instance print "Using file to test for presense of other AFoto instances." status(widget, "Using file-based IPC") appConfig.using_ipc = False initWidget2(widget, {'flag':'done'}) else: # this is a child instance. initWidget2(widget, {'flag':'skip full setup'}) def initWidget2(widget, args): global appConfig ''' Args - a dict type with possible following keys: 'themenameunique' 'themename' 'themelist' 'homethread' 'competeThread' 'flag' ''' # print "initWidget2: Flag is ", args['flag'] if args['flag'] == 'warning': print "Problem with DCOPHelper detected" if args.has_key('message'): status(widget, args['message']) else: status(widget, "Unknown error") #close this theme if args.has_key('message'): mycommand = 'kdialog --msgbox "'+args['message_long']+'"' karamba.execute(mycommand) if type(appConfig.dcophelper) != type(None): appConfig.dcophelper.closeTheme() elif args['flag'] == 'done': # This means this instance of a-foto is 1st in this SuperKaramba thread # and there is no other concurrent afoto running. status(widget, "Setting up the instance") appConfig.ini = localconf.cfgObject() if commonConfig(widget) and scanPlugins(widget): appConfig.common['cleanStart'] = True if appConfig.common['cleanStart']: # if this is another instance of an already running A-Foto, in the same SK thread, # we go straight to setting up the frame and picture. instanceConfig(widget) status(widget, "Instance was set up, Drawing") loadTema(widget) # loads the frame images. # removing the "welcome, loading" images. meter = karamba.getThemeImage(widget, "loading") karamba.deleteImage(widget,meter) meter = karamba.getThemeImage(widget, "status") karamba.deleteImage(widget,meter) meter = karamba.getThemeText(widget, "message") karamba.deleteText(widget,meter) redrawObjects(widget) # loads foto and arranges already loaded frame pieces around it. appConfig.ini.delayed_action() # it might have been set to "save" by tryNextImage(), and was set by sure by redrawObjects if widget location changed. karamba.acceptDrops(widget) else: # This is the place that gives you RED SAD / UNHAPPY FACE meter = karamba.getThemeImage(widget, "status") karamba.setImagePath(widget, meter, 'error.png') karamba.changeInterval(widget, 1000000) karamba.redrawWidget(widget) #this is called everytime your widget is updated #the update inverval is specified in the .instance file def widgetUpdated(widget): global appConfig if appConfig.common['cleanStart']: if appConfig.instance[widget].cfg['update']: if tryNextImage(widget): redrawObjects(widget) #This is called when your widget is closed. You can use this to clean #up open files, etc. You don't need to delete text and images in your #theme. That is done automatically. This callback is just for cleaning up #external things. Most people don't need to put anything here. def widgetClosed(widget): # we will only save widget position here. If there was a "widgetMoved" I would use that instead. # since the actual position of the widget is derivative from center of current image, we also need to save last width, height # (those are NOT saved in slideshow mode) We save those dimmensions as part of instance.pushSettingsDown() # INI file is opened and immediately closed on every write so we dont worry about closing it. # all other settings are saved when changed. if debug(): print "closing, widget number is ", widget try: if appConfig.common['cleanStart']: flag = True except: flag = False if flag: appConfig.instance[widget].pushSettingsDown() # As a curtesy, we also demote ourselves in the Order line. # demote returns the instance's new place in the Order array. # if it is 0 - we are the last running instance. # If you run 3 a-foto and exit SuperKaramba, you need to save settigs 3 times. # this way we only save once. # we could also just check if common['active'] == [], but it would not be as fun. # delayed action. This is to guard agains mass-closing # due to SK.quit() message = [ "appConfig.demoteInstance", appConfig.instance[widget].cfg['associatedName'], ] text = "afoto:action=%s&args=%s" % ('function', '+'.join(message)) t = Timer(2.0, karamba.callTheme, [widget, 'A-Foto', text]) t.start() if appConfig.instance.__len__() < 2 and not appConfig.using_ipc: os.remove(appConfig.common['homepath']+'running') del appConfig.instance[widget] def itemDropped(widget, dropText, x=0, y=0): global appConfig, _ if debug(): print "in the itemDrag dropText is ", dropText, type(dropText) if tryNextImage(widget, dropText): # returns True if anything was changed. redrawObjects(widget) appConfig.instance[widget].pushSettingsDown() appConfig.ini.save() else: # create some sort of feedback when image is not accepted pass #This gets called everytime our widget is clicked. #Notes: # widget = reference to our widget # x = x position (relative to our widget) # y = y position (relative to our widget) # botton = button clicked: # 1 = Left Mouse Button # 2 = Middle Mouse Button # 3 = Right Mouse Button, but this will never happen # because the right mouse button brings up the # Karamba menu. # 4,5 = Scroll wheel up and down def widgetClicked(widget, x, y, button): #btnPressed(widget, x, y, button) #pass global appConfig if(button == 1): karamba.popupMenu(widget, appConfig.instance[widget].menu['menu1'], x, y) elif(button == 2): karamba.popupMenu(widget, appConfig.instance[widget].menu['menu2'], x, y) #This gets called everytime mouse is moved over our widget. #Notes # widget = reference to our widget # x = x position (relative to our widget) # y = y position (relative to our widget) # botton = button being held: # 0 = No Mouse Button # 1 = Left Mouse Button # 2 = Middle Mouse Button # 3 = Right Mouse Button, but this will never happen # because the right mouse button brings up the # Karamba menu. def themeNotify(widget, caller, message): global appConfig # the following is a part of closing / saving settings scheme. # in cases when SK quits, all themes rush to close in disorder. # (well actually in order, but not in the one we need.) # by delaying the report that we are closing, we are # relying on themes that stay behind to save the order. # It they are around - the closing order is saved. # If noone is around to pick up the message, the closing was chaotic - no need to save. if message[:6] == 'afoto:': #example afoto:action=function&args=appConfig.demoteInstance+instance_2 message = message[6:].split('&') collection = {} for part in message: key, value = part.split('=') collection.update({key:value}) if collection['action'] == 'function': args = collection['args'].split('+') print args exec args[0]+'("'+args[1]+'")' # This will be printed when the widget loads. print "Afoto is loaded."