13 def add_key_binding(widget
, keyname
, callback
):
14 accelgroup
= gtk
.AccelGroup()
15 key
, modifier
= gtk
.accelerator_parse(keyname
)
16 accelgroup
.connect_group(key
, modifier
, gtk
.ACCEL_VISIBLE
, callback
)
17 widget
.add_accel_group(accelgroup
)
19 class StockButton(gtk
.Button
):
20 def __init__(self
, label
=None, stock
=None, use_underline
=True):
21 gtk
.Button
.__init
__(self
, label
, stock
, use_underline
)
23 self
.set_markup(label
)
24 def set_markup(self
, label
):
25 a
= self
.get_children()[0]
26 a
= a
.get_children()[0]
27 a
, lbl
= a
.get_children()
29 def set_pixbuf(self
, data
):
30 a
= self
.get_children()[0]
31 a
= a
.get_children()[0]
32 img
, a
= a
.get_children()
34 img
.set_from_icon_name(data
[1], data
[2])
36 img
.set_from_pixbuf(data
[0])
37 img
.props
.visible
= True
40 def get_comment_text():
41 tb
= txtv
.get_buffer()
42 s
, e
= tb
.get_start_iter(), tb
.get_end_iter()
43 return tb
.get_text(s
, e
)
46 txt
= get_comment_text()
47 Metadata
.comment
= txt
48 update_index(SubjectFile
, txt
)
50 def save_comment_and_exit(*a
):
54 def tag_entered(entry
):
55 tagstr
= entry
.get_text()
56 if tagstr
not in get_comment_text().split("\n"):
57 tb
= txtv
.get_buffer()
58 end
= tb
.get_end_iter()
59 lastchar
= tb
.get_text(tb
.get_iter_at_offset(tb
.get_char_count() - 1), end
)
60 tb
.insert(end
, ("\n" if lastchar
not in ["\n", ''] else '') + tagstr
+ "\n")
64 dbconn
= sqlite3
.connect(dbfile
)
65 dbconn
.text_factory
= str
66 dbcur
= dbconn
.cursor()
67 dbcur
.execute("PRAGMA foreign_keys = ON")
68 return (dbconn
, dbcur
)
70 def db_oper(dbfile
, relpath
, fname
, valtext
):
71 values
= filter(lambda x
: x
.strip() != '', valtext
.split('\n'))
72 (dbconn
, dbcur
) = db_open(dbfile
)
74 dbcur
.execute("CREATE TABLE IF NOT EXISTS folder (folderid INTEGER PRIMARY KEY, relpath TEXT UNIQUE)")
75 dbcur
.execute("CREATE TABLE IF NOT EXISTS tag (tagid INTEGER PRIMARY KEY, tag TEXT UNIQUE)")
76 dbcur
.execute("CREATE TABLE IF NOT EXISTS comment (folder REFERENCES folder (folderid), filename TEXT, tag REFERENCES tag (tagid), UNIQUE (folder, filename, tag))")
78 dbcur
.execute("INSERT OR IGNORE INTO folder (relpath) VALUES (?)", [relpath
])
79 dbcur
.executemany("INSERT OR IGNORE INTO tag (tag) VALUES (?)", map(lambda x
: [x
], values
))
81 dbcur
.execute("DELETE FROM comment WHERE folder = (SELECT folderid FROM folder WHERE relpath=?) AND filename = ? AND tag NOT IN (SELECT tagid FROM tag WHERE tag IN (" + ','.join(map(lambda x
: '?', values
)) + "))", [relpath
, fname
] + values
)
82 dbcur
.executemany("INSERT OR IGNORE INTO comment (folder, filename, tag) VALUES ((SELECT folderid FROM folder WHERE relpath=?), ?, (SELECT tagid FROM tag WHERE tag=?))", map(lambda x
: [relpath
, fname
, x
], values
))
84 except sqlite3
.OperationalError
, exc
:
85 print "SQLite Error:", exc
.message
91 def update_index(key
, val
):
92 (dbfilepath
, relpath
) = find_dbfile()
93 if dbfilepath
is None:
95 name
= os
.path
.basename(key
)
96 return db_oper(dbfilepath
, relpath
, name
, val
)
99 path
= os
.path
.realpath(os
.path
.dirname(SubjectFile
))
102 dbfilepath
= os
.path
.join(path
, 'exifcomment.db')
103 print "checking DB in", dbfilepath
104 if os
.path
.exists(dbfilepath
):
106 return (dbfilepath
, relpath
)
110 relpath
= os
.path
.basename(path
)
112 relpath
= os
.path
.join(os
.path
.basename(path
), relpath
)
113 path
= os
.path
.dirname(path
)
117 def db_load_tags(store
):
118 (dbfile
, unused
) = find_dbfile()
119 if dbfile
is None: return False
120 (dbconn
, dbcur
) = db_open(dbfile
)
122 dbcur
.execute("SELECT tag FROM tag")
124 tag
= dbcur
.fetchone()
125 if tag
is None: break
126 store
.append([tag
[0], -1])
127 except sqlite3
.OperationalError
, exc
:
128 print "SQLite Error:", exc
.message
134 def display_error(msg
):
135 dlg
= gtk
.MessageDialog(None, gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
, gtk
.MESSAGE_WARNING
, gtk
.BUTTONS_OK
, msg
)
139 def load_thumbnail():
141 thumbnail_loaded
= False
142 thumbnail
= Metadata
.thumbnail
143 if thumbnail
is not None:
145 ldr
= gtk
.gdk
.PixbufLoader('jpeg')
148 thnl
.set_from_pixbuf(ldr
.get_pixbuf())
149 thumbnail_loaded
= True
151 traceback
.print_exc(e
)
152 display_error("Can not load thumbnail.\n"+str(e
))
154 if not thumbnail_loaded
:
155 pb
= gtk
.gdk
.pixbuf_new_from_file(SubjectFile
)
156 w0
, h0
= pb
.get_width(), pb
.get_height()
159 pb
= pb
.scale_simple(w
, h
, gtk
.gdk
.INTERP_BILINEAR
)
160 thnl
.set_from_pixbuf(pb
)
161 thumbnail_loaded
= True
163 traceback
.print_exc(e
)
164 display_error("Can not load thumbnail.\n"+str(e
))
167 def open_externally(*X
):
169 subprocess
.Popen(['gpicview', SubjectFile
], stdout
=sys
.stdout
, stderr
=sys
.stderr
)
171 def completion_matcher(completion
, userinput
, treeiter
, userdata
):
172 column_text
, column_rank
= userdata
[:]
173 model
= completion
.get_model()
174 return model
[treeiter
][column_text
].lower().find(userinput
) > -1
176 def periodic_sorter():
178 model
= completion
.get_model()
179 userinput
= entr
.get_text()
180 last_userinput
= completion
.get_data('last-userinput')
181 if last_userinput
is None or last_userinput
!= userinput
:
182 completion
.set_data('last-userinput', userinput
)
184 item
[column_rank
] = item
[column_text
].lower().find(userinput
)
185 # continue scheduling:
188 def sorter_cb(model
, iter_a
, iter_b
, userdata
):
189 column_text
, column_rank
= userdata
[:]
190 c
= cmp(model
[iter_a
][column_rank
], model
[iter_b
][column_rank
])
192 c
= cmp(model
[iter_a
][column_text
].lower(), model
[iter_b
][column_text
].lower())
195 def program_start(widget
, event
):
196 widget
.disconnect(start_event_handle
)
198 text
= Metadata
.tags
.get('DateTimeOriginal') or Metadata
.tags
.get('DateTime', '')
200 text
+= os
.path
.splitext(SubjectFile
)[0].replace('/', '\n').strip()
201 lbl_annotation
.set_text(text
)
203 txtv
.get_buffer().set_text(Metadata
.comment
)
209 win
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
210 win
.connect('delete-event', lambda a
, b
: gtk
.main_quit())
211 win
.set_default_size(400, 300)
212 add_key_binding(win
, 'Escape', gtk
.main_quit
)
213 add_key_binding(win
, 'F2', save_comment_and_exit
)
214 add_key_binding(win
, '<Control>O', open_externally
)
221 lbl_load_thnl
= gtk
.Label("Loading thumbnail...")
223 lbl_annotation
= gtk
.Label()
224 scrl
= gtk
.ScrolledWindow()
225 txtv
= gtk
.TextView()
226 cmpl
= gtk
.EntryCompletion()
228 lst0
= gtk
.ListStore(str, int)
231 btn0
= StockButton("Save & Close (F2)", gtk
.STOCK_SAVE
)
233 box111
.set_border_width(2)
234 box112
.set_border_width(2)
235 lbl_annotation
.set_selectable(True)
236 scrl
.set_policy(gtk
.POLICY_AUTOMATIC
, gtk
.POLICY_AUTOMATIC
)
237 btn0
.connect('clicked', save_comment_and_exit
)
238 entr
.connect('activate', tag_entered
)
239 entr
.set_completion(cmpl
)
240 cmpl
.set_match_func(completion_matcher
, (column_text
, column_rank
))
242 cmpl
.set_text_column(column_text
)
243 lst0
.set_sort_column_id(column_rank
, gtk
.SORT_ASCENDING
)
244 lst0
.set_sort_func(column_rank
, sorter_cb
, (column_text
, column_rank
))
245 glib
.timeout_add(1000, periodic_sorter
)
249 box0
.pack_start(box11
, True, True, 0)
250 box0
.pack_start(box12
, False, True, 0)
251 box11
.pack_start(box111
, True, True, 0)
252 box11
.pack_start(box112
, False, True, 0)
253 box111
.pack_start(scrl
, True, True, 0)
255 box111
.pack_start(entr
, False, True, 0)
256 box112
.pack_start(lbl_load_thnl
, False, False, 0)
257 box112
.pack_start(thnl
, False, False, 0)
258 box112
.pack_start(lbl_annotation
, False, False, 0)
259 box12
.pack_start(btn0
, True, False, 0)
261 if len(sys
.argv
) < 2:
262 sys
.stderr
.write("Usage: %s <filepath>\n" % sys
.argv
[0])
264 SubjectFile
= sys
.argv
[1]
265 Metadata
= imagemetadata
.Image(open(SubjectFile
, 'r').read())
267 start_event_handle
= win
.connect('map-event', program_start
)