diff options
author | Martyn James Russell <mr@src.gnome.org> | 2008-09-26 10:47:33 +0000 |
---|---|---|
committer | Martyn James Russell <mr@src.gnome.org> | 2008-09-26 10:47:33 +0000 |
commit | 2f30be89ca554dc4639c7c8a6ee465274f52721a (patch) | |
tree | bb333517426d0d68cad130b34b8892a45ff7f54d /python | |
parent | caa9320c8ae7800acf2160701eb9284ac05e4eb9 (diff) | |
download | tracker-2f30be89ca554dc4639c7c8a6ee465274f52721a.tar.gz |
Merge indexer-split branch.
svn path=/trunk/; revision=2275
Diffstat (limited to 'python')
-rw-r--r-- | python/FUSE/trackerfs.py | 279 | ||||
-rw-r--r-- | python/Makefile.am | 4 | ||||
-rw-r--r-- | python/SearchTool/COPYING | 340 | ||||
-rw-r--r-- | python/SearchTool/README | 19 | ||||
-rw-r--r-- | python/SearchTool/mainform.py | 191 | ||||
-rwxr-xr-x | python/SearchTool/trackergui.py | 163 | ||||
-rwxr-xr-x | python/applet/applet.py | 131 | ||||
-rw-r--r-- | python/applet/applet.svg | 453 | ||||
-rw-r--r-- | python/deskbar-handler/Makefile.am | 19 | ||||
-rw-r--r-- | python/deskbar-handler/README | 0 | ||||
-rw-r--r-- | python/deskbar-handler/tracker-handler-static.py | 71 | ||||
-rw-r--r-- | python/deskbar-handler/tracker-handler.py | 402 | ||||
-rw-r--r-- | python/deskbar-handler/tracker-module.py | 492 | ||||
-rw-r--r-- | python/music/lyrics.py | 70 | ||||
-rw-r--r-- | python/nautilus/README | 15 | ||||
-rw-r--r-- | python/nautilus/tracker-tags-tab.py | 192 |
16 files changed, 2841 insertions, 0 deletions
diff --git a/python/FUSE/trackerfs.py b/python/FUSE/trackerfs.py new file mode 100644 index 000000000..74e585127 --- /dev/null +++ b/python/FUSE/trackerfs.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +# Created by Eugenio Cutolo me at eugesoftware dot com +# +# This program can be distributed under the terms of the GNU GPL. +# See the file COPYING. +# + +from fuse import Fuse +from optparse import OptionParser +from errno import * +from stat import * +import os,statvfs,logging,dbus,re + +#Simple class to interface with tracker dbus function +class TrackerClient: + + def __init__(self): + #Initialize dbus session and tracker interfaces + bus = dbus.SessionBus() + obj = bus.get_object('org.freedesktop.Tracker','/org/freedesktop/tracker') + + self.tracker_iface = dbus.Interface(obj, 'org.freedesktop.Tracker') + self.keywords_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Keywords') + self.search_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Search') + self.files_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Files') + + self.version = self.tracker_iface.GetVersion() + self.query_id = -1 + + def search(self,text,service='Files',offset=0,max_hits=-1): + self.resultlist = self.search_iface.Text(self.query_id, service, text, offset, max_hits) + self.resultlist = map(lambda x: str(x),self.resultlist) + + def search_by_tag(self,tag,service='Files',offset=0,max_hits=-1): + self.resultlist = self.keywords_iface.Search(self.query_id, service,tag,offset,max_hits) + self.resultlist = map(lambda x: str(x),self.resultlist) + + def add_db_files(self,path): + self.files_iface.Exists(path,True) + self.files_iface.Exists(path,False) + return True + + def add_tag(self,path,tags,service='Files'): + self.keywords_iface.Add(service,path,tags) + return + + #In future will be implemented live_query support + def on_tracker_reply(self, results): + print results + + def on_tracker_error(self, e): + print "Error:"+e + +#This class it's an extension of Fuse and TrackerClient +class TrackerFs (Fuse,TrackerClient): + + def __init__(self): + #Initialize tracker client + TrackerClient.__init__(self) + + #Shell inteface + usage = "usage: %prog mountpoint [options]" + + self.parser = OptionParser(usage) + self.parser.add_option("-a", "--auto",action="store_true",help="Mount point will be populated with the rdf query in ~/.Tracker/fs", dest="automatic", default=True) + self.parser.add_option("-s", "--search", dest="keys",help="Use a key to find the contents of mounted dir", metavar="key") + self.parser.add_option("-t", "--tag",dest="tag",help="Use a tag/s to find the contents of mounted dir", metavar="tag") + self.parser.add_option("-q", "--query",dest="query",help="Use a rdf file to find the contents of mounted dir", metavar="path") + self.parser.add_option("-v", "--verbose",action="store_true",help="Verbose the output", dest="verbose", default=False) + self.params, args = self.parser.parse_args() + + if os.path.exists(args[0]) == False: + print "The mount point doesen't exist make it?" + #self.log.debug("Create target directory") + os.mkdir(args[0]) + + #check old files + #save_dir = open(args[0], O_RDONLY); + #fchdir(save_dir); + #close(save_dir); + + #Init fuse + Fuse.__init__(self,args,{}) + + if self.params.verbose: + verbmode = logging.DEBUG + else: + verbmode = logging.WARNING + + #Setup logger + self.log = logging.getLogger("trackerfs");self.log.setLevel(verbmode) + fh = logging.StreamHandler();fh.setLevel(verbmode) + formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") + fh.setFormatter(formatter) + self.log.addHandler(fh) + + #This is the path when file will be "really" created/moved/edited + self.realdir = "/media/Dati/.data/" + self.rdfdir = os.environ["HOME"]+"/.Tracker/fs/" + + if os.path.exists(self.realdir) == False: + self.log.debug("Create target directory") + os.mkdir(self.realdir) + + self.log.debug("mountpoint: %s" % repr(self.mountpoint)) + + #Get list of rdf query + #files = os.listdir(self.rdfdir) + #self.queryfiles = [f for f in files if f[-4:]] + #print self.queryfiles + self._refresh_filelist() + pass + + def mythread(self): + self.log.debug("Start Thread") + + +#Refresh file list calling the TrackerClient function + def _refresh_filelist(self): + if self.params.tag != None: + self.log.debug("Use Tag") + self.search_by_tag(self.params.tag.split("+")) + #Command Line support for query soon will be move to d-bus + elif self.params.query != None: + self.log.debug("Use Query") + self.resultlist = os.popen("/usr/bin/tracker-query "+self.params.query,"r").readlines(); + self.resultlist = map(lambda x:(x.split(' : ')),self.resultlist) + self.resultlist = map(lambda x:(x[0].strip(' \t\r\n')),self.resultlist) + elif self.params.keys != None: + self.log.debug("Use Search") + self.search(self.params.keys)#.search,self.params.service) + else: + print 'You must specify an options' + return 0 + self.log.debug("Refresh Filelist") + + def _get_file_path(self,filename): + if os.path.dirname(filename) == "/": + for file in self.resultlist: + if filename== "/"+os.path.basename(file): + return file + else: + path = filename.split("/") + relpath = filename.replace("/"+path[1],"") + for file in self.resultlist: + if path[1] == os.path.basename(file): + return file+relpath + return filename + + def create_file_or_dir(self,type,path,mode): + if os.path.dirname(path) == "/" and self.params.tag != None: + newpath = self.realdir+os.path.basename(path) + elif os.path.dirname(path) != "/": + newpath = self._get_file_path(path) + else: + self.log.error("Fs based on Search and Query doesen't have write access") + return 0 + if S_ISREG(mode) and type == 0: + self.log.debug("MkFile:"+newpath) + res = os.open(newpath, os.O_CREAT | os.O_WRONLY,mode); + elif type == 1: + self.log.debug("MkDir:"+newpath) + res = os.mkdir(newpath,mode) + else: + return -EINVAL + print "path:"+os.path.dirname(path) + if os.path.dirname(path) == "/": + print "Add to fs" + self.add_to_fs(newpath) + self._refresh_filelist() + self.getdir("/") + return res + +#Add file/dir to the tracker database and tag it with used tag + def add_to_fs(self,path): + if self.add_db_files(path): + self.log.debug("Added "+os.path.basename(path)+" to tracker database") + self.add_tag(path,self.params.tag.split("+")) + self.log.debug("Added "+os.path.basename(path)+" to fs") + else: + self.log.debug("Falied to add "+path+"to tracker database") + + def getattr(self,path): + self.log.debug("GetAttr:"+self._get_file_path(path)) + return os.lstat(self._get_file_path(path)) + + def readlink(self, path): + self.log.debug("ReadLink:"+self._get_file_path(path)) + return os.readlink(self._get_file_path(path)) + + def getdir(self, path): + if path == "/": + self._refresh_filelist() + return map(lambda x: (os.path.basename(x),0),self.resultlist) + else: + return map(lambda x: (os.path.basename(x),0),os.listdir(self._get_file_path(path))) + + + def unlink(self, path): + return os.unlink(path) + + def rmdir(self, path): + return os.rmdir(path) + + def symlink(self, path, path1): + return os.symlink(path, path1) + + def rename(self, path, path1): + return os.rename(path, path1) + + def link(self, path, path1): + return os.link(path, path1) + + def chmod(self, path, mode): + return os.chmod(self._get_file_path(path), mode) + + def chown(self, path, user, group): + return os.chown(self._get_file_path(path), user, group) + + def truncate(self, path, size): + f = open(self._get_file_path(path), "w+") + return f.truncate(size) + + def mknod(self, path, mode, dev): + res = self.create_file_or_dir(0,path,mode) + + def mkdir(self, path, mode): + return self.create_file_or_dir(1,path,mode) + + def utime(self, path, times): + return os.utime(self._get_file_path(path), times) + + def open(self, path, flags): + path = self._get_file_path(path) + res = os.open(path,flags) + self.log.debug("open"+path) + os.close(res) + return 0 + + def read(self, path, length, offset): + path = self._get_file_path(path) + self.log.debug("read:"+ path) + f = open(self._get_file_path(path), "r") + f.seek(offset) + return f.read(length) + + def write(self, path, buf, off): + path = self._get_file_path(path) + self.log.debug(":write:"+path) + f = open(path, "w") + f.seek(off) + f.write(buf) + return len(buf) + return 0 + + def release(self, path, flags): + self.log.debug("release: %s %s" % (self._get_file_path(path), flags)) + return 0 + + def statfs(self): + self.sysconst = os.statvfs(self.realdir) + self.log.debug("statfs: returning user's home value ") + block_size = self.sysconst[statvfs.F_BSIZE] + blocks = self.sysconst[statvfs.F_BLOCKS] + blocks_free = self.sysconst[statvfs.F_BFREE] + blocks_avail = self.sysconst[statvfs.F_BAVAIL] + files = self.sysconst[statvfs.F_FILES] + files_free = self.sysconst[statvfs.F_FFREE] + namelen = self.sysconst[statvfs.F_NAMEMAX] + return (block_size, blocks, blocks_free, blocks_avail, files, files_free, namelen) + + def fsync(self, path, isfsyncfile): + self.log.debug("fsync: path=%s, isfsyncfile=%s" % (path, isfsyncfile)) + return 0 + +if __name__ == '__main__': + fs = TrackerFs() + fs.multithreaded = 0; + fs.main() diff --git a/python/Makefile.am b/python/Makefile.am new file mode 100644 index 000000000..2fd5b0a28 --- /dev/null +++ b/python/Makefile.am @@ -0,0 +1,4 @@ +include $(top_srcdir)/Makefile.decl + +SUBDIRS = deskbar-handler + diff --git a/python/SearchTool/COPYING b/python/SearchTool/COPYING new file mode 100644 index 000000000..5b6e7c66c --- /dev/null +++ b/python/SearchTool/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/python/SearchTool/README b/python/SearchTool/README new file mode 100644 index 000000000..c9eb381e3 --- /dev/null +++ b/python/SearchTool/README @@ -0,0 +1,19 @@ +TrackerQt it's a tracker front-end writtend with qt and python + +----------Requirements---------- +The qtgui requires recent versions of: +* Python (>= 2.3) +* Tracker(>= 5.2) +* Python Qt3 (can possibly use 3.16) +* Python Kde3 (>= 3.10) +* Python DBus + +If you use Ubuntu these can be installed via apt-get with this command: +sudo apt-get install python-qt3 python-kde3 python-dbus + +----------Installation----------- + +cp ./*.py /usr/local/bin + +----------Todo----------- +-Thumbail support in result list diff --git a/python/SearchTool/mainform.py b/python/SearchTool/mainform.py new file mode 100644 index 000000000..83fbe066a --- /dev/null +++ b/python/SearchTool/mainform.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# Created by Eugenio Cutolo <me@eugesoftware.com> using QtDesinger +# +# This program can be distributed under the terms of the GNU GPL. +# See the file COPYING. +# + +#QtTracker Relase 0.1 + +from qt import * + +class QRichListViewItem(QListViewItem): + def __init__(self,*args): + QListViewItem.__init__(self,*args) + self.richtext = 0 + self.indent = 0 + self.rText = QString() + self.recreateRichText(); + + def setText(self,column,text): + if column == 1: + self.rText = text + self.recreateRichText(); + else: + QListViewItem.setText(self,column,text) + + def recreateRichText(self): + if self.richtext != 0: + del(self.richtext) + self.richtext = 0 + self.richtext = QSimpleRichText("<small>"+self.rText+"</small>", self.listView().font()) + + def widthChanged(self,c): + if c == -1 or c == 1: + self.richtext.setWidth(self.listView().columnWidth(1)-15) + QListViewItem.widthChanged(self,c) + + def paintCell(self, p, cg, column, width, align): + if column == 1: + paper = QBrush() + + palette = self.listView().viewport().palette() + itemRectangle = self.listView().itemRect(self) + itemRectangle.setX(self.listView().columnWidth(0)) + self.listView().viewport().erase(itemRectangle) + colourGroup = QColorGroup(cg) + + if self.isSelected() == 1: + paper = QBrush(cg.highlight()) + else: + txtcolor = QColor(100,100,100) + colourGroup.setColor(QColorGroup.Text,txtcolor) + width = self.listView().width() - self.listView().columnWidth(0) + self.listView().setColumnWidth(1,width) + self.richtext.draw( p,self.listView().itemMargin(), 0, QRect( 0, 0, width, self.height() ), colourGroup, paper ); + self.widthChanged(1) + else: + QListViewItem.paintCell(self, p, cg, column,width, align) + +class MainForm(QDialog): + def __init__(self,parent = None,name = None,modal = 0,fl = 0): + QDialog.__init__(self,parent,name,modal,fl) + + if not name: + self.setName("MainForm") + + self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding,0,0,self.sizePolicy().hasHeightForWidth())) + self.setModal(1) + + MainFormLayout = QGridLayout(self,1,1,11,6,"MainFormLayout") + + self.mainframe = QFrame(self,"mainframe") + self.mainframe.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding,0,0,self.mainframe.sizePolicy().hasHeightForWidth())) + self.mainframe.setFrameShape(QFrame.StyledPanel) + self.mainframe.setFrameShadow(QFrame.Raised) + mainframeLayout = QGridLayout(self.mainframe,1,1,11,6,"mainframeLayout") + + self.nextbtn = QPushButton(self.mainframe,"nextbtn") + self.nextbtn.setEnabled(0) + + mainframeLayout.addWidget(self.nextbtn,2,5) + + self.prevbtn = QPushButton(self.mainframe,"prevbtn") + self.prevbtn.setEnabled(0) + + mainframeLayout.addWidget(self.prevbtn,2,4) + + self.mode_combo = QComboBox(0,self.mainframe,"mode_combo") + + mainframeLayout.addWidget(self.mode_combo,0,2) + + self.searchinp = QLineEdit(self.mainframe,"searchinp") + + mainframeLayout.addWidget(self.searchinp,0,1) + + self.pagen_display = QLabel(self.mainframe,"pagen_display") + + mainframeLayout.addMultiCellWidget(self.pagen_display,2,2,0,3) + + self.services_combo = QComboBox(0,self.mainframe,"services_combo") + + mainframeLayout.addMultiCellWidget(self.services_combo,0,0,3,4) + + self.findbtn = QPushButton(self.mainframe,"findbtn") + + mainframeLayout.addWidget(self.findbtn,0,5) + + self.textLabel1 = QLabel(self.mainframe,"textLabel1") + + mainframeLayout.addWidget(self.textLabel1,0,0) + + self.result_list = QListView(self.mainframe,"result_list") + self.result_list.addColumn(self.__tr("Info")) + self.result_list.header().setResizeEnabled(0,self.result_list.header().count() - 1) + self.result_list.addColumn(self.__tr("Text")) + self.result_list.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,QSizePolicy.Expanding,0,0,self.result_list.sizePolicy().hasHeightForWidth())) + self.result_list.setLineWidth(1) + self.result_list.setResizeMode(QListView.LastColumn) + self.result_list.setMargin(0) + self.result_list.setMidLineWidth(0) + self.result_list.setResizePolicy(QScrollView.Manual) + self.result_list.setHScrollBarMode(QListView.AlwaysOff) + self.result_list.header().hide() + self.result_list.setAllColumnsShowFocus(0) + self.result_list.setShowSortIndicator(0) + self.result_list.setItemMargin(5) + self.result_list.setRootIsDecorated(0) + + mainframeLayout.addMultiCellWidget(self.result_list,1,1,0,5) + + MainFormLayout.addWidget(self.mainframe,0,0) + + self.languageChange() + + self.resize(QSize(629,520).expandedTo(self.minimumSizeHint())) + self.clearWState(Qt.WState_Polished) + + self.connect(self.searchinp,SIGNAL("returnPressed()"),self.searchinp_returnPressed) + self.connect(self.services_combo,SIGNAL("activated(int)"),self.services_combo_textChanged) + self.connect(self.findbtn,SIGNAL("clicked()"),self.findbtn_clicked) + self.connect(self.nextbtn,SIGNAL("clicked()"),self.nextbtn_clicked) + self.connect(self.prevbtn,SIGNAL("clicked()"),self.prevbtn_clicked) + self.connect(self.result_list,SIGNAL("doubleClicked(QListViewItem*)"),self.result_list_doubleClicked) + self.connect(self.result_list,SIGNAL("contextMenuRequested(QListViewItem*,const QPoint&,int)"),self.result_list_contextMenuRequested) + + + def languageChange(self): + self.setCaption(self.__tr("Search Tool")) + self.nextbtn.setText(self.__tr("Next")) + self.prevbtn.setText(self.__tr("Previous")) + self.mode_combo.clear() + self.mode_combo.insertItem(self.__tr("key")) + self.mode_combo.insertItem(self.__tr("tag")) + self.pagen_display.setText(QString.null) + self.services_combo.clear() + self.services_combo.insertItem(self.__tr("All Files")) + self.services_combo.insertItem(self.__tr("Development")) + self.services_combo.insertItem(self.__tr("Documents")) + self.services_combo.insertItem(self.__tr("Images")) + self.services_combo.insertItem(self.__tr("Music")) + self.services_combo.insertItem(self.__tr("Plain Text")) + self.services_combo.insertItem(self.__tr("Videos")) + self.findbtn.setText(self.__tr("Find")) + self.textLabel1.setText(self.__tr("Search:")) + self.result_list.header().setLabel(0,self.__tr("Info")) + self.result_list.header().setLabel(1,self.__tr("Text")) + QToolTip.add(self.result_list,self.__tr("Lista Risultati")) + + def services_combo_textChanged(self,int): + self.findbtn_clicked() + + def findbtn_clicked(self): + print "MainForm.findbtn_clicked(): Not implemented yet" + + def prevbtn_clicked(self): + print "MainForm.prevbtn_clicked(): Not implemented yet" + + def nextbtn_clicked(self): + print "MainForm.nextbtn_clicked(): Not implemented yet" + + def result_list_doubleClicked(self,a0): + print "MainForm.result_list_doubleClicked(QListViewItem*): Not implemented yet" + + def result_list_contextMenuRequested(self,a0,a1,a2): + print "MainForm.result_list_contextMenuRequested(QListViewItem*,const QPoint&,int): Not implemented yet" + + def searchinp_returnPressed(self): + print "MainForm.searchinp_returnPressed(): Not implemented yet" + + def __tr(self,s,c = None): + return qApp.translate("MainForm",s,c) diff --git a/python/SearchTool/trackergui.py b/python/SearchTool/trackergui.py new file mode 100755 index 000000000..e7eebce55 --- /dev/null +++ b/python/SearchTool/trackergui.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# Created by Eugenio Cutolo <me@eugesoftware.com> +# +# This program can be distributed under the terms of the GNU GPL. +# See the file COPYING. +# + +#QtTracker Relase 0.1 + +import sys,os,dbus,re +from kdecore import KApplication, KCmdLineArgs, KURL,KIconTheme,KIcon +from kio import KMimeType, KServiceTypeProfile +from mainform import * + +class TrackerClient: + + def __init__(self): + bus = dbus.SessionBus() + obj = bus.get_object('org.freedesktop.Tracker','/org/freedesktop/tracker') + #self.tracker = dbus.Interface(obj, 'org.freedesktop.Tracker') + self.keywords_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Keywords') + self.search_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Search') + self.files_iface = dbus.Interface(obj, 'org.freedesktop.Tracker.Files') + + #self.version = self.tracker.GetVersion() + + #Self service eheheh + #self.services = self.tracker.GetServices(True) + self.services = ['Files','Development Files','Documents','Images','Music','Text Files','Videos'] + self.query_id = 0 + + def search(self,text,service='Files',offset=0,max_hits=-1): + self.returnedfiles = self.search_iface.TextDetailed(1,service,text,offset,max_hits) + if len(self.returnedfiles) > 0: + return self.returnedfiles + else: + self.on_tracker_error("Nothing files found") + return 0 + + def search_by_tag(self,tag,service='Files',offset=0,max_hits=-1): + self.returnedfiles = self.keywords_iface.Search(-1,service,[tag,] ,offset,max_hits) + output = [] + for path in self.returnedfiles: + output.append([path,self.files_iface.GetServiceType(path),'mime']) + self.returnedfiles = output + return output + +#Thank you Mikkel + def text_snippet(self, text, result, service='Files'): + snippet = self.search_iface.GetSnippet(service, result, text) + snippet = snippet.replace('<!--', '<!--').strip() + return snippet + + def on_tracker_error(self, e): + print "Error:",e + +class TrackerGui(MainForm,TrackerClient): + + def __init__(self): + MainForm.__init__(self) + TrackerClient.__init__(self) + self.searchinp.setFocus(); + self.fatalerr = 0 + self.mip = 6#Max item for page + + def refresh_page(self): + self.result_list.clear() + + start = (self.mip*self.pagen) + end = (self.mip*self.pagen)+self.mip + 1 + + self.setCursor(QCursor(3)) + if self.mode_combo.currentItem() == 0: + self.search(self.input,self.service,start,self.mip+1) + elif self.mode_combo.currentItem() == 1: + self.search_by_tag(self.input,self.service,start,self.mip+1) + + if self.fatalerr: + return + + if self.pagen > 0: + self.prevbtn.setEnabled(1) + elif self.pagen == 0: + self.prevbtn.setEnabled(0) + if len(self.returnedfiles) > self.mip: + self.nextbtn.setEnabled(1) + self.returnedfiles.pop() + else: + self.nextbtn.setEnabled(0) + + self.pagen_display.setText("Results "+str(start+1)+" - "+str(end-1)) + self.show_result(self.returnedfiles) + + def show_result(self,result): + self.result_list.setSorting(-1) + self.returnedfiles.reverse() + for (path,service,mime) in result: + item = QRichListViewItem(self.result_list,None) + item.setMultiLinesEnabled(1) + if len(os.path.dirname(path)) > 40: + dirname = os.path.dirname(path)[0:40]+"..." + else: + dirname = os.path.dirname(path) + item.setText(0,os.path.basename(path)+"\n"+dirname+"\n"+service) + item.setPixmap(0,self._get_iconc(path)) + item.setText(1,self.text_snippet(self.input,path)) + self.setCursor(QCursor(0)) + self.returnedfiles.reverse() + + def _get_iconc(self,path): + mobj = KMimeType.findByPath(path) + return mobj.pixmap(KURL(""),KIcon .Desktop,48) + + def on_tracker_error(self, e): + print "Error:",e + self.result_list.clear() + self.pagen_display.setText(e) + self.fatalerr = 1 + + def exec_file(self): + item = self.result_list.currentItem() + npos = (item.itemPos() / item.totalHeight()) + mime = KMimeType.findByPath(self.returnedfiles[npos][0]).name() + offer = KServiceTypeProfile.preferredService(mime, "Application") + offer = re.sub('%.','',str(offer.exec_ ())) + print offer+" "+self.returnedfiles[npos][0] + os.system(offer+" '"+self.returnedfiles[npos][0]+"' &") + +#----------------Qt Events---------------------------- + + def findbtn_clicked(self): + self.pagen = 0 + self.input = str(self.searchinp.text()) + self.service = self.services[self.services_combo.currentItem()] + self.prevbtn.setEnabled(0) + self.nextbtn.setEnabled(0) + self.refresh_page() + + def nextbtn_clicked(self): + self.pagen = self.pagen + 1 + self.refresh_page() + + def prevbtn_clicked(self): + self.pagen = self.pagen - 1 + self.refresh_page() + + def result_list_doubleClicked(self,item): + self.exec_file() + + def result_list_contextMenuRequested(self,item): + if item != self.result_list: + contextMenu = QPopupMenu(self) + contextMenu.insertItem( "&Open", self.exec_file, Qt.CTRL+Qt.Key_O ) + contextMenu.insertItem( "&Open with...", self.exec_file, Qt.CTRL+Qt.Key_S ) + contextMenu.exec_loop( QCursor.pos() ) + +if __name__ == "__main__": + KCmdLineArgs.init(sys.argv, "qttrackergui", "qtgui", "0.1") + app = KApplication() + gui = TrackerGui() + gui.show() + app.setMainWidget(gui) + app.exec_loop()
\ No newline at end of file diff --git a/python/applet/applet.py b/python/applet/applet.py new file mode 100755 index 000000000..c7d0360b7 --- /dev/null +++ b/python/applet/applet.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python + +import os +import gtk +import dbus +import gobject + +POPUP_TIMEOUT_MILLIS = 3000 +POLL_MILLIS = 5000 + +class Popup (gtk.Window): + def __init__ (self, status, widget): + gtk.Window.__init__ (self) + self.set_decorated (False) + self.set_skip_taskbar_hint (True) + self.set_skip_pager_hint (True) + self.set_keep_above (True) + self.set_resizable (False) + label = gtk.Label ("MetaTracker is " + status) + label.show () + ebox = gtk.EventBox () + ebox.set_visible_window (True) + ebox.set_above_child (True) + ebox.add (label) + ebox.modify_bg (gtk.STATE_NORMAL, gtk.gdk.Color (65535, 65535, 56576)) + ebox.set_border_width (1) + ebox.show () + self.add (ebox) + self.show () + scr, rect, orient = widget.get_geometry () + wdir = (rect.x > scr.get_width () / 2) and -1 or 1 + hdir = (rect.y > scr.get_height () / 2) and -1 or 1 + width, height = self.get_size () + self.modify_bg (gtk.STATE_NORMAL, gtk.gdk.Color (0,0,0)) + self.move (rect.x + (width / 2) * wdir, rect.y + (height / 2) * hdir) + #self.style.paint_flat_box(self.window, gtk.STATE_NORMAL, gtk.SHADOW_OUT, None, self, 'tooltip', 0, 0, width, height) + +class TrackerStatusIcon(gtk.StatusIcon): + def __init__(self): + gtk.StatusIcon.__init__(self) + menu = ''' + <ui> + <menubar name="Menubar"> + <menu action="Menu"> + <menuitem action="Search"/> + <menuitem action="Preferences"/> + <separator/> + <menuitem action="About"/> + <separator/> + <menuitem action="Quit"/> + </menu> + </menubar> + </ui> + ''' + actions = [ + ('Menu', None, 'Menu'), + ('Search', None, '_Search...', None, 'Search files with MetaTracker', self.on_activate), + ('Preferences', gtk.STOCK_PREFERENCES, '_Preferences...', None, 'Change MetaTracker preferences', self.on_preferences), + ('About', gtk.STOCK_ABOUT, '_About...', None, 'About MetaTracker', self.on_about), + ('Quit', gtk.STOCK_QUIT, '_Quit...', None, 'Quit Status Applet', gtk.main_quit)] + ag = gtk.ActionGroup('Actions') + ag.add_actions(actions) + self.manager = gtk.UIManager() + self.manager.insert_action_group(ag, 0) + self.manager.add_ui_from_string(menu) + self.menu = self.manager.get_widget('/Menubar/Menu/About').props.parent + search = self.manager.get_widget('/Menubar/Menu/Search') + search.get_children()[0].set_markup('<b>_Search...</b>') + search.get_children()[0].set_use_underline(True) + search.get_children()[0].set_use_markup(True) + pixbuf = gtk.gdk.pixbuf_new_from_file_at_size('applet.svg', 16, 16) + search.get_children()[1].set_from_pixbuf(pixbuf) + self.set_from_file('applet.svg') + self.set_tooltip('MetaTracker Desktop Search') + self.set_visible(True) + self.connect('activate', self.on_activate) + self.connect('popup-menu', self.on_popup_menu) + self.old_status = "" + self.connectToDBus () + gobject.timeout_add (0, self.check_tracker_state) + + def connectToDBus (self): + self.bus = dbus.SessionBus () + self.connectToTracker () + + def connectToTracker (self): + self.obj = self.bus.get_object('org.freedesktop.Tracker','/org/freedesktop/tracker') + self.tracker = dbus.Interface(self.obj, 'org.freedesktop.Tracker') + + def getTrackerStatus (self): + st = "" + try: st = str (self.tracker.GetStatus ()) + except: + st = "Unreachable!" + try: self.connectToTracker () + except: pass + return st + + def check_tracker_state (self): + stat = self.getTrackerStatus () + if stat != self.old_status: + p = Popup (stat, self) + gobject.timeout_add (POPUP_TIMEOUT_MILLIS, p.destroy) + self.old_status = stat + self.set_tooltip ("MetaTracker is " + stat) + gobject.timeout_add (POLL_MILLIS, self.check_tracker_state) + + def on_activate(self, data): + tst="tracker-search-tool" + os.spawnlp (os.P_NOWAIT, tst, tst) + + def on_popup_menu(self, status, button, time): + self.menu.popup(None, None, None, button, time) + + def on_preferences(self, data): + tp="tracker-preferences" + os.spawnlp (os.P_NOWAIT, tp, tp) + + def on_about(self, data): + dialog = gtk.AboutDialog() + dialog.set_name('MetaTracker') + dialog.set_version('0.6.2') + dialog.set_comments('A desktop indexing and search tool') + dialog.set_website('http://www.tracker-project.org/') + dialog.set_logo(gtk.gdk.pixbuf_new_from_file_at_size('applet.svg', 64, 64)) + dialog.run() + dialog.destroy() + +if __name__ == '__main__': + TrackerStatusIcon() + gtk.main() diff --git a/python/applet/applet.svg b/python/applet/applet.svg new file mode 100644 index 000000000..848121825 --- /dev/null +++ b/python/applet/applet.svg @@ -0,0 +1,453 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://web.resource.org/cc/" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + sodipodi:docname="system-search.svg" + sodipodi:docbase="/home/sciyoshi/Build/tracker" + inkscape:version="0.44" + sodipodi:version="0.32" + id="svg11300" + height="48px" + width="48px"> + <defs + id="defs3"> + <linearGradient + id="linearGradient2846"> + <stop + id="stop2848" + offset="0.0000000" + style="stop-color:#8a8a8a;stop-opacity:1.0000000;" /> + <stop + id="stop2850" + offset="1.0000000" + style="stop-color:#484848;stop-opacity:1.0000000;" /> + </linearGradient> + <linearGradient + id="linearGradient2366"> + <stop + id="stop2368" + offset="0" + style="stop-color:#ffffff;stop-opacity:1;" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.21904762;" + offset="0.50000000" + id="stop2374" /> + <stop + id="stop2370" + offset="1.0000000" + style="stop-color:#ffffff;stop-opacity:1.0000000;" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient4487"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4489" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop4491" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + id="linearGradient4477"> + <stop + style="stop-color:#000000;stop-opacity:1;" + offset="0" + id="stop4479" /> + <stop + style="stop-color:#000000;stop-opacity:0;" + offset="1" + id="stop4481" /> + </linearGradient> + <linearGradient + id="linearGradient4467"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop4469" /> + <stop + style="stop-color:#ffffff;stop-opacity:0.24761905;" + offset="1.0000000" + id="stop4471" /> + </linearGradient> + <linearGradient + id="linearGradient4454"> + <stop + style="stop-color:#729fcf;stop-opacity:0.20784314;" + offset="0.0000000" + id="stop4456" /> + <stop + style="stop-color:#729fcf;stop-opacity:0.67619050;" + offset="1.0000000" + id="stop4458" /> + </linearGradient> + <linearGradient + id="linearGradient4440"> + <stop + style="stop-color:#7d7d7d;stop-opacity:1;" + offset="0" + id="stop4442" /> + <stop + id="stop4448" + offset="0.50000000" + style="stop-color:#b1b1b1;stop-opacity:1.0000000;" /> + <stop + style="stop-color:#686868;stop-opacity:1.0000000;" + offset="1.0000000" + id="stop4444" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient4440" + id="linearGradient4446" + x1="30.656250" + y1="34.000000" + x2="33.218750" + y2="31.062500" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1.334593,0,0,1.291292,-6.973842,-7.460658)" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4454" + id="radialGradient4460" + cx="18.240929" + cy="21.817987" + fx="18.240929" + fy="21.817987" + r="8.3085051" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4467" + id="radialGradient4473" + cx="15.414371" + cy="13.078408" + fx="15.414371" + fy="13.078408" + r="6.6562500" + gradientTransform="matrix(2.592963,0,0,2.252104,-25.05975,-18.941)" + gradientUnits="userSpaceOnUse" /> + <radialGradient + inkscape:collect="always" + xlink:href="#linearGradient4487" + id="radialGradient4493" + cx="24.130018" + cy="37.967922" + fx="24.130018" + fy="37.967922" + r="16.528622" + gradientTransform="matrix(1,0,0,0.237968,0,28.93278)" + gradientUnits="userSpaceOnUse" /> + <linearGradient + gradientUnits="userSpaceOnUse" + y2="25.743469" + x2="17.500893" + y1="13.602121" + x1="18.292673" + id="linearGradient2372" + xlink:href="#linearGradient2366" + inkscape:collect="always" /> + <radialGradient + r="16.528622" + fy="37.967922" + fx="24.130018" + cy="37.967922" + cx="24.130018" + gradientTransform="matrix(1,0,0,0.237968,0,28.93278)" + gradientUnits="userSpaceOnUse" + id="radialGradient2842" + xlink:href="#linearGradient4477" + inkscape:collect="always" /> + <linearGradient + gradientUnits="userSpaceOnUse" + y2="30.557772" + x2="31.335964" + y1="26.580296" + x1="27.366341" + id="linearGradient2852" + xlink:href="#linearGradient2846" + inkscape:collect="always" /> + <linearGradient + gradientUnits="userSpaceOnUse" + y2="42.5" + x2="31.5" + y1="35.75" + x1="33" + id="linearGradient2246" + xlink:href="#linearGradient2240" + inkscape:collect="always" /> + <linearGradient + gradientUnits="userSpaceOnUse" + y2="42.5" + x2="31.5" + y1="35.75" + x1="33" + id="linearGradient2238" + xlink:href="#linearGradient2232" + inkscape:collect="always" /> + <radialGradient + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(-1.290127e-2,1.685197,1.713082,1.311475e-2,-1.041499,-10.11571)" + r="19.0625" + fy="11.132236" + fx="16.563837" + cy="11.132236" + cx="16.563837" + id="radialGradient4997" + xlink:href="#linearGradient4991" + inkscape:collect="always" /> + <linearGradient + y2="12.583769" + x2="12.624337" + y1="11.39502" + x1="17.060806" + gradientTransform="matrix(0,-1.171926,1.171926,0,1.782801,54.10111)" + gradientUnits="userSpaceOnUse" + id="linearGradient1764" + xlink:href="#linearGradient2187" + inkscape:collect="always" /> + <linearGradient + inkscape:collect="always" + id="linearGradient2187"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop2189" /> + <stop + style="stop-color:#ffffff;stop-opacity:0;" + offset="1" + id="stop2191" /> + </linearGradient> + <radialGradient + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(1,0,0,0.536723,0,16.87306)" + r="15.644737" + fy="36.421127" + fx="24.837126" + cy="36.421127" + cx="24.837126" + id="radialGradient8668" + xlink:href="#linearGradient8662" + inkscape:collect="always" /> + <linearGradient + id="linearGradient8662" + inkscape:collect="always"> + <stop + id="stop8664" + offset="0" + style="stop-color:#000000;stop-opacity:1;" /> + <stop + id="stop8666" + offset="1" + style="stop-color:#000000;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient4991" + inkscape:collect="always"> + <stop + id="stop4993" + offset="0" + style="stop-color:#ffffff;stop-opacity:1;" /> + <stop + id="stop4995" + offset="1" + style="stop-color:#ffffff;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient2232" + inkscape:collect="always"> + <stop + id="stop2234" + offset="0" + style="stop-color:#788600;stop-opacity:1;" /> + <stop + id="stop2236" + offset="1" + style="stop-color:#788600;stop-opacity:0;" /> + </linearGradient> + <linearGradient + id="linearGradient2240" + inkscape:collect="always"> + <stop + id="stop2242" + offset="0" + style="stop-color:#99b00b;stop-opacity:1;" /> + <stop + id="stop2244" + offset="1" + style="stop-color:#99b00b;stop-opacity:0;" /> + </linearGradient> + </defs> + <sodipodi:namedview + stroke="#3465a4" + inkscape:window-y="0" + inkscape:window-x="0" + inkscape:window-height="754" + inkscape:window-width="1280" + inkscape:showpageshadow="false" + inkscape:document-units="px" + inkscape:grid-bbox="true" + showgrid="false" + inkscape:current-layer="layer1" + inkscape:cy="23.756772" + inkscape:cx="25.937787" + inkscape:zoom="15.030623" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="0.25490196" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" + fill="#729fcf" /> + <metadata + id="metadata4"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:creator> + <cc:Agent> + <dc:title>Jakub Steiner</dc:title> + </cc:Agent> + </dc:creator> + <dc:source>http://jimmac.musichall.cz</dc:source> + <cc:license + rdf:resource="http://creativecommons.org/licenses/by-sa/2.0/" /> + </cc:Work> + <cc:License + rdf:about="http://creativecommons.org/licenses/by-sa/2.0/"> + <cc:permits + rdf:resource="http://web.resource.org/cc/Reproduction" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/Distribution" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/Notice" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/Attribution" /> + <cc:permits + rdf:resource="http://web.resource.org/cc/DerivativeWorks" /> + <cc:requires + rdf:resource="http://web.resource.org/cc/ShareAlike" /> + </cc:License> + </rdf:RDF> + </metadata> + <g + inkscape:groupmode="layer" + inkscape:label="Layer 1" + id="layer1"> + <g + id="g1772" + transform="matrix(0.826429,0,0,0.826429,8.249162,4.375344)"> + <path + sodipodi:type="arc" + style="opacity:0.17112301;color:black;fill:url(#radialGradient2842);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + id="path4475" + sodipodi:cx="24.130018" + sodipodi:cy="37.967922" + sodipodi:rx="16.528622" + sodipodi:ry="3.9332814" + d="M 40.65864 37.967922 A 16.528622 3.9332814 0 1 1 7.6013966,37.967922 A 16.528622 3.9332814 0 1 1 40.65864 37.967922 z" + transform="matrix(1.446431,0,0,1.51999,-10.97453,-17.75168)" /> + <path + sodipodi:nodetypes="csscccscccscczzzz" + id="path2844" + d="M 18.627569,3.1435548 C 10.488439,3.1435548 3.8827682,9.7492259 3.8827682,17.888356 C 3.8827682,26.027486 10.488439,32.633158 18.627569,32.633158 C 22.107124,32.633158 25.17857,31.248765 27.701292,29.230511 C 27.495915,30.237392 27.623257,31.265879 28.457436,31.990436 L 39.42152,41.517846 C 40.654936,42.589175 42.508982,42.448806 43.58031,41.215389 C 44.651638,39.981971 44.511269,38.127927 43.277853,37.056599 L 32.313769,27.529188 C 31.642242,26.945909 30.820891,26.773219 30.007531,26.886466 C 31.994231,24.374044 33.37237,21.337663 33.37237,17.888356 C 33.37237,9.7492259 26.766699,3.1435548 18.627569,3.1435548 z M 18.551954,4.3697381 C 26.191413,4.3697381 31.843729,9.1586886 31.843729,17.661513 C 31.843729,26.336626 26.027039,30.953288 18.551954,30.953288 C 11.249005,30.953288 5.2601806,25.475196 5.2601806,17.661513 C 5.2601806,9.6774061 11.084819,4.369738 18.551954,4.3697381 z " + style="opacity:1;color:black;fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2852);stroke-width:2.00000095;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + <path + style="opacity:1;color:black;fill:#dcdcdc;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.00000036;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 18.602905,3.0803551 C 10.437465,3.0803551 3.8104408,9.7073791 3.8104408,17.872819 C 3.8104408,26.038259 10.437465,32.665283 18.602905,32.665283 C 22.093708,32.665283 25.175082,31.276416 27.70596,29.251638 C 27.499919,30.261774 27.627672,31.293585 28.464547,32.020484 L 39.464073,41.578691 C 40.701476,42.653483 42.561515,42.512661 43.636306,41.275256 C 44.711097,40.037852 44.570274,38.177814 43.332871,37.103023 L 32.333346,27.544815 C 31.659648,26.959651 30.835642,26.786402 30.019653,26.900016 C 32.012775,24.379472 33.395369,21.333276 33.395369,17.872819 C 33.395369,9.7073791 26.768345,3.0803551 18.602905,3.0803551 z M 18.527046,6.2664243 C 24.808154,6.2664245 29.905864,11.364135 29.905864,17.645243 C 29.905864,23.926351 24.808154,29.024061 18.527046,29.024061 C 12.245938,29.024061 7.1482276,23.926351 7.1482276,17.645243 C 7.1482278,11.364135 12.245938,6.2664243 18.527046,6.2664243 z " + id="path4430" /> + <path + style="opacity:1;color:black;fill:url(#linearGradient4446);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 39.507004,41.57769 C 39.028332,39.304503 40.904334,36.766268 43.091057,36.789315 C 43.091057,36.789315 32.33069,27.531204 32.33069,27.531204 C 29.385899,27.474498 28.061188,29.80382 28.553876,32.131126 L 39.507004,41.57769 z " + id="path4438" + sodipodi:nodetypes="ccccc" /> + <path + sodipodi:type="arc" + style="opacity:1;color:black;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2372);stroke-width:0.8027336;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + id="path4450" + sodipodi:cx="17.500893" + sodipodi:cy="18.920233" + sodipodi:rx="11.048544" + sodipodi:ry="11.048544" + d="M 28.549437 18.920233 A 11.048544 11.048544 0 1 1 6.4523487,18.920233 A 11.048544 11.048544 0 1 1 28.549437 18.920233 z" + transform="matrix(1.245743,0,0,1.245743,-3.425346,-6.177033)" /> + <path + transform="matrix(0.497764,0,0,0.609621,8.973526,15.61929)" + d="M 40.65864 37.967922 A 16.528622 3.9332814 0 1 1 7.6013966,37.967922 A 16.528622 3.9332814 0 1 1 40.65864 37.967922 z" + sodipodi:ry="3.9332814" + sodipodi:rx="16.528622" + sodipodi:cy="37.967922" + sodipodi:cx="24.130018" + id="path4485" + style="opacity:1;color:black;fill:url(#radialGradient4493);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + sodipodi:type="arc" /> + <rect + style="opacity:0.43315507;color:black;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:white;stroke-width:1.00003111;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + id="rect4495" + width="19.048439" + height="4.4404783" + x="40.373337" + y="0.14086054" + rx="2.1366608" + ry="1.8879365" + transform="matrix(0.752986,0.658037,-0.648902,0.760872,0,0)" /> + <path + sodipodi:type="arc" + style="color:black;fill:url(#radialGradient4460);fill-opacity:1;fill-rule:evenodd;stroke:#3063a3;stroke-width:0.71499395;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dashoffset:0;stroke-opacity:1;visibility:visible" + id="path4452" + sodipodi:cx="17.589281" + sodipodi:cy="18.478292" + sodipodi:rx="8.3085051" + sodipodi:ry="8.3085051" + d="M 25.897786 18.478292 A 8.3085051 8.3085051 0 1 1 9.280776,18.478292 A 8.3085051 8.3085051 0 1 1 25.897786 18.478292 z" + transform="matrix(1.398614,0,0,1.398614,-6.224338,-8.298958)" /> + <path + style="opacity:0.83422457;color:black;fill:url(#radialGradient4473);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + d="M 18.156915,7.3966938 C 12.949325,7.3966938 8.7323681,11.613651 8.7323681,16.821241 C 8.7323681,18.325216 9.1526753,19.709014 9.77954,20.971144 C 11.03192,21.432757 12.362297,21.746827 13.774307,21.746827 C 19.945262,21.746827 24.873589,16.88519 25.254413,10.809698 C 23.523449,8.7641668 21.044374,7.3966938 18.156915,7.3966938 z " + id="path4462" /> + </g> + <g + inkscape:label="Layer 1" + id="g2194" + transform="matrix(0.651446,0,0,0.651446,3.41175,11.0539)" + style="opacity:0.84795323"> + <path + sodipodi:type="arc" + style="opacity:0.14117647;color:black;fill:url(#radialGradient8668);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" + id="path8660" + sodipodi:cx="24.837126" + sodipodi:cy="36.421127" + sodipodi:rx="15.644737" + sodipodi:ry="8.3968935" + d="M 40.481863 36.421127 A 15.644737 8.3968935 0 1 1 9.1923885,36.421127 A 15.644737 8.3968935 0 1 1 40.481863 36.421127 z" + transform="matrix(1.489736,0,0,-1.001252,-12.64716,75.3126)" /> + <path + sodipodi:nodetypes="ccccccc" + id="path1432" + d="M 38.37476,45.034369 C -1.6510486,46.355509 4.6747954,12.29355 25.49479,12.49765 L 25.49479,3.1222396 L 42.143271,17.708819 L 25.49479,33.006349 C 25.49479,33.006349 25.49479,23.337969 25.49479,23.337969 C 11.43168,22.751999 7.3172614,44.770549 38.37476,45.034369 z " + style="opacity:1;color:black;fill:url(#linearGradient2246);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2238);stroke-width:1.00000012;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:block;overflow:visible" /> + <path + style="opacity:0.69886361;color:black;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient1764);stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:block;overflow:visible" + d="M 16.92492,39.315519 C 5.2018204,33.235892 8.7371274,13.087489 26.5085,13.549959 L 26.5085,5.4508678 C 26.5085,5.4508678 40.556238,17.714589 40.556238,17.714589 L 26.5085,30.658617 C 26.5085,30.658617 26.5085,22.380979 26.5085,22.380979 C 11.66865,22.032709 12.34859,35.138579 16.92492,39.315519 z " + id="path2177" + sodipodi:nodetypes="ccccccc" /> + <path + sodipodi:nodetypes="ccccccc" + id="path4989" + d="M 26.036989,4.5686095 L 36.723727,14.798241 C 29.786227,14.79824 32.036989,23.735424 25.911989,26.610424 L 25.974489,22.943609 C 10.786989,22.881109 11.661989,38.443609 22.724489,42.693609 C 3.6363414,37.811681 6.2869904,13.381109 25.911989,12.88111 L 26.036989,4.5686095 z " + style="opacity:0.49431817;color:black;fill:url(#radialGradient4997);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.9999997;stroke-linecap:butt;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:10;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;visibility:visible;display:inline;overflow:visible" /> + </g> + </g> +</svg> diff --git a/python/deskbar-handler/Makefile.am b/python/deskbar-handler/Makefile.am new file mode 100644 index 000000000..b5770bb4a --- /dev/null +++ b/python/deskbar-handler/Makefile.am @@ -0,0 +1,19 @@ +include $(top_srcdir)/Makefile.decl + +if USING_DESKBAR_HANDLER +handlerdir = $(DESKBAR_HANDLER_DIR) +handler_DATA = \ + tracker-handler.py \ + tracker-handler-static.py +endif + +if USING_DESKBAR_MODULE +moduledir = $(DESKBAR_MODULE_DIR) +module_DATA = tracker-module.py +endif + +EXTRA_DIST = \ + tracker-handler.py \ + tracker-handler-static.py \ + tracker-module.py \ + README diff --git a/python/deskbar-handler/README b/python/deskbar-handler/README new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/python/deskbar-handler/README diff --git a/python/deskbar-handler/tracker-handler-static.py b/python/deskbar-handler/tracker-handler-static.py new file mode 100644 index 000000000..5f797321e --- /dev/null +++ b/python/deskbar-handler/tracker-handler-static.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# This handler was originaly created by Mikkel Kamstrup (c) 2006 and updated by Eugenio Cutolo (eulin) +# +# The static search Handler was splitted to a separate file by Marcus Fritzsch +# +# This program can be distributed under the terms of the GNU GPL version 2 or later. +# See the file COPYING. +# + +import sys +import os.path +import gnome +import gobject + +import gettext +gettext.install('tracker') + +import deskbar.Handler +import deskbar.Match + + + + +class TrackerSearchToolMatch (deskbar.Match.Match): + + def __init__(self, backend, **args): + deskbar.Match.Match.__init__(self, backend, **args) + self._icon = deskbar.Utils.load_icon ('tracker') + + def action(self, text=None): + try: + gobject.spawn_async(['tracker-search-tool', self.name], flags=gobject.SPAWN_SEARCH_PATH) + except gobject.GError, e: + print >> sys.stderr, "*** Error when executing tracker-search-tool:", e + + def get_verb(self): + return _('Search for %s with Tracker Search Tool') % ('<b>%(name)s</b>') + + def get_category (self): + return 'actions' + + def get_hash (self, text=None): + return 'tst-more-hits-action-'+self.name + + + + +class TrackerSearchToolHandler(deskbar.Handler.Handler): + + def __init__(self): + deskbar.Handler.Handler.__init__(self, 'tracker') + + def query(self, query): + return [TrackerSearchToolMatch(self, name=query)] + + @staticmethod + def requirements (): + if deskbar.Utils.is_program_in_path ('tracker-search-tool'): + return (deskbar.Handler.HANDLER_IS_HAPPY, None, None) + return (deskbar.Handler.HANDLER_IS_NOT_APPLICABLE, 'tracker-search-tool seems not to be installed properly.', None) + + + + +HANDLERS = { + 'TrackerSearchToolHandler': { + 'name': 'Search for files using Tracker Search Tool', + 'description': _('Search all of your documents with Tracker Search Tool'), + 'requirements': TrackerSearchToolHandler.requirements, # XXX makes deskbar 2.18.1 not load the handler!! + }, +} diff --git a/python/deskbar-handler/tracker-handler.py b/python/deskbar-handler/tracker-handler.py new file mode 100644 index 000000000..3e386ac2f --- /dev/null +++ b/python/deskbar-handler/tracker-handler.py @@ -0,0 +1,402 @@ +# -*- coding: utf-8 -*- +# This handler was originaly created by Mikkel Kamstrup (c) 2006 and updated by Eugenio Cutolo (eulin) +# +# The handler was rewritten and splitted into live and static search by Marcus Fritzsch +# +# This program can be distributed under the terms of the GNU GPL version 2 or later. +# See the file COPYING. +# + +# Notes on URL escaping and quoting: +# * Fields displayed in the deskbar applet should be escaped +# * There _MUST_ be an unescaped 'uri' field in order to open it +# (see also 'escqped_uri' - this URI however should urllib quoted +# - Marcus Fritzsch, fritschy@googlemail.com, 2007-08-13 + +import re +import cgi +import sys +import os.path +import time +import urllib +import string +import gnome +import gobject + +import gettext +gettext.install('tracker') + +import deskbar, deskbar.Utils, deskbar.gnomedesktop +import deskbar.Handler +import deskbar.Match + +# For now description param it's not used +TYPES = { + 'Applications': { + 'description': (_('Launch %s (%s)') % ('<b>%(name)s</b>', '%(app_name)s')), + 'category': 'actions', + }, + + 'GaimConversations': { + 'description': (_('See %s conversation\n%s %s\nfrom %s') % ('<b>%(proto)s</b>', '%(channel)s', '<b>%(conv_to)s</b>', '<i>%(time)s</i>')), + 'category': 'conversations', + 'icon': 'stock_people', + }, + + 'Emails': { + 'description': (_('Email from %s') % '<i>%(publisher)s</i>' ) + '\n<b>%(title)s</b>', + 'category': 'emails', + 'action': { # more actions for different MUAs + 'key': 'mua', # see TrackerLiveSearchMatch.action for a demo + 'Evolution/Email': 'evolution %(uri)s', + 'Modest/Email': 'modest-open %(uri)s', + 'Thunderbird/Email': 'thunderbird -viewtracker %(uri)s', + 'KMail/Email': 'kmail --view %(uri)s', + }, + 'icon': 'stock_mail', + }, + + 'Music': { + 'description': _('Listen to music %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'music', + }, + + 'Documents': { + 'description': _('See document %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'documents', + }, + + 'Development': { + 'description': _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'develop', + }, + + 'Images': { + 'description': _('View image %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'images', + 'icon': 'image', + }, + + 'Videos': { + 'description': _('Watch video %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'videos', + 'icon': 'video', + }, + + 'Files': { + 'description': _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'files', + }, + + 'Folders': { + 'description': _('Open folder %s\n%s') % ('<b>%(name)s</b>', '<i>%(dir)s/%(name)s</i>'), + 'category': 'places', + 'icon': 'stock_folder', + }, +} + + + + +class TrackerLiveSearchMatch (deskbar.Match.Match): + + def __init__(self, handler,result=None, **args): + deskbar.Match.Match.__init__ (self, handler,name=result['name'], **args) + self.result = result + self.init_names(result['escaped_uri']) + + # Set the match icon + try: + self._icon = deskbar.Utils.load_icon(TYPES[result['type']]['icon']) + except: + if self.result.has_key ('icon'): + self._icon = deskbar.Utils.load_icon_for_desktop_icon (result ['icon']) + else: + self._icon = deskbar.Utils.load_icon_for_file(result['uri']) + + def get_name(self, text=None): + return self.result + + def get_verb(self): + try: + return TYPES[self.result['type']]['description'] + except: + return _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>') + + def get_hash(self, text=None): + if self.result ['type'] == 'Applications': + # return a name that matches the one returned by the Program handler of deskbar + return 'generic_' + self.result ['app_basename'] + return self.result['uri'] + + def action(self, text=None): + try: + if TYPES[self.result['type']].has_key('action'): + if isinstance (TYPES[self.result['type']]['action'], dict): + try: + key = TYPES[self.result['type']]['action']['key'] + cmd = TYPES[self.result['type']]['action'][self.result[key]] + except: + print >> sys.stderr, "Unknown action for URI %s (Error: %s)" % (self.result['uri'], sys.exc_info()[1]) + return + else: + cmd = TYPES[self.result['type']]['action'] + cmd = map(lambda arg : arg % self.result, cmd.split()) # we need this to handle spaces correctly + + print 'Opening Tracker hit with command:', cmd + try: + # deskbar >= 2.17 + deskbar.Utils.spawn_async(cmd) + except AttributeError: + # deskbar <= 2.16 + gobject.spawn_async(cmd, flags=gobject.SPAWN_SEARCH_PATH) + else: + if 'desktop' in self.result: + self.result['desktop'].launch([]) + else: + try: + # deskbar >= 2.17 + deskbar.Utils.url_show ('file://'+self.result['uri']) + except AttributeError: + gnome.url_show('file://'+self.result['uri']) + print 'Opening Tracker hit:', self.result['uri'] + except: + print >> sys.stderr, '*** Could not open URL %s: %s' % (self.result['uri'], sys.exc_info ()[1]) + + def get_category (self): + try: + return TYPES[self.result['type']]['category'] + except: + return 'files' + + def init_names (self, fullpath): + dirname, filename = os.path.split(fullpath) + if filename == '': #We had a trailing slash + dirname, filename = os.path.split(dirname) + + #Reverse-tilde-expansion + home = os.path.normpath(os.path.expanduser('~')) + regexp = re.compile(r'^%s(/|$)' % re.escape(home)) + dirname = re.sub(regexp, r'~\1', dirname) + + self.result['base'] = filename + self.result['dir'] = dirname + + + + +class TrackerLiveSearchHandler(deskbar.Handler.SignallingHandler): + + def __init__(self): + deskbar.Handler.SignallingHandler.__init__(self, 'tracker') + # initing on search request, see self.query + self.tracker = self.search_iface = self.keywords_iface = self.files_iface = None + self.set_delay (500) + self.conv_re = re.compile (r'^.*?/logs/([^/]+)/([^/]+)/([^/]+)/(.+?)\.(:?txt|html)$') # all, proto, account, to-whom, time + + def handle_email_hits (self, info, output): + if len (info) < 5: + print >> sys.stderr, "*** Hit for Service Emails had incomplete data, ignoring (%s)" % info[0] + return 0 + output['title'] = cgi.escape(info[3]) + output['publisher'] = cgi.escape(info[4]) + output['mua'] = info[2] + if output['mua'] == 'Thunderbird/Email': + output['uri'] = info[0] + return 1 + + def handle_conversation_hits (self, info, output): + m = self.conv_re.match (output['escaped_uri']) + output['channel']=_('with') + output['proto']=output['conv_from']=output['conv_to']=output['time']='' # XXX, never happened during tests + if m: + output['proto'] = cgi.escape (m.group (1)) + output['conv_from'] = urllib.unquote (cgi.escape (m.group (2))) + output['conv_to'] = urllib.unquote (cgi.escape (m.group (3))) + output['time'] = cgi.escape (time_from_purple_log (m.group (4))) + if output['conv_to'].endswith ('.chat'): + output['channel'] = _('in channel') + output['conv_to'] = output['conv_to'].replace ('.chat','') + if output['proto'] == 'irc': + nick_server = output['conv_from'].split ('@') + if len (nick_server) > 1: + output['conv_to'] = '%s on %s' % (output['conv_to'], nick_server[1]) + + def handle_application_hits (self, info, output): + # print info + # dbus.Array( + # [ + # dbus.String(u'/usr/share/applications/gksu.desktop'), # TrackerUri 0 + # dbus.String(u'Applications'), # TrackerType 1 + # dbus.String(u'Application'), # DesktopType 2 + # dbus.String(u'Root Terminal'), # DesktopName 3 + # dbus.String(u'gksu /usr/bin/x-terminal-emulator'), # DesktopExec 4 + # dbus.String(u'gksu-root-terminal') # DesktopIcon 5 + # ], + # signature=dbus.Signature('s')) + # Strip %U or whatever arguments in Exec field + if len (info) < 6: + print >> sys.stderr, "*** Hit for Service Applications had incomplete data, ignoring (%s)" % info[0] + return 0 + output['app_name'] = re.sub(r'%\w+', '', info [4]).strip () + output['app_basename'] = cgi.escape (os.path.basename (output['app_name'])) + output['app_name'] = cgi.escape (output['app_name']) + if output['app_basename'] == '': # strange // in app_name, e.g. nautilus burn:/// + output['app_basename'] = output['app_name'] + output['name'] = cgi.escape (info [3]) + output['icon'] = info [5] # no escaping, as it is not displayed as a string + + desktop = parse_desktop_file (info[0]) + if not desktop: + print >> sys.stderr, '*** Could not read .desktop file: %s' % info[0] + else: + output['desktop'] = desktop + return 1 + + def receive_hits (self, qstring, hits, max): + matches = [] + + for info in hits: + output = {} + + if len (info) < 2: + print >> sys.stderr, "*** Hit had incomplete data, ignoring" + continue + + info = [str (i) for i in info] + + output['escaped_uri'] = cgi.escape (info[0]) + output['uri'] = url_quote (info[0], ';?:@&=+$,./') + output['name'] = os.path.basename(output['escaped_uri']) + output['type'] = info[1] + + if not TYPES.has_key(output['type']): + output['type'] = 'Files' + + if output['type'] == 'Emails': + if not self.handle_email_hits (info, output): + continue + + elif output['type'] in ('GaimConversations', 'Conversations'): + if not self.handle_conversation_hits (info, output): + continue + + elif output['type'] == 'Applications': + if not self.handle_application_hits (info, output): + continue + + # applications are launched by .desktop file, if not readable: exclude + if output['type'] != 'Applications' or output.has_key ('desktop'): + matches.append(TrackerLiveSearchMatch (self, output)) + + if len (matches): + self.emit_query_ready(qstring, matches) + print 'Tracker response for query "%s" (service %s); %s hits returned, %s shown' % (qstring, hits[0][1], len(hits), len(matches)) + + def recieve_error (self, error): + print >> sys.stderr, '*** Tracker dbus error:', error + + def query (self, qstring, max): + if self.tracker: + try: self.tracker.GetStatus () + except: self.tracker = None # reconnect + if not self.tracker: + try: + print "Connecting to Tracker (first search or trackerd restarted)" + import dbus + bus = dbus.SessionBus() + self.tracker = bus.get_object('org.freedesktop.Tracker','/org/freedesktop/tracker') + self.search_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Search') + self.keywords_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Keywords') + self.files_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Files') + except: + print >> sys.stderr, '*** DBus connection to tracker failed, check your settings.' + return + for service in TYPES.iterkeys (): + self.search_iface.TextDetailed (-1, service, qstring, 0, max, \ + reply_handler = lambda hits: self.receive_hits (qstring, hits, max), \ + error_handler = self.recieve_error) + print 'Tracker query:', qstring + + @staticmethod + def requirements (): + try: + import dbus + try : + if getattr(dbus, 'version', (0,0,0)) >= (0,41,0): + import dbus.glib + + # Check that Tracker can be started via dbus activation, we will have trouble if it's not + bus = dbus.SessionBus() + proxy_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus') + activatables = dbus_iface.ListActivatableNames() + if not 'org.freedesktop.Tracker' in activatables: + return (deskbar.Handler.HANDLER_IS_NOT_APPLICABLE, 'Tracker is not activatable via dbus', None) + except: + return (deskbar.Handler.HANDLER_IS_NOT_APPLICABLE, 'Python dbus.glib bindings not found.', None) + return (deskbar.Handler.HANDLER_IS_HAPPY, None, None) + except: + return (deskbar.Handler.HANDLER_IS_NOT_APPLICABLE, 'Python dbus bindings not found.', None) + + + + +# this code is stolen from the programs handler of deskbar +def parse_desktop_file(desktop, only_if_visible=False): + try: + desktop = deskbar.gnomedesktop.item_new_from_file(desktop, deskbar.gnomedesktop.LOAD_ONLY_IF_EXISTS) + except Exception, e: + print 'Couldn\'t read desktop file:%s:%s' % (desktop, e) + return None + if desktop == None or desktop.get_entry_type() != deskbar.gnomedesktop.TYPE_APPLICATION: + return None + if only_if_visible and desktop.get_boolean(deskbar.gnomedesktop.KEY_NO_DISPLAY): + return None + return desktop + + + + +def time_from_purple_log (instr): + try: + if instr.find ('+') != -1: # new kind of log timestamp... + return time.strftime ('%c', time.strptime (re.sub (r'\+\d{4}', '', instr), '%Y-%m-%d.%H%M%S%Z')) + else: # ...from ancient times + return time.strftime ('%c', time.strptime (instr, '%Y-%m-%d.%H%M%S')) + except: + print >> sys.stderr, '*** time parsing for purple chat log failed: %s' % sys.exc_info ()[1] + return instr + + + + +def url_quote (instr, safe = '/'): + """A unicode capable quote, see http://bugs.python.org/issue1712522""" + return ''.join (map (lambda x: x in (safe+string.letters+string.digits) and x or ('%%%02X' % ord(x)), instr.encode ('utf-8'))) + + + + + +HANDLERS = { + 'TrackerLiveSearchHandler': { + 'name': 'Search for files using Tracker', + 'description': _('Search all of your documents, <b>as you type</b>'), + 'requirements': TrackerLiveSearchHandler.requirements, + 'categories': { + 'develop': { + 'name': _('Development Files'), + }, + 'music': { + 'name': _('Music'), + }, + 'images': { + 'name': _('Images'), + }, + 'videos': { + 'name': _('Videos'), + }, + }, + }, +} diff --git a/python/deskbar-handler/tracker-module.py b/python/deskbar-handler/tracker-module.py new file mode 100644 index 000000000..d4d71b111 --- /dev/null +++ b/python/deskbar-handler/tracker-module.py @@ -0,0 +1,492 @@ +# This deskbar module was ported from deskbar <= 2.18 handler by Marcus Fritzsch + +import gnome +import gobject +import re +import sys +import urllib +import string +import time +import cgi +import os.path +import deskbar +import deskbar.core.Utils +import deskbar.core.gnomedesktop +import deskbar.interfaces.Module +import deskbar.interfaces.Match +import deskbar.interfaces.Action +from deskbar.core.Utils import is_program_in_path, spawn_async +from deskbar.handlers.actions.OpenWithApplicationAction import \ + OpenWithApplicationAction +from deskbar.handlers.actions.OpenDesktopFileAction import \ + OpenDesktopFileAction +from deskbar.handlers.actions.ShowUrlAction import \ + ShowUrlAction +from deskbar.handlers.actions.ActionsFactory import \ + get_actions_for_uri + +import gettext +gettext.install('tracker') + + +MAX_RESULTS = 10 +HANDLERS = ['TrackerSearchToolHandler', 'TrackerLiveSearchHandler'] + + +class TrackerSearchToolMatch (deskbar.interfaces.Match): + + def __init__(self, **kwargs): + deskbar.interfaces.Match.__init__(self, **kwargs) + self.add_action (TrackerSearchToolAction (self.get_name ())) + self._pixbuf = deskbar.core.Utils.load_icon ('tracker') + + def get_hash (self, text=None): + return 'tst-more-hits-action-'+self.get_name () + + def get_category (self): + return 'actions' + + + + +class TrackerSearchToolAction (deskbar.interfaces.Action): + def __init__(self, name): + deskbar.interfaces.Action.__init__ (self, name) + self.name = name + + def activate(self, text=None): + try: + gobject.spawn_async(['tracker-search-tool', self.name], \ + flags=gobject.SPAWN_SEARCH_PATH) + except gobject.GError, e: + print >> sys.stderr, "*** Error when executing tracker-search-tool:", e + + def get_verb(self): + return _('Search for %s with Tracker Search Tool') % '<b>%(name)s</b>' + + def get_hash (self): + return 't-s-t:'+self.name + + def get_category (self): + return 'actions' + + + + +class TrackerSearchToolHandler(deskbar.interfaces.Module): + INFOS = { + 'icon': deskbar.core.Utils.load_icon ('tracker'), + 'name': _('Tracker Search'), + 'description': _('Search with Tracker Search Tool'), + 'version': '0.6.4', + } + + def __init__(self): + deskbar.interfaces.Module.__init__(self) + + def query(self, query): + self._emit_query_ready (query, [TrackerSearchToolMatch(name=query, priority=self.get_priority ())]) + + @staticmethod + def has_requirements (): + return is_program_in_path ('tracker-search-tool') + + + + +#For now description param it's not used +TYPES = { + 'Applications': { + 'description': (_('Launch %s (%s)') % ('<b>%(name)s</b>', '%(app_name)s') ), + 'category': 'actions', + }, + + 'GaimConversations': { + 'description': (_('See %s conversation\n%s %s\nfrom %s') % ('<b>%(proto)s</b>', '%(channel)s', '<b>%(conv_to)s</b>', '<i>%(time)s</i>')), + 'category': 'conversations', + }, + + 'Emails': { + 'description': (_('Email from %s') % '<i>%(publisher)s</i>' ) + '\n<b>%(title)s</b>', + 'category': 'emails', + 'action': { # more actions for different MUAs + 'key': 'mua', # see TrackerLiveSearchAction.action for a demo + 'Evolution/Email': 'evolution %(uri)s', + 'Modest/Email': 'modest-open %(uri)s', + 'Thunderbird/Email': 'thunderbird -viewtracker %(uri)s', + 'KMail/Email': 'kmail --view %(uri)s', + }, + }, + + 'Music': { + 'description': _('Listen to music %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'music', + #'icon': 'audio-x-generic', + }, + + 'Documents': { + 'description': _('See document %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'documents', + }, + + 'Development': { + 'description': _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'develop', + }, + + 'Images': { + 'description': _('View image %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'images', + 'icon': 'image', + }, + + 'Videos': { + 'description': _('Watch video %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'videos', + #'icon': 'video-x-generic', + }, + + 'Folders': { + 'description': _('Open folder %s\n%s') % ('<b>%(name)s</b>', '<i>%(dir)s/%(name)s</i>'), + 'category': 'places', + }, + + 'Files': { + 'description': _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>'), + 'category': 'files', + }, + + 'Extra': { + 'description': _('Search for %s with Tracker Search Tool') % ('<b>%(name)s</b>'), + }, +} + + + + +class TrackerLiveSearchMatch (deskbar.interfaces.Match): + + def __init__(self, result, **args): + deskbar.interfaces.Match.__init__ (self) + self.result = result + try: + desktop = result['desktop'] + del result['desktop'] + except: + desktop = None + + # Set the match icon + try: + self._pixbuf = deskbar.core.Utils.load_icon(TYPES[result['type']]['icon']) + except: + if self.result.has_key ('icon'): + self._pixbuf = deskbar.core.Utils.load_icon_for_desktop_icon (result ['icon']) + else: + if not self.result['type'] in ('GaimConversations', 'Emails'): + try: + self._pixbuf = deskbar.core.Utils.load_icon ('file://'+result['quoted_uri']) + except: + pass # some icons cannot be loaded... (e.g. for non existent file or illegal URI) + + self.add_action (TrackerLiveSearchAction (result, desktop)) + + # Add extra default actions where it makes sense + if not result['type'] in ["Emails", "Applications", "GaimConversations"]: + try: + self.add_all_actions (get_actions_for_uri(result['quoted_uri'])) + except: + print >> sys.stderr, "*** Error when adding all actions for hit %s: %s" % (self.result['uri'], sys.exc_info()[1]) + + def get_name (self, text = None): + return self.get_verb() % self.result + + def get_verb(self): + try: + return TYPES[self.result['type']]['description'] + except: + return _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>') + + def get_hash(self, text=None): + return self.result['uri'] + + def get_category (self): + try: + return TYPES[self.result['type']]['category'] + except: + return 'files' + + + + +class TrackerLiveSearchAction (deskbar.interfaces.Action): + + def __init__ (self, result, desktop): + deskbar.interfaces.Action (self) + self.name = result['name'] + self.desktop = desktop + self.result = result + self.init_names (result['uri']) + + def get_name(self, text=None): + return self.result + + def get_hash(self, text=None): + if self.result ['type'] == 'Applications': + # return a name that matches the one returned by the Program handler of deskbar + return 'generic_' + self.result ['app_basename'] + return self.result['uri'] + + def get_verb(self): + try: + return TYPES[self.result['type']]['description'] + except: + return _('Open file %s\nin %s') % ('<b>%(base)s</b>', '<i>%(dir)s</i>') + + def activate (self, text=None): + try: + if TYPES[self.result['type']].has_key('action'): + if isinstance (TYPES[self.result['type']]['action'], dict): + try: + key = TYPES[self.result['type']]['action']['key'] + cmd = TYPES[self.result['type']]['action'][self.result[key]] + except: + print >> sys.stderr, "Unknown action for URI %s (Error: %s)" % (self.result['uri'], sys.exc_info()[1]) + return + else: + cmd = TYPES[self.result['type']]['action'] + cmd = map(lambda arg : arg % self.result, cmd.split()) # we need this to handle spaces correctly + + print 'Opening Tracker hit with command:', cmd + deskbar.core.Utils.spawn_async(cmd) + else: + if self.desktop: + self.desktop.launch ([]) + else: + deskbar.core.Utils.url_show ('file://'+self.result['quoted_uri']) + print 'Opening Tracker hit:', self.result['quoted_uri'] + except: + print >> sys.stderr, "*** Could not activate Hit %s: %s" % (self.result['uri'], sys.exc_info()[1]) + + def init_names (self, fullpath): + dirname, filename = os.path.split(fullpath) + if filename == '': #We had a trailing slash + dirname, filename = os.path.split(dirname) + + #Reverse-tilde-expansion + home = os.path.normpath(os.path.expanduser('~')) + regexp = re.compile(r'^%s(/|$)' % re.escape(home)) + dirname = re.sub(regexp, r'~\1', dirname) + + self.result ['base'] = filename + self.result ['dir'] = dirname + + + + +class TrackerLiveSearchHandler(deskbar.interfaces.Module): + + INFOS = { + 'icon': deskbar.core.Utils.load_icon ('tracker'), + 'name': _('Tracker Live Search'), + 'description': _('Search with Tracker, as you type'), + 'version': '0.6.4', + 'categories': { + 'develop': { + 'name': _('Development Files'), + }, + 'music': { + 'name': _('Music'), + }, + 'images': { + 'name': _('Images'), + }, + 'videos': { + 'name': _('Videos'), + }, + }, + } + + @staticmethod + def has_prerequisites (): + try: + import dbus + try : + if getattr(dbus, 'version', (0,0,0)) >= (0,41,0): + import dbus.glib + + # Check that Tracker can be started via dbus activation, we will have trouble if it's not + bus = dbus.SessionBus() + proxy_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus') + dbus_iface = dbus.Interface(proxy_obj, 'org.freedesktop.DBus') + activatables = dbus_iface.ListActivatableNames() + if not 'org.freedesktop.Tracker' in activatables: + TrackerLiveSearchHandler.INSTRUCTIONS = ('Tracker is not activatable via dbus') + return False + except: + TrackerLiveSearchHandler.INSTRUCTIONS = ('Python dbus.glib bindings not found.') + return False + return True + except: + TrackerLiveSearchHandler.INSTRUCTIONS = ('Python dbus bindings not found.') + return False + + def __init__(self): + deskbar.interfaces.Module.__init__(self) + # initing on search request, see self.query + self.tracker = self.search_iface = self.keywords_iface = self.files_iface = None + self.conv_re = re.compile (r'^.*?/logs/([^/]+)/([^/]+)/([^/]+)/(.+?)\.(:?txt|html)$') # all, proto, account, to-whom, time + + def handle_email_hits (self, info, output): + if len (info) < 5: + print >> sys.stderr, "*** Hit for Service Emails had incomplete data, ignoring (%s)" % info[0] + return 0 + output['title'] = info[3] + output['publisher'] = info[4] + output['mua'] = info[2] + return 1 + + def handle_conversation_hits (self, info, output): + output ['uri'] = info [0] + m = self.conv_re.match (output['uri']) + output['channel']=_('with') + output['proto']=output['conv_from']=output['conv_to']=output['time']='' # XXX, never happened during tests + if m: + output['proto'] = m.group (1) + output['conv_from'] = urllib.unquote (m.group (2)) + output['conv_to'] = urllib.unquote (m.group (3)) + output['time'] = time_from_purple_log (m.group (4)) + if output['conv_to'].endswith ('.chat'): + output['channel'] = _('in channel') + output['conv_to'] = output['conv_to'].replace ('.chat','') + if output['proto'] == 'irc': + nick_server = output['conv_from'].split ('@') + if len (nick_server) > 1: + output['conv_to'] = '%s on %s' % (output['conv_to'], nick_server[1]) + return 1 + + def handle_application_hits (self, info, output): + # print info + # dbus.Array( + # [ + # dbus.String(u'/usr/share/applications/gksu.desktop'), # TrackerUri 0 + # dbus.String(u'Applications'), # TrackerType 1 + # dbus.String(u'Application'), # DesktopType 2 + # dbus.String(u'Root Terminal'), # DesktopName 3 + # dbus.String(u'gksu /usr/bin/x-terminal-emulator'), # DesktopExec 4 + # dbus.String(u'gksu-root-terminal') # DesktopIcon 5 + # ], + # signature=dbus.Signature('s')) + # Strip %U or whatever arguments in Exec field + if len (info) < 6: + print >> sys.stderr, "*** Hit for Service Applications had incomplete data, ignoring (%s)" % info[0] + return 0 + output['app_name'] = re.sub(r'%\w+', '', info [4]).strip () + output['app_basename'] = os.path.basename (output['app_name']) + output['app_name'] = output['app_name'] + if output['app_basename'] == '': # strange // in app_name, e.g. nautilus burn:/// + output['app_basename'] = output['app_name'] + output['name'] = info [3] + output['icon'] = info [5] + desktop = parse_desktop_file (output['uri']) + if desktop: + output['desktop'] = desktop + return 1 + + def recieve_hits (self, qstring, hits, max): + matches = [] + + for info in hits: + output = {} + + if len (info) < 2: + print >> sys.stderr, "*** Hit had incomplete data, ignoring" + continue + + info = [str (i) for i in info] + + output['uri'] = info[0] + output['name'] = os.path.basename(output['uri']) + output['type'] = info[1] + output['quoted_uri'] = url_quote (info[0], ';?@&=+$,./') + + if not TYPES.has_key(output['type']): + output['type'] = 'Files' + + if output['type'] == 'Emails': + if not self.handle_email_hits (info, output): + continue + + elif output['type'] == 'GaimConversations': + if not self.handle_conversation_hits (info, output): + continue + + elif output['type'] == 'Applications': + if not self.handle_application_hits (info, output): + continue + + # applications are launched by .desktop file, if not readable: exclude + if output['type'] != 'Applications' or output.has_key ('desktop'): + matches.append(TrackerLiveSearchMatch (output)) + + if len (matches): + self._emit_query_ready (qstring, matches) + print 'Tracker response for %s; %d hits returned, %d shown' % \ + (qstring, len(hits), len(matches)) + + def recieve_error (self, error): + print >> sys.stderr, '*** Tracker dbus error:', error + + def query (self, qstring): + max = MAX_RESULTS + + if self.tracker: + try: self.tracker.GetStatus () + except: self.tracker = None # reconnect + if not self.tracker: + try: + print "Connecting to Tracker (first search or trackerd restarted)" + import dbus + bus = dbus.SessionBus() + self.tracker = bus.get_object('org.freedesktop.Tracker', '/org/freedesktop/tracker') + self.search_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Search') + self.keywords_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Keywords') + self.files_iface = dbus.Interface(self.tracker, 'org.freedesktop.Tracker.Files') + except: + print >> sys.stderr, 'DBus connection to tracker failed, check your settings.' + return + for service in [key for key in TYPES.iterkeys () if key != 'Extra']: + print 'Searching %s' % service + self.search_iface.TextDetailed (-1, service, qstring, 0, max, \ + reply_handler = lambda hits: self.recieve_hits(qstring, hits, max), + error_handler = self.recieve_error) + print 'Tracker query:', qstring + + + + +# this code is stolen from the programs handler of deskbar +def parse_desktop_file(desktop, only_if_visible=False): + try: + desktop = deskbar.core.gnomedesktop.item_new_from_file(desktop, deskbar.core.gnomedesktop.LOAD_ONLY_IF_EXISTS) + except Exception, e: + print 'Couldn\'t read desktop file:%s:%s' % (desktop, e) + return None + if desktop == None or desktop.get_entry_type() != deskbar.core.gnomedesktop.TYPE_APPLICATION: + return None + if only_if_visible and desktop.get_boolean(deskbar.core.gnomedesktop.KEY_NO_DISPLAY): + return None + return desktop + +def time_from_purple_log (instr): + try: + if instr.find ('+') != -1: # new kind of log timestamp... + return time.strftime ('%c', time.strptime (re.sub (r'\+\d{4}', '', instr), '%Y-%m-%d.%H%M%S%Z')) + else: # ...from ancient times + return time.strftime ('%c', time.strptime (instr, '%Y-%m-%d.%H%M%S')) + except: + print >> sys.stderr, '*** time parsing for purple chat log failed: %s' % sys.exc_info ()[1] + return instr + +def url_quote (instr, safe = '/'): + """A unicode capable quote, see http://bugs.python.org/issue1712522""" + return ''.join (map (lambda x: x in (safe+string.letters+string.digits) and x or ('%%%02X' % ord(x)), instr.encode ('utf-8'))) diff --git a/python/music/lyrics.py b/python/music/lyrics.py new file mode 100644 index 000000000..f71f1cf24 --- /dev/null +++ b/python/music/lyrics.py @@ -0,0 +1,70 @@ +import os +import urllib +import sys +import re +from xml.dom import minidom + +LYRIC_TITLE_STRIP = ["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)"] +LYRIC_TITLE_REPLACE = [("/", "-"), (" & ", " and ")] +LYRIC_ARTIST_REPLACE = [("/", "-"), (" & ", " and ")] + +def get_lyrics(artist, title): + # replace ampersands and the like + for exp in LYRIC_ARTIST_REPLACE: + p = re.compile (exp[0]) + artist = p.sub(exp[1], artist) + for exp in LYRIC_TITLE_REPLACE: + p = re.compile (exp[0]) + title = p.sub(exp[1], title) + + # strip things like "(live at Somewhere)", "(accoustic)", etc + for exp in LYRIC_TITLE_STRIP: + p = re.compile (exp) + title = p.sub ('', title) + + # compress spaces + title = title.strip() + artist = artist.strip() + + url = "http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s" % ( + urllib.quote(artist.encode('utf-8')), + urllib.quote(title.encode('utf-8'))) + + data = urllib.urlopen(url).read() + xmldoc = minidom.parseString(data).documentElement + + result_code = xmldoc.getElementsByTagName('response')[0].getAttribute('code') + if result_code != '0': + xmldoc.unlink() + return + + matches = xmldoc.getElementsByTagName('result')[:10] + hids = map(lambda x: x.getAttribute('hid'), matches) + if len(hids) == 0: + self.callback("Unable to find lyrics for this track.") + xmldoc.unlink() + return + + xmldoc.unlink() + + url = "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s" % (urllib.quote(hids[0].encode('utf-8'))) + data = urllib.urlopen(url).read() + xmldoc = minidom.parseString(data).documentElement + + text = xmldoc.getElementsByTagName('title')[0].firstChild.nodeValue + text += ' - ' + xmldoc.getElementsByTagName('artist')[0].getElementsByTagName('name')[0].firstChild.nodeValue + '\n\n' + text += xmldoc.getElementsByTagName('text')[0].firstChild.nodeValue + + xmldoc.unlink() + return text + + +if __name__ == '__main__': + if len(sys.argv) != 4: + print 'usage: %s artist title output' % (sys.argv[0]) + sys.exit(1) + + f = open(sys.argv[3], 'w') + lyrics = get_lyrics(sys.argv[1], sys.argv[2]) + f.write(lyrics.encode('utf-8')) + f.close() diff --git a/python/nautilus/README b/python/nautilus/README new file mode 100644 index 000000000..921bf7209 --- /dev/null +++ b/python/nautilus/README @@ -0,0 +1,15 @@ +tracker-tags-tab.py is a nautilus extension providing document tagging interface powered by tracker + +----------Requirements---------- +* python-dev (>= 2.3) +* tracker(>= 5.0) +* python-nautilus (>=0.4.3) + +If you use Ubuntu these can be installed via apt-get with this command: +sudo apt-get install python-dev python-nautilus + +----------Installation----------- + +cp ./*.py ~/.nautilus/python-extensions/ + + diff --git a/python/nautilus/tracker-tags-tab.py b/python/nautilus/tracker-tags-tab.py new file mode 100644 index 000000000..44a26bd44 --- /dev/null +++ b/python/nautilus/tracker-tags-tab.py @@ -0,0 +1,192 @@ +# Copyright (C) 2006/7, Edward B. Duffy <eduffy@gmail.com> +# tracker-tags-tab.py: Tag your files in your Tracker database +# via Nautilus's property dialog. +# +# 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +if __name__ == '__main__': + import os + import sys + + print 'This is nautilus extension, not a standalone application.' + print 'Please copy this file into your nautulis extensions directory:' + print + print '\t# cp %s %s/.nautilus/python-extensions' % \ + (__file__,os.path.expanduser('~')) + + sys.exit(1) + + +import gtk +import dbus +import urllib +import operator +import nautilus + +class TrackerTagsPage(nautilus.PropertyPageProvider): + + def __init__(self): + bus = dbus.SessionBus() + obj = bus.get_object('org.freedesktop.Tracker', + '/org/freedesktop/tracker') + self.tracker = dbus.Interface(obj, 'org.freedesktop.Tracker') + self.keywords = dbus.Interface(obj, 'org.freedesktop.Tracker.Keywords') + + def _on_toggle(self, cell, path, files): + on = not self.store.get_value(self.store.get_iter(path), 0) + self.store.set_value(self.store.get_iter(path), 0, on) + tag = self.store.get_value(self.store.get_iter(path), 1) + if on: func = self.keywords.Add + else: func = self.keywords.Remove + for f in files: + func('Files', f, [tag]) + + def _on_add_tag(self, button): + self.store.append([False, '']) + + def _on_edit_tag(self, cell, path, text, files): + old_text = self.store.get_value(self.store.get_iter(path), 1) + on = self.store.get_value(self.store.get_iter(path), 0) + if on: + for f in files: + self.keywords.Remove('Files', f, [old_text]) + self.keywords.Add('Files', f, [text]) + self.store.set_value(self.store.get_iter(path), 1, text) + + def _on_update_tag_summary(self, store, path, iter): + tags = [ ] + for row in store: + if row[0]: + tags.append(row[1]) + self.entry_tag.handler_block(self.entry_changed_id) + self.entry_tag.set_text(','.join(tags)) + self.entry_tag.handler_unblock(self.entry_changed_id) + + def _on_tag_summary_changed(self, entry, files): + new_tags = set(entry.get_text().split(',')) + new_tags.discard('') # remove the empty string + for f in files: + old_tags = set(self.keywords.Get('Files', f)) + tbr = list(old_tags.difference(new_tags)) + tba = list(new_tags.difference(old_tags)) + if tbr: + self.keywords.Remove('Files', f, tbr) + if tba: + self.keywords.Add('Files', f, tba) + + # update check-box list (remove outdated tags, add the new ones) + self.store.handler_block(self.store_changed_id) + all_tags = [ t for t,c in self.keywords.GetList('Files') ] + i = 0 + while i < len(self.store): + if self.store[i][1] in all_tags: + self.store[i][0] = (self.store[i][1] in new_tags) + all_tags.remove(self.store[i][1]) + i += 1 + else: + del self.store[i] + # assert len(all_tags) == 1 ??? + for t in all_tags: + self.store.append([True, t]) + self.store.handler_unblock(self.store_changed_id) + + def get_property_pages(self, files): + property_label = gtk.Label('Tags') + property_label.show() + + # get the list of tags + all_tags = self.keywords.GetList('Files') + # convert usage count to an integer + all_tags = [ (t,int(c)) for t,c in all_tags ] + # sort by usage count + all_tags = sorted(all_tags, key=operator.itemgetter(1)) + all_tags.reverse() + # strip away usage count + all_tags = [ t for t,c in all_tags ] + + files = [ urllib.url2pathname(f.get_uri()[7:]) for f in files ] + indiv_count = dict([ (t,0) for t in all_tags ]) + tags = { } + for f in files: + tags[f] = self.keywords.Get('Files', f) + for t in tags[f]: + indiv_count[t] += 1 + + main = gtk.VBox() + + hbox = gtk.HBox() + hbox.set_border_width(6) + hbox.set_spacing(12) + label = gtk.Label('Tags: ') + self.entry_tag = gtk.Entry() + # self.entry_tag.props.editable = False + hbox.pack_start(label, False, False) + hbox.pack_start(self.entry_tag, True, True) + main.pack_start(hbox, False, False) + self.entry_changed_id = self.entry_tag.connect( + 'changed', self._on_tag_summary_changed, files) + + sw = gtk.ScrolledWindow() + sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + sw.set_shadow_type(gtk.SHADOW_ETCHED_OUT) + + self.store = gtk.ListStore(bool, str) + self.store_changed_id = self.store.connect( + 'row-changed', self._on_update_tag_summary) + for tag in all_tags: + iter = self.store.append([False, tag]) + if indiv_count[tag] == len(files): + self.store.set_value(iter, 0, True) + elif indiv_count[tag] == 0: + self.store.set_value(iter, 0, False) + else: + print 'inconsistant' + tv = gtk.TreeView(self.store) + tv.set_headers_visible(False) + + column = gtk.TreeViewColumn() + tv.append_column(column) + cell = gtk.CellRendererToggle() + column.pack_start(cell, True) + column.add_attribute(cell, 'active', 0) + # column.add_attribute(cell, 'inconsistent', 0) + cell.connect('toggled', self._on_toggle, files) + cell.set_property('activatable', True) + # cell.set_property('inconsistent', True) + + column = gtk.TreeViewColumn() + tv.append_column(column) + cell = gtk.CellRendererText() + column.pack_start(cell, True) + column.add_attribute(cell, 'text', 1) + cell.connect('edited', self._on_edit_tag, files) + cell.set_property('editable', True) + + sw.add(tv) + main.pack_start(sw) + + hbox = gtk.HBox() + hbox.set_border_width(6) + btn = gtk.Button(stock='gtk-add') + btn.get_child().get_child().get_children()[1].props.label = '_Add Tag' + btn.connect('clicked', self._on_add_tag) + hbox.pack_end(btn, False, False) + main.pack_start(hbox, False, False) + + main.show_all() + + return nautilus.PropertyPage("NautilusPython::tags", property_label, main), + |