#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import logging
import logging.config
import uuid
import os
import os.path
logger = logging.getLogger()
import codecs
import yaml
import platform
if platform.python_version() >= '3':
import tkinter
from tkinter import (
ACTIVE,
BooleanVar,
BOTH,
BROWSE,
Button,
Checkbutton,
END,
Entry,
FLAT,
Frame,
INSERT,
IntVar,
Label,
LEFT,
Listbox,
Radiobutton,
RAISED,
RIGHT,
Scrollbar,
StringVar,
TclError,
Text,
Toplevel,
W,
X,
)
from tkinter import ttk
from tkinter import font as tkFont
unicode = str
else:
import Tkinter as tkinter
from Tkinter import (
ACTIVE,
BooleanVar,
BOTH,
BROWSE,
Button,
Checkbutton,
END,
Entry,
FLAT,
Frame,
INSERT,
IntVar,
Label,
LEFT,
Listbox,
Radiobutton,
RAISED,
RIGHT,
Scrollbar,
StringVar,
TclError,
Text,
Toplevel,
W,
X,
)
import ttk
import tkFont
from datetime import datetime, timedelta
from etmTk.data import (
BGCOLOR,
FGCOLOR,
commandShortcut,
completion_regex,
ensureMonthly,
fmt_date,
fmt_period,
fmt_shortdatetime,
fmt_time,
get_current_time,
get_reps,
getFileTuples,
hsh2str,
import_ical,
parse_str,
parse_period,
relpath,
rrulefmt,
s2or3,
str2hsh,
uniqueId,
)
SOMEREPS = _('selected repetitions')
ALLREPS = _('all repetitions')
MESSAGES = _('Error messages')
VALID = _("Valid entry")
FOUND = "found" # for found text marking
FINISH = _("Save and Close")
# MAKE = _("Make")
PRINT = _("Print")
EXPORTTEXT = _("Export report in text format ...")
EXPORTCSV = _("Export report in CSV format ...")
SAVESPECS = _("Save changes to report specifications")
CLOSE = _("Close")
# VALID = _("Valid {0}").format(u"\u2714")
SAVEANDEXIT = _("Save changes and exit?")
UNCHANGEDEXIT = _("Item is unchanged. Exit?")
CREATENEW = _("creating a new item")
EDITEXISTING = _("editing an existing item")
type2Text = {
'$': _("In Basket"),
'=': _("Default"),
'^': _("Occasion"),
'*': _("Event"),
'~': _("Action"),
'!': _("Note"), # undated only appear in folders
'-': _("Task"), # for next view
'+': _("Task Group"), # for next view
'%': _("Delegated Task"),
'?': _("Someday"),
'#': _("Hidden")
}
def sanitize_id(id):
if type(id) == str:
return id.strip().replace(" ", "")
else:
return id
(_ADD, _DELETE, _INSERT) = range(3)
(_ROOT, _DEPTH, _WIDTH) = range(3)
ONEMINUTE = timedelta(minutes=1)
ONEHOUR = timedelta(hours=1)
ONEDAY = timedelta(days=1)
ONEWEEK = timedelta(weeks=1)
STOPPED = _('stopped')
PAUSED = _('paused')
RUNNING = _('running')
FOUND = "found"
class SimpleEditor(Toplevel):
def __init__(self, parent=None, master=None, file=None, line=None, newhsh=None, rephsh=None, options=None, title=None, start=None, modified=False):
"""
If file is given, open file for editing.
Otherwise, we are creating a new item and/or replacing an item
mode:
1: new: edit newhsh, replace none
2: replace: edit and replace rephsh
3: new and replace: edit newhsh, replace rephsh
:param parent:
:param file: path to file to be edited
"""
# self.frame = frame = Frame(parent)
if master is None:
master = parent
self.master = master
Toplevel.__init__(self, master)
self.minsize(400, 300)
self.geometry('500x200')
self.transient(parent)
self.parent = parent
self.loop = parent.loop
BGCOLOR = self.loop.options['background_color']
self.BGCOLOR = BGCOLOR
HLCOLOR = self.loop.options['highlight_color']
self.HLCOLOR = HLCOLOR
FGCOLOR = self.loop.options['foreground_color']
self.FGCOLOR = FGCOLOR
self.configure(background=BGCOLOR, highlightcolor=HLCOLOR)
self.messages = self.loop.messages
self.messages = []
self.mode = None
self.changed = False
self.scrollbar = None
self.listbox = None
self.autocompletewindow = None
self.line = None
self.match = None
self.file = file
self.initfile = None
self.fileinfo = None
self.repinfo = None
self.title = title
self.edithsh = {}
self.newhsh = newhsh
self.rephsh = rephsh
self.value = ''
self.options = options
self.tkfixedfont = tkFont.nametofont("TkFixedFont")
self.tkfixedfont.configure(size=self.options['fontsize_fixed'])
# self.text_value.trace_variable("w", self.setSaveStatus)
frame = Frame(self, bd=0)
frame.pack(side="bottom", fill=X, padx=4, pady=0)
frame.configure(background=BGCOLOR, highlightcolor=HLCOLOR, highlightbackground=BGCOLOR)
qb = ttk.Button(frame, text=_("Cancel"), style="bg.TButton", command=self.quit)
qb.pack(side=LEFT, padx=4)
self.bind("<Escape>", self.quit)
l, c = commandShortcut('q')
self.bind(c, self.quit)
self.bind("<Escape>", self.cancel)
# finish will evaluate the item entry and, if repeating, show reps
finish = ttk.Button(frame, text=FINISH, style="bg.TButton", command=self.onFinish)
finish.pack(side=RIGHT, padx=4)
self.bind("<Control-s>", self.onFinish)
# find
xb = ttk.Button(frame, text='\u2716', command=self.clearFind, style="bg.TButton", width=0)
xb.pack(side=LEFT, padx=0)
self.find_text = StringVar(frame)
self.e = Entry(frame, textvariable=self.find_text, width=10, highlightcolor=HLCOLOR, background=BGCOLOR, highlightbackground=BGCOLOR, highlightthickness=2, bd=2, foreground=FGCOLOR)
self.e.pack(side=LEFT, padx=2, expand=1, fill=X)
self.e.bind("<Return>", self.onFind)
nb = ttk.Button(frame, text='\u279c', command=self.onFind, style="bg.TButton", width=0)
nb.pack(side=LEFT, padx=0)
text = Text(self, wrap="word", bd=2, relief="sunken", padx=3, pady=2, font=self.tkfixedfont, undo=True, width=70)
text.configure(highlightthickness=0, background=BGCOLOR, foreground=FGCOLOR, insertbackground=FGCOLOR)
text.tag_configure(FOUND, background=FGCOLOR, foreground=BGCOLOR)
text.pack(side="bottom", padx=4, pady=3, expand=1, fill=BOTH)
self.text = text
self.completions = self.loop.options['completions']
if start is not None:
# we have the starting text but will need a new uid
text = start
if self.rephsh is None:
self.edithsh = {}
self.mode = 1
self.title = CREATENEW
else:
self.edithsh = self.rephsh
self.mode = 2
self.title = EDITEXISTING
elif file is not None:
# we're editing a file - if it's a data file we will add uid's
# as necessary when saving
self.mode = 'file'
if not os.path.isfile(file):
logger.warn('could not open: {0}'.format(file))
text = ""
else:
with codecs.open(file, 'r', self.options['encoding']['file']) as f:
text = f.read()
else:
# we are creating a new item and/or replacing an item
# mode:
# 1: new
# 2: replace
# 3: new and replace
initfile = ensureMonthly(options=self.options, date=datetime.now())
# set the mode
if newhsh is None and rephsh is None:
# we are creating a new item from scratch and will need
# a new uid
self.mode = 1
self.title = CREATENEW
self.edithsh = {}
self.edithsh['I'] = uniqueId()
text = ''
elif rephsh is None: # newhsh is not None
# we are creating a new item as a copy and will need
# a new uid
self.mode = 1
self.title = CREATENEW
self.edithsh = self.newhsh
self.edithsh['I'] = uniqueId()
if ('fileinfo' in newhsh and newhsh['fileinfo']):
initfile = newhsh['fileinfo'][0]
text, msg = hsh2str(self.edithsh, self.options)
elif newhsh is None:
# we are editing and replacing rephsh - no file prompt
# using existing uid
self.title = EDITEXISTING
self.mode = 2
# self.repinfo = rephsh['fileinfo']
self.edithsh = self.rephsh
text, msg = hsh2str(self.edithsh, self.options)
else: # neither is None
# we are changing some instances of a repeating item
# we will be writing but not editing rephsh using its fileinfo
# and its existing uid
# we will be editing and saving newhsh using self.initfile
# we will need a new uid for newhsh
self.mode = 3
self.title = CREATENEW
self.edithsh = self.newhsh
self.edithsh['I'] = uniqueId()
if 'fileinfo' in newhsh and newhsh['fileinfo'][0]:
initfile = self.newhsh['fileinfo'][0]
text, msg = hsh2str(self.edithsh, self.options)
self.initfile = initfile
logger.debug('mode: {0}; initfile: {1}; edit: {2}'.format(self.mode, self.initfile, self.edithsh))
if self.title is not None:
self.wm_title(self.title)
self.settext(text)
# clear the undo buffer
if not modified:
self.text.edit_reset()
self.setmodified(False)
self.text.bind('<<Modified>>', self.updateSaveStatus)
self.text.focus_set()
self.protocol("WM_DELETE_WINDOW", self.quit)
if parent:
self.geometry("+%d+%d" % (parent.winfo_rootx() + 50,
parent.winfo_rooty() + 50))
self.configure(background=BGCOLOR)
l, c = commandShortcut('f')
self.bind(c, lambda e: self.e.focus_set())
l, c = commandShortcut('g')
self.bind(c, lambda e: self.onFind())
if start:
# self.text.tag_add("sel", "1.1", "1.{0}".format(len(start)))
self.text.mark_set(INSERT, 0.0)
elif line:
self.text.mark_set(INSERT, "{0}.0".format(line))
else:
self.text.mark_set(INSERT, END)
self.text.see(INSERT)
# l, c = commandShortcut('/')
logger.debug("/: {0}, {1}".format(l, c))
self.text.bind("<Control-space>", self.showCompletions)
self.grab_set()
self.wait_window(self)
def settext(self, text=''):
self.text.delete('1.0', END)
self.text.insert(INSERT, text)
self.text.mark_set(INSERT, '1.0')
self.text.focus()
logger.debug("modified: {0}".format(self.checkmodified()))
def gettext(self):
return self.text.get('1.0', END + '-1c')
def setCompletions(self, *args):
match = self.filterValue.get()
self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())]
self.listbox.delete(0, END)
for item in matches:
self.listbox.insert(END, item)
self.listbox.select_set(0)
self.listbox.see(0)
self.fltr.focus_set()
def showCompletions(self, e=None):
if not self.completions:
return "break"
if self.autocompletewindow:
return "break"
line = self.text.get("insert linestart", INSERT)
m = completion_regex.search(line)
if not m:
logger.debug("no match in {0}".format(line))
return "break"
# set self.match here since it determines the characters to be replaced
self.match = match = m.groups()[0]
logger.debug("found match '{0}' in line '{1}'".format(match, line))
self.autocompletewindow = acw = Toplevel(master=self.text)
acw.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50))
self.autocompletewindow.wm_attributes("-topmost", 1)
self.filterValue = StringVar(self)
self.filterValue.set(match)
self.filterValue.trace_variable("w", self.setCompletions)
self.fltr = Entry(acw, textvariable=self.filterValue)
self.fltr.pack(side="top", fill="x")
self.fltr.icursor(END)
self.listbox = listbox = Listbox(acw, exportselection=False, width=self.loop.options['completions_width'])
listbox.pack(side="bottom", fill=BOTH, expand=True)
self.autocompletewindow.bind("<Double-1>", self.completionSelected)
self.autocompletewindow.bind("<Return>", self.completionSelected)
self.autocompletewindow.bind("<Escape>", self.hideCompletions)
self.autocompletewindow.bind("<Up>", self.cursorUp)
self.autocompletewindow.bind("<Down>", self.cursorDown)
self.fltr.bind("<Up>", self.cursorUp)
self.fltr.bind("<Down>", self.cursorDown)
self.setCompletions()
def is_active(self):
return self.autocompletewindow is not None
def hideCompletions(self, e=None):
if not self.is_active():
return
# destroy widgets
self.listbox.destroy()
self.listbox = None
self.autocompletewindow.destroy()
self.autocompletewindow = None
def completionSelected(self, event):
# Put the selected completion in the text, and close the list
modified = False
if self.matches:
cursel = self.matches[int(self.listbox.curselection()[0])]
else:
cursel = self.filterValue.get()
modified = True
start = "insert-{0}c".format(len(self.match))
end = "insert-1c wordend"
logger.debug("cursel: {0}; match: {1}; start: {2}; insert: {3}".format(
cursel, self.match, start, INSERT))
self.text.delete(start, end)
self.text.insert(INSERT, cursel)
self.hideCompletions()
if modified:
file = FileChoice(self, "append completion to file", prefix=self.loop.options['etmdir'], list=self.loop.options['completion_files']).returnValue()
if (file and os.path.isfile(file)):
with codecs.open(file, 'r', self.loop.options['encoding']['file']) as fo:
lines = fo.readlines()
lines.append(cursel)
lines.sort()
content = "\n".join([x.strip() for x in lines if x.strip()])
with codecs.open(file, 'w', self.loop.options['encoding']['file']) as fo:
fo.write(content)
self.completions.append(cursel)
self.completions.sort()
def cursorUp(self, event=None):
cursel = int(self.listbox.curselection()[0])
# newsel = max(0, cursel=1)
newsel = max(0, cursel - 1)
self.listbox.select_clear(cursel)
self.listbox.select_set(newsel)
self.listbox.see(newsel)
return "break"
def cursorDown(self, event=None):
cursel = int(self.listbox.curselection()[0])
newsel = min(len(self.matches) - 1, cursel + 1)
self.listbox.select_clear(cursel)
self.listbox.select_set(newsel)
self.listbox.see(newsel)
return "break"
def setmodified(self, bool):
if bool is not None:
self.text.edit_modified(bool)
def checkmodified(self):
return self.text.edit_modified()
def updateSaveStatus(self, event=None):
# Called by <<Modified>>
if self.checkmodified():
self.wm_title("{0} (modified)".format(self.title))
else:
self.wm_title("{0}".format(self.title))
def onFinish(self, e=None):
if self.mode == 'file':
self.onSave()
else:
self.onCheck()
def onSave(self, e=None, v=0):
if not self.checkmodified():
self.quit()
elif self.file is not None:
# we are editing a file
alltext = self.gettext()
self.loop.safe_save(self.file, alltext)
self.setmodified(False)
self.changed = True
self.quit()
else:
# we are editing an item
if self.mode in [1, 3]: # new
dir = self.options['datadir']
if 's' in self.edithsh and self.edithsh['s']:
dt = self.edithsh['s']
file = ensureMonthly(self.options, dt.date())
else:
dt = None
file = ensureMonthly(self.options)
dir, initfile = os.path.split(file)
# we need a filename for the new item
# make datadir the root
prefix, tuples = getFileTuples(self.options['datadir'], include=r'*.txt')
if v == 2:
filename = file
else:
ret = FileChoice(self, "etm data files", prefix=prefix, list=tuples, start=file).returnValue()
if not ret:
return False
filename = os.path.join(prefix, ret)
if not os.path.isfile(filename):
return False
filename = os.path.normpath(filename)
logger.debug('saving to: {0}'.format(filename))
self.text.focus_set()
logger.debug('edithsh: {0}'.format(self.edithsh))
if self.mode == 1:
if self.loop.append_item(self.edithsh, filename):
logger.debug('append mode: {0}'.format(self.mode))
elif self.mode == 2:
if self.loop.replace_item(self.edithsh):
logger.debug('replace mode: {0}'.format(self.mode))
else: # self.mode == 3
if self.loop.append_item(self.edithsh, filename):
logger.debug('append mode: {0}'.format(self.mode))
if self.loop.replace_item(self.rephsh):
logger.debug('replace mode: {0}'.format(self.mode))
# update the return value so that when it is not null then modified
# is false and when modified is true then it is null
self.setmodified(False)
self.changed = True
self.quit()
return "break"
def onCheck(self, event=None, showreps=True, showres=True):
# only called when editing an item and finish is pressed
self.loop.messages = []
text = self.gettext()
msg = []
reps = []
if text.startswith("BEGIN:VCALENDAR"):
text = import_ical(vcal=text)
logger.debug("text: {0} '{01}'".format(type(text), text))
if self.edithsh and 'i' in self.edithsh:
uid = self.edithsh['i']
else:
uid = None
hsh, msg = str2hsh(text, options=self.options, uid=uid)
logger.debug('itemtype: "{0}"'.format(hsh.get('itemtype', 'none')))
if not msg:
# we have a good hsh
pre = post = warn = ""
if 'r' in hsh:
pre = _("Repeating ")
elif 's' in hsh:
dt = hsh['s']
if hsh['itemtype'] in ['*', '~']:
if self.options['early_hour'] and dt.hour < self.options['early_hour']:
warn = _("Is {0} the starting time you intended?".format(fmt_time(dt, options=self.options)))
dtfmt = fmt_shortdatetime(hsh['s'], self.options)
else:
if not dt.hour and not dt.minute:
dtfmt = fmt_date(dt, short=True)
else:
dtfmt = fmt_shortdatetime(hsh['s'], self.options)
post = _(" starting {0}.").format(dtfmt)
else: # unscheduled
pre = _("Unscheduled ")
prompt = "{0}{1}{2}".format(pre, type2Text[hsh['itemtype']], post)
if warn:
prompt = prompt + "\n\n" + warn
if self.edithsh and 'fileinfo' in self.edithsh:
fileinfo = self.edithsh['fileinfo']
self.edithsh = hsh
self.edithsh['fileinfo'] = fileinfo
else:
# we have a new item without fileinfo
self.edithsh = hsh
# update missing fields
logger.debug('calling hsh2str with {0}'.format(hsh))
str, msg = hsh2str(hsh, options=self.options)
self.loop.messages.extend(msg)
if self.loop.messages:
messages = "{0}".format("\n".join(self.loop.messages))
logger.debug("messages: {0}".format(messages))
self.messageWindow(MESSAGES, messages, opts=self.options)
return False
logger.debug("back from hsh2str with: {0}".format(str))
if 'r' in hsh:
showing_all, reps = get_reps(self.loop.options['bef'], hsh)
if reps:
if showreps:
try:
repsfmt = [unicode(x.strftime(rrulefmt)) for x in reps]
except:
repsfmt = [unicode(x.strftime("%X %x")) for x in reps]
logger.debug("{0}: {1}".format(showing_all, repsfmt))
if showing_all:
reps = ALLREPS
else:
reps = SOMEREPS
prompt = "{0}, {1}:\n\n {2}".format(prompt, reps, "\n ".join(repsfmt))
else:
warn = "No repetitions were generated.\nConsider removing the @r entry."
prompt = prompt + "\n\n" + warn
if self.loop.messages:
messages = "{0}".format("\n".join(self.loop.messages))
logger.debug("messages: {0}".format(messages))
self.messageWindow(MESSAGES, messages, opts=self.options)
return False
if self.checkmodified():
prompt += "\n\n{0}".format(SAVEANDEXIT)
else:
prompt += "\n\n{0}".format(UNCHANGEDEXIT)
if str != text:
self.settext(str)
ans, value = OptionsDialog(parent=self, title=self.title, prompt=prompt, yesno=False, list=True).getValue()
if ans:
self.onSave(v=value)
return
def clearFind(self, *args):
self.text.tag_remove(FOUND, "0.0", END)
self.find_text.set("")
def onFind(self, *args):
target = self.find_text.get()
logger.debug('target: {0}'.format(target))
if target:
where = self.text.search(target, INSERT, nocase=1)
if where:
pastit = where + ('+%dc' % len(target))
self.text.tag_add(FOUND, where, pastit)
self.text.mark_set(INSERT, pastit)
self.text.see(INSERT)
self.text.focus()
def cancel(self, e=None):
t = self.find_text.get()
if t.strip():
self.clearFind()
return "break"
if self.autocompletewindow:
self.hideCompletions()
return "break"
if self.text.tag_ranges("sel"):
self.text.tag_remove('sel', "1.0", END)
return
logger.debug(('calling quit'))
self.quit()
def quit(self, e=None):
if self.checkmodified():
ans = self.confirm(parent=self, title=_('Quit'), prompt=_("There are unsaved changes.\nDo you really want to quit?"))
else:
ans = True
if ans:
if self.master:
logger.debug('setting focus')
self.master.focus()
self.master.focus_set()
logger.debug('focus set')
self.destroy()
logger.debug('done')
def messageWindow(self, title, prompt, opts=None, height=8, width=52):
win = Toplevel(self,
highlightcolor=self.HLCOLOR,
background=self.BGCOLOR,
highlightbackground=self.BGCOLOR)
win.title(title)
win.geometry("+%d+%d" % (self.text.winfo_rootx() + 50, self.text.winfo_rooty() + 50))
f = Frame(win)
# pack the button first so that it doesn't disappear with resizing
b = ttk.Button(win, text=_('OK'),style="bg.TButton", command=win.destroy)
b.pack(side='bottom', fill=tkinter.NONE, expand=0, pady=0)
win.bind('<Return>', (lambda e, b=b: b.invoke()))
win.bind('<KP_Enter>', (lambda e, b=b: b.invoke()))
win.bind('<Escape>', (lambda e, b=b: b.invoke()))
tkfixedfont = tkFont.nametofont("TkFixedFont")
if 'fontsize_fixed' in self.loop.options and self.loop.options['fontsize_fixed']:
tkfixedfont.configure(size=self.loop.options['fontsize_fixed'])
t = ReadOnlyText(
f, wrap="word", padx=2, pady=2, bd=2, relief="sunken",
font=tkfixedfont,
height=height,
background=self.BGCOLOR,
highlightcolor=self.HLCOLOR,
foreground=self.FGCOLOR,
width=width,
takefocus=False)
t.insert("0.0", prompt)
t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0)
if height > 1:
ysb = ttk.Scrollbar(f, orient='vertical', command=t.yview)
ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0)
t.configure(state="disabled", yscroll=ysb.set)
t.configure(yscroll=ysb.set)
f.pack(padx=2, pady=2, fill=tkinter.BOTH, expand=1)
win.focus_set()
win.grab_set()
win.transient(self)
win.wait_window(win)
def confirm(self, parent=None, title="", prompt="", instance="xyz"):
ok, value = OptionsDialog(parent=parent, title=_("confirm").format(instance), prompt=prompt).getValue()
return ok
class OriginalCommand:
def __init__(self, redir, operation):
self.redir = redir
self.operation = operation
self.tk = redir.tk
self.orig = redir.orig
self.tk_call = self.tk.call
self.orig_and_operation = (self.orig, self.operation)
def __repr__(self):
return "OriginalCommand(%r, %r)" % (self.redir, self.operation)
def __call__(self, *args):
return self.tk_call(self.orig_and_operation + args)
########################################################
# WidgetRedirector and OriginalCommand are from idlelib
########################################################
class WidgetRedirector:
"""Support for redirecting arbitrary widget subcommands.
Some Tk operations don't normally pass through Tkinter. For example, if a
character is inserted into a Text widget by pressing a key, a default Tk
binding to the widget's 'insert' operation is activated, and the Tk library
processes the insert without calling back into Tkinter.
Although a binding to <Key> could be made via Tkinter, what we really want
to do is to hook the Tk 'insert' operation itself.
When a widget is instantiated, a Tcl command is created whose name is the
same as the pathname widget._w. This command is used to invoke the various
widget operations, e.g. insert (for a Text widget). We are going to hook
this command and provide a facility ('register') to intercept the widget
operation.
In IDLE, the function being registered provides access to the top of a
Percolator chain. At the bottom of the chain is a call to the original
Tk widget operation.
"""
def __init__(self, widget):
self._operations = {}
self.widget = widget # widget instance
self.tk = tk = widget.tk # widget's root
w = widget._w # widget's (full) Tk pathname
self.orig = w + "_orig"
# Rename the Tcl command within Tcl:
tk.call("rename", w, self.orig)
# Create a new Tcl command whose name is the widget's pathname, and
# whose action is to dispatch on the operation passed to the widget:
tk.createcommand(w, self.dispatch)
def __repr__(self):
return "WidgetRedirector(%s<%s>)" % (self.widget.__class__.__name__,
self.widget._w)
def close(self):
for operation in list(self._operations):
self.unregister(operation)
widget = self.widget
del self.widget
orig = self.orig
del self.orig
tk = widget.tk
w = widget._w
tk.deletecommand(w)
# restore the original widget Tcl command:
tk.call("rename", orig, w)
def register(self, operation, function):
self._operations[operation] = function
setattr(self.widget, operation, function)
return OriginalCommand(self, operation)
def unregister(self, operation):
if operation in self._operations:
function = self._operations[operation]
del self._operations[operation]
if hasattr(self.widget, operation):
delattr(self.widget, operation)
return function
else:
return None
def dispatch(self, operation, *args):
'''Callback from Tcl which runs when the widget is referenced.
If an operation has been registered in self._operations, apply the
associated function to the args passed into Tcl. Otherwise, pass the
operation through to Tk via the original Tcl function.
Note that if a registered function is called, the operation is not
passed through to Tk. Apply the function returned by self.register()
to *args to accomplish that. For an example, see ColorDelegator.py.
'''
m = self._operations.get(operation)
try:
if m:
return m(*args)
else:
return self.tk.call((self.orig, operation) + args)
except TclError:
return ""
class Node:
def __init__(self, name, identifier=None, expanded=True):
self.__identifier = (uuid.uuid1()) if identifier is None else sanitize_id(s2or3(identifier))
self.name = name
self.expanded = expanded
self.__bpointer = None
self.__fpointer = []
@property
def identifier(self):
return self.__identifier
@property
def fpointer(self):
return self.__fpointer
def update_fpointer(self, identifier, mode=_ADD):
if mode is _ADD:
self.__fpointer.append(sanitize_id(identifier))
elif mode is _DELETE:
self.__fpointer.remove(sanitize_id(identifier))
elif mode is _INSERT:
self.__fpointer = [sanitize_id(identifier)]
class MenuTree:
"""
Used for the shortcuts menu
"""
def __init__(self):
self.nodes = []
self.lst = []
def get_index(self, position):
for index, node in enumerate(self.nodes):
if node.identifier == position:
break
return index
def create_node(self, name, identifier=None, parent=None):
# logger.debug("name: {0}, identifier: {1}; parent: {2}".format(name, identifier, parent))
node = Node(name, identifier)
self.nodes.append(node)
self.__update_fpointer(parent, node.identifier, _ADD)
node.bpointer = parent
return node
def showMenu(self, position, level=_ROOT):
queue = self[position].fpointer
if level == _ROOT:
self.lst = []
else:
name, key = self[position].name.split("::")
name = "{0}{1}".format(" " * (level - 1), name.strip())
s = "{0:<48} {1:^12}".format(name, key.strip())
self.lst.append(s)
logger.debug("position: {0}, level: {1}, name: {2}, key: {3}".format(position, level, name, key))
if self[position].expanded:
level += 1
for element in queue:
self.showMenu(element, level) # recursive call
return "\n".join(self.lst)
def __update_fpointer(self, position, identifier, mode):
if position is None:
return
else:
self[position].update_fpointer(identifier, mode)
def __getitem__(self, key):
return self.nodes[self.get_index(key)]
class Timer():
def __init__(self, parent=None, options={}):
"""
Methods providing timers
"""
self.parent = parent
self.options = options
self.loop = parent.loop
self.idleactive = False
self.showIdle = self.loop.options['display_idletime']
BGCOLOR = self.loop.options['background_color']
HLCOLOR = self.loop.options['highlight_color']
FGCOLOR = self.loop.options['foreground_color']
self.BGCOLOR = BGCOLOR
self.HLCOLOR = HLCOLOR
self.FGCOLOR = FGCOLOR
self.timermenu = parent.timermenu
self.match = ""
self.etmtimers = os.path.normpath(os.path.join(options['etmdir'], ".etmtimers"))
self.dfile_encoding = options['encoding']['file']
self.resetTimers()
def updateMenu(self, e=None):
if self.activeTimers:
self.timermenu.entryconfig(1, state="active")
if self.currentTimer:
self.timermenu.entryconfig(2, state="active")
else:
self.timermenu.entryconfig(2, state="disabled")
self.timermenu.entryconfig(3, state="active")
self.timermenu.entryconfig(4, state="active")
self.timermenu.entryconfig(5, state="active")
self.timermenu.entryconfig(6, state="active")
elif self.idleactive:
self.timermenu.entryconfig(1, state="disabled")
self.timermenu.entryconfig(2, state="disabled")
self.timermenu.entryconfig(3, state="disabled")
self.timermenu.entryconfig(4, state="active")
self.timermenu.entryconfig(5, state="active")
self.timermenu.entryconfig(6, state="active")
else:
self.timermenu.entryconfig(1, state="disabled")
self.timermenu.entryconfig(2, state="disabled")
self.timermenu.entryconfig(3, state="disabled")
self.timermenu.entryconfig(4, state="disabled")
self.timermenu.entryconfig(5, state="disabled")
self.timermenu.entryconfig(6, state="disabled")
def resetTimers(self):
try:
self.loadTimers()
logger.info("reloaded saved timer data")
except:
self.activeDate = datetime.now().date()
self.activeTimers = {} # summary -> { total, start, stop }
self.currentTimer = None # summary
self.currentStatus = STOPPED
self.currentMinutes = 0
self.idletime = 0 * ONEMINUTE
self.idlestart = None
logger.info("reset timer data")
def clearIdle(self, e=None):
# reset idle
self.idletime = 0 * ONEMINUTE
if self.activeTimers:
if self.currentStatus == RUNNING:
self.idlestart = None
else:
self.idlestart = datetime.now()
else:
self.idlestart = None
self.idleactive = False
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def toggleIdle(self, e=None):
if not self.idleactive:
return
self.showIdle = not self.showIdle
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def selectTimer(self, e=None, new=True, title=None, name=None):
"""
Combo box with list of active timer summaries and option to create a new, unique summary.
"""
self.selected = None
self.new = new
now = datetime.now()
if not self.activeTimers:
if not new:
return False
self.completions = []
if title is None:
title = _("Create Timer")
else:
if title is None:
title = _("Create or Choose Timer")
self.completions = []
tmp = [(self.activeTimers[x]['stop'], x) for x in self.activeTimers]
# put the most recently stopped timers at the top
sort = sorted(tmp, reverse=True)
self.completions = []
for x in sort:
timer = x[1]
h = self.activeTimers[timer]
if timer == self.currentTimer and self.currentStatus == RUNNING:
t = fmt_period(h['total'] + now - h['start'])
s = '\u279c'
else:
t = fmt_period(h['total'])
s = '\u2716'
self.completions.append(" {0} {1} {2} {3} ".format(timer, '\u25aa', t, s))
# return the focus to the right place
if self.parent.weekly or self.parent.monthly:
master = self.parent.canvas
else:
master = self.parent.tree
master=self.parent
self.timerswindow = win = Toplevel(master=master, highlightcolor=self.HLCOLOR, background=self.BGCOLOR, highlightbackground=self.BGCOLOR, bd=4)
self.timerswindow.title(title)
self.filterValue = StringVar(master)
self.timerswindow.geometry("+%d+%d" % (master.winfo_rootx() + 50, master.winfo_rooty() + 50))
self.timerswindow.minsize(240, 30)
self.timerswindow.wm_attributes("-topmost", 1)
self.filterValue = StringVar(master)
self.filterValue.set("")
self.filterValue.trace_variable("w", self.setCompletions)
self.fltr = Entry(self.timerswindow,
highlightcolor=self.HLCOLOR,
highlightbackground=self.BGCOLOR,
background=self.BGCOLOR,
foreground=self.FGCOLOR,
highlightthickness=0,
selectbackground=self.FGCOLOR,
selectforeground=self.BGCOLOR,
textvariable=self.filterValue)
self.fltr.pack(side="top", fill="x")
self.fltr.icursor(END)
self.listbox = listbox = Listbox(self.timerswindow,
highlightcolor=self.HLCOLOR,
highlightbackground=self.BGCOLOR,
background=self.BGCOLOR,
foreground=self.FGCOLOR,
selectbackground=self.FGCOLOR,
selectforeground=self.BGCOLOR,
highlightthickness=0,
exportselection=False)
listbox.pack(side="bottom", fill=BOTH, expand=True)
self.timerswindow.bind("<Double-1>", self.completionSelected)
self.timerswindow.bind("<Return>", self.completionSelected)
self.timerswindow.bind("<Escape>", self.hideCompletions)
self.timerswindow.bind("<Up>", self.cursorUp)
self.timerswindow.bind("<Down>", self.cursorDown)
self.fltr.bind("<Up>", self.cursorUp)
self.fltr.bind("<Down>", self.cursorDown)
if name is not None:
self.filterValue.set(name)
self.setCompletions()
win.wait_window(win)
def setCompletions(self, *args):
match = self.filterValue.get()
self.matches = matches = [x for x in self.completions if x and x.lower().startswith(match.lower())]
self.listbox.delete(0, END)
for item in matches:
self.listbox.insert(END, item)
self.listbox.select_set(0)
self.listbox.see(0)
self.fltr.focus_set()
def is_active(self):
return self.timerswindow is not None
def hideCompletions(self, e=None):
# destroy widgets
if not self.is_active():
return
self.fltr.destroy()
self.fltr = None
self.listbox.destroy()
self.listbox = None
self.timerswindow.destroy()
self.timerswindow = None
def completionSelected(self, event):
# Put the selected completion in the text, and close the list
cursel = None
if self.matches:
cursel = self.matches[int(self.listbox.curselection()[0])]
else:
tmp = self.filterValue.get()
if tmp in self.activeTimers or self.new:
cursel = tmp
logger.debug("cursel: {0}; match: {1}".format(cursel, self.match))
self.hideCompletions(e=event)
if cursel is not None:
self.selected = cursel.split('\u25aa')[0].strip()
if self.new:
self.startTimer()
def cursorUp(self, event=None):
cursel = int(self.listbox.curselection()[0])
# newsel = max(0, cursel=1)
newsel = max(0, cursel - 1)
self.listbox.select_clear(cursel)
self.listbox.select_set(newsel)
self.listbox.see(newsel)
return "break"
def cursorDown(self, event=None):
cursel = int(self.listbox.curselection()[0])
newsel = min(len(self.matches) - 1, cursel + 1)
self.listbox.select_clear(cursel)
self.listbox.select_set(newsel)
self.listbox.see(newsel)
return "break"
def saveTimers(self):
"""
dump activeTimers, ...
"""
tmp = (
self.activeDate,
self.activeTimers,
self.currentTimer,
self.currentStatus,
self.currentMinutes,
self.idlestart,
self.idletime
)
fo = codecs.open(self.etmtimers, 'w', self.dfile_encoding)
yaml.dump(tmp, fo)
fo.close()
self.updateMenu()
def loadTimers(self):
"""
load activeTimers
"""
fo = codecs.open(self.etmtimers, 'r', self.dfile_encoding)
tmp = yaml.load(fo)
fo.close()
(self.activeDate, self.activeTimers, self.currentTimer, self.currentStatus, self.currentMinutes, self.idlestart, self.idletime) = tmp
if self.idlestart or self.idletime:
self.idleactive = True
if self.activeDate != datetime.now().date():
self.newDay()
self.updateMenu()
def startTimer(self, e=None):
self.pauseTimer()
self.idleactive = True
if not self.selected:
return
if self.currentStatus == PAUSED:
if self.idlestart:
self.idletime += datetime.now() - self.idlestart
self.idlestart = None
summary = self.selected
if summary not in self.activeTimers:
# new timer
hsh = {}
hsh['total'] = 0 * ONEMINUTE
hsh['stop'] = datetime.now()
else:
hsh = self.activeTimers[summary]
hsh['start'] = datetime.now()
self.activeTimers[summary] = hsh
self.currentTimer = summary
self.currentStatus = RUNNING
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def finishTimer(self, e=None):
self.pauseTimer()
self.selectTimer(new=False, title="Finish Timer")
if not self.selected:
return
self.currentStatus = STOPPED
self.currentTimer = None
hsh = self.activeTimers[self.selected]
hsh['summary'] = self.selected
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
return hsh
def deleteTimer(self, e=None, timer=None):
if timer is None:
self.selectTimer(new=False, title=_("Delete Timer"))
timer = self.selected
if not timer or timer not in self.activeTimers:
return
self.pauseTimer()
if self.currentTimer == timer:
self.currentTimer = None
self.currentMinutes = 0
self.currentStatus = STOPPED
if self.idlestart:
idle = (datetime.now() - self.idlestart) + self.idletime
elif self.idletime:
idle = self.idletime
else:
idle = 0 * ONEMINUTE
self.idletime = idle + self.activeTimers[timer]['total']
del self.activeTimers[timer]
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def newDay(self, e=None):
now = datetime.now()
self.activeDate = now.date()
self.idlestart = None
self.idletime = 0 * ONEMINUTE
if not self.activeTimers:
self.idleactive = False
self.activeTimers = {} # summary -> { total, start, stop }
self.currentTimer = None # summary
self.currentStatus = STOPPED
self.currentMinutes = 0
running = (self.currentTimer and self.currentStatus == RUNNING)
curfile = ensureMonthly(self.options, date=now.date())
tmp = []
for timer in self.activeTimers:
# create inbox entries
thsh = self.activeTimers[timer]
hsh = {"itemtype": "$", "_summary": timer, "s": thsh['start'], "e": thsh['total']}
tmp.append([timer, hsh])
for timer, hsh in tmp:
res = self.loop.append_item(hsh, curfile)
if res:
del self.activeTimers[timer]
if running:
# currentStatus == RUNNING
hsh = {}
hsh['total'] = 0 * ONEMINUTE
hsh['start'] = hsh['stop'] = now
self.activeTimers[self.currentTimer] = hsh
else:
self.currentTimer = None
self.currentStatus = STOPPED
self.currentMinutes = 0
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def toggleCurrent(self, e=None):
"""
"""
if not self.activeTimers or not self.currentTimer:
return
hsh = self.activeTimers[self.currentTimer]
if self.currentStatus == RUNNING:
hsh['total'] += datetime.now() - hsh['start']
hsh['stop'] = datetime.now()
self.idlestart = datetime.now()
self.currentStatus = PAUSED
elif self.currentStatus == PAUSED:
hsh['start'] = datetime.now()
if self.idlestart:
self.idletime += datetime.now() - self.idlestart
self.idlestart = None
self.currentStatus = RUNNING
self.activeTimers[self.currentTimer] = hsh
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
def pauseTimer(self):
"""
Pause the running timer
"""
if self.activeTimers and self.currentTimer and self.currentStatus == RUNNING:
self.toggleCurrent()
self.saveTimers()
if self.parent:
self.parent.updateTimerStatus()
self.parent.update_idletasks()
return False
def getStatus(self):
"""
Return the status of the timers for the status bar
"""
if not self.activeTimers and not self.idlestart and not self.idletime:
return "", ""
idlestatus = ""
now = datetime.now()
if self.currentTimer and self.currentStatus:
hsh = self.activeTimers[self.currentTimer]
if self.currentStatus == RUNNING:
status='\u279c'
idlestatus='\u2716'
hsh['total'] = hsh['total'] + (now - hsh['start'])
hsh['start'] = now
else:
status='\u2716'
idlestatus='\u279c'
ret1 = "{0}: {1} {2}".format(self.currentTimer, fmt_period(hsh['total']), status)
total = hsh['total']
self.currentMinutes = total.seconds // 60
elif self.activeTimers:
ret1 = "{0}".format(_("all paused"))
else:
ret1 = ""
if self.showIdle:
if self.idlestart:
idle = (now - self.idlestart) + self.idletime
elif self.idletime:
idle = self.idletime
else:
idle = 0 * ONEMINUTE
ret2 = "{0}: {1} {2}".format(_("idle"), fmt_period(idle), idlestatus)
else:
ret2 = ""
logger.debug("timer: {0} {1}".format(ret1, ret2))
return ret1, ret2
class ReadOnlyText(Text):
# noinspection PyShadowingNames
def __init__(self, *args, **kwargs):
Text.__init__(self, *args, **kwargs)
bg = kwargs.pop('background', None)
self.redirector = WidgetRedirector(self)
self.insert = self.redirector.register("insert", lambda *args, **kw: "break")
self.delete = self.redirector.register("delete", lambda *args, **kw: "break")
self.configure(highlightthickness=0, insertwidth=0, takefocus=0, wrap="word", background=bg)
class MessageWindow():
# noinspection PyShadowingNames
def __init__(self, parent, title, prompt, opts={}):
self.loop = parent.loop
BGCOLOR = self.loop.options['background_color']
HLCOLOR = self.loop.options['highlight_color']
FGCOLOR = self.loop.options['foreground_color']
self.win = Toplevel(parent, highlightcolor=HLCOLOR, background=BGCOLOR)
self.win.protocol("WM_DELETE_WINDOW", self.cancel)
self.parent = parent
self.options = opts
self.win.title(title)
tkfixedfont = tkFont.nametofont("TkFixedFont")
if 'fontsize_fixed' in self.options and self.options['fontsize_fixed']:
tkfixedfont.configure(size=self.options['fontsize_fixed'])
self.content = ReadOnlyText(self.win, wrap="word", padx=3, bd=2, height=10, relief="sunken", font=tkfixedfont, width=46, takefocus=False, background=BGCOLOR, highlightcolor=HLCOLOR, foreground=FGCOLOR)
self.content.pack(fill=tkinter.BOTH, expand=1, padx=10, pady=10)
self.content.insert("1.0", prompt)
b = ttk.Button(self.win, text=_("OK"), style="bg.TButton", command=self.cancel)
# b = Button(self.win, text=_('OK'), width=10, command=self.cancel, default='active', pady=2)
b.pack()
self.win.bind('<Return>', (lambda e, b=b: b.invoke()))
self.win.bind('<Escape>', (lambda e, b=b: b.invoke()))
self.win.focus_set()
self.win.grab_set()
self.win.transient(parent)
self.win.wait_window(self.win)
return
def cancel(self, event=None):
# put focus back to the parent window
self.parent.focus_set()
self.win.destroy()
class FileChoice(object):
def __init__(self, parent, title=None, prefix=None, list=[], start='', ext="txt", new=False):
self.parent = parent
self.loop = parent.loop
BGCOLOR = self.loop.options['background_color']
self.BGCOLOR = BGCOLOR
HLCOLOR = self.loop.options['highlight_color']
self.HLCOLOR = HLCOLOR
FGCOLOR = self.loop.options['foreground_color']
self.FGCOLOR = FGCOLOR
self.value = None
self.prefix = prefix
self.list = list
if prefix and start:
self.start = relpath(start, prefix)
else:
self.start = start
self.ext = ext
self.new = new
self.modalPane = Toplevel(self.parent, highlightcolor=HLCOLOR, background=BGCOLOR)
logger.debug('winfo: {0}, {1}; {2}, {3}'.format(parent.winfo_rootx(), type(parent.winfo_rootx()), parent.winfo_rooty(), type(parent.winfo_rooty())))
self.modalPane.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
self.modalPane.transient(self.parent)
self.modalPane.grab_set()
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
if title:
self.modalPane.title(title)
if new:
nameFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR)
nameFrame.pack(side="top", padx=18, pady=2, fill="x")
nameLabel = Label(nameFrame, text=_("file:"), bd=1, relief="flat", anchor="w", padx=0, pady=0, highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR)
nameLabel.pack(side="left")
self.fileName = StringVar(self.modalPane)
self.fileName.set("untitled.{0}".format(ext))
self.fileName.trace_variable("w", self.onSelect)
self.fname = Entry(nameFrame, textvariable=self.fileName, bd=1, highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR)
self.fname.pack(side="left", fill="x", expand=1, padx=0, pady=0)
self.fname.icursor(END)
self.fname.bind("<Up>", self.cursorUp)
self.fname.bind("<Down>", self.cursorDown)
filterFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR)
filterFrame.pack(side="top", padx=18, pady=4, fill="x")
filterLabel = Label(filterFrame, text=_("filter:"), bd=1, relief="flat", anchor="w", padx=0, pady=0, highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR)
filterLabel.pack(side="left")
self.filterValue = StringVar(self.modalPane)
self.filterValue.set("")
self.filterValue.trace_variable("w", self.setMatching)
self.fltr = Entry(filterFrame, textvariable=self.filterValue, bd=1, highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR)
self.fltr.pack(side="left", fill="x", expand=1, padx=0, pady=0)
self.fltr.icursor(END)
prefixFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR)
prefixFrame.pack(side="top", padx=8, pady=2, fill="x")
self.prefixLabel = Label(prefixFrame, text=_("{0}:").format(prefix), bd=1, highlightcolor=BGCOLOR, background=BGCOLOR, foreground=FGCOLOR)
self.prefixLabel.pack(side="left", expand=0, padx=0, pady=0)
buttonFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR)
buttonFrame.pack(side="bottom", padx=10, pady=2)
chooseButton = ttk.Button(buttonFrame, text="Choose", style="bg.TButton", command=self._choose)
chooseButton.pack(side="right", padx=10)
cancelButton = ttk.Button(buttonFrame, text="Cancel", style="bg.TButton", command=self._cancel, )
cancelButton.pack(side="left")
selectionFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR)
selectionFrame.pack(side="bottom", padx=8, pady=2, fill="x")
self.selectionValue = StringVar(self.modalPane)
self.selectionValue.set("")
self.selection = Label(selectionFrame, textvariable=self.selectionValue, bd=1, highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR)
self.selection.pack(side="left", fill="x", expand=1, padx=0, pady=0)
listFrame = Frame(self.modalPane, highlightcolor=HLCOLOR, background=BGCOLOR, width=40)
listFrame.pack(side="top", fill="both", expand=1, padx=5, pady=2)
scrollBar = Scrollbar(listFrame, width=8)
scrollBar.pack(side="right", fill="y")
self.listBox = Listbox(listFrame, selectmode=BROWSE, width=36, foreground=FGCOLOR, background=BGCOLOR, selectbackground=FGCOLOR, selectforeground=BGCOLOR)
self.listBox.pack(side="left", fill="both", expand=1, ipadx=4, padx=2, pady=0)
self.listBox.bind('<<ListboxSelect>>', self.onSelect)
self.listBox.bind("<Double-1>", self._choose)
self.modalPane.bind("<Return>", self._choose)
self.modalPane.bind("<Escape>", self._cancel)
# self.modalPane.bind("<Up>", self.cursorUp)
# self.modalPane.bind("<Down>", self.cursorDown)
self.fltr.bind("<Up>", self.cursorUp)
self.fltr.bind("<Down>", self.cursorDown)
scrollBar.config(command=self.listBox.yview)
self.listBox.config(yscrollcommand=scrollBar.set)
self.setMatching()
def ignore(self, e=None):
return "break"
def onSelect(self, *args):
# Note here that Tkinter passes an event object to onselect()
if self.listBox.curselection():
firstIndex = self.listBox.curselection()[0]
value = self.matches[int(firstIndex)]
r = value[1]
p = os.path.join(self.prefix, r)
if self.new:
if os.path.isfile(p):
p = os.path.split(p)[0]
r = os.path.split(r)[0]
f = self.fileName.get()
r = os.path.join(r, f)
p = os.path.join(p, f)
self.selectionValue.set(r)
self.value = p
return "break"
def cursorUp(self, event=None):
cursel = int(self.listBox.curselection()[0])
newsel = max(0, cursel - 1)
self.listBox.select_clear(cursel)
self.listBox.select_set(newsel)
self.listBox.see(newsel)
self.onSelect()
return "break"
def cursorDown(self, event=None):
cursel = int(self.listBox.curselection()[0])
newsel = min(len(self.list) - 1, cursel + 1)
self.listBox.select_clear(cursel)
self.listBox.select_set(newsel)
self.listBox.see(newsel)
self.onSelect()
return "break"
def setMatching(self, *args):
# disabled = "#BADEC3"
# disabled = "#91CC9E"
disabled = "#62B374"
match = self.filterValue.get()
if match:
self.matches = matches = [x for x in self.list if x and match.lower() in x[1].lower()]
else:
self.matches = matches = self.list
self.listBox.delete(0, END)
index = 0
init_index = 0
for item in matches:
if type(item) is tuple:
# only show the label
self.listBox.insert(END, item[0])
if self.new:
if not item[-1]:
self.listBox.itemconfig(index, fg=disabled)
else:
self.listBox.itemconfig(index, fg=self.FGCOLOR)
if self.start and item[1] == self.start:
init_index = index
else:
if item[-1]:
self.listBox.itemconfig(index, fg=disabled)
else:
self.listBox.itemconfig(index, fg=self.FGCOLOR)
if self.start and item[1] == self.start:
init_index = index
# elif files:
else:
self.listBox.insert(END, item)
index += 1
self.listBox.select_set(init_index)
self.listBox.see(init_index)
self.fltr.focus_set()
self.onSelect()
def _choose(self, event=None):
try:
if self.listBox.curselection():
firstIndex = self.listBox.curselection()[0]
if self.new:
if not self.value or os.path.isfile(self.value):
return
else:
tup = self.matches[int(firstIndex)]
if tup[-1]:
return
self.value = os.path.join(self.prefix, tup[1])
else:
return
except IndexError:
self.value = None
self.modalPane.destroy()
def _cancel(self, event=None):
self.value = None
self.modalPane.destroy()
def returnValue(self):
if self.parent is not None:
self.parent.wait_window(self.modalPane)
return self.value
class Dialog(Toplevel):
def __init__(self, parent, title=None, prompt=None, opts=None, default=None, modal=True, xoffset=50, yoffset=50, event=None, process=None, font=None, close=0):
# global BGCOLOR, HLCOLOR, FGCOLOR
self.parent = parent
self.loop = parent.loop
BGCOLOR = self.loop.options['background_color']
self.BGCOLOR = BGCOLOR
HLCOLOR = self.loop.options['highlight_color']
self.HLCOLOR = HLCOLOR
FGCOLOR = self.loop.options['foreground_color']
self.FGCOLOR = FGCOLOR
Toplevel.__init__(self, parent, highlightcolor=HLCOLOR, background=BGCOLOR)
self.protocol("WM_DELETE_WINDOW", self.quit)
if modal:
logger.debug('modal')
self.transient(parent)
else:
logger.debug('non modal')
if title:
self.title(title)
self.event = event
logger.debug("parent: {0}".format(self.parent))
self.prompt = prompt
self.options = opts
self.font = font
self.default = default
self.value = ""
self.process = process
self.error_message = None
# self.buttonbox()
body = Frame(self, highlightcolor=HLCOLOR, background=BGCOLOR)
# self.initial_focus = self.body(body)
self.body(body).focus_set()
self.buttonbox()
# don't expand body or it will fill below the actual content
body.pack(side="top", fill=tkinter.BOTH, padx=0, pady=0, expand=1)
self.protocol("WM_DELETE_WINDOW", self.quit)
if parent:
self.geometry("+%d+%d" % (parent.winfo_rootx() + xoffset, parent.winfo_rooty() + yoffset))
if close:
self.after(close, lambda: self.cancel())
if modal:
self.grab_set()
self.wait_window(self)
def body(self, master):
# create dialog body. return widget that should have
# initial focus. this method should be overridden
pass
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self, background=self.BGCOLOR, highlightcolor=self.HLCOLOR)
w = ttk.Button(box, text="OK", style="bg.TButton", command=self.ok, default=ACTIVE)
w.pack(side="right", padx=5, pady=2)
w = ttk.Button(box, text="Cancel", style="bg.TButton", command=self.cancel)
w.pack(side="right", padx=5, pady=2)
self.bind("<Return>", self.ok)
self.bind("<KP_Enter>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack(side='bottom')
# standard button semantics
def ok(self, event=None):
res = self.validate()
logger.debug('validate: {0}, value: "{1}"'.format(res, self.value))
if not res:
if self.error_message:
self.messageWindow('error', self.error_message)
# self.initial_focus.focus_set() # put focus back
return "break"
self.withdraw()
self.update_idletasks()
self.apply()
self.quit()
def cancel(self, event=None):
# return the focus to the tree view in the main window
self.value = None
logger.debug('value: "{0}"'.format(self.value))
self.quit()
def quit(self, event=None):
if self.parent:
logger.debug("returning focus to parent: {0}".format(self.parent))
self.parent.focus()
# self.parent.tree.focus_set()
if self.parent.weekly or self.parent.monthly:
self.parent.canvas.focus_set()
else:
self.parent.tree.focus_set()
else:
logger.debug("returning focus, no parent")
self.destroy()
# command hooks
def validate(self):
return 1 # override
def apply(self):
pass # override
def messageWindow(self, title, prompt):
MessageWindow(self.parent, title, prompt)
class TextVariableWindow(Dialog):
def body(self, master):
if 'textvariable' not in self.options:
return
self.entry = Entry(master, textvariable=self.options['textvariable'])
self.entry.pack(side="bottom", padx=5, pady=5)
Label(master, text=self.prompt, justify='left', highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR).pack(side="top", fill=tkinter.BOTH, expand=1, padx=10, pady=5)
self.entry.focus_set()
self.entry.bind('<Escape>', self.entry.delete(0, END))
return self.entry
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self, highlightcolor=self.HLCOLOR, background=self.BGCOLOR)
w = ttk.Button(box, text=CLOSE, style="bg.TButton", command=self.ok, default=ACTIVE)
w.pack(side=LEFT, padx=5, pady=5)
self.bind("<Return>", self.ok)
self.bind("<KP_Enter>", self.ok)
self.bind("<Escape>", self.ok)
box.pack(side='bottom')
def quit(self, event=None):
if self.parent:
logger.debug("returning focus to parent: {0}".format(self.parent))
self.parent.focus()
self.parent.focus_set()
else:
logger.debug("returning focus, no parent")
self.entry.delete(0, END)
self.options['textvariable'].set("")
self.destroy()
class DialogWindow(Dialog):
# master will be a frame in Dialog
def body(self, master):
self.entry = Entry(master,
highlightthickness=0,
highlightcolor=self.HLCOLOR,
highlightbackground=self.BGCOLOR,
selectbackground=self.FGCOLOR,
selectforeground=self.BGCOLOR,
background=self.BGCOLOR,
foreground=self.FGCOLOR)
self.entry.pack(side="bottom", padx=5, pady=2, fill=X)
tkfixedfont = self.font
lines = self.prompt.split('\n')
height = min(20, len(lines) + 1)
lengths = [len(line) for line in lines]
width = min(70, max(lengths) + 2)
self.text = ReadOnlyText(
master, wrap="word", padx=2, pady=2, bd=2, relief="sunken",
font=tkfixedfont,
height=height,
width=width,
background=self.BGCOLOR,
highlightcolor=self.HLCOLOR,
highlightbackground=self.BGCOLOR,
foreground=self.FGCOLOR,
takefocus=False)
self.text.insert("1.1", self.prompt)
self.text.pack(side="top", fill=tkinter.BOTH, expand=1, padx=6, pady=2)
if self.default is not None:
self.entry.insert(0, self.default)
self.entry.select_range(0, END)
return self.entry
class TextDialog(Dialog):
def body(self, master):
tkfixedfont = self.font
lines = self.prompt.split('\n')
height = min(25, len(lines) + 1)
lengths = [len(line) for line in lines]
width = min(70, max(lengths) + 2)
self.text = ReadOnlyText(
master, wrap="word", padx=2, pady=2, bd=2, relief="sunken",
# font=tkFont.Font(family="Lucida Sans Typewriter"),
font=tkfixedfont,
height=height,
width=width,
background=self.BGCOLOR,
highlightbackground=self.BGCOLOR,
highlightcolor=self.HLCOLOR,
foreground=self.FGCOLOR,
takefocus=False)
self.text.insert("1.1", self.prompt)
self.text.pack(side='left', fill=tkinter.BOTH, expand=1, padx=5, pady=2)
return self.text
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self, highlightcolor=self.HLCOLOR, background=self.BGCOLOR)
w = ttk.Button(box, text="OK", style="bg.TButton", command=self.cancel, default=ACTIVE)
w.pack(side=LEFT, padx=5, pady=0)
self.bind("<Return>", self.ok)
self.bind("<KP_Enter>", self.ok)
self.bind("<Escape>", self.ok)
box.pack(side='bottom')
class OptionsDialog():
def __init__(self, parent, master=None, title="", prompt="", opts=None, radio=True, yesno=True, list=False):
if not opts:
opts = []
self.parent = parent
self.loop = parent.loop
BGCOLOR = self.loop.options['background_color']
self.BGCOLOR = BGCOLOR
HLCOLOR = self.loop.options['highlight_color']
self.HLCOLOR = HLCOLOR
FGCOLOR = self.loop.options['foreground_color']
self.FGCOLOR = FGCOLOR
self.win = Toplevel(parent, background=BGCOLOR, highlightcolor=HLCOLOR)
self.win.protocol("WM_DELETE_WINDOW", self.quit)
if parent:
self.win.geometry("+%d+%d" % (parent.winfo_rootx() + 50, parent.winfo_rooty() + 50))
# self.parent = parent
self.master = master
self.options = opts
self.radio = radio
self.win.title(title)
if list:
self.win.configure(bg=BGCOLOR)
tkfixedfont = tkFont.nametofont("TkFixedFont")
if 'fontsize_fixed' in self.parent.options and self.parent.options['fontsize_fixed']:
tkfixedfont.configure(size=self.parent.options['fontsize_fixed'])
self.content = ReadOnlyText(self.win, wrap="word", padx=3, bd=2, height=10, relief="sunken", font=tkfixedfont, background=BGCOLOR, highlightcolor=HLCOLOR, foreground=FGCOLOR, width=46, takefocus=False)
self.content.pack(fill=tkinter.BOTH, expand=1, padx=10, pady=5)
self.content.insert("1.0", prompt)
else:
Label(self.win, text=prompt, justify='left', highlightcolor=HLCOLOR, background=BGCOLOR, foreground=FGCOLOR).pack(fill=tkinter.BOTH, expand=1, padx=10, pady=5)
self.sv = StringVar(parent)
self.sv = IntVar(parent)
self.sv.set(1)
if self.options:
if radio:
self.value = opts[0]
for i in range(min(9, len(self.options))):
txt = self.options[i]
val = i + 1
# bind keyboard numbers 1-9 (at most) to options selection, i.e., press 1 to select option 1, 2 to select 2, etc.
self.win.bind(str(val), (lambda e, x=val: self.sv.set(x)))
Radiobutton(self.win, text="{0}: {1}".format(val, txt), padx=20, indicatoron=True, variable=self.sv, background=BGCOLOR, highlightcolor=HLCOLOR, foreground=FGCOLOR, command=self.getValue, value=val).pack(padx=10, anchor=W)
else:
self.check_values = {}
# show 0, check 1, return 2
for i in range(min(9, len(self.options))):
txt = self.options[i][0]
self.check_values[i] = BooleanVar(self.parent)
self.check_values[i].set(self.options[i][1])
Checkbutton(self.win, text=self.options[i][0], padx=20, variable=self.check_values[i]).pack(padx=10, anchor=W)
box = Frame(self.win)
box.configure(bg=BGCOLOR, highlightcolor=HLCOLOR)
# if list:
# box.configure(bg=BGCOLOR)
if yesno:
YES = _("Yes")
NO = _("No")
else:
YES = _("Ok")
NO = _("Cancel")
c = ttk.Button(box, text=NO, style="bg.TButton", command=self.cancel)
c.pack(side=LEFT, padx=5, pady=5)
o = ttk.Button(box, text=YES, style="bg.TButton", default='active', command=self.ok)
o.pack(side=LEFT, padx=5, pady=5)
box.pack()
self.win.bind('<Return>', (lambda e, o=o: o.invoke()))
self.win.bind('<KP_Enter>', (lambda e, o=o: o.invoke()))
self.win.bind('<Control-w>', self.Ok)
self.win.bind('<Escape>', (lambda e, c=c: c.invoke()))
logger.debug('parent: {0}'.format(parent))
self.win.focus_set()
self.win.transient(parent)
self.win.wait_window(self.win)
def getValue(self, e=None):
v = self.sv.get()
logger.debug("sv: {0}".format(v))
if self.options:
if self.radio:
if v - 1 in range(len(self.options)):
o = self.options[v - 1]
logger.debug(
'OptionsDialog returning {0}: {1}'.format(v, o))
return v, o
# return o, v
else:
logger.debug(
'OptionsDialog returning {0}: {1}'.format(v, None))
return 0, None
else: # checkbutton
values = []
for i in range(len(self.options)):
bool = self.check_values[i].get() > 0
values.append([self.options[i][0], bool, self.options[i][2]])
return values
else: # askokcancel type dialog
logger.debug(
'OptionsDialog returning {0}: {1}'.format(v, None))
return v, v
def ok(self, event=None):
# self.parent.update_idletasks()
self.quit()
def Ok(self, event=None):
# self.parent.update_idletasks()
self.sv.set(2)
self.quit()
def cancel(self, event=None):
self.sv.set(0)
self.quit()
def quit(self, event=None):
# put focus back to the parent window
if self.master:
self.master.focus_set()
elif self.parent:
self.parent.focus_set()
self.win.destroy()
class GetInteger(DialogWindow):
def validate(self):
minvalue = maxvalue = None
if len(self.options) > 0:
minvalue = self.options[0]
if len(self.options) > 1:
maxvalue = self.options[1]
res = self.entry.get()
try:
val = int(res)
ok = (minvalue is None or val >= minvalue) and (
maxvalue is None or val <= maxvalue)
except:
val = None
ok = False
if ok:
self.value = val
return True
else:
self.value = None
msg = [_('an integer')]
conj = ""
if minvalue is not None:
msg.append(_("no less than {0}".format(minvalue)))
conj = _("and ")
if maxvalue is not None:
msg.append(_("{0}no greater than {1}").format(conj, maxvalue))
msg.append(_("is required"))
self.error_message = "\n".join(msg)
return False
class GetRepeat(GetInteger):
def buttonbox(self):
# add standard button box. override if you don't want the
# standard buttons
box = Frame(self, background=self.BGCOLOR, highlightcolor=self.HLCOLOR)
w = ttk.Button(box, text=_("Repeat"), style="bg.TButton", command=self.ok, default=ACTIVE)
w.pack(side="right", padx=5, pady=2)
w = ttk.Button(box, text=_("Close"), style="bg.TButton", command=self.cancel)
w.pack(side="right", padx=5, pady=2)
self.bind("<Return>", self.ok)
self.bind("<KP_Enter>", self.ok)
self.bind("<Escape>", self.cancel)
box.pack(side='bottom')
class GetDateTime(DialogWindow):
def validate(self):
res = self.entry.get()
logger.debug('res: {0}'.format(res))
ok = False
if not res.strip():
# return the current date if ok is pressed with no entry
# val = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0)
val = get_current_time()
ok = True
else:
try:
val = parse_str(res)
ok = True
except:
val = None
if ok:
self.value = val
return True
else:
self.value = False
self.error_message = _('could not parse "{0}"').format(res)
return False
class GetString(DialogWindow):
def validate(self):
nullok = False
if 'nullok' in self.options and self.options['nullok']:
nullok = True
# an entry is required
val = self.entry.get()
if val.strip():
self.value = val
return True
elif nullok: # null and null is ok
if self.value is None:
return False
else:
self.value = val
return True
else:
self.error_message = _('an entry is required')
return False
def ok(self, event=None):
res = self.validate()
logger.debug('validate: {0}, value: "{1}"'.format(res, self.value))
if not res:
if self.error_message:
self.messageWindow('error', self.error_message)
return "break"
if self.process:
res = self.process(self.value)
self.text.delete("1.0", END)
self.text.insert("1.0", res)
else:
self.withdraw()
self.update_idletasks()
self.apply()
self.quit()