1
0
mirror of https://github.com/gryf/pygtktalog.git synced 2026-03-27 06:33:30 +01:00

58 Commits

Author SHA1 Message Date
de247e9bc8 Added branch with implementation for images in db 2012-02-21 21:13:47 +01:00
ad1703cd90 Added logic for Image and Thumbnail db objects. Added scan functionality 2011-05-15 16:50:54 +02:00
3e2634bc57 Added details/discs/files models, added scan module and its test
Changes im logging module, but also on others
2011-03-27 20:48:28 +02:00
3c6c9a552a Added observable property for discs current directory,
Clean up dialogs a bit
Some additional variables in init (mainly for about() stuff)
Rearrange video module for usage PIL, which is slightly faster than
 ImageMagick utils
2010-11-17 22:25:53 +01:00
54b6a377bf Lots of changes, callbacks for gui events, added details and so on 2010-11-07 16:34:26 +01:00
22c24fbaf7 - added version for package,
- added about signal callback,
- use sorting in additional relations in models,
- added prototype for about class/function
- corrected README for small typos, added links, conformed to be valid
  reStructuredText
2010-05-06 21:14:55 +02:00
62ab67ecc5 Resolved issue with sort order of children of the file obj. 2010-05-04 06:48:58 +02:00
7417b9e98e Introduced files treeview, refactor code for discs view, struggle with files model 2010-05-04 05:55:41 +02:00
1cf1390567 Implemented rename functionality in discs controller, reduced redundant information in discs treestore 2010-05-03 10:30:00 +02:00
5db02183a5 Added script for converting between scheme for DB in version 1.x of app and 2.x 2010-05-02 19:32:56 +02:00
3590f90751 Cleaned up the code 2010-05-02 19:24:04 +02:00
2b5b53ada1 Added popup menu for discs treeview, added separated glade files with interface and controllers 2010-05-02 19:02:35 +02:00
7536e2c60a Repaired test task 2010-05-02 11:29:53 +02:00
3b0cb80407 Added discs controller, change way that paver run application 2010-05-02 11:21:34 +02:00
9769dfdb76 Implementation of discs tree, added popup menu 2010-05-02 11:14:54 +02:00
dbb01acd3f Early design for separating main classes into smaller parts. Added logger module. 2010-04-28 21:55:49 +02:00
9b7f15122d Added requrements (SQLAlchemy, gettext) 2009-08-26 20:29:26 +00:00
1cd6ad5b84 Cleaned up messages for quit. 2009-08-26 20:20:48 +00:00
58c0c1ecdc Imports for new database handle. 2009-08-26 20:18:09 +00:00
20501fcf54 Updated locale 2009-08-26 20:16:49 +00:00
5e83363fe7 Removed unnecessary prints. 2009-08-26 20:15:26 +00:00
313db80101 Migration of database stuff into objects managed by SQLAlchemy ORM. 2009-08-26 20:12:35 +00:00
3f797b0bf8 Replace generate_pot.py script with xgettext/intltool-extract tools. Python
script have defect in multiline texts and with couple of different messages in
one line.
2009-08-26 20:10:54 +00:00
8d6cb75b8e Added tests for common DB actions 2009-07-19 18:16:47 +00:00
71162da225 Simplify db interactions. Follow KISS principle. 2009-07-19 18:16:14 +00:00
6b1fdb90e9 Added DataBase class for connectiions, schema creating and other common tasks.
This version contains copying to RAM functions.
2009-07-19 13:56:16 +00:00
c1dd854f62 removed trailing characters 2009-06-29 20:10:53 +00:00
2b47d9b869 added coverage option for nosetests 2009-06-29 20:09:59 +00:00
53c3a444e0 Separated code for geting information about movie, minor fixes 2009-06-29 20:09:06 +00:00
b54aa1849b Added additional capture() method test to cover all cases 2009-06-29 20:07:26 +00:00
37f06726b4 Small changes in README 2009-05-19 19:53:14 +00:00
e93b7291b8 Generalize locale support with pavement script. 2009-05-19 19:52:55 +00:00
ca7fdf15e8 Isolated method for gathering movie info. Now it can be changed or replaced
easely.
2009-05-19 19:52:02 +00:00
7b5c76f1d9 Added fallback to C if there is no suitable locales 2009-05-19 19:50:46 +00:00
f0f8d27d19 Added on_quit method. 2009-05-19 19:50:05 +00:00
b493b66ea8 Msg catalogs update. 2009-05-19 19:49:30 +00:00
83b9d944cf Cosmetic changes to test files for modules video and misc. 2009-05-19 19:46:45 +00:00
5ccdd8ee6f Added module with dialog helpers, and unit test 2009-05-19 19:45:56 +00:00
8013eec7d2 Removed needless test collector. Used nosetest instead. 2009-05-09 05:44:05 +00:00
14f654251d Added forgotten pot file.
Added i18n into package init.
Made some changes in README file.
2009-05-08 20:15:38 +00:00
8a24e30bde Moved start script into bin directory.
Removed makefile, substituded by the pavement script from paver package.
2009-05-08 20:14:21 +00:00
bcacb75229 cosmetic changes 2009-05-08 20:11:11 +00:00
d04a84c72d Small change to make plgen makefile target work. 2009-05-04 20:32:24 +00:00
e69e2300e8 Moved scripts into bin subdir, slightly modified Makefile, added plgen and
dist targets, removed CZYTAJTO.
2009-05-04 20:29:29 +00:00
08c38bf63d Change Makefile to make tests work, removed cleanup shell script, removed
start script. There should be created new starscript suitable for running app
from current directory and installed as an egg.
2009-05-04 16:36:39 +00:00
52b293c459 Added unit test for misc module, added initial mvc for main window. 2009-05-04 16:24:37 +00:00
bdf059d11f Moved main.glade into pygtktalog package. 2009-05-04 16:23:41 +00:00
ccec14f3ea Move misc and video modules into package pygtktalog. 2009-05-04 16:19:48 +00:00
36c4e7e4f2 Added pygtktalog module 2009-05-04 16:13:37 +00:00
b0964ca031 Removed dummy tests, remove runtests bash script, since there is make target
for invoke running tests.
2009-05-04 16:09:48 +00:00
efaccd8902 Moved test module into root catalog of the project. 2009-05-04 16:05:47 +00:00
434df58b16 Head to v.2. Now it uses gtkmvc with version 1.99, makefile, i18n. 2009-05-04 16:02:36 +00:00
56c77ae9a4 added EXIF patch and misc in lib
removed some cruft
2009-04-07 20:25:00 +00:00
c46d29a5bb added video module - replacement for midentify
added some mocks for images tests
2009-04-07 19:42:44 +00:00
5e8c33f05a changes in tests, upgrade to new EXIF module, etc 2009-04-07 19:40:15 +00:00
fb920f58bc * Removed cruft.
* Version change to 1.0.2.
2008-12-15 20:54:16 +00:00
292d290723 * Added shell script for preparing distfile.
* Added unitests.
 * Added mocks for video files.
 * Added midentify module.
2008-12-15 20:46:26 +00:00
0adcdaba8d * Lots of changes, code cleanup, import/export in GUI. 2008-12-15 20:40:24 +00:00
68 changed files with 7825 additions and 2530 deletions

4
MANIFEST.in Normal file
View File

@@ -0,0 +1,4 @@
include setup.py
include pavement.py
include paver-minilib.zip
include pygtktalog/locale/*/*/*.mo

198
README
View File

@@ -1,67 +1,98 @@
pyGTKtalog 1.0
==================
pyGTKtalog
==========
pyGTKtalog is Linux/FreeBSD program for indexing CD/DVD or directories on
filesystem. It is similar to gtktalog <http://www.nongnu.org/gtktalog/> or
gwhere <http://www.gwhere.org/home.php3>. There is no coincidence in name of
application, because it's ment to be replacement (in some way) for gtktalog,
filesystem. It is similar to `gtktalog <http://www.nongnu.org/gtktalog/>`_ or
`gwhere <http://www.gwhere.org/home.php3>`_. There is no coincidence in name of
application, because it's menat to be replacement (in some way) for gtktalog,
which seems to be dead project for years.
FEATURES
========
Current version is 1.9.
- scan for files in selected media
- get/generate thumbnails from exif and other images
- most important exif tags
- add/edit description and notes
- fetch comments for images made in gThumb <http://gthumb.sourceforge.net>
- add/remove unlimited images to any file or directory
- tagging files <http://en.wikipedia.org/wiki/Tag_%28metadata%29>
- and more :)
FEATURES
--------
* scan for files in selected media
* get/generate thumbnails from exif and other images
* most important exif tags
* add/edit description and notes
* fetch comments for images made in `gThumb <http://gthumb.sourceforge.net>`_
* add/remove unlimited images to any file or directory
* `tagging files <http://en.wikipedia.org/wiki/Tag_%28metadata%29>`_
* and more :)
REQUIREMENTS
============
------------
pyGTKtalog is written in python with following dependencies:
pyGTKtalog requires python and following libraries:
- python 2.4 or higher
- pygtk 2.10 or higher <http://www.pygtk.org>
- pysqlite2 <http://pysqlite.org/> (unnecessary, if python 2.5 is used)
* `python 2.6 <http://www.python.org/>`_
* `pygtk 2.16 <http://www.pygtk.org>`_
* `pygtkmvc 1.99 <http://sourceforge.net/apps/trac/pygtkmvc/wiki>`_
* `sqlalchemy 0.6 <http://www.sqlalchemy.org>`_
Optional modules:
It may work on other (lower) version of libraries, and it should work with
higher versions of libraries.
- PIL <http://www.pythonware.com/products/pil/index.htm> for image manipulation
.. note::
Additional pyGTKtalog uses pygtkmvc <http://pygtkmvc.sourceforge.net> by Roberto
Cavada and EXIF module by Gene Cash (slightly updatetd to EXIF 2.2 by me) which
are included in sources.
Although pygtkmvc is `listed on pypi
<http://pypi.python.org/pypi/python-gtkmvc/>`_ it may happen that you
have to download it directly from
`sourceforge <http://sourceforge.net/apps/trac/pygtkmvc/wiki>`_ page and
install manually. I don't know about pygtk (I've installed it by my
system package manager), but all the others python libraries (sqlalchemy,
paver, nose, coverage) should be installable via `pip
<http://pypi.python.org/pypi/pip>`_
Optional modules
^^^^^^^^^^^^^^^^
* `PIL <http://www.pythonware.com/products/pil/index.htm>`_ for image manipulation
Additional pyGTKtalog uses EXIF module by Gene Cash (slightly updatetd to EXIF
2.2 by me) which is included in sources.
pyGTKtalog extensivly uses external programs in unix spirit, however there is
small possibility of using it Windows (probably with limitations) and quite big
possiblity to run it on other sofisticated unix-like systems (i.e.
BeOS/ZETA/Haiku, QNX or MacOSX).
Programs that are used:
* mencoder (provided by mplayer package)
* montage, convert from ImageMagick
For development process following programs are used:
* `gettext <http://www.gnu.org/software/gettext/gettext.html>`_
* `intltool <http://www.gnome.org/>`_
* `nose <http://code.google.com/p/python-nose/>`_
* `coverage <http://nedbatchelder.com/code/coverage/>`_
* `paver <http://code.google.com/p/paver/>`__
INSTALATION
===========
-----------
You don't have to install it if you don't want to. You can just change current
directory to pyGTKtalog and simply run:
directory to pyGTKtalog and simply run::
./pyGTKtalog
$ paver run
That's it. Alternatively, if you like to put it in more system wide place, all
you have to do is:
- put pyGTKtalog directory into your destination of choice (/usr/local/share,
/opt or ~/ is typical bet)
- copy pyGTKtalog shell script to /usr/bin, /usr/local/bin or in
other place, where PATH variable is pointing or you feel like.
- then modify pyGTKtalog line 6 to match right pygtktalog.py directory
#. put pyGTKtalog directory into your destination of choice (/usr/local/share,
/opt or ~/ is typical bet)
#. copy pyGTKtalog shell script to /usr/bin, /usr/local/bin or in
other place, where PATH variable is pointing or you feel like.
#. then modify pyGTKtalog line 6 to match right pygtktalog.py directory
Then, just run pyGTKtalog script.
TODO
====
----
PyGTKtalog is still under heavy development, however there is small chance to
change structure of catalogs (and if it'll change, there will be transparent
@@ -70,53 +101,82 @@ function to update DB schema).
For version 1.0 there are no features to be done, just bug fixes.
There are still minor aims for versions 1.x to be done:
- consolidate popup-menus with edit menu
- add popup menu for directly removing tag from tag cloud
- implement advanced search
* consolidate popup-menus with edit menu
* add popup menu for directly removing tag from tag cloud
* implement advanced search
For version 2.0:
- Icon grid in files view
- command line support: query, adding media to collection etc
- internationalization
- export to XLS
- user definied group of tags (represented by color in cloud tag)
- hiding specified files - configurable, like dot prefixed, cfg and manualy
* Export/Import
* Icon grid in files view
* command line support: query, adding media to collection etc
* internationalization
* export to XLS
* user definied group of tags (represented by color in cloud tag)
* hiding specified files - configurable, like dot prefixed, cfg and manualy
selected
* tests
* warning about existing image in media directory
Removed:
- filetypes handling (movies, images, archives, documents etc). Now it have
* filetypes handling (movies, images, archives, documents etc). Now it have
common, unified external "plugin" system - simple text output from command
line programs.
- anime/movie
- title
- alt title
- type (anime movie, movie, anime oav, anime tv series, tv series, etc)
- cover/images
- genre
- lang
- sub lang
- release date (from - to)
- anidb link/imdb link
Maybe in future versions. Now text file descriptions/notes and tags have to
be enough for good and fast information search.
* anime/movie
* title
* alt title
* type (anime movie, movie, anime oav, anime tv series, tv series, etc)
* cover/images
* genre
* lang
* sub lang
* release date (from - to)
* anidb link/imdb link
Maybe in future versions. Now text file descriptions/notes and tags have to
be enough for good and fast information search.
NOTES
=====
-----
Catalog file is tared and gziped sqlite database and directories with images and
thumbnails. If there are more images, the size of catalog file will grow. So be
carefull with adding big images in your catalog file!
Catalog file is plain sqlite database (optionally compressed with bzip2). All
images are stored in ``~/.pygtktalog/images`` directory. Names for images are
generated sha512 hash from image file itself. There is small possibility for two
identical hash for different image files. However, no images are overwritten.
Thumbnail filename for each image is simply concatenation of image filename in
images directory and '_t' string.
There is also converter form old database to new. In fact no image are stored in
archive with katalog. All thumnails will be lost. All images without big image
will be lost. There ar serious changes with application design, and I decided,
that is better to keep media unpacked on disk, instead of pack it every time
with save and unpack with open methods. New design prevent from deleting eny
file from media directory (placed in ~/.pygtktalog/images). Functionality for
exporting images and corresponding db file is planned.
There is also converter from old database to new for internal use only. In
public release there will be no other formats so it will be useless, and
deleted. There are some issues with converting. All thumbnails will be lost.
All images without big image will be lost. There are serious changes with
application design, and I decided, that is better to keep media unpacked on
disk, instead of pack it every time with save and unpack with open methods. New
design prevent from deleting any file from media directory (placed in
``~/.pygtktalog/images``). Functionality for exporting images and corresponding
db file is planned.
DEVELOPMENT
-----------
Several tools has been used to develop pyGTKtalog.
Paver
^^^^^
I've choose `Paver <http://www.blueskyonmars.com/projects/paver/>`_ as make
equivalent. Inside main project directory there is pavement.py script, which
provides several tasks, that can be helpfull in a work with sources. Paver is
also used to generate standard setup.py.
Nose
^^^^
BUGS
====
----
All bugs please report to Roman 'gryf' Dobosz <roman.dobosz@gmail.com>
All bugs please report to Roman 'gryf' Dobosz <gryf73@gmail.com>.

View File

@@ -1,5 +0,0 @@
#!/bin/sh
# remove ~, pyc, pyo files from current directory
find . -name \*~ -exec rm '{}' ';'
find . -name \*pyc -exec rm '{}' ';'
find . -name \*pyo -exec rm '{}' ';'

121
convert_1.x_to_2.x.py Executable file
View File

@@ -0,0 +1,121 @@
#!/usr/bin/env python
"""
Project: pyGTKtalog
Description: convert db created with v.1.x into v.2.x
Type: tool
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-14
"""
import sys
import os
import bz2
import shutil
from tempfile import mkstemp
from sqlite3 import dbapi2 as sqlite
from datetime import datetime
# import db objects just to create schema
from pygtktalog.dbobjects import File, Exif, Group, Gthumb
from pygtktalog.dbobjects import Image, Tag, Thumbnail
from pygtktalog.dbcommon import connect
def create_schema(cur):
pass
def create_temporary_db_file():
"""create temporary db file"""
fd, fname = mkstemp()
os.close(fd)
return fname
def connect_to_db(filename):
"""initialize db connection and store it in class attributes"""
db_connection = sqlite.connect(filename, \
detect_types=sqlite.PARSE_DECLTYPES | sqlite.PARSE_COLNAMES)
db_cursor = db_connection.cursor()
return db_connection, db_cursor
def opendb(filename=None):
"""try to open db file"""
db_tmp_path = create_temporary_db_file()
try:
test_file = open(filename).read(15)
except IOError:
os.unlink(db_tmp_path)
return False
if test_file == "SQLite format 3":
db_tmp = open(db_tmp_path, "wb")
db_tmp.write(open(filename).read())
db_tmp.close()
elif test_file[0:10] == "BZh91AY&SY":
open_file = bz2.BZ2File(filename)
try:
curdb = open(db_tmp_path, "w")
curdb.write(open_file.read())
curdb.close()
open_file.close()
except IOError:
# file is not bz2
os.unlink(db_tmp_path)
return False
else:
os.unlink(db_tmp_path)
return False
return connect_to_db(db_tmp_path), db_tmp_path
if __name__ == "__main__":
if len(sys.argv) != 3:
print "usage: %s src_base dst_base" % sys.argv[0]
exit()
result = opendb(sys.argv[1])
if not result:
print "unable to open src db file"
exit()
(src_con, src_c), src_tmpf = result
shutil.copy(src_tmpf, sys.argv[2])
# close src db.
src_c.close()
src_con.close()
os.unlink(src_tmpf)
# create or update shema
connect(sys.argv[2])
dst_con = sqlite.connect(sys.argv[2])
dst_c = dst_con.cursor()
sql = "select id, date from files"
for id, date in dst_c.execute(sql).fetchall():
sql = "update files set date=? where id=?"
if date and int(date) > 0:
dst_c.execute(sql, (datetime.fromtimestamp(int(date)), id))
else:
dst_c.execute(sql, (None, id))
sql = "select id, date from gthumb"
for id, date in dst_c.execute(sql).fetchall():
sql = "update gthumb set date=? where id=?"
try:
if int(date) > 0:
dst_c.execute(sql, (datetime.fromtimestamp(int(date)), id))
else:
dst_c.execute(sql, (None, id))
except:
print id, date
dst_con.commit()
dst_c.close()
dst_con.close()

View File

@@ -1,213 +0,0 @@
#!/usr/bin/python
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
import sys
import os
import shutil
import tarfile
try:
import sqlite3 as sqlite
except ImportError:
from pysqlite2 import dbapi2 as sqlite
from datetime import datetime
class OldModel(object):
"""Create, load, save, manipulate db file which is container for data"""
def __init__(self):
"""initialize"""
self.db_cursor = None
self.db_connection = None
self.internal_dirname = None
def cleanup(self):
"""remove temporary directory tree from filesystem"""
self.__close_db_connection()
if self.internal_dirname != None:
try:
shutil.rmtree(self.internal_dirname)
except OSError:
pass
return
def open(self, filename=None):
"""try to open db file"""
self.__create_internal_dirname()
self.filename = filename
try:
tar = tarfile.open(filename, "r:gz")
except:
try:
tar = tarfile.open(filename, "r")
except:
self.internal_dirname = None
return False
os.chdir(self.internal_dirname)
try:
tar.extractall()
if __debug__:
print "OldModel 73: extracted tarfile into",
print self.internal_dirname
except AttributeError:
# python 2.4 tarfile module lacks of method extractall()
directories = []
for tarinfo in tar:
if tarinfo.isdir():
# Extract directory with a safe mode, so that
# all files below can be extracted as well.
try:
os.makedirs(os.path.join('.', tarinfo.name), 0700)
except EnvironmentError:
pass
directories.append(tarinfo)
else:
tar.extract(tarinfo, '.')
# Reverse sort directories.
directories.sort(lambda a, b: cmp(a.name, b.name))
directories.reverse()
# Set correct owner, mtime and filemode on directories.
for tarinfo in directories:
try:
os.chown(os.path.join('.', tarinfo.name),
tarinfo.uid, tarinfo.gid)
os.utime(os.path.join('.', tarinfo.name),
(0, tarinfo.mtime))
except OSError:
if __debug__:
print "OldModel 103: open(): setting corrext owner,",
print "mtime etc"
tar.close()
self.__connect_to_db()
return True
# private class functions
def __connect_to_db(self):
"""initialize db connection and store it in class attributes"""
self.db_connection = sqlite.connect("%s" % \
(self.internal_dirname + '/db.sqlite'),
detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES)
self.db_cursor = self.db_connection.cursor()
return
def __close_db_connection(self):
"""close db conection"""
if self.db_cursor != None:
self.db_cursor.close()
self.db_cursor = None
if self.db_connection != None:
self.db_connection.close()
self.db_connection = None
return
def __create_internal_dirname(self):
"""create temporary directory for working thumb/image files and
database"""
# TODO: change this stupid rutine into tempfile mkdtemp method
self.cleanup()
self.internal_dirname = "/tmp/pygtktalog%d" % \
datetime.now().microsecond
try:
os.mkdir(self.internal_dirname)
except IOError, (errno, strerror):
print "OldModel 138: __create_internal_dirname(): ", strerror
return
def setup_path():
"""Sets up the python include paths to include needed directories"""
import os.path
from src.utils.globals import TOPDIR
sys.path = [os.path.join(TOPDIR, "src")] + sys.path
return
if __name__ == "__main__":
"""run the stuff"""
if len(sys.argv) != 3:
print "Usage: %s old_katalog_filename new_katalog_filename" % \
sys.argv[0]
print "All available pictures will be exported aswell, however",
print "thumbnails without"
print "images will be lost."
sys.exit()
# Directory from where pygtkatalog was invoced. We need it for calculate
# path for argument (catalog file)
execution_dir = os.path.abspath(os.path.curdir)
# Directory, where this files lies. We need it to setup private source
# paths
libraries_dir = os.path.dirname(__file__)
os.chdir(libraries_dir)
setup_path()
from shutil import copy
from utils.img import Img
from models.m_main import MainModel as NewModel
model = OldModel()
new_model = NewModel()
if not model.open(os.path.join(execution_dir, sys.argv[1])):
print "cannot open katalog in 1.0RC1 format"
sys.exit()
model.db_cursor.execute("""create table
images2(id INTEGER PRIMARY KEY AUTOINCREMENT,
file_id INTEGER,
filename TEXT);""")
model.db_cursor.execute("delete from thumbnails")
result = model.db_cursor.execute("select file_id, filename from images")
# (id, filename)
# (4921, u'images/13/39.jpg')
for row in result.fetchall():
if row[1] and os.path.exists(os.path.join(model.internal_dirname,
row[1])):
im = Img(os.path.join(model.internal_dirname, row[1]),
new_model.image_path)
image = im.save()
sql = "insert into images2(file_id, filename) values (?, ?)"
model.db_cursor.execute(sql, (row[0], image))
model.db_cursor.execute("select id from thumbnails where file_id=?", (row[0], ))
thumb = model.db_cursor.fetchone()
if not (thumb and thumb[0]):
sql = "insert into thumbnails(file_id, filename) values (?, ?)"
model.db_cursor.execute(sql, (row[0], image))
model.db_connection.commit()
model.db_cursor.execute("drop table images")
model.db_cursor.execute("alter table images2 rename to images")
copy(os.path.join(model.internal_dirname, 'db.sqlite'),
os.path.join(execution_dir, sys.argv[2]))
# remove stuff
model.cleanup()

375
locale/pl.po Normal file
View File

@@ -0,0 +1,375 @@
#
# pygtktalog Language File
#
msgid ""
msgstr ""
"Project-Id-Version: pygtktalog\n"
"POT-Creation-Date: 2009-08-26 22:10:33.892815\n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
"Generated-By: slightly modified generate_pot.py\n"
#: pygtktalog/views/glade/main.glade.h:1
msgid "Add CD"
msgstr "Dodaj CD"
#: pygtktalog/views/glade/main.glade.h:2
msgid "Add CD/DVD to catalog"
msgstr "Dodaj CD/DVD do katalogu"
#: pygtktalog/views/glade/main.glade.h:3
msgid "Add Dir"
msgstr "Dodaj katalog"
#: pygtktalog/views/glade/main.glade.h:4
msgid "Add _CD/DVD"
msgstr "Dodaj _Cd/DVD"
#: pygtktalog/views/glade/main.glade.h:5
msgid "Add _Directory"
msgstr "Dodaj kata_log"
#: pygtktalog/views/glade/main.glade.h:6
#: pygtktalog/views/glade/files.glade.h:1
msgid "Add _Images"
msgstr "Dodaj _obrazy"
#: pygtktalog/views/glade/main.glade.h:7
#: pygtktalog/views/glade/files.glade.h:2
msgid "Add _Thumbnail"
msgstr "Dodaj miniaturÄ™"
#: pygtktalog/views/glade/main.glade.h:8
#: pygtktalog/views/glade/files.glade.h:3
msgid ""
"Add images to file. If file have no thumbnail,\n"
"thumbnail from first image will be generated."
msgstr ""
"Dodanie obrazów do pliku. Jeśli plik nie posiada miniatury,\n"
"zostanie wygenerowana miniatura z pierwszego obrazu."
#: pygtktalog/views/glade/main.glade.h:10
#: pygtktalog/views/glade/main.glade.h:6
msgid "Cancel"
msgstr "Analuj"
#: pygtktalog/views/glade/main.glade.h:11
#: pygtktalog/views/glade/main.glade.h:7
msgid "Catalog _statistics"
msgstr "_Statystyka katalogu"
#: pygtktalog/views/glade/main.glade.h:12
#: pygtktalog/views/glade/discs.glade.h:1
msgid "Collapse all nodes"
msgstr "Zwiń wszystkie gałęzie"
#: pygtktalog/views/glade/main.glade.h:13
#: pygtktalog/views/glade/main.glade.h:8
msgid "Create new catalog"
msgstr "UtwĂłrz nowy katalog"
#: pygtktalog/views/glade/main.glade.h:14
#: pygtktalog/views/glade/main.glade.h:9
msgid "Delete all images"
msgstr "Usuń wszystkie obrazy"
#: pygtktalog/views/glade/main.glade.h:15
#: pygtktalog/views/glade/main.glade.h:10
msgid "Delete all thumbnals"
msgstr "Usuń wszystkie miniatury"
#: pygtktalog/views/glade/main.glade.h:16
#: pygtktalog/views/glade/main.glade.h:11
msgid "Delete tag"
msgstr "Usuń tag"
#: pygtktalog/views/glade/main.glade.h:17
#: pygtktalog/views/glade/main.glade.h:12
msgid "Deletes all images from files in current colection"
msgstr "Usuwa wszystkie obrazy z plikĂłw w bieĹĽÄ…cej kolekcji"
#: pygtktalog/views/glade/main.glade.h:18
#: pygtktalog/views/glade/main.glade.h:13
msgid "Discs"
msgstr "Dyski"
#: pygtktalog/views/glade/main.glade.h:19
#: pygtktalog/views/glade/details.glade.h:1
msgid "Double click to open image"
msgstr "Dwukrotne kliknięcie otwiera obraz"
#: pygtktalog/views/glade/main.glade.h:20
#: pygtktalog/views/glade/details.glade.h:2
msgid "EXIF"
msgstr "EXIF"
#: pygtktalog/views/glade/main.glade.h:21
#: pygtktalog/views/glade/discs.glade.h:2
msgid "Expand all nodes"
msgstr "Rozwiń wszystkie gałęzie"
#: pygtktalog/views/glade/main.glade.h:22
#: pygtktalog/views/glade/main.glade.h:14
msgid "Export"
msgstr "Eksport"
#: pygtktalog/views/glade/main.glade.h:23
#: pygtktalog/views/glade/details.glade.h:3
msgid "File info"
msgstr "Informacje o pliku"
#: pygtktalog/views/glade/main.glade.h:24
#: pygtktalog/views/glade/main.glade.h:15
msgid "Find file"
msgstr "ZnajdĹş plik"
#: pygtktalog/views/glade/main.glade.h:25
#: pygtktalog/views/glade/details.glade.h:4
msgid "Images"
msgstr "Obrazy"
#: pygtktalog/views/glade/main.glade.h:26
#: pygtktalog/views/glade/main.glade.h:16
msgid "Import"
msgstr "Import"
#: pygtktalog/views/glade/main.glade.h:27
#: pygtktalog/views/glade/main.glade.h:17
msgid "Open catalog file"
msgstr "OtwĂłrz plik katalogu"
#: pygtktalog/views/glade/main.glade.h:28
#: pygtktalog/views/glade/main.glade.h:18
msgid "Quit pyGTKtalog"
msgstr "Zakończ pyGTKtalog"
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:5
msgid "Re_move Thumbnail"
msgstr "Usuń miniaturę"
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/main.glade.h:19
msgid "Recent files"
msgstr "Ostatnio uĹĽywane pliki"
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/files.glade.h:6
msgid "Rem_ove All Images"
msgstr "Usuń wszystkie obrazy"
#: pygtktalog/views/glade/main.glade.h:32
#: pygtktalog/views/glade/files.glade.h:7
msgid "Remo_ve tag"
msgstr "Usuń tag"
#: pygtktalog/views/glade/main.glade.h:33
#: pygtktalog/views/glade/main.glade.h:20
msgid "Save all images..."
msgstr "Zapisz wszytkie obrazy..."
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/main.glade.h:21
msgid "Save catalog"
msgstr "Zapisz katalog"
#: pygtktalog/views/glade/main.glade.h:35
#: pygtktalog/views/glade/main.glade.h:22
msgid "Set as _thumbnail"
msgstr "Ustaw jako miniaturÄ™"
#: pygtktalog/views/glade/main.glade.h:36
#: pygtktalog/views/glade/main.glade.h:23
msgid "Status bar"
msgstr "Pasek stanu"
#: pygtktalog/views/glade/main.glade.h:37
#: pygtktalog/views/glade/main.glade.h:24
msgid "Tags"
msgstr "Tagi"
#: pygtktalog/views/glade/main.glade.h:38
#: pygtktalog/views/glade/main.glade.h:25
msgid "Toolbar"
msgstr "Pasek narzędzi"
#: pygtktalog/views/glade/main.glade.h:39
#: pygtktalog/views/glade/main.glade.h:26
msgid "_Add images"
msgstr "Dodaj obrazy"
#: pygtktalog/views/glade/main.glade.h:40
#: pygtktalog/views/glade/files.glade.h:8
msgid "_Add tag"
msgstr "_Dodaj tag"
#: pygtktalog/views/glade/main.glade.h:41
#: pygtktalog/views/glade/main.glade.h:27
msgid "_Catalog"
msgstr "_Katalog"
#: pygtktalog/views/glade/main.glade.h:42
#: pygtktalog/views/glade/discs.glade.h:3
msgid "_Collapse all"
msgstr "_Zwiń wszystko"
#: pygtktalog/views/glade/main.glade.h:43
#: pygtktalog/views/glade/discs.glade.h:4
#: pygtktalog/views/glade/files.glade.h:9
msgid "_Delete"
msgstr "_Usuń"
#: pygtktalog/views/glade/main.glade.h:44
#: pygtktalog/views/glade/main.glade.h:28
msgid "_Delete images"
msgstr "Usuń obrazy"
#: pygtktalog/views/glade/main.glade.h:45
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:10
#: pygtktalog/views/glade/test.glade.h:1
msgid "_Edit"
msgstr "_Edycja"
#: pygtktalog/views/glade/main.glade.h:46
#: pygtktalog/views/glade/discs.glade.h:5
msgid "_Expand all"
msgstr "_Rozwiń wszystko"
#: pygtktalog/views/glade/main.glade.h:47 pygtktalog/views/main_menu.py:19
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/test.glade.h:2
msgid "_File"
msgstr "_Plik"
#: pygtktalog/views/glade/main.glade.h:48
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/test.glade.h:3
msgid "_Help"
msgstr "_Pomoc"
#: pygtktalog/views/glade/main.glade.h:49
#: pygtktalog/views/glade/main.glade.h:32
msgid "_Remove Thumbnail"
msgstr "Usuń miniaturę"
#: pygtktalog/views/glade/main.glade.h:50
#: pygtktalog/views/glade/discs.glade.h:6
#: pygtktalog/views/glade/files.glade.h:11
msgid "_Rename"
msgstr "_Zmień nazwę"
#: pygtktalog/views/glade/main.glade.h:51
#: pygtktalog/views/glade/main.glade.h:33
msgid "_Save images to..."
msgstr "Zapisz obrazy do..."
#: pygtktalog/views/glade/main.glade.h:52
#: pygtktalog/views/glade/discs.glade.h:7
msgid "_Statistics"
msgstr "_Statystyka"
#: pygtktalog/views/glade/main.glade.h:53
#: pygtktalog/views/glade/discs.glade.h:8
msgid "_Update"
msgstr "_Uaktualnij"
#: pygtktalog/views/glade/main.glade.h:54
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/test.glade.h:4
msgid "_View"
msgstr "_Widok"
#: pygtktalog/views/glade/main.glade.h:55
msgid "gtk-clear"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:56
#: pygtktalog/views/glade/main.glade.h:35
msgid "pyGTKtalog"
msgstr "pyGTKtalog"
#: pygtktalog/views/glade/main.glade.h:57
#: pygtktalog/views/glade/main.glade.h:36
msgid "pyGTKtalog - Image"
msgstr "pyGTKtalog - Obraz"
#: pygtktalog/controllers/main.py:47
msgid "Do you really want to quit?"
msgstr "Czy na pewno chcesz zakończyć?"
#: pygtktalog/controllers/main.py:48
msgid "Current database is not saved, any changes will be lost."
msgstr "BieĹĽÄ…cy katalog nie jest zapisany, wszelkie zmiany zostanÄ… utracone."
#: pygtktalog/controllers/main.py:48 pygtktalog/controllers/main.py:49
msgid "Quit application"
msgstr "Zakończ aplikację"
#: pygtktalog/models/main.py:16 pygtktalog/models/main.py:31
msgid "Idle"
msgstr "Bezczynny"
#: pygtktalog/controllers/files.py:33
#, fuzzy
msgid "Disc"
msgstr "Dyski"
#: pygtktalog/controllers/files.py:39
#, fuzzy
msgid "Filename"
msgstr "_Zmień nazwę"
#: pygtktalog/controllers/files.py:50
msgid "Path"
msgstr ""
#: pygtktalog/controllers/files.py:56
msgid "Size"
msgstr ""
#: pygtktalog/controllers/files.py:61
msgid "Date"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:5
msgid "gtk-about"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:6
msgid "gtk-copy"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:7
msgid "gtk-cut"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:8
msgid "gtk-delete"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:9
msgid "gtk-new"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:10
msgid "gtk-open"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:11
msgid "gtk-paste"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:12
msgid "gtk-quit"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:13
msgid "gtk-save"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:14
msgid "gtk-save-as"
msgstr ""

370
locale/pygtktalog.pot Normal file
View File

@@ -0,0 +1,370 @@
#
# pygtktalog Language File
#
msgid ""
msgstr ""
"Project-Id-Version: pygtktalog\n"
"POT-Creation-Date: 2009-08-26 22:10:33.892815\n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: utf-8\n"
#: pygtktalog/views/glade/main.glade.h:1
msgid "Add CD"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:2
msgid "Add CD/DVD to catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:3
msgid "Add Dir"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:4
msgid "Add _CD/DVD"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:5
msgid "Add _Directory"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:6
#: pygtktalog/views/glade/files.glade.h:1
msgid "Add _Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:7
#: pygtktalog/views/glade/files.glade.h:2
msgid "Add _Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:8
#: pygtktalog/views/glade/files.glade.h:3
msgid ""
"Add images to file. If file have no thumbnail,\n"
"thumbnail from first image will be generated."
msgstr ""
#: pygtktalog/views/glade/main.glade.h:10
#: pygtktalog/views/glade/main.glade.h:6
msgid "Cancel"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:11
#: pygtktalog/views/glade/main.glade.h:7
msgid "Catalog _statistics"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:12
#: pygtktalog/views/glade/discs.glade.h:1
msgid "Collapse all nodes"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:13
#: pygtktalog/views/glade/main.glade.h:8
msgid "Create new catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:14
#: pygtktalog/views/glade/main.glade.h:9
msgid "Delete all images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:15
#: pygtktalog/views/glade/main.glade.h:10
msgid "Delete all thumbnals"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:16
#: pygtktalog/views/glade/main.glade.h:11
msgid "Delete tag"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:17
#: pygtktalog/views/glade/main.glade.h:12
msgid "Deletes all images from files in current colection"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:18
#: pygtktalog/views/glade/main.glade.h:13
msgid "Discs"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:19
#: pygtktalog/views/glade/details.glade.h:1
msgid "Double click to open image"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:20
#: pygtktalog/views/glade/details.glade.h:2
msgid "EXIF"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:21
#: pygtktalog/views/glade/discs.glade.h:2
msgid "Expand all nodes"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:22
#: pygtktalog/views/glade/main.glade.h:14
msgid "Export"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:23
#: pygtktalog/views/glade/details.glade.h:3
msgid "File info"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:24
#: pygtktalog/views/glade/main.glade.h:15
msgid "Find file"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:25
#: pygtktalog/views/glade/details.glade.h:4
msgid "Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:26
#: pygtktalog/views/glade/main.glade.h:16
msgid "Import"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:27
#: pygtktalog/views/glade/main.glade.h:17
msgid "Open catalog file"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:28
#: pygtktalog/views/glade/main.glade.h:18
msgid "Quit pyGTKtalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:5
msgid "Re_move Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/main.glade.h:19
msgid "Recent files"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/files.glade.h:6
msgid "Rem_ove All Images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:32
#: pygtktalog/views/glade/files.glade.h:7
msgid "Remo_ve tag"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:33
#: pygtktalog/views/glade/main.glade.h:20
msgid "Save all images..."
msgstr ""
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/main.glade.h:21
msgid "Save catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:35
#: pygtktalog/views/glade/main.glade.h:22
msgid "Set as _thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:36
#: pygtktalog/views/glade/main.glade.h:23
msgid "Status bar"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:37
#: pygtktalog/views/glade/main.glade.h:24
msgid "Tags"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:38
#: pygtktalog/views/glade/main.glade.h:25
msgid "Toolbar"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:39
#: pygtktalog/views/glade/main.glade.h:26
msgid "_Add images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:40
#: pygtktalog/views/glade/files.glade.h:8
msgid "_Add tag"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:41
#: pygtktalog/views/glade/main.glade.h:27
msgid "_Catalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:42
#: pygtktalog/views/glade/discs.glade.h:3
msgid "_Collapse all"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:43
#: pygtktalog/views/glade/discs.glade.h:4
#: pygtktalog/views/glade/files.glade.h:9
msgid "_Delete"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:44
#: pygtktalog/views/glade/main.glade.h:28
msgid "_Delete images"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:45
#: pygtktalog/views/glade/main.glade.h:29
#: pygtktalog/views/glade/files.glade.h:10
#: pygtktalog/views/glade/test.glade.h:1
msgid "_Edit"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:46
#: pygtktalog/views/glade/discs.glade.h:5
msgid "_Expand all"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:47 pygtktalog/views/main_menu.py:19
#: pygtktalog/views/glade/main.glade.h:30
#: pygtktalog/views/glade/test.glade.h:2
msgid "_File"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:48
#: pygtktalog/views/glade/main.glade.h:31
#: pygtktalog/views/glade/test.glade.h:3
msgid "_Help"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:49
#: pygtktalog/views/glade/main.glade.h:32
msgid "_Remove Thumbnail"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:50
#: pygtktalog/views/glade/discs.glade.h:6
#: pygtktalog/views/glade/files.glade.h:11
msgid "_Rename"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:51
#: pygtktalog/views/glade/main.glade.h:33
msgid "_Save images to..."
msgstr ""
#: pygtktalog/views/glade/main.glade.h:52
#: pygtktalog/views/glade/discs.glade.h:7
msgid "_Statistics"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:53
#: pygtktalog/views/glade/discs.glade.h:8
msgid "_Update"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:54
#: pygtktalog/views/glade/main.glade.h:34
#: pygtktalog/views/glade/test.glade.h:4
msgid "_View"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:55
msgid "gtk-clear"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:56
#: pygtktalog/views/glade/main.glade.h:35
msgid "pyGTKtalog"
msgstr ""
#: pygtktalog/views/glade/main.glade.h:57
#: pygtktalog/views/glade/main.glade.h:36
msgid "pyGTKtalog - Image"
msgstr ""
#: pygtktalog/controllers/main.py:47
msgid "Do you really want to quit?"
msgstr ""
#: pygtktalog/controllers/main.py:48
msgid "Current database is not saved, any changes will be lost."
msgstr ""
#: pygtktalog/controllers/main.py:48 pygtktalog/controllers/main.py:49
msgid "Quit application"
msgstr ""
#: pygtktalog/models/main.py:16 pygtktalog/models/main.py:31
msgid "Idle"
msgstr ""
#: pygtktalog/controllers/files.py:33
msgid "Disc"
msgstr ""
#: pygtktalog/controllers/files.py:39
msgid "Filename"
msgstr ""
#: pygtktalog/controllers/files.py:50
msgid "Path"
msgstr ""
#: pygtktalog/controllers/files.py:56
msgid "Size"
msgstr ""
#: pygtktalog/controllers/files.py:61
msgid "Date"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:5
msgid "gtk-about"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:6
msgid "gtk-copy"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:7
msgid "gtk-cut"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:8
msgid "gtk-delete"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:9
msgid "gtk-new"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:10
msgid "gtk-open"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:11
msgid "gtk-paste"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:12
msgid "gtk-quit"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:13
msgid "gtk-save"
msgstr ""
#: pygtktalog/views/glade/test.glade.h:14
msgid "gtk-save-as"
msgstr ""

228
pavement.py Normal file
View File

@@ -0,0 +1,228 @@
"""
Project: pyGTKtalog
Description: Makefile and setup.py replacement. Used python packages -
paver, nosetests. External commands - xgettext, intltool-extract, hg,
grep.
Type: management
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-07
"""
import os
import sys
import shutil
from datetime import datetime
from paver.easy import sh, dry, call_task, options, Bunch
from paver.tasks import task, needs, help, cmdopts
from paver.setuputils import setup
from paver.misctasks import generate_setup, minilib
import paver.doctools
try:
from pylint import lint
HAVE_LINT = True
except ImportError:
HAVE_LINT = False
PO_HEADER = """#
# pygtktalog Language File
#
msgid ""
msgstr ""
"Project-Id-Version: pygtktalog\\n"
"POT-Creation-Date: %(time)s\\n"
"Last-Translator: Roman Dobosz<gryf73@gmail.com>\\n"
"MIME-Version: 1.0\\n"
"Content-Type: text/plain; charset=UTF-8\\n"
"Content-Transfer-Encoding: utf-8\\n"
"""
REV = os.popen("hg sum 2>/dev/null|grep ^Revis|cut -d ' ' -f 2").readlines()
if REV:
REV = "r" + REV[0].strip()
else:
REV = '0'
LOCALES = {'pl': 'pl_PL.utf8', 'en': 'en_EN'}
POTFILE = 'locale/pygtktalog.pot'
# distutil/setuptool setup method.
setup(
name='pyGTKtalog',
version='1.99.%s' % REV,
long_description='pyGTKtalog is application similar to WhereIsIT, '
'for collecting information on files from CD/DVD '
'or directories.',
description='pyGTKtalog is a file indexing tool written in pyGTK',
author='Roman Dobosz',
author_email='gryf73@gmail.com',
url='http://google.com',
platforms=['Linux', 'BSD'],
license='GNU General Public License (GPL)',
classifiers=['Development Status :: 2 - Pre-Alpha',
'Environment :: X11 Applications :: GTK',
'Intended Audience :: End Users/Desktop',
'License :: OSI Approved :: GNU General Public License '
'(GPL)',
'Natural Language :: English',
'Natural Language :: Polish',
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Topic :: Desktop Environment',
'Topic :: Utilities'],
include_package_data=True,
exclude_package_data={'': ['*.patch']},
packages=["pygtktalog"],
scripts=['bin/gtktalog.py'],
test_suite='nose.collector'
)
options(sphinx=Bunch(builddir="build", sourcedir="source"))
@task
@needs(['locale_gen', 'minilib', 'generate_setup'])
def sdist():
"""sdist with message catalogs"""
call_task("setuptools.command.sdist")
@task
@needs(['locale_gen'])
def build():
"""build with message catalogs"""
call_task("setuptools.command.build")
@task
def clean():
"""remove 'pyo', 'pyc', 'h' and '~' files"""
# clean *.pyc, *.pyo and jEdit backup files *~
for root, dummy, files in os.walk("."):
for fname in files:
if fname.endswith(".pyc") or fname.endswith(".pyo") or \
fname.endswith("~") or fname.endswith(".h") or \
fname == '.coverage':
fdel = os.path.join(root, fname)
os.unlink(fdel)
print "deleted", fdel
@task
@needs(["clean"])
def distclean():
"""make clean, and remove any dist/build/egg stuff from project"""
for dirname in [os.path.join('pygtktalog', 'locale'), 'build', 'dist',
'pyGTKtalog.egg-info']:
if os.path.exists(dirname):
shutil.rmtree(dirname, ignore_errors=True)
print "removed directory", dirname
for filename in ['paver-minilib.zip', 'setup.py', 'test/.coverage']:
if os.path.exists(filename):
os.unlink(filename)
print "deleted", filename
@task
def run():
"""run application"""
sh("PYTHONPATH=%s:$PYTHONPATH bin/gtktalog.py" % _setup_env())
#import gtktalog
#gtktalog.run()
@task
def pot():
"""generate 'pot' file out of python/glade files"""
if not os.path.exists('locale'):
os.mkdir('locale')
if not os.path.exists(POTFILE):
fname = open(POTFILE, "w")
fname.write(PO_HEADER % {'time': datetime.now()})
cmd = "xgettext --omit-header -k_ -kN_ -j -o %s %s"
cmd_glade = "intltool-extract --type=gettext/glade %s"
for walk_tuple in os.walk("pygtktalog"):
root = walk_tuple[0]
for fname in walk_tuple[2]:
if fname.endswith(".py"):
sh(cmd % (POTFILE, os.path.join(root, fname)))
elif fname.endswith(".glade"):
sh(cmd_glade % os.path.join(root, fname))
sh(cmd % (POTFILE, os.path.join(root, fname + ".h")))
@task
@needs(['pot'])
def locale_merge():
"""create or merge if exists 'po' translation files"""
potfile = os.path.join('locale', 'pygtktalog.pot')
for lang in LOCALES:
msg_catalog = os.path.join('locale', "%s.po" % lang)
if os.path.exists(msg_catalog):
sh('msgmerge -U %s %s' % (msg_catalog, potfile))
else:
shutil.copy(potfile, msg_catalog)
@task
@needs(['locale_merge'])
def locale_gen():
"""generate message catalog file for available locale files"""
full_path = os.path.join('pygtktalog', 'locale')
if not os.path.exists(full_path):
os.mkdir(full_path)
for lang in LOCALES:
lang_path = full_path
for dirname in [lang, 'LC_MESSAGES']:
lang_path = os.path.join(lang_path, dirname)
if not os.path.exists(lang_path):
os.mkdir(lang_path)
catalog_file = os.path.join(lang_path, 'pygtktalog.mo')
msg_catalog = os.path.join('locale', "%s.po" % lang)
sh('msgfmt %s -o %s' % (msg_catalog, catalog_file))
if HAVE_LINT:
@task
def pylint():
'''Check the module you're building with pylint.'''
pylintopts = ['pygtktalog']
dry('pylint %s' % (" ".join(pylintopts)), lint.Run, pylintopts)
@task
@cmdopts([('coverage', 'c', 'display coverage information')])
def test(options):
"""run unit tests"""
cmd = "PYTHONPATH=%s:$PYTHONPATH nosetests -w test" % _setup_env()
if hasattr(options.test, 'coverage'):
cmd += " --with-coverage --cover-package pygtktalog"
os.system(cmd)
@task
@needs(['locale_gen'])
def runpl():
"""run application with pl_PL localization. This is just for my
convenience"""
os.environ['LC_ALL'] = 'pl_PL.utf8'
run()
def _setup_env():
"""Helper function to set up paths"""
# current directory
this_path = os.path.dirname(os.path.abspath(__file__))
if this_path not in sys.path:
sys.path.insert(0, this_path)
return this_path

View File

@@ -1,11 +0,0 @@
#!/bin/sh
# Simple shell wraper to launch pyGTKtalog
# change path to pygtktalog.py file full path, then you can move this shell
# script to desired place (like /usr/bin, /usr/local/bin, ~/bin and so on)
path_to_gtktalog_directory="."
# python interpreter
python_intrpreter="/usr/bin/python"
exec $python_intrpreter -OO ${path_to_gtktalog_directory}/pygtktalog.py "$@"

View File

@@ -1,118 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
import sys
import os
try:
import gtk
except ImportError:
print "You need to install pyGTK v2.10.x or newer."
sys.exit(1)
def setup_path():
"""Sets up the python include paths to include needed directories"""
import os.path
from src.utils.globals import TOPDIR
sys.path = [os.path.join(TOPDIR, "src")] + sys.path
return
def check_requirements():
"""Checks versions and other requirements"""
import sys
import gtkmvc
gtkmvc.require("1.2.0")
try:
from models.m_config import ConfigModel
except ImportError:
print "Some fundamental files are missing.",
print "Try runnig pyGTKtalog in his root directory"
sys.exit(1)
conf = ConfigModel()
conf.load()
try:
import pygtk
#tell pyGTK, if possible, that we want GTKv2
pygtk.require("2.0")
except ImportError:
#Some distributions come with GTK2, but not pyGTK
pass
try:
from pysqlite2 import dbapi2 as sqlite
except ImportError:
print "pyGTKtalog uses SQLite DB.\nYou'll need to get it and the",
print "python bindings as well.",
print "http://www.sqlite.org"
print "http://initd.org/tracker/pysqlite"
sys.exit(1)
if conf.confd['exportxls']:
try:
import pyExcelerator
except ImportError:
print "WARNING: You'll need pyExcelerator, if you want to export",
print "DB to XLS format."
print "http://sourceforge.net/projects/pyexcelerator"
if conf.confd['thumbs'] and conf.confd['retrive']:
try:
import Image
except ImportError:
print "WARNING: You'll need Python Imaging Library (PIL), if you",
print "want to make\nthumbnails!"
return
if __name__ == "__main__":
# Directory from where pygtkatalog was invoced. We need it for calculate
# path for argument (catalog file)
execution_dir = os.path.abspath(os.path.curdir)
# Directory, where this files lies. We need it to setup private source
# paths
libraries_dir = os.path.dirname(__file__)
os.chdir(libraries_dir)
setup_path()
check_requirements()
from models.m_main import MainModel
from ctrls.c_main import MainController
from views.v_main import MainView
model = MainModel()
if len(sys.argv) > 1:
model.open(os.path.join(execution_dir, sys.argv[1]))
controler = MainController(model)
view = MainView(controler)
try:
gtk.main()
except KeyboardInterrupt:
model.config.save()
model.cleanup()
gtk.main_quit

1781
pygtktalog/EXIF.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
diff -au EXIF.py EXIF_mine.py
--- EXIF.py 2008-07-31 15:53:50.000000000 +0200
+++ EXIF_mine.py 2009-02-25 18:39:48.000000000 +0100
@@ -251,40 +251,54 @@
2: 'CenterWeightedAverage',
3: 'Spot',
4: 'MultiSpot',
- 5: 'Pattern'}),
+ 5: 'Pattern',
+ 6: 'Partial',
+ 255: 'other'}),
0x9208: ('LightSource',
{0: 'Unknown',
1: 'Daylight',
2: 'Fluorescent',
- 3: 'Tungsten',
- 9: 'Fine Weather',
- 10: 'Flash',
+ 3: 'Tungsten (incandescent light)',
+ 4: 'Flash',
+ 9: 'Fine weather',
+ 10: 'Cloudy weather',
11: 'Shade',
- 12: 'Daylight Fluorescent',
- 13: 'Day White Fluorescent',
- 14: 'Cool White Fluorescent',
- 15: 'White Fluorescent',
- 17: 'Standard Light A',
- 18: 'Standard Light B',
- 19: 'Standard Light C',
+ 12: 'Daylight fluorescent (D 5700 - 7100K)',
+ 13: 'Day white fluorescent (N 4600 - 5400K)',
+ 14: 'Cool white fluorescent (W 3900 - 4500K)',
+ 15: 'White fluorescent (WW 3200 - 3700K)',
+ 17: 'Standard light A',
+ 18: 'Standard light B',
+ 19: 'Standard light C',
20: 'D55',
21: 'D65',
22: 'D75',
- 255: 'Other'}),
+ 23: 'D50',
+ 24: 'ISO studio tungsten',
+ 255: 'other light source',}),
0x9209: ('Flash',
- {0: 'No',
- 1: 'Fired',
- 5: 'Fired (?)', # no return sensed
- 7: 'Fired (!)', # return sensed
- 9: 'Fill Fired',
- 13: 'Fill Fired (?)',
- 15: 'Fill Fired (!)',
- 16: 'Off',
- 24: 'Auto Off',
- 25: 'Auto Fired',
- 29: 'Auto Fired (?)',
- 31: 'Auto Fired (!)',
- 32: 'Not Available'}),
+ {0: 'Flash did not fire',
+ 1: 'Flash fired',
+ 5: 'Strobe return light not detected',
+ 7: 'Strobe return light detected',
+ 9: 'Flash fired, compulsory flash mode',
+ 13: 'Flash fired, compulsory flash mode, return light not detected',
+ 15: 'Flash fired, compulsory flash mode, return light detected',
+ 16: 'Flash did not fire, compulsory flash mode',
+ 24: 'Flash did not fire, auto mode',
+ 25: 'Flash fired, auto mode',
+ 29: 'Flash fired, auto mode, return light not detected',
+ 31: 'Flash fired, auto mode, return light detected',
+ 32: 'No flash function',
+ 65: 'Flash fired, red-eye reduction mode',
+ 69: 'Flash fired, red-eye reduction mode, return light not detected',
+ 71: 'Flash fired, red-eye reduction mode, return light detected',
+ 73: 'Flash fired, compulsory flash mode, red-eye reduction mode',
+ 77: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected',
+ 79: 'Flash fired, compulsory flash mode, red-eye reduction mode, return light detected',
+ 89: 'Flash fired, auto mode, red-eye reduction mode',
+ 93: 'Flash fired, auto mode, return light not detected, red-eye reduction mode',
+ 95: 'Flash fired, auto mode, return light detected, red-eye reduction mode'}),
0x920A: ('FocalLength', ),
0x9214: ('SubjectArea', ),
0x927C: ('MakerNote', ),

65
pygtktalog/__init__.py Normal file
View File

@@ -0,0 +1,65 @@
"""
Project: pyGTKtalog
Description: Initialization for main module - i18n and so.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-05
"""
__version__ = "1.9.0"
__appname__ = "pyGTKtalog"
__copyright__ = u"\u00A9 Roman 'gryf' Dobosz"
__summary__ = "%s is simple tool for managing file collections." % __appname__
__web__ = "http://bitbucket.org/gryf"
__logo_img__ = "views/pixmaps/Giant Worms.png"
import os
import sys
import locale
import gettext
import __builtin__
import gtk.glade
from logger import get_logger
__all__ = ['controllers',
'models',
'views',
'EXIF',
'dbcommon',
'dbobjects',
'dialogs',
'logger',
'misc']
GETTEXT_DOMAIN = 'pygtktalog'
# There should be message catalogs in "locale" directory placed by setup.py
# script. If there is no such directory, let's assume that message catalogs are
# placed in system wide location such as /usr/share/locale by Linux
# distribution package maintainer.
LOCALE_PATH = os.path.join(os.path.abspath(os.path.dirname(__file__)),
'locale')
try:
locale.setlocale(locale.LC_ALL, '')
except locale.Error:
# unknown locale string, fallback to C
locale.setlocale(locale.LC_ALL, 'C')
for module in gtk.glade, gettext:
if os.path.exists(LOCALE_PATH):
module.bindtextdomain(GETTEXT_DOMAIN, LOCALE_PATH)
else:
module.bindtextdomain(GETTEXT_DOMAIN)
module.textdomain(GETTEXT_DOMAIN)
# register the gettext function for the whole interpreter as "_"
__builtin__._ = gettext.gettext
# wrap errors into usefull message
def log_exception(exc_type, exc_val, traceback):
get_logger(__name__).error(exc_val)
sys.excepthook = log_exception

View File

View File

@@ -0,0 +1,18 @@
"""
Project: pyGTKtalog
Description: Controller for Details NoteBook
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
from gtkmvc import Controller
class DetailsController(Controller):
"""
Controller for details NoteBook.
"""
def register_view(self, view):
"""Default view registration stuff"""
pass

View File

@@ -0,0 +1,214 @@
"""
Project: pyGTKtalog
Description: Controller for Discs TreeView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
import gtk
from gtkmvc import Controller
from pygtktalog.logger import get_logger
LOG = get_logger("discs ctrl")
class DiscsController(Controller):
"""
Controller for discs TreeView
"""
def __init__(self, model, view):
"""
Initialize DiscsController
"""
LOG.debug(self.__init__.__doc__.strip())
Controller.__init__(self, model, view)
self.discs_model = self.model.discs.discs
def register_view(self, view):
"""
Do DiscTree registration
"""
LOG.debug(self.register_view.__doc__.strip())
view['discs'].set_model(self.discs_model)
# register observers
self.model.discs.register_observer(self)
# connect signals to popup menu - framework somehow omits automatic
# signal connection for subviews which are not included to widgets
# tree
sigs = {'expand_all': ('activate', self.on_expand_all_activate),
'collapse_all': ('activate', self.on_collapse_all_activate),
'update': ('activate', self.on_update_activate),
'rename': ('activate', self.on_rename_activate),
'delete': ('activate', self.on_delete_activate),
'statistics': ('activate', self.on_statistics_activate)}
for signal in sigs:
view.menu[signal].connect(sigs[signal][0], sigs[signal][1])
col = gtk.TreeViewColumn('discs_column')
cellpb = gtk.CellRendererPixbuf()
cell = gtk.CellRendererText()
# make cell text editabe
cell.set_property('editable', True)
cell.connect('edited', self.on_editing_done, self.discs_model)
# TODO: find a way how to disable default return keypress on editable
# fields
col.pack_start(cellpb, False)
col.pack_start(cell, True)
col.set_attributes(cellpb, stock_id=2)
col.set_attributes(cell, text=1)
view['discs'].append_column(col)
view['discs'].show()
# treeview signals
def on_discs_button_press_event(self, treeview, event):
"""
Handle right click on discs treeview - show popup menu.
"""
LOG.debug(self.on_discs_button_press_event.__doc__.strip())
pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
if event.button == 3:
if pathinfo:
path = pathinfo[0]
# Make sure, that there is selected row
sel = treeview.get_selection()
sel.unselect_all()
sel.select_path(path)
self._popup_menu(sel, event, event.button)
else:
self._popup_menu(None, event, event.button)
return True
def on_discs_cursor_changed(self, treeview):
"""
Show files on right treeview, after clicking the left disc treeview.
"""
LOG.debug(self.on_discs_cursor_changed.__doc__.strip())
selection = treeview.get_selection()
path = selection.get_selected_rows()[1][0]
self.model.files.refresh(self.discs_model.get_value(\
self.discs_model.get_iter(path), 0))
def on_discs_key_release_event(self, treeview, event):
"""
Watch for specific keys
"""
LOG.debug(self.on_discs_key_release_event.__doc__.strip())
if gtk.gdk.keyval_name(event.keyval) == 'Menu':
LOG.debug('Menu key pressed')
self._popup_menu(treeview.get_selection(), event, 0)
return True
return False
def on_discs_row_activated(self, treeview, path, treecolumn):
"""
If possible, expand or collapse branch of discs tree
"""
LOG.debug(self.on_discs_row_activated.__doc__.strip())
if treeview.row_expanded(path):
treeview.collapse_row(path)
else:
treeview.expand_row(path, False)
def on_editing_done(self, cell, path, new_text, model):
"""
Store changed filename text
"""
LOG.debug(self.on_editing_done.__doc__.strip())
model[path][0].filename = new_text
model[path][1] = new_text
return
# popup menu signals
def on_expand_all_activate(self, menu_item):
"""
Expand all
"""
LOG.debug(self.on_expand_all_activate.__doc__.strip())
self.view['discs'].expand_all()
def on_collapse_all_activate(self, menu_item):
"""
Collapse all
"""
LOG.debug(self.on_collapse_all_activate.__doc__.strip())
self.view['discs'].collapse_all()
def on_update_activate(self, menu_item):
"""
Trigger update specified tree entry
"""
LOG.debug(self.on_update_activate.__doc__.strip())
raise NotImplementedError
def on_rename_activate(self, menu_item):
"""
Rename disk or directory
"""
LOG.debug(self.on_rename_activate.__doc__.strip())
treeview = self.view['discs']
selection = treeview.get_selection()
path = selection.get_selected_rows()[1][0]
treeview.set_cursor(path, treeview.get_column(0), start_editing=True)
def on_delete_activate(self, menu_item):
"""
Delete disk or directory from catalog
"""
LOG.debug(self.on_delete_activate.__doc__.strip())
raise NotImplementedError
def on_statistics_activate(self, menu_item):
"""
Show statistics for selected item
"""
LOG.debug(self.on_statistics_activate.__doc__.strip())
raise NotImplementedError
# observable properties
def property_currentdir_value_change(self, model, old, new):
"""
Change of a current dir signalized by other controllers/models
"""
LOG.debug(self.property_currentdir_value_change.__doc__.strip())
self._set_cursor_to_obj_position(new)
# private methods
def _popup_menu(self, selection, event, button):
"""
Popup menu for discs treeview. Gather information from discs model,
and trigger menu popup.
"""
LOG.debug(self._popup_menu.__doc__.strip())
if selection is None:
self.view.menu.set_menu_items_sensitivity(False)
else:
model, list_of_paths = selection.get_selected_rows()
for path in list_of_paths:
self.view.menu.set_menu_items_sensitivity(True)
self.view.menu.set_update_sensitivity(model.get_value(\
model.get_iter(path), 0).parent_id == 1)
self.view.menu['discs_popup'].popup(None, None, None,
button, event.time)
def _set_cursor_to_obj_position(self, obj):
"""
Set cursor/focus to specified object postion in Discs treeview.
"""
path = self.model.discs.find_path(obj)
self.view['discs'].expand_to_path(path)
self.view['discs'].set_cursor(path)

View File

@@ -0,0 +1,282 @@
"""
Project: pyGTKtalog
Description: Controller for Files TreeView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
import gtk
from gtkmvc import Controller
from pygtktalog.pygtkutils import get_tv_item_under_cursor
from pygtktalog.logger import get_logger
LOG = get_logger("files ctrl")
class FilesController(Controller):
"""
Controller for files TreeView list.
"""
def __init__(self, model, view):
"""
FilesController initialization
"""
Controller.__init__(self, model, view)
self.DND_TARGETS = [('files_tags', 0, 69)]
self.files_model = self.model.files
def register_view(self, view):
"""
Register view, and setup columns for files treeview
"""
view['files'].set_model(self.files_model.files)
sigs = {"add_tag": ("activate", self.on_add_tag1_activate),
"delete_tag": ("activate", self.on_delete_tag_activate),
"add_thumb": ("activate", self.on_add_thumb1_activate),
"remove_thumb": ("activate", self.on_remove_thumb1_activate),
"add_image": ("activate", self.on_add_image1_activate),
"remove_image": ("activate", self.on_remove_image1_activate),
"edit": ("activate", self.on_edit2_activate),
"delete": ("activate", self.on_delete3_activate),
"rename": ("activate", self.on_rename2_activate)}
for signal in sigs:
view.menu[signal].connect(sigs[signal][0], sigs[signal][1])
view['files'].get_selection().set_mode(gtk.SELECTION_MULTIPLE)
col = gtk.TreeViewColumn(_('Disc'), gtk.CellRendererText(), text=1)
col.set_sort_column_id(1)
col.set_resizable(True)
col.set_visible(False)
view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Filename'))
cellpb = gtk.CellRendererPixbuf()
cell = gtk.CellRendererText()
col.pack_start(cellpb, False)
col.pack_start(cell, True)
col.set_attributes(cellpb, stock_id=7)
col.set_attributes(cell, text=2)
col.set_sort_column_id(2)
col.set_resizable(True)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Path'), gtk.CellRendererText(), text=3)
col.set_sort_column_id(3)
col.set_resizable(True)
col.set_visible(False)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Size'), gtk.CellRendererText(), text=4)
col.set_sort_column_id(4)
col.set_resizable(True)
self.view['files'].append_column(col)
col = gtk.TreeViewColumn(_('Date'), gtk.CellRendererText(), text=5)
col.set_sort_column_id(5)
col.set_resizable(True)
self.view['files'].append_column(col)
self.view['files'].set_search_column(2)
# setup d'n'd support
self.view['files'].drag_source_set(gtk.gdk.BUTTON1_MASK,
self.DND_TARGETS,
gtk.gdk.ACTION_COPY)
# signals
def on_files_drag_data_get(self, treeview, context, selection,
targetType, eventTime):
"""responce to "data get" DnD signal"""
# get selection, and send it to the client
if targetType == self.DND_TARGETS[0][2]:
# get selection
treesrl = treeview.get_selection()
model, list_of_paths = treesrl.get_selected_rows()
ids = []
for path in list_of_paths:
fid = model.get_value(model.get_iter(path), 0)
ids.append(fid)
string = str(tuple(ids)).replace(",)", ")")
selection.set(selection.target, 8, string)
def on_files_button_press_event(self, treeview, event):
"""
Handle right click on files treeview - show popup menu.
"""
LOG.debug("Mouse button pressed")
pathinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
if event.button == 3: # Right mouse button. Show context menu.
LOG.debug("It's a right button")
if pathinfo:
path = pathinfo[0]
# Make sure, that there is selected row
sel = treeview.get_selection()
sel.unselect_all()
sel.select_path(path)
self._popup_menu(sel, event, event.button)
else:
self._popup_menu(None, event, event.button)
return True
#try:
# selection = tree.get_selection()
# model, list_of_paths = selection.get_selected_rows()
#except TypeError:
# list_of_paths = []
#if len(list_of_paths) == 0:
# # try to select item under cursor
# try:
# path, column, x, y = tree.get_path_at_pos(int(event.x),
# int(event.y))
# except TypeError:
# # failed, do not show any popup and return
# tree.get_selection().unselect_all()
# return False
# selection.select_path(path[0])
#if len(list_of_paths) > 1:
# self.view['add_image'].set_sensitive(False)
# self.view['rename'].set_sensitive(False)
# self.view['edit'].set_sensitive(False)
#else:
# self.view['add_image'].set_sensitive(True)
# self.view['rename'].set_sensitive(True)
# self.view['edit'].set_sensitive(True)
#self.__popup_menu(event, 'files_popup')
#return True
LOG.debug("It's other button")
def on_files_cursor_changed(self, treeview):
"""Show details of selected file/directory"""
file_id = get_tv_item_under_cursor(treeview)
LOG.debug("found item: %s" % file_id)
return
def on_files_key_release_event(self, treeview, event):
"""do something with pressed keys"""
if gtk.gdk.keyval_name(event.keyval) == 'Menu':
try:
selection = treeview.get_selection()
model, list_of_paths = selection.get_selected_rows()
if not list_of_paths:
return
except TypeError:
return
self._popup_menu(selection, event, 0)
if gtk.gdk.keyval_name(event.keyval) == 'BackSpace':
row, gtk_column = self.view['files'].get_cursor()
if row and gtk_column:
fileob = self.files_model.get_value(row)
if fileob.parent.parent.id != 1:
#self.files_model.refresh(fileob.parent.parent)
# TODO: synchronize with disks
self.model.discs.currentdir = fileob.parent.parent
self.view['files'].grab_focus()
def on_files_row_activated(self, files_obj, row, column):
"""
On directory doubleclick in files listview dive into desired branch.
"""
fileob = self.files_model.get_value(row=row)
if not fileob.children:
# ONLY directories. files are omitted.
return
self.files_model.refresh(fileob)
self.model.discs.currentdir = fileob
self.view['files'].grab_focus()
# TODO: synchronize with disks
return
def on_add_tag1_activate(self, menu_item):
"""
TODO
"""
LOG.debug(self.on_add_tag1_activate.__doc__.strip())
raise NotImplementedError
def on_delete_tag_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_delete_tag_activate.__doc__.strip())
raise NotImplementedError
def on_add_thumb1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_add_thumb1_activate.__doc__.strip())
raise NotImplementedError
def on_remove_thumb1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_remove_thumb1_activate.__doc__.strip())
raise NotImplementedError
def on_add_image1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_add_image1_activate.__doc__.strip())
raise NotImplementedError
def on_remove_image1_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_remove_image1_activate.__doc__.strip())
raise NotImplementedError
def on_edit2_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_edit2_activate.__doc__.strip())
raise NotImplementedError
def on_delete3_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_delete3_activate.__doc__.strip())
raise NotImplementedError
def on_rename2_activate(self, menuitem):
"""
TODO
"""
LOG.debug(self.on_rename2_activate.__doc__.strip())
raise NotImplementedError
# private methods
def _popup_menu(self, selection, event, button):
"""
Popup menu for files treeview. Gather information from discs model,
and trigger menu popup.
"""
LOG.debug(self._popup_menu.__doc__.strip())
if selection is None:
self.view.menu.set_menu_items_sensitivity(False)
else:
model, list_of_paths = selection.get_selected_rows()
for path in list_of_paths:
self.view.menu.set_menu_items_sensitivity(True)
self.view.menu['files_popup'].popup(None, None, None,
button, event.time)

View File

@@ -0,0 +1,201 @@
"""
Project: pyGTKtalog
Description: Controller for main window
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import gtk
from gtkmvc import Controller
from pygtktalog.controllers.discs import DiscsController
from pygtktalog.controllers.files import FilesController
#from pygtktalog.controllers.details import DetailsController
#from pygtktalog.controllers.tags import TagcloudController
#from pygtktalog.dialogs import yesno, okcancel, info, warn, error
from pygtktalog.dialogs import open_catalog, save_catalog, error, yesno, about
from pygtktalog.logger import get_logger
LOG = get_logger("main controller")
class MainController(Controller):
"""
Controller for main application window
"""
TITLE = "pyGTKtalog"
UNTITLED = _("untitled")
def __init__(self, model, view):
"""
Initialize MainController, add controllers for trees and details.
"""
LOG.debug(self.__init__.__doc__.strip())
Controller.__init__(self, model, view)
# add controllers for files/tags/details components
self.discs = DiscsController(model, view.discs)
self.files = FilesController(model, view.files)
#self.details = DetailsController(model, view.details)
#self.tags = TagcloudController(model, view.tags)
def register_view(self, view):
"""
Registration view for MainController class
"""
LOG.debug(self.register_view.__doc__.strip())
LOG.debug("replace hardcoded defaults with configured!")
view['main'].set_default_size(800, 600)
view['hpaned1'].set_position(200)
if not self.model.tmp_filename:
view.set_widgets_app_sensitivity(False)
view['main'].show()
# status bar
LOG.debug("register statusbar")
self.context_id = self.view['mainStatus'].get_context_id('status')
self.statusbar_id = \
self.view['mainStatus'].push(self.context_id,
self.model.status_bar_message)
# signals
def on_main_destroy_event(self, widget, event):
"""
Window destroyed. Cleanup before quit.
"""
LOG.debug(self.on_main_destroy_event.__doc__.strip())
self.on_quit_activate(widget)
return True
def on_quit_activate(self, widget):
"""
Quit and save window parameters to config file
"""
LOG.debug(self.on_quit_activate.__doc__.strip())
#if yesno(_("Do you really want to quit?"),
# _("Current database is not saved, any changes will be "
# "lost."), _("Quit application") + " - pyGTKtalog", 0):
self.model.cleanup()
LOG.debug("quit application")
gtk.main_quit()
return False
def on_new_activate(self, widget):
"""
Create new catalog file
"""
LOG.debug(self.on_new_activate.__doc__.strip())
self.model.new()
self._set_title()
self.view.set_widgets_app_sensitivity(True)
def on_open_activate(self, widget):
"""
Open catalog file
"""
LOG.debug(self.on_open_activate.__doc__.strip())
if self.model.db_unsaved and 'confirm' in self.model.config and \
self.model.config['confirm']:
if not yesno(_("Current database is not saved"),
_("Do you really want to open new file and abandon "
"current one?"),
_("Unsaved data")):
LOG.debug("Cancel opening catalog file - unsaved data remain")
return
initial_path = None
#if self.model.config.recent and self.model.config.recent[0]:
# initial_path = os.path.dirname(self.model.config.recent[0])
#if not path:
path = open_catalog(path=initial_path)
if not path:
return
# cleanup files and details
try:
self.model.files_list.clear()
except:
pass
#self.__hide_details()
#self.view['tag_path_box'].hide()
#buf = self.view['tag_cloud_textview'].get_buffer()
#buf.set_text('')
#self.view['tag_cloud_textview'].set_buffer(buf)
if not self.model.open(path):
error(_("Cannot open file."),
_("File %s cannot be open") % path,
_("Error opening file"))
else:
#self.__generate_recent_menu()
#self.__activate_ui(path)
#self.__tag_cloud()
self.view.set_widgets_app_sensitivity()
self._set_title()
return
def on_about1_activate(self, widget):
"""Show about dialog"""
about()
def on_save_activate(self, widget):
"""
Save current catalog
"""
LOG.debug(self.on_save_activate.__doc__.strip())
if not self.model.cat_fname:
self.on_save_as_activate(widget)
else:
self.model.save()
self._set_title()
def on_save_as_activate(self, widget):
"""
Save current catalog under differnet file
"""
LOG.debug(self.on_save_as_activate.__doc__.strip())
initial_path = None
#if self.model.config.recent[0]:
# initial_path = os.path.dirname(self.model.config.recent[0])
path = save_catalog(path=initial_path)
if path:
ret, err = self.model.save(path)
if ret:
#self.model.config.add_recent(path)
self._set_title()
pass
else:
error(_("Cannot write file %s.") % path,
"%s" % err,
_("Error writing file"))
# helpers
def _set_title(self):
"""
Get title of the main window, to reflect state of the catalog file
Returns:
String with apropriate title for main form
"""
LOG.debug("change the title")
if not self.model.tmp_filename:
LOG.debug("application has been initialized, title should"
" be empty")
fname = ""
elif not self.model.cat_fname:
fname = self.UNTITLED
else:
fname = self.model.cat_fname
modified = self.model.db_unsaved and "*" or ""
self.view['main'].set_title("%s%s - %s" % (fname,
modified, self.TITLE))

View File

@@ -0,0 +1,23 @@
"""
Project: pyGTKtalog
Description: Controller for Tagcloud TextView
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-30
"""
from gtkmvc import Controller
class TagcloudController(Controller):
"""
Controller for Tagcloud TextView
"""
#def __init__(self, model, view):
# """Initialize main controller"""
# Controller.__init__(self, model, view)
# return
def register_view(self, view):
"""Default view registration stuff"""
pass

45
pygtktalog/dbcommon.py Normal file
View File

@@ -0,0 +1,45 @@
"""
Project: pyGTKtalog
Description: Common database operations.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-07
"""
from sqlalchemy import MetaData, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base
from pygtktalog.logger import get_logger
# setup SQLAlchemy logging facility
# TODO: Logger("sqlalchemy")
# or maybe it will be better to separate sqlalchemy stuff from application
#get_logger("sqlalchemy", 'INFO')
# Prepare SQLAlchemy objects
Meta = MetaData()
Base = declarative_base(metadata=Meta)
Session = sessionmaker()
LOG = get_logger("dbcommon")
def connect(filename=None):
"""
create engine and bind to Meta object.
Arguments:
@filename - string with absolute or relative path to sqlite database
file. If None, db in-memory will be created
"""
if not filename:
filename = ':memory:'
LOG.info("db filename: %s" % filename)
connect_string = "sqlite:///%s" % filename
engine = create_engine(connect_string)
Meta.bind = engine
Meta.create_all(checkfirst=True)

299
pygtktalog/dbobjects.py Normal file
View File

@@ -0,0 +1,299 @@
"""
Project: pyGTKtalog
Description: Definition of DB objects classes. Using SQLAlchemy.
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-08-07
"""
import os
from cStringIO import StringIO
from hashlib import sha256
from sqlalchemy import Column, Table, Integer, Text, Binary, \
DateTime, ForeignKey, Sequence
from sqlalchemy.orm import relation, backref
from pygtktalog.dbcommon import Base
from pygtktalog.thumbnail import ThumbCreator
IMG_PATH = "/home/gryf/.pygtktalog/imgs/" # FIXME: should be configurable
tags_files = Table("tags_files", Base.metadata,
Column("file_id", Integer, ForeignKey("files.id")),
Column("tag_id", Integer, ForeignKey("tags.id")))
TYPE = {'root': 0, 'dir': 1, 'file': 2, 'link': 3}
class File(Base):
__tablename__ = "files"
id = Column(Integer, Sequence("file_id_seq"), primary_key=True)
parent_id = Column(Integer, ForeignKey("files.id"))
filename = Column(Text)
filepath = Column(Text)
date = Column(DateTime)
size = Column(Integer)
type = Column(Integer)
source = Column(Integer)
note = Column(Text)
description = Column(Text)
checksum = Column(Text)
thumbnail = Column(Binary)
children = relation('File',
backref=backref('parent', remote_side="File.id"),
order_by=[type, filename])
tags = relation("Tag", secondary=tags_files, order_by="Tag.tag")
#thumbnail = relation("Thumbnail", backref="file")
images = relation("Image", backref="file")
def __init__(self, filename=None, path=None, date=None, size=None,
ftype=None, src=None):
"""Create file object with empty defaults"""
self.filename = filename
self.filepath = path
self.date = date
self.size = size
self.type = ftype
self.source = src
def __repr__(self):
return "<File('%s', %s)>" % (str(self.filename), str(self.id))
def generate_checksum(self):
"""
Generate checksum of first 10MB of the file
"""
if self.type != TYPE['file']:
return
buf = open(os.path.join(self.filepath, self.filename)).read(10485760)
self.checksum = sha256(buf).hexdigest()
def get_all_children(self):
"""
Return list of all node direct and indirect children
"""
def _recursive(node):
children = []
if node.children:
for child in node.children:
children += _recursive(child)
if node != self:
children.append(node)
return children
if self.children:
return _recursive(self)
else:
return []
class Group(Base):
__tablename__ = "groups"
id = Column(Integer, Sequence("group_id_seq"), primary_key=True)
name = Column(Text)
color = Column(Text)
def __init__(self, name=None, color=None):
self.name = name
self.color = color
def __repr__(self):
return "<Group('%s', %s)>" % (str(self.name), str(self.id))
class Tag(Base):
__tablename__ = "tags"
id = Column(Integer, Sequence("tags_id_seq"), primary_key=True)
group_id = Column(Integer, ForeignKey("groups.id"))
tag = Column(Text)
group = relation('Group', backref=backref('tags', remote_side="Group.id"))
files = relation("File", secondary=tags_files)
def __init__(self, tag=None, group=None):
self.tag = tag
self.group = group
def __repr__(self):
return "<Tag('%s', %s)>" % (str(self.tag), str(self.id))
#class Thumbnail(Base):
# __tablename__ = "thumbnails"
# id = Column(Integer, Sequence("thumbnail_id_seq"), primary_key=True)
# file_id = Column(Integer, ForeignKey("files.id"))
# filename = Column(Text)
#
# def __init__(self, filename=None, file_obj=None):
# self.filename = filename
# self.file = file_obj
# if self.filename:
# self.save(self.filename)
#
# def save(self, fname):
# """
# Create file related thumbnail, add it to the file object.
# """
# new_name = sha1(str(uuid1())).hexdigest()
# new_name = [new_name[start:start+10] for start in range(0,
# len(new_name),
# 10)]
# try:
# os.makedirs(os.path.join(IMG_PATH, *new_name[:-1]))
# except OSError as exc:
# if exc.errno != errno.EEXIST:
# raise
#
# ext = os.path.splitext(self.filename)[1]
# if ext:
# new_name.append("".join([new_name.pop(), ext]))
#
# thumb = thumbnail.Thumbnail(self.filename)
# thumb_tmp_name = thumb.save()
# name, ext = os.path.splitext(new_name.pop())
# new_name.append("".join([name, "_t", '.jpg']))
# self.filename = os.path.sep.join(new_name)
# shutil.move(thumb_tmp_name, os.path.join(IMG_PATH, *new_name))
#
# def get_copy(self):
# """
# Create the very same object as self with exception of id field
# """
# thumb = Thumbnail()
# thumb.filename = self.filename
# return thumb
#
# def __repr__(self):
# return "<Thumbnail('%s', %s)>" % (str(self.filename), str(self.id))
#
class Image(Base):
__tablename__ = "images"
id = Column(Integer, Sequence("images_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
image = Column(Binary)
thumb = Column(Binary)
checksum = Column(Text)
def __init__(self, filename=None, file_obj=None):
self.file = file_obj
if filename:
self.save(filename)
def save(self, fname):
"""
Save and create coressponding thumbnail (note: it differs from file
related thumbnail!)
"""
file_buffer = StringIO()
with open(fname) as f:
file_buffer.write(f.read())
self.image = file_buffer.getvalue()
self.checksum = sha256(file_buffer.getvalue()).hexdigest()
file_buffer.seek(0)
thumb = ThumbCreator(fname).generate()
if thumb:
self.thumb = thumb.getvalue()
thumb.close()
file_buffer.close()
def get_copy(self):
"""
Create the very same object as self with exception of id field
"""
img = Image()
img.image = self.image
img.thumb = self.thumb
img.checksum = self.checksum
return img
@property
def fthumb(self):
"""
Return file-like object with thumbnail
"""
if self.thumb:
buf = StringIO()
buf.write(self.thumb)
buf.seek(0)
return buf
else:
return None
@property
def fimage(self):
"""
Return file-like object with image
"""
if self.image:
buf = StringIO()
buf.write(self.image)
buf.seek(0)
return buf
else:
return None
def __repr__(self):
return "<Image(%s)>" % str(self.id)
class Exif(Base):
__tablename__ = "exif"
id = Column(Integer, Sequence("exif_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
camera = Column(Text)
date = Column(Text)
aperture = Column(Text)
exposure_program = Column(Text)
exposure_bias = Column(Text)
iso = Column(Text)
focal_length = Column(Text)
subject_distance = Column(Text)
metering_mode = Column(Text)
flash = Column(Text)
light_source = Column(Text)
resolution = Column(Text)
orientation = Column(Text)
def __init__(self):
self.camera = None
self.date = None
self.aperture = None
self.exposure_program = None
self.exposure_bias = None
self.iso = None
self.focal_length = None
self.subject_distance = None
self.metering_mode = None
self.flash = None
self.light_source = None
self.resolution = None
self.orientation = None
def __repr__(self):
return "<Exif('%s', %s)>" % (str(self.date), str(self.id))
class Gthumb(Base):
__tablename__ = "gthumb"
id = Column(Integer, Sequence("gthumb_id_seq"), primary_key=True)
file_id = Column(Integer, ForeignKey("files.id"))
note = Column(Text)
place = Column(Text)
date = Column(DateTime)
def __init__(self, note=None, place=None, date=None):
self.note = note
self.place = place
self.date = date
def __repr__(self):
return "<Gthumb('%s', '%s', %s)>" % (str(self.date), str(self.place),
str(self.id))

231
pygtktalog/dialogs.py Normal file
View File

@@ -0,0 +1,231 @@
"""
Project: pyGTKtalog
Description: Simple dialogs for interact with user
Type: helper
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-12
"""
import os
import gtk
import pygtktalog
class Dialog(object):
"""
Show simple dialog for questions
Returns: Bool - True, if "OK" button pressed, False otherwise.
"""
def __init__(self, dialog_type, message, secondary_msg="", title=""):
"""
Initialize some defaults
"""
self.dialog = None
self.buttons = gtk.BUTTONS_OK
self.ok_default = False
self.message = message
self.secondary_msg = secondary_msg
self.type = dialog_type
self.title = title
def run(self):
"""Show the dialog"""
if self.dialog is None:
self._create_dialog()
# Change default/focus from cancel/no to ok/yes. Suitable only for
# Question dialog.
# Ofcourse, if something changes in the future, this could break
# things.
if self.ok_default:
# this is tricky: Ok/Yes buttons appears as first on the list, but
# they are visibile in oposite order. This could be a bug.
button = self.dialog.get_action_area().get_children()[0]
button.grab_default()
retval = self.dialog.run()
self.dialog.destroy()
if retval == gtk.RESPONSE_OK or retval == gtk.RESPONSE_YES:
return True
return False
def _create_dialog(self):
"""Create MessageDialog widgt"""
if self.type == gtk.MESSAGE_QUESTION and \
self.buttons not in (gtk.BUTTONS_YES_NO, gtk.BUTTONS_OK_CANCEL):
self.buttons = gtk.BUTTONS_YES_NO
self.dialog = gtk.MessageDialog(buttons=self.buttons, type=self.type,
message_format=self.message)
self.dialog.format_secondary_text(self.secondary_msg)
self.dialog.set_title(self.title)
class ChooseFile(object):
"""
Common file chooser
"""
URI = None
BUTTON_PAIRS = {'cancel': (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL),
'ok': (gtk.STOCK_OK, gtk.RESPONSE_APPLY),
'save': (gtk.STOCK_SAVE, gtk.RESPONSE_APPLY),
'open': (gtk.STOCK_OPEN, gtk.RESPONSE_APPLY)}
CHOOSER_TYPES = {'open': gtk.FILE_CHOOSER_ACTION_OPEN,
'save': gtk.FILE_CHOOSER_ACTION_SAVE}
FILTERS = {'catalogs': {'name': "Catalog files",
'patterns': ("*.sqlite", "*.sqlite.bz2")},
'all': {'name': "All files", 'patterns': ("*.*",)}}
def __init__(self, title="", buttons=('cancel', 'ok'), path=None,
chooser_type="open"):
super(ChooseFile, self).__init__()
self.path = path
self.title = title
self.action = self.CHOOSER_TYPES[chooser_type]
self.buttons = []
for button in buttons:
self.buttons.append(self.BUTTON_PAIRS[button][0])
self.buttons.append(self.BUTTON_PAIRS[button][1])
self.buttons = tuple(self.buttons)
self.confirmation = False
self.dialog = None
self.filters = []
def _mk_dialog(self):
"""
Create FileChooserDialog object
"""
self.dialog = gtk.FileChooserDialog(self.title, None, self.action,
self.buttons)
self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
self.dialog.set_default_response(gtk.RESPONSE_OK)
self.dialog.set_do_overwrite_confirmation(self.confirmation)
self.dialog.set_title(self.title)
if self.URI:
self.dialog.set_current_folder_uri(self.URI)
elif self.path and os.path.exists(self.path):
self.path = "file://" + os.path.abspath(self.path)
self.dialog.set_current_folder_uri(self.path)
for filtr in self._get_filters():
self.dialog.add_filter(filtr)
def _get_filters(self):
"""
"""
filters = []
for filter_def in self.filters:
filtr = gtk.FileFilter()
filtr.set_name(self.FILTERS[filter_def]['name'])
for pat in self.FILTERS[filter_def]['patterns']:
filtr.add_pattern(pat)
filters.append(filtr)
return filters
def run(self):
"""
Show dialog, get response.
Returns:
Returns: String - with filename, None otherwise.
"""
if self.dialog is None:
self._mk_dialog()
response = self.dialog.run()
filename = None
if response == gtk.RESPONSE_APPLY:
filename = self.dialog.get_filename()
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
def yesno(message, secondarymsg="", title="", default=False):
"""Question with yes-no buttons. Returns False on 'no', True on 'yes'"""
dialog = Dialog(gtk.MESSAGE_QUESTION, message, secondarymsg, title)
dialog.buttons = gtk.BUTTONS_YES_NO
dialog.ok_default = default
return dialog.run()
def okcancel(message, secondarymsg="", title="", default=False):
"""Question with ok-cancel buttons. Returns False on 'cancel', True on
'ok'"""
dialog = Dialog(gtk.MESSAGE_QUESTION, message, secondarymsg, title)
dialog.buttons = gtk.BUTTONS_OK_CANCEL
dialog.ok_default = default
return dialog.run()
def info(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Info dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_INFO, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def warn(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Warning dialog. Button defaults to gtk.BUTTONS_OK, but can be changed
with gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_WARNING, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def error(message, secondarymsg="", title="", button=gtk.BUTTONS_OK):
"""Error dialog. Button defaults to gtk.BUTTONS_OK, but can be changed with
gtk.BUTTONS_CANCEL, gtk.BUTTONS_CLOSE or gtk.BUTTONS_NONE.
Always returns True."""
dialog = Dialog(gtk.MESSAGE_ERROR, message, secondarymsg, title)
dialog.buttons = button
dialog.run()
return True
def open_catalog(title=_("Open catalog"), path=None):
"""
Request filename from user to open.
Returns: string - full path and filename or None
"""
requester = ChooseFile(title)
requester.filters = ['catalogs', 'all']
return requester.run()
def save_catalog(title=_("Open catalog"), path=None):
"""
Request filename from user for save.
Returns: string - full path and filename or None
"""
requester = ChooseFile(title, chooser_type="save")
requester.filters = ['catalogs', 'all']
requester.confirmation = True
return requester.run()
def about():
"""
Show About dialog
"""
dialog = gtk.AboutDialog()
dialog.set_version(pygtktalog.__version__)
dialog.set_program_name(pygtktalog.__appname__)
dialog.set_copyright(pygtktalog.__copyright__)
dialog.set_comments(pygtktalog.__summary__)
dialog.set_website(pygtktalog.__web__)
dialog.set_logo(gtk.gdk.pixbuf_new_from_file(\
os.path.join(os.path.dirname(__file__), pygtktalog.__logo_img__)))
dialog.run()
dialog.destroy()

81
pygtktalog/logger.py Normal file
View File

@@ -0,0 +1,81 @@
"""
Project: pyGTKtalog
Description: Logging functionality
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-09-02
"""
import os
import sys
import logging
LEVEL = {'DEBUG': logging.DEBUG,
'INFO': logging.INFO,
'WARN': logging.WARN,
'ERROR': logging.ERROR,
'CRITICAL': logging.CRITICAL}
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
RESET_SEQ = "\033[0m"
COLOR_SEQ = "\033[1;%dm"
BOLD_SEQ = "\033[1m"
COLORS = {'WARNING': YELLOW,
'INFO': GREEN,
'DEBUG': BLUE,
'CRITICAL': WHITE,
'ERROR': RED}
class ColoredFormatter(logging.Formatter):
def __init__(self, msg, use_color=True):
logging.Formatter.__init__(self, msg)
self.use_color = use_color
def format(self, record):
levelname = record.levelname
if self.use_color and levelname in COLORS:
levelname_color = COLOR_SEQ % (30 + COLORS[levelname]) \
+ levelname + RESET_SEQ
record.levelname = levelname_color
return logging.Formatter.format(self, record)
#def get_logger(module_name, level='INFO', to_file=False):
#def get_logger(module_name, level='DEBUG', to_file=True):
def get_logger(module_name, level='DEBUG', to_file=False):
"""
Prepare and return log object. Standard formatting is used for all logs.
Arguments:
@module_name - String name for Logger object.
@level - Log level (as string), one of DEBUG, INFO, WARN, ERROR and
CRITICAL.
@to_file - If True, additionally stores full log in file inside
.pygtktalog config directory and to stderr, otherwise log
is only redirected to stderr.
Returns: object of logging.Logger class
"""
path = os.path.join(os.path.expanduser("~"), ".pygtktalog", "app.log")
#path = "/dev/null"
log = logging.getLogger(module_name)
log.setLevel(LEVEL[level])
console_handler = logging.StreamHandler(sys.stderr)
console_formatter = ColoredFormatter("%(filename)s:%(lineno)s - "
"%(levelname)s - %(message)s")
console_handler.setFormatter(console_formatter)
log.addHandler(console_handler)
if to_file:
file_handler = logging.FileHandler(path)
file_formatter = logging.Formatter("%(asctime)s %(levelname)6s "
"%(filename)s: %(lineno)s - "
"%(message)s")
file_handler.setFormatter(file_formatter)
file_handler.setLevel(LEVEL[level])
log.addHandler(file_handler)
return log

22
pygtktalog/misc.py Normal file
View File

@@ -0,0 +1,22 @@
"""
Project: pyGTKtalog
Description: Misc functions used more than once in src
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-05
"""
def float_to_string(float_length):
"""
Parse float digit into time string
Arguments:
@number - digit to be converted into time.
Returns HH:MM:SS formatted string
"""
hour = int(float_length / 3600)
float_length -= hour*3600
minutes = int(float_length / 60)
float_length -= minutes * 60
sec = int(float_length)
return "%02d:%02d:%02d" % (hour, minutes, sec)

View File

View File

@@ -0,0 +1,31 @@
"""
Project: pyGTKtalog
Description: Model(s) for details part of the application
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-09
"""
import gtk
import gobject
from gtkmvc import Model
class DetailsModel(Model):
"""
Main model for application.
It is responsible for communicate with database objects and I/O
operations.
"""
exif = gtk.ListStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_UINT64,
gobject.TYPE_STRING,
gobject.TYPE_INT,
str)
__observables__ = ['exif']

107
pygtktalog/models/discs.py Normal file
View File

@@ -0,0 +1,107 @@
"""
Project: pyGTKtalog
Description: Model for discs representation
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import gtk
import gobject
from gtkmvc import Model
from pygtktalog.dbobjects import File
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
LOG = get_logger("discs model")
class DiscsModel(Model):
"""
Model for discs representation.
"""
currentdir = None
__observables__ = ("currentdir",)
def __init__(self):
"""
Initialization. Make some nice defaults.
"""
Model.__init__(self)
self.discs = gtk.TreeStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
str)
self.files_model = None
def clear(self):
"""
Make TreeStore empty
"""
self.discs.clear()
def refresh(self, session=Session()):
"""
Read objects from database, fill TreeStore model with discs
information
Arguments:
@session current sqlalchemy.orm.session.Session object
"""
LOG.debug("session obj: %s" % str(session))
dirs = session.query(File).filter(File.type == 1)
dirs = dirs.order_by(File.filename).all()
def get_children(parent_id=1, iterator=None):
"""
Get all children of the selected parent.
Arguments:
@parent_id - integer with id of the parent (from db)
@iterator - gtk.TreeIter, which points to a path inside model
"""
for fileob in dirs:
if fileob.parent_id == parent_id:
myiter = self.discs.insert_before(iterator, None)
self.discs.set_value(myiter, 0, fileob)
self.discs.set_value(myiter, 1, fileob.filename)
if iterator is None:
self.discs.set_value(myiter, 2, gtk.STOCK_CDROM)
else:
self.discs.set_value(myiter, 2, gtk.STOCK_DIRECTORY)
get_children(fileob.id, myiter)
return
get_children()
return True
def find_path(self, obj):
"""
Return path of specified File object (which should be the first one)
"""
path = None
gtkiter = self.discs.get_iter_first()
def get_children(iterator):
"""
Iterate through entire TreeModel, and return path for specified in
outter scope File object
"""
if self.discs.get_value(iterator, 0) == obj:
return self.discs.get_path(iterator)
if self.discs.iter_has_child(iterator):
path = get_children(self.discs.iter_children(iterator))
if path:
return path
iterator = self.discs.iter_next(iterator)
if iterator is None:
return None
return get_children(iterator)
path = get_children(gtkiter)
LOG.debug("found path for object '%s': %s" % (str(obj), str(path)))
return path

View File

@@ -0,0 +1,79 @@
"""
Project: pyGTKtalog
Description: Model for files representation
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-12
"""
import gtk
import gobject
from gtkmvc import Model
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
LOG = get_logger("files model")
class FilesModel(Model):
"""
Model for files representation
"""
def __init__(self):
"""
Initialization. Make some nice defaults.
"""
Model.__init__(self)
self.files = gtk.ListStore(gobject.TYPE_PYOBJECT,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_STRING,
gobject.TYPE_UINT64,
gobject.TYPE_STRING,
gobject.TYPE_INT,
str)
self.discs_model = None
def clear(self):
"""
Cleanup ListStore model
"""
self.files.clear()
def refresh(self, fileob):
"""
Update files ListStore
Arguments:
fileob - File object
"""
LOG.info("found %d files for File object: %s" % (len(fileob.children),
str(fileob)))
self.files.clear()
for child in fileob.children:
myiter = self.files.insert_before(None, None)
self.files.set_value(myiter, 0, child)
self.files.set_value(myiter, 1, child.parent_id \
if child.parent_id != 1 else None)
self.files.set_value(myiter, 2, child.filename)
self.files.set_value(myiter, 3, child.filepath)
self.files.set_value(myiter, 4, child.size)
self.files.set_value(myiter, 5, child.date)
self.files.set_value(myiter, 6, 1)
self.files.set_value(myiter, 7, gtk.STOCK_DIRECTORY \
if child.type == 1 else gtk.STOCK_FILE)
def get_value(self, row=None, fiter=None, column=0):
"""
TODO:
"""
if row:
fiter = self.files.get_iter(row)
if not fiter:
LOG.error("ERROR: there is no way to determine gtk_iter object!"
" Please specify valid row or gtk_iter!")
return None
return self.files.get_value(fiter, column)

289
pygtktalog/models/main.py Normal file
View File

@@ -0,0 +1,289 @@
"""
Project: pyGTKtalog
Description: Model for main application
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import os
import bz2
from string import printable
from tempfile import mkstemp
import gtk
import gobject
from gtkmvc import ModelMT
from sqlalchemy import create_engine
from pygtktalog.dbobjects import File, Exif, Group, Gthumb
from pygtktalog.dbobjects import Image, Tag, Thumbnail
from pygtktalog.dbcommon import connect, Meta, Session
from pygtktalog.logger import get_logger
from pygtktalog.models.details import DetailsModel
from pygtktalog.models.discs import DiscsModel
from pygtktalog.models.files import FilesModel
LOG = get_logger("main model")
class MainModel(ModelMT):
"""
Main model for application.
It is responsible for communicate with database objects and I/O
operations.
"""
status_bar_message = _("Idle")
current_disc = None
__observables__ = ("status_bar_message", "current_disc")
def __init__(self, filename=None):
"""
Initialization. Make some nice defaults.
Arguments:
filename - String that indicates optionally compressed with bzip2
file containing sqlite3 database.
"""
ModelMT.__init__(self)
# Opened/saved database location in filesystem. Could be compressed.
self.cat_fname = filename
# Temporary (usually in /tmp) working database.
self.tmp_filename = None
self.config = {}
# SQLAlchemy session object for internal use
self._session = None
# Flag indicates, that db was compressed
# TODO: make it depend on configuration
self.compressed = False
self.db_unsaved = None
self.discs = DiscsModel()
self.files = FilesModel()
if self.cat_fname:
self.open(self.cat_fname)
def open(self, filename):
"""
Open catalog file and read db
Arguments:
@filename - see MainModel __init__ docstring.
Returns: Bool - true for success, false otherwise.
"""
LOG.debug("filename: '%s'", filename)
self.unsaved_project = False
if not os.path.exists(filename):
LOG.warn("db file '%s' doesn't exist.", filename)
return False
if not os.path.isfile(filename):
LOG.warn("db file '%s' is not a regular file.", filename)
return False
self.cat_fname = filename
if self._open_or_decompress():
return self.discs.refresh(self._session)
else:
return False
def save(self, filename=None):
"""
Save tared directory at given catalog fielname
Arguments:
@filename - see MainModel __init__ docstring.
Returns: tuple:
Bool - true for success, false otherwise.
String or None - error message
"""
if not filename and not self.cat_fname:
LOG.debug("no filename detected!")
return False, None
if filename:
if not '.sqlite' in filename:
filename += '.sqlite'
else:
filename = filename[:filename.rindex('.sqlite')] + '.sqlite'
if 'compress' in self.config and self.config['compress']:
filename += '.bz2'
self.cat_fname = filename
val, err = self._compress_and_save()
if not val:
self.cat_fname = None
return val, err
def new(self):
"""
Create new catalog
"""
self.cleanup()
self._create_temp_db_file()
self._create_schema()
self.discs.clear()
self.files.clear()
self.db_unsaved = False
def cleanup(self):
"""
Remove temporary directory tree from filesystem
"""
if self._session:
self._session.close()
self._session = None
if self.tmp_filename is None:
return
try:
os.unlink(self.tmp_filename)
except OSError:
LOG.error("temporary db file doesn't exists!")
except TypeError:
# TODO: file does not exist - create? print error message?
LOG.error("temporary db file doesn't exists!")
else:
LOG.debug("file %s succesfully deleted", self.tmp_filename)
def _examine_file(self, filename):
"""
Try to recognize file.
Arguments:
@filename - String with full path to file to examine
Returns: 'sql', 'bz2' or None for file sqlite, compressed with bzip2
or other respectively
"""
try:
head_of_file = open(filename).read(15)
LOG.debug("head of file: %s", filter(lambda x: x in printable,
head_of_file))
except IOError:
LOG.exception("Error opening file '%s'!", filename)
self.cleanup()
self.cat_fname = None
self.tmp_filename = None
return None
if head_of_file == "SQLite format 3":
LOG.debug("File format: uncompressed sqlite")
return 'sql'
elif head_of_file[0:10] == "BZh91AY&SY":
LOG.debug("File format: bzip2")
return 'bz2'
else:
return None
def _open_or_decompress(self):
"""
Try to open file, which user thinks is our catalog file.
Returns: Bool - true for success, false otherwise
"""
filename = os.path.abspath(self.cat_fname)
LOG.info("catalog file: %s", filename)
if self._session:
self.cleanup()
self._create_temp_db_file()
LOG.debug("tmp database file: %s", str(self.tmp_filename))
examine = self._examine_file(filename)
if examine == "sql":
db_tmp = open(self.tmp_filename, "wb")
db_tmp.write(open(filename).read())
db_tmp.close()
elif examine == "bz2":
open_file = bz2.BZ2File(filename)
try:
db_tmp = open(self.tmp_filename, "w")
db_tmp.write(open_file.read())
db_tmp.close()
open_file.close()
except IOError:
self.cleanup()
self.cat_fname = None
self.internal_dirname = None
LOG.exception("File is probably not a bz2!")
return False
if self._examine_file(self.tmp_filename) is not 'sql':
LOG.error("Error opening file '%s' - not a catalog file!",
self.tmp_filename)
self.cleanup()
self.cat_fname = None
self.tmp_filename = None
return False
else:
LOG.error("Error opening file '%s' - not a catalog file!",
self.tmp_filename)
self.cleanup()
self.cat_fname = None
self.internal_dirname = None
return False
connect(os.path.abspath(self.tmp_filename))
self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
return True
def _create_temp_db_file(self):
"""
Create new DB file, populate schema.
"""
fd, self.tmp_filename = mkstemp()
LOG.debug("new db filename: %s" % self.tmp_filename)
# close file descriptor, otherwise it can be source of app crash!
# http://www.logilab.org/blogentry/17873
os.close(fd)
def _create_schema(self):
"""
"""
self._session = Session()
LOG.debug("session obj: %s" % str(self._session))
connect(os.path.abspath(self.tmp_filename))
root = File()
root.id = 1
root.filename = 'root'
root.size = 0
root.source = 0
root.type = 0
root.parent_id = 1
self._session.add(root)
self._session.commit()
def _compress_and_save(self):
"""
Create (and optionaly compress) tar archive from working directory and
write it to specified file.
"""
# flush all changes
self._session.commit()
try:
if 'compress' in self.config and self.config['compress']:
output_file = bz2.BZ2File(self.cat_fname, "w")
else:
output_file = open(self.cat_fname, "w")
LOG.debug("save (and optionally compress) successed")
except IOError, (errno, strerror):
LOG.error("error saving or compressing file", errno, strerror)
return False, strerror
dbpath = open(self.tmp_filename)
output_file.write(dbpath.read())
dbpath.close()
output_file.close()
self.db_unsaved = False
return True, None

25
pygtktalog/pygtkutils.py Normal file
View File

@@ -0,0 +1,25 @@
"""
Project: pyGTKtalog
Description: pyGTK common utility functions
Type: tility
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-11-07 13:30:37
"""
def get_tv_item_under_cursor(treeview):
"""
Get item (most probably id of the row) form tree view under cursor.
Arguments:
@treeview - gtk.TreeView
Returns:
Item in first column of TreeModel, which TreeView is connected with,
None in other cases
"""
path, column = treeview.get_cursor()
if path and column:
model = treeview.get_model()
tm_iter = model.get_iter(path)
item_id = model.get_value(tm_iter, 0)
return item_id
return None

665
pygtktalog/scan.py Normal file
View File

@@ -0,0 +1,665 @@
"""
Project: pyGTKtalog
Description: Filesystem scan and file automation layer
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-03-27
"""
import os
import sys
import re
from datetime import datetime
import mimetypes
from pygtktalog.dbobjects import File, Image, TYPE
from pygtktalog.thumbnail import ThumbCreator
from pygtktalog.dbcommon import Session
from pygtktalog.logger import get_logger
from pygtktalog.video import Video
LOG = get_logger(__name__)
PAT = re.compile("(\[[^\]]*\]"
".*\(\d\d\d\d\))"
"\s[^\[]*\[.{8}\]"
".[a-zA-Z0-9]*$")
#PAT = re.compile(r'(?P<group>\[[^\]]*\]\s)?'
# r'(?P<title>.*)\s'
# r'(?P<year>\(\d{4}\))\s'
# r'(?P<kind>.*)'
# r'(?P<checksum>\[[A-Z0-9]{8}\])'
# r'\.(?P<extension>(avi|asf|mpeg|mpg|mp4|ogm|ogv|mkv|mov|wmv'
# r'|rm|rmvb|flv|jpg|png|gif|nfo))\.?(conf)?$')
class NoAccessError(Exception):
pass
class Scan(object):
"""
Retrieve and identify all files recursively on given path
"""
def __init__(self, path):
"""
Initialize
@Arguments:
@path - string with initial root directory to scan
"""
self.abort = False
self.path = path.rstrip(os.path.sep)
self._files = []
self._existing_files = [] # for re-use purpose in adding
self._existing_branch = [] # for branch storage, mainly for updating
self._session = Session()
self.files_count = self._get_files_count()
self.current_count = 0
def add_files(self):
"""
Returns list, which contain object, modification date and file
size.
"""
self._files = []
self._existing_branch = []
LOG.debug("given path: %s" % self.path)
# See, if file exists. If not it would raise OSError exception
os.stat(self.path)
if not os.access(self.path, os.R_OK | os.X_OK) \
or not os.path.isdir(self.path):
raise NoAccessError("Access to %s is forbidden" % self.path)
directory = os.path.basename(self.path)
path = os.path.dirname(self.path)
if not self._recursive(None, directory, path, 0):
return None
# add only first item from _files, because it is a root of the other,
# so other will be automatically added aswell.
self._session.add(self._files[0])
self._session.commit()
return self._files
def update_files(self, node_id):
"""
Updtate DB contents of provided node.
"""
self.current_count = 0
old_node = self._session.query(File).get(node_id)
if old_node is None:
LOG.warning("No such object in db: %s", node_id)
return
parent = old_node.parent
self._files = []
self._existing_branch = old_node.get_all_children()
self._existing_branch.insert(0, old_node)
# Break the chain of parent-children relations
for fobj in self._existing_branch:
fobj.parent = None
update_path = os.path.join(old_node.filepath, old_node.filename)
# refresh objects
self._get_all_files()
LOG.debug("path for update: %s" % update_path)
# See, if file exists. If not it would raise OSError exception
os.stat(update_path)
if not os.access(update_path, os.R_OK | os.X_OK) \
or not os.path.isdir(update_path):
raise NoAccessError("Access to %s is forbidden" % update_path)
directory = os.path.basename(update_path)
path = os.path.dirname(update_path)
if not self._recursive(parent, directory, path, 0):
return None
# update branch
#self._session.merge(self._files[0])
self._session.query(File).filter(File.parent==None).delete()
self._session.commit()
return self._files
def _get_dirsize(self, path):
"""
Returns sum of all files under specified path (also in subdirs)
"""
size = 0
for root, dirs, files in os.walk(path):
for fname in files:
try:
size += os.stat(os.path.join(root, fname)).st_size
except OSError:
LOG.warning("Cannot access file "
"%s" % os.path.join(root, fname))
return size
def _gather_information(self, fobj):
"""
Try to guess type and gather information about File object if possible
"""
mimedict = {'audio': self._audio,
'video': self._video,
'image': self._image}
fp = os.path.join(fobj.filepath.encode(sys.getfilesystemencoding()),
fobj.filename.encode(sys.getfilesystemencoding()))
mimeinfo = mimetypes.guess_type(fp)
if mimeinfo[0] and mimeinfo[0].split("/")[0] in mimedict.keys():
mimedict[mimeinfo[0].split("/")[0]](fobj, fp)
else:
LOG.debug("Filetype not supported " + str(mimeinfo) + " " + fp)
pass
def _audio(self, fobj, filepath):
return
def _image(self, fobj, filepath):
#Thumbnail(filepath, fobj)
return
def _video(self, fobj, filepath):
"""
Make captures for a movie. Save it under uniq name.
"""
result = PAT.search(fobj.filename)
if result:
self._check_related(fobj, result.groups()[0])
vid = Video(filepath)
fobj.description = vid.get_formatted_tags()
preview_fn = vid.capture()
if preview_fn:
Image(preview_fn, fobj)
def _check_related(self, fobj, pattern):
"""
Try to search for related files which belongs to specified File
object and pattern. If found, additional objects are created.
"""
for filen in os.listdir(fobj.filepath):
if pattern in filen and \
os.path.splitext(filen)[1] in (".jpg", ".png", ".gif"):
full_fname = os.path.join(fobj.filepath, filen)
LOG.debug('found cover file: %s' % full_fname)
Image(full_fname, fobj)
if not fobj.thumbnail:
fthumb = ThumbCreator(full_fname).generate()
fobj.thumbnail = fthumb.getvalue()
fthumb.close()
def _name_matcher(self, fpath, fname, media=False):
"""
Try to match special pattern to filename which may be looks like this:
[aXXo] Batman (1989) [D3ADBEEF].avi
[aXXo] Batman (1989) [D3ADBEEF].avi.conf
[aXXo] Batman (1989) cover [BEEFD00D].jpg
[aXXo] Batman (1989) cover2 [FEEDD00D].jpg
[aXXo] Batman (1989) trailer [B00B1337].avi
or
Batman (1989) [D3ADBEEF].avi (and so on)
For media=False it will return True for filename, that matches
pattern, and there are at least one corresponding media files (avi,
mpg, mov and so on) _in case the filename differs from media_. This is
usfull for not storing covers, nfo, conf files in the db.
For kind == 2 it will return all images and other files that should be
gather due to video file examinig as a dict of list (conf, nfo and
images).
"""
# TODO: dokonczyc to na podstawie tego cudowanego patternu u gory.
return
def _get_all_files(self):
self._existing_files = self._session.query(File).all()
def _mk_file(self, fname, path, parent, ftype=TYPE['file']):
"""
Create and return File object
"""
fullpath = os.path.join(path, fname)
fname = fname.decode(sys.getfilesystemencoding())
path = path.decode(sys.getfilesystemencoding())
if ftype == TYPE['link']:
fname = fname + " -> " + os.readlink(fullpath)
fob = {'filename': fname,
'path': path,
'ftype': ftype}
try:
fob['date'] = datetime.fromtimestamp(os.stat(fullpath).st_mtime)
fob['size'] = os.stat(fullpath).st_size
except OSError:
# in case of dead softlink, we will have no time and size
fob['date'] = None
fob['size'] = 0
fobj = self._get_old_file(fob, ftype)
if fobj:
LOG.debug("found existing file in db: %s" % str(fobj))
fobj.size = fob['size'] # TODO: update whole tree sizes (for directories/discs)
fobj.filepath = fob['path']
fobj.type = fob['ftype']
else:
fobj = File(**fob)
if parent is None:
fobj.parent_id = 1
else:
fobj.parent = parent
self._files.append(fobj)
return fobj
def _recursive(self, parent, fname, path, size):
"""
Do the walk through the file system
@Arguments:
@parent - directory File object which is parent for the current
scope
@fname - string that hold filename
@path - full path for further scanning
@size - size of the object
"""
if self.abort:
return False
fullpath = os.path.join(path, fname)
parent = self._mk_file(fname, path, parent, TYPE['dir'])
parent.size = self._get_dirsize(fullpath)
parent.type = TYPE['dir']
root, dirs, files = os.walk(fullpath).next()
for fname in files:
fpath = os.path.join(root, fname)
self.current_count += 1
LOG.debug("Processing %s [%s/%s]", fname, self.current_count,
self.files_count)
result = PAT.search(fname)
test_ = False
if result and os.path.splitext(fpath)[1] in ('.jpg', '.gif',
'.png'):
newpat = result.groups()[0]
matching_files = []
for fn_ in os.listdir(root):
if newpat in fn_:
matching_files.append(fn_)
if len(matching_files) > 1:
LOG.debug('found cover "%s" in group: %s, skipping', fname,
str(matching_files))
test_ = True
if test_:
continue
if os.path.islink(fpath):
fob = self._mk_file(fname, root, parent, TYPE['link'])
else:
fob = self._mk_file(fname, root, parent)
existing_obj = self._object_exists(fob)
if existing_obj:
fob.tags = existing_obj.tags
fob.thumbnail = existing_obj.thumbnail
fob.images = [img.get_copy() \
for img in existing_obj.images] # TODO: many-to-many?
else:
LOG.debug("gather information for %s",
os.path.join(root, fname))
self._gather_information(fob)
size += fob.size
if fob not in self._existing_files:
self._existing_files.append(fob)
for dirname in dirs:
dirpath = os.path.join(root, dirname)
if not os.access(dirpath, os.R_OK|os.X_OK):
LOG.info("Cannot access directory %s" % dirpath)
continue
if os.path.islink(dirpath):
fob = self._mk_file(dirname, root, parent, TYPE['link'])
else:
LOG.debug("going into %s" % os.path.join(root, dirname))
self._recursive(parent, dirname, fullpath, size)
LOG.debug("size of items: %s" % parent.size)
return True
def _get_old_file(self, fdict, ftype):
"""
Search for object with provided data in dictionary in stored branch
(which is updating). Return such object on success, remove it from
list.
"""
for index, obj in enumerate(self._existing_branch):
if ftype == TYPE['link'] and fdict['filename'] == obj.filename:
return self._existing_branch.pop(index)
elif fdict['filename'] == obj.filename and \
fdict['date'] == obj.date and \
ftype == TYPE['file'] and \
fdict['size'] in (obj.size, 0):
obj = self._existing_branch.pop(index)
obj.size = fdict['size']
return obj
elif fdict['filename'] == obj.filename:
obj = self._existing_branch.pop(index)
obj.size = fdict['date']
return obj
return False
def _object_exists(self, fobj):
"""
Perform check if current File object already exists in collection. If
so, return first matching one, None otherwise.
"""
for efobj in self._existing_files:
if efobj.size == fobj.size \
and efobj.type == fobj.type \
and efobj.date == fobj.date \
and efobj.filename == fobj.filename:
return efobj
return None
def _get_files_count(self):
count = 0
for root, dirs, files in os.walk(self.path):
count += len(files)
LOG.debug("count of files: %s", count)
return count
class asdScan(object):
"""
Retrieve and identify all files recursively on given path
"""
def __init__(self, path, tree_model):
self.path = path
self.abort = False
self.label = None
self.DIR = None
self.source = None
def scan(self):
"""
scan content of the given path
"""
self.busy = True
# count files in directory tree
LOG.debug("Calculating number of files in directory tree...")
step = 0
try:
for root, dirs, files in os.walk(self.path):
step += len(files)
except Exception, ex:
LOG.warning("exception on file %s: %s: %s" \
% (self.path, ex.__class__.__name__, str(ex)))
pass
step = 1 / float(step or 1)
self.count = 0
def _recurse(parent_id, name, path, date, size, filetype,
discs_tree_iter=None):
"""recursive scans given path"""
if self.abort:
return -1
_size = size
if parent_id == 1:
sql = """INSERT INTO
files(parent_id, filename, filepath, date,
size, type, source)
VALUES(?,?,?,?,?,?,?)"""
print(sql, (parent_id, name, path, date, size,
filetype, self.source))
else:
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (parent_id, name, path,
date, size, filetype))
sql = """SELECT seq FROM sqlite_sequence WHERE name='files'"""
print(sql)
currentid = None #db_cursor.fetchone()[0]
try:
root, dirs, files = os.walk(path).next()
except:
LOG.warning("Cannot access ", path)
return 0
#############
# directories
for i in dirs:
j = i #j = self.__decode_filename(i)
current_dir = os.path.join(root, i)
try:
st = os.stat(current_dir)
st_mtime = st.st_mtime
except OSError:
st_mtime = 0
# do NOT follow symbolic links
if os.path.islink(current_dir):
l = self.__decode_filename(os.readlink(current_dir))
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j + " -> " + l,
current_dir, st_mtime, 0,
self.LIN))
dirsize = 0
else:
myit = None
dirsize = _recurse(currentid, j, current_dir,
st_mtime, 0, self.DIR, myit)
if dirsize == -1:
break
else:
_size = _size + dirsize
########
# files:
for i in files:
if self.abort:
break
self.count = self.count + 1
current_file = os.path.join(root, i)
try:
st = os.stat(current_file)
st_mtime = st.st_mtime
st_size = st.st_size
except OSError:
st_mtime = 0
st_size = 0
_size = _size + st_size
j = i #self.__decode_filename(i)
# do NOT follow symbolic links
if os.path.islink(current_file):
l = self.__decode_filename(os.readlink(current_file))
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j + " -> " + l,
current_file, st_mtime, 0,
self.LIN))
else:
sql = """INSERT INTO
files(parent_id, filename, filepath, date, size, type)
VALUES(?,?,?,?,?,?)"""
print(sql, (currentid, j, current_file,
st_mtime, st_size, self.FIL))
if self.count % 32 == 0:
update = True
else:
update = False
###########################
# fetch details about files
if self.config.confd['retrive']:
update = True
exif = None
sql = """SELECT seq FROM sqlite_sequence
WHERE name='files'"""
print(sql)
fileid = 1 # dummy!
ext = i.split('.')[-1].lower()
# Video
if ext in self.MOV:
v = Video(current_file)
cfn = v.capture()
img = Img(cfn, self.image_path)
th = img.save()
if th:
sql = """INSERT INTO
thumbnails(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th + "_t"))
sql = """INSERT INTO images(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th))
os.unlink(cfn)
# Images - thumbnails and exif data
if self.config.confd['thumbs'] and ext in self.IMG:
thumb = Thumbnail(current_file, self.image_path)
th, exif = thumb.save()
if th:
sql = """INSERT INTO
thumbnails(file_id, filename)
VALUES(?, ?)"""
print(sql, (fileid, th))
# exif - store data in exif table
jpg = ['jpg', 'jpeg']
if self.config.confd['exif'] and ext in jpg:
p = None
if self.config.confd['thumbs'] and exif:
p = ParseExif(exif_dict=exif)
else:
p = ParseExif(exif_file=current_file)
if not p.exif_dict:
p = None
if p:
p = p.parse()
p = list(p)
p.insert(0, fileid)
sql = """INSERT INTO exif (file_id,
camera,
date,
aperture,
exposure_program,
exposure_bias,
iso,
focal_length,
subject_distance,
metering_mode,
flash,
light_source,
resolution,
orientation)
values(?,?,?,?,?,?,?,?,?,?,?,?,?,?)"""
print(sql, (tuple(p)))
# gthumb - save comments from gThumb program
if self.config.confd['gthumb']:
gt = GthumbCommentParser(root, i)
cmnts = gt.parse()
if cmnts:
sql = """insert into gthumb(file_id,
note,
place,
date)
values(?,?,?,?)"""
print(sql, (fileid,
cmnts['note'],
cmnts['place'],
cmnts['date']))
if 'keywords' in cmnts:
# TODO: add gthumb keywords to tags
pass
# Extensions - user defined actions
if ext in self.config.confd['extensions'].keys():
cmd = self.config.confd['extensions'][ext]
arg = current_file.replace('"', '\\"')
output = os.popen(cmd % arg).readlines()
desc = ''
for line in output:
desc += line
sql = """UPDATE files SET description=?
WHERE id=?"""
print(sql, (desc, fileid))
### end of scan
if update:
self.statusmsg = "Scannig: %s" % current_file
self.progress = step * self.count
sql = """UPDATE files SET size=? WHERE id=?"""
print(sql, (_size, currentid))
if self.abort:
return -1
else:
return _size
if _recurse(1, self.label, self.path, 0, 0, self.DIR) == -1:
LOG.debug("interrupted self.abort = True")
else:
LOG.debug("recursive goes without interrupt")
if self.currentid:
LOG.debug("removing old branch")
self.statusmsg = "Removing old branch..."
self.currentid = None
self.busy = False
# refresh discs tree
self.statusmsg = "Idle"
self.progress = 0
self.abort = False

106
pygtktalog/thumbnail.py Normal file
View File

@@ -0,0 +1,106 @@
"""
Project: pyGTKtalog
Description: Create thumbnail for sepcified image by its filename
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-05-15
"""
import os
import sys
from cStringIO import StringIO
from tempfile import mkstemp
import Image
from pygtktalog.logger import get_logger
from pygtktalog import EXIF
LOG = get_logger(__name__)
class ThumbCreator(object):
"""
Class for generate/extract thumbnail from image file
"""
def __init__(self, filename):
self.thumb_x = 160
self.thumb_y = 160
self.filename = filename.decode(sys.getfilesystemencoding())
self.fobj = StringIO()
def generate(self):
"""
Generate and return file-like object with thumbnail
"""
exif = {}
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
3: Image.ROTATE_180, # Rotated 180
4: Image.FLIP_TOP_BOTTOM, # Mirrored vertical
5: Image.ROTATE_90, # Mirrored horizontal then
# rotated 90 CCW
6: Image.ROTATE_270, # Rotated 90 CW
7: Image.ROTATE_270, # Mirrored horizontal then
# rotated 90 CW
8: Image.ROTATE_90} # Rotated 90 CCW
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
exif = self._get_exif()
file_desc, thumb_fn = mkstemp(suffix=".jpg")
os.close(file_desc)
if 'JPEGThumbnail' not in exif:
LOG.debug("no exif thumb for file %s; creating." % self.filename)
thumb = self._scale_image()
if thumb:
thumb.save(self.fobj, "JPEG")
else:
LOG.debug("exif thumb for filename %s" % self.filename)
exif_thumbnail = exif['JPEGThumbnail']
self.fobj.write(exif_thumbnail)
self.fobj.seek(0)
if 'Image Orientation' in exif:
orient = exif['Image Orientation'].values[0]
if orient > 1 and orient in orientations:
thumb_image = Image.open(self.fobj)
tmp_thumb_img = thumb_image.transpose(orientations[orient])
if orient in flips:
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
self.fobj.seek(0)
self.fobj.truncate()
tmp_thumb_img.save(self.fobj, 'JPEG')
return self.fobj
def _get_exif(self):
"""
Get exif (if available), return as a dict
"""
image_file = open(self.filename, 'rb')
try:
exif = EXIF.process_file(image_file)
except Exception:
exif = {}
LOG.info("Exif crashed on '%s'." % self.filename)
finally:
image_file.close()
return exif
def _scale_image(self):
"""
Create thumbnail. returns image object or None
"""
try:
image_thumb = Image.open(self.filename).convert('RGB')
except:
return None
it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y:
image_thumb.thumbnail((self.thumb_x, self.thumb_y),
Image.ANTIALIAS)
return image_thumb

281
pygtktalog/video.py Normal file
View File

@@ -0,0 +1,281 @@
"""
Project: pyGTKtalog
Description: Gather video file information, make "screenshot" with content
of the movie file. Uses external tools like mplayer.
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-04
"""
import os
import shutil
from tempfile import mkdtemp, mkstemp
import math
import Image
from pygtktalog.misc import float_to_string
from pygtktalog.logger import get_logger
LOG = get_logger("Video")
class Video(object):
"""Class for retrive midentify script output and put it in dict.
Usually there is no need for such a detailed movie/clip information.
Midentify script belongs to mplayer package.
"""
def __init__(self, filename, out_width=1024):
"""
Init class instance.
Arguments:
@filename - Filename of a video file (required).
@out_width - width of final image to be scaled to.
"""
self.filename = filename
self.out_width = out_width
self.tags = {}
output = self._get_movie_info()
attrs = {'ID_VIDEO_WIDTH': ['width', int],
'ID_VIDEO_HEIGHT': ['height', int],
# length is in seconds
'ID_LENGTH': ['length', lambda x: int(x.split(".")[0])],
'ID_START_TIME': ['start', self._get_start_pos],
'ID_DEMUXER': ['container', self._return_lower],
'ID_VIDEO_FORMAT': ['video_format', self._return_lower],
'ID_VIDEO_CODEC': ['video_codec', self._return_lower],
'ID_AUDIO_CODEC': ['audio_codec', self._return_lower],
'ID_AUDIO_FORMAT': ['audio_format', self._return_lower],
'ID_AUDIO_NCH': ['audio_no_channels', int]}
# TODO: what about audio/subtitle language/existence?
for key in output:
if key in attrs:
self.tags[attrs[key][0]] = attrs[key][1](output[key])
if 'length' in self.tags and self.tags['length'] > 0:
start = self.tags.get('start', 0)
length = self.tags['length'] - start
hours = length / 3600
seconds = length - hours * 3600
minutes = seconds / 60
seconds -= minutes * 60
length_str = "%02d:%02d:%02d" % (hours, minutes, seconds)
self.tags['duration'] = length_str
def capture(self):
"""
Extract images for given video filename and montage it into one, big
picture, similar to output from Windows Media Player thing, but without
captions and time (who need it anyway?).
Returns: image filename or None
NOTE: You should remove returned file manually, or move it in some
other place, otherwise it stays in filesystem.
"""
if not ('length' in self.tags and 'width' in self.tags):
# no length or width
return None
if not (self.tags['length'] > 0 and self.tags['width'] > 0):
# zero length or wight
return None
# Calculate number of pictures. Base is equivalent 72 pictures for
# 1:30:00 movie length
scale = int(10 * math.log(self.tags['length'], math.e) - 11)
if scale < 1:
return None
no_pictures = self.tags['length'] / scale
if no_pictures > 8:
no_pictures = (no_pictures / 8) * 8 # only multiple of 8, please.
else:
# for really short movies
no_pictures = 4
tempdir = mkdtemp()
file_desc, image_fn = mkstemp(suffix=".jpg")
os.close(file_desc)
self._make_captures(tempdir, no_pictures)
self._make_montage(tempdir, image_fn, no_pictures)
shutil.rmtree(tempdir)
return image_fn
def get_formatted_tags(self):
"""
Return formatted tags as a string
"""
out_tags = u''
if 'container' in self.tags:
out_tags += u"Container: %s\n" % self.tags['container']
if 'width' in self.tags and 'height' in self.tags:
out_tags += u"Resolution: %sx%s\n" % (self.tags['width'],
self.tags['height'])
if 'duration' in self.tags:
out_tags += u"Duration: %s\n" % self.tags['duration']
if 'video_codec' in self.tags:
out_tags += "Video codec: %s\n" % self.tags['video_codec']
if 'video_format' in self.tags:
out_tags += "Video format: %s\n" % self.tags['video_format']
if 'audio_codec' in self.tags:
out_tags += "Audio codec: %s\n" % self.tags['audio_codec']
if 'audio_format' in self.tags:
out_tags += "Audio format: %s\n" % self.tags['audio_format']
if 'audio_no_channels' in self.tags:
out_tags += "Audio channels: %s\n" % self.tags['audio_no_channels']
return out_tags
def _get_movie_info(self):
"""
Gather movie file information with midentify shell command.
Returns: dict of command output. Each dict element represents pairs:
variable=value, for example output from midentify will be:
ID_VIDEO_ID=0
ID_AUDIO_ID=1
....
ID_AUDIO_CODEC=mp3
ID_EXIT=EOF
so method returns dict:
{'ID_VIDEO_ID': '0',
'ID_AUDIO_ID': 1,
....
'ID_AUDIO_CODEC': 'mp3',
'ID_EXIT': 'EOF'}
"""
output = os.popen('midentify "%s"' % self.filename).readlines()
return_dict = {}
for line in output:
line = line.strip()
key = line.split('=')
if len(key) > 1:
return_dict[key[0]] = line.replace("%s=" % key[0], "")
return return_dict
def _make_captures(self, directory, no_pictures):
"""
Make screens with mplayer into given directory
Arguments:
@directory - full output directory name
@no_pictures - number of pictures to take
"""
step = float(self.tags['length'] / (no_pictures + 1))
current_time = 0
for dummy in range(1, no_pictures + 1):
current_time += step
time = float_to_string(current_time)
cmd = "mplayer \"%s\" -ao null -brightness 0 -hue 0 " \
"-saturation 0 -contrast 0 -vf-clr -vo jpeg:outdir=\"%s\" -ss %s" \
" -frames 1 2>/dev/null"
os.popen(cmd % (self.filename, directory, time)).readlines()
try:
shutil.move(os.path.join(directory, "00000001.jpg"),
os.path.join(directory, "picture_%s.jpg" % time))
except IOError, (errno, strerror):
LOG.error('error capturing file from movie "%s" at position '
'%s. Errors: %s, %s', self.filename, time, errno,
strerror)
def _make_montage(self, directory, image_fn, no_pictures):
"""
Generate one big image from screnshots and optionally resize it. Uses
PIL package to create output image.
Arguments:
@directory - source directory containing images
@image_fn - destination final image
@no_pictures - number of pictures
timeit result:
python /usr/lib/python2.6/timeit.py -n 1 -r 1 'from \
pygtktalog.video import Video; v = Video("/home/gryf/t/a.avi"); \
v.capture()'
1 loops, best of 1: 18.8 sec per loop
"""
row_length = 4
if no_pictures < 8:
row_length = 2
if not (self.tags['width'] * row_length) > self.out_width:
for i in [8, 6, 5]:
if (no_pictures % i) == 0 and \
(i * self.tags['width']) <= self.out_width:
row_length = i
break
coef = float(self.out_width - row_length - 1) / \
(self.tags['width'] * row_length)
if coef < 1:
dim = (int(self.tags['width'] * coef),
int(self.tags['height'] * coef))
else:
dim = int(self.tags['width']), int(self.tags['height'])
ifn_list = os.listdir(directory)
ifn_list.sort()
img_list = [Image.open(os.path.join(directory, fn)).resize(dim) \
for fn in ifn_list]
rows = no_pictures / row_length
cols = row_length
isize = (cols * dim[0] + cols + 1,
rows * dim[1] + rows + 1)
inew = Image.new('RGB', isize, (80, 80, 80))
for irow in range(no_pictures * row_length):
for icol in range(row_length):
left = 1 + icol * (dim[0] + 1)
right = left + dim[0]
upper = 1 + irow * (dim[1] + 1)
lower = upper + dim[1]
bbox = (left, upper, right, lower)
try:
img = img_list.pop(0)
except:
break
inew.paste(img, bbox)
inew.save(image_fn, 'JPEG')
def _return_lower(self, chain):
"""
Return lowercase version of provided string argument
Arguments:
@chain string to be lowered
Returns:
@string with lowered string
"""
return str(chain).lower()
def _get_start_pos(self, chain):
"""
Return integer for starting point of the movie
"""
try:
return int(chain.split(".")[0])
except:
return 0
def __str__(self):
str_out = ''
for key in self.tags:
str_out += "%20s: %s\n" % (key, self.tags[key])
return str_out

View File

View File

@@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Sat Aug 29 15:13:44 2009 -->
<glade-interface>
<widget class="GtkWindow" id="top_details">
<child>
<widget class="GtkNotebook" id="notebook_details">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkHBox" id="fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow4">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTextView" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="left_margin">2</property>
<property name="right_margin">2</property>
<property name="cursor_visible">False</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkViewport" id="thumb_box">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<child>
<widget class="GtkImage" id="thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xpad">3</property>
<property name="ypad">3</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">6</property>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="nb_fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">File info</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="img_container">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkIconView" id="images">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Double click to open image</property>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Images</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="exifinfo">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTreeView" id="exif_tree">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="rules_hint">True</property>
</widget>
</child>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">EXIF</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_discs">
<child>
<widget class="GtkTreeView" id="discs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_discs_button_press_event"/>
<signal name="cursor_changed" handler="on_discs_cursor_changed"/>
<signal name="row_activated" handler="on_discs_row_activated"/>
<signal name="key_release_event" handler="on_discs_key_release_event"/>
</widget>
</child>
</widget>
<widget class="GtkMenu" id="discs_popup">
<child>
<widget class="GtkMenuItem" id="expand_all">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Expand all nodes</property>
<property name="label" translatable="yes">_Expand all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_expand_all_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="collapse_all">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Collapse all nodes</property>
<property name="label" translatable="yes">_Collapse all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_collapse_all_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="update">
<property name="visible">True</property>
<property name="label" translatable="yes">_Update</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_update_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename">
<property name="visible">True</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete">
<property name="visible">True</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="statistics">
<property name="visible">True</property>
<property name="label" translatable="yes">_Statistics</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_statistics_activate"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -0,0 +1,122 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_files">
<child>
<widget class="GtkTreeView" id="files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_files_button_press_event"/>
<signal name="cursor_changed" handler="on_files_cursor_changed"/>
<signal name="row_activated" handler="on_files_row_activated"/>
<signal name="drag_data_get" handler="on_files_drag_data_get"/>
<signal name="key_release_event" handler="on_files_key_release_event"/>
</widget>
</child>
</widget>
<widget class="GtkMenu" id="files_popup">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkMenuItem" id="add_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Add tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_tag1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Remo_ve tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_tag_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Add _Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Re_move Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator1">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_image">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="has_tooltip">True</property>
<property name="tooltip" translatable="yes">Add images to file. If file have no thumbnail,
thumbnail from first image will be generated.</property>
<property name="label" translatable="yes">Add _Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_image">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Rem_ove All Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator2">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="edit">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_edit2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete3_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename2_activate"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--*- mode: xml -*-->
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.16 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="main">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="title" translatable="yes">pyGTKtalog</property>
@@ -11,6 +11,7 @@
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkMenuBar" id="mainMenubar">
<property name="visible">True</property>
@@ -23,8 +24,8 @@
<widget class="GtkMenu" id="file1_menu">
<child>
<widget class="GtkImageMenuItem" id="new1">
<property name="visible">True</property>
<property name="label">gtk-new</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_new_activate"/>
@@ -32,8 +33,8 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="open1">
<property name="visible">True</property>
<property name="label">gtk-open</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_open_activate"/>
@@ -41,8 +42,8 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="save1">
<property name="visible">True</property>
<property name="label">gtk-save</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_save_activate"/>
@@ -50,8 +51,8 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="save_as1">
<property name="visible">True</property>
<property name="label">gtk-save-as</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_save_as_activate"/>
@@ -62,6 +63,27 @@
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="import">
<property name="visible">True</property>
<property name="label" translatable="yes">Import</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_import_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="export">
<property name="visible">True</property>
<property name="label" translatable="yes">Export</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_export_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator13">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="recent_files1">
<property name="visible">True</property>
@@ -77,8 +99,8 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="quit1">
<property name="visible">True</property>
<property name="label">gtk-quit</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_quit_activate"/>
@@ -98,12 +120,12 @@
<widget class="GtkMenu" id="edit1_menu">
<child>
<widget class="GtkImageMenuItem" id="delete1">
<property name="visible">True</property>
<property name="label">gtk-delete</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_delete1_activate"/>
<accelerator key="Delete" modifiers="" signal="activate"/>
<accelerator key="Delete" signal="activate"/>
</widget>
</child>
<child>
@@ -113,12 +135,12 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="find1">
<property name="visible">True</property>
<property name="label">gtk-find</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_find_activate"/>
<accelerator key="f" modifiers="GDK_CONTROL_MASK" signal="activate"/>
<accelerator key="f" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</widget>
</child>
<child>
@@ -128,8 +150,8 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="properties1">
<property name="visible">True</property>
<property name="label">gtk-preferences</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_preferences_activate"/>
@@ -152,7 +174,7 @@
<property name="label" translatable="yes">Add _CD/DVD</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_cd_activate"/>
<accelerator key="e" modifiers="GDK_CONTROL_MASK" signal="activate"/>
<accelerator key="e" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</widget>
</child>
<child>
@@ -161,7 +183,7 @@
<property name="label" translatable="yes">Add _Directory</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_directory_activate"/>
<accelerator key="d" modifiers="GDK_CONTROL_MASK" signal="activate"/>
<accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/>
</widget>
</child>
<child>
@@ -217,17 +239,11 @@
</child>
<child>
<widget class="GtkImageMenuItem" id="cancel1">
<property name="label">gtk-cancel</property>
<property name="visible">True</property>
<property name="label" translatable="yes">Cancel</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_cancel_clicked"/>
<child internal-child="image">
<widget class="GtkImage" id="image3">
<property name="visible">True</property>
<property name="stock">gtk-cancel</property>
<property name="icon_size">1</property>
</widget>
</child>
</widget>
</child>
</widget>
@@ -257,33 +273,6 @@
<signal name="activate" handler="on_status_bar1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separatormenuitem4">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkRadioMenuItem" id="list1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Show as a list</property>
<property name="label" translatable="yes">List</property>
<property name="use_underline">True</property>
<property name="active">True</property>
<property name="draw_as_radio">True</property>
</widget>
</child>
<child>
<widget class="GtkRadioMenuItem" id="thumbnails1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Show as thumbnails</property>
<property name="label" translatable="yes">Thumbnails</property>
<property name="use_underline">True</property>
<property name="draw_as_radio">True</property>
<property name="group">list1</property>
</widget>
</child>
</widget>
</child>
</widget>
@@ -297,8 +286,8 @@
<widget class="GtkMenu" id="help1_menu">
<child>
<widget class="GtkImageMenuItem" id="about1">
<property name="visible">True</property>
<property name="label">gtk-about</property>
<property name="visible">True</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
<signal name="activate" handler="on_about1_activate"/>
@@ -312,12 +301,13 @@
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkToolbar" id="maintoolbar">
<property name="visible">True</property>
<property name="toolbar_style">GTK_TOOLBAR_BOTH</property>
<property name="toolbar_style">both</property>
<child>
<widget class="GtkToolButton" id="tb_new">
<property name="visible">True</property>
@@ -327,6 +317,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -338,6 +329,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -349,6 +341,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -362,7 +355,6 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
@@ -376,6 +368,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -388,6 +381,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -399,6 +393,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -412,7 +407,6 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">False</property>
</packing>
</child>
<child>
@@ -426,6 +420,7 @@
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
<child>
@@ -437,18 +432,7 @@
</widget>
<packing>
<property name="expand">False</property>
</packing>
</child>
<child>
<widget class="GtkToolButton" id="debugbtn">
<property name="visible">True</property>
<property name="label" translatable="yes">Debug</property>
<property name="use_underline">True</property>
<property name="stock_id">gtk-dialog-info</property>
<signal name="clicked" handler="on_debugbtn_clicked"/>
</widget>
<packing>
<property name="expand">False</property>
<property name="homogeneous">True</property>
</packing>
</child>
</widget>
@@ -469,18 +453,24 @@
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="shadow_type">GTK_SHADOW_ETCHED_IN</property>
<property name="shadow_type">etched-in</property>
<property name="caps_lock_warning">False</property>
<property name="secondary_icon_stock">gtk-clear</property>
<property name="secondary_icon_activatable">True</property>
<property name="secondary_icon_sensitive">True</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkButton" id="clear">
<property name="label">gtk-clear</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">gtk-clear</property>
<property name="use_stock">True</property>
<property name="response_id">0</property>
<signal name="clicked" handler="on_clear_clicked"/>
</widget>
<packing>
@@ -505,23 +495,14 @@
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow1">
<widget class="GtkScrolledWindow" id="scrolledwindow_discs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<widget class="GtkTreeView" id="discs">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="headers_visible">False</property>
<property name="rules_hint">True</property>
<signal name="button_press_event" handler="on_discs_button_press_event"/>
<signal name="row_activated" handler="on_discs_row_activated"/>
<signal name="cursor_changed" handler="on_discs_cursor_changed"/>
<signal name="key_release_event" handler="on_discs_key_release_event"/>
</widget>
<placeholder/>
</child>
</widget>
</child>
@@ -532,8 +513,8 @@
<property name="label" translatable="yes">Discs</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
<property name="type">tab</property>
</packing>
</child>
<child>
@@ -541,23 +522,10 @@
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<child>
<widget class="GtkTextView" id="tag_cloud_textview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="cursor_visible">False</property>
<signal name="visibility_notify_event" handler="on_tag_cloud_textview_visibility_notify_event"/>
<signal name="drag_leave" handler="on_tag_cloud_textview_drag_leave"/>
<signal name="event_after" handler="on_tag_cloud_textview_event_after"/>
<signal name="drag_motion" handler="on_tag_cloud_textview_drag_motion"/>
<signal name="drag_data_received" handler="on_tag_cloud_textview_drag_data_received"/>
<signal name="drag_drop" handler="on_tag_cloud_textview_drag_drop"/>
</widget>
<placeholder/>
</child>
</widget>
<packing>
@@ -571,9 +539,9 @@
<property name="label" translatable="yes">Tags</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
<property name="type">tab</property>
</packing>
</child>
</widget>
@@ -586,24 +554,16 @@
<widget class="GtkVPaned" id="vpaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="orientation">vertical</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow2">
<widget class="GtkScrolledWindow" id="scrolledwindow_files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<property name="hscrollbar_policy">automatic</property>
<property name="vscrollbar_policy">automatic</property>
<property name="shadow_type">in</property>
<child>
<widget class="GtkTreeView" id="files">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="rules_hint">True</property>
<signal name="drag_data_get" handler="on_files_drag_data_get"/>
<signal name="button_press_event" handler="on_files_button_press_event"/>
<signal name="row_activated" handler="on_files_row_activated"/>
<signal name="cursor_changed" handler="on_files_cursor_changed"/>
<signal name="key_release_event" handler="on_files_key_release_event"/>
</widget>
<placeholder/>
</child>
</widget>
<packing>
@@ -612,144 +572,7 @@
</packing>
</child>
<child>
<widget class="GtkNotebook" id="notebook_details">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkHBox" id="fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkScrolledWindow" id="scrolledwindow4">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTextView" id="description">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="editable">False</property>
<property name="wrap_mode">GTK_WRAP_WORD</property>
<property name="left_margin">2</property>
<property name="right_margin">2</property>
<property name="cursor_visible">False</property>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkViewport" id="thumb_box">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<signal name="button_press_event" handler="on_thumb_box_button_press_event"/>
<child>
<widget class="GtkImage" id="thumb">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="xpad">3</property>
<property name="ypad">3</property>
<property name="stock">gtk-missing-image</property>
<property name="icon_size">6</property>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
</child>
<child>
<widget class="GtkLabel" id="nb_fileinfo">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">File info</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="img_container">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkIconView" id="images">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Double click to open image</property>
<signal name="button_press_event" handler="on_images_button_press_event"/>
<signal name="item_activated" handler="on_images_item_activated"/>
<signal name="key_release_event" handler="on_images_key_release_event"/>
</widget>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Images</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">1</property>
<property name="tab_fill">False</property>
</packing>
</child>
<child>
<widget class="GtkScrolledWindow" id="exifinfo">
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
<property name="shadow_type">GTK_SHADOW_IN</property>
<child>
<widget class="GtkTreeView" id="exif_tree">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="headers_clickable">True</property>
<property name="rules_hint">True</property>
</widget>
</child>
</widget>
<packing>
<property name="position">2</property>
</packing>
</child>
<child>
<widget class="GtkLabel" id="label4">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">EXIF</property>
</widget>
<packing>
<property name="type">tab</property>
<property name="position">2</property>
<property name="tab_fill">False</property>
</packing>
</child>
</widget>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
<placeholder/>
</child>
</widget>
<packing>
@@ -780,6 +603,9 @@
<property name="visible">True</property>
<property name="has_resize_grip">False</property>
</widget>
<packing>
<property name="position">0</property>
</packing>
</child>
<child>
<widget class="GtkProgressBar" id="progressbar1">
@@ -801,63 +627,6 @@
</widget>
</child>
</widget>
<widget class="GtkMenu" id="discs_popup">
<child>
<widget class="GtkMenuItem" id="expand_all1">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Expand all nodes</property>
<property name="label" translatable="yes">_Expand all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_expand_all1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="collapse_all1">
<property name="visible">True</property>
<property name="tooltip" translatable="yes">Collapse all nodes</property>
<property name="label" translatable="yes">_Collapse all</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_collapse_all1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator4">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="update1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Update</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_update1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete2">
<property name="visible">True</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="statistics1">
<property name="visible">True</property>
<property name="label" translatable="yes">_Statistics</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_statistics1_activate"/>
</widget>
</child>
</widget>
<widget class="GtkWindow" id="win_exifinfo">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
@@ -870,108 +639,6 @@
</widget>
</child>
</widget>
<widget class="GtkMenu" id="files_popup">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<child>
<widget class="GtkMenuItem" id="add_tag1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Add tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_tag1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete_tag">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Remo_ve tag</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete_tag_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator11">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_thumb1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Add _Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_thumb1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Re_move Thumbnail</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_thumb1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator7">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="add_image1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="tooltip" translatable="yes">Add images to file. If file have no thumbnail,
thumbnail from first image will be generated.</property>
<property name="label" translatable="yes">Add _Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_add_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="remove_image1">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">Rem_ove All Images</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_remove_image1_activate"/>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separator8">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="edit2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_edit2_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="delete3">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Delete</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_delete3_activate"/>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="rename2">
<property name="visible">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="label" translatable="yes">_Rename</property>
<property name="use_underline">True</property>
<signal name="activate" handler="on_rename2_activate"/>
</widget>
</child>
</widget>
<widget class="GtkWindow" id="image_show">
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="title" translatable="yes">pyGTKtalog - Image</property>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<glade-interface>
<!-- interface-requires gtk+ 2.6 -->
<!-- interface-naming-policy toplevel-contextual -->
<widget class="GtkWindow" id="top_tags">
<child>
<widget class="GtkTextView" id="tag_cloud_textview">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="wrap_mode">word</property>
<property name="cursor_visible">False</property>
<signal name="visibility_notify_event" handler="on_tag_cloud_textview_visibility_notify_event"/>
<signal name="drag_motion" handler="on_tag_cloud_textview_drag_motion"/>
<signal name="event_after" handler="on_tag_cloud_textview_event_after"/>
<signal name="drag_drop" handler="on_tag_cloud_textview_drag_drop"/>
<signal name="drag_leave" handler="on_tag_cloud_textview_drag_leave"/>
<signal name="drag_data_received" handler="on_tag_cloud_textview_drag_data_received"/>
</widget>
</child>
</widget>
</glade-interface>

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
<!--Generated with glade3 3.4.5 on Fri Oct 23 18:28:53 2009 -->
<glade-interface>
<widget class="GtkWindow" id="window1">
<child>
<widget class="GtkVBox" id="vbox1">
<property name="visible">True</property>
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<property name="orientation">GTK_ORIENTATION_VERTICAL</property>
<child>
<widget class="GtkMenuBar" id="menubar1">
<property name="visible">True</property>
<child>
<widget class="GtkMenuItem" id="menuitem1">
<property name="visible">True</property>
<property name="label" translatable="yes">_File</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu1">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem1">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-new</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem2">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-open</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem3">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-save</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem4">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-save-as</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkSeparatorMenuItem" id="separatormenuitem1">
<property name="visible">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem5">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-quit</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem2">
<property name="visible">True</property>
<property name="label" translatable="yes">_Edit</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu2">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem6">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-cut</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem7">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-copy</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem8">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-paste</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem9">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-delete</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem3">
<property name="visible">True</property>
<property name="label" translatable="yes">_View</property>
<property name="use_underline">True</property>
</widget>
</child>
<child>
<widget class="GtkMenuItem" id="menuitem4">
<property name="visible">True</property>
<property name="label" translatable="yes">_Help</property>
<property name="use_underline">True</property>
<child>
<widget class="GtkMenu" id="menu3">
<property name="visible">True</property>
<child>
<widget class="GtkImageMenuItem" id="imagemenuitem10">
<property name="visible">True</property>
<property name="label" translatable="yes">gtk-about</property>
<property name="use_underline">True</property>
<property name="use_stock">True</property>
</widget>
</child>
</widget>
</child>
</widget>
</child>
</widget>
<packing>
<property name="expand">False</property>
</packing>
</child>
<child>
<widget class="GtkHPaned" id="hpaned1">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child>
<widget class="GtkViewport" id="viewport1">
<property name="visible">True</property>
<property name="resize_mode">GTK_RESIZE_QUEUE</property>
<child>
<widget class="GtkTreeView" id="treeview1">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
</child>
</widget>
<packing>
<property name="resize">False</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<widget class="GtkTextView" id="textview1">
<property name="visible">True</property>
<property name="can_focus">True</property>
</widget>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</widget>
<packing>
<property name="position">1</property>
</packing>
</child>
<child>
<widget class="GtkHBox" id="hbox2">
<property name="visible">True</property>
<child>
<widget class="GtkStatusbar" id="statusbar1">
<property name="visible">True</property>
<property name="spacing">2</property>
<property name="has_resize_grip">False</property>
</widget>
</child>
<child>
<widget class="GtkProgressBar" id="progressbar1">
<property name="visible">True</property>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">1</property>
</packing>
</child>
</widget>
<packing>
<property name="expand">False</property>
<property name="position">2</property>
</packing>
</child>
</widget>
</child>
</widget>
</glade-interface>

189
pygtktalog/views/main.py Normal file
View File

@@ -0,0 +1,189 @@
"""
Project: pyGTKtalog
Description: View for main window
Type: core
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-02
"""
import os.path
from gtkmvc import View
def get_glade(glade_filename):
"""
Return full path to specified glade file
"""
return os.path.join(os.path.dirname(__file__), "glade", glade_filename)
class MainView(View):
"""
Create window from glade file. Note, that glade file is placed in view
module under glade subdirectory.
"""
glade = get_glade("main.glade")
top = "main"
def __init__(self, top="main"):
"""
Initialize view
"""
View.__init__(self)
self.app_sensitive = None
self['tag_path_box'].hide()
self.discs = DiscsView()
#self['scrolledwindow_discs'].add_with_viewport(\
# self.discs.get_top_widget())
self['scrolledwindow_discs'].add(self.discs.get_top_widget())
self.files = FilesView()
self['scrolledwindow_files'].add_with_viewport(\
self.files.get_top_widget())
self.details = DetailsView()
self['vpaned1'].add2(self.details.get_top_widget())
def set_widgets_scan_sensitivity(self, sensitive=True):
"""
Activate/deactivate selected widgets while scanning is active
"""
pass
def set_widgets_app_sensitivity(self, sensitive=True):
"""
Enable/disable widgets for empty application. Usefull for first run
of an application (without any db filename as an argument).
"""
if self.app_sensitive is sensitive:
return
for widget in ['scrolledwindow_discs', 'scrolledwindow_files',
'tb_save', 'tb_addcd', 'tb_adddir', 'tb_find',
'edit1', 'catalog1', 'save1', 'save_as1', 'import',
'export']:
self[widget].set_sensitive(sensitive)
# widgets from subclasses
self.details['notebook_details'].set_sensitive(sensitive)
class DiscsView(View):
"""
Separate Discs TreeView subview.
"""
glade = get_glade("discs.glade")
top = 'discs'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
self.menu = DiscsPopupView()
class DiscsPopupView(View):
"""
Separate Discs PopUp subview.
"""
glade = get_glade("discs.glade")
top = 'discs_popup'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
def set_update_sensitivity(self, state):
"""
Set sensitivity for 'update' popup menu item
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
self['update'].set_sensitive(state)
def set_menu_items_sensitivity(self, state):
"""
Set sensitivity for couple of popup menu items, which should be
disabled if user right-clicks on no item in treeview.
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
for item in ['update', 'rename', 'delete', 'statistics']:
self[item].set_sensitive(state)
class FilesView(View):
"""
Separate subview of Files TreeView as a table.
"""
glade = get_glade("files.glade")
top = 'files'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
self.menu = FilesPopupView()
class FilesPopupView(View):
"""
Separate Files PopUp subview.
"""
glade = get_glade("files.glade")
top = 'files_popup'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
def set_menu_items_sensitivity(self, state):
"""
Set sensitivity for couple of popup menu items, which should be
disabled if user right-clicks on no item in treeview.
Arguments:
@state - Bool, if True update menu item will be sensitive,
otherwise not
"""
for item in ["add_tag", "delete_tag", "add_thumb", "remove_thumb",
"add_image", "remove_image", "edit", "delete", "rename"]:
self[item].set_sensitive(state)
class TagcloudView(View):
"""
Textview subview with clickable tags.
"""
glade = get_glade("tagcloud.glade")
top = 'tag_cloud_textview'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)
class DetailsView(View):
"""
Notebook subview containing tabs with details and possibly Exif, images
assocated with object and alternatively thumbnail.
"""
glade = get_glade("details.glade")
top = 'notebook_details'
def __init__(self):
"""
Initialize view
"""
View.__init__(self)

View File

@@ -0,0 +1,36 @@
"""
Project: pyGTKtalog
Description: Menu for the main window
Type: interface
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-03-14 21:31:57
"""
import gtk
from gtkmvc import View
class MainMenu(View):
def __init__(self):
View.__init__(self)
self['mainMenu'] = gtk.MenuBar()
self['file_menu'] = gtk.MenuItem(_("_File"))
accel_group = gtk.AccelGroup()
menu_items = (("/_File", None, None, 0, "<Branch>"),
("/File/_New", "<control>N", None, 0, None),
("/File/_Open", "<control>O", None, 0, None),
("/File/_Save", "<control>S", None, 0, None),
("/File/Save _As", None, None, 0, None),
("/File/sep1", None, None, 0, "<Separator>"),
("/File/Quit", "<control>Q", gtk.main_quit, 0, None),
("/_Options", None, None, 0, "<Branch>"),
("/Options/Test", None, None, 0, None),
("/_Help", None, None, 0, "<LastBranch>"),
("/_Help/About", None, None, 0, None),)
item_factory = gtk.ItemFactory(gtk.MenuBar, "<main>", accel_group)
item_factory.create_items(menu_items)

View File

@@ -0,0 +1,18 @@
"""
Project: pyGTKtalog
Description: Toolbar for the main window
Type: interface
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-04-20 18:47:49
"""
import gtk
from gtkmvc import View
class ToolBar(View):
def __init__(self):
View.__init__(self)
self['maintoolbar'] = gtk.Toolbar()

View File

@@ -22,27 +22,26 @@
# -------------------------------------------------------------------------
__version__ = "1.0 RC2"
LICENCE = open('LICENCE').read()
import os.path
from os import popen
from utils import deviceHelper
from gtkmvc import Controller
from c_config import ConfigController
from views.v_config import ConfigView
from c_search import SearchController
from views.v_search import SearchView
import views.v_dialogs as Dialogs
from views.v_image import ImageView
import gtk
import pango
from gtkmvc import Controller
from lib import device_helper
from lib.globs import APPL_VERSION
from c_config import ConfigController
from views.v_config import ConfigView
from c_search import SearchController
from views.v_search import SearchView
import views.v_dialogs as Dialogs
from views.v_image import ImageView
class MainController(Controller):
"""Controller for main application window"""
scan_cd = False
@@ -253,7 +252,7 @@ class MainController(Controller):
except TypeError:
if __debug__:
print "c_main.py: on_edit2_activate(): 0",
print "zaznaczonych wierszy"
print "selected rows"
return
val = self.model.get_file_info(id)
@@ -282,11 +281,9 @@ class MainController(Controller):
def on_remove_thumb1_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete thumbnails'
question = 'Delete thumbnails?'
description = "Thumbnails for selected items will be permanently"
description += " removed from catalog."
obj = Dialogs.Qst(title, question, description)
obj = Dialogs.Qst(_("Delete thumbnails"), _("Delete thumbnails?"),
_("Thumbnails for selected items will be "
"permanently removed from catalog."))
if not obj.run():
return
try:
@@ -308,11 +305,9 @@ class MainController(Controller):
def on_remove_image1_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete images'
question = 'Delete all images?'
description = 'All images for selected items will be permanently'
description += ' removed from catalog.'
obj = Dialogs.Qst(title, question, description)
obj = Dialogs.Qst(_("Delete images"), _("Delete all images?"),
_("All images for selected items will be "
"permanently removed from catalog."))
if not obj.run():
return
try:
@@ -344,8 +339,8 @@ class MainController(Controller):
else:
ImageView(img)
else:
Dialogs.Inf("Image view", "No Image",
"Image file does not exist.")
Dialogs.Inf(_("Image view"), _("No Image"),
_("Image file does not exist."))
def on_rename1_activate(self, widget):
model, iter = self.view['discs'].get_selection().get_selected()
@@ -493,11 +488,10 @@ class MainController(Controller):
# check if any unsaved project is on go.
if self.model.unsaved_project and \
self.model.config.confd['confirmquit']:
title = 'Quit application - pyGTKtalog'
question = 'Do you really want to quit?'
description = "Current database is not saved, any changes will "
description += "be lost."
if not Dialogs.Qst(title, question, description).run():
if not Dialogs.Qst(_("Quit application") + " - pyGTKtalog",
_("Do you really want to quit?"),
_("Current database is not saved, any changes "
"will be lost.")).run():
return
self.__store_settings()
self.model.cleanup()
@@ -507,10 +501,10 @@ class MainController(Controller):
def on_new_activate(self, widget):
"""Create new database file"""
if self.model.unsaved_project:
title = 'Unsaved data - pyGTKtalog'
question = "Do you want to abandon changes?"
desc = "Current database is not saved, any changes will be lost."
if not Dialogs.Qst(title, question, desc).run():
if not Dialogs.Qst(_("Unsaved data") + " - pyGTKtalog",
_("Do you want to abandon changes?"),
_("Current database is not saved, any changes "
"will be lost.")).run():
return
self.model.new()
@@ -526,9 +520,9 @@ class MainController(Controller):
def on_add_cd_activate(self, widget, label=None, current_id=None):
"""Add directory structure from cd/dvd disc"""
mount = deviceHelper.volmount(self.model.config.confd['cd'])
if mount == 'ok':
guessed_label = deviceHelper.volname(self.model.config.confd['cd'])
mount, msg = device_helper.volmount(self.model.config.confd['cd'])
if mount:
guessed_label = device_helper.volname(self.model.config.confd['cd'])
if not label:
label = Dialogs.InputDiskLabel(guessed_label).run()
if label:
@@ -541,13 +535,13 @@ class MainController(Controller):
self.model.unsaved_project = True
self.__set_title(filepath=self.model.filename, modified=True)
else:
deviceHelper.volumount(self.model.config.confd['cd'])
device_helper.volumount(self.model.config.confd['cd'])
return True
else:
Dialogs.Wrn("Error mounting device - pyGTKtalog",
"Cannot mount device pointed to %s" %
Dialogs.Wrn(_("Error mounting device") + " - pyGTKtalog",
_("Cannot mount device pointed to %s") %
self.model.config.confd['cd'],
"Last mount message:\n%s" % mount)
_("Last mount message:\n%s") % msg)
return False
def on_add_directory_activate(self, widget, path=None, label=None,
@@ -571,7 +565,7 @@ class MainController(Controller):
# NOTE: about
def on_about1_activate(self, widget):
"""Show about dialog"""
Dialogs.Abt("pyGTKtalog", __version__, "About",
Dialogs.Abt("pyGTKtalog", "%d.%d.%d" % APPL_VERSION, "About",
["Roman 'gryf' Dobosz"], LICENCE)
return
@@ -600,6 +594,7 @@ class MainController(Controller):
def on_save_activate(self, widget):
"""Save catalog to file"""
# FIXME: Traceback when recent is null
if self.model.filename:
self.model.save()
self.__set_title(filepath=self.model.filename)
@@ -619,8 +614,8 @@ class MainController(Controller):
self.model.config.add_recent(path)
self.__set_title(filepath=path)
else:
Dialogs.Err("Error writing file - pyGTKtalog",
"Cannot write file %s." % path, "%s" % err)
Dialogs.Err(_("Error writing file") + " - pyGTKtalog",
_("Cannot write file %s.") % path, "%s" % err)
def on_stat1_activate(self, menu_item, selected_id=None):
data = self.model.get_stats(selected_id)
@@ -641,9 +636,9 @@ class MainController(Controller):
"""Open catalog file"""
confirm = self.model.config.confd['confirmabandon']
if self.model.unsaved_project and confirm:
obj = Dialogs.Qst('Unsaved data - pyGTKtalog',
'There is not saved database',
'Pressing "Ok" will abandon catalog.')
obj = Dialogs.Qst(_("Unsaved data") + " - pyGTKtalog",
_("There is not saved database"),
_("Pressing <b>Ok</b> will abandon catalog."))
if not obj.run():
return
@@ -667,8 +662,8 @@ class MainController(Controller):
if path:
if not self.model.open(path):
Dialogs.Err("Error opening file - pyGTKtalog",
"Cannot open file %s." % path)
Dialogs.Err(_("Error opening file") + " - pyGTKtalog",
_("Cannot open file %s.") % path)
else:
self.__generate_recent_menu()
self.__activate_ui(path)
@@ -734,13 +729,14 @@ class MainController(Controller):
"""delete selected images (with thumbnails)"""
list_of_paths = self.view['images'].get_selected_items()
if not list_of_paths:
Dialogs.Inf("Delete images", "No images selected",
"You have to select at least one image to delete.")
Dialogs.Inf(_("Delete images"), _("No images selected"),
_("You have to select at least one image to delete."))
return
if self.model.config.confd['delwarn']:
obj = Dialogs.Qst('Delete images', 'Delete selected images?',
'Selected images will be permanently removed from catalog.')
obj = Dialogs.Qst(_("Delete images"), _("Delete selected images?"),
_("Selected images will be permanently removed"
" from catalog."))
if not obj.run():
return
@@ -769,7 +765,7 @@ class MainController(Controller):
def on_img_save_activate(self, menu_item):
"""export images (not thumbnails) into desired direcotry"""
dialog = Dialogs.SelectDirectory("Choose directory to save images")
dialog = Dialogs.SelectDirectory(_("Choose directory to save images"))
filepath = dialog.run()
if not filepath:
@@ -794,14 +790,13 @@ class MainController(Controller):
count += 1
if count > 0:
Dialogs.Inf("Save images",
"%d images was succsefully saved." % count,
"Images are placed in directory:\n%s." % filepath)
Dialogs.Inf(_("Save images"),
_("%d images was succsefully saved.") % count,
_("Images are placed in directory:\n%s.") % filepath)
else:
description = "Images probably don't have real images - only"
description += " thumbnails."
Dialogs.Inf("Save images",
"No images was saved.",
description = _("Images probably don't have real images - only"
" thumbnails.")
Dialogs.Inf(_("Save images"), _("No images was saved."),
description)
return
@@ -810,12 +805,12 @@ class MainController(Controller):
list_of_paths = self.view['images'].get_selected_items()
if not list_of_paths:
Dialogs.Inf("Set thumbnail", "No image selected",
"You have to select one image to set as thumbnail.")
Dialogs.Inf(_("Set thumbnail"), _("No image selected"),
_("You have to select one image to set as thumbnail."))
return
if len(list_of_paths) >1:
Dialogs.Inf("Set thumbnail", "To many images selected",
"You have to select one image to set as thumbnail.")
Dialogs.Inf(_("Set thumbnail"), _("To many images selected"),
_("You have to select one image to set as thumbnail."))
return
model = self.view['images'].get_model()
@@ -867,9 +862,15 @@ class MainController(Controller):
self.__popup_menu(event)
def on_expand_all1_activate(self, menu_item):
self.view['discs'].expand_all()
return
def on_export_activate(self, menu_item):
"""export db file and coressponding images to tar.bz2 archive"""
dialog = Dialogs.ChooseFilename(None, _("Choose export file"))
filepath = dialog.run()
if not filepath:
return
# TODO: dokończyć ten shit
def on_collapse_all1_activate(self, menu_item):
self.view['discs'].collapse_all()
@@ -1009,8 +1010,8 @@ class MainController(Controller):
def on_delete_tag_activate(self, menu_item):
ids = self.__get_tv_selection_ids(self.view['files'])
if not ids:
Dialogs.Inf("Remove tags", "No files selected",
"You have to select some files first.")
Dialogs.Inf(_("Remove tags"), _("No files selected"),
_("You have to select some files first."))
return
tags = self.model.get_tags_by_file_id(ids)
@@ -1018,8 +1019,9 @@ class MainController(Controller):
d = Dialogs.TagsRemoveDialog(tags)
retcode, retval = d.run()
if retcode=="ok" and not retval:
Dialogs.Inf("Remove tags", "No tags selected",
"You have to select any tag to remove from files.")
Dialogs.Inf(_("Remove tags"), _("No tags selected"),
_("You have to select any tag to remove from"
" files."))
return
elif retcode == "ok" and retval:
self.model.delete_tags(ids, retval)
@@ -1044,7 +1046,7 @@ class MainController(Controller):
def on_add_image1_activate(self, menu_item):
dialog = Dialogs.LoadImageFile(True)
msg = "Don't copy images. Generate only thumbnails."
msg = _("Don't copy images. Generate only thumbnails.")
toggle = gtk.CheckButton(msg)
toggle.show()
dialog.dialog.set_extra_widget(toggle)
@@ -1108,15 +1110,15 @@ class MainController(Controller):
return
if not selected_iter:
Dialogs.Inf("Delete disc", "No disc selected",
"You have to select disc first before you " +\
"can delete it")
Dialogs.Inf(_("Delete disc"), _("No disc selected"),
_("You have to select disc first before you "
"can delete it"))
return
if self.model.config.confd['delwarn']:
name = model.get_value(selected_iter, 1)
obj = Dialogs.Qst('Delete %s' % name, 'Delete %s?' % name,
'Object will be permanently removed.')
obj = Dialogs.Qst(_("Delete %s") % name, _("Delete %s?") % name,
_("Object will be permanently removed."))
if not obj.run():
return
@@ -1159,14 +1161,14 @@ class MainController(Controller):
return
if not list_of_paths:
Dialogs.Inf("Delete files", "No files selected",
"You have to select at least one file to delete.")
Dialogs.Inf(_("Delete files"), _("No files selected"),
_("You have to select at least one file to delete."))
return
if self.model.config.confd['delwarn']:
description = "Selected files and directories will be "
description += "permanently\n removed from catalog."
obj = Dialogs.Qst("Delete files", "Delete files?", description)
obj = Dialogs.Qst(_("Delete files"), _("Delete files?"),
_("Selected files and directories will be "
"permanently\n removed from catalog."))
if not obj.run():
return
@@ -1207,10 +1209,9 @@ class MainController(Controller):
def on_th_delete_activate(self, menu_item):
if self.model.config.confd['delwarn']:
title = 'Delete thumbnail'
question = 'Delete thumbnail?'
dsc = "Current thumbnail will be permanently removed from catalog."
obj = Dialogs.Qst(title, question, dsc)
obj = Dialogs.Qst(_("Delete thumbnail"), _("Delete thumbnail?"),
_("Current thumbnail will be permanently removed"
" from catalog."))
if not obj.run():
return
path, column = self.view['files'].get_cursor()
@@ -1354,20 +1355,22 @@ class MainController(Controller):
# umount/eject cd
ejectapp = self.model.config.confd['ejectapp']
if self.model.config.confd['eject'] and ejectapp:
msg = deviceHelper.eject_cd(ejectapp,
msg = device_helper.eject_cd(ejectapp,
self.model.config.confd['cd'])
if msg != 'ok':
Dialogs.Wrn("error ejecting device - pyGTKtalog",
"Cannot eject device pointed to %s" %
title = _("Error ejecting device")
Dialogs.Wrn(title + " - pyGTKtalog",
_("Cannot eject device pointed to %s") %
self.model.config.confd['cd'],
"Last eject message:\n%s" % msg)
_("Last eject message:\n%s") % msg)
else:
msg = deviceHelper.volumount(self.model.config.confd['cd'])
msg = device_helper.volumount(self.model.config.confd['cd'])
if msg != 'ok':
Dialogs.Wrn("error unmounting device - pyGTKtalog",
"Cannot unmount device pointed to %s" %
title = _("Error unmounting device")
Dialogs.Wrn(title + " - pyGTKtalog",
_("Cannot unmount device pointed to %s") %
self.model.config.confd['cd'],
"Last umount message:\n%s" % msg)
_("Last umount message:\n%s") % msg)
return
def property_progress_value_change(self, model, old, new):

86
src/lib/device_helper.py Normal file
View File

@@ -0,0 +1,86 @@
"""
Project: pyGTKtalog
Description: Simple functions for device management.
Type: lib
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import os
import locale
import gettext
from src.lib.globs import APPL_SHORT_NAME
locale.setlocale(locale.LC_ALL, '')
gettext.install(APPL_SHORT_NAME, 'locale', unicode=True)
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
label = None
if dev != None:
try:
disk = open(dev, "rb")
disk.seek(32808)
label = disk.read(32).strip()
disk.close()
except IOError:
return None
return label
def volmount(mntp):
"""
Mount device.
@param mountpoint
@returns tuple with bool status of mount, and string with error message
"""
_in, _out, _err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
return False, inf[0].strip()
else:
return True, ''
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in, _out, _err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
return inf[0].strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device = parts
if device[0] == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess device name by mountpoint from fstab"""
fstab = open("/etc/fstab")
device = None
for line in fstab.readlines():
output = line.split()
# lengtht of single valid fstab line is at least 5
if len(output) > 5 and output[1] == mntp and output[0][0] != '#':
device = output[0]
fstab.close()
return device
def eject_cd(eject_app, cdrom):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in, _out, _err = os.popen3("%s %s" % (eject_app, cdrom))
inf = _err.readlines()
if len(inf) > 0 and inf[0].strip() != '':
return inf[0].strip()
return 'ok'
return _("Eject program not specified")

View File

@@ -25,15 +25,17 @@
import os.path
import sys
if sys.argv[0]: top_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
else: top_dir = "."
if sys.argv[0]:
top_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
else:
top_dir = "."
# ----------------------------------------------------------------------
TOPDIR = top_dir
RESOURCES_DIR = os.path.join(TOPDIR, "resources")
GLADE_DIR = os.path.join(RESOURCES_DIR, "glade")
STYLES_DIR = os.path.join(RESOURCES_DIR, "styles")
APPL_SHORT_NAME = "pycolector"
APPL_VERSION = (1, 0, 0)
APPL_SHORT_NAME = "pygtktalog"
APPL_VERSION = (1, 0, 2)
# ----------------------------------------------------------------------

View File

@@ -27,7 +27,7 @@ import os
from datetime import date
class GthumbCommentParser(object):
"""Read and return comments created eith gThumb program"""
def __init__(self, image_path, image_filename):
self.path = image_path
self.filename = image_filename
@@ -36,16 +36,16 @@ class GthumbCommentParser(object):
"""Return dictionary with apropriate fields, or None if no comment
available"""
try:
gf = gzip.open(os.path.join(self.path,
'.comments', self.filename + '.xml'))
gzf = gzip.open(os.path.join(self.path, '.comments',
self.filename + '.xml'))
except:
return None
try:
xml = gf.read()
gf.close()
xml = gzf.read()
gzf.close()
except:
gf.close()
gzf.close()
return None
if not xml:

View File

@@ -23,11 +23,9 @@
# -------------------------------------------------------------------------
from shutil import copy
from os import path, mkdir
from os import path
from hashlib import sha512
from tempfile import mkstemp
from utils import EXIF
import Image
class Img(object):
@@ -46,14 +44,14 @@ class Img(object):
"""Save image and asociated thumbnail into specific directory structure
returns filename for image"""
image_filename = path.join(self.base, self.sha512)
thumbnail = path.join(self.base, self.sha512 + "_t")
# check wheter image already exists
if path.exists(image_filename) and path.exists(thumbnail):
if __debug__:
print "image", self.filename, "with hash", self.sha512, "already exist"
print "image", self.filename, "with hash",
print self.sha512, "already exist"
return self.sha512
if not path.exists(thumbnail):

83
src/lib/thumbnail.py Normal file
View File

@@ -0,0 +1,83 @@
"""
Project: pyGTKtalog
Description: Thumbnail helper
Type: library
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2012-02-19
"""
from hashlib import sha256
from cStringIO import StringIO
from lib import EXIF
import Image
class Thumbnail(object):
"""Class for generate/extract thumbnail from image file"""
def __init__(self, fp, base=''):
self.thumb_x = 160
self.thumb_y = 160
self.base = base
self.sha256 = sha256(fp.read(10485760)).hexdigest()
fp.seek(0)
self.fp = fp
def save(self):
"""Save thumbnail into specific directory structure
return exif obj and fp to thumbnail"""
exif = {}
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
3: Image.ROTATE_180, # Rotated 180
4: Image.FLIP_TOP_BOTTOM, # Mirrored vertical
5: Image.ROTATE_90, # Mirrored horizontal then
# rotated 90 CCW
6: Image.ROTATE_270, # Rotated 90 CW
7: Image.ROTATE_270, # Mirrored horizontal then
# rotated 90 CW
8: Image.ROTATE_90} # Rotated 90 CCW
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
try:
exif = EXIF.process_file(self.fp)
except:
self.fp.seek(0)
thumb_file = StringIO()
if 'JPEGThumbnail' in exif:
thumb_file.write(exif['JPEGThumbnail'])
if 'Image Orientation' in exif:
orient = exif['Image Orientation'].values[0]
if orient > 1 and orient in orientations:
tmp_thumb_img = StringIO()
thumb_image = Image.open(self.fp)
tmp_thumb_img = thumb_image.transpose(orientations[orient])
if orient in flips:
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
if tmp_thumb_img:
thumb_file.seek(0)
tmp_thumb_img.save(thumb_file, 'JPEG')
tmp_thumb_img.close()
else:
thumb = self.__scale_image()
if thumb:
thumb.save(self.thumbnail_path, "JPEG")
return exif, thumb_file
def __scale_image(self):
"""create thumbnail. returns image object or None"""
try:
image_thumb = Image.open(self.fp).convert('RGB')
except:
return None
it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y:
image_thumb.thumbnail((self.thumb_x, self.thumb_y),
Image.ANTIALIAS)
return image_thumb

View File

@@ -27,28 +27,27 @@ import sys
import shutil
import bz2
import math
import sqlite3 as sqlite
from tempfile import mkstemp
from datetime import datetime
import threading as _threading
import gtk
import gobject
from gtkmvc.model_mt import ModelMT
from pysqlite2 import dbapi2 as sqlite
from datetime import datetime
import threading as _threading
from m_config import ConfigModel
try:
from utils.thumbnail import Thumbnail
from utils.img import Img
from lib.thumbnail import Thumbnail
from lib.img import Img
except:
pass
from utils.parse_exif import ParseExif
from utils.gthumb import GthumbCommentParser
from lib.parse_exif import ParseExif
from lib.gthumb import GthumbCommentParser
from utils.no_thumb import no_thumb as no_thumb_img
from lib.no_thumb import no_thumb as no_thumb_img
from lib.video import Video
class MainModel(ModelMT):
"""Create, load, save, manipulate db file which is container for data"""
@@ -87,6 +86,7 @@ class MainModel(ModelMT):
# images extensions - only for PIL and EXIF
IMG = ['jpg', 'jpeg', 'gif', 'png', 'tif', 'tiff', 'tga', 'pcx', 'bmp',
'xbm', 'xpm', 'jp2', 'jpx', 'pnm']
MOV = ['avi', 'mpg', 'mpeg', 'mkv', 'wmv', 'ogm', 'mov']
def __init__(self):
"""initialize"""
@@ -592,6 +592,14 @@ class MainModel(ModelMT):
return
if filename:
if not '.sqlite' in filename:
filename += '.sqlite'
else:
filename = filename[:filename.rindex('.sqlite')] + '.sqlite'
if self.config.confd['compress']:
filename += '.bz2'
self.filename = filename
val, err = self.__compress_and_save()
if not val:
@@ -1742,6 +1750,23 @@ class MainModel(ModelMT):
ext = i.split('.')[-1].lower()
# Video
if ext in self.MOV:
#import rpdb2; rpdb2.start_embedded_debugger('pass')
v = Video(current_file)
cfn = v.capture()
img = Img(cfn, self.image_path)
th = img.save()
if th:
sql = """INSERT INTO
thumbnails(file_id, filename)
VALUES(?, ?)"""
db_cursor.execute(sql, (fileid, th+"_t"))
sql = """INSERT INTO images(file_id, filename)
VALUES(?, ?)"""
db_cursor.execute(sql, (fileid, th))
os.unlink(cfn)
# Images - thumbnails and exif data
if self.config.confd['thumbs'] and ext in self.IMG:
thumb = Thumbnail(current_file, self.image_path)

File diff suppressed because it is too large Load Diff

View File

@@ -1,112 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
"""
device (cd, dvd) helper
"""
import os
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
if dev != None:
try:
a = open(dev,"rb")
a.seek(32808)
b = a.read(32).strip()
a.close()
except:
return None
return b
return None
def volmount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for i in inf:
i.strip()
return i
else:
return 'ok'
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for error in inf:
error.strip()
if error.strip()[:7] == 'umount:':
return error.strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device, txt1, mount_point, txt2, filesystem, options = parts
if device == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess mountpoint from fstab"""
fstab = open("/etc/fstab")
for line in fstab.readlines():
a = line.split()
try:
if a[1] == mntp and a[0][0] != '#':
fstab.close()
return a[0]
except:
pass
fstab.close()
return None
def eject_cd(eject_app, cd):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in,_out,_err = os.popen3("%s %s" % (eject_app, cd))
inf = _err.readlines()
error = ''
for error in inf:
error.strip()
if error !='':
return error
return 'ok'
return "Eject program not specified"

View File

@@ -1,112 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
"""
device (cd, dvd) helper
"""
import os
def volname(mntp):
"""read volume name from cd/dvd"""
dev = mountpoint_to_dev(mntp)
if dev != None:
try:
a = open(dev,"rb")
a.seek(32808)
b = a.read(32).strip()
a.close()
except:
return None
return b
return None
def volmount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("mount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for i in inf:
i.strip()
return i
else:
return 'ok'
def volumount(mntp):
"""mount device, return 'ok' or error message"""
_in,_out,_err = os.popen3("umount %s" % mntp)
inf = _err.readlines()
if len(inf) > 0:
for error in inf:
error.strip()
if error.strip()[:7] == 'umount:':
return error.strip()
return 'ok'
def check_mount(dev):
"""Refresh the entries from fstab or mount."""
mounts = os.popen('mount')
for line in mounts.readlines():
parts = line.split()
device, txt1, mount_point, txt2, filesystem, options = parts
if device == dev:
return True
return False
def mountpoint_to_dev(mntp):
"""guess mountpoint from fstab"""
fstab = open("/etc/fstab")
for line in fstab.readlines():
a = line.split()
try:
if a[1] == mntp and a[0][0] != '#':
fstab.close()
return a[0]
except:
pass
fstab.close()
return None
def eject_cd(eject_app, cd):
"""mount device, return 'ok' or error message"""
if len(eject_app) > 0:
_in,_out,_err = os.popen3("%s %s" % (eject_app, cd))
inf = _err.readlines()
error = ''
for error in inf:
error.strip()
if error !='':
return error
return 'ok'
return "Eject program not specified"

View File

@@ -1,116 +0,0 @@
# This Python file uses the following encoding: utf-8
#
# Author: Roman 'gryf' Dobosz gryf@elysium.pl
#
# Copyright (C) 2007 by Roman 'gryf' Dobosz
#
# This file is part of pyGTKtalog.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# -------------------------------------------------------------------------
from tempfile import mkstemp
from hashlib import sha512
from shutil import move
from os import path
import sys
from utils import EXIF
import Image
class Thumbnail(object):
"""Class for generate/extract thumbnail from image file"""
def __init__(self, filename=None, base=''):
self.thumb_x = 160
self.thumb_y = 160
self.filename = filename
self.base = base
self.sha512 = sha512(open(filename).read()).hexdigest()
self.thumbnail_path = path.join(self.base, self.sha512 + "_t")
def save(self):
"""Save thumbnail into specific directory structure
return filename base and exif object or None"""
exif = {}
orientations = {2: Image.FLIP_LEFT_RIGHT, # Mirrored horizontal
3: Image.ROTATE_180, # Rotated 180
4: Image.FLIP_TOP_BOTTOM, # Mirrored vertical
5: Image.ROTATE_90, # Mirrored horizontal then
# rotated 90 CCW
6: Image.ROTATE_270, # Rotated 90 CW
7: Image.ROTATE_270, # Mirrored horizontal then
# rotated 90 CW
8: Image.ROTATE_90} # Rotated 90 CCW
flips = {7: Image.FLIP_LEFT_RIGHT, 5: Image.FLIP_LEFT_RIGHT}
image_file = open(self.filename, 'rb')
try:
exif = EXIF.process_file(image_file)
except:
if __debug__:
print "exception", sys.exc_info()[0], "raised with file:"
print self.filename
finally:
image_file.close()
if path.exists(self.thumbnail_path):
if __debug__:
print "file", self.filename, "with hash", self.sha512, "exists"
return self.sha512, exif
if 'JPEGThumbnail' in exif:
if __debug__:
print self.filename, "exif thumb"
exif_thumbnail = exif['JPEGThumbnail']
thumb_file = open(self.thumbnail_path, 'wb')
thumb_file.write(exif_thumbnail)
thumb_file.close()
if 'Image Orientation' in exif:
orient = exif['Image Orientation'].values[0]
if orient > 1 and orient in orientations:
temp_image_path = mkstemp()[1]
thumb_image = Image.open(self.thumbnail_path)
tmp_thumb_img = thumb_image.transpose(orientations[orient])
if orient in flips:
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
if tmp_thumb_img:
tmp_thumb_img.save(temp_image_path, 'JPEG')
move(temp_image_path, self.thumbnail_path)
return self.sha512, exif
else:
if __debug__:
print self.filename, "no exif thumb"
thumb = self.__scale_image()
if thumb:
thumb.save(self.thumbnail_path, "JPEG")
return self.sha512, exif
return None, exif
def __scale_image(self):
"""create thumbnail. returns image object or None"""
try:
image_thumb = Image.open(self.filename).convert('RGB')
except:
return None
it_x, it_y = image_thumb.size
if it_x > self.thumb_x or it_y > self.thumb_y:
image_thumb.thumbnail((self.thumb_x, self.thumb_y), Image.ANTIALIAS)
return image_thumb

View File

@@ -24,11 +24,11 @@
from gtkmvc import View
import os.path
import utils.globals
import lib.globs
class ConfigView(View):
"""Preferences window from glade file """
GLADE = os.path.join(utils.globals.GLADE_DIR, "config.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "config.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)

View File

@@ -25,7 +25,7 @@
import gtk
import gobject
import os
import utils.globals
import lib.globs
class Qst(object):
"""Show simple dialog for questions
@@ -128,7 +128,7 @@ class InputDiskLabel(object):
"""Sepcific dialog for quering user for a disc label"""
def __init__(self, label=""):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.label = ""
if label!= None:
self.label = label
@@ -148,7 +148,7 @@ class InputNewName(object):
"""Sepcific dialog for quering user for a disc label"""
def __init__(self, name=""):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.label = ""
self.name = name
@@ -169,7 +169,7 @@ class PointDirectoryToAdd(object):
URI="file://"+os.path.abspath(os.path.curdir)
def __init__(self,volname='',dirname=''):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.gladexml = gtk.glade.XML(self.gladefile, "addDirDialog")
self.volname = self.gladexml.get_widget("dirvolname")
self.volname.set_text(volname)
@@ -257,15 +257,15 @@ class SelectDirectory(object):
dialog.destroy()
return retval
class ChooseDBFilename(object):
"""Sepcific dialog for quering user for selecting filename for database"""
class ChooseFilename(object):
"""Dialog for quering user for selecting filename"""
URI=None
def __init__(self, path=None):
def __init__(self, path=None, title=''):
self.path = path
self.dialog = gtk.FileChooserDialog(
title="Save catalog as...",
title="",
action=gtk.FILE_CHOOSER_ACTION_SAVE,
buttons=(
gtk.STOCK_CANCEL,
@@ -276,7 +276,44 @@ class ChooseDBFilename(object):
self.dialog.set_action(gtk.FILE_CHOOSER_ACTION_SAVE)
self.dialog.set_default_response(gtk.RESPONSE_OK)
self.dialog.set_do_overwrite_confirmation(True)
self.dialog.set_title('Save catalog to file...')
self.dialog.set_title(title)
f = gtk.FileFilter()
f.set_name("Catalog files")
f.add_pattern("*.sqlite")
f.add_pattern("*.sqlite.bz2")
self.dialog.add_filter(f)
f = gtk.FileFilter()
f.set_name("All files")
f.add_pattern("*.*")
self.dialog.add_filter(f)
def run(self):
if self.URI:
self.dialog.set_current_folder_uri(self.URI)
elif self.path and os.path.exists(self.path):
self.path = "file://"+os.path.abspath(self.path)
self.dialog.set_current_folder_uri(self.path)
response = self.dialog.run()
if response == gtk.RESPONSE_OK:
filename = self.dialog.get_filename()
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
else:
self.dialog.destroy()
return None
pass
class ChooseDBFilename(ChooseFilename):
"""Sepcific dialog for quering user for selecting filename for database"""
URI=None
def __init__(self, path=None):
ChooseFilename.__init__(self)
self.dialog.set_title('Save catalog as...')
f = gtk.FileFilter()
f.set_name("Catalog files")
@@ -298,11 +335,6 @@ class ChooseDBFilename(object):
response = self.dialog.run()
if response == gtk.RESPONSE_OK:
filename = self.dialog.get_filename()
print filename, ' do ',
if filename[-11:].lower() != '.sqlite.bz2' and \
filename[-7:].lower() != '.sqlite':
filename = filename + '.sqlite.bz2'
print filename
self.__class__.URI = self.dialog.get_current_folder_uri()
self.dialog.destroy()
return filename
@@ -454,7 +486,7 @@ class StatsDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, values={}):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.values = values
def run(self):
@@ -497,7 +529,7 @@ class TagsDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
def run(self):
gladexml = gtk.glade.XML(self.gladefile, "tagsDialog")
@@ -516,7 +548,7 @@ class TagsRemoveDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, tag_dict=None):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.tag_dict = tag_dict
def run(self):
@@ -585,7 +617,7 @@ class EditDialog(object):
"""Sepcific dialog for display stats"""
def __init__(self, values={}):
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
self.gladefile = os.path.join(lib.globs.GLADE_DIR, "dialogs.glade")
self.values = values
def run(self):

View File

@@ -23,14 +23,14 @@
# -------------------------------------------------------------------------
import os.path
import utils.globals
import lib.globs
from gtkmvc import View
class MainView(View):
"""This handles only the graphical representation of the
application. The widgets set is loaded from glade file"""
GLADE = os.path.join(utils.globals.GLADE_DIR, "main.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "main.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)

View File

@@ -24,12 +24,12 @@
from gtkmvc import View
import os.path
import utils.globals
import lib.globs
class SearchView(View):
"""Search window from glade file """
GLADE = os.path.join(utils.globals.GLADE_DIR, "search.glade")
GLADE = os.path.join(lib.globs.GLADE_DIR, "search.glade")
def __init__(self, ctrl):
View.__init__(self, ctrl, self.GLADE)

0
test/unit/__init__.py Normal file
View File

View File

@@ -0,0 +1,28 @@
"""
Project: pyGTKtalog
Description: Tests for DataBase class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-07-19
"""
import unittest
import os
from pygtktalog.dbcommon import connect, Meta, Session, Base
class TestDataBase(unittest.TestCase):
"""
Class responsible for database connection and schema creation
"""
def test_connect(self):
"""
Test connection to database. Memory and file method will be tested.
"""
connect(":memory:")
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

77
test/unit/dialogs_test.py Normal file
View File

@@ -0,0 +1,77 @@
"""
Project: pyGTKtalog
Description: Test simple dialogs
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-05-19
"""
import unittest
import os
import gtk
from pygtktalog.dialogs import Dialog, yesno, okcancel, info, warn, error
class MessageDialogMock(gtk.MessageDialog):
"""Mock class for MessageDialog, which shouldn't be displayed in a test"""
def run(self):
"""Carefull! only for MESSAGE_INFO return value is RESPONSE_OK!"""
if self.get_property('message-type') == gtk.MESSAGE_INFO:
return gtk.RESPONSE_OK
else:
return gtk.RESPONSE_CANCEL
class TestDialog(unittest.TestCase):
"""Tests for Dialog class"""
def test_dialog_create(self):
"""Test dialog creation and run method"""
# overwrite MessageDialog class in gtk module
gtk.MessageDialog = MessageDialogMock
dialog = Dialog(gtk.MESSAGE_INFO, 'msg', 'secondarymsg', 'title')
self.assertTrue(dialog.buttons == gtk.BUTTONS_OK, "dialog should have"
" gtk.BUTTONS_OK")
self.assertTrue(dialog.run(), "dialog should return True")
dialog = Dialog(gtk.MESSAGE_QUESTION, 'msg', 'secondarymsg', 'title')
self.assertFalse(dialog.run(), "dialog should return False")
# NOTE: dialog should be run before test against buttons attribute
self.assertTrue(dialog.buttons == gtk.BUTTONS_YES_NO,
"dialog should have gtk.BUTTONS_YES_NO")
dialog = Dialog(gtk.MESSAGE_QUESTION, 'msg', 'secondarymsg', 'title')
dialog.buttons = gtk.BUTTONS_OK
dialog.ok_default = True
self.assertFalse(dialog.run(), "dialog should return True")
def test_error(self):
"""Test error function"""
result = error('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_warn(self):
"""Test warn function"""
result = warn('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_info(self):
"""Test info function"""
result = info('msg', 'secondarymsg', 'title')
self.assertTrue(result, "Should return True")
def test_yesno(self):
"""Test yesno function"""
result = yesno('msg', 'secondarymsg', 'title')
self.assertFalse(result, "Should return False")
def test_okcancel(self):
"""Test yesno function"""
result = okcancel('msg', 'secondarymsg', 'title')
self.assertFalse(result, "Should return False")
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

View File

@@ -0,0 +1,8 @@
"""
Project: pyGTKtalog
Description: Tests for main view class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2010-03-14
"""

33
test/unit/misc_test.py Normal file
View File

@@ -0,0 +1,33 @@
"""
Project: pyGTKtalog
Description: Tests for misc functions.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2009-04-09
"""
import unittest
import os
import pygtktalog.misc as pgtkmisc
class TestMiscModule(unittest.TestCase):
"""
Tests functions from misc module
"""
def test_float_to_string(self):
"""
test conversion between digits to formated output
"""
self.assertEqual(pgtkmisc.float_to_string(10), '00:00:10')
self.assertEqual(pgtkmisc.float_to_string(76), '00:01:16')
self.assertEqual(pgtkmisc.float_to_string(22222), '06:10:22')
self.assertRaises(TypeError, pgtkmisc.float_to_string)
self.assertRaises(TypeError, pgtkmisc.float_to_string, None)
self.assertRaises(TypeError, pgtkmisc.float_to_string, '10')
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

124
test/unit/scan_test.py Normal file
View File

@@ -0,0 +1,124 @@
"""
Project: pyGTKtalog
Description: Tests for scan files.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2011-03-26
"""
import os
import unittest
from pygtktalog import scan
from pygtktalog.dbobjects import File
from pygtktalog.dbcommon import connect, Session
TEST_DIR = "/home/share/_test_/test_dir"
TEST_DIR_PERMS = "/home/share/_test_/test_dir_permissions/"
class TestScan(unittest.TestCase):
"""
Test cases for scan functionality
1. execution scan function:
1.1 simple case - should pass
1.2 non-existent directory passed
1.3 file passed
1.4 directory has permission that forbids file listing
2. rescan directory; looking for changes
2.0 don't touch records for changed files (same directories, same
filename, same type and size)
2.1 search for files of the same type, same size.
2.2 change parent node for moved files (don't insert new)
3. adding new directory tree which contains same files like already stored
in the database
"""
def setUp(self):
connect()
root = File()
root.id = 1
root.filename = 'root'
root.size = 0
root.source = 0
root.type = 0
root.parent_id = 1
sess = Session()
sess.add(root)
sess.commit()
def test_happy_scenario(self):
"""
make scan, count items
"""
scanob = scan.Scan(os.path.abspath(os.path.join(__file__,
"../../../mocks")))
scanob = scan.Scan(TEST_DIR)
result_list = scanob.add_files()
self.assertEqual(len(result_list), 143)
self.assertEqual(len(result_list[0].children), 8)
# check soft links
self.assertEqual(len([x for x in result_list if x.type == 3]), 2)
def test_wrong_and_nonexistent(self):
"""
Check for accessing non existent directory, regular file instead of
the directory, or file.directory with no access to it.
"""
scanobj = scan.Scan('/nonexistent_directory_')
self.assertRaises(OSError, scanobj.add_files)
scanobj.path = '/root'
self.assertRaises(scan.NoAccessError, scanobj.add_files)
scanobj.path = '/bin/sh'
self.assertRaises(scan.NoAccessError, scanobj.add_files)
# dir contains some non accessable items. Should just pass, and on
# logs should be messages about it
scanobj.path = TEST_DIR_PERMS
scanobj.add_files()
def test_abort_functionality(self):
scanobj = scan.Scan(TEST_DIR)
scanobj.abort = True
self.assertEqual(None, scanobj.add_files())
def test_double_scan(self):
"""
Do the scan twice.
"""
ses = Session()
self.assertEqual(len(ses.query(File).all()), 1)
scanob = scan.Scan(TEST_DIR)
scanob.add_files()
# note: we have 144 elements in db, because of root element
self.assertEqual(len(ses.query(File).all()), 144)
scanob2 = scan.Scan(TEST_DIR)
scanob2.add_files()
# it is perfectly ok, since we don't update collection, but just added
# same directory twice.
self.assertEqual(len(ses.query(File).all()), 287)
file_ob = scanob._files[2]
file2_ob = scanob2._files[2]
# File objects are different
self.assertTrue(file_ob is not file2_ob)
# While Image objects points to the same file
self.assertTrue(file_ob.images[0].filename == \
file2_ob.images[0].filename)
# they are different objects
self.assertTrue(file_ob.images[0] is not file2_ob.images[0])
ses.close()
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()

133
test/unit/video_test.py Normal file
View File

@@ -0,0 +1,133 @@
"""
Project: pyGTKtalog
Description: Tests for Video class.
Type: test
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
Created: 2008-12-15
"""
import unittest
import os
from pygtktalog.video import Video
class TestVideo(unittest.TestCase):
"""Class for retrive midentify script output and put it in dict.
Usually there is no need for such a detailed movie/clip information.
Video script belongs to mplayer package.
"""
def test_avi(self):
"""test mock avi file, should return dict with expected values"""
avi = Video("mocks/m.avi")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'xvid')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffodivx')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_avi2(self):
"""test another mock avi file, should return dict with expected
values"""
avi = Video("mocks/m1.avi")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '85')
self.assertEqual(avi.tags['width'], 128)
self.assertEqual(avi.tags['audio_no_channels'], 2)
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'h264')
self.assertEqual(avi.tags['length'], 4)
self.assertEqual(avi.tags['audio_codec'], 'mp3')
self.assertEqual(avi.tags['video_codec'], 'ffh264')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertEqual(avi.tags['container'], 'avi')
def test_mkv(self):
"""test mock mkv file, should return dict with expected values"""
avi = Video("mocks/m.mkv")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 128)
self.assertTrue(avi.tags['audio_no_channels'] in (1, 2))
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], 'mp4v')
self.assertEqual(avi.tags['length'], 4)
self.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3'))
self.assertEqual(avi.tags['video_codec'], 'ffodivx')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertTrue(avi.tags['container'] in ('mkv', 'lavfpref'))
def test_mpg(self):
"""test mock mpg file, should return dict with expected values"""
avi = Video("mocks/m.mpg")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertFalse(avi.tags.has_key('audio_format'))
self.assertEqual(avi.tags['width'], 128)
self.assertFalse(avi.tags.has_key('audio_no_channels'))
self.assertEqual(avi.tags['height'], 96)
self.assertEqual(avi.tags['video_format'], '0x10000001')
self.assertFalse(avi.tags.has_key('lenght'))
self.assertFalse(avi.tags.has_key('audio_codec'))
self.assertEqual(avi.tags['video_codec'], 'ffmpeg1')
self.assertFalse(avi.tags.has_key('duration'))
self.assertEqual(avi.tags['container'], 'mpeges')
def test_ogm(self):
"""test mock ogm file, should return dict with expected values"""
avi = Video("mocks/m.ogm")
self.assertTrue(len(avi.tags) != 0, "result should have lenght > 0")
self.assertEqual(avi.tags['audio_format'], '8192')
self.assertEqual(avi.tags['width'], 160)
self.assertTrue(avi.tags['audio_no_channels'] in (1, 2))
self.assertEqual(avi.tags['height'], 120)
self.assertEqual(avi.tags['video_format'], 'h264')
self.assertEqual(avi.tags['length'], 4)
self.assertTrue(avi.tags['audio_codec'] in ('a52', 'ffac3'))
self.assertEqual(avi.tags['video_codec'], 'ffh264')
self.assertEqual(avi.tags['duration'], '00:00:04')
self.assertTrue(avi.tags['container'] in ('ogg', 'lavfpref'))
def test_capture(self):
"""test capture with some small movie and play a little with tags"""
avi = Video("mocks/m.avi")
filename = avi.capture()
self.assertTrue(filename != None)
self.assertTrue(os.path.exists(filename))
file_size = os.stat(filename)[6]
self.assertAlmostEqual(file_size/10000.0, 0.9, 0)
os.unlink(filename)
for length in (480, 380, 4):
avi.tags['length'] = length
filename = avi.capture()
self.assertTrue(filename is not None)
os.unlink(filename)
avi.tags['length'] = 3
self.assertTrue(avi.capture() is None)
avi.tags['length'] = 4
avi.tags['width'] = 0
self.assertTrue(avi.capture() is None)
avi.tags['width'] = 1025
filename = avi.capture()
self.assertTrue(filename is not None)
os.unlink(filename)
del(avi.tags['length'])
self.assertTrue(avi.capture() is None)
self.assertTrue(len(str(avi)) > 0)
if __name__ == "__main__":
os.chdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), "../"))
unittest.main()