├── debian ├── compat ├── picty.install ├── docs ├── dirs ├── README ├── README.Debian ├── control ├── copyright └── rules ├── modules ├── picty │ ├── __init__.py │ ├── fstools │ │ ├── __init__.py │ │ ├── dbusserver.py │ │ ├── monitor2.py │ │ └── fileops.py │ ├── uitools │ │ ├── __init__.py │ │ ├── register_icons.py │ │ ├── context_menu.py │ │ ├── floats.py │ │ ├── completions.py │ │ ├── toolbar_helpers.py │ │ ├── overlay_tools.py │ │ └── searchbox.py │ ├── plugins │ │ ├── __init__.py │ │ ├── rotate.py │ │ ├── enhance.py │ │ └── metadata_viewer.py │ ├── collectiontypes │ │ ├── __init__.py │ │ └── simpleview.py │ ├── _legacy │ │ ├── plugins │ │ │ ├── webupload_services │ │ │ │ ├── __init__.py │ │ │ │ ├── picasaweb.py │ │ │ │ └── flickr.py │ │ │ └── webupload.py │ │ └── images.py │ ├── metadata │ │ └── __init__.py │ ├── logger.py │ ├── pluginimporter.py │ ├── simple_parser.py │ └── pluginmanager.py └── pyfb │ ├── __init__.py │ ├── utils.py │ ├── auth.py │ ├── pyfb.py │ └── client.py ├── .gitignore ├── icons ├── picty.ico ├── picty-map.png ├── picty-sidebar.png ├── picty-transfer.png ├── Gnome-emblem-mail.png ├── picty-5-polaroids.png ├── picty-image-crop.png ├── picty-image-write.png ├── picty-rotate-left.png ├── picty-web-upload.png ├── picty-image-rotate.png ├── picty-rotate-right.png ├── picty-polaroids-and-frame.png └── license ├── test ├── test.png └── test_metadata.py ├── desktop ├── picty.png ├── org.spillz.picty.service ├── picty.desktop ├── picty-import.desktop └── picty-open.desktop ├── user-guide ├── screenshots │ ├── start.png │ ├── new-collection.png │ ├── main-window-annotated.png │ ├── new-collection-created.png │ ├── new-collection-details.png │ ├── main-window-annotated-complete.png │ └── main-window-annotated.svg ├── index.rst ├── collections.rst ├── start.rst └── user_interface.rst ├── bin ├── picty-import ├── picty-open └── picty ├── picty.iss ├── INSTALL ├── README-SF.md ├── utils └── entrycompletion.py ├── setup.py ├── TODO └── README.rst /debian/compat: -------------------------------------------------------------------------------- 1 | 7 2 | -------------------------------------------------------------------------------- /debian/picty.install: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- 1 | README.rst 2 | -------------------------------------------------------------------------------- /modules/picty/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /modules/picty/fstools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /modules/picty/uitools/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | bin/picty.py 3 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | usr/sbin 3 | -------------------------------------------------------------------------------- /modules/picty/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /modules/picty/collectiontypes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /icons/picty.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty.ico -------------------------------------------------------------------------------- /test/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/test/test.png -------------------------------------------------------------------------------- /desktop/picty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/desktop/picty.png -------------------------------------------------------------------------------- /icons/picty-map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-map.png -------------------------------------------------------------------------------- /icons/picty-sidebar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-sidebar.png -------------------------------------------------------------------------------- /icons/picty-transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-transfer.png -------------------------------------------------------------------------------- /desktop/org.spillz.picty.service: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.spillz.picty 3 | Exec=/usr/bin/picty 4 | -------------------------------------------------------------------------------- /icons/Gnome-emblem-mail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/Gnome-emblem-mail.png -------------------------------------------------------------------------------- /icons/picty-5-polaroids.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-5-polaroids.png -------------------------------------------------------------------------------- /icons/picty-image-crop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-image-crop.png -------------------------------------------------------------------------------- /icons/picty-image-write.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-image-write.png -------------------------------------------------------------------------------- /icons/picty-rotate-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-rotate-left.png -------------------------------------------------------------------------------- /icons/picty-web-upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-web-upload.png -------------------------------------------------------------------------------- /icons/picty-image-rotate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-image-rotate.png -------------------------------------------------------------------------------- /icons/picty-rotate-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-rotate-right.png -------------------------------------------------------------------------------- /user-guide/screenshots/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/start.png -------------------------------------------------------------------------------- /icons/picty-polaroids-and-frame.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/icons/picty-polaroids-and-frame.png -------------------------------------------------------------------------------- /user-guide/screenshots/new-collection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/new-collection.png -------------------------------------------------------------------------------- /user-guide/screenshots/main-window-annotated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/main-window-annotated.png -------------------------------------------------------------------------------- /user-guide/screenshots/new-collection-created.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/new-collection-created.png -------------------------------------------------------------------------------- /user-guide/screenshots/new-collection-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/new-collection-details.png -------------------------------------------------------------------------------- /user-guide/screenshots/main-window-annotated-complete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spillz/picty/HEAD/user-guide/screenshots/main-window-annotated-complete.png -------------------------------------------------------------------------------- /modules/picty/_legacy/plugins/webupload_services/__init__.py: -------------------------------------------------------------------------------- 1 | services = [ 2 | ['Flickr',None,'flickr','FlickrService'], 3 | ['PicasaWeb Albums',None,'picasaweb','PicasaService'] 4 | ] 5 | 6 | -------------------------------------------------------------------------------- /debian/README: -------------------------------------------------------------------------------- 1 | The Debian Package picty 2 | ---------------------------- 3 | 4 | Comments regarding the Package 5 | 6 | -- Damien Moore Sat, 11 Apr 2009 14:13:04 -0400 7 | -------------------------------------------------------------------------------- /debian/README.Debian: -------------------------------------------------------------------------------- 1 | picty for Debian 2 | ------------------ 3 | 4 | 5 | 6 | -- Damien Moore Sat, 11 Apr 2009 14:13:04 -0400 7 | -------------------------------------------------------------------------------- /desktop/picty.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=picty Photo Manager 3 | GenericName=Photo Manager 4 | Comment=View and organize your images 5 | Categories=GNOME;GTK;Graphics;Viewer;RasterGraphics;2DGraphics;Photography; 6 | Exec=picty %U 7 | Icon=picty 8 | StartupNotify=true 9 | Terminal=false 10 | Type=Application 11 | X-Ubuntu-Gettext-Domain=picty 12 | -------------------------------------------------------------------------------- /bin/picty-import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import dbus, sys 4 | 5 | bus = dbus.SessionBus() 6 | bus.start_service_by_name('org.spillz.picty') ##THIS ONLY WORKS FOR SERVICES WITH A SERVICE FILE INSTALLED 7 | server = dbus.Interface(bus.get_object('org.spillz.picty', '/org/spillz/picty'), 8 | 'org.spillz.picty') 9 | uri = sys.argv[1] if len(sys.argv)>1 else '' 10 | print server.media_connected(uri) 11 | -------------------------------------------------------------------------------- /desktop/picty-import.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=picty Photo Importer 3 | GenericName=Photo Import Tool 4 | Comment=Import the photos on your camera card 5 | Categories=GNOME;GTK;Graphics;Viewer;RasterGraphics;2DGraphics;Photography; 6 | Exec=picty-import %U 7 | Icon=picty 8 | MimeType=x-content/image-dcf;x-content/image-picturecd; 9 | NoDisplay=true 10 | StartupNotify=true 11 | Terminal=false 12 | Type=Application 13 | X-Ubuntu-Gettext-Domain=picty 14 | -------------------------------------------------------------------------------- /desktop/picty-open.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=picty Photo Viewer 3 | GenericName=Photo Viewer 4 | Comment=Display a photo in picty image viewer 5 | Categories=GNOME;GTK;Graphics;Viewer;RasterGraphics;2DGraphics;Photography; 6 | Exec=picty-open %U 7 | Icon=picty 8 | MimeType=image/bmp;image/jpeg;image/gif;image/png;image/tiff;image/x-bmp;image/x-ico;image/x-png;image/x-pcx;image/x-tga;image/xpm;image/svg+xml; 9 | NoDisplay=true 10 | StartupNotify=true 11 | Terminal=false 12 | Type=Application 13 | X-Ubuntu-Gettext-Domain=picty 14 | -------------------------------------------------------------------------------- /bin/picty-open: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import dbus, sys 4 | 5 | bus = dbus.SessionBus() 6 | bus.start_service_by_name('org.spillz.picty') ##THIS ONLY WORKS FOR SERVICES WITH A SERVICE FILE INSTALLED 7 | server = dbus.Interface(bus.get_object('org.spillz.picty', '/org/spillz/picty'), 8 | 'org.spillz.picty') 9 | if len(sys.argv) == 3: 10 | if sys.argv[1] == '-d': 11 | server.open_device(sys.argv[2]) 12 | else: 13 | uri = sys.argv[1] if len(sys.argv)>1 else '' 14 | print server.open_uri(uri) 15 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: picty 2 | Section: graphics 3 | Priority: extra 4 | Maintainer: Damien Moore 5 | Build-Depends: debhelper (>= 7), dh-python, bzr 6 | Standards-Version: 3.8.0 7 | Homepage: https://launchpad.net/picty 8 | 9 | Package: picty 10 | Architecture: any 11 | Depends: ${shlibs:Depends}, ${misc:Depends}, python, python-pil, python-pyinotify, python-pyexiv2, python-gtk2, python-gnome2 12 | Recommends: dcraw, totem, python-gdata, python-flickrapi, python-osmgpsmap 13 | Description: picty Photo Manager 14 | picty helps you manage your photos. 15 | -------------------------------------------------------------------------------- /modules/pyfb/__init__.py: -------------------------------------------------------------------------------- 1 | # This program is free software; you can redistribute it and/or modify 2 | # it under the terms of the GNU General Public License as published by 3 | # the Free Software Foundation; either version 3 of the License, or 4 | # (at your option) any later version. 5 | # 6 | # This program is distributed in the hope that it will be useful, 7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | # GNU General Public License for more details. 10 | # 11 | # You should have received a copy of the GNU General Public License 12 | # along with this program; if not, write to the Free Software 13 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 14 | # MA 02110-1301, USA. 15 | 16 | __author__ = "Juan Manuel Garcia" 17 | __version__ = "0.4.2" 18 | __license__ = 'GPL v3' 19 | 20 | from pyfb import Pyfb, PyfbException 21 | -------------------------------------------------------------------------------- /user-guide/index.rst: -------------------------------------------------------------------------------- 1 | picty User Guide 2 | ================ 3 | 4 | Topics 5 | ------ 6 | 7 | | `Getting Started `_ 8 | | `User Interface `_ 9 | | `Collections `_ 10 | | `Image Browser `_ 11 | | `Image Viewer `_ 12 | | `Tagging `_ 13 | | `Transferring Images Between Collections `_ 14 | | `Image Editing `_ 15 | | `External Tools `_ 16 | 17 | Sidebar Plugins 18 | --------------- 19 | 20 | | `Folder Sidebar `_ 21 | | `Tag Sidebar `_ 22 | 23 | Image Editing Plugins 24 | --------------------- 25 | 26 | | `Crop `_ 27 | | `Rotate `_ 28 | | `Enhance `_ 29 | | `Exporter `_ 30 | 31 | Other Plugins 32 | ------------- 33 | 34 | | `Image Transfer `_ 35 | | `Map Plugin `_ 36 | | `Metadata Viewer `_ 37 | | `Emailer `_ 38 | -------------------------------------------------------------------------------- /picty.iss: -------------------------------------------------------------------------------- 1 | ; -- Example1.iss -- 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING .ISS SCRIPT FILES! 3 | 4 | [Setup] 5 | AppName=Picty 6 | AppVersion=%source% 7 | AppPublisher=Damien Moore 8 | AppPublisherURL=http://launchpad.net/picty 9 | DefaultDirName={pf}\picty 10 | DefaultGroupName=picty 11 | UninstallDisplayIcon={app}\picty.exe 12 | Compression=lzma2 13 | SolidCompression=yes 14 | OutputBaseFilename=picty-%source%-setup 15 | 16 | [Dirs] 17 | Name: {app}; Flags: uninsalwaysuninstall; 18 | 19 | [Files] 20 | Source: dist\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs 21 | 22 | [Icons] 23 | Name: "{group}\picty"; Filename: "{app}\picty.exe" 24 | ;Name: {group}\handytool; Filename: {app}\handytool.exe; WorkingDir: {app} 25 | 26 | [Run] 27 | ; If you are using GTK's built-in SVG support, uncomment the following line. 28 | ;Filename: {cmd}; WorkingDir: "{app}"; Parameters: "/C gdk-pixbuf-query-loaders.exe > lib/gdk-pixbuf-2.0/2.10.0/loaders.cache"; Description: "GDK Pixbuf Loader Cache Update"; Flags: nowait runhidden 29 | Filename: {app}\picty.exe; Description: {cm:LaunchProgram,picty}; Flags: nowait postinstall skipifsilent 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /user-guide/collections.rst: -------------------------------------------------------------------------------- 1 | Collections 2 | =========== 3 | 4 | In picty, a collection can be a: 5 | 6 | * Local Store: The images in a directory and its sub-directories that 7 | you access repeatedly. This is the collection type that most users 8 | will use to manage their collection on their local disks. 9 | 10 | * Temporary Directory: You can use picty to temporarily browse a folder of images 11 | wihout creating a local store. 12 | 13 | * Device: The images on a camera, smartphone, memory card or other 14 | pluggable device. picty automatically tracks these devices are they 15 | are added and remove from the system. 16 | 17 | * (experimental) Web Service: The images hosted on a web service such as flickr 18 | 19 | picty will keep the collection in sync with the underlying images. By default, each 20 | time you open a collection picty will look for new, changed and deleted images 21 | update in the source and the collection accordingly. (If the service is not online, 22 | e.g. a webs service when you are not connected to the internet, you will be able to 23 | browse the cached version of the collection.) 24 | 25 | The Start Page 26 | ============== 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /modules/picty/metadata/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | ''' 23 | metadata.py 24 | 25 | This module describes the subset of exif, iptc and xmp metadata used by the program 26 | and provides a dictionary to handle conversion between exiv2 formats and the internal 27 | representation 28 | ''' 29 | 30 | import pyexiv2 31 | 32 | if '__version__' in dir(pyexiv2) and pyexiv2.__version__>='0.2': 33 | print 'Using pyexiv2 version',pyexiv2.__version__ 34 | from metadata2 import * 35 | else: 36 | print 'Using pyexiv2 version 0.1.x' 37 | from metadata1 import * 38 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | This package was debianized by Damien Moore on 2 | Sat, 11 Apr 2009 14:13:04 -0400. 3 | 4 | Copyright: 5 | 6 | Copyright (C) 2009 Damien Moore 7 | Copyright (C) 2009 Olivier Tilloy 8 | 9 | License: 10 | 11 | This package is free software; you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation; either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This package is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this package; if not, write to the Free Software 23 | Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA 24 | 25 | On Debian systems, the complete text of the GNU General 26 | Public License can be found in `/usr/share/common-licenses/GPL'. 27 | 28 | The Debian packaging is (C) 2009, Damien Moore and 29 | is licensed under the GPL, see above. 30 | 31 | 32 | # Please also look if there are files or directories which have a 33 | # different copyright/license attached and list them here. 34 | -------------------------------------------------------------------------------- /test/test_metadata.py: -------------------------------------------------------------------------------- 1 | 2 | if __name__ == '__main__': 3 | import sys, os.path 4 | sys.path.insert(0, os.path.abspath('../modules')) 5 | from picty.metadata.metadata2 import load_metadata, save_metadata 6 | import shutil 7 | shutil.copyfile('test.png','test1.png') 8 | class Item: 9 | meta = None 10 | thumb = None 11 | uid = '1' 12 | def mark_meta_saved(self): 13 | pass 14 | 15 | try: 16 | print 'Test 1' 17 | item = Item() 18 | assert(load_metadata(item, 'test1.png')) 19 | item.meta['Title'] = 'The Title' 20 | item.meta['Keywords'] = ['keyword1', 'keyword2'] 21 | assert(save_metadata(item, 'test1.png')) 22 | print 'Test 1 passed' 23 | 24 | print 'Test 2' 25 | item2 = Item() 26 | assert(load_metadata(item2, 'test1.png')) 27 | assert(item2.meta['Title'] == 'The Title') 28 | assert(item2.meta['Keywords'] == ['keyword1', 'keyword2']) 29 | del item2.meta['Keywords'] 30 | save_metadata(item2, 'test1.png') 31 | print 'Test 2 passed' 32 | 33 | print 'Test 3' 34 | item3 = Item() 35 | load_metadata(item3, 'test1.png') 36 | assert(item3.meta['Title'] == 'The Title') 37 | assert('Keywords' not in item3.meta) 38 | print 'Test 3 passed' 39 | 40 | print 'All tests passed' 41 | finally: 42 | import os 43 | os.remove('test1.png') 44 | -------------------------------------------------------------------------------- /modules/picty/logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | import logging 23 | import os 24 | 25 | import settings 26 | 27 | # create logger 28 | log = logging.getLogger("picty core") 29 | log.setLevel(logging.DEBUG) 30 | # create console handler and set level to debug 31 | ch = logging.StreamHandler() 32 | ch.setLevel(logging.ERROR) 33 | 34 | fh = logging.FileHandler(os.path.join(settings.settings_dir,'log')) 35 | fh.setLevel(logging.DEBUG) 36 | 37 | # create formatter 38 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s\n%(message)s\n") 39 | # add formatter to ch 40 | ch.setFormatter(formatter) 41 | fh.setFormatter(formatter) 42 | # add ch to logger 43 | log.addHandler(ch) 44 | log.addHandler(fh) 45 | -------------------------------------------------------------------------------- /INSTALL: -------------------------------------------------------------------------------- 1 | Installing picty on a Linux-based System 2 | ========================================== 3 | 4 | Dependencies 5 | ============ 6 | 7 | python(>=2.5) 8 | python-pil 9 | python-gtk2 10 | python-gnome2 11 | python-pyinotify 12 | python-pyexiv2 13 | 14 | recommended packages: dcraw, totem, python-gdata, python-flickrapi, python-osmgpsmap 15 | 16 | 17 | Create and Install a Debian Package 18 | =================================== 19 | 20 | To create the package run as root: 21 | 22 | cd 23 | dpkg-buildpackage -rfakeroot 24 | 25 | To install, run as root: 26 | 27 | dpkg -i ../picty*.deb 28 | 29 | Installing using your package manager 30 | ===================================== 31 | 32 | You can also use checkinstall to create a package that is compatible with many 33 | linux package management systems (for example, RPM, Deb) 34 | 35 | (Make sure you do not already have picty installed into the system folders before running these steps) 36 | 37 | Run the following as root: 38 | 39 | checkinstall --pkgversion=0.4 --maintainer="joebloggs@freesoftware" \ 40 | --pkgname=picty \ 41 | ./setup.py install --home=/usr \ 42 | --install-purelib=/usr/share/picty 43 | 44 | You will prompted to supply additional information. 45 | 46 | This will create and install the package. You will be able to remove the 47 | package using standard tools 48 | 49 | Installing without a package manager 50 | ==================================== 51 | 52 | Run the following as root 53 | 54 | sudo ./setup.py install --no-compile --home=debian/picty/usr \ 55 | --install-purelib=debian/picty/usr/share/picty 56 | 57 | Note that installing in this way provides no mechanism for 58 | uninstalling. 59 | 60 | 61 | -------------------------------------------------------------------------------- /modules/picty/uitools/register_icons.py: -------------------------------------------------------------------------------- 1 | import gtk 2 | import os.path 3 | 4 | from picty import settings 5 | 6 | '''Registers new icons with gtk. Tries to use already existing icons 7 | if they are available, otherwise it loads them from files.''' 8 | 9 | ICON_INFO = [ 10 | ('picty-5', 'picty-5-polaroids.png'), 11 | ('picty-rotate-left', 'picty-rotate-left.png'), 12 | ('picty-rotate-right', 'picty-rotate-right.png'), 13 | ('picty-sidebar', 'picty-sidebar.png'), 14 | ('picty-image-crop', 'picty-image-crop.png'), 15 | ('picty-image-rotate', 'picty-image-rotate.png'), 16 | ('picty-image-write', 'picty-image-write.png'), 17 | ('picty-transfer', 'picty-transfer.png'), 18 | ('picty-map', 'picty-map.png'), 19 | ('picty-emailer', 'Gnome-emblem-mail.png'), 20 | ('flickr', 'flickr.png'), 21 | ] 22 | 23 | filename=os.path.abspath(__file__) 24 | user_local = os.path.expanduser('~/.local') 25 | prefix = user_local if filename.startswith(user_local) else '/usr' 26 | if filename.startswith(prefix): 27 | icon_path=prefix+'/share/picty/icons/' 28 | else: 29 | icon_path=os.path.join(os.path.split(filename)[0],'..','..','..','icons/') 30 | 31 | def register_iconset(icon_info): 32 | iconfactory = gtk.IconFactory() 33 | stock_ids = gtk.stock_list_ids() 34 | for stock_id, file in icon_info: 35 | # only load image files when our stock_id is not present 36 | if stock_id not in stock_ids: 37 | try: 38 | pixbuf = gtk.gdk.pixbuf_new_from_file(icon_path+file) 39 | iconset = gtk.IconSet(pixbuf) 40 | iconfactory.add(stock_id, iconset) 41 | except: 42 | pass 43 | iconfactory.add_default() 44 | return iconfactory 45 | 46 | iconfactory = register_iconset(ICON_INFO) 47 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | 13 | 14 | 15 | 16 | configure: configure-stamp 17 | configure-stamp: 18 | dh_testdir 19 | # Add here commands to configure the package. 20 | 21 | touch configure-stamp 22 | 23 | 24 | build: build-stamp 25 | 26 | build-stamp: configure-stamp 27 | dh_testdir 28 | 29 | # # Add here commands to compile the package. 30 | # $(MAKE) 31 | #s #docbook-to-man debian/picty.sgml > picty.1 32 | 33 | touch $@ 34 | 35 | clean: 36 | dh_testdir 37 | dh_testroot 38 | rm -f build-stamp configure-stamp 39 | 40 | ./setup.py clean 41 | 42 | dh_clean 43 | 44 | install: build 45 | dh_testdir 46 | dh_testroot 47 | dh_clean -k 48 | dh_installdirs 49 | 50 | ./setup.py install --no-compile --home=debian/picty/usr --install-purelib=debian/picty/usr/share/picty 51 | 52 | 53 | # Build architecture-independent files here. 54 | binary-indep: build install 55 | # We have nothing to do by default. 56 | 57 | # Build architecture-dependent files here. 58 | binary-arch: build install 59 | dh_testdir 60 | dh_testroot 61 | dh_installchangelogs 62 | dh_installdocs 63 | dh_installexamples 64 | # dh_install 65 | # dh_installmenu 66 | # dh_installdebconf 67 | # dh_installlogrotate 68 | # dh_installemacsen 69 | # dh_installpam 70 | # dh_installmime 71 | dh_python2 72 | # dh_installinit 73 | # dh_installcron 74 | # dh_installinfo 75 | dh_installman 76 | dh_link 77 | dh_strip 78 | dh_compress 79 | dh_fixperms 80 | # dh_perl 81 | # dh_makeshlibs 82 | dh_installdeb 83 | dh_shlibdeps 84 | dh_gencontrol 85 | dh_md5sums 86 | dh_builddeb 87 | 88 | binary: binary-indep binary-arch 89 | .PHONY: build clean binary-indep binary-arch binary install configure 90 | -------------------------------------------------------------------------------- /README-SF.md: -------------------------------------------------------------------------------- 1 | picty - A photo collection manager 2 | ================================== 3 | 4 | SourceForge Hosted Windows Releases of picty 5 | --------------------------------------------- 6 | 7 | These are experimental releases of picty for windows. The software has not been thoroughly tested, so use at your own risk. 8 | 9 | How to install 10 | -------------- 11 | 12 | Download the file _picty-***************-setup.exe_, run it and follow the prompts. You can find the program in your start menu. 13 | 14 | A note on filenames: 15 | _picty-stable-v####-setup.exe_ - these are stable releases 16 | _picty-testing-r####-setup.exe_ - these are unstable testing builds 17 | 18 | Before you can run picty, you may also need the windows redistributable found at: 19 | [http://www.microsoft.com/en-us/download/details.aspx?id=29](http://www.microsoft.com/en-us/download/details.aspx?id=29) 20 | 21 | What's Broken? 22 | -------------- 23 | 24 | Compared to the linux version a few things are broken (I am working on fixes): 25 | 26 | * Filesystem monitoring (i.e. if you change an image outside picty, you need 27 | to manually rescan to update the metadata in the program) 28 | 29 | * Drag and drop to desktop does nothing (on Linux the files will be copied) 30 | 31 | * Automatic file associatons for right click "open with" 32 | 33 | * Occasional freezes/hangs 34 | 35 | Support 36 | ------- 37 | 38 | [Get help](http://groups.google.com/group/pictyphotomanager) 39 | 40 | [Report bugs](https://bugs.launchpad.net/picty) 41 | 42 | Enjoy! 43 | 44 | License: GPL v3 45 | --------------- 46 | 47 | _This program is free software: you can redistribute it and/or modify 48 | it under the terms of the GNU General Public License as published by 49 | the Free Software Foundation, either version 3 of the License, or 50 | (at your option) any later version._ 51 | 52 | _This program is distributed in the hope that it will be useful, 53 | but WITHOUT ANY WARRANTY; without even the implied warranty of 54 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 55 | GNU General Public License for more details._ 56 | 57 | _You should have received a copy of the GNU General Public License 58 | along with this program. If not, see ._ 59 | 60 | -------------------------------------------------------------------------------- /modules/picty/fstools/dbusserver.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | import dbus 23 | import dbus.service 24 | import dbus.glib 25 | 26 | from picty import pluginmanager 27 | 28 | ##todo: create dbus service file to autostart picty if dbus is inactive 29 | 30 | server=None 31 | 32 | class DBusServer(dbus.service.Object): 33 | def __init__(self,bus): 34 | # set service name 35 | bus_name = dbus.service.BusName('org.spillz.picty',bus=bus) 36 | # set the object path 37 | dbus.service.Object.__init__(self, bus_name, '/org/spillz/picty') 38 | 39 | @dbus.service.method('org.spillz.picty',in_signature='s',out_signature='s') 40 | def media_connected(self, uri): 41 | pluginmanager.mgr.callback('media_connected',uri) 42 | print "DBus media connection event for "+uri 43 | return 'success' 44 | 45 | @dbus.service.method('org.spillz.picty',in_signature='s',out_signature='s') 46 | def open_uri(self, uri): 47 | pluginmanager.mgr.callback('open_uri',uri) 48 | print "DBus open uri event for "+uri 49 | return 'success' 50 | 51 | @dbus.service.method('org.spillz.picty',in_signature='s',out_signature='s') 52 | def open_device(self, device): 53 | pluginmanager.mgr.callback('open_device',device) 54 | print "DBus open device event for "+device 55 | return 'success' 56 | 57 | def start(): 58 | global server 59 | bus=dbus.SessionBus() 60 | if bus.name_has_owner('org.spillz.picty'): 61 | print 'Another picty instance already has the DBus name org.spillz.picty' 62 | return False 63 | ##could also just abort here and send a "bring to front" message to picty main window 64 | server = DBusServer(bus) 65 | print 'Registered dbus server' 66 | return True 67 | 68 | -------------------------------------------------------------------------------- /modules/picty/pluginimporter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | 5 | picty 6 | Copyright (C) 2013 Damien Moore 7 | 8 | License: 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | ''' 23 | 24 | ##todo: need to add location of userplugins to sys.path 25 | 26 | import sys, os, os.path 27 | 28 | import settings 29 | 30 | #find the plugins by scanning the plugin directories 31 | try: 32 | zfilename = os.path.split(os.path.split(__file__)[0])[0] 33 | if zfilename.endswith('library.zip'): 34 | #on the windows py2exe build the plugins are stored in the library.zip 35 | #package of modules. Need to use zipfile to find them 36 | import zipfile 37 | z = zipfile.ZipFile(zfilename) 38 | plugins=['picty.plugins.'+os.path.splitext(os.path.split(n)[1])[0] 39 | for n in z.namelist() 40 | if n.startswith('picty/plugins') and not n.startswith('_')] 41 | else: 42 | plugins=['picty.plugins.'+os.path.splitext(n)[0] 43 | for n in os.listdir(os.path.join(os.path.dirname(__file__),'plugins')) 44 | if not n.startswith('_')] 45 | except: 46 | plugins=[] 47 | 48 | try: 49 | userplugins=['userplugins.'+os.path.splitext(path)[0] for path in os.listdir(os.path.join(settings.settings_dir,'plugins')) if not n.startswith('_')] 50 | except: 51 | userplugins=[] 52 | 53 | #converting to set ensures a unique set of plugins 54 | plugins = set(plugins) 55 | userplugins = set(userplugins) 56 | 57 | #import the global and user modules containing plugins 58 | for p in plugins: 59 | try: 60 | print 'Importing system plugin',p 61 | __import__(p) 62 | except: 63 | import sys 64 | import traceback 65 | tb_text=traceback.format_exc(sys.exc_info()[2]) 66 | print >>sys.stderr,'Error importing system plugin',p 67 | print >>sys.stderr,tb_text 68 | for p in userplugins: 69 | try: 70 | print 'Importing user plugin',p 71 | __import__(p) 72 | except: 73 | import sys 74 | import traceback 75 | tb_text=traceback.format_exc(sys.exc_info()[2]) 76 | print 'Error importing user plugin',p 77 | print tb_text 78 | 79 | 80 | -------------------------------------------------------------------------------- /utils/entrycompletion.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import pygtk 5 | pygtk.require('2.0') 6 | import gtk 7 | 8 | class EntryCompletion: 9 | def __init__(self): 10 | window = gtk.Window() 11 | window.connect('destroy', lambda w: gtk.main_quit()) 12 | vbox = gtk.VBox() 13 | label = gtk.Label('Type a, b, c or d\nfor completion') 14 | vbox.pack_start(label) 15 | entry = gtk.Entry() 16 | self.entry=entry 17 | vbox.pack_start(entry) 18 | window.add(vbox) 19 | completion = gtk.EntryCompletion() 20 | self.liststore = gtk.ListStore(str) 21 | for s in ['apple', 'banana', 'cap', 'comb', 'color', 22 | 'dog', 'doghouse']: 23 | self.liststore.append([s]) 24 | completion.set_model(self.liststore) 25 | entry.set_completion(completion) 26 | completion.set_text_column(0) 27 | completion.set_match_func(self.match_func) 28 | completion.connect('match-selected', self.match_selected) 29 | # entry.connect('changed', self.text_changed) 30 | # entry.connect('move-cursor', self.move_cursor) 31 | entry.connect('activate', self.activate_cb) 32 | self.lpos=0 33 | self.rpos=0 34 | window.show_all() 35 | 36 | def compute_word_pos(self): 37 | pos=self.entry.get_property("cursor-position") 38 | key_string=self.entry.get_text() 39 | lpos=pos 40 | while lpos>0 and key_string[lpos-1].isalnum(): 41 | lpos-=1 42 | rpos=pos 43 | while rpos0 else 1000 23 | self.menu.append(item) 24 | self.items.append(item) 25 | 26 | def add_menu(self,text,sub_menu,owner=None, priority=None): 27 | item=gtk.MenuItem(text) 28 | sub_menu.menu.show() 29 | item.set_submenu(sub_menu.menu) 30 | item.owner=owner 31 | item.show_callback = None 32 | self._add_item(item,priority) 33 | 34 | def add_separator(self,owner=None, priority=None): 35 | item = gtk.SeparatorMenuItem() 36 | item.owner = owner 37 | item.show_callback = None 38 | self._add_item(item,priority) 39 | 40 | def add(self,text,callback,show_callback=None,owner=None,priority=None,args=tuple()): 41 | item = gtk.MenuItem(text) 42 | item.connect("activate",callback,*args) 43 | item.owner=owner 44 | item.show() 45 | item.show_callback = show_callback 46 | self._add_item(item,priority) 47 | 48 | def remove_by_owner(self,owner=None): 49 | i=0 50 | while i. 20 | 21 | SOURCES AND EXCEPTIONS 22 | ====================== 23 | 24 | map.svg 25 | adapted from image created by Haltiamieli and downloaded from 26 | http://commons.wikimedia.org/wiki/File:Laurasia-Gondwana_fi.svg 27 | 28 | This image is a derivative work of the following images: 29 | 30 | * File:Laurasia-Gondwana.svg licensed with Cc-by-3.0 31 | o 2008-08-15T17:34:29Z Lenny222 519x435 (25417 Bytes) {{Information |Description= |Source= |Date= |Author= |Permission= |other_versions= }} 32 | o 2008-08-14T22:03:48Z Lenny222 519x435 (25397 Bytes) {{Information |Description= |Source= |Date= |Author=Lennart Kudling |Permission= |other_versions= }} 33 | o 2008-08-14T21:55:30Z Lenny222 519x435 (24066 Bytes) {{Information |Description={{en|1=SVG version of Image:Laurasia-Gondwana.png}} |Source=Own work by uploader |Author=[[User:Lenny222|Lenny222]] |Date=2008-08-14 |Permission= |other_versions= }} [[Category:Maps]] 34 | 35 | Uploaded with derivativeFX 36 | 37 | sidebar.svg, picty-sidebar.png: 38 | Based on Application-default-icon.svg‎ (SVG file, nominally 48 × 48 pixels, file size: 15 KB) 39 | An icon from the GNOME-colors 2.5 icon theme. 40 | Date Jun 4 2008 (last updated Oct 24 2008) 41 | Source http://www.gnome-look.org/content/show.php/GNOME-colors?content=82562 42 | Author perfectska04, GNOME icon artists 43 | Permission 44 | (Reusing this image) 45 | This work is free software; you can redistribute it and/or modify it under the 46 | terms of the GNU General Public License as published by the Free Software Foundation; 47 | either version 2 of the License, or any later version. This work is distributed 48 | in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the 49 | implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See version 2 50 | and version 3 of the GNU General Public License for more details. 51 | 52 | web-upload.svg, import.svg: 53 | Adapted from gnome-colors 5.5.1 icon theme 54 | using camera.svg, applications-internet.svg, gtk-save.svg 55 | 56 | picty-polaroids-and-frame.png: 57 | (c) 2010 yeKcim 58 | License GPL v3 59 | 60 | Gnome-emblem-mail.svg (and Gnome-emblem-mail.svg): 61 | Using the official gnome 2.20 mail emblem 62 | Downloaded from: 63 | http://commons.wikimedia.org/wiki/File:Gnome-emblem-mail.svg 64 | and available here: http://ftp.gnome.org/pub/GNOME/sources/gnome-icon-theme/2.20/ 65 | This work is free software; you can redistribute it and/or modify it under the terms of the GNU 66 | General Public License as published by the Free Software Foundation; either version 2 of the 67 | License, or any later version. This work is distributed in the hope that it will be useful, but 68 | without any warranty; without even the implied warranty of merchantability or fitness for a 69 | particular purpose. See version 2 and version 3 of the GNU General Public License for more details. 70 | 71 | -------------------------------------------------------------------------------- /modules/picty/_legacy/plugins/webupload.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty - Web Upload (Flickr, Picasa, Facebook etc) 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | ''' 22 | 23 | 24 | import os 25 | import os.path 26 | import threading 27 | 28 | import gtk 29 | import gobject 30 | 31 | from picty import settings 32 | from picty import pluginbase 33 | from picty import imagemanip 34 | from picty.fstools import io 35 | from picty import metadata 36 | 37 | import webupload_services as services 38 | 39 | 40 | 41 | class UploadPlugin(pluginbase.Plugin): 42 | name='WebUpload' 43 | display_name='Web Upload Sidebar' 44 | api_version='0.1.0' 45 | version='0.1.0' 46 | def __init__(self): 47 | pass 48 | 49 | def plugin_init(self,mainframe,app_init): 50 | self.mainframe=mainframe 51 | def vbox_group(widgets): 52 | box=gtk.VBox() 53 | for w in widgets: 54 | box.pack_start(*w) 55 | return box 56 | def hbox_group(widgets): 57 | box=gtk.HBox() 58 | for w in widgets: 59 | box.pack_start(*w) 60 | return box 61 | 62 | self.service_label=gtk.Label("Service") 63 | self.service_combo=gtk.combo_box_new_text() 64 | self.services=dict([(sclass[0], sclass) for sclass in services.services]) 65 | for s in self.services: 66 | self.service_combo.append_text(s) 67 | self.service_combo.connect("changed",self.service_combo_changed) 68 | 69 | self.scrolled_window=gtk.ScrolledWindow() 70 | self.scrolled_window.set_policy(gtk.POLICY_AUTOMATIC,gtk.POLICY_AUTOMATIC) 71 | self.viewport=gtk.Viewport() 72 | self.scrolled_window.add(self.viewport) 73 | 74 | self.dialog=self.mainframe.float_mgr.add_panel('Web Upload','Show or hide the web upload panel (use it to upload photos to web services such as flickr and picasa)','picty-web-upload') 75 | self.vbox=self.dialog.vbox 76 | 77 | self.service_box=hbox_group([(self.service_label,False),(self.service_combo,True,False)]) 78 | 79 | self.warning_label=gtk.Label("THIS PLUGIN HAS NOT BEEN WELL TESTED - USE AT YOUR OWN RISK") 80 | 81 | self.vbox.pack_start(self.service_box,False) 82 | self.vbox.pack_start(self.scrolled_window,True) 83 | self.scrolled_window.add_with_viewport(self.warning_label) 84 | self.vbox.show_all() 85 | # self.mainframe.sidebar.append_page(self.vbox,gtk.Label("Web Upload")) 86 | 87 | def plugin_shutdown(self,app_shutdown): 88 | for s in self.services: 89 | if self.services[s][1]!=None: 90 | self.services[s][1].service.shutdown() 91 | self.services[s][1]=None 92 | del self.services 93 | if not app_shutdown: 94 | self.mainframe.float_mgr.remove_panel('Web Upload') 95 | self.vbox.destroy() 96 | del self.vbox 97 | 98 | def service_combo_changed(self,widget): 99 | slist=self.services[widget.get_active_text()] 100 | if slist[1]==None: 101 | import webupload_services.serviceui 102 | slist[1]=services.serviceui.ServiceUI(self.mainframe,slist) 103 | child=self.viewport.get_child() 104 | if child: 105 | self.viewport.remove(child) 106 | self.viewport.add(slist[1]) 107 | # if not child: 108 | # self.viewport.show_all() 109 | 110 | -------------------------------------------------------------------------------- /modules/picty/fstools/monitor2.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import pyinotify 4 | 5 | 6 | 7 | if '__version__' in dir(pyinotify) and pyinotify.__version__>='0.8.0': 8 | mask = (pyinotify.IN_DELETE | 9 | pyinotify.IN_CREATE | 10 | pyinotify.IN_DONT_FOLLOW | 11 | pyinotify.IN_MODIFY| 12 | pyinotify.IN_MOVED_FROM| 13 | pyinotify.IN_MOVED_TO) # watched events 14 | 15 | class Monitor(pyinotify.ProcessEvent): 16 | def __init__(self,dirs,recursive,cb): 17 | pyinotify.ProcessEvent.__init__(self) 18 | self.cb=cb 19 | self.wm=pyinotify.WatchManager() 20 | self.notifier = pyinotify.ThreadedNotifier(self.wm, self) 21 | self.notifier.start() 22 | self.wd=[] 23 | self.recursive=recursive 24 | for d in dirs: 25 | self.wd.append(self.wm.add_watch(d, mask, rec=recursive, auto_add=True)) 26 | def stop(self): 27 | try: 28 | for wd in self.wd: 29 | self.wm.rm_watch(wd.values(),rec=self.recursive) 30 | self.notifier.stop() ##todo: should be checking if there are any other watches still active? 31 | except: 32 | print 'Error removing watch' 33 | import traceback,sys 34 | print traceback.format_exc(sys.exc_info()[2]) 35 | def process_IN_MODIFY(self, event): 36 | path=os.path.join(event.path, event.name) 37 | self.cb(path,'MODIFY',event.dir) 38 | def process_IN_MOVED_FROM(self, event): 39 | path=os.path.join(event.path, event.name) 40 | self.cb(path,'MOVED_FROM',event.dir) 41 | def process_IN_MOVED_TO(self, event): 42 | path=os.path.join(event.path, event.name) 43 | self.cb(path,'MOVED_TO',event.dir) 44 | def process_IN_CREATE(self, event): 45 | path=os.path.join(event.path, event.name) 46 | self.cb(path,'CREATE',event.dir) 47 | def process_IN_DELETE(self, event): 48 | path=os.path.join(event.path, event.name) 49 | self.cb(path,'DELETE',event.dir) 50 | def process_default(self, event=None): 51 | pass 52 | else: 53 | mask = (pyinotify.EventsCodes.IN_DELETE | 54 | pyinotify.EventsCodes.IN_CREATE | 55 | pyinotify.EventsCodes.IN_DONT_FOLLOW | 56 | pyinotify.EventsCodes.IN_MODIFY| 57 | pyinotify.EventsCodes.IN_MOVED_FROM| 58 | pyinotify.EventsCodes.IN_MOVED_TO) # watched events 59 | 60 | class Monitor(pyinotify.ProcessEvent): 61 | def __init__(self,dirs,recursive,cb): 62 | pyinotify.ProcessEvent.__init__(self) 63 | self.cb=cb 64 | self.wm=pyinotify.WatchManager() 65 | self.notifier = pyinotify.ThreadedNotifier(self.wm, self) 66 | self.notifier.start() 67 | self.wd=[] 68 | self.recursive=recursive 69 | for d in dirs: 70 | self.wd.append(self.wm.add_watch(d, mask, rec=recursive, auto_add=True)) 71 | def stop(self): 72 | try: 73 | for wd in self.wd: 74 | self.wm.rm_watch(wd.values(),rec=self.recursive) 75 | self.notifier.stop() ##todo: should be checking if there are any other watches still active? 76 | except: 77 | print 'Error removing watch' 78 | import traceback,sys 79 | print traceback.format_exc(sys.exc_info()[2]) 80 | def process_IN_MODIFY(self, event): 81 | path=os.path.join(event.path, event.name) 82 | self.cb(path,'MODIFY',event.is_dir) 83 | def process_IN_MOVED_FROM(self, event): 84 | path=os.path.join(event.path, event.name) 85 | self.cb(path,'MOVED_FROM',event.is_dir) 86 | def process_IN_MOVED_TO(self, event): 87 | path=os.path.join(event.path, event.name) 88 | self.cb(path,'MOVED_TO',event.is_dir) 89 | def process_IN_CREATE(self, event): 90 | path=os.path.join(event.path, event.name) 91 | self.cb(path,'CREATE',event.is_dir) 92 | def process_IN_DELETE(self, event): 93 | path=os.path.join(event.path, event.name) 94 | self.cb(path,'DELETE',event.is_dir) 95 | def process_default(self, event=None): 96 | pass 97 | -------------------------------------------------------------------------------- /modules/picty/collectiontypes/simpleview.py: -------------------------------------------------------------------------------- 1 | from picty import baseobjects, viewsupport, pluginmanager, simple_parser as sp 2 | import bisect 3 | import cPickle 4 | 5 | class SimpleView(baseobjects.ViewBase): 6 | def __init__(self,key_cb=viewsupport.get_mtime,items=[],collection=None): 7 | self.items=[] 8 | for item in items: 9 | self.add(key_cb(item),item) 10 | self.key_cb=key_cb 11 | self.sort_key_text='' 12 | for text,cb in collection.browser_sort_keys.iteritems(): 13 | if cb==key_cb: 14 | self.sort_key_text=text 15 | self.filter_tree=None 16 | self.filter_text='' 17 | self.reverse=False 18 | self.collection=collection 19 | self.loaded=False 20 | 21 | def set_cb(self): 22 | for text,cb in self.collection.browser_sort_keys.iteritems(): 23 | if self.sort_key_text==text: 24 | self.key_cb=cb 25 | 26 | def load(self,file_handle): 27 | ''' 28 | reconstruct the view by loading it from the file_handle 29 | using pickle to load the keys and current filter 30 | ''' 31 | items,self.filter_text,self.reverse,self.sort_key_text = cPickle.load(file_handle) 32 | self.set_cb() 33 | self.items = [[key, self.collection[self.collection.find(uid)] ] for (key,uid) in items] 34 | self.loaded=True 35 | def save(self,file_handle): 36 | ''' 37 | save the list of keys and uids in the view to the file_handle 38 | ''' 39 | items = [(key,item.uid) for (key,item) in self.items] 40 | view_data = (items,self.filter_text,self.reverse,self.sort_key_text) 41 | cPickle.dump(view_data,file_handle,-1) 42 | print 'SAVED VIEW',self 43 | def copy(self): 44 | dup=SimpleView(self.key_cb,[],self.collection) 45 | dup.sort_key_text=self.sort_key_text 46 | dup.filter_tree=self.filter_tree 47 | dup.filter_text=self.filter_text 48 | dup.reverse=self.reverse 49 | dup.items[:]=self.items[:] 50 | return dup 51 | def set_filter(self,expr): 52 | self.filter_tree=sp.parse_expr(viewsupport.TOKENS[:],expr,viewsupport.literal_converter) 53 | def clear_filter(self,expr): 54 | self.filter_tree=None 55 | def add(self,key,item,apply_filter=True): 56 | if apply_filter and self.filter_tree: 57 | if not sp.call_tree(bool,self.filter_tree,viewsupport.converter,item): 58 | return False 59 | bisect.insort(self.items,[key,item]) 60 | return True 61 | def remove(self,key,item): 62 | ind=bisect.bisect_left(self.items,[key,item]) 63 | i=list.__getitem__(self.items,ind) 64 | if key==i[0]: 65 | if item==i[1]: 66 | list.pop(self.items,ind) 67 | return 68 | raise KeyError 69 | def add_item(self,item,apply_filter=True): 70 | if self.add(self.key_cb(item),item,apply_filter): 71 | pluginmanager.mgr.callback_collection('t_collection_item_added_to_view',self.collection,self,item) 72 | def find_item(self,item): 73 | i=bisect.bisect_left(self.items,[self.key_cb(item),item]) 74 | if i>=len(self) or i<0: 75 | return -1 76 | if self.items[i][1]==item: 77 | return i if not self.reverse else len(self.items)-1-i 78 | return -1 79 | def del_ind(self,ind): 80 | ##todo: check ind is in the required range 81 | if self.reverse: 82 | i=len(self.items)-1-ind 83 | pluginmanager.mgr.callback_collection('t_collection_item_removed_from_view',self.collection,self,self.items[i][1]) 84 | del self.items[i] 85 | else: 86 | pluginmanager.mgr.callback_collection('t_collection_item_removed_from_view',self.collection,self,self.items[ind][1]) 87 | del self.items[ind] 88 | def del_item(self,item): 89 | ind=self.find_item(item) 90 | if ind>=0: 91 | self.del_ind(ind) 92 | return True 93 | return False 94 | def __call__(self,index): 95 | if index<0 or index>=len(self): 96 | return 97 | return self[index] 98 | def __getitem__(self,index): 99 | if self.reverse: 100 | return self.items[-1-index][1] 101 | else: 102 | return self.items[index][1] 103 | def __len__(self): 104 | return len(self.items) 105 | def get_items(self,first,last): 106 | if self.reverse: 107 | return [i[1] for i in self.items[len(self.items)-last:len(self.items)-first]] 108 | else: 109 | return [i[1] for i in self.items[first:last]] 110 | def get_selected_items(self): 111 | return [i[1] for i in self.items if i[1].selected] 112 | def empty(self): 113 | del self.items[:] 114 | 115 | 116 | baseobjects.register_view('SIMPLEVIEW',SimpleView) 117 | -------------------------------------------------------------------------------- /modules/picty/uitools/floats.py: -------------------------------------------------------------------------------- 1 | import gtk 2 | import toolbar_helpers 3 | 4 | 5 | class FloaterBase: 6 | ''' 7 | provides common functionality for floating windows provided by the app (e.g. the fullscreen window, the map window etc) 8 | ''' 9 | def move_to_other_monitor(self,ref_window=None,is_fullscreen=False): 10 | if ref_window is not None: 11 | screen = ref_window.get_screen() 12 | (xs,ys) = ref_window.get_position() 13 | else: 14 | screen = self.get_screen() 15 | (xs,ys) = self.get_position() 16 | (x,y) = self.get_position() 17 | num_mons = screen.get_n_monitors() 18 | if num_mons<=1: 19 | return 20 | mon_old = screen.get_monitor_at_point(xs,ys) 21 | mon_curr = screen.get_monitor_at_point(x,y) 22 | if mon_old!=mon_curr: 23 | return 24 | if is_fullscreen: 25 | self.unfullscreen() 26 | mon_new = num_mons-mon_old-1 27 | old_rect = screen.get_monitor_geometry(mon_old) 28 | new_rect = screen.get_monitor_geometry(mon_new) 29 | new_x = (x - old_rect.x) + new_rect.x 30 | new_x = min(new_x,new_rect.x+3*new_rect.width/4) 31 | new_y = (y - old_rect.y) + new_rect.y 32 | new_y = min(new_y,new_rect.y+3*new_rect.height/4) 33 | self.move(new_x,new_y) 34 | if is_fullscreen: 35 | self.fullscreen() 36 | def move_to_this_monitor(self,ref_window=None,is_fullscreen=False): 37 | if ref_window is not None: 38 | screen = ref_window.get_screen() 39 | (xs,ys) = ref_window.get_position() 40 | else: 41 | screen = self.get_screen() 42 | (xs,ys) = self.get_position() 43 | (x,y) = self.get_position() 44 | num_mons = screen.get_n_monitors() 45 | if num_mons<=1: 46 | return 47 | mon_old = screen.get_monitor_at_point(xs,ys) 48 | mon_curr = screen.get_monitor_at_point(x,y) 49 | if mon_old==mon_curr: 50 | return 51 | if is_fullscreen: 52 | self.unfullscreen() 53 | mon_new = mon_old 54 | old_rect = screen.get_monitor_geometry(mon_curr) 55 | new_rect = screen.get_monitor_geometry(mon_new) 56 | new_x = (x - old_rect.x) + new_rect.x 57 | new_x = min(new_x,new_rect.x+3*new_rect.width/4) 58 | new_y = (y - old_rect.y) + new_rect.y 59 | new_y = min(new_y,new_rect.y+3*new_rect.height/4) 60 | self.move(new_x,new_y) 61 | if is_fullscreen: 62 | self.fullscreen() 63 | 64 | 65 | def toggle_panel(self,widget): 66 | if widget.get_active(): 67 | self.show() 68 | else: 69 | self.hide() 70 | def close_button_cb(self,widget,event): 71 | if self.toggle: 72 | self.toggle.set_active(False) 73 | return True 74 | 75 | 76 | 77 | class FloatingPanel(gtk.Dialog,FloaterBase): 78 | ''' 79 | A floating panel is derived from GTK dialog 80 | ''' 81 | def __init__(self,title): 82 | gtk.Dialog.__init__(self,title) 83 | self.set_default_size(300,400) 84 | self.set_title(title) 85 | # self.set_deletable(False) 86 | self.connect("delete-event",self.close_button_cb) 87 | self.toggle=None 88 | 89 | class FloatingWindow(gtk.Window,FloaterBase): 90 | ''' 91 | A floating panel is derived from GTK window 92 | ''' 93 | def __init__(self,title): 94 | gtk.Window.__init__(self) 95 | self.set_default_size(300,400) 96 | # self.set_deletable(False) 97 | self.connect("delete-event",self.close_button_cb) 98 | self.toggle=None 99 | 100 | 101 | class FloatingPanelManager: 102 | ''' 103 | plugins and the main app should use the instance of this class in mainframe to manage floating panels and windows 104 | the helper creates and the panel and adds a toolbar button to hide/show it 105 | ''' 106 | def __init__(self,mainframe): 107 | self.panels={} 108 | self.mainframe=mainframe 109 | def add_panel(self,title,tooltip_text=None,icon_name=None,panel=True,add_to_toolbar=True,use_other_monitor=True): 110 | if panel: 111 | p=FloatingPanel(title) 112 | else: 113 | p=FloatingWindow(title) 114 | p.set_transient_for(self.mainframe.window) 115 | p.set_destroy_with_parent(True) 116 | if use_other_monitor: 117 | p.move_to_other_monitor() 118 | self.panels[title]=p 119 | if icon_name: 120 | p.toggle=gtk.ToggleToolButton(icon_name) 121 | p.toggle.show() 122 | if add_to_toolbar: 123 | toolbar_helpers.add_item(self.mainframe.toolbar1,p.toggle,p.toggle_panel,title,tooltip_text) 124 | return p 125 | def remove_panel(self,title): 126 | p=self.panels[title] 127 | if p.toggle!=None: 128 | self.mainframe.toolbar1.remove(p.toggle) 129 | p.toggle.destroy() 130 | p.destroy() 131 | del self.panels[title] 132 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | Project todo list 2 | ================= 3 | 4 | The purpose of this document is to keep track of short-term plans (up to 6 months ahead). 5 | As stuff is completed or plans change the list should be updated. 6 | 7 | * Transfer plugin should get preference widgets from collections (e.g. specify import location, metadata handling, privacy etc) 8 | * All collections should have an offline member and when offline turn off GUI that needs read or write access to the store 9 | * Simplify collection initialization/open/close (make interface more uniform) 10 | * The OSD delete button becomes a toggle. Delete is committed along with metadata changes (requires the collection to be online) 11 | * Each collection should specify the metadata that is editable and/or provide the editing widgets 12 | * Support GEO (GPS) info in flickr 13 | * Drag to/from desktop or other apps needs more support 14 | * Add toolbar button options to scan for updates or fully reimport the collection 15 | * Logging pane in the Transfer plugin 16 | * Don't allow duplicates of a tag in an image 17 | * Collections need icons on the start page 18 | * Bug fix: count of selected items sometimes get out of sync 19 | 20 | * Move collection handling (scanning/updating/filter/operations) to a separate process 21 | * Client/Server model 22 | * GUI = client, requests images from server 23 | * Server maintains the index and datastore of underlying images in a collection 24 | * Server process watches for changes to images and updates index/datastore appropriately, notifying GUI and getting user feedback as necessary 25 | * Optional "always on" server (tracks all changes to images in monitored 26 | collections even while picty client is not running) 27 | * This is hard because... 28 | * Passing data between processes is slow 29 | * High priority tasks need to interrupt with low latency 30 | * Have to keep many "lists" of images: 31 | * Master list: server side, keeps track of: uid, metadata, thumbnail info, full image 32 | * View: list known to both server and client, each item in the view is a tuple of (sortkey,uid) 33 | * A solution is to move the job handler to the main thread, call the jobs in a separate process and use 34 | gobject signals (and/or dbus) to handle notifications from the jobs 35 | * The job handler assembles the information needed for the job and writes changes back to the items/view/collection 36 | * The gobject loop supports callbacks 37 | * Avoids soem nasty thread synchroniization issues by enforcing safer data manipulation 38 | * Only do the intensive work on a separate process 39 | * E.g. for a write metadata request 40 | 1. create a list of changed items on the main thread 41 | 2. dispatch the intructions to write changes to the list of items as a batch (or with single requests) 42 | * Need a locking mechanism: user should not be able to modify items that are being modified by background processes 43 | * Need a cancellation mechanism: any job should be interruptable 44 | * Plugins will now get their update messages on the main thread instead of on a worker thread (plugin IPC too much of a mess) 45 | 46 | 47 | * Overhaul image class, metadata and image handling 48 | * track file attributes (file size, image dimensions, mtime, presence of thumbnails) 49 | * mimetype/store specific handler for metadata read/write and image processing 50 | * must be able to handle cloud-based collections 51 | * lossless image manipulation options (keep record of changes, and a cached version 52 | of the altered image) 53 | * support for XMP sidecars (for images that don't support metadata read/write) 54 | * XMP sidecar policies 55 | 1. Copy all metadata from source image/Store only changes from the source image 56 | 2. Use for images that don't support write/Use for all images 57 | * XMP 58 | 59 | * Update the browser 60 | * be more consistent with user theme (colors, look and feel) 61 | * size and align text better 62 | * improve keyboard navigation 63 | * add suport for horizontal (instead of vertical) scrolling 64 | 65 | * General UI 66 | * Padding and alignment tweaks to most of the dialog areas 67 | * Keyboard customization 68 | * Progress bar too distracting, messages don't always disapper (e.g. job cancelled) 69 | and messages not always useful 70 | 71 | * Image viewer enhancements 72 | * Implement the image viewer as a gtk.Container that can dynamically place/size/hide gtk widgets 73 | * Example use: when mouse over right side of image show a zoom slider and combo box 74 | * Keyboard toggles for displaying toolbar 75 | 76 | * Plugins 77 | * New plugins 78 | * Loupe tool in the image viewer 79 | * Histogram 80 | * Exposure tweaks 81 | * More flexible camera import (folder naming scheme) 82 | * Managing Sets/Albums for flickr/picasa uploads 83 | 84 | * Overhaul thumbnail support (e.g. allow user to store thumbs in custom 85 | cache location, support tumbler, use embedded image thumbnails and 86 | fallback to manual creation as necessary) 87 | * Delete thumbnails when image is deleted (add option to scan and delete redundant thumbnails) 88 | * Copy cached thumbnails on transfer (avoids downloading/recreating) 89 | 90 | 91 | picty (c) 2011 Damien Moore 92 | -------------------------------------------------------------------------------- /modules/picty/simple_parser.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | def split_expr(token,text,test_token_cb=None,cb_data=None): 7 | '''looks for token in the expression text and splits the expression 8 | around the token and returns [token,textleft,textright] if found 9 | otherwise returns [text]. this routine is "quote" aware (won't 10 | match on tokens enclosed in quotes) and strips white space around 11 | l and r values''' 12 | otext=text 13 | ltext='' 14 | while True: 15 | i=text.find(token) 16 | j=text.find('"') 17 | if 0<=i=0: 26 | ltext+=text[:j+k+2] 27 | text=text[j+k+2:] 28 | continue 29 | return [otext.strip()] 30 | 31 | def test_token_space(token,ltext,text,cb_data): 32 | ''' 33 | A callback used to determine whether a space char gets treated as an 34 | or operator or just as removable whitespace 35 | returns True if an operator, False otherwise. 36 | ltext is the substring to the left of token, text is the substring to 37 | the right''' 38 | for t in ('&','|'): 39 | if ltext.strip().endswith(t) or text.strip().startswith(t): 40 | return False 41 | return True 42 | 43 | 44 | def parse_expr(tokens,expr,conv,*args): 45 | '''converts a text representation of an expression into a parse 46 | tree using the grammar defined in tokens 47 | tokens define operations on l and r values 48 | tokens is a list of tuples in increasing order of precedence 49 | e.g. [('|',(bool.__or__,bool,bool)),('&',(bool.__and__,bool,bool)),('!',(_not,None,bool))] 50 | expr is a string: e.g. 'abc|de&fg' 51 | result is a nested list: e.g. [bool.__or__,'abc',[bool.__and__,'de',[_not,'','fg']]] 52 | run this program to see a complete example 53 | ''' 54 | token=tokens[0][0] 55 | text=expr 56 | if token==' ': 57 | tree=split_expr(token,text,test_token_space) 58 | else: 59 | tree=split_expr(token,text) 60 | if len(tree)>1: 61 | tree[0]=tokens[0][1] 62 | tree[1]=parse_expr(tokens[:],tree[1],conv,*args) 63 | tree[2]=parse_expr(tokens[:],tree[2],conv,*args) 64 | #convert literals to relevant types 65 | if type(tree[1])==str: 66 | try: 67 | tree[1]=conv[(str,tokens[0][1][1])](tree[1],*args) 68 | except: 69 | pass 70 | if type(tree[2])==str: 71 | try: 72 | tree[2]=conv[(str,tokens[0][1][2])](tree[2],*args) 73 | except: 74 | pass 75 | else: 76 | if len(tokens)>1: 77 | token=tokens.pop(0) 78 | tree=parse_expr(tokens[:],expr,conv,*args) 79 | else: 80 | tree=expr.replace('"','') 81 | return tree 82 | 83 | def call_tree(rtype,tree,conv,*args): 84 | ''' 85 | calls the tree 86 | conv is the conversion dictionary the key is a tuple of types (fromtype, totype), 87 | the value is the conversion function taking the arguments l,r,args 88 | args is the set of caller defined arguments passed to the token callables 89 | ''' 90 | if type(tree)==list: 91 | l=call_tree(tree[0][1],tree[1],conv,*args) 92 | r=call_tree(tree[0][2],tree[2],conv,*args) 93 | return tree[0][0](l,r,*args) 94 | else: 95 | # tree=tree.replace('"','') 96 | if rtype and type(tree)!=rtype: 97 | try: 98 | return conv[(type(tree),rtype)](tree,*args) 99 | except KeyError: 100 | return '' 101 | return tree 102 | 103 | 104 | if __name__=='__main__': 105 | def contains_tag(l,r,*args): 106 | return True 107 | 108 | def is_viewed(l,r,*args): 109 | return False 110 | 111 | def is_selected(l,r,*args): 112 | return True 113 | 114 | def _not(l,r,*args): 115 | return not r 116 | 117 | def str2bool(val): 118 | return True 119 | return keyword_filter(item,val) 120 | 121 | converter={ 122 | (str,bool):str2bool 123 | } 124 | 125 | print split_expr(' ','"abc de "f g') 126 | print split_expr(' ','"anc def" "ghu"') 127 | 128 | 129 | TOKENS=[ 130 | (' ',(bool.__or__,bool,bool)), 131 | ('&',(bool.__and__,bool,bool)), 132 | ('|',(bool.__or__,bool,bool)), 133 | ('!',(_not,None,bool)), 134 | ('tag=',(contains_tag,None,str)), 135 | ('viewed',(is_viewed,None,None)), 136 | ('selected',(is_selected,None,None)) 137 | ] 138 | 139 | ##sample expression tree 'abc def&ab|cd cd' 140 | exprs=( 141 | 'samantha', 142 | 'abc def&ab|cd cd', 143 | '!selected "selected"', 144 | 'damien & sammi', 145 | 'damien & | sammi', 146 | ) 147 | 148 | print 'TEST EXPRESSIONS' 149 | for expr in exprs: 150 | print 'expression',expr 151 | tree=parse_expr(TOKENS[:],expr) 152 | print 'tree',tree 153 | print 'result',call_tree(bool,tree,converter) 154 | -------------------------------------------------------------------------------- /user-guide/start.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | You have two options for running picty: install from binary packages on Ubuntu Linux and Windows, or download and run the source directly (Mac, Linux, and Windows). The windows version lacks some of the features of the linux (and Mac) version. 5 | 6 | Binary Packages 7 | --------------- 8 | 9 | Unstable, testing releases are available for 10 | 11 | * Windows users: sourceforge hosted binaries at 12 | http://sourceforge.net/projects/picty/files 13 | 14 | * Ubuntu users: install the picty PPA following the directions at 15 | http://launchpad.net/~damien-moore/+archive/ppa 16 | 17 | Running from Source 18 | ------------------- 19 | 20 | *Windows* 21 | 22 | You will need to download: 23 | 24 | * Python 2.7 from www.python.org 25 | * PyGtk 2.0 all-in-one installer from http://www.pygtk.org/downloads.html 26 | * pyexiv2 installer from http://tilloy.net/dev/pyexiv2/download.html (make sure you get the python 2.7 version) 27 | * Bazaar Version Control Sytem: http://wiki.bazaar.canonical.com/Download 28 | * Optional: osmgpsmap (for GPS mapping plugin to work) 29 | * Optional: flickrapi (for flickr support) 30 | 31 | Open a terminal (Start menu -> Run -> Cmd.exe) and type:: 32 | 33 | cd 34 | bzr branch lp:picty 35 | cd picty 36 | c:\python27\python bin\picty 37 | 38 | 39 | *Ubuntu and other Debian Based Linux System* 40 | 41 | To install the required dependencies, open a linux terminal and type:: 42 | 43 | sudo apt-get install bzr python-pyinotify python-pyexiv2 python-gtk2 python-gnome2 dcraw python-osmgpsmap python-flickrapi 44 | 45 | (*bzr* is to get the code, and *dcraw*, *python-osmgpsmap* and *python-flickrapi* are optional) 46 | 47 | To get the code:: 48 | 49 | cd 50 | bzr branch lp:picty 51 | 52 | To run:: 53 | 54 | cd picty 55 | bin/picty 56 | 57 | To update to the latest version:: 58 | 59 | cd picty 60 | bzr pull 61 | 62 | You can also install the program into the system, which means picty will show up in your system application menus and be registered as a handler for cameras and image files -- see the INSTALL file 63 | 64 | *Other Linux* 65 | 66 | The basic instructions for Ubuntu and other Debian based systems apply except that you won't be using apt-get to install the packages picty needs and they may be named slightly differently. 67 | 68 | Running picty for the First Time 69 | ================================ 70 | 71 | Most users will want to manage a collection of photos that are already 72 | present in their file system, so the following steps will walk you through 73 | the steps to do that. Note that during this process picty is not going to 74 | alter your images or the directory structure in any way, so it should be 75 | perfectly safe to test picty on your photo collection. 76 | 77 | 1. When you run picty for the first time you will be greeted with the 78 | following window: 79 | 80 | .. image:: screenshots/start.png 81 | 82 | 2. Click on the button labeled ``New Collection`` and you will see the 83 | following dialog: 84 | 85 | .. image:: screenshots/new-collection.png 86 | 87 | On the left side of the dialog you will see a list of the collection types 88 | that picty supports and "Local Store" should be selected. This is the default 89 | collection type and the one you should use for a local photo collection. 90 | 91 | 3. You need to give the collection a name, and specify the directory path 92 | to your images. I have used the name ``main`` and the path 93 | ``/home/damien/Pictures``. Press the "..." button to use the system folder 94 | picker to specify the image path. 95 | 96 | .. image:: screenshots/new-collection-details.png 97 | 98 | 4. There are also a number of advanced options that you can change, but the 99 | defaults should work fine for most users. These are discussed in more detail 100 | in the `collections `_ topic. 101 | 102 | 5. Now click on the button labeled ``Create`` to create your collection. 103 | (The create button will be disabled unless you enter a valid name and 104 | folder for the collection.) 105 | 106 | As soon as the collection is created 107 | picty is going to scan the image folder and (by default) all 108 | of its subfolders for photos and videos. Then picty will generate a 109 | thumbnail and read a subset of its metadata (tags and other information 110 | embedded in the image file). Depending on how many images you have in 111 | your collection, this could take some time, especially the first run. picty 112 | stores the information about the collection in a cache file so that 113 | loading the collection should be very fast after this initial scan has 114 | been completed. 115 | 116 | If all has gone well, the main window should display a grid 117 | of image thumbnails in a notepage like the following: 118 | 119 | .. image:: screenshots/new-collection-created.png 120 | 121 | As the scan continues, new images will be added to this page. 122 | 123 | You should notice that while the scan is going on the app remains (mostly) responsive. 124 | You can view and edit images while the scan continues in the background. 125 | 126 | 127 | *Next:* Learn about picty's `user interface `_ 128 | 129 | Or go back to the `index `_ 130 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ================================== 2 | picty - A photo collection manager 3 | ================================== 4 | 5 | .. image:: icons/picty-polaroids-and-frame.png 6 | :align: center 7 | :scale: 50 % 8 | 9 | picty is a photo manager designed around managing metadata and a lossless approach 10 | to image handling. 11 | 12 | metadata: descriptive and other information about images 13 | (created by you, your camera or the programs you use) that are 14 | embedded inside image files alongside the pixels. 15 | 16 | lossless: by only ever writing information about images, including 17 | image processing instructions, as metadata, the original image pixels 18 | are never altered allowing you to preserve the images as they were taken 19 | on your camera. 20 | 21 | Key features: 22 | 23 | * Supports big photo collections (20,000 plus images). 24 | * Open more than one collection at a time and transfer images between them. 25 | * Collections are: 26 | 27 | - Folders of images in your local file system. 28 | - Images on cameras, phones and other media devices. 29 | - Images on photo hosting services you use (Flickr currently supported). 30 | 31 | * picty does not "Import" photos into its own database, it simply provides 32 | an interface for accessing them wherever they are. To keep things snappy 33 | and to allow you to browse even if you are offline, picty maintains a 34 | cache of thumbnails and metadata. 35 | 36 | * Reads and writes metadata in industry standard formats Exif, IPTC and Xmp 37 | 38 | * Lossless approach: 39 | 40 | - picty writes all changes including image edits as metadata. e.g. an image 41 | crop is stored as any instruction, the original pixels remain in the file 42 | - Changes are stored in picty's collection cache until you save your metadata 43 | changes to the images. You can easily revert unsaved changes that you don't like. 44 | 45 | * Basic image editing: 46 | 47 | - Current support for basic image enhancements such as brightness, contrast, color, cropping, and straightening. 48 | - Improvements to those tools and other tools coming soon (red eye reduction, levels, curves, noise reduction) 49 | 50 | * Image tagging: 51 | 52 | - Use standard IPTC and Xmp keywords for image tags 53 | - A tag tree view lets you easily manage your tags and navigate your collection 54 | 55 | * Folder view: 56 | 57 | - Navigate the directory heirarchy of your image collection 58 | 59 | * Multi-monitor support 60 | 61 | - picty can be configured to let you browse your collection on one screen and view full screen images on another. 62 | 63 | * Customizable 64 | 65 | - Create launchers for external tools 66 | - Supports plugins - many of the current features (tagging and folder views, and all of the image editing tools) are provided by plugins 67 | - Written in python - batteries included! 68 | 69 | Get picty 70 | ---------- 71 | 72 | Nightly builds for Ubuntu 12.04 and above are available at the launchpad ppa: https://launchpad.net/~damien-moore/+archive/ppa 73 | 74 | Experimental Windows builds can be found at: http://sourceforge.net/projects/picty/files/ 75 | 76 | Or run from source... 77 | 78 | Running picty from source 79 | ------------------------- 80 | 81 | **Get the source** 82 | 83 | :: 84 | 85 | git clone https://github.com/spillz/picty 86 | 87 | **Dependencies** 88 | 89 | The python packages required to run picty are (available in most linux repos):: 90 | 91 | python (2.5 - 2.7) 92 | python-pil 93 | python-gtk2 94 | python-gnome2 95 | python-pyexiv2 96 | python-pyinotify 97 | 98 | recommended packages:: 99 | 100 | dcraw (basic raw processing support) 101 | totem (video thumbnailing) 102 | python-flickrapi (flickr collection support) 103 | python-osmgpsmap (geotagging support) 104 | 105 | **Run picty** 106 | 107 | Run the following commands in the terminal:: 108 | 109 | cd 110 | bin/picty 111 | 112 | or see the INSTALL file for installation information. The benefits of installing are media support, desktop menus, and file manager integration (right click, open with picty for any image). 113 | 114 | Documentation and Support 115 | ----------- 116 | 117 | A very incomplete draft of the user documentation is contained within the source code and can be viewed on `GitHub `_. 118 | 119 | Support and other information about picty is available at google groups: http://groups.google.com/group/pictyphotomanager 120 | 121 | Or file an issue on GitHub: https://github.com/spillz/picty/issues 122 | 123 | License Information 124 | ------------------- 125 | 126 | `(c)` 2013 Damien Moore 127 | 128 | 129 | License: GPL v3 130 | 131 | This program is free software: you can redistribute it and/or modify 132 | it under the terms of the GNU General Public License as published by 133 | the Free Software Foundation, either version 3 of the License, or 134 | (at your option) any later version. 135 | 136 | This program is distributed in the hope that it will be useful, 137 | but WITHOUT ANY WARRANTY; without even the implied warranty of 138 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 139 | GNU General Public License for more details. 140 | 141 | You should have received a copy of the GNU General Public License 142 | along with this program. If not, see . 143 | -------------------------------------------------------------------------------- /modules/picty/fstools/fileops.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | ##standard imports 23 | import os 24 | import os.path 25 | import threading 26 | import gobject 27 | 28 | ##picty imports 29 | from picty import settings 30 | 31 | ## contains a list of errors 32 | ## each error is a tuple (optype,imagepath,[destpath]) 33 | fileoperrors=[] 34 | 35 | ##todo: list of items could be the selection in a view/collection (so check selected bit before copy) 36 | 37 | 38 | class Worker: 39 | def __init__(self): 40 | self.active=False 41 | self.kill=False 42 | 43 | def _delete(self): 44 | for i in range(len(self.items)): 45 | item=self.items[i] 46 | if self.kill: 47 | self.active=False 48 | return 49 | if self.cb: 50 | gobject.idle_add(self.cb,None,1.0*i/len(self.items),'Deleting '+item.uid) 51 | if not self.collection.delete_item(item): 52 | fileoperrors.append(('del',item)) 53 | if self.cb: 54 | gobject.idle_add(self.cb,None,2.0,'Finished Deleting') 55 | self.active=False 56 | 57 | def delete(self,collection,items,cb,selected_only=True): 58 | if self.active: 59 | return False 60 | self.active=True 61 | self.kill=False 62 | self.collection=collection 63 | if selected_only: 64 | self.items=[] 65 | for i in range(len(items)): 66 | item=items(i) 67 | if item.selected: 68 | self.items.append(item) 69 | else: 70 | self.items=items 71 | self.cb=cb 72 | self.thread=threading.Thread(target=self._delete) 73 | self.thread.start() 74 | return True 75 | 76 | def _copy(self): 77 | for i in range(len(self.items)): 78 | item=self.items[i] 79 | if self.kill: 80 | self.active=False 81 | return 82 | if self.cb: 83 | gobject.idle_add(self.cb,None,1.0*i/len(self.items),'Copying '+item.uid) 84 | try: 85 | path = self.collection.get_path(item) 86 | fin=open(path,'rb') 87 | fout=open(os.path.join(self.destdir,os.path.split(path)[1]),'wb') ##todo: check exists (and what about perms/attribs?) 88 | fout.write(fin.read()) 89 | except: 90 | fileoperrors.append(('copy',item,self.destdir)) 91 | if self.cb: 92 | gobject.idle_add(self.cb,None,2.0,'Finished Copying') 93 | self.active=False 94 | 95 | def copy(self,collection,items,destdir,cb,selected_only=True): 96 | if self.active: 97 | return False 98 | self.active=True 99 | self.kill=False 100 | self.collection=collection 101 | if selected_only: 102 | self.items=[] 103 | for i in range(len(items)): 104 | item=items(i) 105 | if item.selected: 106 | self.items.append(item) 107 | else: 108 | self.items=items 109 | self.cb=cb 110 | self.destdir=destdir 111 | self.thread=threading.Thread(target=self._copy) 112 | self.thread.start() 113 | return True 114 | 115 | def _move(self): 116 | for i in range(len(self.items)): 117 | item=self.items[i] 118 | if self.kill: 119 | self.active=False 120 | return 121 | if self.cb: 122 | gobject.idle_add(self.cb,None,1.0*i/len(self.items),'Moving '+item.uid) 123 | try: 124 | path = self.collection.get_path(item) 125 | os.renames(path, os.path.join(self.destdir,os.path.split(path)[1])) 126 | except: 127 | fileoperrors.append(('move',item,self.destdir)) 128 | if self.cb: 129 | gobject.idle_add(self.cb,None,2.0,'Finished Moving') 130 | self.active=False 131 | 132 | def move(self,collection,items,destdir,cb,selected_only=True): 133 | if self.active: 134 | return False 135 | self.active=True 136 | self.kill=False 137 | self.collection=collection 138 | if selected_only: 139 | self.items=[] 140 | for i in range(len(items)): 141 | item=items(i) 142 | if item.selected: 143 | self.items.append(items(i)) 144 | else: 145 | self.items=items 146 | self.cb=cb 147 | self.destdir=destdir 148 | self.thread=threading.Thread(target=self._move) 149 | self.thread.start() 150 | return True 151 | 152 | def kill_op(self): 153 | self.kill=True 154 | 155 | def is_active(self): 156 | return self.active 157 | 158 | worker=Worker() 159 | -------------------------------------------------------------------------------- /modules/pyfb/pyfb.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an Easy to Use Python Interface to the Facebook Graph API 3 | 4 | It gives you methods to access your data on facebook and 5 | provides objects instead of json dictionaries! 6 | """ 7 | 8 | import webbrowser 9 | from client import FacebookClient, PyfbException 10 | 11 | class Pyfb(object): 12 | """ 13 | This class is Facade for FacebookClient 14 | """ 15 | 16 | def __init__(self, app_id, access_token=None, raw_data=False): 17 | 18 | self._client = FacebookClient(app_id, access_token, raw_data) 19 | 20 | def authenticate(self): 21 | """ 22 | Open your prefered web browser to make the authentication request 23 | """ 24 | self._show_in_browser(self.get_auth_url()) 25 | 26 | def get_authentication_code(self): 27 | """ 28 | Open your prefered web browser to make the authentication request 29 | """ 30 | self._show_in_browser(self.get_auth_code_url()) 31 | 32 | def get_auth_url(self, redirect_uri=None): 33 | """ 34 | Returns the authentication url 35 | """ 36 | return self._client.get_auth_token_url(redirect_uri) 37 | 38 | def get_auth_code_url(self, redirect_uri=None, state=None): 39 | """ 40 | Returns the url to get a authentication code 41 | """ 42 | return self._client.get_auth_code_url(redirect_uri, state=state) 43 | 44 | def get_access_token(self, app_secret_key, secret_code, redirect_uri=None): 45 | """ 46 | Gets the access token 47 | """ 48 | return self._client.get_access_token(app_secret_key, secret_code, redirect_uri) 49 | 50 | def exchange_token(self, app_secret_key, exchange_token): 51 | """ 52 | Exchanges a short-lived access token (like those obtained from client-side JS api) 53 | for a longer-lived access token 54 | """ 55 | return self._client.exchange_token(app_secret_key, exchange_token) 56 | 57 | def show_dialog(self, redirect_uri=None): 58 | """ 59 | Open your prefered web browser to make the authentication request 60 | """ 61 | self._show_in_browser(self.get_dialog_url(redirect_uri=redirect_uri)) 62 | 63 | def get_dialog_url(self, redirect_uri=None): 64 | """ 65 | Returns a url inside facebook that shows a dialog allowing 66 | users to publish contents. 67 | """ 68 | return self._client.get_dialog_url(redirect_uri) 69 | 70 | def _show_in_browser(self, url): 71 | """ 72 | Opens your prefered web browser to make the authentication request 73 | """ 74 | webbrowser.open(url) 75 | 76 | def set_access_token(self, token): 77 | """ 78 | Sets the access token. Necessary to make the requests that requires autenthication 79 | """ 80 | self._client.access_token = token 81 | 82 | def set_permissions(self, permissions): 83 | """ 84 | Sets a list of data access permissions that the user must give to the application 85 | e.g: 86 | permissions = [auth.USER_ABOUT_ME, auth.USER_LOCATION, auth.FRIENDS_PHOTOS, ...] 87 | """ 88 | self._client.permissions = permissions 89 | 90 | def get_myself(self): 91 | """ 92 | Gets myself data 93 | """ 94 | return self._client.get_one("me", "FBUser") 95 | 96 | def get_user_by_id(self, id=None): 97 | """ 98 | Gets an user by the id 99 | """ 100 | if id is None: 101 | id = "me" 102 | return self._client.get_one(id, "FBUser") 103 | 104 | def get_friends(self, id=None): 105 | """ 106 | Gets a list with your friends 107 | """ 108 | return self._client.get_list(id, "Friends") 109 | 110 | def get_statuses(self, id=None): 111 | """ 112 | Gets a list of status objects 113 | """ 114 | return self._client.get_list(id, "Statuses") 115 | 116 | def get_photos(self, id=None): 117 | """ 118 | Gets a list of photos objects 119 | """ 120 | return self._client.get_list(id, "Photos") 121 | 122 | def get_comments(self, id=None): 123 | """ 124 | Gets a list of photos objects 125 | """ 126 | return self._client.get_list(id, "Comments") 127 | 128 | def publish(self, message, id=None, **kwargs): 129 | """ 130 | Publishes a message on the wall 131 | """ 132 | return self._client.push(id, "feed", message=message, **kwargs) 133 | 134 | def comment(self, message, id=None, **kwargs): 135 | """ 136 | Publishes a message on the wall 137 | """ 138 | return self._client.push(id, "comments", message=message, **kwargs) 139 | 140 | def get_likes(self, id=None): 141 | """ 142 | Get a list of liked objects 143 | """ 144 | return self._client.get_list(id, "likes") 145 | 146 | def like(self, id): 147 | """ 148 | LIKE: It Doesn't work. Seems to be a bug on the Graph API 149 | http://bugs.developers.facebook.net/show_bug.cgi?id=10714 150 | """ 151 | print self.like.__doc__ 152 | return self._client.push(id, "likes") 153 | 154 | def delete(self, id): 155 | """ 156 | Deletes a object 157 | """ 158 | return self._client.delete(id) 159 | 160 | def fql_query(self, query): 161 | """ 162 | Executes a FBQL query 163 | """ 164 | return self._client.execute_fql_query(query) 165 | -------------------------------------------------------------------------------- /modules/picty/uitools/completions.py: -------------------------------------------------------------------------------- 1 | import gtk 2 | 3 | from picty import viewsupport 4 | 5 | #TODO: both of these classes need quite a bit of work to make the completions more useful 6 | # e.g. showing correct completions after a quote 7 | 8 | class TagCompletion(gtk.EntryCompletion): 9 | def __init__(self,entry,index=None): 10 | gtk.EntryCompletion.__init__(self) 11 | self.entry=entry 12 | self.liststore = gtk.ListStore(str, str) #text, icon 13 | self.update_keywords(index) 14 | self.set_model(self.liststore) 15 | 16 | cpb=gtk.CellRendererPixbuf() 17 | cpb.set_property("width",20) ##todo: don't hardcode the width 18 | self.pack_start(cpb,False) 19 | self.add_attribute(cpb, 'stock-id', 1) 20 | cpt=gtk.CellRendererText() 21 | # cpt.set_propery("weight",800) 22 | self.pack_start(cpt,False) 23 | self.add_attribute(cpt, 'text', 0) 24 | 25 | self.set_match_func(self.match_func) 26 | self.connect('match-selected', self.match_selected) 27 | entry.set_completion(self) 28 | self.lpos=0 29 | self.rpos=0 30 | 31 | def update_keywords(self,index=None): 32 | self.liststore.clear() 33 | if not index: 34 | return 35 | if 'Keywords' in index.index: 36 | for r in index.index['Keywords']: 37 | self.liststore.append([r,gtk.STOCK_ADD]) 38 | 39 | def compute_word_pos(self): 40 | pos=self.entry.get_property("cursor-position") 41 | key_string=self.entry.get_text() 42 | lpos=pos 43 | while lpos>0 and key_string[lpos-1].isalnum(): 44 | lpos-=1 45 | rpos=pos 46 | while rpos0 and key_string[lpos-1].isalnum(): 117 | lpos-=1 118 | rpos=pos 119 | while rpos. 22 | ''' 23 | 24 | 25 | import sys 26 | import os.path 27 | 28 | try: 29 | #TODO: relies on __file__ not existing on windows, that's a bit of a hack 30 | filename=os.path.abspath(__file__) 31 | user_local = os.path.expanduser('~/.local') 32 | prefix = user_local if filename.startswith(user_local) else '/usr' 33 | if filename==prefix+'/bin/picty': 34 | #installed locations on linux 35 | module_path=prefix+'/share/picty' 36 | icon_file = prefix+'/share/pixmaps/picty.png' 37 | splash_file=prefix+'/share/picty/icons/picty-polaroids-and-frame.png' 38 | else: 39 | #default locations when running from source 40 | module_path=os.path.join(os.path.split(os.path.split(filename)[0])[0],'modules') 41 | icon_file=os.path.join(os.path.split(os.path.split(filename)[0])[0],'desktop/picty.png') 42 | splash_file=os.path.join(os.path.split(os.path.split(filename)[0])[0],'icons/picty-polaroids-and-frame.png') 43 | print 'running picty with module path',module_path 44 | if prefix == '/usr': 45 | sys.path.insert(0,module_path) 46 | except: 47 | #installed locations on windows relative to picty.exe 48 | module_path='modules' 49 | icon_file='desktop/picty.png' 50 | splash_file='icons/picty-polaroids-and-frame.png' 51 | 52 | 53 | try: 54 | import gobject 55 | import gtk 56 | import pyexiv2 57 | gobject.threads_init() 58 | except: 59 | print 'ERROR: missing modules gobject, gtk, gio, and pyexiv2' 60 | import sys 61 | sys.exit() 62 | 63 | from picty import settings 64 | 65 | #on a windows py2exe build, an annoying dialog pops up on exit warning about errors 66 | #redirecting program output to a log effectively prevents the dialog. 67 | if settings.is_windows: 68 | import warnings 69 | warnings.simplefilter('ignore') #TODO: Is this needed, the next 3 lines might be enough 70 | f=open(os.path.join(settings.settings_dir,"picty-log.txt"),"w") 71 | sys.stderr = f 72 | sys.stdout = f 73 | 74 | if len(sys.argv) > 1: 75 | custom_dir=os.path.expanduser(os.path.join('~',sys.argv[1])) 76 | if os.path.isdir(custom_dir): 77 | settings.settings_dir=os.path.join(custom_dir,'settings') 78 | if not os.path.isdir(settings.settings_dir): 79 | os.makedirs(settings.settings_dir) 80 | settings.data_dir=os.path.join(custom_dir,'data') 81 | if not os.path.isdir(settings.data_dir): 82 | os.makedirs(settings.data_dir) 83 | settings.cache_dir=os.path.join(custom_dir,'cache') 84 | if not os.path.isdir(settings.cache_dir): 85 | os.makedirs(settings.cache_dir) 86 | else: 87 | print "Chosen test direcory does not exist:", custom_dir 88 | sys.exit() 89 | 90 | settings.init() ##todo: make more of the settings module dependant on this call?? 91 | settings.icon_file=icon_file 92 | settings.splash_file=splash_file 93 | settings.module_path=module_path 94 | 95 | from picty import mainframe 96 | 97 | class MainWindow: 98 | def __init__(self): 99 | 100 | self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) 101 | self.window.set_default_size(680, 400) 102 | self.window.set_title("picty") 103 | self.window.set_icon_from_file(icon_file) 104 | self.window.connect("destroy", self.destroy) 105 | 106 | #configure-event being used to save changes to screen size for start up 107 | self.window.connect("configure-event", self.app_win_resize) 108 | self.window.connect("window_state_event", self.window_new_state) 109 | 110 | self.mainframe = mainframe.MainFrame(self.window) 111 | 112 | #Resize app window to size when last quit 113 | #TODO: could also do this for plugins that popup external windows 114 | if self.mainframe.toplevel_window_max: 115 | self.window.set_default_size(self.mainframe.toplevel_window_state[0], self.mainframe.toplevel_window_state[1]) 116 | self.window.maximize() 117 | else: 118 | self.window.resize(self.mainframe.toplevel_window_state[0], self.mainframe.toplevel_window_state[1]) 119 | self.window.move(self.mainframe.toplevel_window_state[2], self.mainframe.toplevel_window_state[3]) 120 | 121 | vb=gtk.VBox() 122 | vb.pack_start(self.mainframe) 123 | self.window.add(vb) 124 | 125 | self.window.show() 126 | vb.show() 127 | self.mainframe.show() 128 | 129 | def window_new_state(self, widget, event): 130 | state = event.new_window_state 131 | if state == gtk.gdk.WINDOW_STATE_MAXIMIZED: 132 | self.mainframe.toplevel_window_max = True 133 | else: 134 | self.mainframe.toplevel_window_max = False 135 | 136 | def app_win_resize(self, widget, event): 137 | if not self.mainframe.toplevel_window_max: 138 | self.mainframe.toplevel_window_state = (event.width, event.height, event.x, event.y) 139 | 140 | def delete_event(self, widget, event, data=None): 141 | return False #allows the window to be destroyed 142 | 143 | def destroy(self, widget, data=None): 144 | gtk.main_quit() 145 | 146 | def main(self): 147 | gtk.main() 148 | 149 | if __name__ == "__main__": 150 | wnd = MainWindow() 151 | wnd.main() 152 | -------------------------------------------------------------------------------- /modules/picty/plugins/rotate.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty - Image Rotation Plugin 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | ''' 22 | 23 | import gtk 24 | from PIL import Image 25 | 26 | from picty import imagemanip 27 | from picty import settings 28 | from picty import pluginbase 29 | 30 | class RotatePlugin(pluginbase.Plugin): 31 | name='Rotate' 32 | display_name='Image Rotation' 33 | api_version='0.1.0' 34 | version='0.1.0' 35 | def __init__(self): 36 | self.rotate_mode=False 37 | def plugin_init(self,mainframe,app_init): 38 | #register a button in the viewer to enter rotate mode 39 | self.viewer=mainframe.iv 40 | 41 | self.unrotated_screen_image=None 42 | self.cur_size=None 43 | self.cur_zoom=None 44 | 45 | self.angle_adjustment=gtk.Adjustment(0,-180,180,0.01,0.1,0.1) 46 | self.angle_adjustment.connect("value-changed",self.rotate_adjust) 47 | self.slider=gtk.HScale(self.angle_adjustment) 48 | self.slider.set_draw_value(False) 49 | self.angle_entry=gtk.SpinButton(self.angle_adjustment,0.0,2) 50 | self.ok_button=gtk.Button("_Apply") 51 | self.ok_button.connect("clicked",self.rotate_do_callback) 52 | self.cancel_button=gtk.Button("_Cancel") 53 | self.cancel_button.connect("clicked",self.rotate_cancel_callback) 54 | 55 | self.rotate_bar=gtk.HBox() 56 | self.rotate_bar.pack_start(self.slider) 57 | self.rotate_bar.pack_start(self.angle_entry,False) 58 | self.rotate_bar.pack_start(self.cancel_button,False) 59 | self.rotate_bar.pack_start(self.ok_button,False) 60 | self.rotate_bar.show_all() 61 | imagemanip.transformer.register_transform('rotate',self.do_rotate_transform) 62 | 63 | def plugin_shutdown(self,app_shutdown=False): 64 | #deregister the button in the viewer 65 | if self.rotate_mode: 66 | self.reset(app_shutdown) 67 | imagemanip.transformer.deregister_transform('rotate') 68 | 69 | def do_rotate_transform(self,item,params): 70 | item.image=item.image.rotate(params['angle'],Image.BILINEAR,True) 71 | 72 | def viewer_register_shortcut(self,shortcut_toolbar): 73 | ''' 74 | called by the framework to register shortcut on mouse over commands 75 | append a tuple containing the shortcut commands 76 | ''' 77 | shortcut_toolbar.register_tool_for_plugin(self,'Rotate',self.rotate_button_callback,shortcut_toolbar.cb_showing_tranforms,['picty-image-rotate'],'Rotate or straighten this image',43) 78 | 79 | def rotate_button_callback(self,cmd): 80 | ''' 81 | the user has entered rotate mode 82 | set the viewer to a blocking mode to hand the plugin exclusive control of the viewer 83 | ''' 84 | item=self.viewer.item 85 | if not self.viewer.plugin_request_control(self): 86 | return 87 | self.rotate_mode=True 88 | self.viewer.image_box.pack_start(self.rotate_bar,False) 89 | self.viewer.image_box.reorder_child(self.rotate_bar,0) 90 | self.item=item 91 | 92 | def rotate_do_callback(self,widget): 93 | self.viewer.il.add_transform('rotate',{'angle':-self.angle_adjustment.get_value()}) 94 | #self.viewer.il.transform_image() 95 | self.reset() 96 | 97 | def rotate_cancel_callback(self,widget): 98 | if self.rotate_mode: 99 | self.reset() 100 | 101 | def reset(self,shutdown=False): 102 | self.rotate_mode=False 103 | self.item=None 104 | self.unrotated_screen_image=None 105 | self.viewer.image_box.remove(self.rotate_bar) 106 | self.viewer.plugin_release(self) 107 | self.angle_adjustment.set_value(0) 108 | if not shutdown: 109 | self.viewer.resize_and_refresh_view() 110 | 111 | def rotate_adjust(self,adjustment): 112 | if not self.rotate_mode: 113 | return 114 | self.viewer.resize_and_refresh_view(force=True) 115 | 116 | def viewer_release(self,force=False): 117 | self.reset(True) 118 | return True 119 | 120 | def t_viewer_sizing(self,size,zoom,item): 121 | if not self.rotate_mode: 122 | return 123 | if size!=self.cur_size or not self.unrotated_screen_image or self.viewer.zoom_level!='fit': 124 | self.unrotated_screen_image=item.image.copy() 125 | self.unrotated_screen_image.thumbnail(size) 126 | image=self.unrotated_screen_image.rotate(-self.angle_adjustment.get_value(),Image.NEAREST,expand=True) 127 | image.thumbnail(size) 128 | item.qview=imagemanip.image_to_pixbuf(image) 129 | self.cur_size=size 130 | self.cur_zoom=zoom 131 | return True 132 | 133 | def viewer_render_end(self,drawable,gc,item): 134 | if not self.rotate_mode: 135 | return 136 | W,H=self.viewer.imarea.window.get_size() 137 | 138 | #draw drag handles 139 | colormap=drawable.get_colormap() 140 | white= colormap.alloc_color('white') 141 | handle_gc=drawable.new_gc() 142 | handle_gc.set_function(gtk.gdk.XOR) 143 | handle_gc.set_foreground(white) 144 | handle_gc.set_background(white) 145 | 146 | grid_size=min(max(40,W/12),W) 147 | len_grid_x=int(W/grid_size) 148 | len_grid_y=int(H/grid_size) 149 | 150 | for i in range(len_grid_x): 151 | drawable.draw_line(handle_gc,i*grid_size+grid_size/2,0,i*grid_size+grid_size/2,H) 152 | 153 | for i in range(len_grid_y): 154 | drawable.draw_line(handle_gc,0,i*grid_size+grid_size/2,W,i*grid_size+grid_size/2) 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /modules/picty/pluginmanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | ''' 4 | 5 | picty 6 | Copyright (C) 2013 Damien Moore 7 | 8 | License: 9 | 10 | This program is free software: you can redistribute it and/or modify 11 | it under the terms of the GNU General Public License as published by 12 | the Free Software Foundation, either version 3 of the License, or 13 | (at your option) any later version. 14 | 15 | This program is distributed in the hope that it will be useful, 16 | but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | GNU General Public License for more details. 19 | 20 | You should have received a copy of the GNU General Public License 21 | along with this program. If not, see . 22 | ''' 23 | 24 | import gobject 25 | 26 | import pluginbase 27 | import settings 28 | 29 | ##todo: need try/except blocks around most of this stuff 30 | ##todo: have to make the pluginmanager methods threadsafe (plugins member could change in thread while being accessed in another) 31 | 32 | 33 | 34 | class PluginManager(): 35 | ''' 36 | PluginManager is used by the app to register plugins and notify them 37 | with callbacks for specific application events 38 | ''' 39 | def __init__(self): 40 | self.plugins=dict() 41 | self.mainframe=None 42 | self.collection_suppress={} 43 | self.registered_callbacks={} 44 | def instantiate_all_plugins(self): 45 | ##todo: check for plugin.name conflicts with existing plugins and reject plugin if already present 46 | print 'instantiating plugins except for',settings.plugins_disabled 47 | for plugin in pluginbase.Plugin.__subclasses__(): 48 | # try: 49 | self.plugins[plugin.name]=[plugin(),plugin] if plugin.name not in settings.plugins_disabled else [None,plugin] 50 | # except: 51 | # print 'Error initializing plugin',plugin.name 52 | def enable_plugin(self,name): 53 | ##todo: check for plugin.name conflicts with existing plugins and reject plugin if already present 54 | self.plugins[name][0]=self.plugins[name][1]() 55 | self.plugins[name][0].plugin_init(self.mainframe,False) 56 | self.plugins[name][0].viewer_register_shortcut(self.mainframe.viewer_toolbar) 57 | 58 | def disable_plugin(self,name): 59 | try: 60 | plugin=self.plugins[name][0] 61 | self.plugins[name][0]=None 62 | from uitools import overlay_tools 63 | overlay_tools.deregister_all_tools_for_plugin(plugin) 64 | plugin.plugin_shutdown(False) 65 | except: 66 | pass 67 | 68 | def init_plugins(self,mainframe,app_init=True): 69 | self.mainframe=mainframe 70 | self.callback('plugin_init',mainframe,app_init) 71 | def callback_plugin(self,plugin_name,interface_name,*args): 72 | ''' 73 | for each plugin in self.plugins that defines the interface, runs the callback. 74 | Used in the main app for interfaces that always return None 75 | ''' 76 | plugin=self.plugins[plugin_name][0] 77 | if plugin: 78 | getattr(plugin,interface_name)(*args) 79 | def callback(self,interface_name,*args): 80 | ''' 81 | for each plugin in self.plugins that defines the interface, runs the callback. 82 | Used in the main app for interfaces that always return None 83 | ''' 84 | for name,plugin in self.plugins.iteritems(): 85 | if plugin[0]: 86 | getattr(plugin[0],interface_name)(*args) 87 | def callback_all_until_false(self,interface_name,*args): 88 | ''' 89 | for each plugin in self.plugins that defines the interface, runs the callback. 90 | Will stop callbacks after the first instance that returns False 91 | ''' 92 | a=True 93 | for name,plugin in self.plugins.iteritems(): 94 | a=a and (plugin[0]==None or getattr(plugin[0],interface_name)(*args)) 95 | if not a: 96 | break 97 | return a 98 | def callback_first(self,interface_name,*args): 99 | ''' 100 | iterates over each plugin in self.plugins and runs the callback, stopping at the first 101 | that returns True. Used in cases where there should be exactly one handler 102 | ''' 103 | for name,plugin in self.plugins.iteritems(): 104 | if plugin[0] and getattr(plugin[0],interface_name)(*args): 105 | return True 106 | return False 107 | def callback_iter(self,interface_name,*args): 108 | ''' 109 | for each plugin in self.plugins that defines the interface, runs the callback 110 | and yields the result. Used in the main app for interfaces that return useful results 111 | ''' 112 | for name,plugin in self.plugins.iteritems(): 113 | yield plugin[0] and getattr(plugin[0],callback_name)(*args) 114 | def suspend_collection_events(self,collection): 115 | try: 116 | self.collection_suppress[collection]+=1 117 | except: 118 | self.collection_suppress[collection]=1 119 | self.callback('t_collection_modify_start_hint',collection) 120 | def resume_collection_events(self,collection): 121 | try: 122 | self.collection_suppress[collection]-=1 123 | except: 124 | pass 125 | if collection in self.collection_suppress and self.collection_suppress[collection]<=0: 126 | del self.collection_suppress[collection] 127 | self.callback('t_collection_modify_complete_hint',collection) 128 | def callback_collection(self,interface_name,collection,*args): 129 | # try: 130 | # if self.collection_suppress[collection]>0: 131 | # return 132 | # except: 133 | # pass 134 | for name,plugin in self.plugins.iteritems(): 135 | if plugin[0]: 136 | getattr(plugin[0],interface_name)(collection,*args) 137 | if interface_name in self.registered_callbacks: 138 | for h in self.registered_callbacks[interface_name]: 139 | gobject.idle_add(h,*((collection,)+args)) 140 | 141 | def register_callback(self,callback_name,handler): 142 | try: 143 | self.registered_callbacks[callback_name].append(handler) 144 | except KeyError: 145 | self.registered_callbacks[callback_name] = [handler] 146 | 147 | def deregister_callback(self,callback_name,handler): 148 | try: 149 | ind = self.registered_callbacks[callback_name].find(handler) 150 | if ind>=0: 151 | self.registered_callbacks[callback_name].pop(ind) 152 | except KeyError: 153 | return 154 | 155 | 156 | mgr=PluginManager() ##instantiate the manager (there can only be one) 157 | -------------------------------------------------------------------------------- /modules/picty/uitools/toolbar_helpers.py: -------------------------------------------------------------------------------- 1 | import gtk 2 | import gobject 3 | import overlay_tools 4 | 5 | #Custom toolbutton, toolitem and toolbar (makes it easier to plugins to add remove their own items for picty toolbars) 6 | class ToolButton(gtk.ToolButton): 7 | def __init__(self,label,callback,update_cb,icons,owner='Main',tooltip=None,priority=50,expand=False,): 8 | gtk.ToolButton.__init__(self,icons[0]) 9 | self.icons = icons 10 | self.owner = owner 11 | self.priority = priority 12 | self.update_cb = update_cb 13 | if isinstance(callback,tuple): 14 | self.connect("clicked",callback[0],*callback[1:]) 15 | else: 16 | self.connect("clicked",callback) 17 | if tooltip: 18 | self.set_tooltip_text(tooltip) 19 | if label: 20 | self.set_label(label) 21 | self.set_expand(expand) 22 | 23 | 24 | class ToggleToolButton(gtk.ToggleToolButton): 25 | def __init__(self,label,callback,update_cb,icons,owner='Main',tooltip=None,priority=50,expand=False,): 26 | gtk.ToggleToolButton.__init__(self,icons[0]) 27 | self.icons = icons 28 | self.owner = owner 29 | self.priority = priority 30 | self.update_cb = update_cb 31 | if isinstance(callback,tuple): 32 | self.connect("clicked",callback[0],*callback[1:]) 33 | else: 34 | self.connect("clicked",callback) 35 | if tooltip: 36 | self.set_tooltip_text(tooltip) 37 | if label: 38 | self.set_label(label) 39 | self.set_expand(expand) 40 | 41 | 42 | class ToolItem(gtk.ToolItem): 43 | def __init__(self,widget,owner='Main',update_cb=None,priority=50,expand=False): 44 | gtk.ToolItem.__init__(self) 45 | self.add(widget) 46 | self.owner = owner 47 | self.update_cb=update_cb 48 | self.priority = priority 49 | self.set_expand(expand) 50 | 51 | class Toolbar(gtk.Toolbar): 52 | def __init__(self): 53 | gtk.Toolbar.__init__(self) 54 | overlay_tools.overlay_groups.append(self) 55 | 56 | ##TODO: make these callbacks part of a derived class for the viewer 57 | def default_update_callback(self,tool,viewer): 58 | return self.cb_has_item(tool,viewer) 59 | 60 | def cb_has_item(self, tool, viewer): 61 | if viewer.item!=None: 62 | tool.set_sensitive(True) 63 | else: 64 | tool.set_sensitive(False) 65 | def cb_has_image(self, tool, viewer): 66 | if viewer.item!=None and 'qview' in viewer.item.__dict__ and viewer.item.qview is not None: 67 | tool.set_sensitive(True) 68 | else: 69 | tool.set_sensitive(False) 70 | def cb_showing_tranforms(self, tool, viewer): 71 | if viewer.item!=None: 72 | if 'ImageTransforms' in viewer.item.meta and viewer.il.want_transforms: 73 | tool.set_sensitive(True) 74 | return 75 | if 'ImageTransforms' not in viewer.item.meta: 76 | tool.set_sensitive(True) 77 | return 78 | tool.set_sensitive(False) 79 | def cb_has_image_edits(self, tool, viewer): 80 | if viewer.item!=None and 'ImageTransforms' in viewer.item.meta: 81 | tool.set_sensitive(True) 82 | else: 83 | tool.set_sensitive(False) 84 | def cb_item_changed(self, tool, viewer): 85 | if viewer.item!=None and viewer.item.is_meta_changed(): 86 | tool.set_sensitive(True) 87 | else: 88 | tool.set_sensitive(False) 89 | def cb_item_changed_icon(self, tool, viewer): 90 | if viewer.item!=None and viewer.item.is_meta_changed(): 91 | tool.set_sensitive(True) 92 | if viewer.item.is_meta_changed()==2: 93 | tool.set_stock_id(tool.icons[1]) 94 | else: 95 | tool.set_stock_id(tool.icons[0]) 96 | else: 97 | tool.set_sensitive(False) 98 | tool.set_stock_id(tool.icons[0]) 99 | def cb_item_not_deleted(self,tool, viewer): 100 | if viewer.item!=None and viewer.item.is_meta_changed()!=2: 101 | tool.set_sensitive(True) 102 | else: 103 | tool.set_sensitive(False) 104 | 105 | def register_tool(self,name,action_callback=None,update_callback=overlay_tools.show_on_hover,icons=[],owner='Main',tooltip='This is a tooltip',priority=50,toggle=False): 106 | if toggle: 107 | new_t=ToggleToolButton(name,action_callback,update_callback,icons,owner,tooltip,priority) 108 | else: 109 | new_t=ToolButton(name,action_callback,update_callback,icons,owner,tooltip,priority) 110 | new_t.show() 111 | for i in range(self.get_n_items()): 112 | t = self.get_nth_item(i) 113 | if type(t) in (ToolButton, ToggleToolButton): 114 | if priority>t.priority: 115 | self.insert(new_t,i) 116 | return 117 | self.add(new_t) 118 | 119 | def register_tool_for_plugin(self,plugin,name,action_callback=None,update_callback=overlay_tools.show_on_hover,icons=None,tooltip='This is a tooltip',priority=50): 120 | ''' 121 | adds a new tool whose owner is plugin 122 | ''' 123 | self.register_tool(name,action_callback,update_callback,icons,plugin.name,tooltip,priority) 124 | 125 | def deregister_tools_for_plugin(self,plugin): 126 | ''' 127 | removes all tools whose owners are plugin 128 | ''' 129 | tools=[self.get_nth_item(i) for i in range(self.get_n_items()) if type(self.get_nth_item(i))==ToolButton and self.get_nth_item(i).owner==plugin.name] 130 | for t in tools: 131 | self.remove(t) 132 | 133 | def update_status(self,*args): 134 | tools=[self.get_nth_item(i) for i in range(self.get_n_items()) if type(self.get_nth_item(i)) in (ToolButton,ToolItem)] 135 | for t in tools: 136 | if t.update_cb is not None: 137 | t.update_cb(t,*args) 138 | 139 | ##shortcuts for adding items to a toolbar 140 | def add_item(toolbar,widget,callback,label=None,tooltip=None,expand=False): 141 | toolbar.add(widget) 142 | if callback: 143 | widget.connect("clicked", callback) 144 | if tooltip: 145 | widget.set_tooltip_text(tooltip) 146 | if label: 147 | widget.set_label(label) 148 | if expand: 149 | widget.set_expand(True) 150 | 151 | def add_widget(toolbar,widget,callback,label=None,tooltip=None,expand=False): 152 | item=gtk.ToolItem() 153 | item.add(widget) 154 | toolbar.add(item) 155 | if callback: 156 | widget.connect("clicked", callback) 157 | if tooltip: 158 | widget.set_tooltip_text(tooltip) 159 | if label: 160 | item.set_label(label) 161 | if expand: 162 | item.set_expand(True) 163 | 164 | def set_item(widget,callback,label,tooltip): 165 | if callback: 166 | widget.connect("clicked", callback) 167 | if tooltip: 168 | widget.set_tooltip_text(tooltip) 169 | if label: 170 | widget.set_label(label) 171 | return widget 172 | 173 | def add_frame(toolbar,label,items,expand=False): 174 | item=gtk.ToolItem() 175 | frame=gtk.Frame(label) 176 | box=gtk.HBox() 177 | item.add(frame) 178 | frame.add(box) 179 | for i in items: 180 | if len(i)==5: 181 | box.pack_start(set_item(*i[:4]),i[4]) 182 | else: 183 | box.pack_start(set_item(*i)) 184 | toolbar.add(item) 185 | if expand: 186 | item.set_expand(True) 187 | -------------------------------------------------------------------------------- /modules/picty/_legacy/plugins/webupload_services/picasaweb.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os.path 3 | import threading 4 | 5 | import gtk 6 | import gobject 7 | 8 | from picty import imagemanip 9 | from serviceui import * 10 | 11 | 12 | ##Picasa web uploads use the gdata api 13 | import gdata.photos.service 14 | import gdata.media 15 | import gdata.geo 16 | 17 | MODEL_COL_PICASA_TITLE=MODEL_COL_SERVICE+0 18 | MODEL_COL_PICASA_DESCRIPTION=MODEL_COL_SERVICE+1 19 | MODEL_COL_PICASA_TAGS=MODEL_COL_SERVICE+2 20 | 21 | class PicasaService(UploadServiceBase): 22 | name='Picasa Web Albums' 23 | def __init__(self,service_ui_owner): 24 | UploadServiceBase.__init__(self,service_ui_owner) 25 | def box_add(box,widget,label_text,signal,signal_cb): 26 | hbox=gtk.HBox() 27 | if label_text: 28 | label=gtk.Label(label_text) 29 | hbox.pack_start(label,False) 30 | hbox.pack_start(widget,True) 31 | self.service_ui.pref_change_handlers.append((widget,widget.connect(signal,signal_cb))) 32 | box.pack_start(hbox,False) 33 | return widget 34 | box=self.service_ui.service_pref_box 35 | self.title_entry=box_add(box,gtk.Entry(),"Title","changed",self.title_changed) 36 | self.description_entry=box_add(box,gtk.Entry(),"Description","changed",self.description_changed) 37 | self.tags_entry=box_add(box,gtk.Entry(),"Tags","changed",self.tags_changed) 38 | 39 | def get_default_cols(self,item): 40 | def catch(cb,item): 41 | try: 42 | return cb(item) 43 | except: 44 | return '' 45 | title=metadata.app_key_to_string('Title',catch(lambda item: item.meta['Title'],item)) 46 | description=metadata.app_key_to_string('ImageDescription',catch(lambda item: item.meta['ImageDescription'],item)) 47 | tags=metadata.app_key_to_string('Keywords',catch(lambda item: item.meta['Keywords'],item)) 48 | return (title,description,tags,False) 49 | 50 | def title_changed(self,widget): 51 | selected_rows=self.service_ui.get_selected() 52 | for r in selected_rows: 53 | r[MODEL_COL_PICASA_TITLE]=widget.get_text() 54 | 55 | def description_changed(self,widget): 56 | selected_rows=self.service_ui.get_selected() 57 | for r in selected_rows: 58 | r[MODEL_COL_PICASA_DESCRIPTION]=widget.get_text() 59 | 60 | def tags_changed(self,widget): 61 | print 'tags changed',widget.get_text() 62 | selected_rows=self.service_ui.get_selected() 63 | for r in selected_rows: 64 | r[MODEL_COL_PICASA_TAGS]=widget.get_text() 65 | 66 | # def private_changed(self,widget): 67 | # if widget.get_inconsistent(): 68 | # widget.set_inconsistent(False) 69 | # selected_rows=self.service_ui.get_selected() 70 | # for r in selected_rows: 71 | # r[MODEL_COL_PICASA_PRIVATE]=widget.get_active() 72 | 73 | def login_dialog(self): 74 | self.password_data=password_entry_dialog(self,title='Enter Your Picasa Credentials',data_list=[('Username (E-mail)','',True),('Password','',False)]) 75 | 76 | def get_pref_types(self): 77 | '''return a tuple of gobject type constants defining additional per image upload preferences (e.g. whether image is public or private)''' 78 | return (str,str,str,gobject.TYPE_BOOLEAN) 79 | 80 | def update_prefs(self,selected_rows): 81 | '''the user has just changed the selection in the upload_queue, update preference widgets accordingly''' 82 | val=self.service_ui.all_same(selected_rows,MODEL_COL_PICASA_TITLE) 83 | if val!=None: 84 | self.title_entry.set_text(val) 85 | else: 86 | self.title_entry.set_text("") 87 | val=self.service_ui.all_same(selected_rows,MODEL_COL_PICASA_DESCRIPTION) 88 | if val!=None: 89 | self.description_entry.set_text(val) 90 | else: 91 | self.description_entry.set_text("") 92 | val=self.service_ui.all_same(selected_rows,MODEL_COL_PICASA_TAGS) 93 | if val!=None: 94 | self.tags_entry.set_text(val) 95 | else: 96 | self.tags_entry.set_text("") 97 | # val=self.service_ui.all_same(selected_rows,MODEL_COL_PICASA_PRIVATE) 98 | # if val!=None: 99 | # self.private_check.set_active(val) 100 | # else: 101 | # self.private_check.set_inconsistent(True) 102 | 103 | def t_login(self): 104 | if self.password_data==None: 105 | self.t_notify_login(False,'Not Connected') 106 | return 107 | self.gd_c=gdata.photos.service.PhotosService() 108 | self.gd_c.email=self.password_data[0] 109 | self.gd_c.password=self.password_data[1] 110 | self.gd_c.source='picty-'+settings.file_version 111 | try: 112 | result=self.gd_c.ProgrammaticLogin() 113 | print 'Picasa login returned',result 114 | self.t_notify_login(True,'Logged in as %s'%(self.password_data[0],)) 115 | except: 116 | import traceback, sys 117 | tb_text=traceback.format_exc(sys.exc_info()[2]) 118 | print 'Error on login',tb_text 119 | self.t_notify_login(False,'Login as %s unsuccessful'%(self.password_data[0],)) 120 | 121 | def t_disconnect(self): 122 | del self.gd_c 123 | self.password_data=None 124 | self.t_notify_disconnect() 125 | 126 | def t_upload_photo(self,item,album=None,preferences=None): 127 | try: 128 | if album[0]=='': 129 | album_url = '/data/feed/api/user/%s/albumid/%s' % (self.gd_c.email, 'default') 130 | else: 131 | album_url = '/data/feed/api/user/%s/albumid/%s' % (self.gd_c.email, album[1].gphoto_id.text) ## 132 | self.t_notify_upload_progress(item,50) 133 | 134 | filename=imagemanip.get_jpeg_or_png_image_file(item,preferences[MODEL_COL_SIZE],preferences[MODEL_COL_STRIP]) 135 | 136 | if not filename: 137 | return self.t_notify_photo_uploaded(item,False,'Invalid Image Type') 138 | 139 | photo = gdata.photos.PhotoEntry() 140 | #photo.summary = 'uploaded with picty' 141 | 142 | import atom 143 | photo.title=atom.Title() 144 | if preferences[MODEL_COL_PICASA_TITLE]: 145 | title=preferences[MODEL_COL_PICASA_TITLE] 146 | else: 147 | title=os.path.split(item.uid)[1] 148 | photo.title.text = title 149 | photo.media = gdata.media.Group() 150 | keywords=metadata.tag_bind(metadata.tag_split(preferences[MODEL_COL_PICASA_TAGS]),',') 151 | if keywords: 152 | photo.media.keywords = gdata.media.Keywords(text=keywords) 153 | description=preferences[MODEL_COL_PICASA_DESCRIPTION] 154 | if description: 155 | photo.media.description = gdata.media.Description(text=description) 156 | #photo.media.credit = gdata.media.Credit(text=preferences[MODEL_COL_PICASA_AUTHOR]) 157 | photo=self.gd_c.InsertPhoto(album_url, photo, filename, io.get_mime_type(filename)) 158 | if filename!=item.uid: 159 | print 'WARNING: NOT REMOVING ITEM',item.uid 160 | ##os.remove(filename) ##TODO: Not safe to do this now that item.uid is stored as a relpath 161 | 162 | self.t_notify_photo_uploaded(item,True,'Successful upload') 163 | except: 164 | import traceback, sys 165 | tb_text=traceback.format_exc(sys.exc_info()[2]) 166 | print 'Error on upload',tb_text 167 | self.t_notify_photo_uploaded(item,False,'Upload Failed') 168 | 169 | def t_get_albums(self): 170 | albums=self.gd_c.GetUserFeed() 171 | alist=[(a.title.text,a) for a in albums.entry] 172 | self.t_notify_albums(alist,'') 173 | -------------------------------------------------------------------------------- /modules/picty/uitools/overlay_tools.py: -------------------------------------------------------------------------------- 1 | ''' 2 | 3 | picty 4 | Copyright (C) 2013 Damien Moore 5 | 6 | License: 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | ''' 21 | 22 | import gtk 23 | 24 | overlay_groups=[] #global colection of all overlay groups 25 | 26 | def deregister_all_tools_for_plugin(plugin): 27 | for g in overlay_groups: 28 | g.deregister_tools_for_plugin(plugin) 29 | 30 | def show_on_hover(item,hover_boolean): 31 | return hover_boolean-1 32 | 33 | def always_active(item,hover): 34 | return 0 35 | 36 | 37 | class OverlayTool: 38 | ''' 39 | Class defining a single tool "button" that will be displayed over a thumbnail or fullsize image, 40 | performing an action when the user selects it. 41 | name -- unique name of the tool 42 | action_callback -- action associated with the tool, must have call sig: def action_cb(item) 43 | active_callback -- callback returning True if the item is active, must have call sig: def active_cb(item,hover) 44 | always_active -- True if command should always be displayed (active_callback is never called it True) 45 | icons -- a list of gtk.gdk.Pixbuf representing possible icon states 46 | owner -- 'Main' or the name of a plugin 47 | ''' 48 | def __init__(self,name,action_callback=None,active_callback=show_on_hover,icons=[],owner='Main',tooltip='This is a tooltip',priority=50): 49 | self.name=name 50 | self.action=action_callback 51 | self.is_active=active_callback 52 | self.icons=icons 53 | self.owner=owner 54 | self.priority=priority 55 | self.tooltip=tooltip 56 | 57 | 58 | class OverlayGroup: 59 | ''' 60 | Call containing the collection of Overlay tool buttons display over a thumbnail or fullsize image 61 | ''' 62 | def default_active_callback(self,item,hover): 63 | return int(hover)-1 64 | def __init__(self,widget_render_source,size=gtk.ICON_SIZE_LARGE_TOOLBAR): 65 | ''' 66 | widget_render_source -- should be a gtk.Widget with a valid render_icon method 67 | size -- one of the valid ICON_SIZE_* constants 68 | ''' 69 | self.tools=[] 70 | self.widget_render_source=widget_render_source 71 | self.size=size 72 | overlay_groups.append(self) 73 | def __getitem__(self,index): 74 | return self.tools[index] 75 | def register_tool(self,name,action_callback=None,active_callback=show_on_hover,icons=[],owner='Main',tooltip='This is a tooltip',priority=50): 76 | icons=[self.widget_render_source.render_icon(icon,self.size) if icon else None for icon in icons] 77 | new_t=OverlayTool(name,action_callback,active_callback,icons,owner,tooltip,priority) 78 | for i in range(len(self.tools)): 79 | if priority>self.tools[i].priority: 80 | self.tools.insert(i,new_t) 81 | return 82 | self.tools.append(new_t) 83 | def register_tool_for_plugin(self,plugin,name,action_callback=None,active_callback=show_on_hover,icons=None,tooltip='This is a tooltip',priority=50): 84 | ''' 85 | adds a new tool whose owner is plugin 86 | ''' 87 | self.register_tool(name,action_callback,active_callback,icons,plugin.name,tooltip,priority) 88 | def deregister_tools_for_plugin(self,plugin): 89 | ''' 90 | removes all tools whose owners are plugin 91 | ''' 92 | self.tools=[t for t in self.tools if t.owner!=plugin.name] 93 | def simple_render(self,item,hover_data,drawable,gc,x,y,xpad): 94 | ''' 95 | renders the tools horizontally across the already rendered image in the drawable 96 | item -- the image object that tools will act upon 97 | hover_data -- whatever additional data that will help the tool decide whether it needs to render 98 | drawable -- a gtk.gdk.Drawable that buttons will be drawn on 99 | gc -- a gtk.gdk.GC (graphic context) 100 | x -- horizontal offset for drawing in the drawable 101 | y -- vertical offset for drawing in the drawable 102 | xpad -- amount of space between tools 103 | ''' 104 | offx=0 105 | for t in self.tools: 106 | active=t.is_active(item,hover_data) 107 | w=20 108 | if active>=0: 109 | drawable.draw_pixbuf(gc,t.icons[active],0,0,int(x+offx),int(y)) 110 | w=t.icons[active].get_width() 111 | offx+=w+xpad 112 | def simple_render_with_highlight(self,highlight_ind,button_down,item,hover_data,drawable,gc,x,y,xpad): 113 | ''' 114 | renders the tools horizontally across the already rendered image in the drawable 115 | hover_ind -- the number of the tool to highlight 116 | item -- the image object that tools will act upon 117 | hover_data -- whatever additional data that will help the tool decide whether it needs to render 118 | drawable -- a gtk.gdk.Drawable that buttons will be drawn on 119 | gc -- a gtk.gdk.GC (graphic context) 120 | x -- horizontal offset for drawing in the drawable 121 | y -- vertical offset for drawing in the drawable 122 | xpad -- amount of space between tools 123 | ''' 124 | offx=0 125 | for i in xrange(len(self.tools)): 126 | t=self.tools[i] 127 | adjx,adjy=0,0 128 | active=t.is_active(item,hover_data) 129 | if active>=0 and t.icons and len(t.icons)>0: 130 | if i==highlight_ind: 131 | w,h=t.icons[active].get_width(),t.icons[active].get_height() 132 | if button_down: 133 | highlight_pb=gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,True,8,w+8,h+8) 134 | highlight_pb.fill(0x404060a0) 135 | drawable.draw_pixbuf(gc,highlight_pb,0,0,int(x+offx-4),int(y-4)) 136 | highlight_pb=gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,True,8,w+6,h+6) 137 | highlight_pb.fill(0x606080a0) 138 | drawable.draw_pixbuf(gc,highlight_pb,0,0,int(x+offx-3),int(y-3)) 139 | adjx,adjy=1,1 140 | else: 141 | highlight_pb=gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,True,8,w+8,h+8) 142 | highlight_pb.fill(0xa0a0f0a0) 143 | drawable.draw_pixbuf(gc,highlight_pb,0,0,int(x+offx-4),int(y-4)) 144 | if len(t.icons)>active and t.icons[active]!=None: 145 | drawable.draw_pixbuf(gc,t.icons[active],0,0,int(x+offx+adjx),int(y+adjy)) 146 | if t.icons!=None and len(t.icons)>active and t.icons[active]!=None: 147 | offx+=t.icons[active].get_width()+xpad 148 | else: 149 | offx+=20+xpad 150 | def get_command(self, x, y, offx, offy, xpad, item, hover_data): 151 | left=offx 152 | top=offy 153 | for i in range(len(self.tools)): 154 | active=self.tools[i].is_active(item,hover_data) 155 | t=self.tools[i] 156 | if t.icons!=None and len(t.icons)>active and t.icons[active]!=None: 157 | w=t.icons[active].get_width() 158 | h=t.icons[active].get_height() 159 | else: 160 | w,h=(20,20) 161 | right=left+w 162 | bottom=top+h 163 | if left