mirror of
https://github.com/gryf/pygtktalog.git
synced 2026-03-26 22:03:30 +01:00
Compare commits
89 Commits
legacy
...
c257d6ceeb
| Author | SHA1 | Date | |
|---|---|---|---|
| c257d6ceeb | |||
| 28499868d2 | |||
| 5f13fd7d7a | |||
| a1a17158bb | |||
| 10e7e87031 | |||
| 3141add678 | |||
| dadeebe8a1 | |||
| 6c6f01781a | |||
| 07690f9c94 | |||
| 28a99b0470 | |||
| 25740ea1dc | |||
| 33a8f99d48 | |||
| a87da6b27c | |||
| 577b93b534 | |||
| f611dce4e1 | |||
| d003cecc9c | |||
| fe0b66f1ef | |||
| 5d9c90e4ad | |||
| b22fbd5864 | |||
| 22d7e62357 | |||
| bb5928a9f6 | |||
| 35f01b1e9f | |||
| efab8b4152 | |||
| 287dcb3dc6 | |||
| 50a6847762 | |||
| 95ea6b023c | |||
| 8e08319775 | |||
| 63f2d6fc11 | |||
| 4eac6820c5 | |||
| 9cc2408868 | |||
| 15e3aaeabf | |||
| 43a40014c1 | |||
| ad1703cd90 | |||
| 3e2634bc57 | |||
| 3c6c9a552a | |||
| 54b6a377bf | |||
| 22c24fbaf7 | |||
| 62ab67ecc5 | |||
| 7417b9e98e | |||
| 1cf1390567 | |||
| 5db02183a5 | |||
| 3590f90751 | |||
| 2b5b53ada1 | |||
| 7536e2c60a | |||
| 3b0cb80407 | |||
| 9769dfdb76 | |||
| dbb01acd3f | |||
| 9b7f15122d | |||
| 1cd6ad5b84 | |||
| 58c0c1ecdc | |||
| 20501fcf54 | |||
| 5e83363fe7 | |||
| 313db80101 | |||
| 3f797b0bf8 | |||
| 8d6cb75b8e | |||
| 71162da225 | |||
| 6b1fdb90e9 | |||
| c1dd854f62 | |||
| 2b47d9b869 | |||
| 53c3a444e0 | |||
| b54aa1849b | |||
| 37f06726b4 | |||
| e93b7291b8 | |||
| ca7fdf15e8 | |||
| 7b5c76f1d9 | |||
| f0f8d27d19 | |||
| b493b66ea8 | |||
| 83b9d944cf | |||
| 5ccdd8ee6f | |||
| 8013eec7d2 | |||
| 14f654251d | |||
| 8a24e30bde | |||
| bcacb75229 | |||
| d04a84c72d | |||
| e69e2300e8 | |||
| 08c38bf63d | |||
| 52b293c459 | |||
| bdf059d11f | |||
| ccec14f3ea | |||
| 36c4e7e4f2 | |||
| b0964ca031 | |||
| efaccd8902 | |||
| 434df58b16 | |||
| 56c77ae9a4 | |||
| c46d29a5bb | |||
| 5e8c33f05a | |||
| fb920f58bc | |||
| 292d290723 | |||
| 0adcdaba8d |
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.coverage
|
||||||
|
.tox
|
||||||
|
tags
|
||||||
|
MANIFEST
|
||||||
|
.cache
|
||||||
|
pygtktalog.egg-info
|
||||||
122
README
122
README
@@ -1,122 +0,0 @@
|
|||||||
pyGTKtalog 1.0
|
|
||||||
==================
|
|
||||||
|
|
||||||
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,
|
|
||||||
which seems to be dead project for years.
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
- 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)
|
|
||||||
|
|
||||||
Optional modules:
|
|
||||||
|
|
||||||
- PIL <http://www.pythonware.com/products/pil/index.htm> for image manipulation
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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).
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
./pyGTKtalog
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
selected
|
|
||||||
|
|
||||||
Removed:
|
|
||||||
- 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.
|
|
||||||
|
|
||||||
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!
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
BUGS
|
|
||||||
====
|
|
||||||
|
|
||||||
All bugs please report to Roman 'gryf' Dobosz <roman.dobosz@gmail.com>
|
|
||||||
|
|
||||||
90
README.rst
Normal file
90
README.rst
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
pycatalog
|
||||||
|
==========
|
||||||
|
|
||||||
|
Pycatalog is a commandline Linux/FreeBSD program for indexing CD, DVD, BR or
|
||||||
|
directories on filesystem. It is similar to `gtktalog`_ or `gwhere`_. There is
|
||||||
|
no coincidence in name of application, because it's meant to be replacement
|
||||||
|
(in some way) for gtktalog, which seems to be dead project for years.
|
||||||
|
|
||||||
|
Note, that even if it share same code base with pyGTKtalog, which was meant to
|
||||||
|
be desktop application, now pycatalog is pure console app, just for use with
|
||||||
|
commandline. You can find last version of pyGTKtalog under ``pyGTKtalog``
|
||||||
|
branch, although bear in mind, that it was written with `python 2.7`_ and
|
||||||
|
pyGTK_, which both are dead now.
|
||||||
|
|
||||||
|
Current version is 3.0.
|
||||||
|
|
||||||
|
Features
|
||||||
|
--------
|
||||||
|
|
||||||
|
* Scan for files in selected media
|
||||||
|
* Support for grouping files depending on file name (expected patterns in file
|
||||||
|
names)
|
||||||
|
* Store selected EXIF tags
|
||||||
|
* Add/edit description and notes
|
||||||
|
* Fetch comments for images made in `gThumb`_
|
||||||
|
* `Tagging files`_
|
||||||
|
* And more :)
|
||||||
|
|
||||||
|
Requirements
|
||||||
|
------------
|
||||||
|
|
||||||
|
pycatalog requires python and following libraries:
|
||||||
|
|
||||||
|
* `python 3.10`_ and up
|
||||||
|
* `sqlalchemy 1.4`_
|
||||||
|
* `exifread`_ for parse EXIF information
|
||||||
|
|
||||||
|
Pycatalog extensively uses external programs in unix spirit, however there is
|
||||||
|
small possibility of using it Windows (probably with limitations) and quite big
|
||||||
|
possibility to run it on other sophisticated unix-like systems (i.e.
|
||||||
|
BeOS/ZETA/Haiku, QNX or MacOSX).
|
||||||
|
|
||||||
|
Programs that are used:
|
||||||
|
* ``midentify`` (provided by `mplayer`_ package)
|
||||||
|
|
||||||
|
For development process following programs are used:
|
||||||
|
|
||||||
|
* `nose`_
|
||||||
|
* `coverage`_
|
||||||
|
* `tox`_
|
||||||
|
|
||||||
|
Instalation
|
||||||
|
-----------
|
||||||
|
|
||||||
|
You don't have to install it if you don't want to. You can just change current
|
||||||
|
directory to pycatalog and simply run::
|
||||||
|
|
||||||
|
$ paver run
|
||||||
|
|
||||||
|
That's it. Alternatively, if you like to put it in more system wide place, all
|
||||||
|
you have to do is:
|
||||||
|
|
||||||
|
#. put pycatalog directory into your destination of choice (/usr/local/share,
|
||||||
|
/opt or ~/ is typical bet)
|
||||||
|
|
||||||
|
#. copy pycatalog shell script to /usr/bin, /usr/local/bin or in
|
||||||
|
other place, where PATH variable is pointing or you feel like.
|
||||||
|
|
||||||
|
#. then modify pycatalog line 6 to match right ``pycatalog.py`` directory
|
||||||
|
|
||||||
|
Then, just run pycatalog script.
|
||||||
|
|
||||||
|
LICENSE
|
||||||
|
=======
|
||||||
|
|
||||||
|
This work is licensed under the terms of the GNU GPL, version 3. See the LICENCE
|
||||||
|
file in top-level directory.
|
||||||
|
|
||||||
|
|
||||||
|
.. _coverage: http://nedbatchelder.com/code/coverage/
|
||||||
|
.. _exifread: https://github.com/ianare/exif-py
|
||||||
|
.. _gthumb: http://gthumb.sourceforge.net
|
||||||
|
.. _gtktalog: http://www.nongnu.org/gtktalog/
|
||||||
|
.. _gwhere: http://www.gwhere.org/home.php3
|
||||||
|
.. _mplayer: http://mplayerhq.hu
|
||||||
|
.. _nose: http://code.google.com/p/python-nose/
|
||||||
|
.. _python 3.10: http://www.python.org/
|
||||||
|
.. _sqlalchemy 1.4: http://www.sqlalchemy.org
|
||||||
|
.. _tagging files: http://en.wikipedia.org/wiki/tag_%28metadata%29
|
||||||
|
.. _tox: https://testrun.org/tox
|
||||||
@@ -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 '{}' ';'
|
|
||||||
@@ -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()
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB |
Binary file not shown.
Binary file not shown.
11
pyGTKtalog
11
pyGTKtalog
@@ -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 "$@"
|
|
||||||
520
pycatalog/__init__.py
Normal file
520
pycatalog/__init__.py
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
"""
|
||||||
|
Fast and ugly CLI interface
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from sqlalchemy import or_
|
||||||
|
|
||||||
|
from pycatalog import scan
|
||||||
|
from pycatalog import misc
|
||||||
|
from pycatalog import dbobjects as dbo
|
||||||
|
from pycatalog.dbcommon import connect, Session
|
||||||
|
from pycatalog import logger
|
||||||
|
|
||||||
|
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
|
||||||
|
|
||||||
|
RESET_SEQ = '\033[0m'
|
||||||
|
COLOR_SEQ = '\033[1;%dm'
|
||||||
|
BOLD_SEQ = '\033[1m'
|
||||||
|
|
||||||
|
LOG = logger.get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def colorize(txt, color):
|
||||||
|
"""Pretty print with colors to console."""
|
||||||
|
color_map = {'black': BLACK,
|
||||||
|
'red': RED,
|
||||||
|
'green': GREEN,
|
||||||
|
'yellow': YELLOW,
|
||||||
|
'blue': BLUE,
|
||||||
|
'magenta': MAGENTA,
|
||||||
|
'cyan': CYAN,
|
||||||
|
'white': WHITE}
|
||||||
|
return COLOR_SEQ % color_map[color] + txt + RESET_SEQ
|
||||||
|
|
||||||
|
|
||||||
|
def asserdb(func):
|
||||||
|
def wrapper(args):
|
||||||
|
if not os.path.exists(args.db):
|
||||||
|
print(colorize("File `%s' does not exists!" % args.db, 'red'))
|
||||||
|
sys.exit(1)
|
||||||
|
func(args)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
TYPE_MAP = {0: 'd', 1: 'd', 2: 'f', 3: 'l'}
|
||||||
|
|
||||||
|
|
||||||
|
class Iface(object):
|
||||||
|
"""Main class which interacts with the pyGTKtalog modules"""
|
||||||
|
def __init__(self, dbname, pretend=False, debug=False):
|
||||||
|
"""Init"""
|
||||||
|
self.engine = connect(dbname)
|
||||||
|
self.sess = Session()
|
||||||
|
self.dry_run = pretend
|
||||||
|
self.root = None
|
||||||
|
self._dbname = dbname
|
||||||
|
if debug:
|
||||||
|
scan.LOG.setLevel('DEBUG')
|
||||||
|
LOG.setLevel('DEBUG')
|
||||||
|
|
||||||
|
def _resolve_path(self, path):
|
||||||
|
"""Identify path in the DB"""
|
||||||
|
if not path.startswith("/"):
|
||||||
|
raise AttributeError('Path have to start with slash (/)')
|
||||||
|
|
||||||
|
last_node = self.root
|
||||||
|
for part in path.split('/'):
|
||||||
|
if not part.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for node in last_node.children:
|
||||||
|
if node.filename == part:
|
||||||
|
last_node = node
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AttributeError('No such path: %s' % path)
|
||||||
|
|
||||||
|
return last_node
|
||||||
|
|
||||||
|
def _get_full_path(self, file_object):
|
||||||
|
"""given the file object, return string with full path to it"""
|
||||||
|
parent = file_object.parent
|
||||||
|
path = [file_object.filename]
|
||||||
|
|
||||||
|
while parent.type:
|
||||||
|
path.insert(0, parent.filename)
|
||||||
|
parent = parent.parent
|
||||||
|
|
||||||
|
return u'/' + u'/'.join(path)
|
||||||
|
|
||||||
|
def _make_path(self, node):
|
||||||
|
"""Make the path to the item in the DB"""
|
||||||
|
orig_node = node
|
||||||
|
if node.parent == node:
|
||||||
|
return {u'/': (u' ', 0, u' ')}
|
||||||
|
|
||||||
|
ext = ''
|
||||||
|
if node.parent.type == dbo.TYPE['root']:
|
||||||
|
ext = colorize(' (%s)' % node.filepath, 'white')
|
||||||
|
|
||||||
|
path = []
|
||||||
|
path.append(node.filename)
|
||||||
|
while node.parent != self.root:
|
||||||
|
path.append(node.parent.filename)
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
path = '/'.join([''] + path[::-1]) + ext
|
||||||
|
|
||||||
|
return {path: (TYPE_MAP[orig_node.type],
|
||||||
|
orig_node.size,
|
||||||
|
orig_node.date)}
|
||||||
|
|
||||||
|
def _walk(self, dirnode):
|
||||||
|
"""Recursively go through the leaves of the node"""
|
||||||
|
items = {}
|
||||||
|
|
||||||
|
for node in dirnode.children:
|
||||||
|
if node.type == dbo.TYPE['dir']:
|
||||||
|
items.update(self._walk(node))
|
||||||
|
|
||||||
|
items.update(self._make_path(node))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def _list(self, node):
|
||||||
|
"""List only current node content"""
|
||||||
|
items = {}
|
||||||
|
for node in node.children:
|
||||||
|
if node != self.root:
|
||||||
|
items.update(self._make_path(node))
|
||||||
|
|
||||||
|
return items
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""Close the session"""
|
||||||
|
self.sess.commit()
|
||||||
|
self.sess.close()
|
||||||
|
|
||||||
|
def list(self, path=None, recursive=False, long_=False):
|
||||||
|
"""Simulate ls command for the provided item path"""
|
||||||
|
self.root = self.sess.query(dbo.File)
|
||||||
|
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
||||||
|
if path:
|
||||||
|
node = self._resolve_path(path)
|
||||||
|
msg = "Content of path `%s':" % path
|
||||||
|
else:
|
||||||
|
node = self.root
|
||||||
|
msg = "Content of path `/':"
|
||||||
|
|
||||||
|
print(colorize(msg, 'white'))
|
||||||
|
|
||||||
|
if recursive:
|
||||||
|
items = self._walk(node)
|
||||||
|
else:
|
||||||
|
items = self._list(node)
|
||||||
|
|
||||||
|
if long_:
|
||||||
|
filenames = []
|
||||||
|
format_str = (u'{} {:>%d,} {} {}' %
|
||||||
|
_get_highest_size_length(items))
|
||||||
|
for fname in sorted(items.keys()):
|
||||||
|
type_, size, date = items[fname]
|
||||||
|
filenames.append(format_str.format(type_, size, date, fname))
|
||||||
|
else:
|
||||||
|
filenames = sorted(items.keys())
|
||||||
|
|
||||||
|
print('\n'.join(filenames))
|
||||||
|
|
||||||
|
def update(self, path, dir_to_update=None):
|
||||||
|
"""
|
||||||
|
Update the DB against provided path and optionally directory on the
|
||||||
|
real filesystem
|
||||||
|
"""
|
||||||
|
self.root = self.sess.query(dbo.File)
|
||||||
|
self.root = self.root.filter(dbo.File.type == dbo.TYPE['root']).first()
|
||||||
|
node = self._resolve_path(path)
|
||||||
|
if node == self.root:
|
||||||
|
print(colorize('Cannot update entire db, since root was provided '
|
||||||
|
'as path.', 'red'))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not dir_to_update:
|
||||||
|
dir_to_update = os.path.join(node.filepath, node.filename)
|
||||||
|
|
||||||
|
if not os.path.exists(dir_to_update):
|
||||||
|
raise OSError("Path to updtate doesn't exists: %s", dir_to_update)
|
||||||
|
|
||||||
|
print(colorize("Updating node `%s' against directory "
|
||||||
|
"`%s'" % (path, dir_to_update), 'white'))
|
||||||
|
if not self.dry_run:
|
||||||
|
scanob = scan.Scan(dir_to_update)
|
||||||
|
# scanob.update_files(node.id)
|
||||||
|
scanob.update_files(node.id, self.engine)
|
||||||
|
|
||||||
|
def create(self, dir_to_add, data_dir):
|
||||||
|
"""Create new database"""
|
||||||
|
self.root = dbo.File()
|
||||||
|
self.root.id = 1
|
||||||
|
self.root.filename = 'root'
|
||||||
|
self.root.size = 0
|
||||||
|
self.root.source = 0
|
||||||
|
self.root.type = 0
|
||||||
|
self.root.parent_id = 1
|
||||||
|
|
||||||
|
config = dbo.Config()
|
||||||
|
config.key = 'image_path'
|
||||||
|
config.value = data_dir
|
||||||
|
|
||||||
|
if not self.dry_run:
|
||||||
|
self.sess.add(self.root)
|
||||||
|
self.sess.add(config)
|
||||||
|
self.sess.commit()
|
||||||
|
|
||||||
|
print(colorize("Creating new db against directory `%s'" % dir_to_add,
|
||||||
|
'white'))
|
||||||
|
if not self.dry_run:
|
||||||
|
if data_dir == ':same_as_db:':
|
||||||
|
misc.calculate_image_path(None, True)
|
||||||
|
else:
|
||||||
|
misc.calculate_image_path(data_dir, True)
|
||||||
|
|
||||||
|
scanob = scan.Scan(dir_to_add)
|
||||||
|
scanob.add_files(self.engine)
|
||||||
|
|
||||||
|
def add(self, dir_to_add):
|
||||||
|
"""Add new directory to the db"""
|
||||||
|
self.root = self.sess.query(dbo.File)
|
||||||
|
self.root = self.root.filter(dbo.File.type == 0).first()
|
||||||
|
|
||||||
|
if not os.path.exists(dir_to_add):
|
||||||
|
raise OSError("Path to add doesn't exists: %s", dir_to_add)
|
||||||
|
|
||||||
|
print(colorize("Adding directory `%s'" % dir_to_add, 'white'))
|
||||||
|
if not self.dry_run:
|
||||||
|
scanob = scan.Scan(dir_to_add)
|
||||||
|
scanob.add_files()
|
||||||
|
|
||||||
|
def _annotate(self, item, search_words):
|
||||||
|
"""
|
||||||
|
Find ranges to be highlighted in item, provide them and return result
|
||||||
|
string
|
||||||
|
"""
|
||||||
|
indexes = []
|
||||||
|
for word in search_words:
|
||||||
|
for match in re.finditer(re.escape(word.lower()), item.lower()):
|
||||||
|
for index in range(match.start(), match.end()):
|
||||||
|
indexes.append(index)
|
||||||
|
|
||||||
|
highlight = False
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for idx, char in enumerate(item):
|
||||||
|
if idx in indexes:
|
||||||
|
if not highlight:
|
||||||
|
highlight = True
|
||||||
|
result.append(COLOR_SEQ % WHITE)
|
||||||
|
result.append(char)
|
||||||
|
else:
|
||||||
|
if highlight:
|
||||||
|
highlight = False
|
||||||
|
result.append(RESET_SEQ)
|
||||||
|
result.append(char)
|
||||||
|
|
||||||
|
return "".join(result)
|
||||||
|
|
||||||
|
def find(self, search_words):
|
||||||
|
query = self.sess.query(dbo.File).filter(or_(dbo.File.type == 2,
|
||||||
|
dbo.File.type == 3))
|
||||||
|
result = []
|
||||||
|
|
||||||
|
for word in search_words:
|
||||||
|
phrase = u'%%%s%%' % word
|
||||||
|
query = query.filter(dbo.File.filename.like(phrase))
|
||||||
|
|
||||||
|
for item in query.all():
|
||||||
|
result.append(self._get_full_path(item))
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
print("No results for `%s'" % ' '.join(search_words))
|
||||||
|
return
|
||||||
|
|
||||||
|
result.sort()
|
||||||
|
for item in result:
|
||||||
|
print(self._annotate(item, search_words))
|
||||||
|
|
||||||
|
def fsck(self):
|
||||||
|
"""Fsck orphaned images/thumbs"""
|
||||||
|
image_path = (self.sess.query(dbo.Config)
|
||||||
|
.filter(dbo.Config.key == 'image_path')).one().value
|
||||||
|
|
||||||
|
if image_path == ':same_as_db:':
|
||||||
|
image_path = misc.calculate_image_path(None, False)
|
||||||
|
|
||||||
|
files_to_remove = []
|
||||||
|
|
||||||
|
# remove images/thumbnails which doesn't have file relation
|
||||||
|
for name, obj in (("images", dbo.Image),
|
||||||
|
("thumbnails", dbo.Thumbnail)):
|
||||||
|
self._purge_orphaned_objects(obj, "Scanning %s " % name)
|
||||||
|
|
||||||
|
# find all image files not associate with either Image (image/thumb)
|
||||||
|
# or Thumbnail (thumb) objects
|
||||||
|
sys.stdout.write(40 * " " + "\r")
|
||||||
|
count = 0
|
||||||
|
for root, dirs, files in os.walk(image_path):
|
||||||
|
for fname in files:
|
||||||
|
sys.stdout.write("Scanning files " +
|
||||||
|
"| / - \\".split()[count % 4] + "\r")
|
||||||
|
sys.stdout.flush()
|
||||||
|
count += 1
|
||||||
|
|
||||||
|
fname_ = os.path.join(root.split(image_path)[1],
|
||||||
|
fname).lstrip('/')
|
||||||
|
|
||||||
|
if '_t' in fname:
|
||||||
|
obj = (self.sess.query(dbo.Thumbnail)
|
||||||
|
.filter(dbo.Thumbnail.filename == fname_)).all()
|
||||||
|
if obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
obj = (self.sess.query(dbo.Image)
|
||||||
|
.filter(dbo.Image.filename ==
|
||||||
|
fname_.replace('_t.', '.'))).all()
|
||||||
|
if obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
obj = (self.sess.query(dbo.Image)
|
||||||
|
.filter(dbo.Image.filename == fname_)).all()
|
||||||
|
if obj:
|
||||||
|
continue
|
||||||
|
|
||||||
|
files_to_remove.append(os.path.join(root, fname))
|
||||||
|
|
||||||
|
LOG.debug("Found %d orphaned files", len(files_to_remove))
|
||||||
|
sys.stdout.write(40 * " " + "\r")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
if self.dry_run:
|
||||||
|
print("Following files are not associated to any items in the DB:")
|
||||||
|
for filename in sorted(files_to_remove):
|
||||||
|
print(filename)
|
||||||
|
self.sess.rollback()
|
||||||
|
else:
|
||||||
|
_remove_files(image_path, files_to_remove)
|
||||||
|
self.sess.commit()
|
||||||
|
|
||||||
|
def _purge_orphaned_objects(self, sa_class, msg):
|
||||||
|
"""Return tuple of lists of images that are orphaned"""
|
||||||
|
|
||||||
|
ids_to_remove = []
|
||||||
|
|
||||||
|
for count, item in enumerate(self.sess.query(sa_class).all()):
|
||||||
|
sys.stdout.write(msg + "| / - \\".split()[count % 4] + "\r")
|
||||||
|
if not item.file:
|
||||||
|
self.sess.delete(item)
|
||||||
|
ids_to_remove.append(item.id)
|
||||||
|
del item
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
LOG.debug("Found %d orphaned object of class %s",
|
||||||
|
len(ids_to_remove), sa_class.__name__)
|
||||||
|
self.sess.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_files(image_path, filenames):
|
||||||
|
"""Remove files and empty directories in provided location"""
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for count, fname in enumerate(filenames, start=1):
|
||||||
|
os.unlink(fname)
|
||||||
|
|
||||||
|
LOG.info("Removed %d orphaned files", count)
|
||||||
|
|
||||||
|
count = 0
|
||||||
|
for root, dirs, _ in os.walk(image_path):
|
||||||
|
for dirname in dirs:
|
||||||
|
try:
|
||||||
|
os.rmdir(os.path.join(root, dirname))
|
||||||
|
count += 1
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
LOG.info("Removed %d empty directories", count)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_highest_size_length(item_dict):
|
||||||
|
highest = len(str(sorted([i[1] for i in item_dict.values()])[-1]))
|
||||||
|
return highest + highest / 3
|
||||||
|
|
||||||
|
|
||||||
|
@asserdb
|
||||||
|
def list_db(args):
|
||||||
|
"""List"""
|
||||||
|
obj = Iface(args.db, False, args.debug)
|
||||||
|
obj.list(path=args.path, recursive=args.recursive, long_=args.long)
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
@asserdb
|
||||||
|
def update_db(args):
|
||||||
|
"""Update"""
|
||||||
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
|
obj.update(args.path, dir_to_update=args.dir_to_update)
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
@asserdb
|
||||||
|
def add_dir(args):
|
||||||
|
"""Add"""
|
||||||
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
|
obj.add(args.dir_to_add)
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
def create_db(args):
|
||||||
|
"""List"""
|
||||||
|
__import__('pdb').set_trace()
|
||||||
|
obj = Iface(args.db, args.pretend, args.debug)
|
||||||
|
obj.create(args.dir_to_add, args.imagedir)
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
@asserdb
|
||||||
|
def search(args):
|
||||||
|
"""Find"""
|
||||||
|
obj = Iface(args.db, False, args.debug)
|
||||||
|
obj.find(args.search_words)
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
@asserdb
|
||||||
|
def cleanup(args):
|
||||||
|
"""Cleanup"""
|
||||||
|
obj = Iface(args.db, False, args.debug)
|
||||||
|
obj.fsck()
|
||||||
|
obj.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main"""
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
|
||||||
|
subparser = parser.add_subparsers()
|
||||||
|
list_ = subparser.add_parser('list')
|
||||||
|
list_.add_argument('db')
|
||||||
|
list_.add_argument('path', nargs='?')
|
||||||
|
list_.add_argument('-l', '--long', help='Show size, date and type',
|
||||||
|
action='store_true', default=False)
|
||||||
|
list_.add_argument('-r', '--recursive', help='list items in '
|
||||||
|
'subdirectories', action='store_true', default=False)
|
||||||
|
list_.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
list_.set_defaults(func=list_db)
|
||||||
|
|
||||||
|
update = subparser.add_parser('update')
|
||||||
|
update.add_argument('db')
|
||||||
|
update.add_argument('path')
|
||||||
|
update.add_argument('dir_to_update', nargs='?')
|
||||||
|
update.add_argument('-p', '--pretend', help="Don't do the action, just "
|
||||||
|
"give the info what would gonna to happen.",
|
||||||
|
action='store_true', default=False)
|
||||||
|
update.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
update.set_defaults(func=update_db)
|
||||||
|
|
||||||
|
create = subparser.add_parser('create')
|
||||||
|
create.add_argument('db')
|
||||||
|
create.add_argument('dir_to_add')
|
||||||
|
create.add_argument('-i', '--imagedir', help="Directory where to put "
|
||||||
|
"images for the database. Popular, but deprecated "
|
||||||
|
"choice is `~/.pycatalog/images'. Current default "
|
||||||
|
"is special string `:same_as_db:' which will try to "
|
||||||
|
"create directory with the same name as the db with "
|
||||||
|
"data suffix", default=':same_as_db:')
|
||||||
|
create.add_argument('-p', '--pretend', help="Don't do the action, just "
|
||||||
|
"give the info what would gonna to happen.",
|
||||||
|
action='store_true', default=False)
|
||||||
|
create.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
create.set_defaults(func=create_db)
|
||||||
|
|
||||||
|
add = subparser.add_parser('add')
|
||||||
|
add.add_argument('db')
|
||||||
|
add.add_argument('dir_to_add')
|
||||||
|
add.add_argument('-p', '--pretend', help="Don't do the action, just "
|
||||||
|
"give the info what would gonna to happen.",
|
||||||
|
action='store_true', default=False)
|
||||||
|
add.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
add.set_defaults(func=add_dir)
|
||||||
|
|
||||||
|
find = subparser.add_parser('find')
|
||||||
|
find.add_argument('db')
|
||||||
|
find.add_argument('search_words', nargs='+')
|
||||||
|
find.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
find.set_defaults(func=search)
|
||||||
|
|
||||||
|
fsck = subparser.add_parser('fsck')
|
||||||
|
fsck.add_argument('db')
|
||||||
|
fsck.add_argument('-p', '--pretend', help="Don't do the action, just give"
|
||||||
|
" the info what would gonna to happen.",
|
||||||
|
action='store_true', default=False)
|
||||||
|
fsck.add_argument('-d', '--debug', help='Turn on debug',
|
||||||
|
action='store_true', default=False)
|
||||||
|
fsck.set_defaults(func=cleanup)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if 'func' in args:
|
||||||
|
args.func(args)
|
||||||
|
else:
|
||||||
|
parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
43
pycatalog/dbcommon.py
Normal file
43
pycatalog/dbcommon.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
"""
|
||||||
|
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 pycatalog.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
# Prepare SQLAlchemy objects
|
||||||
|
Meta = MetaData()
|
||||||
|
Base = declarative_base(metadata=Meta)
|
||||||
|
Session = sessionmaker()
|
||||||
|
DbFilename = None
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
global DbFilename
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
filename = ':memory:'
|
||||||
|
|
||||||
|
LOG.info("db filename: %s" % filename)
|
||||||
|
DbFilename = filename
|
||||||
|
|
||||||
|
connect_string = "sqlite:///%s" % filename
|
||||||
|
engine = create_engine(connect_string)
|
||||||
|
Meta.bind = engine
|
||||||
|
Meta.create_all(checkfirst=True)
|
||||||
|
return engine
|
||||||
295
pycatalog/dbobjects.py
Normal file
295
pycatalog/dbobjects.py
Normal file
@@ -0,0 +1,295 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from sqlalchemy import Column, Table, Integer, Text
|
||||||
|
from sqlalchemy import DateTime, ForeignKey, Sequence
|
||||||
|
from sqlalchemy.orm import relation, backref
|
||||||
|
|
||||||
|
from pycatalog.dbcommon import Base
|
||||||
|
from pycatalog.thumbnail import ThumbCreator
|
||||||
|
from pycatalog.logger import get_logger
|
||||||
|
from pycatalog.misc import mk_paths
|
||||||
|
|
||||||
|
|
||||||
|
LOG = get_logger(__name__)
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
File mapping. Instances of this object can reference other File object
|
||||||
|
which make the structure to be tree-like
|
||||||
|
"""
|
||||||
|
__tablename__ = "files"
|
||||||
|
id = Column(Integer, Sequence("file_id_seq"), primary_key=True)
|
||||||
|
parent_id = Column(Integer, ForeignKey("files.id"), index=True)
|
||||||
|
filename = Column(Text)
|
||||||
|
filepath = Column(Text)
|
||||||
|
date = Column(DateTime)
|
||||||
|
size = Column(Integer)
|
||||||
|
type = Column(Integer, index=True)
|
||||||
|
source = Column(Integer)
|
||||||
|
note = Column(Text)
|
||||||
|
description = Column(Text)
|
||||||
|
# checksum = Column(Text)
|
||||||
|
|
||||||
|
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", order_by="Image.filename")
|
||||||
|
|
||||||
|
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)>" % (self.filename, str(self.id))
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""TODO: what is this class for?"""
|
||||||
|
__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):
|
||||||
|
"""Tag mapping"""
|
||||||
|
__tablename__ = "tags"
|
||||||
|
id = Column(Integer, Sequence("tags_id_seq"), primary_key=True)
|
||||||
|
group_id = Column(Integer, ForeignKey("groups.id"), index=True)
|
||||||
|
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):
|
||||||
|
"""Thumbnail for the file"""
|
||||||
|
__tablename__ = "thumbnails"
|
||||||
|
id = Column(Integer, Sequence("thumbnail_id_seq"), primary_key=True)
|
||||||
|
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
||||||
|
filename = Column(Text)
|
||||||
|
|
||||||
|
def __init__(self, filename=None, img_path=None, file_obj=None):
|
||||||
|
self.filename = filename
|
||||||
|
self.file = file_obj
|
||||||
|
self.img_path = img_path
|
||||||
|
if filename and file_obj and img_path:
|
||||||
|
self.save(self.filename, img_path)
|
||||||
|
|
||||||
|
def save(self, fname, img_path):
|
||||||
|
"""
|
||||||
|
Create file related thumbnail, add it to the file object.
|
||||||
|
"""
|
||||||
|
new_name = mk_paths(fname, img_path)
|
||||||
|
ext = os.path.splitext(self.filename)[1]
|
||||||
|
if ext:
|
||||||
|
new_name.append("".join([new_name.pop(), ext]))
|
||||||
|
|
||||||
|
thumb = ThumbCreator(self.filename).generate()
|
||||||
|
name, ext = os.path.splitext(new_name.pop())
|
||||||
|
new_name.append("".join([name, "_t", ext]))
|
||||||
|
self.filename = os.path.sep.join(new_name)
|
||||||
|
if not os.path.exists(os.path.join(img_path, *new_name)):
|
||||||
|
shutil.move(thumb, os.path.join(img_path, *new_name))
|
||||||
|
else:
|
||||||
|
LOG.info("Thumbnail already exists (%s: %s)",
|
||||||
|
fname, "/".join(new_name))
|
||||||
|
os.unlink(thumb)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Thumbnail('%s', %s)>" % (str(self.filename), str(self.id))
|
||||||
|
|
||||||
|
|
||||||
|
class Image(Base):
|
||||||
|
"""Images and their thumbnails"""
|
||||||
|
__tablename__ = "images"
|
||||||
|
id = Column(Integer, Sequence("images_id_seq"), primary_key=True)
|
||||||
|
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
||||||
|
filename = Column(Text)
|
||||||
|
|
||||||
|
def __init__(self, filename=None, img_path=None, file_obj=None, move=True):
|
||||||
|
self.filename = None
|
||||||
|
self.file = file_obj
|
||||||
|
self.img_path = img_path
|
||||||
|
if filename and img_path:
|
||||||
|
self.filename = filename
|
||||||
|
self.save(filename, img_path, move)
|
||||||
|
|
||||||
|
def save(self, fname, img_path, move=True):
|
||||||
|
"""
|
||||||
|
Save and create coressponding thumbnail (note: it differs from file
|
||||||
|
related thumbnail!)
|
||||||
|
"""
|
||||||
|
new_name = mk_paths(fname, img_path)
|
||||||
|
ext = os.path.splitext(self.filename)[1]
|
||||||
|
|
||||||
|
if ext:
|
||||||
|
new_name.append("".join([new_name.pop(), ext]))
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.join(img_path, *new_name)):
|
||||||
|
if move:
|
||||||
|
shutil.move(self.filename, os.path.join(img_path, *new_name))
|
||||||
|
else:
|
||||||
|
shutil.copy(self.filename, os.path.join(img_path, *new_name))
|
||||||
|
else:
|
||||||
|
LOG.warning("Image with same CRC already exists "
|
||||||
|
"('%s', '%s')" % (self.filename, "/".join(new_name)))
|
||||||
|
|
||||||
|
self.filename = os.path.sep.join(new_name)
|
||||||
|
|
||||||
|
name, ext = os.path.splitext(new_name.pop())
|
||||||
|
new_name.append("".join([name, "_t", ext]))
|
||||||
|
|
||||||
|
if not os.path.exists(os.path.join(img_path, *new_name)):
|
||||||
|
thumb = ThumbCreator(os.path.join(img_path, self.filename))
|
||||||
|
shutil.move(thumb.generate(), os.path.join(img_path, *new_name))
|
||||||
|
else:
|
||||||
|
LOG.info("Thumbnail already generated %s" % "/".join(new_name))
|
||||||
|
|
||||||
|
def get_copy(self):
|
||||||
|
"""
|
||||||
|
Create the very same object as self with exception of id field
|
||||||
|
"""
|
||||||
|
img = Image()
|
||||||
|
img.filename = self.filename
|
||||||
|
return img
|
||||||
|
|
||||||
|
@property
|
||||||
|
def thumbnail(self):
|
||||||
|
"""
|
||||||
|
Return path to thumbnail for this image
|
||||||
|
"""
|
||||||
|
path, fname = os.path.split(self.filename)
|
||||||
|
base, ext = os.path.splitext(fname)
|
||||||
|
return os.path.join(path, base + "_t" + ext)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Image('%s', %s)>" % (str(self.filename), str(self.id))
|
||||||
|
|
||||||
|
|
||||||
|
class Exif(Base):
|
||||||
|
"""Selected EXIF information"""
|
||||||
|
__tablename__ = "exif"
|
||||||
|
id = Column(Integer, Sequence("exif_id_seq"), primary_key=True)
|
||||||
|
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
||||||
|
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):
|
||||||
|
"""Gthumb information"""
|
||||||
|
__tablename__ = "gthumb"
|
||||||
|
id = Column(Integer, Sequence("gthumb_id_seq"), primary_key=True)
|
||||||
|
file_id = Column(Integer, ForeignKey("files.id"), index=True)
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
class Config(Base):
|
||||||
|
"""Per-database configuration"""
|
||||||
|
__tablename__ = "config"
|
||||||
|
id = Column(Integer, Sequence("config_id_seq"), primary_key=True)
|
||||||
|
key = Column(Text)
|
||||||
|
value = Column(Text)
|
||||||
|
|
||||||
|
def __init__(self, key=None, value=None):
|
||||||
|
self.key = key
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Config('%s', '%s')>" % (str(self.key), str(self.value))
|
||||||
107
pycatalog/logger.py
Normal file
107
pycatalog/logger.py
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
"""
|
||||||
|
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}
|
||||||
|
|
||||||
|
|
||||||
|
def cprint(txt, color):
|
||||||
|
color_map = {"black": BLACK,
|
||||||
|
"red": RED,
|
||||||
|
"green": GREEN,
|
||||||
|
"yellow": YELLOW,
|
||||||
|
"blue": BLUE,
|
||||||
|
"magenta": MAGENTA,
|
||||||
|
"cyan": CYAN,
|
||||||
|
"white": WHITE}
|
||||||
|
print(COLOR_SEQ % (30 + color_map[color]) + txt + RESET_SEQ)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyFormater(logging.Formatter):
|
||||||
|
"""Just don't output anything"""
|
||||||
|
def format(self, record):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
log_obj = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(module_name, level='INFO', to_file=True, to_console=True):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
.pycatalog 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("~"), ".pycatalog", "app.log")
|
||||||
|
|
||||||
|
log = logging.getLogger(module_name)
|
||||||
|
log.setLevel(LEVEL[level])
|
||||||
|
|
||||||
|
if to_console:
|
||||||
|
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)
|
||||||
|
|
||||||
|
elif 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)
|
||||||
|
else:
|
||||||
|
devnull = open(os.devnull, "w")
|
||||||
|
dummy_handler = logging.StreamHandler(devnull)
|
||||||
|
dummy_formatter = DummyFormater("")
|
||||||
|
dummy_handler.setFormatter(dummy_formatter)
|
||||||
|
log.addHandler(dummy_handler)
|
||||||
|
|
||||||
|
return log
|
||||||
77
pycatalog/misc.py
Normal file
77
pycatalog/misc.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""
|
||||||
|
Project: pyGTKtalog
|
||||||
|
Description: Misc functions used more than once in src
|
||||||
|
Type: lib
|
||||||
|
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
|
||||||
|
Created: 2009-04-05
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import errno
|
||||||
|
from zlib import crc32
|
||||||
|
|
||||||
|
import pycatalog.dbcommon
|
||||||
|
from pycatalog.logger import get_logger
|
||||||
|
|
||||||
|
LOG = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_image_path(dbpath=None, create=False):
|
||||||
|
"""Calculate image path out of provided path or using current connection"""
|
||||||
|
if not dbpath:
|
||||||
|
dbpath = pycatalog.dbcommon.DbFilename
|
||||||
|
if dbpath == ":memory:":
|
||||||
|
raise OSError("Cannot create image path out of in-memory db!")
|
||||||
|
|
||||||
|
dir_, file_ = (os.path.dirname(dbpath), os.path.basename(dbpath))
|
||||||
|
file_base, dummy = os.path.splitext(file_)
|
||||||
|
images_dir = os.path.join(dir_, file_base + "_images")
|
||||||
|
else:
|
||||||
|
if dbpath and "~" in dbpath:
|
||||||
|
dbpath = os.path.expanduser(dbpath)
|
||||||
|
if dbpath and "$" in dbpath:
|
||||||
|
dbpath = os.path.expandvars(dbpath)
|
||||||
|
images_dir = dbpath
|
||||||
|
|
||||||
|
if create:
|
||||||
|
if not os.path.exists(images_dir):
|
||||||
|
try:
|
||||||
|
os.mkdir(images_dir)
|
||||||
|
except OSError as err:
|
||||||
|
if err.errno != errno.EEXIST:
|
||||||
|
raise
|
||||||
|
elif not os.path.exists(images_dir):
|
||||||
|
raise OSError("%s: No such directory" % images_dir)
|
||||||
|
|
||||||
|
return os.path.abspath(images_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def mk_paths(fname, img_path):
|
||||||
|
"""Make path for provided pathname by calculating crc32 out of file"""
|
||||||
|
with open(fname, 'r+b') as fobj:
|
||||||
|
new_path = "%x" % (crc32(fobj.read(10*1024*1024)) & 0xffffffff)
|
||||||
|
|
||||||
|
new_path = [new_path[i:i + 2] for i in range(0, len(new_path), 2)]
|
||||||
|
full_path = os.path.join(img_path, *new_path[:-1])
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.makedirs(full_path)
|
||||||
|
except OSError as exc:
|
||||||
|
if exc.errno != errno.EEXIST:
|
||||||
|
LOG.debug("Directory %s already exists." % full_path)
|
||||||
|
|
||||||
|
return new_path
|
||||||
25
pycatalog/pygtkutils.py
Normal file
25
pycatalog/pygtkutils.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Project: pyGTKtalog
|
||||||
|
Description: pyGTK common utility functions
|
||||||
|
Type: utility
|
||||||
|
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
|
||||||
501
pycatalog/scan.py
Normal file
501
pycatalog/scan.py
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
"""
|
||||||
|
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 re
|
||||||
|
from datetime import datetime
|
||||||
|
import mimetypes
|
||||||
|
|
||||||
|
import pycatalog.misc
|
||||||
|
from pycatalog.dbobjects import File, Image, Thumbnail, Config, TYPE
|
||||||
|
from pycatalog.dbcommon import Session
|
||||||
|
from pycatalog.logger import get_logger
|
||||||
|
from pycatalog.video import Video
|
||||||
|
|
||||||
|
|
||||||
|
LOG = get_logger(__name__)
|
||||||
|
RE_FN_START = re.compile(r'(?P<fname_start>'
|
||||||
|
r'(\[[^\]]*\]\s)?'
|
||||||
|
r'([^(]*)\s'
|
||||||
|
r'((\(\d{4}\))\s)?).*'
|
||||||
|
r'(\[[A-Fa-f0-9]{8}\])\..*')
|
||||||
|
|
||||||
|
|
||||||
|
class NoAccessError(Exception):
|
||||||
|
"""No access exception"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Scan(object):
|
||||||
|
"""
|
||||||
|
Retrieve and identify all files recursively on given path
|
||||||
|
"""
|
||||||
|
def __init__(self, path):
|
||||||
|
"""
|
||||||
|
Initialize
|
||||||
|
@Arguments:
|
||||||
|
@path - string with path to be added to topmost node (root)
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
self._set_image_path()
|
||||||
|
|
||||||
|
def add_files(self, engine=None):
|
||||||
|
"""
|
||||||
|
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 get_all_children(self, node_id, engine):
|
||||||
|
"""
|
||||||
|
Get children by pure SQL
|
||||||
|
|
||||||
|
Starting from sqlite 3.8.3 it is possile to do this operation as a
|
||||||
|
one query using WITH statement. For now on it has to be done in
|
||||||
|
application.
|
||||||
|
"""
|
||||||
|
query = "select id from files where parent_id=? and type=1"
|
||||||
|
query2 = "select id from files where parent_id in (%s)"
|
||||||
|
|
||||||
|
row = ((node_id,),)
|
||||||
|
all_ids = []
|
||||||
|
|
||||||
|
def req(obj):
|
||||||
|
"""Requrisve function for gathering all child ids for given node"""
|
||||||
|
for line in obj:
|
||||||
|
all_ids.append(line[0])
|
||||||
|
res = engine.execute(query, (line[0],)).fetchall()
|
||||||
|
if res:
|
||||||
|
req(res)
|
||||||
|
|
||||||
|
req(row)
|
||||||
|
|
||||||
|
sql = query2 % ",".join("?" * len(all_ids))
|
||||||
|
all_ids = [row_[0] for row_ in engine
|
||||||
|
.execute(sql, tuple(all_ids))
|
||||||
|
.fetchall()]
|
||||||
|
|
||||||
|
all_obj = []
|
||||||
|
# number of objects to retrieve at once. Limit is 999. Let's do a
|
||||||
|
# little bit below.
|
||||||
|
num = 900
|
||||||
|
steps = len(all_ids) // num + 1
|
||||||
|
for step in range(steps):
|
||||||
|
all_obj.extend(self._session
|
||||||
|
.query(File)
|
||||||
|
.filter(File.id
|
||||||
|
.in_(all_ids[step * num:step * num + num]))
|
||||||
|
.all())
|
||||||
|
return all_obj
|
||||||
|
|
||||||
|
def update_files(self, node_id, engine=None):
|
||||||
|
"""
|
||||||
|
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 = []
|
||||||
|
|
||||||
|
if engine:
|
||||||
|
LOG.debug("Getting all File objects via SQL")
|
||||||
|
self._existing_branch = self.get_all_children(node_id, engine)
|
||||||
|
else:
|
||||||
|
LOG.debug("Getting all File objects via ORM (yeah, it SLOW)")
|
||||||
|
self._existing_branch = old_node.get_all_children()
|
||||||
|
|
||||||
|
self._existing_branch.insert(0, old_node)
|
||||||
|
|
||||||
|
# Break the chain of parent-children relations
|
||||||
|
LOG.debug("Make them orphans")
|
||||||
|
for fobj in self._existing_branch:
|
||||||
|
fobj.parent = None
|
||||||
|
|
||||||
|
update_path = os.path.join(old_node.filepath, old_node.filename)
|
||||||
|
# gimme a string. unicode can't handle strange filenames in paths, so
|
||||||
|
# in case of such, better get me a byte string. It is not perfect
|
||||||
|
# though, since it WILL crash if the update_path would contain some
|
||||||
|
# unconvertable characters.
|
||||||
|
update_path = update_path
|
||||||
|
|
||||||
|
# refresh objects
|
||||||
|
LOG.debug("Refreshing 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):
|
||||||
|
LOG.error("Access to %s is forbidden", 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])
|
||||||
|
LOG.debug("Deleting objects whitout parent: %s",
|
||||||
|
str(self._session.query(File)
|
||||||
|
.filter(File.parent.is_(None)).all()))
|
||||||
|
self._session.query(File).filter(File.parent.is_(None)).delete()
|
||||||
|
|
||||||
|
self._session.commit()
|
||||||
|
return self._files
|
||||||
|
|
||||||
|
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}
|
||||||
|
extdict = {'.mkv': 'video', # TODO: move this to config/plugin(?)
|
||||||
|
'.rmvb': 'video',
|
||||||
|
'.ogm': 'video',
|
||||||
|
'.ogv': 'video'}
|
||||||
|
|
||||||
|
fp = os.path.join(fobj.filepath, fobj.filename)
|
||||||
|
|
||||||
|
mimeinfo = mimetypes.guess_type(fp)
|
||||||
|
if mimeinfo[0]:
|
||||||
|
mimeinfo = mimeinfo[0].split("/")[0]
|
||||||
|
|
||||||
|
ext = os.path.splitext(fp)[1]
|
||||||
|
|
||||||
|
if mimeinfo and mimeinfo in mimedict:
|
||||||
|
mimedict[mimeinfo](fobj, fp)
|
||||||
|
elif ext and ext in extdict:
|
||||||
|
mimedict[extdict[ext]](fobj, fp)
|
||||||
|
else:
|
||||||
|
LOG.debug("Filetype not supported %s %s", str(mimeinfo), fp)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _audio(self, fobj, filepath):
|
||||||
|
# LOG.warning('audio')
|
||||||
|
return
|
||||||
|
|
||||||
|
def _image(self, fobj, filepath):
|
||||||
|
# LOG.warning('image')
|
||||||
|
return
|
||||||
|
|
||||||
|
def _video(self, fobj, filepath):
|
||||||
|
"""
|
||||||
|
Make captures for a movie. Save it under uniq name.
|
||||||
|
"""
|
||||||
|
result = RE_FN_START.match(fobj.filename)
|
||||||
|
if result:
|
||||||
|
self._check_related(fobj, result.groupdict()['fname_start'])
|
||||||
|
|
||||||
|
vid = Video(filepath)
|
||||||
|
|
||||||
|
fobj.description = vid.get_formatted_tags()
|
||||||
|
|
||||||
|
preview_fn = vid.capture()
|
||||||
|
if preview_fn:
|
||||||
|
Image(preview_fn, self.img_path, fobj)
|
||||||
|
|
||||||
|
def _check_related(self, fobj, filename_start):
|
||||||
|
"""
|
||||||
|
Try to search for related files which belongs to specified File
|
||||||
|
object and pattern. If found, additional File objects are created.
|
||||||
|
|
||||||
|
For example, if we have movie file named like:
|
||||||
|
[aXXo] Batman (1989) [D3ADBEEF].avi
|
||||||
|
[aXXo] Batman (1989) trailer [B00B1337].avi
|
||||||
|
Batman (1989) [D3ADBEEF].avi
|
||||||
|
Batman [D3ADBEEF].avi
|
||||||
|
|
||||||
|
And for example file '[aXXo] Batman (1989) [D3ADBEEF].avi' might have
|
||||||
|
some other accompanied files, like:
|
||||||
|
|
||||||
|
[aXXo] Batman (1989) [D3ADBEEF].avi.conf
|
||||||
|
[aXXo] Batman (1989) [DEADC0DE].nfo
|
||||||
|
[aXXo] Batman (1989) cover [BEEFD00D].jpg
|
||||||
|
[aXXo] Batman (1989) poster [FEEDD00D].jpg
|
||||||
|
|
||||||
|
Which can be atuomatically asociated with the movie.
|
||||||
|
|
||||||
|
This method find such files, and for some of them (currently images)
|
||||||
|
will perform extra actions - like creating corresponding Image objects.
|
||||||
|
|
||||||
|
"""
|
||||||
|
for fname in os.listdir(fobj.filepath):
|
||||||
|
extension = os.path.splitext(fname)[1]
|
||||||
|
if fname.startswith(filename_start) and \
|
||||||
|
extension in ('.jpg', '.gif', '.png'):
|
||||||
|
full_fname = os.path.join(fobj.filepath, fname)
|
||||||
|
LOG.debug('found corresponding image file: %s', full_fname)
|
||||||
|
|
||||||
|
Image(full_fname, self.img_path, fobj, False)
|
||||||
|
|
||||||
|
if not fobj.thumbnail:
|
||||||
|
Thumbnail(full_fname, self.img_path, fobj)
|
||||||
|
|
||||||
|
def _get_all_files(self):
|
||||||
|
"""Gather all File objects"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
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))
|
||||||
|
# TODO: update whole tree sizes (for directories/discs)
|
||||||
|
fobj.size = fob['size']
|
||||||
|
fobj.filepath = fob['path']
|
||||||
|
fobj.type = fob['ftype']
|
||||||
|
else:
|
||||||
|
fobj = File(**fob)
|
||||||
|
# SLOW. Don't do this. Checksums has no value eventually
|
||||||
|
# fobj.mk_checksum()
|
||||||
|
|
||||||
|
if parent is None:
|
||||||
|
fobj.parent_id = 1
|
||||||
|
else:
|
||||||
|
fobj.parent = parent
|
||||||
|
|
||||||
|
self._files.append(fobj)
|
||||||
|
|
||||||
|
return fobj
|
||||||
|
|
||||||
|
def _non_recursive(self, parent, fname, path, size):
|
||||||
|
"""
|
||||||
|
Do the walk through the file system. Non recursively, since it's
|
||||||
|
slow as hell.
|
||||||
|
@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
|
||||||
|
"""
|
||||||
|
fullpath = os.path.join(path, fname)
|
||||||
|
parent = self._mk_file(fname, path, parent, TYPE['dir'])
|
||||||
|
parent.size = 0
|
||||||
|
parent.type = TYPE['dir']
|
||||||
|
|
||||||
|
for root, dirs, files in os.walk(fullpath):
|
||||||
|
for dir_ in dirs:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for file_ in files:
|
||||||
|
self.current_count += 1
|
||||||
|
stat = os.lstat(os.path.join(root, file_))
|
||||||
|
parent.size += stat.st_size
|
||||||
|
|
||||||
|
# TODO: finish that up
|
||||||
|
|
||||||
|
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 = _get_dirsize(fullpath)
|
||||||
|
parent.type = TYPE['dir']
|
||||||
|
|
||||||
|
LOG.info("Scanning `%s' [%s/%s]", fullpath, self.current_count,
|
||||||
|
self.files_count)
|
||||||
|
|
||||||
|
root, dirs, files = next(os.walk(fullpath))
|
||||||
|
for fname in files:
|
||||||
|
fpath = os.path.join(root, fname)
|
||||||
|
extension = os.path.splitext(fname)[1]
|
||||||
|
self.current_count += 1
|
||||||
|
LOG.debug("Processing %s [%s/%s]", fname, self.current_count,
|
||||||
|
self.files_count)
|
||||||
|
|
||||||
|
result = RE_FN_START.match(fname)
|
||||||
|
test_ = False
|
||||||
|
|
||||||
|
if result and extension in ('.jpg', '.gif', '.png'):
|
||||||
|
startfrom = result.groupdict()['fname_start']
|
||||||
|
matching_files = []
|
||||||
|
for fn_ in os.listdir(root):
|
||||||
|
if fn_.startswith(startfrom):
|
||||||
|
matching_files.append(fn_)
|
||||||
|
|
||||||
|
if len(matching_files) > 1:
|
||||||
|
LOG.debug('found image "%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:
|
||||||
|
existing_obj.parent = fob.parent
|
||||||
|
fob = existing_obj
|
||||||
|
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):
|
||||||
|
"""return size in bytes"""
|
||||||
|
count = 0
|
||||||
|
for _, _, files in os.walk(str(self.path)):
|
||||||
|
count += len(files)
|
||||||
|
LOG.debug("count of files: %s", count)
|
||||||
|
return count
|
||||||
|
|
||||||
|
def _set_image_path(self):
|
||||||
|
"""Get or calculate the images path"""
|
||||||
|
image_path = (self._session.query(Config)
|
||||||
|
.filter(Config.key == "image_path")).one()
|
||||||
|
if image_path.value == ":same_as_db:":
|
||||||
|
image_path = pycatalog.misc.calculate_image_path()
|
||||||
|
else:
|
||||||
|
image_path = pycatalog.misc.calculate_image_path(image_path.value)
|
||||||
|
|
||||||
|
self.img_path = image_path
|
||||||
|
|
||||||
|
|
||||||
|
def _get_dirsize(path):
|
||||||
|
"""
|
||||||
|
Returns sum of all files under specified path (also in subdirs)
|
||||||
|
"""
|
||||||
|
|
||||||
|
size = 0
|
||||||
|
|
||||||
|
for root, _, files in os.walk(path):
|
||||||
|
for fname in files:
|
||||||
|
try:
|
||||||
|
size += os.lstat(os.path.join(root, fname)).st_size
|
||||||
|
except OSError:
|
||||||
|
LOG.warning("Cannot access file %s",
|
||||||
|
os.path.join(root, fname))
|
||||||
|
LOG.debug("_get_dirsize, %s: %d", path, size)
|
||||||
|
return size
|
||||||
114
pycatalog/thumbnail.py
Normal file
114
pycatalog/thumbnail.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"""
|
||||||
|
Project: pyGTKtalog
|
||||||
|
Description: Create thumbnail for sepcified image
|
||||||
|
Type: lib
|
||||||
|
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
|
||||||
|
Created: 2011-05-15
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from tempfile import mkstemp
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
import exifread
|
||||||
|
|
||||||
|
from pycatalog.logger import get_logger
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def generate(self):
|
||||||
|
"""
|
||||||
|
Save thumbnail into temporary file
|
||||||
|
"""
|
||||||
|
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 exif and 'JPEGThumbnail' in exif and exif['JPEGThumbnail']:
|
||||||
|
LOG.debug("exif thumb for filename %s" % self.filename)
|
||||||
|
exif_thumbnail = exif['JPEGThumbnail']
|
||||||
|
thumb = open(thumb_fn, 'wb')
|
||||||
|
thumb.write(exif_thumbnail)
|
||||||
|
thumb.close()
|
||||||
|
else:
|
||||||
|
LOG.debug("no exif thumb")
|
||||||
|
if self.is_image_smaller():
|
||||||
|
shutil.copyfile(self.filename, thumb_fn)
|
||||||
|
else:
|
||||||
|
thumb = self._scale_image()
|
||||||
|
if thumb:
|
||||||
|
thumb.save(thumb_fn, "JPEG")
|
||||||
|
|
||||||
|
if exif and 'Image Orientation' in exif:
|
||||||
|
orient = exif['Image Orientation'].values[0]
|
||||||
|
if orient > 1 and orient in orientations:
|
||||||
|
thumb_image = Image.open(thumb_fn)
|
||||||
|
tmp_thumb_img = thumb_image.transpose(orientations[orient])
|
||||||
|
|
||||||
|
if orient in flips:
|
||||||
|
tmp_thumb_img = tmp_thumb_img.transpose(flips[orient])
|
||||||
|
|
||||||
|
tmp_thumb_img.save(thumb_fn, 'JPEG')
|
||||||
|
|
||||||
|
return thumb_fn
|
||||||
|
|
||||||
|
def is_image_smaller(self):
|
||||||
|
"""Check if image is smaller than desired dimention, return boolean"""
|
||||||
|
image = Image.open(self.filename)
|
||||||
|
im_x, im_y = image.size
|
||||||
|
image.close()
|
||||||
|
return im_x <= self.thumb_x and im_y <= self.thumb_y
|
||||||
|
|
||||||
|
def _get_exif(self):
|
||||||
|
"""
|
||||||
|
Get exif (if available), return as a dict
|
||||||
|
"""
|
||||||
|
image_file = open(self.filename, 'rb')
|
||||||
|
try:
|
||||||
|
exif = exifread.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 Exception:
|
||||||
|
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
|
||||||
283
pycatalog/video.py
Normal file
283
pycatalog/video.py
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
"""
|
||||||
|
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 math
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from pycatalog.misc import float_to_string
|
||||||
|
from pycatalog.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 = tempfile.mkdtemp()
|
||||||
|
file_desc, image_fn = tempfile.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 = ''
|
||||||
|
if 'container' in self.tags:
|
||||||
|
out_tags += "Container: %s\n" % self.tags['container']
|
||||||
|
|
||||||
|
if 'width' in self.tags and 'height' in self.tags:
|
||||||
|
out_tags += "Resolution: %sx%s\n" % (self.tags['width'],
|
||||||
|
self.tags['height'])
|
||||||
|
|
||||||
|
if 'duration' in self.tags:
|
||||||
|
out_tags += "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 = 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 -mc 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 as exc:
|
||||||
|
errno, strerror = exc.args
|
||||||
|
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 \
|
||||||
|
pycatalog.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 Exception:
|
||||||
|
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 Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
str_out = ''
|
||||||
|
for key in self.tags:
|
||||||
|
str_out += "%20s: %s\n" % (key, self.tags[key])
|
||||||
|
return str_out
|
||||||
118
pygtktalog.py
118
pygtktalog.py
@@ -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
|
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Pillow
|
||||||
|
exifread
|
||||||
|
sqlalchemy
|
||||||
@@ -1,803 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
|
|
||||||
<!--*- mode: xml -*-->
|
|
||||||
<glade-interface>
|
|
||||||
<widget class="GtkDialog" id="config">
|
|
||||||
<property name="width_request">550</property>
|
|
||||||
<property name="height_request">400</property>
|
|
||||||
<property name="title" translatable="yes">Preferences - pyGTKtalog</property>
|
|
||||||
<property name="modal">True</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_NORMAL</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHPaned" id="hpaned1">
|
|
||||||
<property name="width_request">168</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="position">140</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow1">
|
|
||||||
<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>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTreeView" id="category_tree">
|
|
||||||
<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="cursor_changed" handler="on_category_tree_cursor_changed"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="resize">False</property>
|
|
||||||
<property name="shrink">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="desc">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xpad">3</property>
|
|
||||||
<property name="ypad">3</property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow2">
|
|
||||||
<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>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkViewport" id="pref_group">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="main_group_holder">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="disk_group">
|
|
||||||
<property name="border_width">1</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTable" id="table1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="column_spacing">3</property>
|
|
||||||
<property name="row_spacing">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Mount point:</property>
|
|
||||||
<property name="mnemonic_widget">mnt_entry</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="mnt_entry">
|
|
||||||
<property name="width_request">100</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button_mnt">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Browse...</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_button_mnt_clicked"/>
|
|
||||||
<signal name="activate" handler="on_button_mnt_activate"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">2</property>
|
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="ejt_entry">
|
|
||||||
<property name="width_request">100</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button_ejt">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Browse...</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_button_ejt_clicked"/>
|
|
||||||
<signal name="activate" handler="on_button_ejt_activate"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">2</property>
|
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label5">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Eject program:</property>
|
|
||||||
<property name="mnemonic_widget">ejt_entry</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>CD/DVD drive options</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="general_group">
|
|
||||||
<property name="border_width">1</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_win">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Save main window size</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_pan">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Save paned window sizes</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_eject">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Eject CD/DVD after scan</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_compress">
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Compress collection</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_imageviewer">
|
|
||||||
<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="label" translatable="yes">Use external image viewer</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="on_ch_imageviewer_toggled"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">4</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTable" id="table4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="n_rows">1</property>
|
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="column_spacing">3</property>
|
|
||||||
<property name="row_spacing">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label_imv">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Image viewer:</property>
|
|
||||||
<property name="mnemonic_widget">mnt_entry</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="entry_imv">
|
|
||||||
<property name="width_request">100</property>
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button_imv">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="sensitive">False</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Browse...</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_button_imv_clicked"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">2</property>
|
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="x_options"></property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">5</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label8">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>General options</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame5">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment5">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox7">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_xls">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Export to XLS</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label9">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>Misc</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame6">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment6">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox8">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_quit">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Confirm quit if there are unsaved data</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_warnnew">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Confirm "new" if there are unsaved data</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_wrnmount">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Warn about mount/umount errors</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_wrndel">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Warn on delete</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label10">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>Confirmations</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="scan_group">
|
|
||||||
<property name="border_width">1</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox6">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_thumb">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Create thumbnails for images</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_exif">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Scan EXIF data</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_gthumb">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Include gThumb image description</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkCheckButton" id="ch_retrive">
|
|
||||||
<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="label" translatable="yes">Retrive extra information</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<property name="draw_indicator">True</property>
|
|
||||||
<signal name="toggled" handler="on_ch_retrive_toggled"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="ft_group">
|
|
||||||
<property name="border_width">1</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame7">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment7">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="ext_ed">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTable" id="table3">
|
|
||||||
<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="n_rows">2</property>
|
|
||||||
<property name="n_columns">2</property>
|
|
||||||
<property name="row_spacing">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="ext_entry">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label12">
|
|
||||||
<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">Extension:</property>
|
|
||||||
<property name="justify">GTK_JUSTIFY_RIGHT</property>
|
|
||||||
<property name="mnemonic_widget">ext_entry</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label6">
|
|
||||||
<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">Command:</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="com_entry">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHButtonBox" id="hbuttonbox1">
|
|
||||||
<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="border_width">3</property>
|
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="ext_add">
|
|
||||||
<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">Add/Change</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_ext_add_clicked"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="ext_del">
|
|
||||||
<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">Delete</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_ext_del_clicked"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow3">
|
|
||||||
<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="GtkTreeView" id="extension_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>
|
|
||||||
<signal name="cursor_changed" handler="on_extension_tree_cursor_changed"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>Files extensions</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="resize">True</property>
|
|
||||||
<property name="shrink">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancelbutton1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="label">gtk-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
<signal name="clicked" handler="on_cancelbutton_clicked"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="okbutton1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="label">gtk-save</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
<signal name="clicked" handler="on_okbutton_clicked"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</glade-interface>
|
|
||||||
@@ -1,838 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
|
|
||||||
<!--*- mode: xml -*-->
|
|
||||||
<glade-interface>
|
|
||||||
<widget class="GtkDialog" id="inputDialog">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="title" translatable="yes">Disk label - pyGTKtalog</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHBox" id="hbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xpad">3</property>
|
|
||||||
<property name="label" translatable="yes">Disk label</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="volname">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="activates_default">True</property>
|
|
||||||
<signal name="activate" handler="on_volname_activate"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancelbutton1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="label">gtk-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="okbutton1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="has_default">True</property>
|
|
||||||
<property name="label">gtk-ok</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="addDirDialog">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="title" translatable="yes">dialog1</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">3</property>
|
|
||||||
<property name="label_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="border_width">3</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTable" id="table1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="n_rows">2</property>
|
|
||||||
<property name="n_columns">3</property>
|
|
||||||
<property name="column_spacing">3</property>
|
|
||||||
<property name="row_spacing">3</property>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="browse">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="label" translatable="yes">Browse...</property>
|
|
||||||
<property name="use_underline">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
<signal name="clicked" handler="on_browse_clicked"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">2</property>
|
|
||||||
<property name="right_attach">3</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label7">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Select directory:</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label6">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
<property name="label" translatable="yes">Disk Label:</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="x_options">GTK_FILL</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="directory">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="dirvolname">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="text" translatable="yes">New</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options"></property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label4">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="label" translatable="yes"><b>Select directory and enter label</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancelbutton2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="label">gtk-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="okbutton2">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="label">gtk-ok</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="statDialog">
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="title" translatable="yes">pyGTKtalog - stats</property>
|
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox3">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTable" id="table2">
|
|
||||||
<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="n_rows">4</property>
|
|
||||||
<property name="n_columns">2</property>
|
|
||||||
<property name="column_spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="size_label">
|
|
||||||
<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">Total size:</property>
|
|
||||||
<property name="justify">GTK_JUSTIFY_RIGHT</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">3</property>
|
|
||||||
<property name="bottom_attach">4</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="files_label">
|
|
||||||
<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">Number of files:</property>
|
|
||||||
<property name="justify">GTK_JUSTIFY_RIGHT</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">2</property>
|
|
||||||
<property name="bottom_attach">3</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="dirs_label">
|
|
||||||
<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">Number of directories:</property>
|
|
||||||
<property name="justify">GTK_JUSTIFY_RIGHT</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="discs_label">
|
|
||||||
<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">Number of discs:</property>
|
|
||||||
<property name="justify">GTK_JUSTIFY_RIGHT</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="size_entry">
|
|
||||||
<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="shadow_type">GTK_SHADOW_ETCHED_IN</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">3</property>
|
|
||||||
<property name="bottom_attach">4</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="files_entry">
|
|
||||||
<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="shadow_type">GTK_SHADOW_ETCHED_IN</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">2</property>
|
|
||||||
<property name="bottom_attach">3</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="dirs_entry">
|
|
||||||
<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="shadow_type">GTK_SHADOW_ETCHED_IN</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="top_attach">1</property>
|
|
||||||
<property name="bottom_attach">2</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="discs_entry">
|
|
||||||
<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="shadow_type">GTK_SHADOW_ETCHED_IN</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="left_attach">1</property>
|
|
||||||
<property name="right_attach">2</property>
|
|
||||||
<property name="y_options">GTK_FILL</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area3">
|
|
||||||
<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="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button1">
|
|
||||||
<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-close</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">0</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="renameDialog">
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="title" translatable="yes">pyGTKtalog - rename</property>
|
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox4">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHBox" id="hbox3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="xpad">3</property>
|
|
||||||
<property name="label" translatable="yes">Rename</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="fill">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="name">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="activates_default">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area4">
|
|
||||||
<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="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button2">
|
|
||||||
<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-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button3">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="has_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-ok</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="file_editDialog">
|
|
||||||
<property name="width_request">500</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox5">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox2">
|
|
||||||
<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="GtkFrame" id="frame1">
|
|
||||||
<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_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment1">
|
|
||||||
<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="top_padding">3</property>
|
|
||||||
<property name="bottom_padding">3</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<property name="right_padding">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="filename_entry">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label2">
|
|
||||||
<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"><b>Filename</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame3">
|
|
||||||
<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_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment3">
|
|
||||||
<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="top_padding">3</property>
|
|
||||||
<property name="bottom_padding">3</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<property name="right_padding">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow1">
|
|
||||||
<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_text">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label5">
|
|
||||||
<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"><b>Description</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame4">
|
|
||||||
<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_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment4">
|
|
||||||
<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="top_padding">3</property>
|
|
||||||
<property name="bottom_padding">3</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<property name="right_padding">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow2">
|
|
||||||
<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="note_text">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label8">
|
|
||||||
<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"><b>Note</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area5">
|
|
||||||
<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="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancel">
|
|
||||||
<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-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="ok">
|
|
||||||
<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-save</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="tagsDialog">
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox6">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox3">
|
|
||||||
<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="GtkLabel" id="label9">
|
|
||||||
<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">Fill field below with comma separated keywords you will desired to tag selected files.</property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="padding">3</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkEntry" id="tag_entry1">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="has_focus">True</property>
|
|
||||||
<property name="is_focus">True</property>
|
|
||||||
<property name="can_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="activates_default">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area6">
|
|
||||||
<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="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancel4">
|
|
||||||
<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-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="button5">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="can_default">True</property>
|
|
||||||
<property name="has_default">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-ok</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<widget class="GtkDialog" id="tagRemove">
|
|
||||||
<property name="width_request">600</property>
|
|
||||||
<property name="height_request">400</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">5</property>
|
|
||||||
<property name="title" translatable="yes">pyGTKtalog - remove tags</property>
|
|
||||||
<property name="modal">True</property>
|
|
||||||
<property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
|
|
||||||
<property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
|
|
||||||
<property name="has_separator">False</property>
|
|
||||||
<child internal-child="vbox">
|
|
||||||
<widget class="GtkVBox" id="dialog-vbox7">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow3">
|
|
||||||
<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="GtkTreeView" id="treeview1">
|
|
||||||
<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">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child internal-child="action_area">
|
|
||||||
<widget class="GtkHButtonBox" id="dialog-action_area7">
|
|
||||||
<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="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="cancel5">
|
|
||||||
<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-cancel</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="ok2">
|
|
||||||
<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-remove</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-5</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="pack_type">GTK_PACK_END</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</glade-interface>
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,276 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
|
|
||||||
<!--Generated with glade3 3.4.0 on Tue May 13 10:23:51 2008 -->
|
|
||||||
<glade-interface>
|
|
||||||
<widget class="GtkWindow" id="search_window">
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="border_width">3</property>
|
|
||||||
<signal name="delete_event" handler="on_search_window_delete_event"/>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="vbox3">
|
|
||||||
<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="GtkHBox" id="hbox3">
|
|
||||||
<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="spacing">3</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label5">
|
|
||||||
<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">Search for:</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkComboBoxEntry" id="comboboxentry">
|
|
||||||
<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 internal-child="entry">
|
|
||||||
<widget class="GtkEntry" id="search_entry">
|
|
||||||
<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>
|
|
||||||
<signal name="activate" handler="on_search_activate"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHButtonBox" id="hbuttonbox2">
|
|
||||||
<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="GtkButton" id="search_button">
|
|
||||||
<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-find</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">1</property>
|
|
||||||
<signal name="clicked" handler="on_search_activate"/>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkFrame" id="frame3">
|
|
||||||
<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_xalign">0</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkAlignment" id="alignment3">
|
|
||||||
<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="top_padding">3</property>
|
|
||||||
<property name="bottom_padding">3</property>
|
|
||||||
<property name="left_padding">12</property>
|
|
||||||
<property name="right_padding">3</property>
|
|
||||||
<child>
|
|
||||||
<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>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow3">
|
|
||||||
<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="GtkTreeView" id="result">
|
|
||||||
<property name="width_request">600</property>
|
|
||||||
<property name="height_request">300</property>
|
|
||||||
<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>
|
|
||||||
<signal name="row_activated" handler="on_result_row_activated"/>
|
|
||||||
<signal name="button_release_event" handler="on_result_button_release_event"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkStatusbar" id="statusbar">
|
|
||||||
<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="spacing">2</property>
|
|
||||||
<property name="has_resize_grip">False</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkLabel" id="label6">
|
|
||||||
<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"><b>Search results</b></property>
|
|
||||||
<property name="use_markup">True</property>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="type">label_item</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHButtonBox" id="hbuttonbox1">
|
|
||||||
<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="border_width">3</property>
|
|
||||||
<property name="layout_style">GTK_BUTTONBOX_END</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkButton" id="close">
|
|
||||||
<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-close</property>
|
|
||||||
<property name="use_stock">True</property>
|
|
||||||
<property name="response_id">-6</property>
|
|
||||||
<signal name="clicked" handler="on_close_clicked"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="expand">False</property>
|
|
||||||
<property name="position">2</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
</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_tag_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="separator16">
|
|
||||||
<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_thumb_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_thumb_activate"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkSeparatorMenuItem" id="separator17">
|
|
||||||
<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="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_image_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_image_activate"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkSeparatorMenuItem" id="separator18">
|
|
||||||
<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_edit_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_delete_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_rename_activate"/>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</glade-interface>
|
|
||||||
41
setup.cfg
Normal file
41
setup.cfg
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[metadata]
|
||||||
|
name = pycatalog
|
||||||
|
summary = Catalog application for keeping content list of disks and discs
|
||||||
|
description_file = README.rst
|
||||||
|
author = Roman Dobosz
|
||||||
|
author_email = gryf73@gmail.com
|
||||||
|
home_page = https://github.com/gryf/pycatalog
|
||||||
|
license = BSD
|
||||||
|
keywords = catalog, gwhere, collection
|
||||||
|
classifier =
|
||||||
|
Development Status :: 4 - Beta
|
||||||
|
Environment :: Console
|
||||||
|
Intended Audience :: End Users/Desktop
|
||||||
|
License :: OSI Approved :: BSD License
|
||||||
|
Operating System :: POSIX :: Linux
|
||||||
|
Programming Language :: Python
|
||||||
|
Programming Language :: Python :: 3
|
||||||
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
|
Programming Language :: Python :: 3.10
|
||||||
|
Topic :: Database
|
||||||
|
Topic :: Desktop Environment
|
||||||
|
|
||||||
|
[install]
|
||||||
|
record = install.log
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
pycatalog = pycatalog:main
|
||||||
|
|
||||||
|
[files]
|
||||||
|
packages =
|
||||||
|
pycatalog
|
||||||
|
|
||||||
|
[options]
|
||||||
|
install_requires =
|
||||||
|
pillow
|
||||||
|
sqlalchemy
|
||||||
|
|
||||||
|
[bdist_wheel]
|
||||||
|
universal = 1
|
||||||
5
setup.py
Executable file
5
setup.py
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import setuptools
|
||||||
|
|
||||||
|
|
||||||
|
setuptools.setup(setup_requires=['pbr>=2.0.0'], pbr=True)
|
||||||
@@ -1,25 +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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,25 +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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,282 +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 gtkmvc import Controller
|
|
||||||
import views.v_dialogs as Dialogs
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
|
|
||||||
class ConfigController(Controller):
|
|
||||||
category_dict = {
|
|
||||||
'Disk options':'disk_group',
|
|
||||||
'General':'general_group',
|
|
||||||
'Scan options':'scan_group',
|
|
||||||
'Files extensions':'ft_group',
|
|
||||||
}
|
|
||||||
category_order = ['General', 'Disk options', 'Scan options',
|
|
||||||
'Files extensions',]
|
|
||||||
|
|
||||||
def __init__(self, model):
|
|
||||||
Controller.__init__(self, model)
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_view(self, view):
|
|
||||||
Controller.register_view(self, view)
|
|
||||||
|
|
||||||
# get data from Config object and put it into view
|
|
||||||
self.view['mnt_entry'].set_text(self.model.confd['cd'])
|
|
||||||
self.view['ejt_entry'].set_text(self.model.confd['ejectapp'])
|
|
||||||
self.view['ch_win'].set_active(self.model.confd['savewin'])
|
|
||||||
self.view['ch_pan'].set_active(self.model.confd['savepan'])
|
|
||||||
self.view['ch_eject'].set_active(self.model.confd['eject'])
|
|
||||||
self.view['ch_xls'].set_active(self.model.confd['exportxls'])
|
|
||||||
self.view['ch_quit'].set_active(self.model.confd['confirmquit'])
|
|
||||||
self.view['ch_wrnmount'].set_active(self.model.confd['mntwarn'])
|
|
||||||
self.view['ch_wrndel'].set_active(self.model.confd['delwarn'])
|
|
||||||
self.view['ch_warnnew'].set_active(self.model.confd['confirmabandon'])
|
|
||||||
self.view['ch_thumb'].set_active(self.model.confd['thumbs'])
|
|
||||||
self.view['ch_exif'].set_active(self.model.confd['exif'])
|
|
||||||
self.view['ch_gthumb'].set_active(self.model.confd['gthumb'])
|
|
||||||
self.view['ch_compress'].set_active(self.model.confd['compress'])
|
|
||||||
self.view['ch_retrive'].set_active(self.model.confd['retrive'])
|
|
||||||
self.view['ch_imageviewer'].set_active(self.model.confd['imgview'])
|
|
||||||
self.view['entry_imv'].set_text(self.model.confd['imgprog'])
|
|
||||||
|
|
||||||
self.__toggle_scan_group()
|
|
||||||
|
|
||||||
# initialize tree view
|
|
||||||
self.__setup_category_tree()
|
|
||||||
|
|
||||||
# initialize models for files extensions
|
|
||||||
vi = self.view['extension_tree']
|
|
||||||
vi.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
||||||
#self.view['extension_tree'].set_model(self.model.ext_tree)
|
|
||||||
self.__setup_extension_tree()
|
|
||||||
|
|
||||||
self.view['config'].show()
|
|
||||||
return
|
|
||||||
|
|
||||||
#################
|
|
||||||
# connect signals
|
|
||||||
def on_extension_tree_cursor_changed(self, tree):
|
|
||||||
model = tree.get_model()
|
|
||||||
selected = model.get_value(model.get_iter(tree.get_cursor()[0]), 0)
|
|
||||||
ext = self.model.confd['extensions']
|
|
||||||
self.view['ext_entry'].set_text(selected)
|
|
||||||
self.view['com_entry'].set_text(ext[selected])
|
|
||||||
|
|
||||||
def on_category_tree_cursor_changed(self, tree):
|
|
||||||
"""change view to selected row corresponding to group of properties"""
|
|
||||||
model = tree.get_model()
|
|
||||||
selected = model.get_value(model.get_iter(tree.get_cursor()[0]), 0)
|
|
||||||
iterator = tree.get_model().get_iter_first();
|
|
||||||
while iterator != None:
|
|
||||||
if model.get_value(iterator, 0) == selected:
|
|
||||||
self.view[self.category_dict[model.get_value(iterator,
|
|
||||||
0)]].show()
|
|
||||||
self.view['desc'].set_markup("<b>%s</b>" % selected)
|
|
||||||
else:
|
|
||||||
self.view[self.category_dict[model.get_value(iterator,
|
|
||||||
0)]].hide()
|
|
||||||
iterator = tree.get_model().iter_next(iterator);
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_cancelbutton_clicked(self, button):
|
|
||||||
self.view['config'].destroy()
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_okbutton_clicked(self, button):
|
|
||||||
# get data from view and put it into Config object
|
|
||||||
self.model.confd['cd'] = self.view['mnt_entry'].get_text()
|
|
||||||
self.model.confd['ejectapp'] = self.view['ejt_entry'].get_text()
|
|
||||||
self.model.confd['savewin'] = self.view['ch_win'].get_active()
|
|
||||||
self.model.confd['savepan'] = self.view['ch_pan'].get_active()
|
|
||||||
self.model.confd['eject'] = self.view['ch_eject'].get_active()
|
|
||||||
self.model.confd['exportxls'] = self.view['ch_xls'].get_active()
|
|
||||||
self.model.confd['confirmquit'] = self.view['ch_quit'].get_active()
|
|
||||||
self.model.confd['mntwarn'] = self.view['ch_wrnmount'].get_active()
|
|
||||||
self.model.confd['delwarn'] = self.view['ch_wrndel'].get_active()
|
|
||||||
v = self.view['ch_warnnew']
|
|
||||||
self.model.confd['confirmabandon'] = v.get_active()
|
|
||||||
self.model.confd['thumbs'] = self.view['ch_thumb'].get_active()
|
|
||||||
self.model.confd['exif'] = self.view['ch_exif'].get_active()
|
|
||||||
self.model.confd['gthumb'] = self.view['ch_gthumb'].get_active()
|
|
||||||
self.model.confd['compress'] = self.view['ch_compress'].get_active()
|
|
||||||
self.model.confd['retrive'] = self.view['ch_retrive'].get_active()
|
|
||||||
self.model.confd['imgview'] = self.view['ch_imageviewer'].get_active()
|
|
||||||
self.model.confd['imgprog'] = self.view['entry_imv'].get_text()
|
|
||||||
self.model.save()
|
|
||||||
self.view['config'].destroy()
|
|
||||||
|
|
||||||
def on_button_ejt_clicked(self, button):
|
|
||||||
fn = self.__show_filechooser("Choose eject program")
|
|
||||||
self.view['ejt_entryentry_imv'].set_text(fn)
|
|
||||||
|
|
||||||
def on_button_mnt_clicked(self, button):
|
|
||||||
fn = self.__show_filechooser("Choose mount point")
|
|
||||||
self.view['mnt_entry'].set_text(fn)
|
|
||||||
|
|
||||||
def on_ch_retrive_toggled(self, widget):
|
|
||||||
self.__toggle_scan_group()
|
|
||||||
|
|
||||||
def on_ch_imageviewer_toggled(self, checkbox):
|
|
||||||
state = self.view['ch_imageviewer'].get_active()
|
|
||||||
for i in ['label_imv', 'entry_imv', 'button_imv']:
|
|
||||||
self.view[i].set_sensitive(state)
|
|
||||||
|
|
||||||
def on_button_imv_clicked(self, widget):
|
|
||||||
fn = self.__show_filechooser("Choose image viewer")
|
|
||||||
self.view['entry_imv'].set_text(fn)
|
|
||||||
|
|
||||||
def on_ext_add_clicked(self, widget):
|
|
||||||
ext = self.view['ext_entry'].get_text().lower()
|
|
||||||
com = self.view['com_entry'].get_text()
|
|
||||||
if len(ext) == 0 and len(com) == 0:
|
|
||||||
Dialogs.Err("Config - pyGTKtalog", "Error",
|
|
||||||
"Extension and command required")
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(com) == 0:
|
|
||||||
Dialogs.Err("Config - pyGTKtalog", "Error", "Command is empty")
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(ext) == 0:
|
|
||||||
Dialogs.Err("Config - pyGTKtalog", "Error", "Extension is empty")
|
|
||||||
return
|
|
||||||
|
|
||||||
if ext in self.model.confd['extensions'].keys():
|
|
||||||
obj = Dialogs.Qst('Alter extension',
|
|
||||||
'Alter extension?',
|
|
||||||
'Extension "%s" will be altered.' % ext)
|
|
||||||
if not obj.run():
|
|
||||||
return
|
|
||||||
self.model.confd['extensions'][ext] = com
|
|
||||||
|
|
||||||
self.__setup_extension_tree()
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_ext_del_clicked(self, widget):
|
|
||||||
v = self.view['extension_tree']
|
|
||||||
model, selection = v.get_selection().get_selected_rows()
|
|
||||||
if len(selection) == 0:
|
|
||||||
Dialogs.Err("Config - pyGTKtalog", "Error", "No item selected")
|
|
||||||
return
|
|
||||||
elif len(selection) == 1:
|
|
||||||
sufix = ''
|
|
||||||
else:
|
|
||||||
sufix = "s"
|
|
||||||
|
|
||||||
if self.model.confd['delwarn']:
|
|
||||||
obj = Dialogs.Qst('Delete extension%s' % sufix,
|
|
||||||
'Delete extension%s?' % sufix,
|
|
||||||
'Object%s will be permanently removed.' % sufix)
|
|
||||||
if not obj.run():
|
|
||||||
return
|
|
||||||
|
|
||||||
for i in selection:
|
|
||||||
m = self.model.confd['extensions']
|
|
||||||
m.pop(model.get_value(model.get_iter(i), 0))
|
|
||||||
|
|
||||||
self.__setup_extension_tree()
|
|
||||||
return
|
|
||||||
|
|
||||||
############################
|
|
||||||
# private controller methods
|
|
||||||
def __setup_extension_tree(self):
|
|
||||||
self.model.refresh_ext()
|
|
||||||
|
|
||||||
self.view['extension_tree'].set_model(self.model.ext_tree)
|
|
||||||
|
|
||||||
for i in self.view['extension_tree'].get_columns():
|
|
||||||
self.view['extension_tree'].remove_column(i)
|
|
||||||
cell = gtk.CellRendererText()
|
|
||||||
column = gtk.TreeViewColumn("Extension", cell, text=0)
|
|
||||||
column.set_resizable(True)
|
|
||||||
self.view['extension_tree'].append_column(column)
|
|
||||||
|
|
||||||
column = gtk.TreeViewColumn("Command", cell, text=1)
|
|
||||||
column.set_resizable(True)
|
|
||||||
self.view['extension_tree'].append_column(column)
|
|
||||||
|
|
||||||
def __toggle_scan_group(self):
|
|
||||||
for i in ('ch_thumb','ch_exif','ch_gthumb'):
|
|
||||||
self.view[i].set_sensitive(self.view['ch_retrive'].get_active())
|
|
||||||
return
|
|
||||||
|
|
||||||
def __setup_category_tree(self):
|
|
||||||
category_tree = self.view['category_tree']
|
|
||||||
category_tree.set_model(self.model.category_tree)
|
|
||||||
|
|
||||||
self.model.category_tree.clear()
|
|
||||||
for i in self.category_order:
|
|
||||||
myiter = self.model.category_tree.insert_before(None,None)
|
|
||||||
self.model.category_tree.set_value(myiter,0,i)
|
|
||||||
|
|
||||||
cell = gtk.CellRendererText()
|
|
||||||
column = gtk.TreeViewColumn("Name",cell,text=0)
|
|
||||||
column.set_resizable(True)
|
|
||||||
category_tree.append_column(column)
|
|
||||||
|
|
||||||
def __show_filechooser(self, title):
|
|
||||||
"""dialog for choose eject"""
|
|
||||||
fn = None
|
|
||||||
dialog = gtk.FileChooserDialog(
|
|
||||||
title=title,
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons=(gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
if __debug__:
|
|
||||||
print "c_config.py: __show_filechooser()",
|
|
||||||
print dialog.get_filename()
|
|
||||||
fn = dialog.get_filename()
|
|
||||||
dialog.destroy()
|
|
||||||
return fn
|
|
||||||
|
|
||||||
def __show_dirchooser(self):
|
|
||||||
"""dialog for point the mountpoint"""
|
|
||||||
dialog = gtk.FileChooserDialog(
|
|
||||||
title="Choose mount point",
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons=(gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
dialog.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
||||||
dialog.set_filename(self.view['mnt_entry'].get_text())
|
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
self.view['mnt_entry'].set_text(dialog.get_filename())
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
1693
src/ctrls/c_main.py
1693
src/ctrls/c_main.py
File diff suppressed because it is too large
Load Diff
@@ -1,379 +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 gtk
|
|
||||||
|
|
||||||
from gtkmvc import Controller
|
|
||||||
import views.v_dialogs as Dialogs
|
|
||||||
|
|
||||||
class SearchController(Controller):
|
|
||||||
"""Controller for main application window"""
|
|
||||||
|
|
||||||
def __init__(self, model):
|
|
||||||
"""Initialize controller"""
|
|
||||||
Controller.__init__(self, model)
|
|
||||||
self.search_string = ""
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_view(self, view):
|
|
||||||
Controller.register_view(self, view)
|
|
||||||
|
|
||||||
# Setup TreeView result widget, as columned list
|
|
||||||
v = self.view['result']
|
|
||||||
v.set_model(self.model.search_list)
|
|
||||||
|
|
||||||
v.get_selection().set_mode(gtk.SELECTION_MULTIPLE)
|
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Disc', gtk.CellRendererText(), text=1)
|
|
||||||
c.set_sort_column_id(1)
|
|
||||||
c.set_resizable(True)
|
|
||||||
v.append_column(c)
|
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Filename')
|
|
||||||
cellpb = gtk.CellRendererPixbuf()
|
|
||||||
cell = gtk.CellRendererText()
|
|
||||||
c.pack_start(cellpb, False)
|
|
||||||
c.pack_start(cell, True)
|
|
||||||
c.set_attributes(cellpb, stock_id=7)
|
|
||||||
c.set_attributes(cell, text=2)
|
|
||||||
c.set_sort_column_id(2)
|
|
||||||
c.set_resizable(True)
|
|
||||||
v.append_column(c)
|
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Path', gtk.CellRendererText(), text=3)
|
|
||||||
c.set_sort_column_id(3)
|
|
||||||
c.set_resizable(True)
|
|
||||||
v.append_column(c)
|
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Size', gtk.CellRendererText(), text=4)
|
|
||||||
c.set_sort_column_id(4)
|
|
||||||
c.set_resizable(True)
|
|
||||||
v.append_column(c)
|
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Date', gtk.CellRendererText(), text=5)
|
|
||||||
c.set_sort_column_id(5)
|
|
||||||
c.set_resizable(True)
|
|
||||||
v.append_column(c)
|
|
||||||
v.set_search_column(2)
|
|
||||||
|
|
||||||
# combobox
|
|
||||||
self.view['comboboxentry'].set_model(self.model.search_history)
|
|
||||||
self.view['comboboxentry'].set_text_column(0)
|
|
||||||
# statusbar
|
|
||||||
self.context_id = self.view['statusbar'].get_context_id('search')
|
|
||||||
self.view['statusbar'].pop(self.context_id)
|
|
||||||
self.view['search_window'].show()
|
|
||||||
self.model.search_created = True;
|
|
||||||
return
|
|
||||||
|
|
||||||
#########################################################################
|
|
||||||
# Connect signals from GUI, like menu objects, toolbar buttons and so on.
|
|
||||||
def on_search_window_delete_event(self, window, event):
|
|
||||||
"""if window was closed, reset attributes"""
|
|
||||||
self.model.point = None
|
|
||||||
self.model.search_created = False;
|
|
||||||
return False
|
|
||||||
|
|
||||||
def on_close_clicked(self, button):
|
|
||||||
"""close search window"""
|
|
||||||
self.model.point = None
|
|
||||||
self.model.search_created = False;
|
|
||||||
self.view['search_window'].destroy()
|
|
||||||
|
|
||||||
def on_search_activate(self, entry):
|
|
||||||
"""find button or enter pressed on entry search. Do the search"""
|
|
||||||
search_txt = self.view['search_entry'].get_text()
|
|
||||||
self.search_string = search_txt
|
|
||||||
found = self.model.search(search_txt)
|
|
||||||
self.model.add_search_history(search_txt)
|
|
||||||
self.__set_status_bar(found)
|
|
||||||
|
|
||||||
def on_result_row_activated(self, treeview, path, treecolumn):
|
|
||||||
"""result treeview row activated, change models 'point' observable
|
|
||||||
variable to id of elected item. rest is all in main controler hands."""
|
|
||||||
model = treeview.get_model()
|
|
||||||
s_iter = model.get_iter(path)
|
|
||||||
self.model.point = model.get_value(s_iter, 0)
|
|
||||||
|
|
||||||
def on_result_button_release_event(self, tree, event):
|
|
||||||
if event.button == 3: # Right mouse button. Show context menu.
|
|
||||||
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.view['files_popup'].popup(None, None, None, 0, 0)
|
|
||||||
self.view['files_popup'].show_all()
|
|
||||||
return True
|
|
||||||
|
|
||||||
def on_add_tag_activate(self, menu_item):
|
|
||||||
"""Add tags to selected files"""
|
|
||||||
tags = Dialogs.TagsDialog().run()
|
|
||||||
if not tags:
|
|
||||||
return
|
|
||||||
ids = self.__get_tv_selection_ids(self.view['result'])
|
|
||||||
for item_id in ids:
|
|
||||||
self.model.add_tags(item_id, tags)
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_delete_tag_activate(self, menu_item):
|
|
||||||
ids = self.__get_tv_selection_ids(self.view['result'])
|
|
||||||
if not ids:
|
|
||||||
Dialogs.Inf("Remove tags", "No files selected",
|
|
||||||
"You have to select some files first.")
|
|
||||||
return
|
|
||||||
|
|
||||||
tags = self.model.get_tags_by_file_id(ids)
|
|
||||||
if tags:
|
|
||||||
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.")
|
|
||||||
return
|
|
||||||
elif retcode == "ok" and retval:
|
|
||||||
self.model.delete_tags(ids, retval)
|
|
||||||
found = self.model.search(self.search_string)
|
|
||||||
self.__set_status_bar(found)
|
|
||||||
|
|
||||||
def on_add_thumb_activate(self, menu_item):
|
|
||||||
image, only_thumbs = Dialogs.LoadImageFile().run()
|
|
||||||
if not image:
|
|
||||||
return
|
|
||||||
ids = self.__get_tv_selection_ids(self.view['result'])
|
|
||||||
for item_id in ids:
|
|
||||||
self.model.add_thumbnail(image, item_id)
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_remove_thumb_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)
|
|
||||||
if not obj.run():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
ids = self.__get_tv_selection_ids(self.view['result'])
|
|
||||||
for item_id in ids:
|
|
||||||
self.model.del_thumbnail(item_id)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "c_search.py: on_remove_thumb_activate(): error on",
|
|
||||||
print "getting selected items or removing thumbnails"
|
|
||||||
return
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_add_image_activate(self, menu_item):
|
|
||||||
dialog = Dialogs.LoadImageFile(True)
|
|
||||||
msg = "Don't copy images. Generate only thumbnails."
|
|
||||||
toggle = gtk.CheckButton(msg)
|
|
||||||
toggle.show()
|
|
||||||
dialog.dialog.set_extra_widget(toggle)
|
|
||||||
|
|
||||||
images, only_thumbs = dialog.run()
|
|
||||||
if not images:
|
|
||||||
return
|
|
||||||
|
|
||||||
for image in images:
|
|
||||||
try:
|
|
||||||
selection = self.view['result'].get_selection()
|
|
||||||
model, list_of_paths = selection.get_selected_rows()
|
|
||||||
fid = model.get_value(model.get_iter(list_of_paths[0]), 0)
|
|
||||||
except:
|
|
||||||
try:
|
|
||||||
path, column = self.view['result'].get_cursor()
|
|
||||||
model = self.view['result'].get_model()
|
|
||||||
fiter = model.get_iter(path)
|
|
||||||
fid = model.get_value(fiter, 0)
|
|
||||||
except:
|
|
||||||
return
|
|
||||||
self.model.add_image(image, fid, only_thumbs)
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_remove_image_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)
|
|
||||||
if not obj.run():
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
ids = self.__get_tv_selection_ids(self.view['result'])
|
|
||||||
for item_id in ids:
|
|
||||||
self.model.del_images(item_id)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "c_search.py: on_remove_thumb_activate(): error on",
|
|
||||||
print "getting selected items or removing thumbnails"
|
|
||||||
return
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_edit_activate(self, menu_item):
|
|
||||||
try:
|
|
||||||
selection = self.view['result'].get_selection()
|
|
||||||
model, list_of_paths = selection.get_selected_rows()
|
|
||||||
fid = model.get_value(model.get_iter(list_of_paths[0]), 0)
|
|
||||||
except TypeError:
|
|
||||||
if __debug__:
|
|
||||||
print "c_main.py: on_edit2_activate(): 0",
|
|
||||||
print "zaznaczonych wierszy"
|
|
||||||
return
|
|
||||||
|
|
||||||
val = self.model.get_file_info(fid)
|
|
||||||
ret = Dialogs.EditDialog(val).run()
|
|
||||||
if ret:
|
|
||||||
self.model.rename(fid, ret['filename'])
|
|
||||||
self.model.update_desc_and_note(fid,
|
|
||||||
ret['description'], ret['note'])
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
|
|
||||||
def on_delete_activate(self, menu_item):
|
|
||||||
dmodel = self.model.discs_tree
|
|
||||||
try:
|
|
||||||
selection = self.view['result'].get_selection()
|
|
||||||
model, list_of_paths = selection.get_selected_rows()
|
|
||||||
except TypeError:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not list_of_paths:
|
|
||||||
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)
|
|
||||||
if not obj.run():
|
|
||||||
return
|
|
||||||
|
|
||||||
def foreach_searchtree(zmodel, zpath, ziter, d):
|
|
||||||
if d[0] == zmodel.get_value(ziter, 0):
|
|
||||||
d[1].append(zpath)
|
|
||||||
return False
|
|
||||||
|
|
||||||
ids = []
|
|
||||||
for p in list_of_paths:
|
|
||||||
ids.append(model.get_value(model.get_iter(p), 0))
|
|
||||||
|
|
||||||
for fid in ids:
|
|
||||||
# delete from db
|
|
||||||
self.model.delete(fid)
|
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
found = self.model.search(self.search_string)
|
|
||||||
self.__set_status_bar(found)
|
|
||||||
return
|
|
||||||
|
|
||||||
def on_rename_activate(self, menu_item):
|
|
||||||
try:
|
|
||||||
selection = self.view['result'].get_selection()
|
|
||||||
model, list_of_paths = selection.get_selected_rows()
|
|
||||||
fid = model.get_value(model.get_iter(list_of_paths[0]), 0)
|
|
||||||
except TypeError:
|
|
||||||
if __debug__:
|
|
||||||
print "c_main.py: on_edit2_activate(): 0",
|
|
||||||
print "zaznaczonych wierszy"
|
|
||||||
return
|
|
||||||
|
|
||||||
fid = model.get_value(model.get_iter(list_of_paths[0]), 0)
|
|
||||||
name = model.get_value(model.get_iter(list_of_paths[0]),2)
|
|
||||||
new_name = Dialogs.InputNewName(name).run()
|
|
||||||
|
|
||||||
if __debug__:
|
|
||||||
print "c_search.py: on_rename_activate(): label:", new_name
|
|
||||||
|
|
||||||
if new_name and new_name != name:
|
|
||||||
self.model.rename(fid, new_name)
|
|
||||||
self.model.unsaved_project = True
|
|
||||||
return
|
|
||||||
|
|
||||||
#####################
|
|
||||||
# observed properetis
|
|
||||||
|
|
||||||
#########################
|
|
||||||
# private class functions
|
|
||||||
def __set_status_bar(self, found):
|
|
||||||
"""sets number of founded items in statusbar"""
|
|
||||||
if found == 0:
|
|
||||||
msg = "No files found."
|
|
||||||
elif found == 1:
|
|
||||||
msg = "Found 1 file."
|
|
||||||
else:
|
|
||||||
msg = "Found %d files." % found
|
|
||||||
self.view['statusbar'].push(self.context_id, "%s" % msg)
|
|
||||||
|
|
||||||
def __get_tv_selection_ids(self, treeview):
|
|
||||||
"""get selection from treeview and return coresponding ids' from
|
|
||||||
connected model or None"""
|
|
||||||
ids = []
|
|
||||||
try:
|
|
||||||
selection = treeview.get_selection()
|
|
||||||
model, list_of_paths = selection.get_selected_rows()
|
|
||||||
for path in list_of_paths:
|
|
||||||
ids.append(model.get_value(model.get_iter(path), 0))
|
|
||||||
return ids
|
|
||||||
except:
|
|
||||||
# DEBUG: treeview have no selection or smth is broken
|
|
||||||
if __debug__:
|
|
||||||
print "c_search.py: __get_tv_selection_ids(): error on",
|
|
||||||
print "getting selected items"
|
|
||||||
return
|
|
||||||
return None
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["model", "view", "controller", "observable", "observer", "support"]
|
|
||||||
|
|
||||||
__version = (1,2,2)
|
|
||||||
|
|
||||||
from model import Model, TreeStoreModel, ListStoreModel, TextBufferModel
|
|
||||||
from model_mt import ModelMT
|
|
||||||
from controller import Controller
|
|
||||||
from view import View
|
|
||||||
from observer import Observer
|
|
||||||
import observable
|
|
||||||
import adapters
|
|
||||||
|
|
||||||
def get_version(): return __version
|
|
||||||
|
|
||||||
def require(ver):
|
|
||||||
if isinstance(ver, str): ver = ver.split(".")
|
|
||||||
ver = tuple(map(int, ver))
|
|
||||||
|
|
||||||
if get_version() < ver:
|
|
||||||
raise AssertionError("gtkmvc required version '%s', found '%s'"\
|
|
||||||
% (ver, get_version()))
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2007 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
from gtkmvc.adapters.basic import Adapter, UserClassAdapter, RoUserClassAdapter
|
|
||||||
from gtkmvc.adapters.containers import StaticContainerAdapter
|
|
||||||
@@ -1,428 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2007 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
import types
|
|
||||||
import gtk
|
|
||||||
import time
|
|
||||||
|
|
||||||
from gtkmvc.adapters.default import *
|
|
||||||
from gtkmvc.observer import Observer
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class Adapter (Observer):
|
|
||||||
|
|
||||||
def __init__(self, model, prop_name,
|
|
||||||
prop_read=None, prop_write=None,
|
|
||||||
value_error=None):
|
|
||||||
"""
|
|
||||||
Creates a new adapter that handles setting of value of a
|
|
||||||
model single model property when a corresponding widgets set
|
|
||||||
is changed and viceversa when the property is also
|
|
||||||
observable.
|
|
||||||
|
|
||||||
This class handles only assignments to properties. For other
|
|
||||||
kinds of setting (e.g. user-defined classes used as
|
|
||||||
observable properties, containers, etc.) use other types of
|
|
||||||
Adapters derived from this class.
|
|
||||||
|
|
||||||
prop_name is the model's property name (as a string). It is
|
|
||||||
possible to use a dotted notation to identify a property
|
|
||||||
contained into a hierarchy of models. For example 'a.b.c'
|
|
||||||
identifies property 'c' into model 'b' inside model 'a',
|
|
||||||
where model 'a' is an attribute of given top level model.
|
|
||||||
Last name must be an observable or non-observable attribute,
|
|
||||||
and previous names (if specified) must all refer to
|
|
||||||
instances of class Model. First name from the left must be
|
|
||||||
the name of a model instance inside the given model.
|
|
||||||
|
|
||||||
prop_{write,read} are two optional functions that apply
|
|
||||||
custom modifications to the value of the property before
|
|
||||||
setting and reading it. Both take a value and must return a
|
|
||||||
transformed value whose type must be compatible with the
|
|
||||||
type of the property.
|
|
||||||
|
|
||||||
value_error can be a function (or a method) to be called
|
|
||||||
when a ValueError exception occurs while trying to set a
|
|
||||||
wrong value for the property inside the model. The function
|
|
||||||
will receive: the adapter, the property name and the value
|
|
||||||
coming from the widget that offended the model.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# registration is delayed, as we need to create possible
|
|
||||||
# listener before:
|
|
||||||
Observer.__init__(self)
|
|
||||||
|
|
||||||
self._prop_name = prop_name
|
|
||||||
self._prop_read = prop_read
|
|
||||||
self._prop_write = prop_write
|
|
||||||
self._value_error = value_error
|
|
||||||
self._wid = None
|
|
||||||
self._wid_info = {}
|
|
||||||
|
|
||||||
# this flag is set when self is changing the property or the
|
|
||||||
# widget, in order to avoid infinite looping.
|
|
||||||
self._itsme = False
|
|
||||||
|
|
||||||
self._connect_model(model)
|
|
||||||
return
|
|
||||||
|
|
||||||
def connect_widget(self, wid,
|
|
||||||
getter=None, setter=None,
|
|
||||||
signal=None, arg=None, update=True):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Called when the widget is instantiated, and the adapter is
|
|
||||||
ready to connect the widget and the property inside the
|
|
||||||
observed model. arg is the (optional) argument that will be
|
|
||||||
passed when connecting the signal.
|
|
||||||
|
|
||||||
getter and setter are the (optional) methods used
|
|
||||||
for reading and writing the widget's value. When not
|
|
||||||
specified, default getter and setter will be guessed by
|
|
||||||
looking at the widget type the adapter will be connected
|
|
||||||
with. Guessing is carried out by querying information
|
|
||||||
specified into module 'adapters.default'.
|
|
||||||
|
|
||||||
Finally, if update is false, the widget will not be updated
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._wid_info.has_key(wid):
|
|
||||||
raise ValueError("Widget " + str(wid) + " was already connected")
|
|
||||||
|
|
||||||
wid_type = None
|
|
||||||
|
|
||||||
if None in (getter, setter, signal):
|
|
||||||
w = search_adapter_info(wid)
|
|
||||||
if getter is None: getter = w[GETTER]
|
|
||||||
if setter is None:
|
|
||||||
setter = w[SETTER]
|
|
||||||
wid_type = w[WIDTYPE]
|
|
||||||
pass
|
|
||||||
|
|
||||||
if signal is None: signal = w[SIGNAL]
|
|
||||||
pass
|
|
||||||
|
|
||||||
# saves information about the widget
|
|
||||||
self._wid_info[wid] = (getter, setter, wid_type)
|
|
||||||
|
|
||||||
# connects the widget
|
|
||||||
if signal:
|
|
||||||
if arg: wid.connect(signal, self._on_wid_changed, arg)
|
|
||||||
else: wid.connect(signal, self._on_wid_changed)
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._wid = wid
|
|
||||||
|
|
||||||
# updates the widget:
|
|
||||||
if update: self.update_widget()
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_model(self):
|
|
||||||
"""Forces the property to be updated from the value hold by
|
|
||||||
the widget. This method should be called directly by the
|
|
||||||
user in very unusual conditions."""
|
|
||||||
self._write_property(self._read_widget())
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_widget(self):
|
|
||||||
"""Forces the widget to be updated from the property
|
|
||||||
value. This method should be called directly by the user
|
|
||||||
when the property is not observable, or in very unusual
|
|
||||||
conditions."""
|
|
||||||
self._write_widget(self._read_property())
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Private methods
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
def _connect_model(self, model):
|
|
||||||
"""
|
|
||||||
Used internally to connect the property into the model, and
|
|
||||||
register self as a value observer for that property"""
|
|
||||||
|
|
||||||
parts = self._prop_name.split(".")
|
|
||||||
if len(parts) > 1:
|
|
||||||
# identifies the model
|
|
||||||
models = parts[:-1]
|
|
||||||
for name in models:
|
|
||||||
model = getattr(model, name)
|
|
||||||
if not isinstance(model, gtkmvc.Model):
|
|
||||||
raise TypeError("Attribute '" + name +
|
|
||||||
"' was expected to be a Model, but found: " +
|
|
||||||
str(model))
|
|
||||||
pass
|
|
||||||
prop = parts[-1]
|
|
||||||
else: prop = parts[0]
|
|
||||||
|
|
||||||
# prop is inside model?
|
|
||||||
if not hasattr(model, prop):
|
|
||||||
raise ValueError("Attribute '" + prop +
|
|
||||||
"' not found in model " + str(model))
|
|
||||||
|
|
||||||
# is it observable?
|
|
||||||
if model.has_property(prop):
|
|
||||||
# we need to create an observing method before registering
|
|
||||||
self._add_method(self._get_observer_src(prop))
|
|
||||||
pass
|
|
||||||
|
|
||||||
self._prop = getattr(model, prop)
|
|
||||||
self._prop_name = prop
|
|
||||||
|
|
||||||
# registration of model:
|
|
||||||
self.register_model(model)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _get_observer_src(self, prop_name):
|
|
||||||
"""This is the code for an value change observer"""
|
|
||||||
return """def property_%s_value_change(self, model, old, new):
|
|
||||||
if self._itsme or old == new: return
|
|
||||||
self._on_prop_changed()""" % prop_name
|
|
||||||
|
|
||||||
|
|
||||||
def _add_method(self, src):
|
|
||||||
"""Private service to add a new method to the instance,
|
|
||||||
given method code"""
|
|
||||||
|
|
||||||
from gtkmvc.support.utils import get_function_from_source
|
|
||||||
import new
|
|
||||||
|
|
||||||
func = get_function_from_source(src)
|
|
||||||
meth = new.instancemethod(func, self, self.__class__)
|
|
||||||
setattr(self, func.__name__, meth)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _get_property(self):
|
|
||||||
"""Private method that returns the value currently stored
|
|
||||||
into the property"""
|
|
||||||
return getattr(self.get_model(), self._prop_name)
|
|
||||||
#return self._prop # bug fix reported by A. Dentella
|
|
||||||
|
|
||||||
def _set_property(self, val):
|
|
||||||
"""Private method that sets the value currently of the property."""
|
|
||||||
return setattr(self.get_model(), self._prop_name, val)
|
|
||||||
|
|
||||||
def _read_property(self, *args):
|
|
||||||
"""Returns the (possibly transformed) value that is stored
|
|
||||||
into the property"""
|
|
||||||
if self._prop_read: return self._prop_read(self._get_property(*args))
|
|
||||||
return self._get_property(*args)
|
|
||||||
|
|
||||||
def _write_property(self, val, *args):
|
|
||||||
"""Sets the value of property. Given val is transformed
|
|
||||||
accodingly to prop_write function when specified at
|
|
||||||
construction-time. A try to cast the value to the property
|
|
||||||
type is given."""
|
|
||||||
|
|
||||||
# 'finally' would be better here, but not supported in 2.4 :(
|
|
||||||
try:
|
|
||||||
totype = type(self._get_property(*args))
|
|
||||||
val_prop = self._cast_value(val, totype)
|
|
||||||
if self._prop_write: val_prop = self._prop_write(val_prop)
|
|
||||||
|
|
||||||
self._itsme = True
|
|
||||||
self._set_property(val_prop, *args)
|
|
||||||
|
|
||||||
except ValueError:
|
|
||||||
self._itsme = False
|
|
||||||
if self._value_error: self._value_error(self, self._prop_name, val)
|
|
||||||
else: raise
|
|
||||||
pass
|
|
||||||
|
|
||||||
except: self._itsme = False; raise
|
|
||||||
|
|
||||||
self._itsme = False
|
|
||||||
return
|
|
||||||
|
|
||||||
def _read_widget(self):
|
|
||||||
"""Returns the value currently stored into the widget, after
|
|
||||||
transforming it accordingly to possibly specified function."""
|
|
||||||
getter = self._wid_info[self._wid][0]
|
|
||||||
return getter(self._wid)
|
|
||||||
|
|
||||||
def _write_widget(self, val):
|
|
||||||
"""Writes value into the widget. If specified, user setter
|
|
||||||
is invoked."""
|
|
||||||
self._itsme = True
|
|
||||||
try:
|
|
||||||
setter = self._wid_info[self._wid][1]
|
|
||||||
wtype = self._wid_info[self._wid][2]
|
|
||||||
if wtype is not None: setter(self._wid, self._cast_value(val, wtype))
|
|
||||||
else: setter(self._wid, val)
|
|
||||||
finally:
|
|
||||||
self._itsme = False
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def _cast_value(self, val, totype):
|
|
||||||
"""Casts given val to given totype. Raises TypeError if not able to cast."""
|
|
||||||
t = type(val)
|
|
||||||
if issubclass(t, totype): return val
|
|
||||||
if issubclass(totype, types.StringType): return str(val)
|
|
||||||
if issubclass(t, types.StringType):
|
|
||||||
if issubclass(totype, types.IntType):
|
|
||||||
if val: return int(float(val))
|
|
||||||
return 0
|
|
||||||
if issubclass(totype, types.FloatType):
|
|
||||||
if val: return float(val)
|
|
||||||
return 0.0
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise TypeError("Not able to cast " + str(t) + " to " + str(totype))
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Callbacks and observation
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _on_wid_changed(self, wid):
|
|
||||||
"""Called when the widget is changed"""
|
|
||||||
if self._itsme: return
|
|
||||||
self.update_model()
|
|
||||||
return
|
|
||||||
|
|
||||||
def _on_prop_changed(self):
|
|
||||||
"""Called by the observation code, when the value in the
|
|
||||||
observed property is changed"""
|
|
||||||
if not self._itsme: self.update_widget()
|
|
||||||
return
|
|
||||||
|
|
||||||
pass # end of class Adapter
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
class UserClassAdapter (Adapter):
|
|
||||||
"""
|
|
||||||
This class handles the communication between a widget and a
|
|
||||||
class instance (possibly observable) that is a property inside
|
|
||||||
the model. The value to be shown is taken and stored by using a
|
|
||||||
getter and a setter. getter and setter can be: names of user
|
|
||||||
class methods, bound or unbound methods of the user class, or a
|
|
||||||
function that will receive the user class instance and possible
|
|
||||||
arguments whose number depends on whether it is a getter or a
|
|
||||||
setter."""
|
|
||||||
|
|
||||||
def __init__(self, model, prop_name,
|
|
||||||
getter, setter,
|
|
||||||
prop_read=None, prop_write=None,
|
|
||||||
value_error=None):
|
|
||||||
|
|
||||||
Adapter.__init__(self, model, prop_name,
|
|
||||||
prop_read, prop_write, value_error)
|
|
||||||
|
|
||||||
self._getter = self._resolve_to_func(getter)
|
|
||||||
self._setter = self._resolve_to_func(setter)
|
|
||||||
return
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Private methods
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _resolve_to_func(self, what):
|
|
||||||
"""This method resolves whatever is passed: a string, a
|
|
||||||
bound or unbound method, a function, to make it a
|
|
||||||
function. This makes internal handling of setter and getter
|
|
||||||
uniform and easier."""
|
|
||||||
if isinstance(what, types.StringType):
|
|
||||||
what = getattr(Adapter._get_property(self), what)
|
|
||||||
pass
|
|
||||||
|
|
||||||
# makes it an unbounded function if needed
|
|
||||||
if type(what) == types.MethodType: what = what.im_func
|
|
||||||
|
|
||||||
if not type(what) == types.FunctionType: raise TypeError("Expected a method name, a method or a function")
|
|
||||||
return what
|
|
||||||
|
|
||||||
|
|
||||||
def _get_observer_src(self, prop_name):
|
|
||||||
"""This is the code for a method after_change observer"""
|
|
||||||
return """def property_%s_after_change(self, model, \
|
|
||||||
instance, meth_name, res, args, kwargs):
|
|
||||||
if self._itsme: return
|
|
||||||
self._on_prop_changed(instance, meth_name, res, args, kwargs)""" % prop_name
|
|
||||||
|
|
||||||
|
|
||||||
def _on_prop_changed(self, instance, meth_name, res, args, kwargs):
|
|
||||||
"""Called by the observation code, when a modifying method
|
|
||||||
is called"""
|
|
||||||
Adapter._on_prop_changed(self)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _get_property(self, *args):
|
|
||||||
"""Private method that returns the value currently stored
|
|
||||||
into the property"""
|
|
||||||
val = self._getter(Adapter._get_property(self), *args)
|
|
||||||
if self._prop_read: return self._prop_read(val, *args)
|
|
||||||
return val
|
|
||||||
|
|
||||||
def _set_property(self, val, *args):
|
|
||||||
"""Private method that sets the value currently of the property"""
|
|
||||||
if self._prop_write: val = self._prop_write(val)
|
|
||||||
return self._setter(Adapter._get_property(self), val, *args)
|
|
||||||
|
|
||||||
pass # end of class UserClassAdapter
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#----------------------------------------------------------------------
|
|
||||||
class RoUserClassAdapter (UserClassAdapter):
|
|
||||||
"""
|
|
||||||
This class is for Read-Only user classes. RO classes are those
|
|
||||||
whose setting methods do not change the instance, but return a
|
|
||||||
new instance that has been changed accordingly to the setters
|
|
||||||
semantics. An example is python datetime class, whose replace
|
|
||||||
method does not change the instance it is invoked on, but
|
|
||||||
returns a new datetime instance.
|
|
||||||
|
|
||||||
This class is likely to be used very rarely.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, model, prop_name,
|
|
||||||
getter, setter,
|
|
||||||
prop_read=None, prop_write=None,
|
|
||||||
value_error=None):
|
|
||||||
|
|
||||||
UserClassAdapter.__init__(self, model, prop_name,
|
|
||||||
getter, setter,
|
|
||||||
prop_read, prop_write, value_error)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Private methods
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _set_property(self, val, *args):
|
|
||||||
"""Private method that sets the value currently of the property"""
|
|
||||||
val = UserClassAdapter._set_property(self, val, *args)
|
|
||||||
if val: Adapter._set_property(self, val, *args)
|
|
||||||
return val
|
|
||||||
|
|
||||||
pass # end of class RoUserClassAdapter
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2007 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
import types
|
|
||||||
import gtk
|
|
||||||
|
|
||||||
from gtkmvc.adapters.basic import UserClassAdapter, Adapter
|
|
||||||
|
|
||||||
from gtkmvc.adapters.default import *
|
|
||||||
from gtkmvc.observer import Observer
|
|
||||||
from gtkmvc.support.wrappers import ObsMapWrapper
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class StaticContainerAdapter (UserClassAdapter):
|
|
||||||
"""
|
|
||||||
This class can be used to bound a set of widgets to a property
|
|
||||||
that is a container, like a tuple, a list or a map, or in
|
|
||||||
general a class that implements __getitem__ and __setitem__
|
|
||||||
methods.
|
|
||||||
|
|
||||||
From the other hand, the set of widgets can be a list provided
|
|
||||||
by the user, or a container widget like a Box, a notebook, etc.
|
|
||||||
Widgets will be linked by their position when the property is
|
|
||||||
list-like, or by their name when the property is map-like.
|
|
||||||
|
|
||||||
This class supports only properties that are static containers,
|
|
||||||
i.e. those containers that do not change their length
|
|
||||||
dynamically. If the container grows up in length, no change will
|
|
||||||
occur in the view-side.
|
|
||||||
"""
|
|
||||||
def __init__(self, model, prop_name,
|
|
||||||
prop_read=None, prop_write=None, value_error=None):
|
|
||||||
|
|
||||||
UserClassAdapter.__init__(self, model, prop_name,
|
|
||||||
lambda c,i: c.__getitem__(i),
|
|
||||||
lambda c,v,i: c.__setitem__(i,v),
|
|
||||||
prop_read, prop_write,
|
|
||||||
value_error)
|
|
||||||
|
|
||||||
prop = Adapter._get_property(self)
|
|
||||||
#prop = self._get_property() # bug fix reported by A. Dentella
|
|
||||||
if not (hasattr(prop, "__getitem__") and
|
|
||||||
hasattr(prop, "__setitem__")):
|
|
||||||
raise TypeError("Property " + self._prop_name +
|
|
||||||
" is not a valid container")
|
|
||||||
|
|
||||||
|
|
||||||
self._prop_is_map = isinstance(prop, types.DictType) or \
|
|
||||||
isinstance(prop, ObsMapWrapper)
|
|
||||||
# contained widgets
|
|
||||||
self._idx2wid = {}
|
|
||||||
self._wid2idx = {}
|
|
||||||
|
|
||||||
self._widgets = None
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def connect_widget(self, wid, getters=None, setters=None,
|
|
||||||
signals=None, arg=None):
|
|
||||||
"""
|
|
||||||
Called when the widget is instantiated, and the adapter is
|
|
||||||
ready to connect the widgets inside it (if a container) or
|
|
||||||
each widget if wid is a list of widgets. getters and setters
|
|
||||||
can be None, a function or a list or a map of
|
|
||||||
functions. signals can be None, a signal name, or a list or
|
|
||||||
a map of signal names. When maps are used, keys can be
|
|
||||||
widgets or widget names. The length of the possible lists or
|
|
||||||
maps must be lesser or equal to the number of widgets that
|
|
||||||
will be connected.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if isinstance(wid, gtk.Container): self._widgets = wid.get_children()
|
|
||||||
elif isinstance(wid, types.ListType) or isinstance(wid, types.TupleType): self._widgets = wid
|
|
||||||
else: raise TypeError("widget must be either a gtk.Container or a list or tuple")
|
|
||||||
|
|
||||||
# prepares the mappings:
|
|
||||||
for idx, w in enumerate(self._widgets):
|
|
||||||
if self._prop_is_map: idx=w.get_name()
|
|
||||||
self._idx2wid[idx] = w
|
|
||||||
self._wid2idx[w] = idx
|
|
||||||
pass
|
|
||||||
|
|
||||||
# prepares the lists for signals
|
|
||||||
getters = self.__handle_par("getters", getters)
|
|
||||||
setters = self.__handle_par("setters", setters)
|
|
||||||
signals = self.__handle_par("signals", signals)
|
|
||||||
|
|
||||||
for wi,ge,se,si in zip(self._widgets, getters, setters, signals):
|
|
||||||
if type(ge) == types.MethodType: ge = ge.im_func
|
|
||||||
if type(se) == types.MethodType: se = se.im_func
|
|
||||||
UserClassAdapter.connect_widget(self, wi, ge, se, si, arg, False)
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.update_widget()
|
|
||||||
self._wid = wid
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def update_model(self, idx=None):
|
|
||||||
"""Updates the value of property at given index. If idx is
|
|
||||||
None, all controlled indices will be updated. This method
|
|
||||||
should be called directly by the user in very unusual
|
|
||||||
conditions."""
|
|
||||||
if idx is None:
|
|
||||||
for w in self._widgets:
|
|
||||||
idx = self._get_idx_from_widget(w)
|
|
||||||
self._write_property(self._read_widget(idx), idx)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
else: self._write_property(self._read_widget(idx), idx)
|
|
||||||
return
|
|
||||||
|
|
||||||
def update_widget(self, idx=None):
|
|
||||||
"""Forces the widget at given index to be updated from the
|
|
||||||
property value. If index is not given, all controlled
|
|
||||||
widgets will be updated. This method should be called
|
|
||||||
directly by the user when the property is not observable, or
|
|
||||||
in very unusual conditions."""
|
|
||||||
if idx is None:
|
|
||||||
for w in self._widgets:
|
|
||||||
idx = self._get_idx_from_widget(w)
|
|
||||||
self._write_widget(self._read_property(idx), idx)
|
|
||||||
pass
|
|
||||||
else: self._write_widget(self._read_property(idx), idx)
|
|
||||||
return
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# Private methods
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _get_idx_from_widget(self, wid):
|
|
||||||
"""Given a widget, returns the corresponding index for the
|
|
||||||
model. Returned value can be either an integer or a string"""
|
|
||||||
return self._wid2idx[wid]
|
|
||||||
|
|
||||||
def _get_widget_from_idx(self, idx):
|
|
||||||
"""Given an index, returns the corresponding widget for the view.
|
|
||||||
Given index can be either an integer or a string"""
|
|
||||||
return self._idx2wid[idx]
|
|
||||||
|
|
||||||
|
|
||||||
def _read_widget(self, idx):
|
|
||||||
sav = self._wid
|
|
||||||
self._wid = self._get_widget_from_idx(idx)
|
|
||||||
val = UserClassAdapter._read_widget(self)
|
|
||||||
self._wid = sav
|
|
||||||
return val
|
|
||||||
|
|
||||||
def _write_widget(self, val, idx):
|
|
||||||
sav = self._wid
|
|
||||||
self._wid = self._get_widget_from_idx(idx)
|
|
||||||
UserClassAdapter._write_widget(self, val)
|
|
||||||
self._wid = sav
|
|
||||||
return
|
|
||||||
|
|
||||||
# This is a private service to factorize code of connect_widget
|
|
||||||
def __handle_par(self, name, par):
|
|
||||||
if par is None or type(par) in (types.FunctionType,
|
|
||||||
types.MethodType, types.StringType):
|
|
||||||
par = [par] * len(self._widgets)
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif isinstance(par, types.DictType):
|
|
||||||
val = []
|
|
||||||
for w in self._widgets:
|
|
||||||
if par.has_key(w): val.append(par[w])
|
|
||||||
elif par.has_key(w.get_name()): val.append(par[w.get_name()])
|
|
||||||
else: val.append(None)
|
|
||||||
pass
|
|
||||||
par = val
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif isinstance(par, types.ListType) or isinstance(par, types.TupleType):
|
|
||||||
par = list(par)
|
|
||||||
par.extend([None]*(len(self._widgets)-len(par)))
|
|
||||||
pass
|
|
||||||
|
|
||||||
else: raise TypeError("Parameter %s has an invalid type (should be None, a sequence or a string)" % name)
|
|
||||||
|
|
||||||
return par
|
|
||||||
|
|
||||||
|
|
||||||
# Callbacks:
|
|
||||||
def _on_wid_changed(self, wid):
|
|
||||||
"""Called when the widget is changed"""
|
|
||||||
if self._itsme: return
|
|
||||||
self.update_model(self._get_idx_from_widget(wid))
|
|
||||||
return
|
|
||||||
|
|
||||||
def _on_prop_changed(self, instance, meth_name, res, args, kwargs):
|
|
||||||
"""Called by the observation code, we are interested in
|
|
||||||
__setitem__"""
|
|
||||||
if not self._itsme and meth_name == "__setitem__": self.update_widget(args[0])
|
|
||||||
return
|
|
||||||
|
|
||||||
pass # end of class StaticContainerAdapter
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
__all__ = ("search_adapter_info",
|
|
||||||
"SIGNAL", "GETTER", "SETTER", "WIDTYPE")
|
|
||||||
|
|
||||||
import types
|
|
||||||
import gtk
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# This list defines a default behavior for widgets.
|
|
||||||
# If no particular behaviour has been specified, adapters will
|
|
||||||
# use information contained into this list to create themself.
|
|
||||||
# This list is ordered: the earlier a widget occurs, the better it
|
|
||||||
# will be matched by the search function.
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
__def_adapter = [ # class, default signal, getter, setter, value type
|
|
||||||
(gtk.Entry, "changed", gtk.Entry.get_text, gtk.Entry.set_text, types.StringType),
|
|
||||||
(gtk.Label, None, gtk.Label.get_text, gtk.Label.set_text, types.StringType),
|
|
||||||
(gtk.Arrow, None, lambda a: a.get_property("arrow-type"),
|
|
||||||
lambda a,v: a.set(v,a.get_property("shadow-type")), gtk.ArrowType),
|
|
||||||
(gtk.ToggleButton, "toggled", gtk.ToggleButton.get_active, gtk.ToggleButton.set_active, types.BooleanType),
|
|
||||||
(gtk.CheckMenuItem, "toggled", gtk.CheckMenuItem.get_active, gtk.CheckMenuItem.set_active, types.BooleanType),
|
|
||||||
(gtk.Expander, "activate", lambda w: not w.get_expanded(), gtk.Expander.set_expanded, types.BooleanType),
|
|
||||||
(gtk.ColorButton, "color-set", gtk.ColorButton.get_color, gtk.ColorButton.set_color, gtk.gdk.Color),
|
|
||||||
(gtk.ColorSelection, "color-changed", gtk.ColorSelection.get_current_color, gtk.ColorSelection.set_current_color, gtk.gdk.Color),
|
|
||||||
]
|
|
||||||
|
|
||||||
if gtk.pygtk_version >= (2,10,0):
|
|
||||||
# conditionally added support
|
|
||||||
__def_adapter.append(
|
|
||||||
(gtk.LinkButton, "clicked", gtk.LinkButton.get_uri, gtk.LinkButton.set_uri, types.StringType))
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# constants to access values:
|
|
||||||
SIGNAL =1
|
|
||||||
GETTER =2
|
|
||||||
SETTER =3
|
|
||||||
WIDTYPE =4
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# To optimize the search
|
|
||||||
__memoize__ = {}
|
|
||||||
def search_adapter_info(wid):
|
|
||||||
"""Given a widget returns the default tuple found in __def_adapter"""
|
|
||||||
t = type(wid)
|
|
||||||
if __memoize__.has_key(t): return __memoize__[t]
|
|
||||||
|
|
||||||
for w in __def_adapter:
|
|
||||||
if isinstance(wid, w[0]):
|
|
||||||
__memoize__[t] = w
|
|
||||||
return w
|
|
||||||
pass
|
|
||||||
|
|
||||||
raise TypeError("Adapter type " + str(t) + " not found among supported adapters")
|
|
||||||
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
from gtkmvc.observer import Observer
|
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
class Controller (Observer):
|
|
||||||
"""
|
|
||||||
We put all of our gtk signal handlers into a class. This lets
|
|
||||||
us bind all of them at once, because their names are in the
|
|
||||||
class dict. This class automatically register its instances as
|
|
||||||
observers into the corresponding model. Also, when a view is
|
|
||||||
created, the view calls method register_view, which can be
|
|
||||||
oveloaded in order to connect signals and perform other specific
|
|
||||||
operation. A controller possibly handles and contains also a set
|
|
||||||
of adapters that makes easier to connect widgets and observable
|
|
||||||
properties into the model.
|
|
||||||
|
|
||||||
parameter spurious controls the way spurious value change
|
|
||||||
notifications are handled. If True, assignments to observable
|
|
||||||
properties that do not actually change the value are
|
|
||||||
notified anyway."""
|
|
||||||
|
|
||||||
def __init__(self, model, spurious=False):
|
|
||||||
Observer.__init__(self, model, spurious)
|
|
||||||
|
|
||||||
self.view = None
|
|
||||||
self.__adapters = []
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_view(self, view):
|
|
||||||
"""
|
|
||||||
This method is called by the framework when registering a
|
|
||||||
view. Derived classes can exploit this call to connect
|
|
||||||
signals manually, create and connect treeview, textview,
|
|
||||||
etc.
|
|
||||||
"""
|
|
||||||
assert(self.view is None)
|
|
||||||
self.view = view
|
|
||||||
self.register_adapters()
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_adapters(self):
|
|
||||||
"""
|
|
||||||
This method is called by register_view during view
|
|
||||||
registration process, when it is time to possibly create all
|
|
||||||
adapters. model and view can safely be taken from self.model
|
|
||||||
and self.view. Derived classes can specilize this method. In
|
|
||||||
this implementation the method does nothing.
|
|
||||||
"""
|
|
||||||
assert(self.model is not None)
|
|
||||||
assert(self.view is not None)
|
|
||||||
return
|
|
||||||
|
|
||||||
def adapt(self, *args):
|
|
||||||
"""
|
|
||||||
Adapts a set of (observable) properties and a set of
|
|
||||||
widgets, by connecting them.
|
|
||||||
|
|
||||||
This method can be used to simplify the creation of one or
|
|
||||||
more adapters, by letting the framework do most of the work
|
|
||||||
needed to connect ('adapt') properties from one hand, and
|
|
||||||
widgets on the other.
|
|
||||||
|
|
||||||
This method is provided in three flavours:
|
|
||||||
|
|
||||||
1. An instance of an Adapter class can be created by the
|
|
||||||
caller and passed as a unique argument. The adapter must
|
|
||||||
be already fully configured.
|
|
||||||
|
|
||||||
2. The name of a model's property is passed as a unique
|
|
||||||
argument. A correponding widget name is searched and if
|
|
||||||
found, an adapter is created. Name matching is performed
|
|
||||||
by searching into view's widget names for words that end
|
|
||||||
with the given property name. Matching is case
|
|
||||||
insensitive and words can be separated by spaces,
|
|
||||||
underscore (_) and CapitalizedWords. For example property
|
|
||||||
'prop' will match widget 'cb_prop'. If no matches or
|
|
||||||
multiple matches are found, a ValueError will be raised.
|
|
||||||
The way names are matched can be customized by deriving
|
|
||||||
method match_prop_name.
|
|
||||||
|
|
||||||
3. Two strings can be passed, respectively containing the
|
|
||||||
name of an observable property in the model, and the name
|
|
||||||
of a widget in the view.
|
|
||||||
|
|
||||||
Flavour 1 allows for a full control, but flavour 2 and 3
|
|
||||||
make easier to create adpaters with basic (default)
|
|
||||||
behaviour.
|
|
||||||
|
|
||||||
This method can be called into the method register_adapters
|
|
||||||
which derived classes can specialise.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# checks arguments
|
|
||||||
n = len(args)
|
|
||||||
if n not in (1,2): raise TypeError("adapt() takes 1 or 2 arguments (%d given)" % n)
|
|
||||||
|
|
||||||
if n == 1: #one argument
|
|
||||||
from gtkmvc.adapters.basic import Adapter
|
|
||||||
|
|
||||||
if isinstance(args[0], Adapter): adapters = (args[0],)
|
|
||||||
|
|
||||||
elif isinstance(args[0], types.StringType):
|
|
||||||
prop_name = args[0]
|
|
||||||
names = []
|
|
||||||
for k in self.view:
|
|
||||||
if self.__match_prop_name(prop_name, k): names.append(k)
|
|
||||||
pass
|
|
||||||
if len(names) != 1:
|
|
||||||
raise ValueError("%d widget candidates match property '%s': %s" % \
|
|
||||||
(len(names), prop_name, names))
|
|
||||||
wid_name = names[0]
|
|
||||||
adapters = self.__create_adapters__(prop_name, wid_name)
|
|
||||||
pass
|
|
||||||
else: raise TypeError("Argument of adapt() must be an Adapter or a string")
|
|
||||||
|
|
||||||
else: # two arguments
|
|
||||||
if not (isinstance(args[0], types.StringType) and
|
|
||||||
isinstance(args[1], types.StringType)):
|
|
||||||
raise TypeError("Arguments of adapt() must be two strings")
|
|
||||||
|
|
||||||
# retrieves both property and widget, and creates an adapter
|
|
||||||
prop_name, wid_name = args
|
|
||||||
adapters = self.__create_adapters__(prop_name, wid_name)
|
|
||||||
pass
|
|
||||||
|
|
||||||
for ad in adapters: self.__adapters.append(ad)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def __match_prop_name(self, prop_name, wid_name):
|
|
||||||
"""
|
|
||||||
Used internally when searching for a suitable widget. To customize
|
|
||||||
its behaviour, re-implement this method into derived classes
|
|
||||||
"""
|
|
||||||
return wid_name.lower().endswith(prop_name.lower())
|
|
||||||
|
|
||||||
|
|
||||||
def __create_adapters__(self, prop_name, wid_name):
|
|
||||||
"""
|
|
||||||
Private service that looks at property and widgets types,
|
|
||||||
and possibly creates one or more (best) fitting adapters
|
|
||||||
that are returned as a list.
|
|
||||||
"""
|
|
||||||
from gtkmvc.adapters.basic import Adapter, RoUserClassAdapter
|
|
||||||
from gtkmvc.adapters.containers import StaticContainerAdapter
|
|
||||||
import gtk
|
|
||||||
|
|
||||||
res = []
|
|
||||||
|
|
||||||
wid = self.view[wid_name]
|
|
||||||
if wid is None: raise ValueError("Widget '%s' not found" % wid_name)
|
|
||||||
|
|
||||||
# Decides the type of adapters to be created.
|
|
||||||
if isinstance(wid, gtk.Calendar):
|
|
||||||
# calendar creates three adapter for year, month and day
|
|
||||||
ad = RoUserClassAdapter(self.model, prop_name,
|
|
||||||
lambda d: d.year, lambda d,y: d.replace(year=y))
|
|
||||||
ad.connect_widget(wid, lambda c: c.get_date()[0],
|
|
||||||
lambda c,y: c.select_month(c.get_date()[1], y),
|
|
||||||
"day-selected")
|
|
||||||
res.append(ad) # year
|
|
||||||
|
|
||||||
ad = RoUserClassAdapter(self.model, prop_name,
|
|
||||||
lambda d: d.month, lambda d,m: d.replace(month=m))
|
|
||||||
ad.connect_widget(wid, lambda c: c.get_date()[1]+1,
|
|
||||||
lambda c,m: c.select_month(m-1, c.get_date()[0]),
|
|
||||||
"day-selected")
|
|
||||||
res.append(ad) # month
|
|
||||||
|
|
||||||
ad = RoUserClassAdapter(self.model, prop_name,
|
|
||||||
lambda d: d.day, lambda d,v: d.replace(day=v))
|
|
||||||
ad.connect_widget(wid, lambda c: c.get_date()[2],
|
|
||||||
lambda c,d: c.select_day(d),
|
|
||||||
"day-selected")
|
|
||||||
res.append(ad) # day
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
try: # tries with StaticContainerAdapter
|
|
||||||
ad = StaticContainerAdapter(self.model, prop_name)
|
|
||||||
ad.connect_widget(wid)
|
|
||||||
res.append(ad)
|
|
||||||
|
|
||||||
except TypeError:
|
|
||||||
# falls back to a simple adapter
|
|
||||||
ad = Adapter(self.model, prop_name)
|
|
||||||
ad.connect_widget(wid)
|
|
||||||
res.append(ad)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
pass # end of class Controller
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free
|
|
||||||
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
import support.metaclasses
|
|
||||||
from support.wrappers import ObsWrapperBase
|
|
||||||
from observable import Signal
|
|
||||||
|
|
||||||
|
|
||||||
class Model (object):
|
|
||||||
"""
|
|
||||||
This class is the application model base class. It handles a set
|
|
||||||
of observable properties which you are interested in showing by
|
|
||||||
one ore more view - via one or more observers of course. The
|
|
||||||
mechanism is the following:
|
|
||||||
|
|
||||||
1. You are interested in showing a set of model property, that
|
|
||||||
you can declare in the __properties__ member map.
|
|
||||||
|
|
||||||
2. You define one or more observers that observe one or more
|
|
||||||
properties you registered. When someone changes a property
|
|
||||||
value the model notifies the changing to each observer.
|
|
||||||
|
|
||||||
The property-observer[s] association is given by the implicit
|
|
||||||
rule in observers method names: if you want the model notified
|
|
||||||
the changing event of the value of the property 'p' you have to
|
|
||||||
define the method called 'property_p_value_change' in each
|
|
||||||
listening observer class.
|
|
||||||
|
|
||||||
Notice that tipically 'controllers' implement the observer
|
|
||||||
pattern. The notification method gets the emitting model, the
|
|
||||||
old value for the property and the new one. Properties
|
|
||||||
functionalities are automatically provided by the
|
|
||||||
ObservablePropertyMeta meta-class."""
|
|
||||||
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyMeta
|
|
||||||
__properties__ = {} # override this
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
object.__init__(self)
|
|
||||||
self.__observers = []
|
|
||||||
# keys are properties names, values are methods inside the observer:
|
|
||||||
self.__value_notifications = {}
|
|
||||||
self.__instance_notif_before = {}
|
|
||||||
self.__instance_notif_after = {}
|
|
||||||
self.__signal_notif = {}
|
|
||||||
|
|
||||||
for key in (self.__properties__.keys() + self.__derived_properties__.keys()):
|
|
||||||
self.register_property(key)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_property(self, name):
|
|
||||||
"""Registers an existing property to be monitored, and sets
|
|
||||||
up notifiers for notifications"""
|
|
||||||
if not self.__value_notifications.has_key(name):
|
|
||||||
self.__value_notifications[name] = []
|
|
||||||
pass
|
|
||||||
|
|
||||||
# registers observable wrappers
|
|
||||||
prop = getattr(self, "_prop_%s" % name)
|
|
||||||
|
|
||||||
if isinstance(prop, ObsWrapperBase):
|
|
||||||
prop.__set_model__(self, name)
|
|
||||||
|
|
||||||
if isinstance(prop, Signal):
|
|
||||||
if not self.__signal_notif.has_key(name):
|
|
||||||
self.__signal_notif[name] = []
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if not self.__instance_notif_before.has_key(name):
|
|
||||||
self.__instance_notif_before[name] = []
|
|
||||||
pass
|
|
||||||
if not self.__instance_notif_after.has_key(name):
|
|
||||||
self.__instance_notif_after[name] = []
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def has_property(self, name):
|
|
||||||
"""Returns true if given property name refers an observable
|
|
||||||
property inside self or inside derived classes"""
|
|
||||||
return self.__properties__.has_key(name) or \
|
|
||||||
self.__derived_properties__.has_key(name)
|
|
||||||
|
|
||||||
|
|
||||||
def register_observer(self, observer):
|
|
||||||
if observer in self.__observers: return # not already registered
|
|
||||||
|
|
||||||
self.__observers.append(observer)
|
|
||||||
for key in (self.__properties__.keys() + self.__derived_properties__.keys()):
|
|
||||||
self.__add_observer_notification(observer, key)
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_observer(self, observer):
|
|
||||||
if observer not in self.__observers: return
|
|
||||||
|
|
||||||
for key in (self.__properties__.keys() + self.__derived_properties__.keys()):
|
|
||||||
self.__remove_observer_notification(observer, key)
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.__observers.remove(observer)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _reset_property_notification(self, prop_name):
|
|
||||||
"""Called when it has be done an assignment that changes the
|
|
||||||
type of a property or the instance of the property has been
|
|
||||||
changed to a different instance. In this case it must be
|
|
||||||
unregistered and registered again"""
|
|
||||||
|
|
||||||
self.register_property(prop_name)
|
|
||||||
|
|
||||||
for observer in self.__observers:
|
|
||||||
self.__remove_observer_notification(observer, prop_name)
|
|
||||||
self.__add_observer_notification(observer, prop_name)
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def __add_observer_notification(self, observer, prop_name):
|
|
||||||
"""Searches in the observer for any possible listener, and
|
|
||||||
stores the notification methods to be called later"""
|
|
||||||
|
|
||||||
method_name = "property_%s_value_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method not in self.__value_notifications[prop_name]:
|
|
||||||
list.append(self.__value_notifications[prop_name], method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# is it a signal?
|
|
||||||
orig_prop = getattr(self, "_prop_%s" % prop_name)
|
|
||||||
if isinstance(orig_prop, Signal):
|
|
||||||
method_name = "property_%s_signal_emit" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method not in self.__signal_notif[prop_name]:
|
|
||||||
list.append(self.__signal_notif[prop_name], method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# is it an instance change notification type?
|
|
||||||
elif isinstance(orig_prop, ObsWrapperBase):
|
|
||||||
method_name = "property_%s_before_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method not in self.__instance_notif_before[prop_name]:
|
|
||||||
list.append(self.__instance_notif_before[prop_name], method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
method_name = "property_%s_after_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method not in self.__instance_notif_after[prop_name]:
|
|
||||||
list.append(self.__instance_notif_after[prop_name], method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def __remove_observer_notification(self, observer, prop_name):
|
|
||||||
if self.__value_notifications.has_key(prop_name):
|
|
||||||
method_name = "property_%s_value_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method in self.__value_notifications[prop_name]:
|
|
||||||
self.__value_notifications[prop_name].remove(method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
orig_prop = getattr(self, "_prop_%s" % prop_name)
|
|
||||||
# is it a signal?
|
|
||||||
if isinstance(orig_prop, Signal):
|
|
||||||
method_name = "property_%s_signal_emit" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method in self.__signal_notif[prop_name]:
|
|
||||||
self.__signal_notif[prop_name].remove(method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# is it an instance change notification type?
|
|
||||||
elif isinstance(orig_prop, ObsWrapperBase):
|
|
||||||
if self.__instance_notif_before.has_key(prop_name):
|
|
||||||
method_name = "property_%s_before_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method in self.__instance_notif_before[prop_name]:
|
|
||||||
self.__instance_notif_before[prop_name].remove(method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
if self.__instance_notif_after.has_key(prop_name):
|
|
||||||
method_name = "property_%s_after_change" % prop_name
|
|
||||||
if hasattr(observer, method_name):
|
|
||||||
method = getattr(observer, method_name)
|
|
||||||
if method in self.__instance_notif_after[prop_name]:
|
|
||||||
self.__instance_notif_after[prop_name].remove(method)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def __notify_observer__(self, observer, method, *args, **kwargs):
|
|
||||||
"""This can be overridden by derived class in order to call
|
|
||||||
the method in a different manner (for example, in
|
|
||||||
multithreading, or a rpc, etc.) This implementation simply
|
|
||||||
calls the given method with the given arguments"""
|
|
||||||
return method(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------- Notifiers:
|
|
||||||
|
|
||||||
def notify_property_value_change(self, prop_name, old, new):
|
|
||||||
assert(self.__value_notifications.has_key(prop_name))
|
|
||||||
for method in self.__value_notifications[prop_name] :
|
|
||||||
obs = method.im_self
|
|
||||||
# notification occurs checking spuriousness of the observer
|
|
||||||
if old != new or obs.accepts_spurious_change():
|
|
||||||
self.__notify_observer__(obs, method,
|
|
||||||
self, old, new) # notifies the change
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
def notify_method_before_change(self, prop_name, instance, meth_name,
|
|
||||||
args, kwargs):
|
|
||||||
assert(self.__instance_notif_before.has_key(prop_name))
|
|
||||||
for method in self.__instance_notif_before[prop_name] :
|
|
||||||
self.__notify_observer__(method.im_self, method, self, instance,
|
|
||||||
meth_name, args, kwargs) # notifies the change
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
def notify_method_after_change(self, prop_name, instance, meth_name,
|
|
||||||
res, args, kwargs):
|
|
||||||
assert(self.__instance_notif_after.has_key(prop_name))
|
|
||||||
for method in self.__instance_notif_after[prop_name] :
|
|
||||||
self.__notify_observer__(method.im_self, method, self, instance,
|
|
||||||
meth_name, res, args, kwargs) # notifies the change
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
def notify_signal_emit(self, prop_name, args, kwargs):
|
|
||||||
assert(self.__signal_notif.has_key(prop_name))
|
|
||||||
for method in self.__signal_notif[prop_name] :
|
|
||||||
self.__notify_observer__(method.im_self, method, self,
|
|
||||||
args, kwargs) # notifies the signal emit
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
pass # end of class Model
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class TreeStoreModel (Model, gtk.TreeStore):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.TreeStore"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMeta
|
|
||||||
|
|
||||||
def __init__(self, column_type, *args):
|
|
||||||
Model.__init__(self)
|
|
||||||
gtk.TreeStore.__init__(self, column_type, *args)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ListStoreModel (Model, gtk.ListStore):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.ListStore"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMeta
|
|
||||||
|
|
||||||
def __init__(self, column_type, *args):
|
|
||||||
Model.__init__(self)
|
|
||||||
gtk.ListStore.__init__(self, column_type, *args)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class TextBufferModel (Model, gtk.TextBuffer):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.TextBuffer"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMeta
|
|
||||||
|
|
||||||
def __init__(self, table=None):
|
|
||||||
Model.__init__(self)
|
|
||||||
gtk.TextBuffer.__init__(self, table)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2006 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
from gtkmvc.model import Model
|
|
||||||
import support.metaclasses
|
|
||||||
|
|
||||||
try: import threading as _threading
|
|
||||||
except ImportError: import dummy_threading as _threading
|
|
||||||
|
|
||||||
import gobject
|
|
||||||
if hasattr(gobject, "threads_init"): gobject.threads_init()
|
|
||||||
else: import gtk; gtk.threads_init()
|
|
||||||
|
|
||||||
|
|
||||||
class ModelMT (Model):
|
|
||||||
"""A base class for models whose observable properties can be
|
|
||||||
changed by threads different than gtk main thread. Notification is
|
|
||||||
performed by exploiting the gtk idle loop only if needed,
|
|
||||||
otherwise the standard notification system (direct method call) is
|
|
||||||
used. In this model, the observer is expected to run in the gtk
|
|
||||||
main loop thread."""
|
|
||||||
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyMetaMT
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
Model.__init__(self)
|
|
||||||
self.__observer_threads = {}
|
|
||||||
self._prop_lock = _threading.Lock()
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_observer(self, observer):
|
|
||||||
Model.register_observer(self, observer)
|
|
||||||
self.__observer_threads[observer] = _threading.currentThread()
|
|
||||||
return
|
|
||||||
|
|
||||||
def unregister_observer(self, observer):
|
|
||||||
Model.unregister_observer(self, observer)
|
|
||||||
del self.__observer_threads[observer]
|
|
||||||
return
|
|
||||||
|
|
||||||
# ---------- Notifiers:
|
|
||||||
|
|
||||||
def __notify_observer__(self, observer, method, *args, **kwargs):
|
|
||||||
"""This makes a call either through the gtk.idle list or a
|
|
||||||
direct method call depending whether the caller's thread is
|
|
||||||
different from the observer's thread"""
|
|
||||||
|
|
||||||
assert self.__observer_threads.has_key(observer)
|
|
||||||
if _threading.currentThread() == self.__observer_threads[observer]:
|
|
||||||
# standard call
|
|
||||||
return Model.__notify_observer__(self, observer, method,
|
|
||||||
*args, **kwargs)
|
|
||||||
|
|
||||||
# multi-threading call
|
|
||||||
gobject.idle_add(self.__idle_callback, observer, method, args, kwargs)
|
|
||||||
return
|
|
||||||
|
|
||||||
def __idle_callback(self, observer, method, args, kwargs):
|
|
||||||
method(*args, **kwargs)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class TreeStoreModelMT (ModelMT, gtk.TreeStore):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.TreeStore"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMetaMT
|
|
||||||
|
|
||||||
def __init__(self, column_type, *args):
|
|
||||||
ModelMT.__init__(self)
|
|
||||||
gtk.TreeStore.__init__(self, column_type, *args)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ListStoreModelMT (ModelMT, gtk.ListStore):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.ListStore"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMetaMT
|
|
||||||
|
|
||||||
def __init__(self, column_type, *args):
|
|
||||||
ModelMT.__init__(self)
|
|
||||||
gtk.ListStore.__init__(self, column_type, *args)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class TextBufferModelMT (ModelMT, gtk.TextBuffer):
|
|
||||||
"""Use this class as base class for your model derived by
|
|
||||||
gtk.TextBuffer"""
|
|
||||||
__metaclass__ = support.metaclasses.ObservablePropertyGObjectMetaMT
|
|
||||||
|
|
||||||
def __init__(self, table=None):
|
|
||||||
ModelMT.__init__(self)
|
|
||||||
gtk.TextBuffer.__init__(self, table)
|
|
||||||
return
|
|
||||||
pass
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
# -------------------------------------------------------------------------
|
|
||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2006 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.ridge, MA 02139, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author <cavada@fbk.eu>.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
from support import decorators
|
|
||||||
from support.wrappers import ObsWrapperBase
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class Observable (ObsWrapperBase):
|
|
||||||
def __init__(self):
|
|
||||||
ObsWrapperBase.__init__(self)
|
|
||||||
return
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
|
|
||||||
@decorators.good_decorator
|
|
||||||
def observed(func):
|
|
||||||
"""Use this decorator to make your class methods observable.
|
|
||||||
|
|
||||||
Your observer will receive at most two notifications:
|
|
||||||
- property_<name>_before_change
|
|
||||||
- property_<name>_after_change
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
|
||||||
self = args[0]
|
|
||||||
assert(isinstance(self, Observable))
|
|
||||||
|
|
||||||
self._notify_method_before(self, func.__name__, args, kwargs)
|
|
||||||
res = func(*args, **kwargs)
|
|
||||||
self._notify_method_after(self, func.__name__, res, args, kwargs)
|
|
||||||
return res
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class Signal (Observable):
|
|
||||||
"""Base class for signals properties"""
|
|
||||||
def __init__(self):
|
|
||||||
Observable.__init__(self)
|
|
||||||
return
|
|
||||||
|
|
||||||
def emit(self, *args, **kwargs):
|
|
||||||
return self.__get_model__().notify_signal_emit(
|
|
||||||
self.__get_prop_name__(), args, kwargs)
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# -------------------------------------------------------------------------
|
|
||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2006 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class Observer (object):
|
|
||||||
"""Use this class as base class of all observers"""
|
|
||||||
|
|
||||||
def __init__(self, model=None, spurious=False):
|
|
||||||
"""
|
|
||||||
When parameter spurious is set to False
|
|
||||||
(default value) the observer declares that it is not
|
|
||||||
interested in receiving value-change notifications when
|
|
||||||
property's value does not really change. This happens when a
|
|
||||||
property got assigned to a value that is the same it had
|
|
||||||
before being assigned.
|
|
||||||
|
|
||||||
A notification was used to be sent to the observer even in
|
|
||||||
this particular condition, because spurious (non-changing)
|
|
||||||
assignments were used as signals when signals were not
|
|
||||||
supported by early version of the framework. The observer
|
|
||||||
was in charge of deciding what to do with spurious
|
|
||||||
assignments, by checking if the old and new values were
|
|
||||||
different at the beginning of the notification code. With
|
|
||||||
latest version providing new notification types like
|
|
||||||
signals, this requirement seems to be no longer needed, and
|
|
||||||
delivering a notification is no longer a sensible
|
|
||||||
behaviour.
|
|
||||||
|
|
||||||
This is the reason for providing parameter
|
|
||||||
spurious that changes the previous behaviour
|
|
||||||
but keeps availability of a possible backward compatible
|
|
||||||
feature.
|
|
||||||
"""
|
|
||||||
|
|
||||||
self.model = None
|
|
||||||
self.__accepts_spurious__ = spurious
|
|
||||||
self.register_model(model)
|
|
||||||
return
|
|
||||||
|
|
||||||
def register_model(self, model):
|
|
||||||
self.unregister_model()
|
|
||||||
self.model = model
|
|
||||||
if self.model: self.model.register_observer(self)
|
|
||||||
return
|
|
||||||
|
|
||||||
def accepts_spurious_change(self):
|
|
||||||
"""
|
|
||||||
Returns True if this observer is interested in receiving
|
|
||||||
spurious value changes. This is queried by the model when
|
|
||||||
notifying a value change."""
|
|
||||||
return self.__accepts_spurious__
|
|
||||||
|
|
||||||
def unregister_model(self):
|
|
||||||
if self.model:
|
|
||||||
self.model.unregister_observer(self)
|
|
||||||
self.model = None
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
def __del__(self):
|
|
||||||
self.unregister_model()
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_model(self): return self.model
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["metaclass_base", "metaclasses", "wrappers", "decorators",
|
|
||||||
"factories"]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# -------------------------------------------------------------------------
|
|
||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2006 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# This file contains decorators to be used (privately) by other parts
|
|
||||||
# of the framework
|
|
||||||
|
|
||||||
def good_decorator(decorator):
|
|
||||||
"""This decorator makes decorators behave well wrt to decorated
|
|
||||||
functions names, doc, etc."""
|
|
||||||
def new_decorator(f):
|
|
||||||
g = decorator(f)
|
|
||||||
g.__name__ = f.__name__
|
|
||||||
g.__doc__ = f.__doc__
|
|
||||||
g.__dict__.update(f.__dict__)
|
|
||||||
return g
|
|
||||||
|
|
||||||
new_decorator.__name__ = decorator.__name__
|
|
||||||
new_decorator.__doc__ = decorator.__doc__
|
|
||||||
new_decorator.__dict__.update(decorator.__dict__)
|
|
||||||
return new_decorator
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# -------------------------------------------------------------------------
|
|
||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2008 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
import new
|
|
||||||
from gtkmvc import Model
|
|
||||||
from noconflict import get_noconflict_metaclass
|
|
||||||
|
|
||||||
class ModelFactory (object):
|
|
||||||
"""This factory constructs classes for models. Use it to build
|
|
||||||
the classes to derive your own models"""
|
|
||||||
|
|
||||||
__memoized = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def __fix_bases(base_classes, have_mt):
|
|
||||||
"""This function check whether base_classes contains a Model
|
|
||||||
instance. If not, choose the best fitting class for
|
|
||||||
model. Furthermore, it makes the list in a cannonical
|
|
||||||
ordering form in a way that ic can be used as memoization
|
|
||||||
key"""
|
|
||||||
fixed = list(base_classes)
|
|
||||||
contains_model = False
|
|
||||||
for b in fixed:
|
|
||||||
if isinstance(fixed, Model): contains_model = True; break
|
|
||||||
pass
|
|
||||||
|
|
||||||
# adds a model when user is lazy
|
|
||||||
if not contains_model:
|
|
||||||
if have_mt:
|
|
||||||
from gtkmvc.model_mt import ModelMT
|
|
||||||
fixed.insert(0, ModelMT)
|
|
||||||
else: fixed.insert(0, Model)
|
|
||||||
pass
|
|
||||||
|
|
||||||
class ModelFactoryWrap (object):
|
|
||||||
__metaclass__ = get_noconflict_metaclass(tuple(fixed), (), ())
|
|
||||||
def __init__(self, *args, **kwargs): pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
fixed.append(ModelFactoryWrap)
|
|
||||||
fixed.sort()
|
|
||||||
return tuple(fixed)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def make(base_classes=(), have_mt=False):
|
|
||||||
"""Use this static method to build a model class that
|
|
||||||
possibly derives from other classes. If have_mt is True,
|
|
||||||
then returned class will take into account multi-threading
|
|
||||||
issues when dealing with observable properties."""
|
|
||||||
|
|
||||||
good_bc = ModelFactory.__fix_bases(base_classes, have_mt)
|
|
||||||
print "Base classes are:", good_bc
|
|
||||||
key = "".join(map(str, good_bc))
|
|
||||||
if ModelFactory.__memoized.has_key(key):
|
|
||||||
return ModelFactory.__memoized[key]
|
|
||||||
|
|
||||||
cls = new.classobj('', good_bc, {'__module__': '__main__', '__doc__': None})
|
|
||||||
ModelFactory.__memoized[key] = cls
|
|
||||||
return cls
|
|
||||||
|
|
||||||
#__
|
|
||||||
#make = staticmethod(make)
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
@@ -1,240 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
import new
|
|
||||||
import re
|
|
||||||
import types
|
|
||||||
|
|
||||||
import gtkmvc.support.wrappers as wrappers
|
|
||||||
from gtkmvc.support.utils import get_function_from_source
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
VERBOSE_LEVEL = 5
|
|
||||||
|
|
||||||
class PropertyMeta (type):
|
|
||||||
"""This is a meta-class that provides auto-property support.
|
|
||||||
The idea is to allow programmers to define some properties which
|
|
||||||
will be automatically connected to auto-generated code which handles
|
|
||||||
access to those properties.
|
|
||||||
How can you use this meta-class?
|
|
||||||
First, '__metaclass__ = PropertyMeta' must be class member of the class
|
|
||||||
you want to make the automatic properties handling.
|
|
||||||
Second, '__properties__' must be a map containing the properties names
|
|
||||||
as keys, values will be initial values for properties.
|
|
||||||
That's all: after the instantiation, your class will contain all properties
|
|
||||||
you named inside '__properties__'. Each of them will be also associated
|
|
||||||
to a couple of automatically-generated functions which get and set the
|
|
||||||
property value inside a generated member variable.
|
|
||||||
About names: suppose the property is called 'x'. The generated variable
|
|
||||||
(which keeps the real value of the property x) is called _prop_x.
|
|
||||||
The getter is called get_prop_x(self), and the setter is called
|
|
||||||
'set_prop_x(self, value)'.
|
|
||||||
|
|
||||||
Customization:
|
|
||||||
The base implementation of getter is to return the value stored in the
|
|
||||||
variable associated to the property. The setter simply sets its value.
|
|
||||||
Programmers can override basic behaviour for getters or setters simply by
|
|
||||||
defining their getters and setters (see at the names convention above).
|
|
||||||
The customized function can lie everywhere in the user classes hierarchy.
|
|
||||||
Every overridden function will not be generated by the metaclass.
|
|
||||||
|
|
||||||
To supply your own methods is good for few methods, but can result in a
|
|
||||||
very unconfortable way for many methods. In this case you can extend
|
|
||||||
the meta-class, and override methods get_[gs]etter_source with your
|
|
||||||
implementation (this can be probably made better).
|
|
||||||
An example is provided in meta-class PropertyMetaVerbose below.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(cls, name, bases, dict):
|
|
||||||
"""class constructor"""
|
|
||||||
properties = {}
|
|
||||||
type.__init__(cls, name, bases, dict)
|
|
||||||
|
|
||||||
props = getattr(cls, '__properties__', {})
|
|
||||||
setattr(cls, '__derived_properties__', {})
|
|
||||||
der_props = getattr(cls, '__derived_properties__')
|
|
||||||
|
|
||||||
# Calculates derived properties:
|
|
||||||
for base in bases:
|
|
||||||
maps = ( getattr(base, '__properties__', {}),
|
|
||||||
getattr(base, '__derived_properties__', {}) )
|
|
||||||
for _map in maps:
|
|
||||||
for p in _map.keys():
|
|
||||||
if not props.has_key(p) and not der_props.has_key(p):
|
|
||||||
der_props[p] = _map[p]
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Generates code for all properties (but not for derived props):
|
|
||||||
props = getattr(cls, '__properties__', {})
|
|
||||||
for prop in props.keys():
|
|
||||||
type(cls).__create_prop_accessors__(cls, prop, props[prop])
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def __msg__(cls, msg, level):
|
|
||||||
"""if level is less or equal to VERBOSE_LEVEL, ths message will
|
|
||||||
be printed"""
|
|
||||||
if level <= VERBOSE_LEVEL: print msg
|
|
||||||
return
|
|
||||||
|
|
||||||
def __create_prop_accessors__(cls, prop_name, default_val):
|
|
||||||
"""Private method that creates getter and setter, and the
|
|
||||||
corresponding property"""
|
|
||||||
getter_name = "get_prop_%s" % prop_name
|
|
||||||
setter_name = "set_prop_%s" % prop_name
|
|
||||||
|
|
||||||
members_names = cls.__dict__.keys()
|
|
||||||
|
|
||||||
# checks if accessors are already defined:
|
|
||||||
if getter_name not in members_names:
|
|
||||||
src = type(cls).get_getter_source(cls, getter_name, prop_name)
|
|
||||||
func = get_function_from_source(src)
|
|
||||||
setattr(cls, getter_name, func)
|
|
||||||
else:
|
|
||||||
cls.__msg__("Warning: Custom member '%s' overloads generated accessor of property '%s'" \
|
|
||||||
% (getter_name, prop_name), 2)
|
|
||||||
pass
|
|
||||||
|
|
||||||
if setter_name not in members_names:
|
|
||||||
src = type(cls).get_setter_source(cls, setter_name, prop_name)
|
|
||||||
func = get_function_from_source(src)
|
|
||||||
setattr(cls, setter_name, func)
|
|
||||||
else:
|
|
||||||
cls.__msg__("Warning: Custom member '%s' overloads generated accessor of property '%s'" \
|
|
||||||
% (setter_name, prop_name), 2)
|
|
||||||
pass
|
|
||||||
|
|
||||||
prop = property(getattr(cls, getter_name), getattr(cls, setter_name))
|
|
||||||
|
|
||||||
if prop_name in members_names:
|
|
||||||
cls.__msg__("Warning: automatic property builder overrids property %s in class %s" \
|
|
||||||
% (prop_name, cls.__name__), 2)
|
|
||||||
pass
|
|
||||||
setattr(cls, prop_name, prop)
|
|
||||||
|
|
||||||
varname = "_prop_%s" % prop_name
|
|
||||||
if not varname in members_names: cls.__create_property(varname, default_val)
|
|
||||||
else: cls.__msg__("Warning: automatic property builder found a possible clashing for variable %s inside class %s" \
|
|
||||||
% (varname, cls.__name__), 2)
|
|
||||||
return
|
|
||||||
|
|
||||||
def __create_property(cls, name, default_val):
|
|
||||||
setattr(cls, name, cls.create_value(name, default_val))
|
|
||||||
return
|
|
||||||
|
|
||||||
def check_value_change(cls, old, new):
|
|
||||||
"""Checks whether the value of the property changed in type
|
|
||||||
or if the instance has been changed to a different instance.
|
|
||||||
If true, a call to model._reset_property_notification should
|
|
||||||
be called in order to re-register the new property instance
|
|
||||||
or type"""
|
|
||||||
return type(old) != type(new) or \
|
|
||||||
isinstance(old, wrappers.ObsWrapperBase) and (old != new)
|
|
||||||
|
|
||||||
def create_value(cls, prop_name, val, model=None):
|
|
||||||
"""This is used to create a value to be assigned to a
|
|
||||||
property. Depending on the type of the value, different values
|
|
||||||
are created and returned. For example, for a list, a
|
|
||||||
ListWrapper is created to wrap it, and returned for the
|
|
||||||
assignment. model is different from model when the value is
|
|
||||||
changed (a model exists). Otherwise, during property creation
|
|
||||||
model is None"""
|
|
||||||
|
|
||||||
if isinstance(val, tuple):
|
|
||||||
# this might be a class instance to be wrapped
|
|
||||||
if len(val) == 3 and \
|
|
||||||
isinstance(val[1], val[0]) and \
|
|
||||||
(isinstance(val[2], tuple) or isinstance(val[2], list)):
|
|
||||||
res = wrappers.ObsUserClassWrapper(val[1], val[2])
|
|
||||||
if model: res.__set_model__(model, prop_name)
|
|
||||||
return res
|
|
||||||
pass
|
|
||||||
|
|
||||||
elif isinstance(val, list):
|
|
||||||
res = wrappers.ObsListWrapper(val)
|
|
||||||
if model: res.__set_model__(model, prop_name)
|
|
||||||
return res
|
|
||||||
|
|
||||||
elif isinstance(val, dict):
|
|
||||||
res = wrappers.ObsMapWrapper(val)
|
|
||||||
if model: res.__set_model__(model, prop_name)
|
|
||||||
return res
|
|
||||||
|
|
||||||
return val
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Services
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
# Override these:
|
|
||||||
def get_getter_source(cls, getter_name, prop_name):
|
|
||||||
"""This must be overridden if you need a different implementation.
|
|
||||||
Simply the generated implementation returns the variable name
|
|
||||||
_prop_name"""
|
|
||||||
|
|
||||||
return "def %s(self): return self._prop_%s" % (getter_name, prop_name)
|
|
||||||
|
|
||||||
def get_setter_source(cls, setter_name, prop_name):
|
|
||||||
"""This must be overridden if you need a different implementation.
|
|
||||||
Simply the generated implementation sets the variable _prop_name"""
|
|
||||||
return "def %s(self, val): self._prop_%s = val" \
|
|
||||||
% (setter_name, prop_name)
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
# What follows underneath is a set of examples of usage
|
|
||||||
|
|
||||||
## class PropertyMetaVerbose (PropertyMeta):
|
|
||||||
## """An example of customization"""
|
|
||||||
## def get_getter_source(cls, getter_name, prop_name):
|
|
||||||
## return "def %s(self): print 'Calling %s!'; return self._prop_%s" \
|
|
||||||
## % (getter_name, getter_name, prop_name)
|
|
||||||
|
|
||||||
## def get_setter_source(cls, setter_name, prop_name):
|
|
||||||
## return "def %s(self, val): print 'Calling %s!'; self._prop_%s = val;" \
|
|
||||||
## % (setter_name, setter_name, prop_name)
|
|
||||||
## pass #end of class
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
## class User:
|
|
||||||
## """An example of usage"""
|
|
||||||
## __metaclass__ = PropertyMetaVerbose
|
|
||||||
## __properties__ = {'x':10, 'y':20}
|
|
||||||
|
|
||||||
## def __init__(self):
|
|
||||||
## print self.x # x is 10
|
|
||||||
## self.x = self.y + 10 # x is now 30
|
|
||||||
## return
|
|
||||||
## pass
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
from metaclass_base import PropertyMeta
|
|
||||||
import types
|
|
||||||
|
|
||||||
|
|
||||||
class ObservablePropertyMeta (PropertyMeta):
|
|
||||||
"""Classes instantiated by this meta-class must provide a method named
|
|
||||||
notify_property_change(self, prop_name, old, new)"""
|
|
||||||
def __init__(cls, name, bases, dict):
|
|
||||||
PropertyMeta.__init__(cls, name, bases, dict)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_setter_source(cls, setter_name, prop_name):
|
|
||||||
return """def %(setter)s(self, val):
|
|
||||||
old = self._prop_%(prop)s
|
|
||||||
new = type(self).create_value('%(prop)s', val, self)
|
|
||||||
self._prop_%(prop)s = new
|
|
||||||
if type(self).check_value_change(old, new): self._reset_property_notification('%(prop)s')
|
|
||||||
self.notify_property_value_change('%(prop)s', old, val)
|
|
||||||
return
|
|
||||||
""" % {'setter':setter_name, 'prop':prop_name}
|
|
||||||
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
class ObservablePropertyMetaMT (ObservablePropertyMeta):
|
|
||||||
"""This class provides multithreading support for accesing
|
|
||||||
properties, through a locking mechanism. It is assumed a lock is
|
|
||||||
owned by the class that uses it. A Lock object called _prop_lock
|
|
||||||
is assumed to be a member of the using class. see for example class
|
|
||||||
ModelMT"""
|
|
||||||
def __init__(cls, name, bases, dict):
|
|
||||||
ObservablePropertyMeta.__init__(cls, name, bases, dict)
|
|
||||||
return
|
|
||||||
|
|
||||||
def get_setter_source(cls, setter_name, prop_name):
|
|
||||||
return """def %(setter)s(self, val):
|
|
||||||
old = self._prop_%(prop)s
|
|
||||||
new = type(self).create_value('%(prop)s', val, self)
|
|
||||||
self._prop_lock.acquire()
|
|
||||||
self._prop_%(prop)s = new
|
|
||||||
self._prop_lock.release()
|
|
||||||
if type(self).check_value_change(old, new): self._reset_property_notification('%(prop)s')
|
|
||||||
self.notify_property_value_change('%(prop)s', old, val)
|
|
||||||
return
|
|
||||||
""" % {'setter':setter_name, 'prop':prop_name}
|
|
||||||
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from gobject import GObjectMeta
|
|
||||||
class ObservablePropertyGObjectMeta (ObservablePropertyMeta, GObjectMeta): pass
|
|
||||||
class ObservablePropertyGObjectMetaMT (ObservablePropertyMetaMT, GObjectMeta): pass
|
|
||||||
except:
|
|
||||||
class ObservablePropertyGObjectMeta (ObservablePropertyMeta): pass
|
|
||||||
class ObservablePropertyGObjectMetaMT (ObservablePropertyMetaMT): pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Author: Michele Simionato <michelesimionato@libero.it>
|
|
||||||
# Copyright (C) 2004 by Michele Simionato
|
|
||||||
# License: Python License (version not specified)
|
|
||||||
# Last Updated: 2nd of March 2007, 10:23 GMT
|
|
||||||
#
|
|
||||||
# Any serious user of metaclasses has been bitten at least once by
|
|
||||||
# the infamous metaclass/metatype conflict. This script contains a
|
|
||||||
# general recipe to solve the problem, as well as some theory and
|
|
||||||
# some examples.
|
|
||||||
|
|
||||||
|
|
||||||
import inspect, types, __builtin__
|
|
||||||
|
|
||||||
############## preliminary: two utility functions #####################
|
|
||||||
|
|
||||||
def skip_redundant(iterable, skipset=None):
|
|
||||||
"Redundant items are repeated items or items in the original skipset."
|
|
||||||
if skipset is None: skipset = set()
|
|
||||||
for item in iterable:
|
|
||||||
if item not in skipset:
|
|
||||||
skipset.add(item)
|
|
||||||
yield item
|
|
||||||
|
|
||||||
|
|
||||||
def remove_redundant(metaclasses):
|
|
||||||
skipset = set([types.ClassType])
|
|
||||||
for meta in metaclasses: # determines the metaclasses to be skipped
|
|
||||||
skipset.update(inspect.getmro(meta)[1:])
|
|
||||||
return tuple(skip_redundant(metaclasses, skipset))
|
|
||||||
|
|
||||||
##################################################################
|
|
||||||
## now the core of the module: two mutually recursive functions ##
|
|
||||||
##################################################################
|
|
||||||
|
|
||||||
memoized_metaclasses_map = {}
|
|
||||||
|
|
||||||
def get_noconflict_metaclass(bases, left_metas, right_metas):
|
|
||||||
"""Not intended to be used outside of this module, unless you know
|
|
||||||
what you are doing."""
|
|
||||||
# make tuple of needed metaclasses in specified priority order
|
|
||||||
metas = left_metas + tuple(map(type, bases)) + right_metas
|
|
||||||
needed_metas = remove_redundant(metas)
|
|
||||||
|
|
||||||
# return existing confict-solving meta, if any
|
|
||||||
if needed_metas in memoized_metaclasses_map:
|
|
||||||
return memoized_metaclasses_map[needed_metas]
|
|
||||||
# nope: compute, memoize and return needed conflict-solving meta
|
|
||||||
elif not needed_metas: # wee, a trivial case, happy us
|
|
||||||
meta = type
|
|
||||||
elif len(needed_metas) == 1: # another trivial case
|
|
||||||
meta = needed_metas[0]
|
|
||||||
# check for recursion, can happen i.e. for Zope ExtensionClasses
|
|
||||||
elif needed_metas == bases:
|
|
||||||
raise TypeError("Incompatible root metatypes", needed_metas)
|
|
||||||
else: # gotta work ...
|
|
||||||
metaname = '_' + ''.join([m.__name__ for m in needed_metas])
|
|
||||||
meta = classmaker()(metaname, needed_metas, {})
|
|
||||||
memoized_metaclasses_map[needed_metas] = meta
|
|
||||||
return meta
|
|
||||||
|
|
||||||
def classmaker(left_metas=(), right_metas=()):
|
|
||||||
def make_class(name, bases, adict):
|
|
||||||
metaclass = get_noconflict_metaclass(bases, left_metas, right_metas)
|
|
||||||
return metaclass(name, bases, adict)
|
|
||||||
return make_class
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
class A (object):
|
|
||||||
a = 10
|
|
||||||
pass
|
|
||||||
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2007 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_function_from_source(source):
|
|
||||||
"""Given source code of a function, a function object is
|
|
||||||
returned"""
|
|
||||||
|
|
||||||
import re
|
|
||||||
m = re.compile("def\s+(\w+)\s*\(.*\):").match(source)
|
|
||||||
if m is None: raise ValueError("Given source is not a valid function:\n"+
|
|
||||||
source)
|
|
||||||
name = m.group(1)
|
|
||||||
|
|
||||||
exec source
|
|
||||||
code = eval("%s.func_code" % name)
|
|
||||||
import new
|
|
||||||
return new.function(code, globals(), name)
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
# -------------------------------------------------------------------------
|
|
||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
#
|
|
||||||
# Copyright (C) 2006 by Roberto Cavada
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
import new
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsWrapperBase (object):
|
|
||||||
"""
|
|
||||||
This class is a base class wrapper for user-defined classes and
|
|
||||||
containers like lists and maps.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__prop_name = None
|
|
||||||
self.__gtkmvc_model = None
|
|
||||||
return
|
|
||||||
|
|
||||||
def __set_model__(self, model, prop_name):
|
|
||||||
self.__prop_name = prop_name
|
|
||||||
self.__gtkmvc_model = model
|
|
||||||
return
|
|
||||||
|
|
||||||
def __get_prop_name__(self): return self.__prop_name
|
|
||||||
def __get_model__(self): return self.__gtkmvc_model
|
|
||||||
|
|
||||||
def _notify_method_before(self, instance, name, args, kwargs):
|
|
||||||
self.__get_model__().notify_method_before_change(self.__prop_name,
|
|
||||||
instance,
|
|
||||||
name, args, kwargs)
|
|
||||||
return
|
|
||||||
|
|
||||||
def _notify_method_after(self, instance, name, res_val, args, kwargs):
|
|
||||||
self.__get_model__().notify_method_after_change(self.__prop_name,
|
|
||||||
instance,
|
|
||||||
name, res_val, args, kwargs)
|
|
||||||
return
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsWrapper (ObsWrapperBase):
|
|
||||||
"""
|
|
||||||
Base class for wrappers, like user-classes and sequences.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, obj, method_names):
|
|
||||||
ObsWrapperBase.__init__(self)
|
|
||||||
|
|
||||||
self._obj = obj
|
|
||||||
self.__doc__ = obj.__doc__
|
|
||||||
|
|
||||||
for name in method_names:
|
|
||||||
if hasattr(self._obj, name):
|
|
||||||
src = self.__get_wrapper_code(name)
|
|
||||||
exec src
|
|
||||||
|
|
||||||
code = eval("%s.func_code" % name)
|
|
||||||
func = new.function(code, globals())
|
|
||||||
meth = new.instancemethod(func, self, type(self).__name__)
|
|
||||||
setattr(self, name, meth)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def __get_wrapper_code(self, name):
|
|
||||||
return """def %(name)s(self, *args, **kwargs):
|
|
||||||
self._notify_method_before(self._obj, "%(name)s", args, kwargs)
|
|
||||||
res = self._obj.%(name)s(*args, **kwargs)
|
|
||||||
self._notify_method_after(self._obj, "%(name)s", res, args, kwargs)
|
|
||||||
return res""" % {'name' : name}
|
|
||||||
|
|
||||||
# For all fall backs
|
|
||||||
def __getattr__(self, name): return getattr(self._obj, name)
|
|
||||||
def __repr__(self): return self._obj.__repr__()
|
|
||||||
def __str__(self): return self._obj.__str__()
|
|
||||||
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsSeqWrapper (ObsWrapper):
|
|
||||||
def __init__(self, obj, method_names):
|
|
||||||
ObsWrapper.__init__(self, obj, method_names)
|
|
||||||
return
|
|
||||||
|
|
||||||
def __setitem__(self, key, val):
|
|
||||||
|
|
||||||
self._notify_method_before(self._obj, "__setitem__", (key,val), {})
|
|
||||||
res = self._obj.__setitem__(key, val)
|
|
||||||
self._notify_method_after(self._obj, "__setitem__", res, (key,val), {})
|
|
||||||
return res
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
self._notify_method_before(self._obj, "__delitem__", (key,), {})
|
|
||||||
res = self._obj.__delitem__(key)
|
|
||||||
self._notify_method_after(self._obj, "__delitem__", res, (key,), {})
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
return self._obj.__getitem__(key)
|
|
||||||
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsMapWrapper (ObsSeqWrapper):
|
|
||||||
def __init__(self, m):
|
|
||||||
methods = ("clear", "pop", "popitem", "update",
|
|
||||||
"setdefault")
|
|
||||||
ObsSeqWrapper.__init__(self, m, methods)
|
|
||||||
return
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsListWrapper (ObsSeqWrapper):
|
|
||||||
def __init__(self, l):
|
|
||||||
methods = ("append", "extend", "insert",
|
|
||||||
"pop", "remove", "reverse", "sort")
|
|
||||||
ObsSeqWrapper.__init__(self, l, methods)
|
|
||||||
return
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
class ObsUserClassWrapper (ObsWrapper):
|
|
||||||
def __init__(self, user_class_instance, obs_method_names):
|
|
||||||
ObsWrapper.__init__(self, user_class_instance, obs_method_names)
|
|
||||||
return
|
|
||||||
pass #end of class
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
# Author: Roberto Cavada <cavada@fbk.eu>
|
|
||||||
# Modified by: Guillaume Libersat <glibersat AT linux62.org>
|
|
||||||
#
|
|
||||||
# Copyright (c) 2005 by Roberto Cavada
|
|
||||||
# Copyright (c) 2007 by Guillaume Libersat
|
|
||||||
#
|
|
||||||
# pygtkmvc is free software; you can redistribute it and/or
|
|
||||||
# modify it under the terms of the GNU Lesser General Public
|
|
||||||
# License as published by the Free Software Foundation; either
|
|
||||||
# version 2 of the License, or (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pygtkmvc 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
|
|
||||||
# Lesser General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU Lesser General Public
|
|
||||||
# License along with this library; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor,
|
|
||||||
# Boston, MA 02110, USA.
|
|
||||||
#
|
|
||||||
# For more information on pygtkmvc see <http://pygtkmvc.sourceforge.net>
|
|
||||||
# or email to the author Roberto Cavada <cavada@fbk.eu>.
|
|
||||||
# Please report bugs to <cavada@fbk.eu>.
|
|
||||||
|
|
||||||
|
|
||||||
import gtk.glade
|
|
||||||
from controller import Controller
|
|
||||||
import types
|
|
||||||
|
|
||||||
class View (object):
|
|
||||||
|
|
||||||
def __init__(self, controller, glade_filename=None,
|
|
||||||
glade_top_widget_name=None, parent_view=None, register=True):
|
|
||||||
"""If register is False you *must* call 'controller.register_view(self)'
|
|
||||||
from the derived class constructor (i.e. registration is delayed)
|
|
||||||
If filename is not given (or None) all following parameters must be
|
|
||||||
not given (or None). In that case widgets must be connected manually.
|
|
||||||
glade_top_widget_name can be either a string name or list of names."""
|
|
||||||
self.manualWidgets = {}
|
|
||||||
self.autoWidgets = None
|
|
||||||
|
|
||||||
self.xmlWidgets = []
|
|
||||||
|
|
||||||
# Sets a callback for custom widgets
|
|
||||||
gtk.glade.set_custom_handler(self._custom_widget_create)
|
|
||||||
|
|
||||||
if (( type(glade_top_widget_name) == types.StringType)
|
|
||||||
or (glade_top_widget_name is None) ):
|
|
||||||
wids = (glade_top_widget_name,)
|
|
||||||
else: wids = glade_top_widget_name # Already a list or tuple
|
|
||||||
|
|
||||||
# retrieves XML objects from glade
|
|
||||||
if (glade_filename is not None):
|
|
||||||
for i in range(0,len(wids)):
|
|
||||||
self.xmlWidgets.append(gtk.glade.XML(glade_filename, wids[i]))
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
# top widget list or singleton:
|
|
||||||
if (glade_top_widget_name is not None):
|
|
||||||
if len(wids) > 1:
|
|
||||||
self.m_topWidget = []
|
|
||||||
for i in range(0, len(wids)):
|
|
||||||
self.m_topWidget.append(self[wids[i]])
|
|
||||||
pass
|
|
||||||
else: self.m_topWidget = self[wids[0]]
|
|
||||||
else: self.m_topWidget = None
|
|
||||||
|
|
||||||
if (glade_filename is not None): self.__autoconnect_signals(controller)
|
|
||||||
if (register): controller.register_view(self)
|
|
||||||
if (not parent_view is None): self.set_parent_view(parent_view)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Gives us the ability to do: view['widget_name'].action()
|
|
||||||
# Returns None if no widget name has been found.
|
|
||||||
def __getitem__(self, key):
|
|
||||||
wid = None
|
|
||||||
|
|
||||||
if self.autoWidgets:
|
|
||||||
if self.autoWidgets.has_key(key): wid = self.autoWidgets[key]
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
for xml in self.xmlWidgets:
|
|
||||||
wid = xml.get_widget(key)
|
|
||||||
if wid is not None: break
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
if wid is None:
|
|
||||||
# try with manually-added widgets:
|
|
||||||
if self.manualWidgets.has_key(key):
|
|
||||||
wid = self.manualWidgets[key]
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
return wid
|
|
||||||
|
|
||||||
# You can also add a single widget:
|
|
||||||
def __setitem__(self, key, wid):
|
|
||||||
self.manualWidgets[key] = wid
|
|
||||||
if (self.m_topWidget is None): self.m_topWidget = wid
|
|
||||||
return
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
ret = True
|
|
||||||
top = self.get_top_widget()
|
|
||||||
if type(top) in (types.ListType, types.TupleType):
|
|
||||||
for t in top:
|
|
||||||
if t is not None: ret = ret and t.show()
|
|
||||||
pass
|
|
||||||
elif (top is not None): ret = top.show_all()
|
|
||||||
else: ret = False
|
|
||||||
return ret
|
|
||||||
|
|
||||||
|
|
||||||
def hide(self):
|
|
||||||
top = self.get_top_widget()
|
|
||||||
if type(top) in (types.ListType, types.TupleType):
|
|
||||||
for t in top:
|
|
||||||
if t is not None: t.hide_all()
|
|
||||||
pass
|
|
||||||
elif top is not None: top.hide_all()
|
|
||||||
return
|
|
||||||
|
|
||||||
# Returns the top-level widget, or a list of top widgets
|
|
||||||
def get_top_widget(self):
|
|
||||||
return self.m_topWidget
|
|
||||||
|
|
||||||
|
|
||||||
# Set parent view:
|
|
||||||
def set_parent_view(self, parent_view):
|
|
||||||
top = self.get_top_widget()
|
|
||||||
if type(top) in (types.ListType, types.TupleType):
|
|
||||||
for t in top:
|
|
||||||
if t is not None:
|
|
||||||
t.set_transient_for(parent_view.get_top_widget())
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
elif (top is not None):
|
|
||||||
top.set_transient_for(parent_view.get_top_widget())
|
|
||||||
pass
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
# Set the transient for the view:
|
|
||||||
def set_transient(self, transient_view):
|
|
||||||
top = self.get_top_widget()
|
|
||||||
if type(top) in (types.ListType, types.TupleType):
|
|
||||||
for t in top:
|
|
||||||
if t is not None:
|
|
||||||
transient_view.get_top_widget().set_transient_for(t)
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
elif (top is not None):
|
|
||||||
transient_view.get_top_widget().set_transient_for(top)
|
|
||||||
pass
|
|
||||||
return
|
|
||||||
|
|
||||||
# Finds the right callback for custom widget creation and calls it
|
|
||||||
# Returns None if an undefined or invalid handler is found
|
|
||||||
def _custom_widget_create(self, glade, function_name, widget_name,
|
|
||||||
str1, str2, int1, int2):
|
|
||||||
# This code was kindly provided by Allan Douglas <zalguod at
|
|
||||||
# users.sourceforge.net>
|
|
||||||
if function_name is not None:
|
|
||||||
handler = getattr(self, function_name, None)
|
|
||||||
if handler is not None: return handler(str1, str2, int1, int2)
|
|
||||||
pass
|
|
||||||
return None
|
|
||||||
|
|
||||||
# implements the iteration protocol
|
|
||||||
def __iter__(self):
|
|
||||||
# pre-calculates the auto widgets if needed:
|
|
||||||
if self.autoWidgets is None:
|
|
||||||
self.autoWidgets = {}
|
|
||||||
|
|
||||||
for xml in self.xmlWidgets:
|
|
||||||
for wid in xml.get_widget_prefix(""):
|
|
||||||
wname = gtk.glade.get_widget_name(wid)
|
|
||||||
assert not self.autoWidgets.has_key(wname)
|
|
||||||
self.autoWidgets[wname] = wid
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.__idx = 0
|
|
||||||
self.__max1 = len(self.autoWidgets)
|
|
||||||
self.__max2 = self.__max1 + len(self.manualWidgets)
|
|
||||||
return self
|
|
||||||
|
|
||||||
# implements the iteration protocol
|
|
||||||
def next(self):
|
|
||||||
if self.__idx >= self.__max2: raise StopIteration()
|
|
||||||
if self.__idx >= self.__max1: m = self.manualWidgets
|
|
||||||
else: m = self.autoWidgets
|
|
||||||
self.__idx += 1
|
|
||||||
return m.keys()[self.__idx-1]
|
|
||||||
|
|
||||||
|
|
||||||
# performs Controller's signals auto-connection:
|
|
||||||
def __autoconnect_signals(self, controller):
|
|
||||||
dic = {}
|
|
||||||
for name in dir(controller):
|
|
||||||
method = getattr(controller, name)
|
|
||||||
if (not callable(method)): continue
|
|
||||||
assert(not dic.has_key(name)) # not already connected!
|
|
||||||
dic[name] = method
|
|
||||||
pass
|
|
||||||
|
|
||||||
for xml in self.xmlWidgets: xml.signal_autoconnect(dic)
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pass # end of class View
|
|
||||||
@@ -1,25 +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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,340 +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 gtkmvc import Model
|
|
||||||
|
|
||||||
import os
|
|
||||||
|
|
||||||
import gtk
|
|
||||||
import gobject
|
|
||||||
|
|
||||||
from ConfigParser import ConfigParser
|
|
||||||
|
|
||||||
class Ini(object):
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.ini = []
|
|
||||||
|
|
||||||
def add_section(self, section):
|
|
||||||
self.ini.append("[%s]" % section)
|
|
||||||
|
|
||||||
def add_key(self, key, value):
|
|
||||||
self.ini.append("%s=%s" % (key, value))
|
|
||||||
|
|
||||||
def add_comment(self, comment):
|
|
||||||
self.ini.append(";%s" % comment)
|
|
||||||
|
|
||||||
def add_verb(self, verb):
|
|
||||||
self.ini.append(verb)
|
|
||||||
|
|
||||||
def show(self):
|
|
||||||
return "\n".join(self.ini)
|
|
||||||
|
|
||||||
class ConfigModel(Model):
|
|
||||||
ini = Ini()
|
|
||||||
|
|
||||||
__properties__ = {}
|
|
||||||
|
|
||||||
filetype_list = ['Images', 'Movies']
|
|
||||||
|
|
||||||
confd = {
|
|
||||||
'savewin': True,
|
|
||||||
'savepan': True,
|
|
||||||
'wx': 800,
|
|
||||||
'wy': 600,
|
|
||||||
'h': 200,
|
|
||||||
'v': 300,
|
|
||||||
'eject': True,
|
|
||||||
'compress': True,
|
|
||||||
|
|
||||||
'exportxls': False,
|
|
||||||
|
|
||||||
'confirmquit': True,
|
|
||||||
'confirmabandon': True,
|
|
||||||
'mntwarn': True,
|
|
||||||
'delwarn': True,
|
|
||||||
|
|
||||||
'cd': '/mnt/cdrom',
|
|
||||||
'ejectapp': 'eject -r',
|
|
||||||
|
|
||||||
'imgview': False,
|
|
||||||
'imgprog': 'gqview',
|
|
||||||
|
|
||||||
'retrive': False,
|
|
||||||
|
|
||||||
'thumbs': True,
|
|
||||||
'exif': True,
|
|
||||||
'gthumb': False,
|
|
||||||
|
|
||||||
'extensions': {'bmp':'identify %s',
|
|
||||||
'gif':'identify "%s"',
|
|
||||||
'jpg':'identify "%s"',
|
|
||||||
'jpeg':'identify "%s"',
|
|
||||||
'png':'identify "%s"',
|
|
||||||
'avi':'midentify "%s"',
|
|
||||||
'mkv':'midentify "%s"',
|
|
||||||
'mpg':'midentify "%s"',
|
|
||||||
'mpeg':'midentify "%s"',
|
|
||||||
'wmv':'midentify "%s"',
|
|
||||||
},
|
|
||||||
|
|
||||||
'showtoolbar':True,
|
|
||||||
'showstatusbar':True,
|
|
||||||
}
|
|
||||||
|
|
||||||
dictconf = {
|
|
||||||
"save main window size": "savewin",
|
|
||||||
"save panes size": "savepan",
|
|
||||||
"main window width": "wx",
|
|
||||||
"main window height": "wy",
|
|
||||||
"horizontal panes": "h",
|
|
||||||
"vertical panes":"v",
|
|
||||||
"export xls":"exportxls",
|
|
||||||
"cd drive":"cd",
|
|
||||||
"eject command":"ejectapp",
|
|
||||||
"eject":"eject",
|
|
||||||
"image support":"thumbs",
|
|
||||||
'confirm quit':'confirmquit',
|
|
||||||
'warn mount/umount errors':'mntwarn',
|
|
||||||
'warn on delete':'delwarn',
|
|
||||||
'confirm abandon current catalog':'confirmabandon',
|
|
||||||
'show toolbar':'showtoolbar',
|
|
||||||
'show statusbar and progress bar':'showstatusbar',
|
|
||||||
'compress catalog':'compress',
|
|
||||||
'retrive extra informatin':'retrive',
|
|
||||||
'scan exif data':'exif',
|
|
||||||
'include gthumb image description':'gthumb',
|
|
||||||
'use external image viewer':'imgview',
|
|
||||||
'external image viewer program':'imgprog',
|
|
||||||
}
|
|
||||||
|
|
||||||
dbool = (
|
|
||||||
'exportxls',
|
|
||||||
'thumbs',
|
|
||||||
'savewin',
|
|
||||||
'savepan',
|
|
||||||
'eject',
|
|
||||||
'gthumb',
|
|
||||||
'exif',
|
|
||||||
'confirmquit',
|
|
||||||
'mntwarn',
|
|
||||||
'delwarn',
|
|
||||||
'confirmabandon',
|
|
||||||
'showtoolbar',
|
|
||||||
'showstatusbar',
|
|
||||||
'delwarn',
|
|
||||||
'compress',
|
|
||||||
'retrive',
|
|
||||||
'imgview',
|
|
||||||
)
|
|
||||||
|
|
||||||
recent = []
|
|
||||||
search_history = []
|
|
||||||
RECENT_MAX = 10
|
|
||||||
HISTORY_MAX = 20
|
|
||||||
|
|
||||||
dstring = ('cd','ejectapp','imgprog')
|
|
||||||
|
|
||||||
try:
|
|
||||||
path = os.path.join(os.environ['HOME'], ".pygtktalog")
|
|
||||||
except KeyError:
|
|
||||||
raise KeyError, "Cannot stat path for current user home!"
|
|
||||||
|
|
||||||
path = os.path.join(path, "config.ini")
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
Model.__init__(self)
|
|
||||||
self.category_tree = gtk.ListStore(gobject.TYPE_STRING)
|
|
||||||
|
|
||||||
self.refresh_ext()
|
|
||||||
return
|
|
||||||
|
|
||||||
def refresh_ext(self):
|
|
||||||
self.ext_tree = gtk.ListStore(gobject.TYPE_STRING,
|
|
||||||
gobject.TYPE_STRING)
|
|
||||||
keys = sorted(self.confd['extensions'].keys())
|
|
||||||
for i in keys:
|
|
||||||
myiter = self.ext_tree.insert_before(None,None)
|
|
||||||
self.ext_tree.set_value(myiter, 0, i)
|
|
||||||
self.ext_tree.set_value(myiter, 1, self.confd['extensions'][i])
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
try:
|
|
||||||
os.lstat(self.path)
|
|
||||||
except:
|
|
||||||
print "Saving preferences to %s." % self.path
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: save() Saving preferences to",
|
|
||||||
print "%s" % self.path
|
|
||||||
newIni = Ini()
|
|
||||||
|
|
||||||
# main section
|
|
||||||
newIni.add_section("pyGTKtalog conf")
|
|
||||||
for opt in self.dictconf:
|
|
||||||
newIni.add_key(opt,self.confd[self.dictconf[opt]])
|
|
||||||
|
|
||||||
# recent section
|
|
||||||
newIni.add_section("pyGTKtalog recent")
|
|
||||||
|
|
||||||
count = 1
|
|
||||||
max_count = self.RECENT_MAX + 1
|
|
||||||
for opt in self.recent:
|
|
||||||
if count < max_count:
|
|
||||||
newIni.add_key(count, opt)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
count+=1
|
|
||||||
|
|
||||||
# search history section
|
|
||||||
newIni.add_section("search history")
|
|
||||||
count = 1
|
|
||||||
max_count = self.HISTORY_MAX + 1
|
|
||||||
for opt in self.search_history:
|
|
||||||
if count < max_count:
|
|
||||||
newIni.add_key(count, opt)
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
count+=1
|
|
||||||
|
|
||||||
# extensions sections
|
|
||||||
newIni.add_section("extensions")
|
|
||||||
count = 1
|
|
||||||
for i in self.confd['extensions']:
|
|
||||||
newIni.add_key(i, self.confd['extensions'][i])
|
|
||||||
count+=1
|
|
||||||
|
|
||||||
# write config
|
|
||||||
try:
|
|
||||||
f = open(self.path, "w")
|
|
||||||
success = True
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: save() Cannot open config file",
|
|
||||||
print "%s for writing." % self.path
|
|
||||||
success = False
|
|
||||||
f.write(newIni.show())
|
|
||||||
f.close()
|
|
||||||
return success
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
try:
|
|
||||||
# try to read config file
|
|
||||||
parser = ConfigParser()
|
|
||||||
parser.read(self.path)
|
|
||||||
r = {}
|
|
||||||
h = {}
|
|
||||||
self.recent = []
|
|
||||||
self.search_history = []
|
|
||||||
for sec in parser.sections():
|
|
||||||
if sec == 'pyGTKtalog conf':
|
|
||||||
for opt in parser.options(sec):
|
|
||||||
i = self.dictconf[opt]
|
|
||||||
try:
|
|
||||||
if self.dictconf[opt] in self.dbool:
|
|
||||||
self.confd[i] = parser.getboolean(sec, opt)
|
|
||||||
elif self.dictconf[opt] in self.dstring:
|
|
||||||
self.confd[i] = parser.get(sec, opt)
|
|
||||||
else:
|
|
||||||
self.confd[i] = parser.getint(sec, opt)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: load() failed to parse",
|
|
||||||
print "option:", opt
|
|
||||||
pass
|
|
||||||
elif sec == 'pyGTKtalog recent':
|
|
||||||
for opt in parser.options(sec):
|
|
||||||
try:
|
|
||||||
r[int(opt)] = parser.get(sec, opt)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: load() failed to parse",
|
|
||||||
print "option:", opt
|
|
||||||
pass
|
|
||||||
elif sec == 'search history':
|
|
||||||
for opt in parser.options(sec):
|
|
||||||
try:
|
|
||||||
h[int(opt)] = parser.get(sec, opt)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: load() failed to parse",
|
|
||||||
print "option:", opt
|
|
||||||
pass
|
|
||||||
elif sec == 'extensions':
|
|
||||||
self.confd['extensions'] = {}
|
|
||||||
for opt in parser.options(sec):
|
|
||||||
try:
|
|
||||||
self.confd['extensions'][opt] = parser.get(sec,
|
|
||||||
opt)
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: load() failed to parse",
|
|
||||||
print "option:", opt
|
|
||||||
pass
|
|
||||||
|
|
||||||
for i in range(1, self.RECENT_MAX + 1):
|
|
||||||
if i in r:
|
|
||||||
self.recent.append(r[i])
|
|
||||||
for i in range(1, self.HISTORY_MAX + 1):
|
|
||||||
if i in h:
|
|
||||||
self.search_history.append(h[i])
|
|
||||||
|
|
||||||
except:
|
|
||||||
if __debug__:
|
|
||||||
print "m_config.py: load() load config file failed"
|
|
||||||
pass
|
|
||||||
|
|
||||||
def add_recent(self, path):
|
|
||||||
if not path:
|
|
||||||
return
|
|
||||||
|
|
||||||
if path in self.recent:
|
|
||||||
self.recent.remove(path)
|
|
||||||
self.recent.insert(0,path)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.recent.insert(0,path)
|
|
||||||
if len(self.recent) > self.RECENT_MAX:
|
|
||||||
self.recent = self.recent[:self.RECENT_MAX]
|
|
||||||
return
|
|
||||||
|
|
||||||
def add_search_history(self, text):
|
|
||||||
if not text:
|
|
||||||
return
|
|
||||||
|
|
||||||
if text in self.search_history:
|
|
||||||
self.search_history.remove(text)
|
|
||||||
self.search_history.insert(0, text)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.search_history.insert(0, text)
|
|
||||||
if len(self.search_history) > self.HISTORY_MAX:
|
|
||||||
self.search_history = self.search_history[:self.HISTORY_MAX]
|
|
||||||
return
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""show prefs in string way"""
|
|
||||||
string = "[varname]\tvalue\n"
|
|
||||||
for i in self.confd:
|
|
||||||
string+="%s\t%s\n" % (i,self.confd[i])
|
|
||||||
return string
|
|
||||||
1977
src/models/m_main.py
1977
src/models/m_main.py
File diff suppressed because it is too large
Load Diff
1214
src/utils/EXIF.py
1214
src/utils/EXIF.py
File diff suppressed because it is too large
Load Diff
@@ -1,25 +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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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"
|
|
||||||
|
|
||||||
@@ -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"
|
|
||||||
|
|
||||||
@@ -1,39 +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 os.path
|
|
||||||
import sys
|
|
||||||
|
|
||||||
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)
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
|
|
||||||
@@ -1,82 +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 xml.dom import minidom
|
|
||||||
import gzip
|
|
||||||
import os
|
|
||||||
from datetime import date
|
|
||||||
|
|
||||||
class GthumbCommentParser(object):
|
|
||||||
|
|
||||||
def __init__(self, image_path, image_filename):
|
|
||||||
self.path = image_path
|
|
||||||
self.filename = image_filename
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
"""Return dictionary with apropriate fields, or None if no comment
|
|
||||||
available"""
|
|
||||||
try:
|
|
||||||
gf = gzip.open(os.path.join(self.path,
|
|
||||||
'.comments', self.filename + '.xml'))
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
xml = gf.read()
|
|
||||||
gf.close()
|
|
||||||
except:
|
|
||||||
gf.close()
|
|
||||||
return None
|
|
||||||
|
|
||||||
if not xml:
|
|
||||||
return None
|
|
||||||
|
|
||||||
retval = {}
|
|
||||||
doc = minidom.parseString(xml)
|
|
||||||
|
|
||||||
try:
|
|
||||||
retval['note'] = doc.getElementsByTagName('Note').item(0)
|
|
||||||
retval['note'] = retval['note'].childNodes.item(0).data
|
|
||||||
except: retval['note'] = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
retval['place'] = doc.getElementsByTagName('Place').item(0)
|
|
||||||
retval['place'] = retval['place'].childNodes.item(0).data
|
|
||||||
except: retval['place'] = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
d = doc.getElementsByTagName('Time').item(0).childNodes
|
|
||||||
d = d.item(0).data
|
|
||||||
if int(d) > 0: retval['date'] = date.fromtimestamp(int(d))
|
|
||||||
else: retval['date'] = None
|
|
||||||
except: retval['date'] = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
retval['keywords'] = doc.getElementsByTagName('Keywords').item(0)
|
|
||||||
retval['keywords'] = retval['keywords'].childNodes.item(0)
|
|
||||||
retval['keywords'] = retval['keywords'].data.split(',')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
if len(retval) > 0: return retval
|
|
||||||
else: return None
|
|
||||||
|
|
||||||
@@ -1,79 +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 shutil import copy
|
|
||||||
from os import path, mkdir
|
|
||||||
from hashlib import sha512
|
|
||||||
from tempfile import mkstemp
|
|
||||||
|
|
||||||
from utils import EXIF
|
|
||||||
import Image
|
|
||||||
|
|
||||||
class Img(object):
|
|
||||||
|
|
||||||
def __init__(self, filename, base=''):
|
|
||||||
self.root = 'images'
|
|
||||||
self.x = 160
|
|
||||||
self.y = 160
|
|
||||||
self.filename = filename
|
|
||||||
self.base = base
|
|
||||||
f = open(filename, "r")
|
|
||||||
self.sha512 = sha512(f.read()).hexdigest()
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
"""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"
|
|
||||||
return self.sha512
|
|
||||||
|
|
||||||
if not path.exists(thumbnail):
|
|
||||||
im = self.__scale_image()
|
|
||||||
im.save(thumbnail, "JPEG")
|
|
||||||
|
|
||||||
# copy image
|
|
||||||
if not path.exists(image_filename):
|
|
||||||
copy(self.filename, image_filename)
|
|
||||||
|
|
||||||
return self.sha512
|
|
||||||
|
|
||||||
# private class functions
|
|
||||||
def __scale_image(self):
|
|
||||||
"""create thumbnail. returns image object or None"""
|
|
||||||
try:
|
|
||||||
im = Image.open(self.filename).convert('RGB')
|
|
||||||
except:
|
|
||||||
return None
|
|
||||||
x, y = im.size
|
|
||||||
if x > self.x or y > self.y:
|
|
||||||
im.thumbnail((self.x, self.y), Image.ANTIALIAS)
|
|
||||||
return im
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,117 +0,0 @@
|
|||||||
import re
|
|
||||||
import EXIF
|
|
||||||
class ParseExif(object):
|
|
||||||
|
|
||||||
def __init__(self, exif_dict=None, exif_file=None):
|
|
||||||
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
|
|
||||||
self.exif_dict = exif_dict
|
|
||||||
if not self.exif_dict:
|
|
||||||
try:
|
|
||||||
f = open(exif_file, 'rb')
|
|
||||||
e = EXIF.process_file(f)
|
|
||||||
if len(e.keys()) >0:
|
|
||||||
self.exif_dict = e
|
|
||||||
f.close()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def parse(self):
|
|
||||||
try:
|
|
||||||
self.camera = "%s" % self.exif_dict['Image Make']
|
|
||||||
self.camera = self.camera.strip()
|
|
||||||
except: pass
|
|
||||||
try:
|
|
||||||
model = "%s" % self.exif_dict['Image Model']
|
|
||||||
self.camera += ", " + model.strip()
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.date = "%s" % self.exif_dict['EXIF DateTimeOriginal']
|
|
||||||
p = re.compile('[\d,:]+')
|
|
||||||
if not p.match(self.date):
|
|
||||||
self.date = None
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.aperture = "%s" % self.exif_dict['EXIF FNumber']
|
|
||||||
if len(self.aperture.split("/")) == 2:
|
|
||||||
self.aperture += '.'
|
|
||||||
self.aperture = "%.1f" % eval(self.aperture)
|
|
||||||
self.aperture = "f/%.1f" % float(self.aperture)
|
|
||||||
self.aperture = self.aperture.replace('.',',')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.exposure_program = "%s" % \
|
|
||||||
self.exif_dict['EXIF ExposureProgram']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.exposure_bias = "%s" % \
|
|
||||||
self.exif_dict['EXIF ExposureBiasValue']
|
|
||||||
if len(self.exposure_bias.split("/")) == 2:
|
|
||||||
self.exposure_bias += '.'
|
|
||||||
self.exposure_bias = "%.1f" % eval(self.exposure_bias)
|
|
||||||
self.exposure_bias = "%.1f" % float(self.exposure_bias)
|
|
||||||
self.exposure_bias = self.exposure_bias.replace('.',',')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.iso = "%s" % self.exif_dict['EXIF ISOSpeedRatings']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.focal_length = "%s" % self.exif_dict['EXIF FocalLength']
|
|
||||||
if len(self.focal_length.split("/")) == 2:
|
|
||||||
self.focal_length += '.'
|
|
||||||
self.focal_length = "%.2f" % eval(self.focal_length)
|
|
||||||
self.focal_length = "%.2f mm" % float(self.focal_length)
|
|
||||||
self.focal_length = self.focal_length.replace('.',',')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.subject_distance = "%s" % \
|
|
||||||
self.exif_dict['EXIF SubjectDistance']
|
|
||||||
if len(self.subject_distance.split("/")) == 2:
|
|
||||||
self.subject_distance += '.'
|
|
||||||
self.subject_distance = "%.3f" % eval(self.subject_distance)
|
|
||||||
self.subject_distance = "%.3f m" % float(self.subject_distance)
|
|
||||||
self.subject_distance = self.subject_distance.replace('.',',')
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.metering_mode = "%s" % self.exif_dict['EXIF MeteringMode']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.flash = "%s" % self.exif_dict['EXIF Flash']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.light_source = "%s" % self.exif_dict['EXIF LightSource']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.resolution = "%s" % self.exif_dict['Image XResolution']
|
|
||||||
except: pass
|
|
||||||
try: self.resolution = self.resolution + " x %s" % \
|
|
||||||
self.exif_dict['Image YResolution']
|
|
||||||
except: pass
|
|
||||||
try: self.resolution = self.resolution + " (%s)" % \
|
|
||||||
self.exif_dict['Image ResolutionUnit']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
try: self.orientation = "%s" % self.exif_dict['Image Orientation']
|
|
||||||
except: pass
|
|
||||||
|
|
||||||
return (self.camera, self.date, self.aperture, self.exposure_program,
|
|
||||||
self.exposure_bias, self.iso, self.focal_length,
|
|
||||||
self.subject_distance, self.metering_mode, self.flash,
|
|
||||||
self.light_source, self.resolution, self.orientation)
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,25 +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
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,36 +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 gtkmvc import View
|
|
||||||
import os.path
|
|
||||||
import utils.globals
|
|
||||||
|
|
||||||
class ConfigView(View):
|
|
||||||
"""Preferences window from glade file """
|
|
||||||
GLADE = os.path.join(utils.globals.GLADE_DIR, "config.glade")
|
|
||||||
|
|
||||||
def __init__(self, ctrl):
|
|
||||||
View.__init__(self, ctrl, self.GLADE)
|
|
||||||
return
|
|
||||||
pass # end of class
|
|
||||||
@@ -1,621 +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 gtk
|
|
||||||
import gobject
|
|
||||||
import os
|
|
||||||
import utils.globals
|
|
||||||
|
|
||||||
class Qst(object):
|
|
||||||
"""Show simple dialog for questions
|
|
||||||
if "OK" button pressed, return "True"
|
|
||||||
"Cancel" button return "False"
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, title="", message="", secondarymsg=""):
|
|
||||||
self.dialog = gtk.MessageDialog(
|
|
||||||
flags = gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
type = gtk.MESSAGE_QUESTION,
|
|
||||||
buttons = gtk.BUTTONS_OK_CANCEL,
|
|
||||||
message_format = message,
|
|
||||||
)
|
|
||||||
self.dialog.set_title(title)
|
|
||||||
self.dialog.format_secondary_text(secondarymsg)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
retval = self.dialog.run()
|
|
||||||
self.dialog.destroy()
|
|
||||||
if retval == gtk.RESPONSE_OK:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class Inf(object):
|
|
||||||
"""Show simple dialog for notices"""
|
|
||||||
|
|
||||||
def __init__(self, title="", message="", secondarymsg=""):
|
|
||||||
self.dialog = gtk.MessageDialog(
|
|
||||||
flags = gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
type = gtk.MESSAGE_INFO,
|
|
||||||
buttons = gtk.BUTTONS_OK,
|
|
||||||
message_format = message,
|
|
||||||
)
|
|
||||||
self.dialog.set_title(title)
|
|
||||||
self.dialog.format_secondary_text(secondarymsg)
|
|
||||||
self.dialog.connect('response',
|
|
||||||
lambda dialog, response: self.ret(response))
|
|
||||||
self.dialog.show()
|
|
||||||
|
|
||||||
def ret(self,result):
|
|
||||||
self.dialog.destroy()
|
|
||||||
return True
|
|
||||||
|
|
||||||
class Wrn(object):
|
|
||||||
"""Show simple dialog for warnings"""
|
|
||||||
|
|
||||||
def __init__(self, title="", message="", secondarymsg=""):
|
|
||||||
self.dialog = gtk.MessageDialog(
|
|
||||||
flags = gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
type = gtk.MESSAGE_WARNING,
|
|
||||||
buttons = gtk.BUTTONS_CLOSE,
|
|
||||||
message_format = message,
|
|
||||||
)
|
|
||||||
self.dialog.set_title(title)
|
|
||||||
self.dialog.format_secondary_text(secondarymsg)
|
|
||||||
self.dialog.connect('response',
|
|
||||||
lambda dialog, response: self.ret(response))
|
|
||||||
self.dialog.show()
|
|
||||||
|
|
||||||
def ret(self,result):
|
|
||||||
self.dialog.destroy()
|
|
||||||
return True
|
|
||||||
|
|
||||||
class Err(object):
|
|
||||||
"""Show simple dialog for errors"""
|
|
||||||
|
|
||||||
def __init__(self, title="", message="", secondarymsg=""):
|
|
||||||
self.dialog = gtk.MessageDialog(
|
|
||||||
flags = gtk.DIALOG_DESTROY_WITH_PARENT,
|
|
||||||
type = gtk.MESSAGE_ERROR,
|
|
||||||
buttons = gtk.BUTTONS_CLOSE,
|
|
||||||
message_format = message)
|
|
||||||
|
|
||||||
self.dialog.set_title(title)
|
|
||||||
self.dialog.format_secondary_text(secondarymsg)
|
|
||||||
self.dialog.connect('response',
|
|
||||||
lambda dialog, response: self.ret(response))
|
|
||||||
self.dialog.run()
|
|
||||||
|
|
||||||
def ret(self,result):
|
|
||||||
self.dialog.destroy()
|
|
||||||
return True
|
|
||||||
|
|
||||||
class Abt(object):
|
|
||||||
"""Show simple about dialog"""
|
|
||||||
|
|
||||||
def __init__(self, name=None, ver="", title="", authors=[],licence=""):
|
|
||||||
self.dialog = gtk.AboutDialog()
|
|
||||||
self.dialog.set_title(title)
|
|
||||||
self.dialog.set_version(ver)
|
|
||||||
self.dialog.set_license(licence)
|
|
||||||
self.dialog.set_name(name)
|
|
||||||
self.dialog.set_authors(authors)
|
|
||||||
self.dialog.connect('response',
|
|
||||||
lambda dialog, response: self.dialog.destroy())
|
|
||||||
self.dialog.show()
|
|
||||||
|
|
||||||
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.label = ""
|
|
||||||
if label!= None:
|
|
||||||
self.label = label
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "inputDialog")
|
|
||||||
dialog = gladexml.get_widget("inputDialog")
|
|
||||||
entry = gladexml.get_widget("volname")
|
|
||||||
entry.set_text(self.label)
|
|
||||||
result = dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
return entry.get_text()
|
|
||||||
return None
|
|
||||||
|
|
||||||
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.label = ""
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "renameDialog")
|
|
||||||
dialog = gladexml.get_widget("renameDialog")
|
|
||||||
entry = gladexml.get_widget("name")
|
|
||||||
entry.set_text(self.name)
|
|
||||||
result = dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
return entry.get_text()
|
|
||||||
return None
|
|
||||||
|
|
||||||
class PointDirectoryToAdd(object):
|
|
||||||
"""Sepcific dialog for quering user for selecting directory to add"""
|
|
||||||
|
|
||||||
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.gladexml = gtk.glade.XML(self.gladefile, "addDirDialog")
|
|
||||||
self.volname = self.gladexml.get_widget("dirvolname")
|
|
||||||
self.volname.set_text(volname)
|
|
||||||
self.directory = self.gladexml.get_widget("directory")
|
|
||||||
self.directory.set_text(dirname)
|
|
||||||
sigs = {"on_browse_activate":self.show_dirchooser,
|
|
||||||
"on_browse_clicked":self.show_dirchooser}
|
|
||||||
self.gladexml.signal_autoconnect(sigs)
|
|
||||||
|
|
||||||
def show_dirchooser(self,widget):
|
|
||||||
"""dialog for point the mountpoint"""
|
|
||||||
dialog = gtk.FileChooserDialog(
|
|
||||||
title="Choose directory to add",
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons=(
|
|
||||||
gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
dialog.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
if self.URI:
|
|
||||||
dialog.set_current_folder_uri(self.URI)
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
self.directory.set_text(dialog.get_filename())
|
|
||||||
self.__class__.URI = dialog.get_current_folder_uri()
|
|
||||||
dialog.destroy()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
dialog = self.gladexml.get_widget("addDirDialog")
|
|
||||||
ch = True
|
|
||||||
result = dialog.run()
|
|
||||||
while ch:
|
|
||||||
if result == gtk.RESPONSE_OK and (self.volname.get_text()=='' or \
|
|
||||||
self.directory.get_text() == ''):
|
|
||||||
a = Err("Error - pyGTKtalog",
|
|
||||||
"There are fields needed to be filled.",
|
|
||||||
"Cannot add directory without path and disc label.")
|
|
||||||
ch = True
|
|
||||||
result = dialog.run()
|
|
||||||
else:
|
|
||||||
ch = False
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
return self.volname.get_text(),self.directory.get_text()
|
|
||||||
else:
|
|
||||||
return None,None
|
|
||||||
|
|
||||||
class SelectDirectory(object):
|
|
||||||
"""Sepcific dialog for quering user for selecting directory to add"""
|
|
||||||
|
|
||||||
URI="file://"+os.path.abspath(os.path.curdir)
|
|
||||||
|
|
||||||
def __init__(self, title=None):
|
|
||||||
if title:
|
|
||||||
self.title = title
|
|
||||||
else:
|
|
||||||
self.title = "Choose directory"
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""dialog for point the mountpoint"""
|
|
||||||
dialog = gtk.FileChooserDialog(
|
|
||||||
title = self.title,
|
|
||||||
action = gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons = (
|
|
||||||
gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
dialog.set_action(gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER)
|
|
||||||
dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
retval = None
|
|
||||||
|
|
||||||
if self.URI:
|
|
||||||
dialog.set_current_folder_uri(self.URI)
|
|
||||||
response = dialog.run()
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
retval = dialog.get_filename()
|
|
||||||
self.__class__.URI = dialog.get_current_folder_uri()
|
|
||||||
dialog.destroy()
|
|
||||||
return retval
|
|
||||||
|
|
||||||
class ChooseDBFilename(object):
|
|
||||||
"""Sepcific dialog for quering user for selecting filename for database"""
|
|
||||||
|
|
||||||
URI=None
|
|
||||||
|
|
||||||
def __init__(self, path=None):
|
|
||||||
self.path = path
|
|
||||||
self.dialog = gtk.FileChooserDialog(
|
|
||||||
title="Save catalog as...",
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_SAVE,
|
|
||||||
buttons=(
|
|
||||||
gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_SAVE,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
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...')
|
|
||||||
|
|
||||||
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()
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
self.dialog.destroy()
|
|
||||||
return None
|
|
||||||
pass
|
|
||||||
|
|
||||||
class LoadDBFile(object):
|
|
||||||
"""Specific class for displaying openFile dialog. It has veryfication
|
|
||||||
for file existence."""
|
|
||||||
|
|
||||||
URI = None
|
|
||||||
|
|
||||||
def __init__(self, path=None):
|
|
||||||
self.path = path
|
|
||||||
|
|
||||||
self.dialog = gtk.FileChooserDialog(
|
|
||||||
title="Open catalog",
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons=(
|
|
||||||
gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
|
|
||||||
self.dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
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 show_dialog(self):
|
|
||||||
response = self.dialog.run()
|
|
||||||
filename = None
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
try:
|
|
||||||
filename = self.dialog.get_filename()
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
#self.dialog.destroy()
|
|
||||||
return 'ok',filename
|
|
||||||
else:
|
|
||||||
return 'cancel',None
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
res,filename = self.show_dialog()
|
|
||||||
ch = True
|
|
||||||
while ch:
|
|
||||||
if res == 'cancel':
|
|
||||||
self.dialog.destroy()
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
os.stat(filename)
|
|
||||||
self.__class__.URI = self.dialog.get_current_folder_uri()
|
|
||||||
self.dialog.destroy()
|
|
||||||
return filename
|
|
||||||
except:
|
|
||||||
a = Err("Error - pyGTKtalog","File doesn't exist.",
|
|
||||||
"The file that you choose does not exist." + \
|
|
||||||
" Choose another one, or cancel operation.")
|
|
||||||
ch = True
|
|
||||||
res, filename = self.show_dialog()
|
|
||||||
|
|
||||||
|
|
||||||
class LoadImageFile(object):
|
|
||||||
"""class for displaying openFile dialog. It have possibility of multiple
|
|
||||||
selection."""
|
|
||||||
|
|
||||||
URI="file://"+os.path.abspath(os.path.curdir)
|
|
||||||
|
|
||||||
def __init__(self, multiple=False):
|
|
||||||
self.dialog = gtk.FileChooserDialog(
|
|
||||||
title="Select image",
|
|
||||||
action=gtk.FILE_CHOOSER_ACTION_OPEN,
|
|
||||||
buttons=(
|
|
||||||
gtk.STOCK_CANCEL,
|
|
||||||
gtk.RESPONSE_CANCEL,
|
|
||||||
gtk.STOCK_OPEN,
|
|
||||||
gtk.RESPONSE_OK))
|
|
||||||
self.dialog.set_select_multiple(multiple)
|
|
||||||
self.dialog.set_default_response(gtk.RESPONSE_OK)
|
|
||||||
|
|
||||||
f = gtk.FileFilter()
|
|
||||||
f.set_name("All Images")
|
|
||||||
for i in ['*.jpg', '*.jpeg', '*.gif', '*.png', '*.tif', '*.tiff',
|
|
||||||
'*.tga', '*.pcx', '*.bmp', '*.xbm', '*.xpm', '*.jp2',
|
|
||||||
'*.jpx', '*.pnm', '*.JPG', '*.JPEG', '*.GIF', '*.PNG',
|
|
||||||
'*.TIF', '*.TIFF', '*.TGA', '*.PCX', '*.BMP', '*.XBM',
|
|
||||||
'*.XPM', '*.JP2', '*.JPX', '*.PNM']:
|
|
||||||
f.add_pattern(i)
|
|
||||||
self.dialog.add_filter(f)
|
|
||||||
f = gtk.FileFilter()
|
|
||||||
f.set_name("All files")
|
|
||||||
f.add_pattern("*.*")
|
|
||||||
self.dialog.add_filter(f)
|
|
||||||
self.preview = gtk.Image()
|
|
||||||
|
|
||||||
self.dialog.set_preview_widget(self.preview)
|
|
||||||
self.dialog.connect("update-preview", self.update_preview_cb)
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if self.URI:
|
|
||||||
self.dialog.set_current_folder_uri(self.URI)
|
|
||||||
response = self.dialog.run()
|
|
||||||
filenames = None
|
|
||||||
only_thumbs = False
|
|
||||||
|
|
||||||
if response == gtk.RESPONSE_OK:
|
|
||||||
try:
|
|
||||||
if self.dialog.get_select_multiple():
|
|
||||||
filenames = self.dialog.get_filenames()
|
|
||||||
else:
|
|
||||||
filenames = self.dialog.get_filename()
|
|
||||||
|
|
||||||
if self.dialog.get_extra_widget().get_active():
|
|
||||||
only_thumbs = True
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.__class__.URI = self.dialog.get_current_folder_uri()
|
|
||||||
self.dialog.destroy()
|
|
||||||
return filenames, only_thumbs
|
|
||||||
|
|
||||||
def update_preview_cb(self, widget):
|
|
||||||
filename = self.dialog.get_preview_filename()
|
|
||||||
try:
|
|
||||||
pixbuf = gtk.gdk.pixbuf_new_from_file_at_size(filename, 128, 128)
|
|
||||||
self.preview.set_from_pixbuf(pixbuf)
|
|
||||||
have_preview = True
|
|
||||||
except:
|
|
||||||
have_preview = False
|
|
||||||
self.dialog.set_preview_widget_active(have_preview)
|
|
||||||
return
|
|
||||||
|
|
||||||
class StatsDialog(object):
|
|
||||||
"""Sepcific dialog for display stats"""
|
|
||||||
|
|
||||||
def __init__(self, values={}):
|
|
||||||
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
|
|
||||||
self.values = values
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "statDialog")
|
|
||||||
dialog = gladexml.get_widget("statDialog")
|
|
||||||
|
|
||||||
if 'discs' in self.values:
|
|
||||||
entry = gladexml.get_widget("discs_entry")
|
|
||||||
entry.set_text(str(self.values['discs']))
|
|
||||||
else:
|
|
||||||
label = gladexml.get_widget("discs_label")
|
|
||||||
entry = gladexml.get_widget("discs_entry")
|
|
||||||
label.hide()
|
|
||||||
entry.hide()
|
|
||||||
|
|
||||||
if 'dirs' in self.values:
|
|
||||||
entry = gladexml.get_widget("dirs_entry")
|
|
||||||
entry.set_text(str(self.values['dirs']))
|
|
||||||
else:
|
|
||||||
label = gladexml.get_widget("dirs_label")
|
|
||||||
entry = gladexml.get_widget("dirs_entry")
|
|
||||||
label.hide()
|
|
||||||
entry.hide()
|
|
||||||
|
|
||||||
if 'files' in self.values:
|
|
||||||
entry = gladexml.get_widget("files_entry")
|
|
||||||
entry.set_text(str(self.values['files']))
|
|
||||||
|
|
||||||
if 'size' in self.values:
|
|
||||||
entry = gladexml.get_widget("size_entry")
|
|
||||||
entry.set_text(str(self.values['size']))
|
|
||||||
|
|
||||||
result = dialog.run()
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
return entry.get_text()
|
|
||||||
return None
|
|
||||||
|
|
||||||
class TagsDialog(object):
|
|
||||||
"""Sepcific dialog for display stats"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "tagsDialog")
|
|
||||||
dialog = gladexml.get_widget("tagsDialog")
|
|
||||||
|
|
||||||
entry = gladexml.get_widget("tag_entry1")
|
|
||||||
|
|
||||||
result = dialog.run()
|
|
||||||
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
return entry.get_text()
|
|
||||||
return None
|
|
||||||
|
|
||||||
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.tag_dict = tag_dict
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
if not self.tag_dict:
|
|
||||||
return None
|
|
||||||
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "tagRemove")
|
|
||||||
dialog = gladexml.get_widget("tagRemove")
|
|
||||||
|
|
||||||
# declare model
|
|
||||||
model = gtk.ListStore(gobject.TYPE_INT,
|
|
||||||
gobject.TYPE_STRING, gobject.TYPE_BOOLEAN)
|
|
||||||
# sort dict
|
|
||||||
values = self.tag_dict.values()
|
|
||||||
values.sort()
|
|
||||||
keys = []
|
|
||||||
for val in values:
|
|
||||||
for d_key, d_value in self.tag_dict.items():
|
|
||||||
if d_value == val:
|
|
||||||
keys.append(d_key)
|
|
||||||
|
|
||||||
# fill model with dict
|
|
||||||
for count in range(len(keys)):
|
|
||||||
myiter = model.insert_before(None, None)
|
|
||||||
model.set_value(myiter, 0, keys[count])
|
|
||||||
model.set_value(myiter, 1, values[count])
|
|
||||||
model.set_value(myiter, 2, None)
|
|
||||||
|
|
||||||
def toggle(cell, path, model):
|
|
||||||
model[path][2] = not model[path][2]
|
|
||||||
|
|
||||||
def toggle_all(column, model):
|
|
||||||
for row in model:
|
|
||||||
row[2] = not row[2]
|
|
||||||
|
|
||||||
treeview = gladexml.get_widget("treeview1")
|
|
||||||
treeview.set_model(model)
|
|
||||||
|
|
||||||
renderer = gtk.CellRendererText()
|
|
||||||
column = gtk.TreeViewColumn("Tag", renderer, text=1)
|
|
||||||
column.set_property('expand', True)
|
|
||||||
treeview.append_column(column)
|
|
||||||
|
|
||||||
renderer = gtk.CellRendererToggle()
|
|
||||||
renderer.set_property('activatable', True)
|
|
||||||
renderer.connect('toggled', toggle, model)
|
|
||||||
column = gtk.TreeViewColumn("Toggle", renderer)
|
|
||||||
column.add_attribute(renderer, "active", 2)
|
|
||||||
column.set_property('expand', False)
|
|
||||||
column.set_property("clickable", True)
|
|
||||||
column.connect("clicked", toggle_all, model)
|
|
||||||
treeview.append_column(column)
|
|
||||||
|
|
||||||
result = dialog.run()
|
|
||||||
|
|
||||||
dialog.destroy()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
ids = []
|
|
||||||
for i in model:
|
|
||||||
if i[2]:
|
|
||||||
ids.append(i[0])
|
|
||||||
return "ok", ids
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
class EditDialog(object):
|
|
||||||
"""Sepcific dialog for display stats"""
|
|
||||||
|
|
||||||
def __init__(self, values={}):
|
|
||||||
self.gladefile = os.path.join(utils.globals.GLADE_DIR, "dialogs.glade")
|
|
||||||
self.values = values
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
gladexml = gtk.glade.XML(self.gladefile, "file_editDialog")
|
|
||||||
dialog = gladexml.get_widget("file_editDialog")
|
|
||||||
|
|
||||||
filename = gladexml.get_widget("filename_entry")
|
|
||||||
filename.set_text(str(self.values['filename']))
|
|
||||||
description = gladexml.get_widget("description_text")
|
|
||||||
note = gladexml.get_widget("note_text")
|
|
||||||
|
|
||||||
if 'description' in self.values:
|
|
||||||
buff = gtk.TextBuffer()
|
|
||||||
buff.set_text(str(self.values['description']))
|
|
||||||
description.set_buffer(buff)
|
|
||||||
|
|
||||||
if 'note' in self.values:
|
|
||||||
buff = gtk.TextBuffer()
|
|
||||||
buff.set_text(str(self.values['note']))
|
|
||||||
note.set_buffer(buff)
|
|
||||||
|
|
||||||
result = dialog.run()
|
|
||||||
if result == gtk.RESPONSE_OK:
|
|
||||||
d = description.get_buffer()
|
|
||||||
n = note.get_buffer()
|
|
||||||
retval = {'filename': filename.get_text(),
|
|
||||||
'description': d.get_text(d.get_start_iter(),
|
|
||||||
d.get_end_iter()),
|
|
||||||
'note': n.get_text(n.get_start_iter(), n.get_end_iter())}
|
|
||||||
dialog.destroy()
|
|
||||||
return retval
|
|
||||||
dialog.destroy()
|
|
||||||
return None
|
|
||||||
@@ -1,68 +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 gtk
|
|
||||||
|
|
||||||
class ImageView(object):
|
|
||||||
"""simple image viewer. no scaling, no zooming, no rotating.
|
|
||||||
simply show stupid image"""
|
|
||||||
|
|
||||||
def __init__(self, image_filename):
|
|
||||||
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
|
||||||
|
|
||||||
image = gtk.Image()
|
|
||||||
image.set_from_file(image_filename)
|
|
||||||
|
|
||||||
pixbuf = image.get_pixbuf()
|
|
||||||
pic_width = pixbuf.get_width() + 23
|
|
||||||
pic_height = pixbuf.get_height() + 23
|
|
||||||
|
|
||||||
screen_width = gtk.gdk.screen_width()
|
|
||||||
screen_height = gtk.gdk.screen_height()
|
|
||||||
|
|
||||||
need_vieport = False
|
|
||||||
if pic_height > (screen_height - 128):
|
|
||||||
height = screen_height - 128
|
|
||||||
need_vieport = True
|
|
||||||
else:
|
|
||||||
height = screen_height - 128
|
|
||||||
|
|
||||||
if pic_width > (screen_width - 128):
|
|
||||||
width = screen_width - 128
|
|
||||||
need_vieport = True
|
|
||||||
else:
|
|
||||||
width = pic_width
|
|
||||||
|
|
||||||
if need_vieport:
|
|
||||||
window.resize(width, height)
|
|
||||||
viewport = gtk.ScrolledWindow()
|
|
||||||
viewport.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
|
|
||||||
viewport.add_with_viewport(image)
|
|
||||||
window.add(viewport)
|
|
||||||
else:
|
|
||||||
window.add(image)
|
|
||||||
window.show_all()
|
|
||||||
return
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
@@ -1,44 +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 os.path
|
|
||||||
import utils.globals
|
|
||||||
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")
|
|
||||||
|
|
||||||
def __init__(self, ctrl):
|
|
||||||
View.__init__(self, ctrl, self.GLADE)
|
|
||||||
|
|
||||||
# hide v2.0 features
|
|
||||||
self['separatormenuitem4'].hide()
|
|
||||||
self['list1'].hide()
|
|
||||||
self['thumbnails1'].hide()
|
|
||||||
return
|
|
||||||
|
|
||||||
pass # end of class
|
|
||||||
@@ -1,38 +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 gtkmvc import View
|
|
||||||
import os.path
|
|
||||||
import utils.globals
|
|
||||||
|
|
||||||
class SearchView(View):
|
|
||||||
"""Search window from glade file """
|
|
||||||
|
|
||||||
GLADE = os.path.join(utils.globals.GLADE_DIR, "search.glade")
|
|
||||||
|
|
||||||
def __init__(self, ctrl):
|
|
||||||
View.__init__(self, ctrl, self.GLADE)
|
|
||||||
return
|
|
||||||
pass # end of class
|
|
||||||
|
|
||||||
4
test-requirements.txt
Normal file
4
test-requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pytest
|
||||||
|
pytest-cov
|
||||||
|
flake8
|
||||||
|
coverage
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
28
tests/dbcommon_test.py
Normal file
28
tests/dbcommon_test.py
Normal 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 pycatalog.dbcommon import connect
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
33
tests/misc_test.py
Normal file
33
tests/misc_test.py
Normal 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 pycatalog.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()
|
||||||
192
tests/scan_test.py
Normal file
192
tests/scan_test.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
"""
|
||||||
|
Project: pyGTKtalog
|
||||||
|
Description: Tests for scan files.
|
||||||
|
Type: test
|
||||||
|
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
|
||||||
|
Created: 2011-03-26
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from pycatalog import scan
|
||||||
|
from pycatalog.dbobjects import File, Config, Image
|
||||||
|
from pycatalog.dbcommon import connect, Session
|
||||||
|
|
||||||
|
|
||||||
|
def populate_with_mock_files(dir_):
|
||||||
|
"""Make some files under specified directory, return number of files"""
|
||||||
|
files1 = ['anim.mkv', 'text.txt', 'image.png', 'photoimage.jpg']
|
||||||
|
files2 = ['music.mp3', 'loseless.flac']
|
||||||
|
|
||||||
|
files_no = 0
|
||||||
|
for file_ in files1:
|
||||||
|
with open(os.path.join(dir_, file_), "wb") as fobj:
|
||||||
|
fobj.write(b"\xde\xad\xbe\xef" * len(file_))
|
||||||
|
files_no += 1
|
||||||
|
|
||||||
|
os.symlink(os.path.join(dir_, files1[-1]), os.path.join(dir_, 'link.jpg'))
|
||||||
|
files_no += 1
|
||||||
|
|
||||||
|
os.mkdir(os.path.join(dir_, 'directory'))
|
||||||
|
for file_ in files2:
|
||||||
|
with open(os.path.join(dir_, 'directory', file_), "wb") as fobj:
|
||||||
|
fobj.write(b"\xfe\xad\xfa\xce" * len(file_))
|
||||||
|
files_no += 1
|
||||||
|
|
||||||
|
return files_no
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: exchange this with mock module
|
||||||
|
def _fake_video(obj, fobj, filepath):
|
||||||
|
fobj.images.append(Image())
|
||||||
|
fobj.images[0].filename = filepath + ".jpg"
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_audio(obj, fobj, filepath):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _fake_image(obj, fobj, filepath):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
scan.Scan._video = _fake_video
|
||||||
|
scan.Scan._audio = _fake_audio
|
||||||
|
scan.Scan._image = _fake_image
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
self.image_path = tempfile.mkdtemp()
|
||||||
|
self.scan_dir = tempfile.mkdtemp()
|
||||||
|
self.no_of_files = populate_with_mock_files(self.scan_dir)
|
||||||
|
|
||||||
|
connect()
|
||||||
|
root = File()
|
||||||
|
root.id = 1
|
||||||
|
root.filename = 'root'
|
||||||
|
root.size = 0
|
||||||
|
root.source = 0
|
||||||
|
root.type = 0
|
||||||
|
root.parent_id = 1
|
||||||
|
|
||||||
|
config = Config()
|
||||||
|
config.key = 'image_path'
|
||||||
|
config.value = self.image_path
|
||||||
|
|
||||||
|
sess = Session()
|
||||||
|
sess.add(root)
|
||||||
|
sess.add(config)
|
||||||
|
sess.commit()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.image_path)
|
||||||
|
shutil.rmtree(self.scan_dir)
|
||||||
|
|
||||||
|
def test_happy_scenario(self):
|
||||||
|
"""
|
||||||
|
make scan, count items
|
||||||
|
"""
|
||||||
|
scanob = scan.Scan(self.scan_dir)
|
||||||
|
result_list = scanob.add_files()
|
||||||
|
|
||||||
|
# the number of added objects (files/links only) + "directory" +
|
||||||
|
# topmost directory (self.scan_dir)
|
||||||
|
self.assertEqual(len(result_list), self.no_of_files + 2)
|
||||||
|
|
||||||
|
# all of topmost nide children - including "directory", but excluding
|
||||||
|
# its contents - so it is all_files + 1 (directory) - 2 files from
|
||||||
|
# subdir contents
|
||||||
|
self.assertEqual(len(result_list[0].children), self.no_of_files - 1)
|
||||||
|
# check soft links
|
||||||
|
self.assertEqual(len([x for x in result_list if x.type == 3]), 1)
|
||||||
|
|
||||||
|
def test_wrong_and_nonexistent(self):
|
||||||
|
"""
|
||||||
|
Check for accessing non existent directory, regular file instead of
|
||||||
|
the directory.
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
def test_abort_functionality(self):
|
||||||
|
scanobj = scan.Scan(self.scan_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(self.scan_dir)
|
||||||
|
scanob.add_files()
|
||||||
|
|
||||||
|
# dirs: main one + "directory" subdir
|
||||||
|
self.assertEqual(len(ses.query(File).filter(File.type == 1).all()), 2)
|
||||||
|
|
||||||
|
# files: '-1' for existing link there, which have it's own type
|
||||||
|
self.assertEqual(len(ses.query(File).filter(File.type == 2).all()),
|
||||||
|
self.no_of_files - 1)
|
||||||
|
# links
|
||||||
|
self.assertEqual(len(ses.query(File).filter(File.type == 3).all()), 1)
|
||||||
|
|
||||||
|
# all - sum of all of the above + root node
|
||||||
|
self.assertEqual(len(ses.query(File).all()), self.no_of_files + 2 + 1)
|
||||||
|
|
||||||
|
# it is perfectly ok, since we don't update collection, but just added
|
||||||
|
# same directory twice.
|
||||||
|
scanob2 = scan.Scan(self.scan_dir)
|
||||||
|
scanob2.add_files()
|
||||||
|
# we have twice as much of files (self.no_of_files), plus 2 * of
|
||||||
|
# topmost dir and subdir "directory" (means 4) + root element
|
||||||
|
self.assertEqual(len(ses.query(File).all()), self.no_of_files * 2 + 5)
|
||||||
|
|
||||||
|
# get some movie files to examine
|
||||||
|
file_ob = [x for x in scanob._files if x.filename == 'anim.mkv'][0]
|
||||||
|
file2_ob = [x for x in scanob2._files if x.filename == 'anim.mkv'][0]
|
||||||
|
|
||||||
|
# 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()
|
||||||
397
tests/video_test.py
Normal file
397
tests/video_test.py
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
"""
|
||||||
|
Project: pyGTKtalog
|
||||||
|
Description: Tests for Video class.
|
||||||
|
Type: test
|
||||||
|
Author: Roman 'gryf' Dobosz, gryf73@gmail.com
|
||||||
|
Created: 2008-12-15
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
import io
|
||||||
|
|
||||||
|
import PIL
|
||||||
|
|
||||||
|
from pycatalog.video import Video
|
||||||
|
|
||||||
|
|
||||||
|
DATA = {"m1.avi": """ID_VIDEO_ID=0
|
||||||
|
ID_AUDIO_ID=1
|
||||||
|
ID_FILENAME=m1.avi
|
||||||
|
ID_DEMUXER=avi
|
||||||
|
ID_VIDEO_FORMAT=H264
|
||||||
|
ID_VIDEO_BITRATE=46184
|
||||||
|
ID_VIDEO_WIDTH=128
|
||||||
|
ID_VIDEO_HEIGHT=96
|
||||||
|
ID_VIDEO_FPS=30.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=85
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=0
|
||||||
|
ID_AUDIO_NCH=0
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=4.03
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffh264
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=2
|
||||||
|
ID_AUDIO_CODEC=mpg123
|
||||||
|
ID_EXIT=EOF
|
||||||
|
""",
|
||||||
|
"m.avi": """ID_VIDEO_ID=0
|
||||||
|
ID_AUDIO_ID=1
|
||||||
|
ID_FILENAME=m.avi
|
||||||
|
ID_DEMUXER=avi
|
||||||
|
ID_VIDEO_FORMAT=XVID
|
||||||
|
ID_VIDEO_BITRATE=313536
|
||||||
|
ID_VIDEO_WIDTH=128
|
||||||
|
ID_VIDEO_HEIGHT=96
|
||||||
|
ID_VIDEO_FPS=30.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=85
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=0
|
||||||
|
ID_AUDIO_NCH=0
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=4.03
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffodivx
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=2
|
||||||
|
ID_AUDIO_CODEC=mpg123
|
||||||
|
ID_EXIT=EOF""",
|
||||||
|
"m.mkv": """ID_VIDEO_ID=0
|
||||||
|
ID_AUDIO_ID=0
|
||||||
|
ID_CLIP_INFO_NAME0=title
|
||||||
|
ID_CLIP_INFO_VALUE0=Avidemux
|
||||||
|
ID_CLIP_INFO_NAME1=encoder
|
||||||
|
ID_CLIP_INFO_VALUE1=Lavf51.12.1
|
||||||
|
ID_CLIP_INFO_N=2
|
||||||
|
ID_FILENAME=m.mkv
|
||||||
|
ID_DEMUXER=lavfpref
|
||||||
|
ID_VIDEO_FORMAT=MP4V
|
||||||
|
ID_VIDEO_BITRATE=0
|
||||||
|
ID_VIDEO_WIDTH=128
|
||||||
|
ID_VIDEO_HEIGHT=96
|
||||||
|
ID_VIDEO_FPS=30.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=8192
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=1
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=4.07
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffodivx
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=1
|
||||||
|
ID_AUDIO_CODEC=ffac3
|
||||||
|
ID_EXIT=EOF""",
|
||||||
|
"m.mpg": """ID_VIDEO_ID=0
|
||||||
|
ID_FILENAME=m.mpg
|
||||||
|
ID_DEMUXER=mpeges
|
||||||
|
ID_VIDEO_FORMAT=0x10000001
|
||||||
|
ID_VIDEO_BITRATE=2200000
|
||||||
|
ID_VIDEO_WIDTH=128
|
||||||
|
ID_VIDEO_HEIGHT=96
|
||||||
|
ID_VIDEO_FPS=30.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=0.97
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffmpeg1
|
||||||
|
ID_EXIT=EOF""",
|
||||||
|
"m.ogm": """ID_VIDEO_ID=0
|
||||||
|
ID_AUDIO_ID=0
|
||||||
|
ID_FILENAME=m.ogm
|
||||||
|
ID_DEMUXER=lavfpref
|
||||||
|
ID_VIDEO_FORMAT=H264
|
||||||
|
ID_VIDEO_BITRATE=0
|
||||||
|
ID_VIDEO_WIDTH=160
|
||||||
|
ID_VIDEO_HEIGHT=120
|
||||||
|
ID_VIDEO_FPS=30.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=8192
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=1
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=4.00
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffh264
|
||||||
|
ID_AUDIO_BITRATE=128000
|
||||||
|
ID_AUDIO_RATE=22050
|
||||||
|
ID_AUDIO_NCH=1
|
||||||
|
ID_AUDIO_CODEC=ffac3
|
||||||
|
ID_EXIT=EOF""",
|
||||||
|
"m.wmv": """ID_AUDIO_ID=1
|
||||||
|
ID_VIDEO_ID=2
|
||||||
|
ID_FILENAME=m.wmv
|
||||||
|
ID_DEMUXER=asf
|
||||||
|
ID_VIDEO_FORMAT=WMV3
|
||||||
|
ID_VIDEO_BITRATE=1177000
|
||||||
|
ID_VIDEO_WIDTH=852
|
||||||
|
ID_VIDEO_HEIGHT=480
|
||||||
|
ID_VIDEO_FPS=1000.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=353
|
||||||
|
ID_AUDIO_BITRATE=0
|
||||||
|
ID_AUDIO_RATE=0
|
||||||
|
ID_AUDIO_NCH=0
|
||||||
|
ID_START_TIME=4.00
|
||||||
|
ID_LENGTH=4656.93
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffwmv3
|
||||||
|
ID_AUDIO_BITRATE=64028
|
||||||
|
ID_AUDIO_RATE=48000
|
||||||
|
ID_AUDIO_NCH=2
|
||||||
|
ID_AUDIO_CODEC=ffwmav2
|
||||||
|
ID_EXIT=EOF""",
|
||||||
|
"m.mp4": """ID_VIDEO_ID=0
|
||||||
|
ID_AUDIO_ID=0
|
||||||
|
ID_AID_0_LANG=unk
|
||||||
|
ID_CLIP_INFO_NAME0=major_brand
|
||||||
|
ID_CLIP_INFO_VALUE0=isom
|
||||||
|
ID_CLIP_INFO_NAME1=minor_version
|
||||||
|
ID_CLIP_INFO_VALUE1=512
|
||||||
|
ID_CLIP_INFO_NAME2=compatible_brands
|
||||||
|
ID_CLIP_INFO_VALUE2=isomiso2avc1mp41
|
||||||
|
ID_CLIP_INFO_NAME3=encoder
|
||||||
|
ID_CLIP_INFO_VALUE3=Lavf56.25.101
|
||||||
|
ID_CLIP_INFO_N=4
|
||||||
|
ID_FILENAME=m.mp4
|
||||||
|
ID_DEMUXER=lavfpref
|
||||||
|
ID_VIDEO_FORMAT=H264
|
||||||
|
ID_VIDEO_BITRATE=1263573
|
||||||
|
ID_VIDEO_WIDTH=720
|
||||||
|
ID_VIDEO_HEIGHT=404
|
||||||
|
ID_VIDEO_FPS=25.000
|
||||||
|
ID_VIDEO_ASPECT=0.0000
|
||||||
|
ID_AUDIO_FORMAT=MP4A
|
||||||
|
ID_AUDIO_BITRATE=155088
|
||||||
|
ID_AUDIO_RATE=44100
|
||||||
|
ID_AUDIO_NCH=2
|
||||||
|
ID_START_TIME=0.00
|
||||||
|
ID_LENGTH=69.18
|
||||||
|
ID_SEEKABLE=1
|
||||||
|
ID_CHAPTERS=0
|
||||||
|
ID_VIDEO_CODEC=ffh264
|
||||||
|
ID_AUDIO_BITRATE=155082
|
||||||
|
ID_AUDIO_RATE=44100
|
||||||
|
ID_AUDIO_NCH=2
|
||||||
|
ID_AUDIO_CODEC=ffaac
|
||||||
|
ID_EXIT=EOF"""}
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: exchange this with mock
|
||||||
|
class Readlines(object):
|
||||||
|
def __init__(self, key=None):
|
||||||
|
self.data = DATA.get(key, "")
|
||||||
|
|
||||||
|
def readlines(self):
|
||||||
|
return self.data.split('\n')
|
||||||
|
|
||||||
|
|
||||||
|
def mock_popen(command):
|
||||||
|
key = None
|
||||||
|
if 'midentify' in command:
|
||||||
|
key = command.split('"')[1]
|
||||||
|
elif 'jpeg:outdir' in command:
|
||||||
|
# simulate capture for mplayer
|
||||||
|
img_dir = command.split('"')[-2]
|
||||||
|
img = PIL.Image.new('RGB', (320, 200))
|
||||||
|
with open(os.path.join(img_dir, "00000001.jpg"), "wb") as fobj:
|
||||||
|
img.save(fobj)
|
||||||
|
|
||||||
|
return Readlines(key)
|
||||||
|
|
||||||
|
|
||||||
|
# os.popen = mock_popen
|
||||||
|
|
||||||
|
|
||||||
|
class TestVideo(unittest.TestCase):
|
||||||
|
"""test class for retrive midentify script output"""
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_avi(self, popen):
|
||||||
|
"""test mock avi file, should return dict with expected values"""
|
||||||
|
fname = "m.avi"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
avi = Video(fname)
|
||||||
|
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'], 'mpg123')
|
||||||
|
self.assertEqual(avi.tags['video_codec'], 'ffodivx')
|
||||||
|
self.assertEqual(avi.tags['duration'], '00:00:04')
|
||||||
|
self.assertEqual(avi.tags['container'], 'avi')
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_avi2(self, popen):
|
||||||
|
"""test another mock avi file, should return dict with expected
|
||||||
|
values"""
|
||||||
|
fname = "m1.avi"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
avi = Video(fname)
|
||||||
|
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'], 'mpg123')
|
||||||
|
self.assertEqual(avi.tags['video_codec'], 'ffh264')
|
||||||
|
self.assertEqual(avi.tags['duration'], '00:00:04')
|
||||||
|
self.assertEqual(avi.tags['container'], 'avi')
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_mkv(self, popen):
|
||||||
|
"""test mock mkv file, should return dict with expected values"""
|
||||||
|
fname = "m.mkv"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
mkv = Video(fname)
|
||||||
|
self.assertTrue(len(mkv.tags) != 0, "result should have lenght > 0")
|
||||||
|
self.assertEqual(mkv.tags['audio_format'], '8192')
|
||||||
|
self.assertEqual(mkv.tags['width'], 128)
|
||||||
|
self.assertTrue(mkv.tags['audio_no_channels'] in (1, 2))
|
||||||
|
self.assertEqual(mkv.tags['height'], 96)
|
||||||
|
self.assertEqual(mkv.tags['video_format'], 'mp4v')
|
||||||
|
self.assertEqual(mkv.tags['length'], 4)
|
||||||
|
self.assertTrue(mkv.tags['audio_codec'] in ('a52', 'ffac3'))
|
||||||
|
self.assertEqual(mkv.tags['video_codec'], 'ffodivx')
|
||||||
|
self.assertEqual(mkv.tags['duration'], '00:00:04')
|
||||||
|
self.assertTrue(mkv.tags['container'] in ('mkv', 'lavfpref'))
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_mpg(self, popen):
|
||||||
|
"""test mock mpg file, should return dict with expected values"""
|
||||||
|
fname = "m.mpg"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
mpg = Video(fname)
|
||||||
|
self.assertTrue(len(mpg.tags) != 0, "result should have lenght > 0")
|
||||||
|
self.assertFalse('audio_format' in mpg.tags)
|
||||||
|
self.assertEqual(mpg.tags['width'], 128)
|
||||||
|
self.assertFalse('audio_no_channels' in mpg.tags)
|
||||||
|
self.assertEqual(mpg.tags['height'], 96)
|
||||||
|
self.assertEqual(mpg.tags['video_format'], '0x10000001')
|
||||||
|
self.assertFalse('lenght' in mpg.tags)
|
||||||
|
self.assertFalse('audio_codec' in mpg.tags)
|
||||||
|
self.assertEqual(mpg.tags['video_codec'], 'ffmpeg1')
|
||||||
|
self.assertFalse('duration' in mpg.tags)
|
||||||
|
self.assertEqual(mpg.tags['container'], 'mpeges')
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_ogm(self, popen):
|
||||||
|
"""test mock ogm file, should return dict with expected values"""
|
||||||
|
fname = "m.ogm"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
ogm = Video(fname)
|
||||||
|
self.assertTrue(len(ogm.tags) != 0, "result should have lenght > 0")
|
||||||
|
self.assertEqual(ogm.tags['audio_format'], '8192')
|
||||||
|
self.assertEqual(ogm.tags['width'], 160)
|
||||||
|
self.assertTrue(ogm.tags['audio_no_channels'] in (1, 2))
|
||||||
|
self.assertEqual(ogm.tags['height'], 120)
|
||||||
|
self.assertEqual(ogm.tags['video_format'], 'h264')
|
||||||
|
self.assertEqual(ogm.tags['length'], 4)
|
||||||
|
self.assertTrue(ogm.tags['audio_codec'] in ('a52', 'ffac3'))
|
||||||
|
self.assertEqual(ogm.tags['video_codec'], 'ffh264')
|
||||||
|
self.assertEqual(ogm.tags['duration'], '00:00:04')
|
||||||
|
self.assertTrue(ogm.tags['container'] in ('ogg', 'lavfpref'))
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_wmv(self, popen):
|
||||||
|
"""test mock wmv file, should return dict with expected values"""
|
||||||
|
fname = "m.wmv"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
wmv = Video(fname)
|
||||||
|
self.assertTrue(len(wmv.tags) != 0, "result should have lenght > 0")
|
||||||
|
self.assertEqual(wmv.tags['audio_format'], '353')
|
||||||
|
self.assertEqual(wmv.tags['width'], 852)
|
||||||
|
self.assertEqual(wmv.tags['audio_no_channels'], 2)
|
||||||
|
self.assertEqual(wmv.tags['height'], 480)
|
||||||
|
self.assertEqual(wmv.tags['video_format'], 'wmv3')
|
||||||
|
self.assertEqual(wmv.tags['length'], 4656)
|
||||||
|
self.assertEqual(wmv.tags['audio_codec'], 'ffwmav2')
|
||||||
|
self.assertEqual(wmv.tags['video_codec'], 'ffwmv3')
|
||||||
|
self.assertEqual(wmv.tags['duration'], '01:17:32')
|
||||||
|
self.assertEqual(wmv.tags['container'], 'asf')
|
||||||
|
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_mp4(self, popen):
|
||||||
|
"""test mock mp4 file, should return dict with expected values"""
|
||||||
|
fname = "m.mp4"
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
mp4 = Video(fname)
|
||||||
|
self.assertTrue(len(mp4.tags) != 0, "result should have lenght > 0")
|
||||||
|
self.assertEqual(mp4.tags['audio_format'], 'mp4a')
|
||||||
|
self.assertEqual(mp4.tags['width'], 720)
|
||||||
|
self.assertEqual(mp4.tags['audio_no_channels'], 2)
|
||||||
|
self.assertEqual(mp4.tags['height'], 404)
|
||||||
|
self.assertEqual(mp4.tags['video_format'], 'h264')
|
||||||
|
self.assertEqual(mp4.tags['length'], 69)
|
||||||
|
self.assertEqual(mp4.tags['audio_codec'], 'ffaac')
|
||||||
|
self.assertEqual(mp4.tags['video_codec'], 'ffh264')
|
||||||
|
self.assertEqual(mp4.tags['duration'], '00:01:09')
|
||||||
|
self.assertEqual(mp4.tags['container'], 'lavfpref')
|
||||||
|
|
||||||
|
@mock.patch('shutil.move')
|
||||||
|
@mock.patch('pycatalog.video.Image')
|
||||||
|
@mock.patch('os.listdir')
|
||||||
|
@mock.patch('shutil.rmtree')
|
||||||
|
@mock.patch('os.close')
|
||||||
|
@mock.patch('tempfile.mkstemp')
|
||||||
|
@mock.patch('tempfile.mkdtemp')
|
||||||
|
@mock.patch('os.popen')
|
||||||
|
def test_capture(self, popen, mkdtemp, mkstemp, fclose, rmtree, listdir,
|
||||||
|
img, move):
|
||||||
|
"""test capture with some small movie and play a little with tags"""
|
||||||
|
fname = 'm.avi'
|
||||||
|
popen.return_value = io.StringIO(DATA[fname])
|
||||||
|
mkdtemp.return_value = '/tmp'
|
||||||
|
mkstemp.return_value = (10, 'foo.jpg')
|
||||||
|
listdir.return_value = ['a.jpg', 'b.jpg', 'c.jpg', 'd.jpg']
|
||||||
|
|
||||||
|
avi = Video(fname)
|
||||||
|
filename = avi.capture()
|
||||||
|
self.assertIsNotNone(filename)
|
||||||
|
|
||||||
|
for length in (480, 380, 4):
|
||||||
|
avi.tags['length'] = length
|
||||||
|
filename = avi.capture()
|
||||||
|
self.assertTrue(filename is not None)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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()
|
||||||
24
tox.ini
Normal file
24
tox.ini
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[tox]
|
||||||
|
envlist = cleanup,py3,pep8
|
||||||
|
|
||||||
|
usedevelop = True
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
basepython = python3
|
||||||
|
usedevelop=True
|
||||||
|
setenv = COVERAGE_FILE = .coverage
|
||||||
|
commands = py.test --cov=pycatalog --cov-report=term-missing
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
|
||||||
|
[testenv:pep8]
|
||||||
|
usedevelop=True
|
||||||
|
commands = flake8
|
||||||
|
deps = -r{toxinidir}/requirements.txt
|
||||||
|
-r{toxinidir}/test-requirements.txt
|
||||||
|
|
||||||
|
[testenv:cleanup]
|
||||||
|
setenv =
|
||||||
|
COVERAGE_FILE = .coverage
|
||||||
|
deps = coverage
|
||||||
|
commands = coverage erase
|
||||||
Reference in New Issue
Block a user