summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorMartyn James Russell <mr@src.gnome.org>2008-09-26 10:47:33 +0000
committerMartyn James Russell <mr@src.gnome.org>2008-09-26 10:47:33 +0000
commit2f30be89ca554dc4639c7c8a6ee465274f52721a (patch)
treebb333517426d0d68cad130b34b8892a45ff7f54d /python
parentcaa9320c8ae7800acf2160701eb9284ac05e4eb9 (diff)
downloadtracker-2f30be89ca554dc4639c7c8a6ee465274f52721a.tar.gz
Merge indexer-split branch.
svn path=/trunk/; revision=2275
Diffstat (limited to 'python')
-rw-r--r--python/FUSE/trackerfs.py279
-rw-r--r--python/Makefile.am4
-rw-r--r--python/SearchTool/COPYING340
-rw-r--r--python/SearchTool/README19
-rw-r--r--python/SearchTool/mainform.py191
-rwxr-xr-xpython/SearchTool/trackergui.py163
-rwxr-xr-xpython/applet/applet.py131
-rw-r--r--python/applet/applet.svg453
-rw-r--r--python/deskbar-handler/Makefile.am19
-rw-r--r--python/deskbar-handler/README0
-rw-r--r--python/deskbar-handler/tracker-handler-static.py71
-rw-r--r--python/deskbar-handler/tracker-handler.py402
-rw-r--r--python/deskbar-handler/tracker-module.py492
-rw-r--r--python/music/lyrics.py70
-rw-r--r--python/nautilus/README15
-rw-r--r--python/nautilus/tracker-tags-tab.py192
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('<!--', '&lt;!--').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),
+