#!/usr/bin/env python3
import tkinter as tk
import tkinter.scrolledtext as scrolledtext
# This doesn't work:
# from tkinter.messagebox import *
# though this does:
#from tkinter.messagebox import _show
# so instead we'll have to preface everything with mb.
# from tkinter import messagebox as mb # doesn't automatically import
from tkinter.simpledialog import Dialog
import sys
WANTED_EXIF_TAGS = [
'ImageDescription',
'Make', 'Model', 'Software',
'LensMake', 'LensModel', 'LensSpecification',
'Orientation',
'DateTime', 'DateTimeOriginal',
'FocalLength', 'FocalLengthIn35mmFilm', 'FNumber', 'ExposureTime',
'ShutterSpeedValue', 'ApertureValue', 'BrightnessValue', 'ExposureTime',
'ISOSpeedRatings',
'GPS coordinates', # As decoded by TkPhoImage
'WhiteBalance', 'ExposureBiasValue', 'MaxApertureValue', 'ColorSpace',
'SubjectDistance',
'MeteringMode', 'Flash', 'LightSource',
'ExposureProgram', 'ExposureMode',
'DigitalZoomRatio', 'SceneCaptureType'
'SubjectDistanceRange',
# 'UserComment', 'MakerNote', # These are long unreadable gibberish
# 'SceneType', 'ColorSpace', 'SensingMethod', 'RecommendedExposureIndex',
# 'Contrast', 'Saturation', 'Sharpness',
# 'CompositeImage'
]
#
# InfoDialog window
#
[docs]
class InfoDialog(tk.Toplevel):
"""The InfoDialog is non-modal, and shows details about the
current image, like EXIF. It should be possible to keep it up
and have it change as the image changes.
Typically it will be used as a singleton.
"""
def __init__(self, *args, **kwargs):
tk.Toplevel.__init__(self, *args, **kwargs)
self.textarea = scrolledtext.ScrolledText(self, padx=7, pady=7)
self.textarea.grid(row=0, column=0, columnspan=3, sticky=tk.NW+tk.SE)
# textarea fonts
self.textarea.tag_configure('normal', font=('sans', 11))
self.textarea.tag_configure('bold', font=('sans', 11, 'bold'))
self.textarea.tag_configure('title', font=('Sans', 15, 'bold'))
tk.Button(self, text="OK", command=self.destroy_func) \
.grid(row=1, column=1, sticky=tk.NW+tk.SE)
# self.bind_all("<KeyDestroy>", self.destroy_func)
self.grid()
# On resizing, fill empty space with the textarea
self.grid_rowconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=0)
# self.focus_set()
self.bind("<Return>", self.popdown)
self.bind("<Escape>", self.popdown)
# Iconify on the titlebar 'x' as well
self.protocol('WM_DELETE_WINDOW', self.popdown)
[docs]
def update_msg(self, cur_im, tagger=None):
"""Update the message shown in the info dialog.
Needs the Tagger in order to map tag numbers to strings,
else it will just show tag numbers.
"""
self.title(cur_im.relpath)
messagelist = []
messagelist.append((cur_im.relpath + '\n', 'title'))
if cur_im.orig_img:
messagelist.append(('Actual size: ', 'bold',
str(cur_im.orig_img.size), 'normal'))
if cur_im.display_img:
messagelist.append(('Displayed size: ', 'bold',
str(cur_im.display_img.size), 'normal'))
messagelist.append(('Rotation: ', 'bold',
str(cur_im.rot), 'normal'))
messagelist.append(('EXIF Rotation: ', 'bold',
cur_im.exif_rotation, 'normal'))
# What tags are set?
if tagger:
messagelist.append(('', 'normal')) # paragraph break
messagelist.append(('Tags: ', 'title'))
if cur_im.tags:
tagdict = tagger.tagdict_for_img(cur_im)
for cat in tagdict:
if cat != 'Tags':
tagmessages = ['Category ' + cat + ': ', 'bold']
else:
tagmessages = []
tagmessages.append(', '.join([
tagger.tag_list[tagno] for tagno in tagdict[cat] ]))
tagmessages.append('normal')
messagelist.append(tagmessages)
else:
tagmessages = ('None', 'normal')
else:
messagelist += ( 'Tags: ', 'bold',
', '.join([ str(t) for t in cur_im.tags ]),
'normal' )
exif = cur_im.get_exif()
if exif:
messagelist.append(('', 'normal')) # paragraph break
messagelist.append(('EXIF: ', 'title'))
for key in WANTED_EXIF_TAGS:
if key in exif:
messagelist.append((key + ': ', 'bold', exif[key], 'normal'))
# The message is now ready to show
self.set_text(messagelist)
[docs]
@staticmethod
def string_size(text):
w = 0
h = 0
for line in text.splitlines():
w = max(w, len(line))
h += 1
return w, h
[docs]
def set_text(self, textlist):
"""Text list is a list of tuples/lists
There will be a line break between each item in textlist.
The items themselves are lists alternating string and font.
"""
# XXX tw, th = self.string_size(text)
tw = 80
th = 25
self.textarea.config(state=tk.NORMAL, width=tw, height=th)
self.textarea.delete('1.0', tk.END) # enable editing
for line in textlist:
lineiter = iter(line)
for text, font in zip(lineiter, lineiter):
self.textarea.insert(tk.END, text, font)
self.textarea.insert(tk.END, '\n')
# self.textarea.insert(1.0, text)
self.textarea.config(state=tk.DISABLED) # back to read-only
# self.focus_set()
[docs]
def popdown(self, event=None):
self.withdraw() # or iconify()
# After dismissing the dialog, the focus tends to end up
# in some random other window. Try to keep focus within the app:
self._root().focus()
# This seems like it should do the same thing,
# but it doesn't actually focus the main window:
# tk._default_root.focus()
[docs]
def destroy_func(self, event=None):
# event=None necessary as we also use button binding.
self.popdown()
#
# Custom messagebox, since messagebox doesn't allow for any additions
#
[docs]
class CustomDialog(Dialog):
def __init__(self, title, message, icon, buttons,
master=None, yes_bindings=[]):
"""
icon can be "information", "warning", "error" or "question"
buttons can be e.g. ["Yes", "No"], affirmative first
"""
self.message = message
self.icon = icon
self.buttons = buttons
self.yes_bindings = yes_bindings
super().__init__(master, title)
[docs]
def body(self, parent):
# get the system icon image
# XXX the icons aren't working
self.photo = tk.PhotoImage(master=parent)
self.tk.call(self.photo, "copy", f"::tk::icons::{self.icon}")
# create label with the icon image and message
tk.Label(parent, text=self.message,
compound="left").pack(padx=50, pady=20)
def _bindings(self):
self.bind("<Return>", self.ok)
self.bind("<Escape>", self.cancel)
for key in self.yes_bindings:
self.bind(key, self.ok)
[docs]
def apply(self, event=None):
self.result = True
# def cancel(self, event=None):
# super().cancel(event)
[docs]
def askyesno_with_bindings(title=None, message=None, yes_bindings=[]):
dlg = CustomDialog(title=title, message=message, icon="question",
buttons=["Yes", "No"],
yes_bindings=yes_bindings)
return dlg.result
[docs]
def message_dialog(title=None, message=None, yes_bindings=[]):
dlg = CustomDialog(title=title, message=message, icon="question",
buttons=["OK"], yes_bindings=yes_bindings)
return dlg.result
# Flash a temporary message
[docs]
def flash_message(msg, root):
print(msg, file=sys.stderr)
flashlbl = tk.Label(root, text=msg,
font=('',14), bg='yellow', relief=tk.SUNKEN)
flashlbl.place(relx=.5, rely=.5, anchor=tk.CENTER)
root.after(3000, flashlbl.destroy)
if __name__ == '__main__':
infobox = None
def popup_dialogs():
infobox = InfoDialog()
infobox.set_text("Show this message\nwith lots\nof extra lines, some of which are long\nand hopefully won't wrap")
ans = askyesno_with_bindings("Custom yesno",
"Do you want to quit now?",
yes_bindings=['<Key-q>', '<Key-d>'])
print("answer:", ans)
if ans:
root.quit()
root = tk.Tk()
tk.Button(root, text="push me", command=popup_dialogs).pack()
root.mainloop()