mirror of
https://github.com/gryf/pygtktalog.git
synced 2025-12-17 19:40:21 +01:00
* Added support for add/remove pictures for any files or directories.
This commit is contained in:
202
img.py
Normal file
202
img.py
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import pygtk; pygtk.require('2.0')
|
||||||
|
import gtk
|
||||||
|
|
||||||
|
import EXIF
|
||||||
|
import Image
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
class Thumbnail(object):
|
||||||
|
def __init__(self, filename=None, x=160, y=120, root='thumbnails', base=''):
|
||||||
|
self.root = root
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.filename = filename
|
||||||
|
self.base = base
|
||||||
|
|
||||||
|
def save(self, image_id):
|
||||||
|
"""Save thumbnail into specific directory structure
|
||||||
|
return full path to the file and exif object or None"""
|
||||||
|
filepath = os.path.join(self.base, self.__get_and_make_path(image_id))
|
||||||
|
f = open(self.filename, 'rb')
|
||||||
|
exif = None
|
||||||
|
returncode = -1
|
||||||
|
try:
|
||||||
|
exif = EXIF.process_file(f)
|
||||||
|
f.close()
|
||||||
|
if exif.has_key('JPEGThumbnail'):
|
||||||
|
thumbnail = exif['JPEGThumbnail']
|
||||||
|
f = open(filepath,'wb')
|
||||||
|
f.write(thumbnail)
|
||||||
|
f.close()
|
||||||
|
if exif.has_key('Image Orientation'):
|
||||||
|
orientation = exif['Image Orientation'].values[0]
|
||||||
|
if orientation > 1:
|
||||||
|
t = "/tmp/thumb%d.jpg" % datetime.now().microsecond
|
||||||
|
im_in = Image.open(filepath)
|
||||||
|
im_out = None
|
||||||
|
if orientation == 8:
|
||||||
|
im_out = im_in.transpose(Image.ROTATE_90)
|
||||||
|
elif orientation == 6:
|
||||||
|
im_out = im_in.transpose(Image.ROTATE_270)
|
||||||
|
if im_out:
|
||||||
|
im_out.save(t, 'JPEG')
|
||||||
|
shutil.move(t, filepath)
|
||||||
|
else:
|
||||||
|
f.close()
|
||||||
|
returncode = 0
|
||||||
|
else:
|
||||||
|
im = self.__scale_image(True)
|
||||||
|
if im:
|
||||||
|
im.save(filepath, "JPEG")
|
||||||
|
returncode = 1
|
||||||
|
except:
|
||||||
|
f.close()
|
||||||
|
im = self.__scale_image(True)
|
||||||
|
if im:
|
||||||
|
im.save(filepath, "JPEG")
|
||||||
|
returncode = 2
|
||||||
|
return filepath, exif, returncode
|
||||||
|
|
||||||
|
# private class functions
|
||||||
|
def __get_and_make_path(self, img_id):
|
||||||
|
"""Make directory structure regards of id
|
||||||
|
and return filepath WITHOUT extension"""
|
||||||
|
try: os.mkdir(self.root)
|
||||||
|
except: pass
|
||||||
|
h = hex(img_id)
|
||||||
|
if len(h[2:])>6:
|
||||||
|
try: os.mkdir(os.path.join(self.root, h[2:4]))
|
||||||
|
except: pass
|
||||||
|
try: os.mkdir(os.path.join(self.root, h[2:4], h[4:6]))
|
||||||
|
except: pass
|
||||||
|
path = os.path.join(self.root, h[2:4], h[4:6], h[6:8])
|
||||||
|
try: os.mkdir(path)
|
||||||
|
except: pass
|
||||||
|
img = "%s.%s" % (h[8:], 'jpg')
|
||||||
|
elif len(h[2:])>4:
|
||||||
|
try: os.mkdir(os.path.join(self.root, h[2:4]))
|
||||||
|
except: pass
|
||||||
|
path = os.path.join(self.root, h[2:4], h[4:6])
|
||||||
|
try: os.mkdir(path)
|
||||||
|
except: pass
|
||||||
|
img = "%s.%s" % (h[6:], 'jpg')
|
||||||
|
elif len(h[2:])>2:
|
||||||
|
path = os.path.join(self.root, h[2:4])
|
||||||
|
try: os.mkdir(path)
|
||||||
|
except: pass
|
||||||
|
img = "%s.%s" %(h[4:], 'jpg')
|
||||||
|
else:
|
||||||
|
path = self.root
|
||||||
|
img = "%s.%s" %(h[2:], 'jpg')
|
||||||
|
return(os.path.join(self.root, img))
|
||||||
|
|
||||||
|
def __scale_image(self, factor=False):
|
||||||
|
"""generate scaled Image object for given file
|
||||||
|
args:
|
||||||
|
factor - if False, adjust height into self.y
|
||||||
|
if True, use self.x for scale portrait pictures height.
|
||||||
|
returns Image object, or False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
im = Image.open(self.filename).convert('RGB')
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
x, y = im.size
|
||||||
|
|
||||||
|
if x > self.x or y > self.y:
|
||||||
|
if x==y:
|
||||||
|
# square
|
||||||
|
imt = im.resize((self.y, self.y), Image.ANTIALIAS)
|
||||||
|
elif x > y:
|
||||||
|
# landscape
|
||||||
|
if int(y/(x/float(self.x))) > self.y:
|
||||||
|
# landscape image: height is non standard
|
||||||
|
self.x1 = int(float(self.y) * self.y / self.x)
|
||||||
|
if float(self.y) * self.y / self.x - self.x1 > 0.49:
|
||||||
|
self.x1 += 1
|
||||||
|
imt = im.resize(((int(x/(y/float(self.y))),self.y)),Image.ANTIALIAS)
|
||||||
|
elif x/self.x==y/self.y:
|
||||||
|
# aspect ratio ok
|
||||||
|
imt = im.resize((self.x, self.y), Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
imt = im.resize((self.x,int(y/(x/float(self.x)))), 1)
|
||||||
|
else:
|
||||||
|
# portrait
|
||||||
|
if factor:
|
||||||
|
if y>self.x:
|
||||||
|
imt = im.resize(((int(x/(y/float(self.x))),self.x)),Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
imt = im
|
||||||
|
else:
|
||||||
|
self.x1 = int(float(self.y) * self.y / self.x)
|
||||||
|
if float(self.y) * self.y / self.x - self.x1 > 0.49:
|
||||||
|
self.x1 += 1
|
||||||
|
|
||||||
|
if x/self.x1==y/self.y:
|
||||||
|
# aspect ratio ok
|
||||||
|
imt = im.resize((self.x1,self.y),Image.ANTIALIAS)
|
||||||
|
else:
|
||||||
|
imt = im.resize(((int(x/(y/float(self.y))),self.y)),Image.ANTIALIAS)
|
||||||
|
return imt
|
||||||
|
else:
|
||||||
|
return im
|
||||||
|
|
||||||
|
class Image_Example(object):
|
||||||
|
|
||||||
|
def pressButton(self, widget, data=None):
|
||||||
|
print "Pressed"
|
||||||
|
|
||||||
|
def delete_event(self, widget, event, data=None):
|
||||||
|
print "delete event occured"
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def destroy(self, widget, data=None):
|
||||||
|
gtk.main_quit()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||||||
|
self.window.connect("delete_event", self.delete_event)
|
||||||
|
self.window.connect("destroy", self.destroy)
|
||||||
|
self.window.set_border_width(10)
|
||||||
|
|
||||||
|
self.button = gtk.Button()
|
||||||
|
self.button.connect("clicked", self.pressButton, None)
|
||||||
|
self.button.connect_object("clicked", gtk.Widget.destroy, self.window)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
root, dirs, files = os.walk('/home/gryf/t/t').next()
|
||||||
|
count = 0
|
||||||
|
for i in files:
|
||||||
|
count+=1
|
||||||
|
path, exif, success = Thumbnail(os.path.join(root, i), base='/home/gryf/t/t').save(count)
|
||||||
|
if exif:
|
||||||
|
print path, len(exif), success
|
||||||
|
if success != -1:
|
||||||
|
p = path
|
||||||
|
|
||||||
|
self.image = gtk.Image()
|
||||||
|
self.image.set_from_file(os.path.join(root, path))
|
||||||
|
self.image.show()
|
||||||
|
|
||||||
|
pb = self.image.get_pixbuf()
|
||||||
|
print pb.get_width(), pb.get_height()
|
||||||
|
|
||||||
|
self.button.add(self.image)
|
||||||
|
self.window.add(self.button)
|
||||||
|
self.button.show()
|
||||||
|
self.window.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def main(self):
|
||||||
|
gtk.main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
Image_Example().main()
|
||||||
|
|
||||||
@@ -395,4 +395,86 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</child>
|
</child>
|
||||||
</widget>
|
</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>
|
||||||
</glade-interface>
|
</glade-interface>
|
||||||
|
|||||||
@@ -650,98 +650,24 @@
|
|||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<widget class="GtkVPaned" id="vpaned3">
|
<widget class="GtkScrolledWindow" id="scrolledwindow4">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">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="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>
|
<child>
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow4">
|
<widget class="GtkTextView" id="description">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">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="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="editable">False</property>
|
||||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
<property name="wrap_mode">GTK_WRAP_WORD</property>
|
||||||
<property name="shadow_type">GTK_SHADOW_IN</property>
|
<property name="left_margin">2</property>
|
||||||
<child>
|
<property name="right_margin">2</property>
|
||||||
<widget class="GtkTextView" id="description">
|
<property name="cursor_visible">False</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="editable">False</property>
|
|
||||||
<property name="wrap_mode">GTK_WRAP_WORD</property>
|
|
||||||
<property name="left_margin">2</property>
|
|
||||||
<property name="right_margin">2</property>
|
|
||||||
<property name="cursor_visible">False</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
</widget>
|
||||||
<packing>
|
|
||||||
<property name="resize">True</property>
|
|
||||||
<property name="shrink">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkVBox" id="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>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="exifinfo">
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
|
||||||
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkTreeView" id="exif_tree">
|
|
||||||
<property name="visible">True</property>
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<property name="headers_clickable">True</property>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkHPaned" id="movieinfo">
|
|
||||||
<property name="can_focus">True</property>
|
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
|
||||||
<child>
|
|
||||||
<widget class="GtkScrolledWindow" id="scrolledwindow5">
|
|
||||||
<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>
|
|
||||||
</widget>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="resize">False</property>
|
|
||||||
<property name="shrink">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="position">1</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
|
||||||
<child>
|
|
||||||
<placeholder/>
|
|
||||||
</child>
|
|
||||||
</widget>
|
|
||||||
<packing>
|
|
||||||
<property name="resize">True</property>
|
|
||||||
<property name="shrink">True</property>
|
|
||||||
</packing>
|
|
||||||
</child>
|
</child>
|
||||||
</widget>
|
</widget>
|
||||||
<packing>
|
<packing>
|
||||||
@@ -761,6 +687,113 @@
|
|||||||
<property name="tab_fill">False</property>
|
<property name="tab_fill">False</property>
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkScrolledWindow" id="img_container">
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
|
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
|
<property name="shadow_type">GTK_SHADOW_IN</property>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkIconView" id="images">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="tooltip" translatable="yes">Double click to open image</property>
|
||||||
|
<signal name="button_press_event" handler="on_images_button_press_event"/>
|
||||||
|
<signal name="item_activated" handler="on_images_item_activated"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkLabel" id="label3">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="label" translatable="yes">Images</property>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="type">tab</property>
|
||||||
|
<property name="position">1</property>
|
||||||
|
<property name="tab_fill">False</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkVBox" id="vbox4">
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkHPaned" id="movieinfo">
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkScrolledWindow" id="scrolledwindow5">
|
||||||
|
<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>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="resize">False</property>
|
||||||
|
<property name="shrink">True</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkScrolledWindow" id="exifinfo">
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="hscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
|
<property name="vscrollbar_policy">GTK_POLICY_AUTOMATIC</property>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkTreeView" id="exif_tree">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="can_focus">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="headers_clickable">True</property>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="position">1</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<placeholder/>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="position">2</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkLabel" id="label4">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="label" translatable="yes">page 3</property>
|
||||||
|
</widget>
|
||||||
|
<packing>
|
||||||
|
<property name="type">tab</property>
|
||||||
|
<property name="position">2</property>
|
||||||
|
<property name="tab_fill">False</property>
|
||||||
|
</packing>
|
||||||
|
</child>
|
||||||
</widget>
|
</widget>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="resize">True</property>
|
<property name="resize">True</property>
|
||||||
@@ -854,6 +887,7 @@
|
|||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="label" translatable="yes">_Rename</property>
|
<property name="label" translatable="yes">_Rename</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_rename1_activate"/>
|
||||||
</widget>
|
</widget>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
@@ -890,7 +924,7 @@
|
|||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
<child>
|
<child>
|
||||||
<widget class="GtkMenuItem" id="add_tag1">
|
<widget class="GtkMenuItem" id="add_tag1">
|
||||||
<property name="visible">True</property>
|
<property name="sensitive">False</property>
|
||||||
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</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="label" translatable="yes">_Add tag</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
@@ -898,11 +932,78 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<widget class="GtkMenuItem" id="add_thumbnail1">
|
<widget class="GtkMenuItem" id="add_image1">
|
||||||
<property name="visible">True</property>
|
<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="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="label" translatable="yes">Add _Images</property>
|
||||||
<property name="use_underline">True</property>
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_add_image1_activate"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkSeparatorMenuItem" id="separator7">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkMenuItem" id="edit2">
|
||||||
|
<property name="sensitive">False</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="label" translatable="yes">_Edit</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_edit2_activate"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkMenuItem" id="delete3">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="label" translatable="yes">_Delete</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_delete3_activate"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkMenuItem" id="rename2">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="label" translatable="yes">_Rename</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_rename2_activate"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<widget class="GtkWindow" id="image_show">
|
||||||
|
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
|
||||||
|
<property name="title" translatable="yes">pyGTKtalog - Image</property>
|
||||||
|
<property name="destroy_with_parent">True</property>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkImage" id="img">
|
||||||
|
<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="stock">gtk-missing-image</property>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
</widget>
|
||||||
|
<widget class="GtkMenu" id="img_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="img_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 image</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_img_delete_activate"/>
|
||||||
|
</widget>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<widget class="GtkMenuItem" id="img_add">
|
||||||
|
<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 images</property>
|
||||||
|
<property name="use_underline">True</property>
|
||||||
|
<signal name="activate" handler="on_img_add_activate"/>
|
||||||
</widget>
|
</widget>
|
||||||
</child>
|
</child>
|
||||||
</widget>
|
</widget>
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ from models.m_config import ConfigModel
|
|||||||
|
|
||||||
import views.v_dialogs as Dialogs
|
import views.v_dialogs as Dialogs
|
||||||
|
|
||||||
|
from views.v_image import ImageView
|
||||||
|
|
||||||
import gtk
|
import gtk
|
||||||
import pango
|
import pango
|
||||||
|
|
||||||
@@ -123,18 +125,57 @@ class MainController(Controller):
|
|||||||
|
|
||||||
#########################################################################
|
#########################################################################
|
||||||
# Connect signals from GUI, like menu objects, toolbar buttons and so on.
|
# Connect signals from GUI, like menu objects, toolbar buttons and so on.
|
||||||
|
def on_images_item_activated(self, iconview, path):
|
||||||
|
model = iconview.get_model()
|
||||||
|
iter = model.get_iter(path)
|
||||||
|
id = model.get_value(iter, 0)
|
||||||
|
ImageView(self.model.get_image_path(id))
|
||||||
|
|
||||||
def on_rename1_activate(self, widget):
|
def on_rename1_activate(self, widget):
|
||||||
model, iter = self.view['discs'].get_selection().get_selected()
|
model, iter = self.view['discs'].get_selection().get_selected()
|
||||||
label_old = model.get_value(iter, 1)
|
name = model.get_value(iter, 1)
|
||||||
id = model.get_value(iter, 0)
|
id = model.get_value(iter, 0)
|
||||||
label = Dialogs.InputDiskLabel(label_old).run()
|
new_name = Dialogs.InputNewName(name).run()
|
||||||
|
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "c_main.py: on_rename1_activate(): label:", label
|
print "c_main.py: on_rename1_activate(): label:", new_name
|
||||||
if label != None and label !=label_old:
|
|
||||||
self.model.set_label(id, label)
|
if new_name != None and new_name != name:
|
||||||
self.model.unsaved_project = True
|
self.model.rename(id, new_name)
|
||||||
self.__set_title(filepath=self.model.filename, modified=True)
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
|
|
||||||
|
def on_rename2_activate(self, widget):
|
||||||
|
try:
|
||||||
|
selection = self.view['files'].get_selection()
|
||||||
|
model, list_of_paths = selection.get_selected_rows()
|
||||||
|
except TypeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if len(list_of_paths) != 1:
|
||||||
|
return
|
||||||
|
|
||||||
|
fid = model.get_value(model.get_iter(list_of_paths[0]),0)
|
||||||
|
name = model.get_value(model.get_iter(list_of_paths[0]),1)
|
||||||
|
|
||||||
|
new_name = Dialogs.InputNewName(name).run()
|
||||||
|
if __debug__:
|
||||||
|
print "c_main.py: on_rename1_activate(): label:", new_name
|
||||||
|
|
||||||
|
if new_name != None and new_name != name:
|
||||||
|
self.model.rename(fid, new_name)
|
||||||
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
path, column = self.view['discs'].get_cursor()
|
||||||
|
print path
|
||||||
|
iter = model.get_iter(path)
|
||||||
|
self.model.get_root_entries(model.get_value(iter, 0))
|
||||||
|
except TypeError:
|
||||||
|
print "zuo"
|
||||||
|
self.model.get_root_entries(1)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
def on_tag_cloud_textview_motion_notify_event(self, widget):
|
def on_tag_cloud_textview_motion_notify_event(self, widget):
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "c_main.py: on_tag_cloud_textview_motion_notify_event():"
|
print "c_main.py: on_tag_cloud_textview_motion_notify_event():"
|
||||||
@@ -247,6 +288,41 @@ class MainController(Controller):
|
|||||||
treeview.expand_row(path,False)
|
treeview.expand_row(path,False)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def on_images_button_press_event(self, iconview, event):
|
||||||
|
try:
|
||||||
|
path_and_cell = iconview.get_item_at_pos(int(event.x), int(event.y))
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if event.button == 3: # Right mouse button. Show context menu.
|
||||||
|
try:
|
||||||
|
iconview.select_path(path_and_cell[0])
|
||||||
|
except TypeError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.__popup_menu(event, 'img_popup')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def on_img_delete_activate(self, menu_item):
|
||||||
|
list_of_paths = self.view['images'].get_selected_items()
|
||||||
|
model = self.view['images'].get_model()
|
||||||
|
iter = model.get_iter(list_of_paths[0])
|
||||||
|
id = model.get_value(iter, 0)
|
||||||
|
self.model.delete_image(id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
path, column = self.view['files'].get_cursor()
|
||||||
|
model = self.view['files'].get_model()
|
||||||
|
iter = model.get_iter(path)
|
||||||
|
id = model.get_value(iter, 0)
|
||||||
|
self.__get_item_info(id)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def on_img_add_activate(self, menu_item):
|
||||||
|
self.on_add_image1_activate(menu_item)
|
||||||
|
|
||||||
def on_discs_button_press_event(self, treeview, event):
|
def on_discs_button_press_event(self, treeview, event):
|
||||||
try:
|
try:
|
||||||
path, column, x, y = treeview.get_path_at_pos(int(event.x),
|
path, column, x, y = treeview.get_path_at_pos(int(event.x),
|
||||||
@@ -271,11 +347,9 @@ class MainController(Controller):
|
|||||||
if self.model.discs_tree.get_value(iter, 3) == 1:
|
if self.model.discs_tree.get_value(iter, 3) == 1:
|
||||||
# if ancestor is 'root', then activate "update" menu item
|
# if ancestor is 'root', then activate "update" menu item
|
||||||
self.view['update1'].set_sensitive(True)
|
self.view['update1'].set_sensitive(True)
|
||||||
self.view['rename1'].set_sensitive(True)
|
|
||||||
else:
|
else:
|
||||||
self.view['update1'].set_sensitive(False)
|
self.view['update1'].set_sensitive(False)
|
||||||
self.view['rename1'].set_sensitive(False)
|
self.__popup_menu(event)
|
||||||
self.__popup_discs_menu(event)
|
|
||||||
|
|
||||||
# elif event.button == 1: # Left click
|
# elif event.button == 1: # Left click
|
||||||
# """Show files on right treeview, after clicking the left disc treeview."""
|
# """Show files on right treeview, after clicking the left disc treeview."""
|
||||||
@@ -315,7 +389,16 @@ class MainController(Controller):
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
list_of_paths = []
|
list_of_paths = []
|
||||||
|
|
||||||
self.__popup_files_menu(event)
|
if len(list_of_paths) == 0:
|
||||||
|
selection.select_path(path[0])
|
||||||
|
|
||||||
|
if len(list_of_paths) > 1:
|
||||||
|
self.view['add_image1'].set_sensitive(False)
|
||||||
|
self.view['rename2'].set_sensitive(False)
|
||||||
|
else:
|
||||||
|
self.view['add_image1'].set_sensitive(True)
|
||||||
|
self.view['rename2'].set_sensitive(True)
|
||||||
|
self.__popup_menu(event, 'files_popup')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def on_files_cursor_changed(self, treeview):
|
def on_files_cursor_changed(self, treeview):
|
||||||
@@ -323,18 +406,18 @@ class MainController(Controller):
|
|||||||
model, paths = treeview.get_selection().get_selected_rows()
|
model, paths = treeview.get_selection().get_selected_rows()
|
||||||
try:
|
try:
|
||||||
itera = model.get_iter(paths[0])
|
itera = model.get_iter(paths[0])
|
||||||
if model.get_value(itera,4) == 1:
|
#if model.get_value(itera,4) == 1:
|
||||||
#directory, do nothin', just turn off view
|
# #directory, do nothin', just turn off view
|
||||||
'''self.view['details'].hide()
|
# '''self.view['details'].hide()
|
||||||
buf = self.view['details'].get_buffer()
|
# buf = self.view['details'].get_buffer()
|
||||||
buf.set_text('')
|
# buf.set_text('')
|
||||||
self.view['details'].set_buffer(buf)'''
|
# self.view['details'].set_buffer(buf)'''
|
||||||
else:
|
#else:
|
||||||
#file, show what you got.
|
#file, show what you got.
|
||||||
#self.details.get_top_widget()
|
#self.details.get_top_widget()
|
||||||
iter = model.get_iter(treeview.get_cursor()[0])
|
iter = model.get_iter(treeview.get_cursor()[0])
|
||||||
selected_item = self.model.files_list.get_value(iter, 0)
|
selected_item = self.model.files_list.get_value(iter, 0)
|
||||||
self.__get_item_info(selected_item)
|
self.__get_item_info(selected_item)
|
||||||
except:
|
except:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "c_main.py: on_files_cursor_changed() insufficient iterator"
|
print "c_main.py: on_files_cursor_changed() insufficient iterator"
|
||||||
@@ -409,39 +492,130 @@ class MainController(Controller):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def on_add_tag1_activate(self, menu_item):
|
def on_add_tag1_activate(self, menu_item):
|
||||||
print self.view['discs'].get_cursor()
|
print self.view['files'].get_cursor()
|
||||||
|
|
||||||
|
def on_add_image1_activate(self, menu_item):
|
||||||
|
images = Dialogs.LoadImageFile().run()
|
||||||
|
if not images:
|
||||||
|
return
|
||||||
|
for image in images:
|
||||||
|
try:
|
||||||
|
selection = self.view['files'].get_selection()
|
||||||
|
model, list_of_paths = selection.get_selected_rows()
|
||||||
|
id = model.get_value(model.get_iter(list_of_paths[0]),0)
|
||||||
|
except:
|
||||||
|
try:
|
||||||
|
path, column = self.view['files'].get_cursor()
|
||||||
|
model = self.view['files'].get_model()
|
||||||
|
iter = model.get_iter(path)
|
||||||
|
id = model.get_value(iter, 0)
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
self.model.add_image(image, id)
|
||||||
|
self.__get_item_info(id)
|
||||||
|
return
|
||||||
|
|
||||||
def on_update1_activate(self, menu_item):
|
def on_update1_activate(self, menu_item):
|
||||||
"""Update disc under cursor position"""
|
"""Update disc under cursor position"""
|
||||||
|
path, column = self.view['discs'].get_cursor()
|
||||||
|
model = self.view['discs'].get_model()
|
||||||
|
|
||||||
# determine origin label and filepath
|
# determine origin label and filepath
|
||||||
path = self.view['discs'].get_cursor()
|
|
||||||
filepath, label = self.model.get_label_and_filepath(path)
|
filepath, label = self.model.get_label_and_filepath(path)
|
||||||
|
|
||||||
|
fid = model.get_value(model.get_iter(path), 0)
|
||||||
|
|
||||||
if self.model.get_source(path) == self.model.CD:
|
if self.model.get_source(path) == self.model.CD:
|
||||||
if self.__add_cd(label):
|
self.__add_cd(label, fid)
|
||||||
self.model.delete(self.model.discs_tree.get_iter(path[0],0))
|
|
||||||
pass
|
|
||||||
elif self.model.get_source(path) == self.model.DR:
|
elif self.model.get_source(path) == self.model.DR:
|
||||||
if self.__add_directory(filepath, label):
|
self.__add_directory(filepath, label, fid)
|
||||||
self.model.delete(self.model.discs_tree.get_iter(path[0]))
|
|
||||||
pass
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def on_delete2_activate(self, menu_item):
|
def on_delete2_activate(self, menu_item):
|
||||||
model = self.view['discs'].get_model()
|
|
||||||
try:
|
try:
|
||||||
path, column = self.view['discs'].get_cursor()
|
#path, column = self.view['discs'].get_cursor()
|
||||||
selected_iter = self.model.discs_tree.get_iter(path)
|
#selected_iter = model.get_iter(path)
|
||||||
|
selection = self.view['discs'].get_selection()
|
||||||
|
model, selected_iter = selection.get_selected()
|
||||||
except:
|
except:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.model.config.confd['delwarn']:
|
if self.model.config.confd['delwarn']:
|
||||||
name = self.model.discs_tree.get_value(selected_iter,1)
|
name = model.get_value(selected_iter, 1)
|
||||||
obj = Dialogs.Qst('Delete %s' % name, 'Delete %s?' % name,
|
obj = Dialogs.Qst('Delete %s' % name, 'Delete %s?' % name,
|
||||||
'Object will be permanently removed.')
|
'Object will be permanently removed.')
|
||||||
if not obj.run():
|
if not obj.run():
|
||||||
return
|
return
|
||||||
self.model.delete(selected_iter)
|
|
||||||
|
# remove from model
|
||||||
|
path = model.get_path(selected_iter)
|
||||||
|
current_id = self.model.discs_tree.get_value(selected_iter, 0)
|
||||||
|
model.remove(selected_iter)
|
||||||
|
selection.select_path(path)
|
||||||
|
|
||||||
|
if not selection.path_is_selected(path):
|
||||||
|
row = path[0]-1
|
||||||
|
if row >= 0:
|
||||||
|
selection.select_path((row,))
|
||||||
|
path = (row, )
|
||||||
|
|
||||||
|
# delete from db
|
||||||
|
print current_id
|
||||||
|
self.model.delete(current_id)
|
||||||
|
|
||||||
|
# refresh files treeview
|
||||||
|
current_id = model.get_value(model.get_iter(path), 0)
|
||||||
|
self.model.get_root_entries(current_id)
|
||||||
|
|
||||||
|
# refresh file info view
|
||||||
|
self.__get_item_info(current_id)
|
||||||
|
|
||||||
|
self.model.unsaved_project = True
|
||||||
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_delete3_activate(self, menu_item):
|
||||||
|
dmodel = self.model.discs_tree
|
||||||
|
try:
|
||||||
|
selection = self.view['files'].get_selection()
|
||||||
|
model, list_of_paths = selection.get_selected_rows()
|
||||||
|
except TypeError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.model.config.confd['delwarn']:
|
||||||
|
obj = Dialogs.Qst('Delete elements', 'Delete items?',
|
||||||
|
'Items will be permanently removed.')
|
||||||
|
if not obj.run():
|
||||||
|
return
|
||||||
|
|
||||||
|
def foreach_disctree(zmodel, zpath, ziter, d):
|
||||||
|
if d[0] == zmodel.get_value(ziter, 0):
|
||||||
|
d[1].append(zpath)
|
||||||
|
return False
|
||||||
|
|
||||||
|
for p in list_of_paths:
|
||||||
|
val = model.get_value(model.get_iter(p), 0)
|
||||||
|
if model.get_value(model.get_iter(p), 4) == self.model.DIR:
|
||||||
|
# remove from disctree model aswell
|
||||||
|
dpath = []
|
||||||
|
dmodel.foreach(foreach_disctree, (val, dpath))
|
||||||
|
for dp in dpath:
|
||||||
|
dmodel.remove(dmodel.get_iter(dp))
|
||||||
|
|
||||||
|
# delete from db
|
||||||
|
self.model.delete(val)
|
||||||
|
|
||||||
|
try:
|
||||||
|
selection = self.view['discs'].get_selection()
|
||||||
|
model, list_of_paths = selection.get_selected_rows()
|
||||||
|
if not list_of_paths:
|
||||||
|
list_of_paths = [1]
|
||||||
|
self.model.get_root_entries(model.get_value(model.get_iter(list_of_paths[0]),0))
|
||||||
|
except TypeError:
|
||||||
|
return
|
||||||
|
|
||||||
self.model.unsaved_project = True
|
self.model.unsaved_project = True
|
||||||
self.__set_title(filepath=self.model.filename, modified=True)
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
return
|
return
|
||||||
@@ -505,8 +679,7 @@ class MainController(Controller):
|
|||||||
"""Open catalog file"""
|
"""Open catalog file"""
|
||||||
confirm = self.model.config.confd['confirmabandon']
|
confirm = self.model.config.confd['confirmabandon']
|
||||||
if self.model.unsaved_project and confirm:
|
if self.model.unsaved_project and confirm:
|
||||||
obj = Dialogs.Qst('Unsaved data - pyGTKtalog','There is not saved \
|
obj = Dialogs.Qst('Unsaved data - pyGTKtalog','There is not saved database','Pressing "Ok" will abandon catalog.')
|
||||||
database','Pressing "Ok" will abandon catalog.')
|
|
||||||
if not obj.run():
|
if not obj.run():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -543,7 +716,7 @@ class MainController(Controller):
|
|||||||
Dialogs.Err("Error writing file - pyGTKtalog","Cannot write \
|
Dialogs.Err("Error writing file - pyGTKtalog","Cannot write \
|
||||||
file %s." % path, "%s" % err)
|
file %s." % path, "%s" % err)
|
||||||
|
|
||||||
def __add_cd(self, label=None):
|
def __add_cd(self, label=None, current_id=None):
|
||||||
"""Add directory structure from cd/dvd disc"""
|
"""Add directory structure from cd/dvd disc"""
|
||||||
mount = deviceHelper.volmount(self.model.config.confd['cd'])
|
mount = deviceHelper.volmount(self.model.config.confd['cd'])
|
||||||
if mount == 'ok':
|
if mount == 'ok':
|
||||||
@@ -555,7 +728,8 @@ class MainController(Controller):
|
|||||||
for widget in self.widgets_all:
|
for widget in self.widgets_all:
|
||||||
self.view[widget].set_sensitive(False)
|
self.view[widget].set_sensitive(False)
|
||||||
self.model.source = self.model.CD
|
self.model.source = self.model.CD
|
||||||
self.model.scan(self.model.config.confd['cd'],label)
|
self.model.scan(self.model.config.confd['cd'], label,
|
||||||
|
current_id)
|
||||||
self.model.unsaved_project = True
|
self.model.unsaved_project = True
|
||||||
self.__set_title(filepath=self.model.filename, modified=True)
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
return True
|
return True
|
||||||
@@ -566,7 +740,7 @@ class MainController(Controller):
|
|||||||
"Last mount message:\n%s" % mount)
|
"Last mount message:\n%s" % mount)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def __add_directory(self, path=None, label=None):
|
def __add_directory(self, path=None, label=None, current_id=None):
|
||||||
if not label or not path:
|
if not label or not path:
|
||||||
res = Dialogs.PointDirectoryToAdd().run()
|
res = Dialogs.PointDirectoryToAdd().run()
|
||||||
if res !=(None,None):
|
if res !=(None,None):
|
||||||
@@ -577,7 +751,7 @@ class MainController(Controller):
|
|||||||
|
|
||||||
self.scan_cd = False
|
self.scan_cd = False
|
||||||
self.model.source = self.model.DR
|
self.model.source = self.model.DR
|
||||||
self.model.scan(path, label)
|
self.model.scan(path, label, current_id)
|
||||||
self.model.unsaved_project = True
|
self.model.unsaved_project = True
|
||||||
self.__set_title(filepath=self.model.filename, modified=True)
|
self.__set_title(filepath=self.model.filename, modified=True)
|
||||||
return True
|
return True
|
||||||
@@ -639,6 +813,12 @@ class MainController(Controller):
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def __setup_iconview(self):
|
||||||
|
"""Setup IconView images widget."""
|
||||||
|
self.view['images'].set_model(self.model.images_store)
|
||||||
|
self.view['images'].set_pixbuf_column(1)
|
||||||
|
return
|
||||||
|
|
||||||
def __setup_tags_treeview(self):
|
def __setup_tags_treeview(self):
|
||||||
"""Setup TreeView discs widget as tree."""
|
"""Setup TreeView discs widget as tree."""
|
||||||
self.view['tags'].set_model(self.model.tagsTree)
|
self.view['tags'].set_model(self.model.tagsTree)
|
||||||
@@ -687,10 +867,10 @@ class MainController(Controller):
|
|||||||
c.set_resizable(True)
|
c.set_resizable(True)
|
||||||
self.view['files'].append_column(c)
|
self.view['files'].append_column(c)
|
||||||
|
|
||||||
c = gtk.TreeViewColumn('Category',gtk.CellRendererText(), text=5)
|
#c = gtk.TreeViewColumn('Category',gtk.CellRendererText(), text=5)
|
||||||
c.set_sort_column_id(5)
|
#c.set_sort_column_id(5)
|
||||||
c.set_resizable(True)
|
#c.set_resizable(True)
|
||||||
self.view['files'].append_column(c)
|
#self.view['files'].append_column(c)
|
||||||
|
|
||||||
# registration of treeview signals:
|
# registration of treeview signals:
|
||||||
|
|
||||||
@@ -742,16 +922,10 @@ class MainController(Controller):
|
|||||||
self.model.config.save()
|
self.model.config.save()
|
||||||
return
|
return
|
||||||
|
|
||||||
def __popup_discs_menu(self, event):
|
def __popup_menu(self, event, menu='discs_popup'):
|
||||||
self.view['discs_popup'].popup(None, None, None, event.button,
|
self.view[menu].popup(None, None, None, event.button,
|
||||||
event.time)
|
event.time)
|
||||||
self.view['discs_popup'].show_all()
|
self.view[menu].show_all()
|
||||||
return
|
|
||||||
|
|
||||||
def __popup_files_menu(self, event):
|
|
||||||
self.view['files_popup'].popup(None, None, None, event.button,
|
|
||||||
event.time)
|
|
||||||
self.view['files_popup'].show_all()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __generate_recent_menu(self):
|
def __generate_recent_menu(self):
|
||||||
@@ -777,11 +951,17 @@ class MainController(Controller):
|
|||||||
tag.set_property('weight', pango.WEIGHT_BOLD)
|
tag.set_property('weight', pango.WEIGHT_BOLD)
|
||||||
buf.insert_with_tags(buf.get_end_iter(), "\nDetails:\n", tag)
|
buf.insert_with_tags(buf.get_end_iter(), "\nDetails:\n", tag)
|
||||||
buf.insert(buf.get_end_iter(), set['description'])
|
buf.insert(buf.get_end_iter(), set['description'])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
buf.set_text('')
|
buf.set_text('')
|
||||||
|
|
||||||
self.view['description'].set_buffer(buf)
|
self.view['description'].set_buffer(buf)
|
||||||
|
|
||||||
|
if set.has_key('images'):
|
||||||
|
self.__setup_iconview()
|
||||||
|
self.view['img_container'].show()
|
||||||
|
else:
|
||||||
|
self.view['img_container'].hide()
|
||||||
|
|
||||||
if set.has_key('thumbnail'):
|
if set.has_key('thumbnail'):
|
||||||
self.view['thumb'].set_from_file(set['thumbnail'])
|
self.view['thumb'].set_from_file(set['thumbnail'])
|
||||||
self.view['thumb'].show()
|
self.view['thumb'].show()
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ except ImportError:
|
|||||||
from m_config import ConfigModel
|
from m_config import ConfigModel
|
||||||
from m_details import DetailsModel
|
from m_details import DetailsModel
|
||||||
from utils.thumbnail import Thumbnail
|
from utils.thumbnail import Thumbnail
|
||||||
|
from utils.img import Img
|
||||||
|
|
||||||
class MainModel(ModelMT):
|
class MainModel(ModelMT):
|
||||||
"""Create, load, save, manipulate db file which is container for data"""
|
"""Create, load, save, manipulate db file which is container for data"""
|
||||||
@@ -79,7 +80,7 @@ class MainModel(ModelMT):
|
|||||||
self.config.load()
|
self.config.load()
|
||||||
self.details = DetailsModel()
|
self.details = DetailsModel()
|
||||||
|
|
||||||
# Directory tree: id, nazwa, ikonka, typ
|
# Directory tree: id, name, icon, type
|
||||||
self.discs_tree = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING,
|
self.discs_tree = gtk.TreeStore(gobject.TYPE_INT, gobject.TYPE_STRING,
|
||||||
str, gobject.TYPE_INT)
|
str, gobject.TYPE_INT)
|
||||||
# File list of selected directory: child_id(?), filename, size,
|
# File list of selected directory: child_id(?), filename, size,
|
||||||
@@ -88,6 +89,8 @@ class MainModel(ModelMT):
|
|||||||
gobject.TYPE_UINT64,
|
gobject.TYPE_UINT64,
|
||||||
gobject.TYPE_STRING, gobject.TYPE_INT,
|
gobject.TYPE_STRING, gobject.TYPE_INT,
|
||||||
gobject.TYPE_STRING, str)
|
gobject.TYPE_STRING, str)
|
||||||
|
# iconview store - image id, pixbuffer
|
||||||
|
self.images_store = gtk.ListStore(gobject.TYPE_INT, gtk.gdk.Pixbuf)
|
||||||
|
|
||||||
# tag cloud array element is a dict with 4 keys:
|
# tag cloud array element is a dict with 4 keys:
|
||||||
# elem = {'id': str(id), 'name': tagname, 'size': size, 'color': color}
|
# elem = {'id': str(id), 'name': tagname, 'size': size, 'color': color}
|
||||||
@@ -108,6 +111,24 @@ class MainModel(ModelMT):
|
|||||||
{'id': str(10), 'name': "windows", 'size': 18, 'color': '#333'},
|
{'id': str(10), 'name': "windows", 'size': 18, 'color': '#333'},
|
||||||
]'''
|
]'''
|
||||||
return
|
return
|
||||||
|
def add_image(self, image, id):
|
||||||
|
sql = """insert into images(file_id, thumbnail, filename)
|
||||||
|
values(?, null, null)"""
|
||||||
|
self.db_cursor.execute(sql, (id,))
|
||||||
|
self.db_connection.commit()
|
||||||
|
|
||||||
|
sql = """select id from images where thumbnail is null and filename is null and file_id=?"""
|
||||||
|
self.db_cursor.execute(sql, (id,))
|
||||||
|
res = self.db_cursor.fetchone()
|
||||||
|
if res:
|
||||||
|
tp, ip, rc = Img(image, self.internal_dirname).save(res[0])
|
||||||
|
if rc != -1:
|
||||||
|
sql = """update images set filename=?, thumbnail=? where id=?"""
|
||||||
|
self.db_cursor.execute(sql,
|
||||||
|
(ip.split(self.internal_dirname)[1][1:],
|
||||||
|
tp.split(self.internal_dirname)[1][1:],
|
||||||
|
res[0]))
|
||||||
|
self.db_connection.commit()
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.__close_db_connection()
|
self.__close_db_connection()
|
||||||
@@ -201,7 +222,7 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def scan(self, path, label):
|
def scan(self, path, label, currentid):
|
||||||
"""scan files in separated thread"""
|
"""scan files in separated thread"""
|
||||||
|
|
||||||
# flush buffer to release db lock.
|
# flush buffer to release db lock.
|
||||||
@@ -209,6 +230,7 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.label = label
|
self.label = label
|
||||||
|
self.currentid = currentid
|
||||||
|
|
||||||
if self.busy:
|
if self.busy:
|
||||||
return
|
return
|
||||||
@@ -216,17 +238,21 @@ class MainModel(ModelMT):
|
|||||||
self.thread.start()
|
self.thread.start()
|
||||||
return
|
return
|
||||||
|
|
||||||
def set_label(self, id, label=None):
|
def rename(self, id, new_name=None):
|
||||||
if label:
|
if new_name:
|
||||||
self.db_cursor.execute("update files set filename=? \
|
self.db_cursor.execute("update files set filename=? \
|
||||||
where id=? and parent_id=1", (label, id))
|
where id=?", (new_name, id))
|
||||||
self.db_connection.commit()
|
self.db_connection.commit()
|
||||||
self.__fetch_db_into_treestore()
|
self.__fetch_db_into_treestore()
|
||||||
|
self.unsaved_project = True
|
||||||
else:
|
else:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "m_main.py: set_label(): no label defined"
|
print "m_main.py: rename(): no label defined"
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def refresh_discs_tree(self):
|
||||||
|
self.__fetch_db_into_treestore()
|
||||||
|
|
||||||
def get_root_entries(self, id=None):
|
def get_root_entries(self, id=None):
|
||||||
"""Get all children down from sepcified root"""
|
"""Get all children down from sepcified root"""
|
||||||
try:
|
try:
|
||||||
@@ -281,11 +307,12 @@ class MainModel(ModelMT):
|
|||||||
def get_file_info(self, id):
|
def get_file_info(self, id):
|
||||||
"""get file info from database"""
|
"""get file info from database"""
|
||||||
retval = {}
|
retval = {}
|
||||||
self.db_cursor.execute("SELECT f.filename, f.date, f.size, f.type, \
|
sql = """SELECT f.filename, f.date, f.size, f.type,
|
||||||
t.filename, f.description \
|
t.filename, f.description
|
||||||
FROM files f \
|
FROM files f
|
||||||
LEFT JOIN thumbnails t ON t.file_id = f.id \
|
LEFT JOIN thumbnails t ON t.file_id = f.id
|
||||||
WHERE f.id = ?", (id,))
|
WHERE f.id = ?"""
|
||||||
|
self.db_cursor.execute(sql, (id,))
|
||||||
set = self.db_cursor.fetchone()
|
set = self.db_cursor.fetchone()
|
||||||
if set:
|
if set:
|
||||||
string = "ID: %d\nFilename: %s\nDate: %s\nSize: %s\ntype: %s" % \
|
string = "ID: %d\nFilename: %s\nDate: %s\nSize: %s\ntype: %s" % \
|
||||||
@@ -296,6 +323,17 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
if set[4]:
|
if set[4]:
|
||||||
retval['thumbnail'] = os.path.join(self.internal_dirname, set[4])
|
retval['thumbnail'] = os.path.join(self.internal_dirname, set[4])
|
||||||
|
|
||||||
|
sql = """SELECT id, filename, thumbnail from images WHERE file_id = ?"""
|
||||||
|
self.db_cursor.execute(sql, (id,))
|
||||||
|
set = self.db_cursor.fetchall()
|
||||||
|
if set:
|
||||||
|
self.images_store = gtk.ListStore(gobject.TYPE_INT, gtk.gdk.Pixbuf)
|
||||||
|
for id, img, thb in set:
|
||||||
|
im = os.path.join(self.internal_dirname,thb)
|
||||||
|
pix = gtk.gdk.pixbuf_new_from_file(im)
|
||||||
|
self.images_store.append([id, pix])
|
||||||
|
retval['images'] = True
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
def get_source(self, path):
|
def get_source(self, path):
|
||||||
@@ -311,7 +349,7 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
def get_label_and_filepath(self, path):
|
def get_label_and_filepath(self, path):
|
||||||
"""get source of top level directory"""
|
"""get source of top level directory"""
|
||||||
bid = self.discs_tree.get_value(self.discs_tree.get_iter(path[0]),
|
bid = self.discs_tree.get_value(self.discs_tree.get_iter(path),
|
||||||
0)
|
0)
|
||||||
self.db_cursor.execute("select filepath, filename from files \
|
self.db_cursor.execute("select filepath, filename from files \
|
||||||
where id = ? and parent_id = 1", (bid,))
|
where id = ? and parent_id = 1", (bid,))
|
||||||
@@ -320,11 +358,92 @@ class MainModel(ModelMT):
|
|||||||
return None, None
|
return None, None
|
||||||
return res[0], res[1]
|
return res[0], res[1]
|
||||||
|
|
||||||
def delete(self, branch_iter):
|
def delete_image(self, id):
|
||||||
if not branch_iter:
|
"""removes image on specified id"""
|
||||||
return
|
sql = """select filename, thumbnail from images where id=?"""
|
||||||
self.__remove_branch_form_db(self.discs_tree.get_value(branch_iter,0))
|
self.db_cursor.execute(sql, (id,))
|
||||||
self.discs_tree.remove(branch_iter)
|
res = self.db_cursor.fetchone()
|
||||||
|
if res[0]:
|
||||||
|
os.unlink(os.path.join(self.internal_dirname, res[0]))
|
||||||
|
os.unlink(os.path.join(self.internal_dirname, res[1]))
|
||||||
|
|
||||||
|
if __debug__:
|
||||||
|
print "m_main.py: delete_image(): removed images:"
|
||||||
|
print res[0]
|
||||||
|
print res[1]
|
||||||
|
# remove images records
|
||||||
|
sql = """delete from images where id = ?"""
|
||||||
|
self.db_cursor.execute(sql, (id,))
|
||||||
|
self.db_connection.commit()
|
||||||
|
|
||||||
|
def delete(self, root_id, db_cursor=None, db_connection=None):
|
||||||
|
"""Remove subtree from main tree, remove tags from database
|
||||||
|
remove all possible data, like thumbnails"""
|
||||||
|
|
||||||
|
# TODO: opanowac syf zwiazany z tym, ze katalogi teraz przechowuja dane nieprawdziwe
|
||||||
|
|
||||||
|
fids = []
|
||||||
|
|
||||||
|
if not db_cursor:
|
||||||
|
db_cursor = self.db_cursor
|
||||||
|
|
||||||
|
if not db_connection:
|
||||||
|
db_connection = self.db_connection
|
||||||
|
|
||||||
|
def get_children(fid):
|
||||||
|
fids.append(fid)
|
||||||
|
sql = """select id from files where parent_id = ?"""
|
||||||
|
db_cursor.execute(sql, (fid,))
|
||||||
|
res = db_cursor.fetchall()
|
||||||
|
if len(res)>0:
|
||||||
|
for i in res:
|
||||||
|
get_children(i[0])
|
||||||
|
|
||||||
|
get_children(root_id)
|
||||||
|
|
||||||
|
def generator():
|
||||||
|
for c in fids:
|
||||||
|
yield (c,)
|
||||||
|
|
||||||
|
# remove files records
|
||||||
|
sql = """delete from files where id = ?"""
|
||||||
|
db_cursor.executemany(sql, generator())
|
||||||
|
|
||||||
|
# remove tags records
|
||||||
|
sql = """delete from tags_files where file_id = ?"""
|
||||||
|
db_cursor.executemany(sql, generator())
|
||||||
|
|
||||||
|
# remove thumbnails
|
||||||
|
arg =''
|
||||||
|
for c in fids:
|
||||||
|
if len(arg) > 0:
|
||||||
|
arg+=", %d" % c
|
||||||
|
else:
|
||||||
|
arg = "%d" % c
|
||||||
|
sql = """select filename from thumbnails where file_id in (%s)""" % arg
|
||||||
|
db_cursor.execute(sql)
|
||||||
|
res = db_cursor.fetchall()
|
||||||
|
if len(res) > 0:
|
||||||
|
for fn in res:
|
||||||
|
os.unlink(os.path.join(self.internal_dirname, fn[0]))
|
||||||
|
|
||||||
|
# remove images
|
||||||
|
sql = """select filename, thumbnail from images where file_id in (%s)""" % arg
|
||||||
|
db_cursor.execute(sql)
|
||||||
|
res = db_cursor.fetchall()
|
||||||
|
if res[0][0]:
|
||||||
|
for fn in res:
|
||||||
|
os.unlink(os.path.join(self.internal_dirname, fn[0]))
|
||||||
|
|
||||||
|
# remove thumbs records
|
||||||
|
sql = """delete from thumbnails where file_id = ?"""
|
||||||
|
db_cursor.executemany(sql, generator())
|
||||||
|
|
||||||
|
# remove images records
|
||||||
|
sql = """delete from images where file_id = ?"""
|
||||||
|
db_cursor.executemany(sql, generator())
|
||||||
|
|
||||||
|
db_connection.commit()
|
||||||
return
|
return
|
||||||
|
|
||||||
def get_stats(self, selected_id):
|
def get_stats(self, selected_id):
|
||||||
@@ -398,25 +517,33 @@ class MainModel(ModelMT):
|
|||||||
retval['size'] = self.__bytes_to_human(res[0])
|
retval['size'] = self.__bytes_to_human(res[0])
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
def get_image_path(self, img_id):
|
||||||
|
"""return image location"""
|
||||||
|
sql = """select filename from images where id=?"""
|
||||||
|
self.db_cursor.execute(sql, (img_id,))
|
||||||
|
res = self.db_cursor.fetchone()
|
||||||
|
if res:
|
||||||
|
return res[0]
|
||||||
|
return None
|
||||||
|
|
||||||
# private class functions
|
# private class functions
|
||||||
def __bytes_to_human(self, integer):
|
def __bytes_to_human(self, integer):
|
||||||
if integer <= 0 or integer < 1024:
|
if integer <= 0 or integer < 1024:
|
||||||
return "%d bytes" % integer
|
return "%d bytes" % integer
|
||||||
|
|
||||||
t = integer /1024.0
|
## convert integer into string with thousands' separator
|
||||||
if t < 1 or t < 1024:
|
#for i in range(len(str(integer))/3+1):
|
||||||
return "%d bytes (%d kB)" % (integer, t)
|
# if i == 0:
|
||||||
|
# s_int = str(integer)[-3:]
|
||||||
|
# else:
|
||||||
|
# s_int = str(integer)[-(3*int(i)+3):-(3*int(i))] + " " + s_int
|
||||||
|
|
||||||
t = t /1024.0
|
t = integer
|
||||||
if t < 1 or t < 1024:
|
for power in ['kB', 'MB', 'GB', 'TB']:
|
||||||
return "%d bytes (%d MB)" % (integer, t)
|
t = t /1024.0
|
||||||
|
if t < 1 or t < 1024:
|
||||||
t = t /1024.0
|
break
|
||||||
if t < 1 or t < 1024:
|
return "%0.2f %s (%d bytes)" % (t, power, integer)
|
||||||
return "%d bytes (%d GB)" % (integer, t)
|
|
||||||
|
|
||||||
t = t /1024.0
|
|
||||||
return "%d bytes (%d TB)" % (integer, t)
|
|
||||||
|
|
||||||
def __clear_trees(self):
|
def __clear_trees(self):
|
||||||
self.__clear_files_tree()
|
self.__clear_files_tree()
|
||||||
@@ -506,7 +633,12 @@ class MainModel(ModelMT):
|
|||||||
thumbnails(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
thumbnails(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
file_id INTEGER,
|
file_id INTEGER,
|
||||||
filename TEXT);""")
|
filename TEXT);""")
|
||||||
self.db_cursor.execute("insert into files values(1, 1, 'root', null, 0, 0, 0, 0, null, null, null, null);")
|
self.db_cursor.execute("""create table
|
||||||
|
images(id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
file_id INTEGER,
|
||||||
|
thumbnail TEXT,
|
||||||
|
filename TEXT);""")
|
||||||
|
self.db_cursor.execute("insert into files values(1, 1, 'root', null, 0, 0, 0, 0, null, null);")
|
||||||
self.db_cursor.execute("insert into groups values(1, 'default', 'black');")
|
self.db_cursor.execute("insert into groups values(1, 'default', 'black');")
|
||||||
|
|
||||||
def __scan(self):
|
def __scan(self):
|
||||||
@@ -678,11 +810,11 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
# Images - thumbnails and exif data
|
# Images - thumbnails and exif data
|
||||||
if self.config.confd['thumbs'] and ext in self.IMG:
|
if self.config.confd['thumbs'] and ext in self.IMG:
|
||||||
path, exif, ret_code = Thumbnail(current_file, base=self.internal_dirname).save(fileid)
|
tpath, exif, ret_code = Thumbnail(current_file, base=self.internal_dirname).save(fileid)
|
||||||
if ret_code != -1:
|
if ret_code != -1:
|
||||||
sql = """insert into thumbnails(file_id, filename) values (?, ?)"""
|
sql = """insert into thumbnails(file_id, filename) values (?, ?)"""
|
||||||
db_cursor.execute(sql, (fileid,
|
db_cursor.execute(sql, (fileid,
|
||||||
path.split(self.internal_dirname)[1][1:]))
|
tpath.split(self.internal_dirname)[1][1:]))
|
||||||
|
|
||||||
if self.config.confd['exif']:
|
if self.config.confd['exif']:
|
||||||
# TODO: exif implementation
|
# TODO: exif implementation
|
||||||
@@ -717,14 +849,21 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
if __recurse(1, self.label, self.path, 0, 0, self.DIR) == -1:
|
if __recurse(1, self.label, self.path, 0, 0, self.DIR) == -1:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "m_main.py: __scan() __recurse() \
|
print "m_main.py: __scan() __recurse()",
|
||||||
interrupted self.abort = True"
|
print "interrupted self.abort = True"
|
||||||
self.discs_tree.remove(self.fresh_disk_iter)
|
self.discs_tree.remove(self.fresh_disk_iter)
|
||||||
db_cursor.close()
|
db_cursor.close()
|
||||||
db_connection.rollback()
|
db_connection.rollback()
|
||||||
else:
|
else:
|
||||||
if __debug__:
|
if __debug__:
|
||||||
print "m_main.py: __scan() __recurse() goes without interrupt"
|
print "m_main.py: __scan() __recurse() goes without interrupt"
|
||||||
|
if self.currentid:
|
||||||
|
if __debug__:
|
||||||
|
print "m_main.py: __scan() removing old branch"
|
||||||
|
self.delete(self.currentid, db_cursor, db_connection)
|
||||||
|
self.currentid = None
|
||||||
|
else:
|
||||||
|
print "new directory/cd"
|
||||||
db_cursor.close()
|
db_cursor.close()
|
||||||
db_connection.commit()
|
db_connection.commit()
|
||||||
db_connection.close()
|
db_connection.close()
|
||||||
@@ -733,6 +872,8 @@ class MainModel(ModelMT):
|
|||||||
|
|
||||||
self.busy = False
|
self.busy = False
|
||||||
|
|
||||||
|
# refresh discs tree
|
||||||
|
self.__fetch_db_into_treestore()
|
||||||
self.statusmsg = "Idle"
|
self.statusmsg = "Idle"
|
||||||
self.progress = 0
|
self.progress = 0
|
||||||
self.abort = False
|
self.abort = False
|
||||||
@@ -797,54 +938,6 @@ class MainModel(ModelMT):
|
|||||||
db_connection.close()
|
db_connection.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
def __remove_branch_form_db(self, root_id):
|
|
||||||
"""Remove subtree from main tree, remove tags from database
|
|
||||||
remove all possible data, like thumbnails"""
|
|
||||||
fids = []
|
|
||||||
|
|
||||||
def get_children(fid):
|
|
||||||
fids.append(fid)
|
|
||||||
sql = """select id from files where parent_id = ?"""
|
|
||||||
self.db_cursor.execute(sql, (fid,))
|
|
||||||
res = self.db_cursor.fetchall()
|
|
||||||
if len(res)>0:
|
|
||||||
for i in res:
|
|
||||||
get_children(i[0])
|
|
||||||
|
|
||||||
get_children(root_id)
|
|
||||||
|
|
||||||
def generator():
|
|
||||||
for c in fids:
|
|
||||||
yield (c,)
|
|
||||||
|
|
||||||
# remove files records
|
|
||||||
sql = """delete from files where id = ?"""
|
|
||||||
self.db_cursor.executemany(sql, generator())
|
|
||||||
|
|
||||||
# remove tags records
|
|
||||||
sql = """delete from tags_files where file_id = ?"""
|
|
||||||
self.db_cursor.executemany(sql, generator())
|
|
||||||
|
|
||||||
# remove thumbnails
|
|
||||||
arg =''
|
|
||||||
for c in fids:
|
|
||||||
if len(arg) > 0:
|
|
||||||
arg+=", %d" % c
|
|
||||||
else:
|
|
||||||
arg = "%d" % c
|
|
||||||
sql = """select filename from thumbnails where file_id in (%s)""" % arg
|
|
||||||
self.db_cursor.execute(sql)
|
|
||||||
res = self.db_cursor.fetchall()
|
|
||||||
if len(res) > 0:
|
|
||||||
for fn in res:
|
|
||||||
os.unlink(os.path.join(self.internal_dirname, fn[0]))
|
|
||||||
|
|
||||||
# remove thumbs records
|
|
||||||
sql = """delete from thumbnails where file_id = ?"""
|
|
||||||
self.db_cursor.executemany(sql, generator())
|
|
||||||
self.db_connection.commit()
|
|
||||||
return
|
|
||||||
|
|
||||||
def __append_added_volume(self):
|
def __append_added_volume(self):
|
||||||
"""append branch from DB to existing tree model"""
|
"""append branch from DB to existing tree model"""
|
||||||
#connect
|
#connect
|
||||||
|
|||||||
156
src/utils/img.py
Normal file
156
src/utils/img.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# 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 gettempdir
|
||||||
|
from shutil import move, copy
|
||||||
|
from os import path, mkdir
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import EXIF
|
||||||
|
import Image
|
||||||
|
|
||||||
|
class Img(object):
|
||||||
|
def __init__(self, filename=None, base=''):
|
||||||
|
self.root = 'images'
|
||||||
|
self.x = 160
|
||||||
|
self.y = 160
|
||||||
|
self.filename = filename
|
||||||
|
self.base = base
|
||||||
|
|
||||||
|
def save(self, image_id):
|
||||||
|
"""Save image and asociated thumbnail into specific directory structure
|
||||||
|
return full path to the file and thumbnail None"""
|
||||||
|
|
||||||
|
base_path = self.__get_and_make_path(image_id)
|
||||||
|
ext = self.filename.split('.')[-1].lower()
|
||||||
|
image_filename = path.join(self.base, base_path + "_im." + ext)
|
||||||
|
|
||||||
|
# make and save image
|
||||||
|
filepath = path.join(self.base, base_path + ".jpg")
|
||||||
|
f = open(self.filename, 'rb')
|
||||||
|
exif = None
|
||||||
|
returncode = -1
|
||||||
|
try:
|
||||||
|
exif = EXIF.process_file(f)
|
||||||
|
f.close()
|
||||||
|
if exif.has_key('JPEGThumbnail'):
|
||||||
|
thumbnail = exif['JPEGThumbnail']
|
||||||
|
f = open(filepath,'wb')
|
||||||
|
f.write(thumbnail)
|
||||||
|
f.close()
|
||||||
|
if exif.has_key('Image Orientation'):
|
||||||
|
orientation = exif['Image Orientation'].values[0]
|
||||||
|
if orientation > 1:
|
||||||
|
# TODO: replace silly datetime function with tempfile
|
||||||
|
t = path.join(gettempdir(), "thumb%d.jpg" % datetime.now().microsecond)
|
||||||
|
im_in = Image.open(filepath)
|
||||||
|
im_out = None
|
||||||
|
if orientation == 8:
|
||||||
|
# Rotated 90 CCW
|
||||||
|
im_out = im_in.transpose(Image.ROTATE_90)
|
||||||
|
elif orientation == 6:
|
||||||
|
# Rotated 90 CW
|
||||||
|
im_out = im_in.transpose(Image.ROTATE_270)
|
||||||
|
elif orientation == 3:
|
||||||
|
# Rotated 180
|
||||||
|
im_out = im_in.transpose(Image.ROTATE_180)
|
||||||
|
elif orientation == 2:
|
||||||
|
# Mirrored horizontal
|
||||||
|
im_out = im_in.transpose(Image.FLIP_LEFT_RIGHT)
|
||||||
|
elif orientation == 4:
|
||||||
|
# Mirrored vertical
|
||||||
|
im_out = im_in.transpose(Image.FLIP_TOP_BOTTOM)
|
||||||
|
elif orientation == 5:
|
||||||
|
# Mirrored horizontal then rotated 90 CCW
|
||||||
|
im_out = im_in.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_90)
|
||||||
|
elif orientation == 7:
|
||||||
|
# Mirrored horizontal then rotated 90 CW
|
||||||
|
im_out = im_in.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270)
|
||||||
|
|
||||||
|
if im_out:
|
||||||
|
im_out.save(t, 'JPEG')
|
||||||
|
move(t, filepath)
|
||||||
|
else:
|
||||||
|
f.close()
|
||||||
|
returncode = 0
|
||||||
|
else:
|
||||||
|
im = self.__scale_image()
|
||||||
|
if im:
|
||||||
|
im.save(filepath, "JPEG")
|
||||||
|
returncode = 1
|
||||||
|
except:
|
||||||
|
f.close()
|
||||||
|
im = self.__scale_image()
|
||||||
|
if im:
|
||||||
|
im.save(filepath, "JPEG")
|
||||||
|
returncode = 2
|
||||||
|
|
||||||
|
if returncode != -1:
|
||||||
|
# copy image
|
||||||
|
copy(self.filename, image_filename)
|
||||||
|
return filepath, image_filename, returncode
|
||||||
|
|
||||||
|
# private class functions
|
||||||
|
def __get_and_make_path(self, img_id):
|
||||||
|
"""Make directory structure regards of id
|
||||||
|
and return filepath and img filename WITHOUT extension"""
|
||||||
|
t = path.join(self.base, self.root)
|
||||||
|
try: mkdir(t)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
h = hex(img_id)
|
||||||
|
if len(h[2:])>6:
|
||||||
|
try: mkdir(path.join(t, h[2:4]))
|
||||||
|
except: pass
|
||||||
|
try: mkdir(path.join(t, h[2:4], h[4:6]))
|
||||||
|
except: pass
|
||||||
|
fpath = path.join(t, h[2:4], h[4:6], h[6:8])
|
||||||
|
try: mkdir(fpath)
|
||||||
|
except: pass
|
||||||
|
img = "%s" % h[8:]
|
||||||
|
elif len(h[2:])>4:
|
||||||
|
try: mkdir(path.join(t, h[2:4]))
|
||||||
|
except: pass
|
||||||
|
fpath = path.join(t, h[2:4], h[4:6])
|
||||||
|
try: mkdir(fpath)
|
||||||
|
except: pass
|
||||||
|
img = "%s" % h[6:]
|
||||||
|
elif len(h[2:])>2:
|
||||||
|
fpath = path.join(t, h[2:4])
|
||||||
|
try: mkdir(fpath)
|
||||||
|
except: pass
|
||||||
|
img = "%s" % h[4:]
|
||||||
|
else:
|
||||||
|
fpath = ''
|
||||||
|
img = "%s" % h[2:]
|
||||||
|
return(path.join(t, fpath, img))
|
||||||
|
|
||||||
|
def __scale_image(self, factor=True):
|
||||||
|
"""create thumbnail. returns image object or None"""
|
||||||
|
try:
|
||||||
|
im = Image.open(self.filename).convert('RGB')
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
im.thumbnail((self.x, self.y), Image.ANTIALIAS)
|
||||||
|
return im
|
||||||
@@ -129,6 +129,24 @@ class InputDiskLabel(object):
|
|||||||
return entry.get_text()
|
return entry.get_text()
|
||||||
return None
|
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):
|
class PointDirectoryToAdd(object):
|
||||||
"""Sepcific dialog for quering user for selecting directory to add"""
|
"""Sepcific dialog for quering user for selecting directory to add"""
|
||||||
def __init__(self,volname='',dirname=''):
|
def __init__(self,volname='',dirname=''):
|
||||||
@@ -277,6 +295,46 @@ class LoadDBFile(object):
|
|||||||
ch = True
|
ch = True
|
||||||
res,filename = self.show_dialog()
|
res,filename = self.show_dialog()
|
||||||
|
|
||||||
|
|
||||||
|
class LoadImageFile(object):
|
||||||
|
"""class for displaying openFile dialog. It have possibility of multiple
|
||||||
|
selection."""
|
||||||
|
def __init__(self):
|
||||||
|
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(True)
|
||||||
|
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']:
|
||||||
|
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)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
response = self.dialog.run()
|
||||||
|
filenames = None
|
||||||
|
|
||||||
|
if response == gtk.RESPONSE_OK:
|
||||||
|
try:
|
||||||
|
filenames = self.dialog.get_filenames()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
self.dialog.destroy()
|
||||||
|
return filenames
|
||||||
|
|
||||||
class StatsDialog(object):
|
class StatsDialog(object):
|
||||||
"""Sepcific dialog for display stats"""
|
"""Sepcific dialog for display stats"""
|
||||||
def __init__(self, values={}):
|
def __init__(self, values={}):
|
||||||
|
|||||||
43
src/views/v_image.py
Normal file
43
src/views/v_image.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# 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 gtk
|
||||||
|
|
||||||
|
import utils.globals
|
||||||
|
|
||||||
|
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)
|
||||||
|
window.add(image)
|
||||||
|
image.show()
|
||||||
|
window.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
pass # end of class
|
||||||
Reference in New Issue
Block a user