"Fossies" - the Fresh Open Source Software Archive

Member "etmtk-3.2.31/etmTk/view.py" (30 Mar 2017, 170510 Bytes) of package /linux/privat/etmtk-3.2.31.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "view.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 3.2.27_vs_3.2.30.

A hint: This file contains one or more very long lines, so maybe it is better readable using the pure text view mode that shows the contents as wrapped lines within the browser window.


    1 #!/usr/bin/env python3
    2 # -*- coding: utf-8 -*-
    3 from __future__ import (absolute_import, division, print_function,
    4                         unicode_literals)
    5 
    6 import os
    7 # import sys
    8 import re
    9 import uuid
   10 from copy import deepcopy
   11 import subprocess
   12 from dateutil.tz import tzlocal
   13 import codecs
   14 
   15 import logging
   16 import logging.config
   17 
   18 logger = logging.getLogger()
   19 
   20 import platform
   21 
   22 if platform.python_version() >= '3':
   23     import tkinter
   24     from tkinter import (
   25         BOTH,
   26         Button,
   27         Canvas,
   28         CENTER,
   29         CURRENT,
   30         END,
   31         Entry,
   32         FLAT,
   33         Frame,
   34         INSERT,
   35         IntVar,
   36         Label,
   37         LEFT,
   38         Menu,
   39         OptionMenu,
   40         PanedWindow,
   41         PhotoImage,
   42         RAISED,
   43         Scrollbar,
   44         StringVar,
   45         Tk,
   46         TOP,
   47         Toplevel,
   48         W,
   49         X,
   50     )
   51     from tkinter import ttk
   52     from tkinter import font as tkFont
   53     utf8 = lambda x: x
   54     unicode = str
   55 
   56 else:
   57     import Tkinter as tkinter
   58     from Tkinter import (
   59         BOTH,
   60         Button,
   61         Canvas,
   62         CENTER,
   63         CURRENT,
   64         END,
   65         Entry,
   66         FLAT,
   67         Frame,
   68         INSERT,
   69         IntVar,
   70         Label,
   71         LEFT,
   72         Menu,
   73         OptionMenu,
   74         PanedWindow,
   75         PhotoImage,
   76         RAISED,
   77         Scrollbar,
   78         StringVar,
   79         Tk,
   80         TOP,
   81         Toplevel,
   82         W,
   83         X,
   84     )
   85     import ttk
   86     import tkFont
   87 
   88     def utf8(s):
   89         return s
   90 
   91 tkversion = tkinter.Tcl().eval('info patchlevel')
   92 
   93 import etmTk.data as data
   94 
   95 from dateutil.parser import parse
   96 
   97 from calendar import Calendar
   98 
   99 from decimal import Decimal
  100 
  101 from etmTk.data import (
  102     fmt_weekday, fmt_dt, fmt_date, str2hsh, hsh2str, tstr2SCI, leadingzero, relpath, s2or3, send_mail, send_text, get_changes, checkForNewerVersion, datetime2minutes, calyear, expand_template, id2Type, fmt_shortdatetime, get_reps, get_current_time, windoz, mac, setup_logging, gettz, commandShortcut, rrulefmt, tree2Text, date_calculator, AFTER, export_json, export_ical_item, export_ical_active, fmt_time, fmt_period, TimeIt, getReportData, getFileTuples, getAllFiles, updateCurrentFiles, availableDates, syncTxt, update_subscription)
  103 
  104 from etmTk.dialog import MenuTree, Timer, ReadOnlyText, MessageWindow, TextDialog, OptionsDialog, GetInteger, GetRepeat, GetDateTime, GetString, FileChoice, FINISH, STOPPED, PAUSED, RUNNING, ONEDAY, ONEMINUTE, SOMEREPS, ALLREPS, type2Text, SimpleEditor
  105 
  106 from datetime import datetime, time, date
  107 
  108 ETM = "etm"
  109 
  110 FILTER = _("filter")
  111 FILTERCOLOR = "gray"
  112 
  113 # Views #
  114 AGENDA = _('Agenda')
  115 
  116 DAY = _('Day')
  117 WEEK = _("Week")
  118 MONTH = _("Month")
  119 
  120 PATH = _('Path')
  121 KEYWORD = _('Keyword')
  122 TAG = _('Tag')
  123 
  124 NOTE = _('Note')
  125 CUSTOM = _("Custom")
  126 
  127 CALENDARS = _("Calendars")
  128 
  129 COPY = _("Copy")
  130 EDIT = _("Edit")
  131 DELETE = _("Delete")
  132 
  133 FILE = _("File")
  134 NEW = _("New")
  135 TIMER = _("Timer")
  136 OPEN = _("Open")
  137 VIEW = _("View")
  138 ITEM = _("Item")
  139 TOOLS = _("Tools")
  140 HELP = _("Help")
  141 
  142 MAKE = _("Make report")
  143 PRINT = _("Print")
  144 EXPORTTEXT = _("Export report in text format ...")
  145 EXPORTCSV = _("Export report in CSV format ...")
  146 SAVESPECS = _("Save changes to report specifications")
  147 
  148 CLOSE = _("Close")
  149 
  150 SEP = "----"
  151 
  152 LASTLTR = re.compile(r'([a-z])$')
  153 
  154 this_dir, this_filename = os.path.split(__file__)
  155 USERMANUAL = os.path.normpath(os.path.join(this_dir, "help", "UserManual.html"))
  156 ICONSETTINGS = os.path.normpath(os.path.join(this_dir, "icons", "icon_settings.gif"))
  157 ICONPLUS = os.path.normpath(os.path.join(this_dir, "icons", "icon_plus.gif"))
  158 # ICONLOGO = os.path.normpath(os.path.join(this_dir, "icons", "etmlogo.gif"))
  159 
  160 class App(Tk):
  161     def __init__(self, path=None):
  162         Tk.__init__(self, className="EtmTk")
  163         self.minsize(360, 460)
  164         self.uuidSelected = None
  165         self.timerItem = None
  166         self.monthly_calendar = Calendar()
  167         self.itemAlerts = []
  168         self.activeAlerts = []
  169 
  170         self.loop = loop
  171         self.options = loop.options
  172         self.countdownActive = False
  173         self.countdownMinutes = self.loop.options['countdown_minutes']
  174         self.countdownTime = None
  175 
  176         self.messageAlerts = {}
  177 
  178         BGCOLOR = self.options['background_color']
  179         self.BGCOLOR = BGCOLOR
  180         HLCOLOR = self.options['highlight_color']
  181         self.HLCOLOR = HLCOLOR
  182         FGCOLOR = self.options['foreground_color']
  183         self.FGCOLOR = FGCOLOR
  184         CALENDAR_COLORS = self.options['calendar_colors']
  185 
  186         self.ACTIVEFILL = CALENDAR_COLORS['active']
  187         self.CONFLICTFILL = CALENDAR_COLORS['conflict']
  188         self.CURRENTFILL = CALENDAR_COLORS['current']
  189         self.GRIDCOLOR = CALENDAR_COLORS['grid']
  190         self.OCCASIONFILL = CALENDAR_COLORS['occasion']
  191         self.BUSYBAR = CALENDAR_COLORS['busybar']
  192         self.CURRDATE = CALENDAR_COLORS['date']
  193         self.OTHERDATE = CALENDAR_COLORS['grid']
  194         self.YEARPAST = CALENDAR_COLORS['year_past']
  195         self.YEARCURRENT = CALENDAR_COLORS['year_current']
  196         self.YEARFUTURE = CALENDAR_COLORS['year_future']
  197 
  198         self.configure(background=BGCOLOR, highlightcolor=HLCOLOR, takefocus=False)
  199         self.option_add('*tearOff', False)
  200         self.menu_lst = []
  201         self.menutree = MenuTree()
  202         self.chosen_date = None
  203         self.active_date = None
  204         self.canvas_date = None
  205         self.busy_info = None
  206         self.weekly = False
  207         self.monthly = False
  208         self.specsModified = False
  209         self.active_tree = {}
  210         self.protocol("WM_DELETE_WINDOW", self.quit)
  211         root = "_"
  212 
  213         self.week_height = 76
  214         self.month_height = 280
  215 
  216 
  217         style = self.options['style']
  218         s = ttk.Style()
  219         styles = s.theme_names()
  220         if style in styles:
  221             logger.info("using style {0}".format(style))
  222         else:
  223             logger.warn("style {0} is not an option from {1} - using default".format(style, ", ".join(styles)))
  224             style = 'default'
  225         s.theme_use(style)
  226         loop.tkstyle = style
  227 
  228         ttk.Style().configure("bg.TButton", background=BGCOLOR, activebackground=BGCOLOR, highlightcolor=HLCOLOR, foreground=FGCOLOR, relief=RAISED, takefocus=False)
  229 
  230         tkfixedfont = tkFont.nametofont("TkFixedFont")
  231         if 'fontsize_fixed' in self.options and self.options['fontsize_fixed']:
  232             tkfixedfont.configure(size=self.options['fontsize_fixed'])
  233         logger.info("using fixedfont size: {0}".format(tkfixedfont.actual()['size']))
  234         self.tkfixedfont = tkfixedfont
  235 
  236         tktreefont = tkFont.nametofont("TkDefaultFont")
  237         treefontfamily = tktreefont['family']
  238         if 'fontsize_tree' in self.options and self.options['fontsize_tree']:
  239             tktreefont.configure(size=self.options['fontsize_tree'])
  240         logger.info("using treefont size: {0}".format(tktreefont.actual()['size']))
  241         self.tktreefont = tktreefont
  242 
  243         ef = "%a %b %d"
  244         if 'ampm' in loop.options and loop.options['ampm']:
  245             self.efmt = "%I:%M%p {0}".format(ef)
  246         else:
  247             self.efmt = "%H:%M {0}".format(ef)
  248 
  249         self.default_calendars = deepcopy(loop.options['calendars'])
  250 
  251         # create the root node for the menu tree
  252         self.menutree.create_node(root, root)
  253 
  254         # leaf: (parent, (option, [accelerator])
  255         self.outline_depths = {}
  256         for view in DAY, WEEK, MONTH, TAG, KEYWORD, NOTE, PATH:
  257             # set all to the default
  258             logger.debug('Setting depth for {0} to {1}'.format(view, loop.options['outline_depth']))
  259             self.outline_depths[view] = loop.options['outline_depth']
  260         # set CUSTOM to 0
  261         self.outline_depths[AGENDA] = 0
  262         self.outline_depths[CUSTOM] = 0
  263 
  264         self.topbar = topbar = Frame(self, bd=0, relief="flat", highlightcolor=HLCOLOR, background=BGCOLOR, takefocus=False)
  265         topbar.pack(side="top", fill="x", expand=0, padx=0, pady=0)
  266 
  267         self.box_value = StringVar()
  268         self.custom_box = ttk.Combobox(self.topbar, textvariable=self.box_value, font=self.tkfixedfont)
  269 
  270         self.statusbar = Frame(self, bd=0, relief="flat", highlightcolor=HLCOLOR, highlightthickness=0, background=BGCOLOR, takefocus=False)
  271         self.statusbar.pack(side="bottom", fill="x", expand=0, padx=4, pady=2)
  272 
  273         self.topwindow = topwindow = PanedWindow(self, orient="vertical", sashwidth=2, sashrelief='flat', background=BGCOLOR)
  274         self.topwindow.pack(side="top", padx=0, fill=BOTH, expand=1)
  275 
  276 
  277         self.toppane = toppane = Frame(
  278             topwindow, bd=0, relief="flat",
  279             highlightthickness=0,
  280             highlightcolor=HLCOLOR,
  281             highlightbackground=BGCOLOR,
  282             background=BGCOLOR,
  283             takefocus=False)
  284         self.toppane.pack(side="top", fill=BOTH, expand=1)
  285 
  286         self.canvas = Canvas(
  287             self.toppane, background=BGCOLOR, bd=2, relief="flat",
  288             highlightthickness=3,
  289             highlightbackground=BGCOLOR,
  290             highlightcolor=HLCOLOR)
  291         self.canvas.pack(fill=BOTH, expand=1, padx=2, pady=0)
  292 
  293         self.botwindow = botwindow = PanedWindow(topwindow, orient="vertical", sashwidth=0, sashpad=0, bd=0, sashrelief='flat', background=BGCOLOR )
  294         topwindow.add(botwindow, padx=0)
  295 
  296         self.treepane = treepane = Frame(
  297             botwindow, bd=0, relief="flat",
  298             highlightthickness=3,
  299             highlightcolor=HLCOLOR,
  300             highlightbackground=BGCOLOR,
  301             background=BGCOLOR,
  302             takefocus=False)
  303 
  304         botwindow.add(treepane, padx=2, pady=0, stretch="first")
  305 
  306         ttk.Style().configure("Treeview",
  307             bd=0,
  308             padding=2,
  309             highlightthickness=0,
  310             background=BGCOLOR,
  311             foreground=FGCOLOR,
  312             highlightcolor=HLCOLOR,
  313             fieldbackground=BGCOLOR,
  314             )
  315 
  316         self.tree = ttk.Treeview(treepane, show='tree', columns=["#1", "#2"], selectmode='browse')
  317 
  318         self.tree.pack(fill="both", expand=1, padx=0, pady=0)
  319 
  320         self.content = ReadOnlyText(
  321             botwindow, font=self.tkfixedfont,
  322             wrap="word", padx=3, bd=2, relief="sunken",
  323             height=loop.options['details_rows'],
  324             width=46, takefocus=False,
  325             highlightthickness=0,
  326             highlightcolor=HLCOLOR,
  327             background=BGCOLOR,
  328             highlightbackground=BGCOLOR,
  329             foreground=FGCOLOR
  330             )
  331         botwindow.add(self.content, padx=2, pady=2, stretch="never")
  332 
  333         self.canvas.bind('<Button-1>', (lambda e: self.selectId(event=e, d=0)))
  334         self.canvas.bind("<Control-Button-1>", self.on_select_item)
  335         self.canvas.bind("<Double-1>", self.on_select_item)
  336 
  337         self.canvas.bind("<Return>", lambda e: self.on_activate_item(event=e))
  338         self.canvas.bind("<KP_Enter>", lambda e: self.on_activate_item(event=e))
  339         self.canvas.bind('<Left>', (lambda e: self.priorWeekMonth(event=e)))
  340         self.canvas.bind('<Right>', (lambda e: self.nextWeekMonth(event=e)))
  341         self.canvas.bind('<Up>', (lambda e: self.selectId(event=e, d=-1)))
  342         self.canvas.bind('<Down>', (lambda e: self.selectId(event=e, d=1)))
  343         self.canvas.bind('<space>', self.goHome)
  344         self.canvas.bind("<Configure>", self.on_resize)
  345 
  346         self.canvas.bind("b", lambda event: self.after(AFTER, self.showBusyPeriods))
  347         self.canvas.bind("f", lambda event: self.after(AFTER, self.showFreePeriods))
  348 
  349         # main menu
  350         self.menubar = menubar = Menu(self)
  351         menu = _("Menu")
  352         self.add2menu(root, (menu,))
  353 
  354         # File menu
  355         filemenu = Menu(menubar, tearoff=0)
  356         path = FILE
  357         self.add2menu(menu, (path, ))
  358 
  359         self.newmenu = newmenu = Menu(filemenu, tearoff=0)
  360         filemenu.add_cascade(label=NEW, menu=newmenu)
  361 
  362         self.add2menu(path, (NEW, ))
  363         path = NEW
  364 
  365         label = _("Item")
  366         l = "N"
  367         c = "n"
  368         newmenu.add_command(label=label, command=self.newItem)
  369         # self.bindTop("n", self.newItem)
  370         self.bindTop(c, lambda e: self.after(AFTER, self.newItem(e)))
  371         self.canvas.bind(c, lambda e: self.after(AFTER, self.newItem(e)))
  372 
  373         newmenu.entryconfig(0, accelerator=l)
  374         self.add2menu(path, (label, l))
  375 
  376         l = "Shift-N"
  377         c = "N"
  378         label = _("File")
  379         newmenu.add_command(label=label, command=self.newFile)
  380         self.bindTop(c, self.newFile)
  381         newmenu.entryconfig(1, accelerator=l)
  382         self.add2menu(path, (label, l))
  383 
  384         path = FILE
  385 
  386         self.timermenu = timermenu = Menu(filemenu, tearoff=0)
  387         filemenu.add_cascade(label=TIMER, menu=timermenu)
  388         self.add2menu(path, (TIMER, ))
  389         path = TIMER
  390 
  391         self.actionTimer = Timer(self, options=loop.options)
  392 
  393         label = _("Start action timer")
  394         l = "T"
  395         c = 't'
  396         timermenu.add_command(label=label, command=self.actionTimer.selectTimer)
  397         self.bindTop(c, self.actionTimer.selectTimer)
  398         timermenu.entryconfig(0, accelerator=l)
  399         self.add2menu(path, (label, l))
  400 
  401         label = _("Finish action timer")
  402         l = "Shift-T"
  403         c = "T"
  404         timermenu.add_command(label=label, command=self.finishActionTimer)
  405         self.bindTop(c, self.finishActionTimer)
  406         timermenu.entryconfig(1, accelerator=l)
  407         self.add2menu(path, (label, l))
  408 
  409         label = _("Toggle current timer")
  410         l = "I"
  411         c = 'i'
  412         timermenu.add_command(label=label, command=self.actionTimer.toggleCurrent)
  413         self.bindTop(c, self.actionTimer.toggleCurrent)
  414         timermenu.entryconfig(2, accelerator=l)
  415         self.add2menu(path, (label, l))
  416 
  417         label = _("Delete action timer")
  418         l = "Shift-I"
  419         c = "I"
  420         timermenu.add_command(label=label, command=self.actionTimer.deleteTimer)
  421         self.bind(c, self.actionTimer.deleteTimer)
  422         timermenu.entryconfig(3, accelerator=l)
  423         self.add2menu(path, (label, l))
  424 
  425         label = _("Assign idle time")
  426         l, c = commandShortcut('i')
  427         timermenu.add_command(label=label,
  428                               command=self.adjustIdle)
  429         self.bindTop(c, self.adjustIdle)
  430         timermenu.entryconfig(4, accelerator=l)
  431         self.add2menu(path, (label, l))
  432 
  433         label = _("Reset idle to zero minutes")
  434         l = ""
  435         timermenu.add_command(label=label,
  436                               command=self.actionTimer.clearIdle)
  437         timermenu.entryconfig(5, accelerator=l)
  438         self.add2menu(path, (label, l))
  439 
  440         label = _("Toggle idle timer display")
  441         l = ""
  442         timermenu.add_command(label=label,
  443                               command=self.actionTimer.toggleIdle)
  444         timermenu.entryconfig(6, accelerator=l)
  445         self.add2menu(path, (label, l))
  446 
  447         label = _("Countdown timer")
  448         l = "Z"
  449         c = "z"
  450         timermenu.add_command(label=label,
  451                               command=self.setcountdownTimer)
  452         timermenu.entryconfig(7, accelerator=l)
  453         self.bind(c, self.setcountdownTimer)
  454 
  455         self.add2menu(path, (label, l))
  456 
  457         self.actionTimer.updateMenu()
  458 
  459         path = FILE
  460 
  461         # Open
  462         openmenu = Menu(filemenu, tearoff=0)
  463         self.add2menu(path, (OPEN, ))
  464         path = OPEN
  465 
  466         l = "Shift-F"
  467         c = "F"
  468         label = _("Data file ...")
  469         openmenu.add_command(label=label, command=self.editData)
  470         self.bindTop(c, self.editData)
  471         openmenu.entryconfig(0, accelerator=l)
  472         self.add2menu(path, (label, l))
  473 
  474         l = "Shift-C"
  475         c = "C"
  476         label = _("Configuration file ...")
  477         openmenu.add_command(label=label, command=self.editCfgFiles)
  478         self.bindTop(c, self.editCfgFiles)
  479         openmenu.entryconfig(1, accelerator=l)
  480         self.add2menu(path, (label, l))
  481 
  482         l = "Shift-P"
  483         c = "P"
  484         label = _("Preferences")
  485         openmenu.add_command(label=label, command=self.editConfig)
  486         self.bindTop(c, self.editConfig)
  487 
  488         openmenu.entryconfig(2, accelerator=l)
  489         self.add2menu(path, (label, l))
  490 
  491         l = "Shift-S"
  492         c = "S"
  493         file = loop.options['scratchpad']
  494         # label = relpath(file, loop.options['etmdir'])
  495         label = _("Scratchpad")
  496         openmenu.add_command(label=label, command=self.editScratch)
  497         self.bindTop(c, self.editScratch)
  498         openmenu.entryconfig(3, accelerator=l)
  499         self.add2menu(path, (label, l))
  500 
  501         filemenu.add_cascade(label=OPEN, menu=openmenu)
  502 
  503         path = FILE
  504 
  505         filemenu.add_separator()
  506         self.add2menu(path, (SEP, ))
  507 
  508         # quit
  509         l, c = commandShortcut('q')
  510         label = _("Quit")
  511         filemenu.add_command(label=label, underline=0,
  512                              command=self.quit)
  513         self.bind(c, self.quit)  # w
  514         self.add2menu(path, (label, l))
  515 
  516         menubar.add_cascade(label=path, underline=0, menu=filemenu)
  517         self.toolsmenu = viewmenu = Menu(menubar, tearoff=0)
  518 
  519         self.viewmenu = viewmenu = Menu(menubar, tearoff=0)
  520         path = VIEW
  521         self.add2menu(menu, (path, ))
  522 
  523         # # agenda
  524         # l = label = AGENDA
  525         # toolsmenu.add_command(label=label, command=self.agendaView)
  526 
  527         self.vm_options = [[AGENDA, 'a'],
  528                            [DAY, 'd'],
  529                            [WEEK, 'w'],
  530                            [MONTH, 'm'],
  531                            [TAG, 't'],
  532                            [KEYWORD, 'k'],
  533                            [PATH, 'p'],
  534                            [NOTE, 'n'],
  535                            [CUSTOM, 'c'],
  536                            ]
  537 
  538         self.view2cmd = {'a': self.agendaView,
  539                          'd': self.dayView,
  540                          'm': self.showMonthly,
  541                          'p': self.pathView,
  542                          'k': self.keywordView,
  543                          'n': self.noteView,
  544                          't': self.tagView,
  545                          'c': self.customView,
  546                          'w': self.showWeekly}
  547 
  548         self.viewname2cmd = {}
  549 
  550         self.view = self.vm_options[0][0]
  551         self.currentView = StringVar(self)
  552         self.currentView.set(self.view)
  553 
  554         self.vm_opts = [x[0] for x in self.vm_options]
  555         for i in range(len(self.vm_options)):
  556             label = self.vm_options[i][0]
  557             k = self.vm_options[i][1]
  558             if label == DAY:
  559                 continue
  560             elif label == "-":
  561                 self.viewmenu.add_separator()
  562                 # self.add2menu(VIEW, (SEP, ))
  563             else:
  564                 l, c = commandShortcut(k)
  565                 viewmenu.add_command(label=label, command=self.view2cmd[k])
  566                 # self.bind(c, lambda e, x=k: self.after(AFTER, self.view2cmd[x]))
  567                 self.bindTop(c, self.view2cmd[k])
  568                 viewmenu.entryconfig(i, accelerator=l)
  569                 self.add2menu(path, (label, l))
  570 
  571 
  572         viewmenu.add_separator()
  573         self.add2menu(path, (SEP, ))
  574 
  575         # apply filter
  576         l, c = commandShortcut('f')
  577         label = _("Set outline filter")
  578         viewmenu.add_command(label=label, underline=1, command=self.setFilter)
  579         self.bindTop(c, self.setFilter)
  580 
  581         viewmenu.entryconfig(10, accelerator=l)
  582         self.add2menu(path, (label, l))
  583 
  584         # clear filter
  585         l = "Shift-Ctrl-F"
  586         label = _("Clear outline filter")
  587         viewmenu.add_command(label=label, underline=1, command=self.clearFilter)
  588 
  589         viewmenu.entryconfig(11, accelerator=l)
  590         self.add2menu(path, (label, l))
  591 
  592         # toggle showing labels
  593         l = "L"
  594         c = "l"
  595         label = _("Toggle labels")
  596         viewmenu.add_command(label=label, underline=1, command=self.toggleLabels)
  597         self.bindTop(c, self.toggleLabels)
  598 
  599         viewmenu.entryconfig(12, accelerator=l)
  600         self.add2menu(path, (label, l))
  601 
  602         # expand to depth
  603         l = "O"
  604         c = "o"
  605         label = _("Set outline depth")
  606         viewmenu.add_command(label=label, underline=1, command=self.expand2Depth)
  607         self.bindTop(c, self.expand2Depth)
  608 
  609         viewmenu.entryconfig(13, accelerator=l)
  610         self.add2menu(path, (label, l))
  611 
  612         # toggle showing finished
  613         l = "X"
  614         c = "x"
  615         label = _("Toggle finished")
  616         viewmenu.add_command(label=label, underline=1, command=self.toggleFinished)
  617         self.bindTop(c, self.toggleFinished)
  618         viewmenu.entryconfig(14, accelerator=l)
  619         self.add2menu(path, (label, l))
  620 
  621         menubar.add_cascade(label=path, underline=0, menu=viewmenu)
  622 
  623         # Item menu
  624         self.itemmenu = itemmenu = Menu(menubar, tearoff=0)
  625         self.itemmenu.bind("<Escape>", self.closeItemMenu)
  626         self.itemmenu.bind("<FocusOut>", self.closeItemMenu)
  627         self.em_options = [
  628             [_('Copy'), 'c'],
  629             [_('Delete'), 'D'],
  630             [_('Edit'), 'e'],
  631             [_('Edit file'), 'E'],
  632             [_('Finish'), 'f'],
  633             [_('Move'), 'm'],
  634             [_('Reschedule'), 'r'],
  635             [_('Schedule new'), 's'],
  636             [_('Klone as timer'), 'k'],
  637             [_('Show date and time details'), 'd'],
  638             [_('Open link'), 'g'],
  639             [_('Show user details'), 'u'],
  640             ]
  641         path = ITEM
  642         self.add2menu(menu, (path, ))
  643         self.edit2cmd = {
  644             'c': self.copyItem,
  645             'D': self.deleteItem,
  646             'e': self.editItem,
  647             'E': self.editItemFile,
  648             'f': self.finishItem,
  649             'm': self.moveItem,
  650             'r': self.rescheduleItem,
  651             's': self.scheduleNewItem,
  652             'd': self.showDateTimeDetails,
  653             'g': self.openWithDefault,
  654             'u': self.showUserDetails,
  655             'k': self.kloneTimer}
  656         self.em_opts = [x[0] for x in self.em_options]
  657         for i in range(len(self.em_options)):
  658             label = self.em_options[i][0]
  659             k = self.em_options[i][1]
  660             if k == 'D':
  661                 l = "BackSpace"
  662                 c = "<BackSpace>"
  663             elif k == 'E':
  664                 l = "Shift-E"
  665                 c = "E"
  666             else:
  667                 l = k.upper()
  668                 c = k
  669             itemmenu.add_command(label=label, underline=0, command=self.edit2cmd[k])
  670             if k == 'f':
  671                 self.tree.bind(c, self.edit2cmd[k])
  672             else:
  673                 self.bindTop(c, self.edit2cmd[k])
  674 
  675             itemmenu.entryconfig(i, accelerator=l)
  676             self.add2menu(path, (label, l))
  677         menubar.add_cascade(label=path, underline=0, menu=itemmenu)
  678 
  679         # tools menu
  680         path = TOOLS
  681         self.add2menu(menu, (path, ))
  682         self.toolsmenu = toolsmenu = Menu(menubar, tearoff=0)
  683 
  684         # go home
  685         l = "Home"
  686         label = _("Home")
  687         toolsmenu.add_command(label=label, command=self.goHome)
  688 
  689         toolsmenu.entryconfig(0, accelerator=l)
  690         self.add2menu(path, (label, l))
  691         self.bindTop('<Home>', self.goHome)
  692 
  693         # go to date
  694         l = "J"
  695         c = "j"
  696         label = _("Jump to date")
  697         toolsmenu.add_command(label=label, command=self.goToDate)
  698         self.bindTop(c, self.goToDate)
  699 
  700         toolsmenu.entryconfig(1, accelerator=l)
  701         self.add2menu(path, (label, l))
  702 
  703         toolsmenu.add_separator()  # 2
  704         self.add2menu(path, (SEP, ))
  705 
  706         # show alerts
  707         l = "A"
  708         c = "a"
  709         label = _("Show remaining alerts for today")
  710         toolsmenu.add_command(label=label, underline=1, command=self.showAlerts)
  711         self.bindTop(c, self.showAlerts)
  712 
  713         toolsmenu.entryconfig(3, accelerator=l)
  714         self.add2menu(path, (label, l))
  715 
  716         l = "B"
  717         c = 'b'
  718         label = _("List busy times in week/month")
  719         toolsmenu.add_command(label=label, underline=5, command=self.showBusyPeriods)
  720 
  721         toolsmenu.entryconfig(4, accelerator=l)
  722         self.add2menu(path, (label, l))
  723 
  724         l = "F"
  725         c = 'f'
  726         label = _("List free times in week/month")
  727         toolsmenu.add_command(label=label, underline=5, command=self.showFreePeriods)
  728 
  729         toolsmenu.entryconfig(5, accelerator=l)
  730         # set binding in showWeekly
  731         self.add2menu(path, (label, l))
  732 
  733 
  734         # date calculator
  735         l = "Shift-D"
  736         c = "D"
  737         label = _("Date and time calculator")
  738         toolsmenu.add_command(label=label, underline=12, command=self.dateCalculator)
  739         self.bindTop(c, self.dateCalculator)
  740 
  741         toolsmenu.entryconfig(6, accelerator=l)
  742         self.add2menu(path, (label, l))
  743 
  744         # available date calculator
  745         l = "Shift-A"
  746         c = "A"
  747         label = _("Available dates calculator")
  748         toolsmenu.add_command(label=label, underline=12, command=self.availableDateCalculator)
  749         self.bindTop(c, self.availableDateCalculator)
  750 
  751         toolsmenu.entryconfig(7, accelerator=l)
  752         self.add2menu(path, (label, l))
  753 
  754         l = "Shift-Y"
  755         c = "Y"
  756 
  757         label = _("Yearly calendar")
  758         toolsmenu.add_command(label=label, underline=8, command=self.showCalendar)
  759         self.bindTop(c, self.showCalendar)
  760 
  761         toolsmenu.entryconfig(8, accelerator=l)
  762         self.add2menu(path, (label, l))
  763 
  764         toolsmenu.add_separator() # 9
  765         self.add2menu(path, (SEP, ))
  766 
  767         # popup active tree
  768         l = "Shift-O"
  769         c = "O"
  770         label = _("Show outline as text")
  771         toolsmenu.add_command(label=label, underline=1, command=self.popupTree)
  772         self.bindTop(c, self.popupTree)
  773 
  774         toolsmenu.entryconfig(10, accelerator=l)
  775         self.add2menu(path, (label, l))
  776 
  777         # print active tree
  778         l = "P"
  779         c = "p"
  780         label = _("Print outline")
  781         toolsmenu.add_command(label=label, underline=1, command=self.printTree)
  782         self.bindTop("p", self.printTree)
  783         toolsmenu.entryconfig(11, accelerator=l)
  784         self.add2menu(path, (label, l))
  785 
  786         # export
  787         l = "Shift-X"
  788         c = "X"
  789         label = _("Export to iCal")
  790         toolsmenu.add_command(label=label, underline=1, command=self.exportToIcal)
  791         self.bind(c, self.exportToIcal)
  792 
  793         toolsmenu.entryconfig(12, accelerator=l)
  794         self.add2menu(path, (label, l))
  795 
  796         l = "Shift-J"
  797         c = "J"
  798         label = _("Export to JSON")
  799         toolsmenu.add_command(label=label, underline=1, command=self.exportToJson)
  800         self.bind(c, self.exportToJson)
  801 
  802         toolsmenu.entryconfig(13, accelerator=l)
  803         self.add2menu(path, (label, l))
  804 
  805         # update subscriptions
  806         l = "Shift-M"
  807         c = "M"
  808         label = _("Update calendar subscriptions")
  809         toolsmenu.add_command(label=label, underline=1, command=self.updateSubscriptions)
  810         self.bind(c, self.updateSubscriptions)
  811 
  812         toolsmenu.entryconfig(14, accelerator=l)
  813         self.add2menu(path, (label, l))
  814 
  815         # changes
  816         if loop.options['vcs_system']:
  817 
  818             l = 'Shift-H'
  819             c = 'H'
  820             label = _("History of changes")
  821             toolsmenu.add_command(label=label, underline=1, command=self.showChanges)
  822             self.bind(c, lambda event: self.after(AFTER, self.showChanges))
  823 
  824             toolsmenu.entryconfig(15, accelerator=l)
  825             self.add2menu(path, (label, l))
  826 
  827         menubar.add_cascade(label=path, menu=toolsmenu, underline=0)
  828 
  829         self.toolsmenu.entryconfig(1, state="disabled")
  830         for i in range(4, 6):
  831             self.toolsmenu.entryconfig(i, state="disabled")
  832 
  833         # report
  834         path = CUSTOM
  835         self.add2menu(menu, (path, ))
  836         self.custommenu = reportmenu = Menu(menubar, tearoff=0)
  837 
  838         self.rm_options = [[MAKE, 'm'],
  839                            [EXPORTTEXT, 't'],
  840                            [EXPORTCSV, 'x'],
  841                            [SAVESPECS, 'w']]
  842 
  843         self.rm2cmd = {'m': self.makeReport,
  844                        't': self.exportText,
  845                        'x': self.exportCSV,
  846                        'w': self.saveSpecs}
  847 
  848         self.rm_opts = [x[0] for x in self.rm_options]
  849 
  850         for i in range(len(self.rm_options)):
  851             label = self.rm_options[i][0]
  852             k = self.rm_options[i][1]
  853             l = k.upper()
  854             c = k
  855 
  856             reportmenu.add_command(label=label, underline=0, command=self.rm2cmd[k])
  857             reportmenu.entryconfig(i, state="disabled")
  858 
  859         self.add2menu(CUSTOM, (_("Create and display selected report"), "Return"))
  860         self.add2menu(CUSTOM, (_("Export report in text format ..."), "Ctrl-T"))
  861         self.add2menu(CUSTOM, (_("Export report in csv format ..."), "Ctrl-X"))
  862         self.add2menu(CUSTOM, (_("Save changes to report specifications"), "Ctrl-W"))
  863         self.add2menu(CUSTOM, (_("Expand report list"), "Down"))
  864 
  865         menubar.add_cascade(label=path, menu=reportmenu, underline=0)
  866 
  867         # help
  868         helpmenu = Menu(menubar, tearoff=0)
  869         path = HELP
  870         self.add2menu(menu, (path, ))
  871 
  872         # search is built in
  873         self.add2menu(path, (_("Search"), ))
  874 
  875         label = _("Shortcuts")
  876         helpmenu.add_command(label=label, underline=1, accelerator="?", command=self.showShortcuts)
  877         self.add2menu(path, (label, "?"))
  878         self.bindTop("?", self.showShortcuts)
  879 
  880         label = _("User manual")
  881         helpmenu.add_command(label=label, underline=1, accelerator="F1", command=self.help)
  882         self.add2menu(path, (label, "F1"))
  883         self.bind("<F1>", lambda e: self.after(AFTER, self.help))
  884 
  885         label = _("About")
  886         helpmenu.add_command(label="About", accelerator="F2", command=self .about)
  887         self.bind("<F2>", self.about)
  888         self.add2menu(path, (label, "F2"))
  889 
  890         # check for updates
  891 
  892         label = _("Check for update")
  893         helpmenu.add_command(label=label, underline=1, accelerator="F3", command=self.checkForUpdate)
  894         self.add2menu(path, (label, "F3"))
  895         self.bind("<F3>", lambda e: self.after(AFTER, self.checkForUpdate))
  896 
  897         menubar.add_cascade(label="Help", menu=helpmenu)
  898 
  899         self.config(menu=menubar)
  900 
  901         self.history = []
  902         self.index = 0
  903         self.count = 0
  904         self.count2id = {}
  905         self.now = get_current_time()
  906         self.today = self.now.date()
  907 
  908         self.popup = ''
  909         self.value = ''
  910         self.firsttime = True
  911         self.mode = 'command'   # or edit or delete
  912         self.item_hsh = {}
  913         self.depth2id = {}
  914         self.prev_week = None
  915         self.next_week = None
  916         self.curr_week = None
  917         self.week_beg = None
  918         self.itemSelected = None
  919         self.uuidSelected = None
  920         self.dtSelected = None
  921         self.rowSelected = None
  922 
  923         self.currentTime = StringVar(self)
  924         self.currentTime.set("")
  925 
  926         # self.title(ETM)
  927         self.title(self.currentTime.get())
  928 
  929         self.columnconfigure(0, minsize=300, weight=1)
  930         self.rowconfigure(1, weight=2)
  931 
  932         # self.etmlogo = PhotoImage(file=ICONLOGO)
  933         # self.iconphoto(True, self.etmlogo)
  934 
  935         # report
  936         self.custom_box.bind("<<ComboboxSelected>>", self.newselection)
  937         self.bind("<Return>", self.makeReport)
  938         self.bind("<KP_Enter>", self.makeReport)
  939         self.bind("<Control-q>", self.quit)
  940         self.saved_specs = ['']
  941         self.getSpecs()
  942         if self.specs:
  943             self.value_of_combo = self.specs[0]
  944             self.custom_box['values'] = self.specs
  945             self.custom_box.current(0)
  946             self.saved_specs = deepcopy(self.specs)
  947         self.custom_box.configure(
  948             width=30,
  949             # background=FGCOLOR,
  950             # foreground=BGCOLOR,
  951             takefocus=False)
  952 
  953 
  954         iconsize = "22"
  955         self.settingsIcon = PhotoImage(file=ICONSETTINGS)
  956         self.settingsbutton = ttk.Button(
  957             topbar, command=self.selectCalendars, style="bg.TButton", takefocus=False, width=0
  958         )
  959         self.settingsbutton.config(image=self.settingsIcon)
  960         self.settingsbutton.pack(side="left", padx=4, pady=2)
  961 
  962 
  963         self.newIcon = PhotoImage(file=ICONPLUS)
  964         self.newbutton = ttk.Button(topbar, command=self.newItem, style="bg.TButton", takefocus=False, width=0)
  965         self.newbutton.config(image=self.newIcon)
  966         self.newbutton.pack(side="right", padx=4, pady=2)
  967 
  968         windowtitle = Label(topbar, textvariable=self.currentView, bd=1, relief="flat",  padx=8, pady=0)
  969         windowtitle.pack(side="left")
  970         windowtitle.configure(background=BGCOLOR, foreground=FGCOLOR)
  971 
  972         # filter
  973         self.filterValue = StringVar(self)
  974         self.filterValue.set('')
  975         self.filterValue.trace_variable("w", self.filterView)
  976 
  977         self.fltr = Entry(topbar, textvariable=self.filterValue, width=14, bg=BGCOLOR, highlightcolor=HLCOLOR, highlightbackground=BGCOLOR, foreground=FGCOLOR, highlightthickness=3, bd=0, takefocus=False)
  978         self.fltr.pack(side="left", padx=0, expand=1, fill=X)
  979         self.fltr.bind("<FocusIn>", self.setFilter)
  980         self.fltr.bind("<Escape>", self.clearFilter)
  981         self.fltr.bind('<Tab>', self.leaveFilter)
  982         self.bind("<Shift-Control-F>", self.clearFilter)
  983         self.filter_active = False
  984 
  985         self.weekly = False
  986 
  987         self.col2_width = tktreefont.measure('abcdgklmprtuX')
  988         self.col3_width = tktreefont.measure('10:30pm ~ 11:30pmX')
  989         self.text_width = 260
  990         logger.info('column widths: {0}, {1}, {2}'.format(self.text_width, self.col2_width, self.col3_width))
  991         self.tree.column('#0', minwidth=140, width=self.text_width, stretch=1)
  992         self.labels = False
  993         # don't show the labels column to start with by setting width=0
  994         self.tree.column('#1', minwidth=0, width=0, stretch=0, anchor='center')
  995         self.tree.column('#2', width=self.col3_width, stretch=0, anchor='center')
  996         self.tree.bind('<<TreeviewSelect>>', self.OnSelect)
  997         self.tree.bind('<Double-1>', self.OnActivate)
  998         self.tree.bind('<Return>', self.OnActivate)
  999         self.tree.bind('<KP_Enter>', self.OnActivate)
 1000         self.tree.bind('<Control-Down>', self.nextItem)
 1001         self.tree.bind('<Control-Up>', self.prevItem)
 1002 
 1003 
 1004         for t in tstr2SCI:
 1005             self.tree.tag_configure(t, foreground=tstr2SCI[t][1])
 1006 
 1007         self.date2id = {}
 1008         self.root = ('', '_')
 1009 
 1010         self.content.bind('<Escape>', self.cleartext)
 1011         self.content.bind('<Tab>', self.focus_next_window)
 1012         self.content.bind("<FocusIn>", self.editItem)
 1013 
 1014 
 1015         self.pendingAlerts = IntVar(self)
 1016         self.pendingAlerts.set(0)
 1017         self.pending = ttk.Button(self.statusbar,
 1018                               textvariable=self.pendingAlerts,
 1019                               command=self.showAlerts,
 1020                               style="bg.TButton",
 1021                               width=0,
 1022                               takefocus=False
 1023                               )
 1024         self.pending.pack(side="right", expand=0, padx=2, pady=2)
 1025 
 1026         self.countdownStatus = StringVar(self)
 1027         self.countdownStatus.set("")
 1028         self.countdown_time = countdown_time = Label(self.statusbar, textvariable=self.countdownStatus, bd=0, relief="flat", anchor=W, justify=LEFT, padx=2, pady=0)
 1029         countdown_time.pack(side="right", expand=0, fill=X, padx=6)
 1030         countdown_time.configure(background=BGCOLOR, foreground=FGCOLOR, highlightthickness=0)
 1031 
 1032         self.timerStatus = StringVar(self)
 1033         self.timerStatus.set("")
 1034         self.timer_status = timer_status = Label(self.statusbar, textvariable=self.timerStatus, bd=0, relief="flat", anchor=W, justify=LEFT, padx=2, pady=0)
 1035         timer_status.pack(side="right", expand=1, fill=X, padx=6)
 1036         timer_status.configure(background=BGCOLOR, foreground=FGCOLOR, highlightthickness=0)
 1037 
 1038         self.timerTitle = StringVar(self)
 1039         self.timerTitle.set("")
 1040         self.timer_title = timer_title = Label(self.statusbar, textvariable=self.timerTitle, bd=0, relief="flat", anchor=W, justify=LEFT, padx=2, pady=0)
 1041 
 1042         timer_title.pack(side="left", expand=1, fill=X, padx=0)
 1043         timer_title.configure(background=BGCOLOR, foreground=FGCOLOR, highlightthickness=0)
 1044 
 1045         # set cal_regex here and update it in updateCalendars
 1046         self.cal_regex = None
 1047         if loop.calendars:
 1048             cal_pattern = r'^%s' % '|'.join(
 1049                 [x[2] for x in loop.calendars if x[1]])
 1050             self.cal_regex = re.compile(cal_pattern)
 1051             logger.debug("cal_pattern: {0}".format(cal_pattern))
 1052 
 1053         self.default_regex = None
 1054         if 'calendars' in loop.options and loop.options['calendars']:
 1055             calendars = loop.options['calendars']
 1056             default_pattern = r'^%s' % '|'.join(
 1057                 [x[2] for x in calendars if x[1]])
 1058             self.default_regex = re.compile(default_pattern)
 1059 
 1060         self.add2menu(root, (EDIT, ))
 1061         self.add2menu(EDIT, (_("Show completions"), "Ctrl-Space"))
 1062         self.add2menu(EDIT, (_("Cancel"), "Escape"))
 1063         self.add2menu(EDIT, (FINISH, "Ctrl-S"))
 1064 
 1065         # start clock
 1066         self.updateClock()
 1067         self.year_month = [self.today.year, self.today.month]
 1068         # showView will be called from updateClock
 1069         self.updateAlerts()
 1070         self.etmgeo = os.path.normpath(os.path.join(loop.options['etmdir'], ".etmgeo"))
 1071         self.restoreGeometry()
 1072         self.etmtimers = os.path.normpath(os.path.join(loop.options['etmdir'], ".etmtimers"))
 1073         self.showWeekly() # hack to fix focus issue in agenda
 1074         self.agendaView()
 1075 
 1076     def on_resize(self, event):
 1077         if self.weekly:
 1078             self.canvas.after_idle(self.showWeek, )
 1079         elif self.monthly:
 1080             self.canvas.after_idle(self.showMonth, )
 1081 
 1082     def bindTop(self, c, cmd, e=None):
 1083         if e and e.char != c:
 1084             # ignore Control-c
 1085             return
 1086         self.tree.bind(c, lambda e: self.after(AFTER, cmd(e)))
 1087         self.canvas.bind(c, lambda e: self.after(AFTER, cmd(e)))
 1088 
 1089     def toggleLabels(self, e=None):
 1090         if e and e.char != "l":
 1091             return
 1092         if self.labels:
 1093             width0 = self.tree.column('#0')['width']
 1094             self.tree.column('#0', width=width0 + self.col2_width)
 1095             self.tree.column('#1', width=0)
 1096             self.labels = False
 1097         else:
 1098             width0 = self.tree.column('#0')['width']
 1099             self.tree.column('#0', width=width0 - self.col2_width)
 1100             self.tree.column('#1', width=self.col2_width)
 1101             self.labels = True
 1102 
 1103     def toggleFinished(self, e=None):
 1104         if e and e.char != "x":
 1105             return
 1106         if loop.options['hide_finished']:
 1107             loop.options['hide_finished'] = False
 1108         else:
 1109             loop.options['hide_finished'] = True
 1110         logger.debug('reloading data')
 1111         # self.updateAlerts()
 1112         if self.weekly:
 1113             self.updateDay()
 1114             self.showWeek()
 1115         elif self.monthly:
 1116             self.updateDay()
 1117             self.showMonth()
 1118         else:
 1119             self.showView()
 1120 
 1121     def saveGeometry(self):
 1122         str = self.geometry()
 1123         fo = open(self.etmgeo, 'w')
 1124         fo.write(str)
 1125         fo.close()
 1126 
 1127     def restoreGeometry(self):
 1128         if os.path.isfile(self.etmgeo):
 1129             fo = open(self.etmgeo, "r")
 1130             str = fo.read()
 1131             fo.close()
 1132             tup = [x.strip() for x in str.split(',')]
 1133             if tup:
 1134                 self.geometry(tup[0])
 1135 
 1136     def closeItemMenu(self, event=None):
 1137         if self.weekly or self.monthly:
 1138             self.canvas.focus_set()
 1139         else:
 1140             self.tree.focus_set()
 1141         self.itemmenu.unpost()
 1142 
 1143     def add2menu(self, parent, child):
 1144         if child == (SEP, ):
 1145             id = uuid.uuid1()
 1146         elif len(child) > 1 and child[1]:
 1147             id = uuid.uuid1()
 1148             m = LASTLTR.search(child[1])
 1149             if m:
 1150 
 1151                 child = tuple(child)
 1152         else:
 1153             id = child[0]
 1154         if len(child) >= 2:
 1155             leaf = "{0}::{1}".format(child[0], child[1])
 1156         else:
 1157             leaf = "{0}::".format(child[0])
 1158 
 1159         self.menutree.create_node(leaf, id, parent=parent)
 1160 
 1161     def confirm(self, parent=None, title="", prompt="", instance="xyz"):
 1162         ok, value = OptionsDialog(parent=self, title=_("confirm").format(instance), prompt=prompt).getValue()
 1163         return ok
 1164 
 1165     def selectCalendars(self):
 1166         if self.default_calendars:
 1167             prompt = _("Display items from calendars selected below.")
 1168             title = CALENDARS
 1169             if self.weekly or self.monthly:
 1170                 master = self.canvas
 1171             else:
 1172                 master = self.tree
 1173 
 1174             values = OptionsDialog(parent=self, master=master, title=title, prompt=prompt, opts=loop.calendars, radio=False, yesno=False).getValue()
 1175 
 1176             if values != loop.calendars:
 1177                 loop.calendars = values
 1178                 loop.options['calendars'] = values
 1179                 data.setConfig(loop.options)
 1180                 self.updateCalendars()
 1181         else:
 1182             prompt = _("No calendars have been specified in etmtk.cfg.")
 1183             self.textWindow(self, CALENDARS, prompt, opts=self.options)
 1184 
 1185     def updateCalendars(self, *args):
 1186         cal_pattern = r'^%s' % '|'.join(
 1187             [x[2] for x in loop.calendars if x[1]])
 1188         self.cal_regex = re.compile(cal_pattern)
 1189         self.update()
 1190         self.updateAlerts()
 1191         if self.weekly:
 1192             self.updateDay()
 1193             self.showWeek()
 1194         elif self.monthly:
 1195             self.updateDay()
 1196             self.showMonth()
 1197         else:
 1198             self.showView()
 1199 
 1200     def quit(self, e=None):
 1201         ans = True
 1202         if self.actionTimer.currentStatus == RUNNING:
 1203             ans = self.confirm(
 1204                 title=_('Quit'),
 1205                 prompt=_("An action timer is running.\nDo you really want to quit?"),
 1206                 parent=self)
 1207         else:
 1208             ans = self.confirm(
 1209                 title=_('Quit'),
 1210                 prompt=_("Do you really want to quit?"),
 1211                 parent=self)
 1212         if ans:
 1213             self.actionTimer.pauseTimer()
 1214             self.saveGeometry()
 1215             self.destroy()
 1216 
 1217     def donothing(self, e=None):
 1218         """For testing"""
 1219         logger.debug('donothing')
 1220         return "break"
 1221 
 1222     def openWithDefault(self, e=None):
 1223         if not self.itemSelected or 'g' not in self.itemSelected:
 1224             return(False)
 1225         # path = self.itemSelected['g']
 1226         path = expand_template(self.itemSelected['g'], self.itemSelected)
 1227 
 1228         if windoz:
 1229             os.startfile(path)
 1230             return()
 1231 
 1232         if mac:
 1233             cmd = 'open' + " {0}".format(path)
 1234         else:
 1235             cmd = 'xdg-open' + " {0}".format(path)
 1236         self.check_output(cmd)
 1237         return
 1238 
 1239     def printWithDefault(self, s, e=None):
 1240         fo = codecs.open(loop.tmpfile, 'w', loop.options['encoding']['file'])
 1241         # add a trailing formfeed
 1242         fo.write(s)
 1243         fo.close()
 1244         if windoz:
 1245             os.startfile(loop.tmpfile, "print")
 1246             return
 1247         else:
 1248             cmd = "lp -s -o media='letter' -o cpi=12 -o lpi=8 -o page-left=48 -o page-right=48 -o page-top=48 -o page-bottom=48 {0}\n".format(loop.tmpfile)
 1249             # cmd = "lpr -l {0}".format(loop.tmpfile)
 1250             self.check_output(cmd)
 1251             return
 1252 
 1253     def showUserDetails(self, e=None):
 1254         if not self.itemSelected or 'u' not in self.itemSelected:
 1255             return
 1256         if not loop.options['user_data']:
 1257             return
 1258         user = self.itemSelected['u']
 1259         if user in loop.options['user_data']:
 1260             detail = "\n".join(loop.options['user_data'][user])
 1261         else:
 1262             detail = _("No record was found for {0}".format(user))
 1263         self.textWindow(self, user, detail, opts=loop.options)
 1264         return
 1265 
 1266     def dateCalculator(self, event=None):
 1267         prompt = """\
 1268 Enter an expression of the form "x [+-] y" where x is a date and y is
 1269 either a date or a time period if "-" is used and a time period if "+"
 1270 is used. Both x and y can be followed by timezones, e.g.,
 1271      4/20 6:15p US/Central - 4/20 4:50p Asia/Shanghai
 1272                        = 14h25m
 1273      4/20 4:50p Asia/Shanghai + 14h25m US/Central
 1274               = 2014-04-20 18:15-0500
 1275 The local timezone is used when none is given."""
 1276         GetString(parent=self, title=_('date and time calculator'), prompt=prompt, opts=loop.options, process=date_calculator)
 1277         return
 1278 
 1279     def availableDateCalculator(self, event=None):
 1280         prompt = """\
 1281 Enter an expression of the form
 1282     start; end; busy
 1283 where start and end are dates and busy is comma separated list of
 1284 busy dates or busy intervals, .e.g,
 1285     6/1; 6/30; 6/2, 6/14-6/22, 6/5-6/9, 6/11-6/15, 6/17-6/29
 1286 returns:
 1287     Sun Jun 01
 1288     Tue Jun 03
 1289     Wed Jun 04
 1290     Tue Jun 10
 1291     Mon Jun 30\
 1292 """
 1293         GetString(parent=self, title=_('available dates calculator'), prompt=prompt, opts={}, process=availableDates, font=self.tkfixedfont)
 1294         return
 1295 
 1296     def exportToIcal(self, e=None):
 1297         if self.itemSelected:
 1298             self._exportItemToIcal()
 1299         else:
 1300             self._exportActiveToIcal()
 1301 
 1302     def exportToJson(self, e=None):
 1303         res = export_json(loop.file2uuids, loop.uuid2hash, loop.options)
 1304         if res:
 1305             prompt = _("Items successfully exported to etm-db.json")
 1306         else:
 1307             prompt = _("Could not export items.")
 1308 
 1309     def _exportItemToIcal(self):
 1310         if 'icsitem_file' in loop.options:
 1311             res = export_ical_item(self.itemSelected, loop.options['icsitem_file'])
 1312             if res:
 1313                 prompt = _("Selected item successfully exported to {0}".format(loop.options['icsitem_file']))
 1314             else:
 1315                 prompt = _("Could not export selected item.")
 1316         else:
 1317             prompt = "icsitem_file is not set in etmtk.cfg"
 1318         MessageWindow(self, 'Selected Item Export', prompt)
 1319 
 1320     def _exportActiveToIcal(self, event=None):
 1321         if 'icscal_file' in loop.options:
 1322             res = export_ical_active(loop.file2uuids, loop.uuid2hash, loop.options['icscal_file'], loop.calendars)
 1323             if res:
 1324                 prompt = _("Active calendars successfully exported to {0}".format(loop.options['icscal_file']))
 1325             else:
 1326                 prompt = _("Could not export active calendars.")
 1327         else:
 1328             prompt = "icscal_file is not set in etmtk.cfg"
 1329         MessageWindow(self, 'Active Calendar Export', prompt)
 1330 
 1331     def newItem(self, e=None):
 1332         # hack to avoid Ctrl-n activation
 1333         if e and e.char != "n":
 1334             return
 1335         if self.weekly or self.monthly:
 1336             master = self.canvas
 1337         else:
 1338             master = self.tree
 1339         if self.view in [AGENDA, WEEK, MONTH]:
 1340             if self.active_date:
 1341                 if self.itemSelected:
 1342                     if 's' in self.itemSelected:
 1343                         text = " @s {0}".format(self.active_date)
 1344                     elif 'c' in self.itemSelected:
 1345                         text = " @c {0}".format(self.itemSelected['c'])
 1346                     else:
 1347                         text = " "
 1348                 else:
 1349                     text = " @s {0}".format(self.active_date)
 1350             elif self.canvas_date:
 1351                 text = " @s {0}".format(self.canvas_date)
 1352             else:
 1353                 text = " "
 1354             changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed
 1355         elif self.view in [KEYWORD, NOTE] and self.itemSelected:
 1356             if self.itemSelected and 'k' in self.itemSelected:
 1357                 text = " @k {0}".format(self.itemSelected['k'])
 1358             else:
 1359                 text = ""
 1360             changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed
 1361         elif self.view in [TAG]:
 1362             if self.itemSelected and 't' in self.itemSelected:
 1363                 text = " @t {0}".format(", ".join(self.itemSelected['t']))
 1364             else:
 1365                 text = ""
 1366             changed = SimpleEditor(parent=self, master=master, start=text, options=loop.options).changed
 1367         else:
 1368             changed = SimpleEditor(parent=self, master=master, options=loop.options).changed
 1369         if changed:
 1370             logger.debug('changed, reloading data')
 1371             loop.do_update = True
 1372             self.updateAlerts()
 1373             if self.weekly:
 1374                 self.updateDay()
 1375                 self.showWeek()
 1376             elif self.monthly:
 1377                 self.updateDay()
 1378                 self.showMonth()
 1379             else:
 1380                 self.showView()
 1381 
 1382     def which(self, act, instance="xyz"):
 1383         prompt = "\n".join([
 1384             _("You have selected an instance of a repeating"),
 1385             _("item. What do you want to {0}?").format(act)])
 1386         if act == DELETE:
 1387             opt_lst = [
 1388                 _("this instance"),
 1389                 _("this and all subsequent instances"),
 1390                 _("all instances"),
 1391                 _("all previous instances")]
 1392         else:
 1393             opt_lst = [
 1394                 _("this instance"),
 1395                 _("this and all subsequent instances"),
 1396                 _("all instances")]
 1397 
 1398         if self.weekly or self.monthly:
 1399             master = self.canvas
 1400         else:
 1401             master = self.tree
 1402         index, value = OptionsDialog(parent=self, master=master, title=_("instance: {0}").format(instance), prompt=prompt, opts=opt_lst, yesno=False).getValue()
 1403         return index, value
 1404 
 1405     def copyItem(self, e=None):
 1406         """
 1407         newhsh = selected, rephsh = None
 1408         """
 1409         if not self.itemSelected:
 1410             return
 1411         if e and e.char != 'c':
 1412             return
 1413         if 'r' in self.itemSelected:
 1414             choice, value = self.which(COPY, self.dtSelected)
 1415             logger.debug("{0}: {1}".format(choice, value))
 1416             if not choice:
 1417                 self.tree.focus_set()
 1418                 return
 1419             self.itemSelected['_dt'] = self.dtSelected
 1420         else:
 1421             ans = self.confirm(
 1422                 parent=self.tree,
 1423                 title=_('Confirm'),
 1424                 prompt=_("Open a copy of this item?"))
 1425             if not ans:
 1426                 self.tree.focus_set()
 1427                 return
 1428             choice = 3
 1429 
 1430         if self.weekly or self.monthly:
 1431             master = self.canvas
 1432         else:
 1433             master = self.tree
 1434 
 1435         hsh_cpy = deepcopy(self.itemSelected)
 1436         hsh_cpy['fileinfo'] = None
 1437 
 1438         title = _("new item")
 1439         self.mode = 'new'
 1440         if choice in [1, 2]:
 1441             # we need to modify the copy according to the choice
 1442             dt = hsh_cpy['_dt'].replace(
 1443                 tzinfo=tzlocal()).astimezone(gettz(hsh_cpy['z']))
 1444             dtn = dt.replace(tzinfo=None)
 1445 
 1446             if choice == 1:
 1447                 # this instance
 1448                 for k in ['_r', 'o', '+', '-']:
 1449                     if k in hsh_cpy:
 1450                         del hsh_cpy[k]
 1451                 hsh_cpy['s'] = dtn
 1452 
 1453             elif choice == 2:
 1454                 # this and all subsequent instances
 1455                 if u'+' in hsh_cpy:
 1456                     tmp_cpy = []
 1457                     for d in hsh_cpy['+']:
 1458                         if d >= dtn:
 1459                             tmp_cpy.append(d)
 1460                     hsh_cpy['+'] = tmp_cpy
 1461                 if u'-' in hsh_cpy:
 1462                     tmp_cpy = []
 1463                     for d in hsh_cpy['-']:
 1464                         if d >= dtn:
 1465                             tmp_cpy.append(d)
 1466                     hsh_cpy['-'] = tmp_cpy
 1467                 hsh_cpy['s'] = dtn
 1468 
 1469         changed = SimpleEditor(parent=self, master=master, newhsh=hsh_cpy, rephsh=None, options=loop.options, title=title, modified=True).changed
 1470         if changed:
 1471             self.updateAlerts()
 1472             if self.weekly:
 1473                 self.updateDay()
 1474                 self.showWeek()
 1475             elif self.monthly:
 1476                 self.updateDay()
 1477                 self.showMonth()
 1478             else:
 1479                 self.showView(row=self.topSelected)
 1480         else:
 1481             if self.weekly or self.monthly:
 1482                 self.canvas.focus_set()
 1483             else:
 1484                 self.tree.focus_set()
 1485 
 1486     def setmessageAlert(self, e=None):
 1487         hsh = self.alertHsh
 1488         alertId = hsh['alertId'] # (summary, s)
 1489         if alertId in self.messageAlerts:
 1490             default_minutes = self.messageAlerts[alertId][0]
 1491         else:
 1492             default_minutes = loop.options['snooze_minutes']
 1493         msg = _("""\
 1494 -----------------------------------------------------------
 1495                     Repeat this alert?
 1496 This is the last alert scheduled for this item. To repeat it,
 1497   enter the number of minutes from now for the repetition.\
 1498 """)
 1499         alert_msg = _("""\
 1500 {0} ({1})
 1501 {2}
 1502 
 1503 {3}\
 1504 """.format(
 1505             expand_template('!summary!', hsh),
 1506             expand_template('!when!', hsh),
 1507             expand_template(self.options['alert_template'], hsh),
 1508             msg))
 1509 
 1510         minutes = GetRepeat(
 1511             parent=self,
 1512             title=_("alert - {0}".format(fmt_time(self.now, options=loop.options))),
 1513             prompt=alert_msg,
 1514             opts=[1, ],
 1515             default=default_minutes,
 1516             close=self.options['message_last'] * 1000
 1517         ).value
 1518         if not minutes:
 1519             if alertId in self.messageAlerts:
 1520                 del self.messageAlerts[alertId]
 1521                 self.updateAlerts()
 1522             return
 1523         # we're snoozing for "minutes" after hitting snooze
 1524         now = datetime.now()
 1525         if now.second > 30:
 1526             now = now + ONEMINUTE
 1527         now = now.replace(second=0, microsecond=0)
 1528         hsh['at'] = now + minutes * ONEMINUTE
 1529         wait = (hsh['at'] - datetime.now()).seconds * 1000
 1530         alert_id = self.after(wait, self.clearmessageAlert, alertId)
 1531         self.messageAlerts[alertId] = [minutes, hsh, alert_id]
 1532         self.updateAlertList()
 1533 
 1534     def clearmessageAlert(self, alertId):
 1535         if ('snooze_command' in self.options and self.options['snooze_command']):
 1536             ccmd = self.options['snooze_command']
 1537             self.check_output(ccmd)
 1538         else:
 1539             Tk.bell(self)
 1540         self.alertHsh = self.messageAlerts[alertId][1]
 1541         self.setmessageAlert()
 1542 
 1543     def setcountdownTimer(self, e=None):
 1544         """
 1545         get time period, default integer minutes, start timer
 1546         """
 1547         if self.countdownActive:
 1548             # prompt to cancel
 1549             ans = self.confirm(
 1550                 title=_('Confirm'),
 1551                 prompt=_("Cancel the countdown?"),
 1552                 parent=self.tree)
 1553             if ans:
 1554                 self.after_cancel(self.countdownActive)
 1555                 self.countdownActive = False
 1556                 self.countdownTime = None
 1557                 self.setcountdownStatus()
 1558                 self.countdownMinutes = loop.options['countdown_minutes']
 1559             return
 1560         prompt = _("""\
 1561                Start a countdown timer?
 1562 Enter an integer number of minutes for the timer below.""")
 1563 
 1564         mm = GetInteger(parent=self, title=_("Countdown timer"), prompt=prompt, opts=[1,], default=self.countdownMinutes).value
 1565         if not mm:
 1566             # reset the default
 1567             self.countdownMinutes = loop.options['countdown_minutes']
 1568             return
 1569         self.countdownMinutes = mm
 1570         ms = mm * 60 * 1000
 1571         self.countdownTime = datetime.now() + mm * ONEMINUTE
 1572         self.setcountdownStatus()
 1573         self.countdownActive = self.after(ms, self.clearcountdownTimer)
 1574 
 1575     def clearcountdownTimer(self, e=None):
 1576         self.countdownActive = False
 1577         self.countdownTime = None
 1578         self.setcountdownStatus()
 1579         if ('countdown_command' in self.options and self.options['countdown_command']):
 1580             ccmd = self.options['countdown_command']
 1581             self.check_output(ccmd)
 1582         else:
 1583             Tk.bell(self)
 1584         self.setcountdownTimer()
 1585 
 1586     def setcountdownStatus(self, e=None):
 1587         if self.countdownTime:
 1588             ds = fmt_time(self.countdownTime, seconds=True, options=self.options)
 1589             self.countdownStatus.set(ds)
 1590         else:
 1591             self.countdownStatus.set("")
 1592 
 1593 
 1594     def deleteItem(self, e=None):
 1595         if not self.itemSelected:
 1596             return
 1597         logger.debug('{0}: {1}'.format(self.itemSelected['_summary'], self.dtSelected))
 1598         indx = 3
 1599         if 'r' in self.itemSelected:
 1600             indx, value = self.which(DELETE, self.dtSelected)
 1601             logger.debug("{0}: {1}/{2}".format(self.dtSelected, indx, value))
 1602             if not indx:
 1603                 if self.weekly or self.monthly:
 1604                     self.canvas.focus_set()
 1605                 else:
 1606                     self.tree.focus_set()
 1607                 return
 1608             self.itemSelected['_dt'] = self.dtSelected
 1609         else:
 1610             ans = self.confirm(
 1611                 title=_('Confirm'),
 1612                 prompt=_("Delete this item?"),
 1613                 parent=self.tree)
 1614             if not ans:
 1615                 self.tree.focus_set()
 1616                 return
 1617         loop.item_hsh = self.itemSelected
 1618         loop.cmd_do_delete(indx)
 1619 
 1620         if 's' in self.itemSelected:
 1621             alertId = (self.itemSelected['_summary'], self.itemSelected['s'])
 1622         else:
 1623             alertId = None
 1624 
 1625         if alertId and alertId in self.messageAlerts:
 1626             # cancel exising snooze - no need to updateAlerts
 1627             self.after_cancel(self.messageAlerts[alertId][2])
 1628             del self.messageAlerts[alertId]
 1629             self.updateAlertList()
 1630 
 1631         self.updateAlerts()
 1632         if self.weekly:
 1633             self.canvas.focus_set()
 1634             self.updateDay()
 1635             self.showWeek()
 1636         elif self.monthly:
 1637             self.canvas.focus_set()
 1638             self.updateDay()
 1639             self.showMonth()
 1640         else:
 1641             self.tree.focus_set()
 1642             self.showView(row=self.topSelected)
 1643 
 1644         self.filterView()
 1645 
 1646     def moveItem(self, e=None):
 1647         if not self.itemSelected:
 1648             return
 1649         if e and e.char != 'm':
 1650             return
 1651         logger.debug('{0}: {1}'.format(self.itemSelected['_summary'], self.dtSelected))
 1652         oldrp, begline, endline = self.itemSelected['fileinfo']
 1653         oldfile = os.path.join(loop.options['datadir'], oldrp)
 1654         newfile = self.getDataFile(title="moving from {0}:".format(oldrp), start=oldfile)
 1655         if not (newfile and os.path.isfile(newfile)):
 1656             return
 1657         if newfile == oldfile:
 1658             return
 1659         ret = loop.append_item(self.itemSelected, newfile)
 1660         if ret != "break":
 1661             # post message and quit
 1662             prompt = _("""\
 1663 Adding item to {1} failed - aborted removing item from {2}""".format(
 1664                 newfile, oldfile))
 1665             MessageWindow(self, 'Error', prompt)
 1666             return
 1667         loop.item_hsh = self.itemSelected
 1668         ret = loop.delete_item()
 1669 
 1670         self.updateAlerts()
 1671         if self.weekly:
 1672             self.canvas.focus_set()
 1673             self.updateDay()
 1674             self.showWeek()
 1675         elif self.monthly:
 1676             self.canvas.focus_set()
 1677             self.updateDay()
 1678             self.showWeek()
 1679         else:
 1680             self.tree.focus_set()
 1681             self.showView(row=self.topSelected)
 1682 
 1683     def editItem(self, e=None):
 1684         if not self.itemSelected:
 1685             return
 1686         logger.debug('starting editItem: {0}; {1}, {2}'.format(self.itemSelected['_summary'], self.dtSelected, type(self.dtSelected)))
 1687 
 1688         if self.weekly or self.monthly:
 1689             master = self.canvas
 1690         else:
 1691             master = self.tree
 1692 
 1693         choice = 3
 1694         title = ETM
 1695         start_text = None
 1696         filename = None
 1697         if self.itemSelected['itemtype'] == '$':
 1698             start_text = self.itemSelected['entry']
 1699             hsh_rev = deepcopy(self.itemSelected)
 1700         elif 'r' in self.itemSelected:
 1701             # repeating
 1702             choice, value = self.which(EDIT, self.dtSelected)
 1703             logger.debug("{0}: {1}".format(choice, value))
 1704             if self.weekly or self.monthly:
 1705                 self.canvas.focus_set()
 1706             else:
 1707                 self.tree.focus_set()
 1708             logger.debug(('dtSelected: {0}, {1}'.format(type(self.dtSelected), self.dtSelected)))
 1709             self.itemSelected['_dt'] = self.dtSelected
 1710             if not choice:
 1711                 self.tree.focus_set()
 1712                 return
 1713         hsh_rev = hsh_cpy = None
 1714         self.mode = 2  # replace
 1715         if choice in [1, 2]:
 1716             self.mode = 3  # new and replace - both newhsh and rephsh
 1717             title = _("new item")
 1718             hsh_cpy = deepcopy(self.itemSelected)
 1719             hsh_rev = deepcopy(self.itemSelected)
 1720             # we will be editing and adding hsh_cpy and replacing hsh_rev
 1721 
 1722             dt = hsh_cpy['_dt'].replace(
 1723                 tzinfo=tzlocal()).astimezone(gettz(hsh_cpy['z']))
 1724             dtn = dt.replace(tzinfo=None)
 1725 
 1726             if choice == 1:
 1727                 # this instance
 1728                 tmp_cpy = []
 1729                 if 'f' in hsh_rev and hsh_rev['f']:
 1730                     for i in range(len(hsh_rev['f'])):
 1731                         d = hsh_rev['f'][i][0]
 1732                         if d == dtn:
 1733                             tmp_cpy.append(hsh_rev['f'][i])
 1734                     if tmp_cpy:
 1735                         hsh_cpy['f'] = tmp_cpy
 1736                         # dtn will be the done date, we need the due date for the copy
 1737                         dtn = hsh_cpy['f'][0][1]
 1738                     else:
 1739                         del hsh_cpy['f']
 1740 
 1741                 if '+' in hsh_rev and dtn in hsh_rev['+']:
 1742                     hsh_rev['+'].remove(dtn)
 1743                     if not hsh_rev['+'] and hsh_rev['r'] == 'l':
 1744                         del hsh_rev['r']
 1745                         del hsh_rev['_r']
 1746                 else:
 1747                     hsh_rev.setdefault('-', []).append(dt)
 1748                 for k in ['_r', 'o', '+', '-']:
 1749                     if k in hsh_cpy:
 1750                         del hsh_cpy[k]
 1751                 hsh_cpy['s'] = dtn
 1752 
 1753             elif choice == 2:
 1754                 # this and all subsequent instances
 1755                 tmp_cpy = []
 1756                 if 'f' in hsh_rev and hsh_rev['f']:
 1757                     for i in range(len(hsh_rev['f'])):
 1758                         d = hsh_rev['f'][i][0]
 1759                         if d >= dtn:
 1760                             tmp_cpy.append(hsh_rev['f'][i])
 1761                     if tmp_cpy:
 1762                         hsh_cpy['f'] = tmp_cpy
 1763                         # dtn will be the done date, we need the due date for the copy
 1764                         dtn = hsh_cpy['f'][0][1]
 1765                     else:
 1766                         del hsh_cpy['f']
 1767 
 1768                 tmp_rev = []
 1769                 for h in hsh_rev['_r']:
 1770                     if 'f' in h and h['f'] != u'l':
 1771                         h['u'] = dtn - ONEMINUTE
 1772                     tmp_rev.append(h)
 1773                 if tmp_rev:
 1774                     hsh_rev['_r'] = tmp_rev
 1775                 else:
 1776                     del hsh_rev['_r']
 1777 
 1778                 if u'+' in hsh_rev:
 1779                     tmp_rev = []
 1780                     tmp_cpy = []
 1781                     for d in hsh_rev['+']:
 1782                         if d < dtn:
 1783                             tmp_rev.append(d)
 1784                         else:
 1785                             tmp_cpy.append(d)
 1786                     if tmp_rev:
 1787                         hsh_rev['+'] = tmp_rev
 1788                     else:
 1789                         del hsh_rev['+']
 1790                     if tmp_cpy:
 1791                         hsh_cpy['+'] = tmp_cpy
 1792                     else:
 1793                         del hsh_cpy['+']
 1794 
 1795                 if u'-' in hsh_rev:
 1796                     tmp_rev = []
 1797                     tmp_cpy = []
 1798                     for d in hsh_rev['-']:
 1799                         if d < dtn:
 1800                             tmp_rev.append(d)
 1801                         else:
 1802                             tmp_cpy.append(d)
 1803                     if tmp_rev:
 1804                         hsh_rev['-'] = tmp_rev
 1805                     else:
 1806                         del hsh_rev['-']
 1807                     if tmp_cpy:
 1808                         hsh_cpy['-'] = tmp_cpy
 1809                     else:
 1810                         del hsh_cpy['-']
 1811                 hsh_cpy['s'] = dtn
 1812         else:  # replace
 1813             self.mode = 2
 1814             hsh_rev = deepcopy(self.itemSelected)
 1815 
 1816         logger.debug("mode: {0}; newhsh: {1}; rephsh: {2}".format(self.mode, hsh_cpy is not None, hsh_rev is not None))
 1817         changed = SimpleEditor(parent=self, master=master, file=filename, newhsh=hsh_cpy, rephsh=hsh_rev, options=loop.options, title=title, start=start_text).changed
 1818 
 1819         if changed:
 1820             logger.debug("starting if changed")
 1821             loop.do_update = True
 1822 
 1823             if 's' in self.itemSelected:
 1824                 alertId = (self.itemSelected['_summary'], self.itemSelected['s'])
 1825             else:
 1826                 alertId = None
 1827 
 1828             if alertId and alertId in self.messageAlerts:
 1829                 # cancel exising snooze - no need to updateAlerts
 1830                 self.after_cancel(self.messageAlerts[alertId][2])
 1831                 del self.messageAlerts[alertId]
 1832                 self.updateAlertList()
 1833 
 1834             self.updateAlerts()
 1835             if self.weekly:
 1836                 self.canvas.focus_set()
 1837                 self.updateDay()
 1838                 self.showWeek()
 1839             elif self.monthly:
 1840                 self.canvas.focus_set()
 1841                 self.updateDay()
 1842                 self.showMonth()
 1843             else:
 1844                 self.tree.focus_set()
 1845                 self.showView(row=self.topSelected)
 1846                 self.update_idletasks()
 1847             logger.debug("leaving if changed")
 1848 
 1849         else:
 1850             if self.weekly or self.monthly:
 1851                 self.canvas.focus_set()
 1852             else:
 1853                 self.tree.focus_set()
 1854         logger.debug('ending editItem')
 1855         self.filterView()
 1856         return
 1857 
 1858     def editItemFile(self, e=None):
 1859         if not self.itemSelected:
 1860             return
 1861         logger.debug('starting editItemFile: {0}; {1}, {2}, {3}'.format(self.itemSelected['_summary'], self.dtSelected, type(self.dtSelected), self.itemSelected['fileinfo']))
 1862         self.editFile(e, file=os.path.join(loop.options['datadir'], self.itemSelected['fileinfo'][0]), line=self.itemSelected['fileinfo'][1])
 1863 
 1864     def editFile(self, e=None, file=None, line=None, config=False):
 1865         if e and e.char and e.char not in ["F", "E", "C", "S", "P"]:
 1866             logger.debug('e.char: "{0}"'.format(e.char))
 1867             return
 1868         titlefile = os.path.normpath(relpath(file, loop.options['datadir']))
 1869         logger.debug('file: {0}; config: {1}'.format(file, config))
 1870         if self.weekly or self.monthly:
 1871             master = self.canvas
 1872         else:
 1873             master = self.tree
 1874         changed = SimpleEditor(parent=self, master=master, file=file, line=line, options=loop.options, title=titlefile).changed
 1875         logger.debug('changed: {0}'.format(changed))
 1876         if changed:
 1877             logger.debug("config: {0}".format(config))
 1878             if config:
 1879                 current_options = deepcopy(loop.options)
 1880                 (user_options, options, use_locale) = data.get_options(
 1881                     d=loop.options['etmdir'])
 1882                 loop.options = self.options = options
 1883                 if options['calendars'] != current_options['calendars']:
 1884                     self.updateCalendars()
 1885             loop.do_update = True
 1886 
 1887             logger.debug("changed - calling updateAlerts")
 1888             self.updateAlerts()
 1889             if self.weekly:
 1890                 self.canvas.focus_set()
 1891                 self.updateDay()
 1892                 self.showWeek()
 1893             elif self.monthly:
 1894                 self.canvas.focus_set()
 1895                 self.updateDay()
 1896                 self.showMonth()
 1897             else:
 1898                 self.tree.focus_set()
 1899                 self.showView()
 1900         return changed
 1901 
 1902     def editConfig(self, e=None):
 1903         file = loop.options['config']
 1904         self.editFile(e, file=file, config=True)
 1905 
 1906     def editCfgFiles(self, e=None):
 1907         other = []
 1908         if 'colors' in loop.options:
 1909             other.append(loop.options['colors'])
 1910         if 'cfg_files' in loop.options:
 1911             for key in ['completions', 'reports', 'users']:
 1912                 other.extend(loop.options['cfg_files'][key])
 1913         prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.cfg', other=other)
 1914         ret = FileChoice(self, "open configuration file", prefix=prefix, list=tuples).returnValue()
 1915         if not (ret and os.path.isfile(ret)):
 1916             return False
 1917         self.editFile(e, file=ret, config=True)
 1918 
 1919     def getReportsFile(self, e=None):
 1920         ret = FileChoice(self, title="append to reports file", prefix=self.loop.options['etmdir'], list=self.loop.options['report_files']).returnValue()
 1921         if not (ret and os.path.isfile(ret)):
 1922             return False
 1923         return ret
 1924 
 1925     def getDataFile(self, e=None, title="data file", start=''):
 1926         prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.txt')
 1927         ret = FileChoice(self, title=title, prefix=prefix, list=tuples, start=start).returnValue()
 1928         if not (ret and os.path.isfile(ret)):
 1929             return False
 1930         return ret
 1931 
 1932     def editScratch(self, e=None):
 1933         file = loop.options['scratchpad']
 1934         self.editFile(e, file=file)
 1935 
 1936     def editColors(self, e=None):
 1937         file = loop.options['colors']
 1938         self.editFile(e, file=file)
 1939 
 1940     def editData(self, e=None):
 1941         if e and e.char != "F":
 1942             return
 1943         prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*.txt', all=False)
 1944         ret = FileChoice(self, "open data file", prefix=prefix, list=tuples).returnValue()
 1945         if not (ret and os.path.isfile(ret)):
 1946             return False
 1947         self.editFile(e, file=ret)
 1948 
 1949     def newFile(self, e=None):
 1950         if e and e.char != "N":
 1951             return
 1952         other = [os.path.join(loop.options['etmdir'], 'etmtk.cfg')]
 1953         if 'cfg_files' in loop.options:
 1954             for key in ['completions', 'reports', 'users']:
 1955                 other.extend(loop.options['cfg_files'][key])
 1956         prefix, tuples = getFileTuples(loop.options['datadir'], include=r'*', other=other, all=True)
 1957         filename = FileChoice(self, "create new file", prefix=prefix, list=tuples, new=True).returnValue()
 1958         if not filename:
 1959             return
 1960         if os.path.isfile(filename):
 1961             prompt = _("Aborting. File {0} already exists.").format(filename)
 1962             MessageWindow(self, title=_("new file"), prompt=prompt)
 1963         else:
 1964             pth = os.path.split(filename)[0]
 1965             if not os.path.isdir(pth):
 1966                 os.makedirs(pth)
 1967             fo = codecs.open(filename, 'w', loop.options['encoding']['file'])
 1968             fo.write("")
 1969             fo.close()
 1970             prompt = _('created: {0}').format(filename)
 1971             if self.weekly or self.monthly:
 1972                 p = self.canvas
 1973             else:
 1974                 p = self.tree
 1975             ans = self.confirm(
 1976                 parent=p,
 1977                 title=_('etm'),
 1978                 prompt=_("file: {0}\nhas been created.\nOpen it for editing?").format(filename))
 1979             if ans:
 1980                 self.editFile(None, filename)
 1981 
 1982     def finishItem(self, e=None):
 1983         if e and e.char != "f":
 1984             return
 1985         if not (self.itemSelected and self.itemSelected['itemtype'] in ['-', '+', '%']):
 1986             return
 1987         prompt = _("""\
 1988 Enter the completion date for the item or return an empty string to
 1989 use the current date. Relative dates and fuzzy parsing are supported.""")
 1990         d = GetDateTime(parent=self, title=_('date'), prompt=prompt)
 1991         chosen_day = d.value
 1992         if chosen_day is None:
 1993             return ()
 1994         logger.debug('completion date: {0}'.format(chosen_day))
 1995         loop.item_hsh = self.itemSelected
 1996         if 's' in self.itemSelected:
 1997             alertId = (self.itemSelected['_summary'], self.itemSelected['s'])
 1998         else:
 1999             alertId = None
 2000 
 2001         loop.cmd_do_finish(chosen_day, options=loop.options)
 2002 
 2003         if alertId and alertId in self.messageAlerts:
 2004             # cancel exising snooze - no need to updateAlerts
 2005             self.after_cancel(self.messageAlerts[alertId][2])
 2006             del self.messageAlerts[alertId]
 2007             self.updateAlertList()
 2008         else:
 2009             self.updateAlerts()
 2010 
 2011         if self.weekly:
 2012             self.canvas.focus_set()
 2013             self.updateDay()
 2014             self.showWeek()
 2015         elif self.monthly:
 2016             self.canvas.focus_set()
 2017             self.updateDay()
 2018             self.showMonth()
 2019         else:
 2020             self.tree.focus_set()
 2021             self.showView(row=self.topSelected)
 2022         self.filterView()
 2023 
 2024     def rescheduleItem(self, e=None):
 2025         if e and e.char != 'r':
 2026             return
 2027         if not self.itemSelected:
 2028             return
 2029         loop.item_hsh = self.itemSelected
 2030         if self.dtSelected:
 2031             loop.old_dt = old_dt = self.dtSelected
 2032             title = _('rescheduling {0}').format(old_dt.strftime(
 2033                 rrulefmt))
 2034         else:
 2035             loop.old_dt = None
 2036             title = _('scheduling an undated item')
 2037         logger.debug('dtSelected: {0}'.format(self.dtSelected))
 2038         prompt = _("""\
 2039 Enter the new date and time for the item or return an empty string to
 2040 use the current time. Relative dates and fuzzy parsing are supported.""")
 2041         dt = GetDateTime(parent=self, title=title, prompt=prompt)
 2042         new_dt = dt.value
 2043         if new_dt is None:
 2044             return
 2045 
 2046         if 's' in self.itemSelected:
 2047             alertId = (self.itemSelected['_summary'], self.itemSelected['s'])
 2048         else:
 2049             alertId = None
 2050 
 2051         if alertId and alertId in self.messageAlerts:
 2052             # cancel exising snooze - no need to updateAlerts
 2053             self.after_cancel(self.messageAlerts[alertId][2])
 2054             del self.messageAlerts[alertId]
 2055             self.updateAlertList()
 2056         else:
 2057             self.updateAlerts()
 2058 
 2059         new_dtn = new_dt.astimezone(gettz(self.itemSelected['z'])).replace(tzinfo=None)
 2060         logger.debug('rescheduled from {0} to {1}'.format(self.dtSelected, new_dtn))
 2061         loop.cmd_do_reschedule(new_dtn)
 2062 
 2063         self.updateAlerts()
 2064         if self.weekly:
 2065             self.canvas.focus_set()
 2066             self.updateDay()
 2067             self.showWeek()
 2068         elif self.monthly:
 2069             self.canvas.focus_set()
 2070             self.updateDay()
 2071             self.showMonth()
 2072         else:
 2073             self.tree.focus_set()
 2074             self.showView(row=self.topSelected)
 2075         self.filterView()
 2076 
 2077     def scheduleNewItem(self, e=None):
 2078         if e and e.char != 's':
 2079             return
 2080         if not self.itemSelected:
 2081             return
 2082         loop.item_hsh = self.itemSelected
 2083         if self.dtSelected:
 2084             loop.old_dt = self.dtSelected
 2085             title = _('adding new instance')
 2086         else:
 2087             loop.old_dt = None
 2088             title = _('scheduling an undated item')
 2089         logger.debug('dtSelected: {0}'.format(self.dtSelected))
 2090         prompt = _("""\
 2091 Enter the new date and time for the item or return an empty string to
 2092 use the current time. Relative dates and fuzzy parsing are supported.""")
 2093         dt = GetDateTime(parent=self, title=title, prompt=prompt)
 2094         new_dt = dt.value
 2095         if new_dt is None:
 2096             return
 2097         new_dtn = new_dt.astimezone(gettz(self.itemSelected['z'])).replace(tzinfo=None)
 2098         logger.debug('scheduled new instance: {0}'.format(new_dtn))
 2099         loop.cmd_do_schedulenew(new_dtn)
 2100 
 2101         self.updateAlerts()
 2102         if self.weekly:
 2103             self.canvas.focus_set()
 2104             self.updateDay()
 2105             self.showWeek()
 2106         elif self.monthly:
 2107             self.canvas.focus_set()
 2108             self.updateDay()
 2109             self.showMonth()
 2110         else:
 2111             self.tree.focus_set()
 2112             self.showView(row=self.topSelected)
 2113         self.filterView()
 2114 
 2115     def showDateTimeDetails(self, e=None):
 2116         if not self.itemSelected:
 2117             return
 2118         if e and e.char != 'd':
 2119             return
 2120         pre = post = warn = ""
 2121         hsh = self.itemSelected
 2122 
 2123         if 'r' in hsh:
 2124             pre = _("Repeating ")
 2125         elif 's' in hsh:
 2126             dt = hsh['s']
 2127             if hsh['itemtype'] in ['*', '~']:
 2128                 dtfmt = fmt_shortdatetime(hsh['s'], self.options)
 2129             else:
 2130                 if not dt.hour and not dt.minute:
 2131                     dtfmt = fmt_date(dt, short=True)
 2132                 else:
 2133                     dtfmt = fmt_shortdatetime(hsh['s'], self.options)
 2134             post = _(" starting {0}.").format(dtfmt)
 2135         else:  # unscheduled
 2136             pre = _("Unscheduled ")
 2137 
 2138         prompt = "{0}{1}{2}".format(pre, type2Text[hsh['itemtype']], post)
 2139 
 2140         if 'r' in hsh:
 2141             showing_all, reps = get_reps(self.loop.options['bef'], hsh)
 2142 
 2143             try:
 2144                 repsfmt = [unicode(x.strftime(rrulefmt)) for x in reps]
 2145             except:
 2146                 repsfmt = [unicode(x.strftime("%X %x")) for x in reps]
 2147             logger.debug("{0}: {1}".format(showing_all, repsfmt))
 2148             if showing_all:
 2149                 reps = ALLREPS
 2150             else:
 2151                 reps = SOMEREPS
 2152             prompt = "{0}, {1}:\n\n  {2}".format(prompt, reps, "\n  ".join(repsfmt))
 2153 
 2154         self.textWindow(parent=self, title=_("Date and time details"), prompt=prompt, opts=self.options)
 2155 
 2156     def showAlerts(self, e=None):
 2157         # hack to avoid activating with Ctrl-a
 2158         if e and e.char != "a":
 2159             return
 2160         t = _('remaining alerts for today')
 2161         header = "{0:^10}\t{1:^7}\t{2:^10}{3:<26}".format(
 2162             _('alert'),
 2163             _('start'),
 2164             _('type'),
 2165             _('summary'))
 2166         divider = '-' * 55
 2167 
 2168         if self.activeAlerts:
 2169             s = '%s\n%s\n%s' % (
 2170                 header, divider, "\n".join(
 2171                     ["{0:^10}\t{1:^7}\t{2:^10}{3:<26}".format(x[1], x[2], x[3], x[4]) for x in self.activeAlerts]))
 2172         else:
 2173             s = _("None                                 ")
 2174         self.textWindow(self, t, s, opts=self.options)
 2175 
 2176     def adjustIdle(self, e=None):
 2177         if not self.actionTimer.currentTimer:
 2178             return
 2179         timer = self.actionTimer.currentTimer
 2180         now = datetime.now()
 2181         restart = False
 2182         if self.actionTimer.idlestart:
 2183             idle = (now - self.actionTimer.idlestart) + self.actionTimer.idletime
 2184             restart = True
 2185         elif self.actionTimer.idletime:
 2186             idle = self.actionTimer.idletime
 2187         else:
 2188             idle = 0 * ONEMINUTE
 2189 
 2190         if idle < ONEMINUTE:
 2191             return
 2192 
 2193         # get idle time in integer minutes
 2194         im = idle.days * 24 * 60
 2195         im += idle.seconds // 60
 2196 
 2197         hsh = self.actionTimer.activeTimers[timer]
 2198         tot = hsh['total']
 2199         if self.actionTimer.currentStatus == RUNNING:
 2200             tot += now - hsh['start']
 2201 
 2202         prompt = _("""\
 2203 Currently "{0}" has an elapsed time of {1}.
 2204 Enter the number of minutes that you would like to add to
 2205 this timer and subtract from idle time, currently {2}.""".format(timer, fmt_period(tot), fmt_period(idle)))
 2206 
 2207         mm = GetInteger(parent=self, title=_("Adjust timer"), prompt=prompt, opts=[1,im], default=1).value
 2208         if not mm:
 2209             return
 2210 
 2211         d = mm * ONEMINUTE
 2212 
 2213         if restart:
 2214             self.actionTimer.idlestart = now
 2215             self.actionTimer.idletime = idle
 2216 
 2217         self.actionTimer.idletime -= d
 2218         self.actionTimer.activeTimers[timer]['total'] += d
 2219         self.updateTimerStatus()
 2220         self.actionTimer.saveTimers()
 2221 
 2222 
 2223     def agendaView(self, e=None):
 2224         self.setView(AGENDA)
 2225 
 2226     def dayView(self, e=None):
 2227         self.setView(DAY)
 2228 
 2229     def pathView(self, e=None):
 2230         self.setView(PATH)
 2231 
 2232     def keywordView(self, e=None):
 2233         self.setView(KEYWORD)
 2234 
 2235     def tagView(self, e=None):
 2236         self.setView(TAG)
 2237 
 2238     def customView(self, e=None):
 2239         # TODO: finish this
 2240         self.content.delete("1.0", END)
 2241         # self.fltr.forget()
 2242         self.clearTree()
 2243         self.setView(CUSTOM)
 2244         pass
 2245 
 2246     def noteView(self, e=None):
 2247         self.setView(NOTE)
 2248 
 2249     def updateDay(self, e=None):
 2250         self.mode = "command"
 2251         self.process_input(event=e, cmd='d')
 2252 
 2253     def setView(self, view, row=None):
 2254         self.rowSelected = None
 2255         if view in [DAY, WEEK, MONTH]:
 2256             self.toolsmenu.entryconfig(1, state="normal")
 2257         else:
 2258             self.toolsmenu.entryconfig(1, state="disabled")
 2259         if self.weekly and view not in [DAY, WEEK]:
 2260             self.closeWeekly()
 2261         if self.monthly and view not in [DAY, MONTH]:
 2262             self.closeMonthly()
 2263         if view == CUSTOM:
 2264             logger.debug('showing custom_box')
 2265             self.fltr.forget()
 2266             self.custom_box.pack(side="left", fill=X, padx=0, expand=1)
 2267             self.custom_box.focus_set()
 2268             for i in range(len(self.rm_options)):
 2269                 self.custommenu.entryconfig(i, state="normal")
 2270         else:
 2271             if self.view == CUSTOM:
 2272                 # we're leaving custom view
 2273                 logger.debug('removing custom_box')
 2274                 self.custom_box.forget()
 2275                 self.fltr.pack(side="left", padx=0, expand=1, fill=X)
 2276                 for i in range(len(self.rm_options)):
 2277                     self.custommenu.entryconfig(i, state="disabled")
 2278                 self.saveSpecs()
 2279         self.view = view
 2280         logger.debug("setView view: {0}. Calling showView.".format(view))
 2281         self.showView(row=row)
 2282 
 2283     def filterView(self, e=None, *args):
 2284         self.depth2id = {}
 2285         fltr = self.filterValue.get()
 2286         cn = self.vm_options[self.vm_opts.index(self.view)][1]
 2287         if cn in ['w', 'm']:
 2288             # with week or month views use the day view command
 2289             cn = 'd'
 2290         cmd = "{0} {1}".format(cn, fltr)
 2291         self.mode = 'command'
 2292         self.process_input(event=e, cmd=cmd)
 2293 
 2294     def showView(self, e=None, row=None):
 2295         tt = TimeIt(loglevel=2, label="{0} view".format(self.view))
 2296         self.depth2id = {}
 2297         self.currentView.set(self.view)
 2298         fltr = self.filterValue.get()
 2299         if self.view != CUSTOM:
 2300             cmd = "{0} {1}".format(
 2301                 self.vm_options[self.vm_opts.index(self.view)][1], fltr)
 2302             self.mode = 'command'
 2303             self.process_input(event=e, cmd=cmd)
 2304             if row:
 2305                 row = max(0, row - 1)
 2306                 self.tree.yview(row)
 2307         tt.stop()
 2308 
 2309     def showBusyPeriods(self, e=None):
 2310         if e and e.char != "b":
 2311             return
 2312         if self.busy_info is None:
 2313             return()
 2314         theweek, weekdays, busy_lst, occasion_lst = self.busy_info
 2315         theweek = _("Busy periods in {0}").format(theweek)
 2316 
 2317         lines = [theweek, '-' * len(theweek)]
 2318         ampm = loop.options['ampm']
 2319         s1 = s2 = ''
 2320         for i in range(len(busy_lst)):
 2321             times = []
 2322             for tup in busy_lst[i]:
 2323                 t1 = max(7 * 60, tup[0])
 2324                 t2 = min(23 * 60, max(420, tup[1]))
 2325                 if t1 != t2:
 2326                     t1h, t1m = (t1 // 60, t1 % 60)
 2327                     t2h, t2m = (t2 // 60, t2 % 60)
 2328                     if ampm:
 2329                         if t1h == 12:
 2330                             s1 = 'pm'
 2331                         elif t1h > 12:
 2332                             t1h -= 12
 2333                             s1 = 'pm'
 2334                         else:
 2335                             s1 = 'am'
 2336                         if t2h == 12:
 2337                             s2 = 'pm'
 2338                         elif t2h > 12:
 2339                             t2h -= 12
 2340                             s2 = 'pm'
 2341                         else:
 2342                             s2 = 'am'
 2343 
 2344                     T1 = "%d:%02d%s" % (t1h, t1m, s1)
 2345                     T2 = "%d:%02d%s" % (t2h, t2m, s2)
 2346 
 2347                     times.append("%s-%s" % (T1, T2))
 2348             if times:
 2349                 lines.append("%s: %s" % (weekdays[i], "; ".join(times)))
 2350         s = "\n".join(lines)
 2351         self.textWindow(parent=self, title=_('busy times'), prompt=s, opts=self.options)
 2352 
 2353     def showFreePeriods(self, e=None):
 2354         if e and e.char != 'f':
 2355             return
 2356         if self.busy_info is None or 'freetimes' not in loop.options:
 2357             return()
 2358         ampm = loop.options['ampm']
 2359         om = loop.options['freetimes']['opening']
 2360         cm = loop.options['freetimes']['closing']
 2361         mm = loop.options['freetimes']['minimum']
 2362         bm = loop.options['freetimes']['buffer']
 2363         prompt = _("""\
 2364 Enter the shortest time period you want displayed in minutes.""")
 2365         mm = GetInteger(parent=self, title=_("depth"), prompt=prompt, opts=[0], default=mm).value
 2366         if mm is None:
 2367             self.canvas.focus_set()
 2368             return
 2369         theweek, weekdays, busy_lst, occasion_lst = self.busy_info
 2370         theweek = _("Free periods in {0}").format(theweek)
 2371         lines = [theweek, '-' * len(theweek)]
 2372         s1 = s2 = ''
 2373         for i in range(len(busy_lst)):
 2374             times = []
 2375             busy = []
 2376             for tup in busy_lst[i]:
 2377                 t1 = max(om, tup[0] - bm)
 2378                 t2 = min(cm, max(om, tup[1]) + bm)
 2379                 if t2 > t1:
 2380                     busy.append((t1, t2))
 2381             lastend = om
 2382             free = []
 2383             for tup in busy:
 2384                 if tup[0] - lastend >= mm:
 2385                     free.append((lastend, tup[0]))
 2386                 lastend = tup[1]
 2387             if cm - lastend >= mm:
 2388                 free.append((lastend, cm))
 2389             for tup in free:
 2390                 t1, t2 = tup
 2391                 if t1 != t2:
 2392                     t1h, t1m = (t1 // 60, t1 % 60)
 2393                     t2h, t2m = (t2 // 60, t2 % 60)
 2394                     if ampm:
 2395                         if t1h == 12:
 2396                             s1 = 'pm'
 2397                         elif t1h > 12:
 2398                             t1h -= 12
 2399                             s1 = 'pm'
 2400                         else:
 2401                             s1 = 'am'
 2402                         if t2h == 12:
 2403                             s2 = 'pm'
 2404                         elif t2h > 12:
 2405                             t2h -= 12
 2406                             s2 = 'pm'
 2407                         else:
 2408                             s2 = 'am'
 2409                     T1 = "%d:%02d%s" % (t1h, t1m, s1)
 2410                     T2 = "%d:%02d%s" % (t2h, t2m, s2)
 2411                     times.append("%s-%s" % (T1, T2))
 2412             if times:
 2413                 lines.append("%s: %s" % (weekdays[i], "; ".join(times)))
 2414         lines.append('-' * len(theweek))
 2415         lines.append("Only periods of at least {0} minutes are displayed.".format(mm))
 2416         s = "\n".join(lines)
 2417         self.textWindow(parent=self, title=_('free times'), prompt=s, opts=self.options)
 2418 
 2419     def getWeek(self, chosen_day=None):
 2420         if chosen_day is None:
 2421             chosen_day = get_current_time().date()
 2422         yn, wn, dn = chosen_day.isocalendar()
 2423         self.prev_week = chosen_day - 7 * ONEDAY
 2424         self.next_week = chosen_day + 7 * ONEDAY
 2425         self.curr_week = chosen_day
 2426         if dn > 1:
 2427             days = dn - 1
 2428         else:
 2429             days = 0
 2430         if type(chosen_day) is not date:
 2431             chosen_day = chosen_day.date()
 2432         self.week_beg = weekbeg = chosen_day - days * ONEDAY
 2433         logger.debug('week_beg: {0}'.format(self.week_beg))
 2434         weekend = chosen_day + (6 - days) * ONEDAY
 2435         weekdays = []
 2436         weekdates = []
 2437 
 2438         day = weekbeg
 2439         self.active_date = weekbeg
 2440         busy_lst = []
 2441         occasion_lst = []
 2442         matching = self.cal_regex is not None and self.default_regex is not None
 2443         while day <= weekend:
 2444             weekdays.append(s2or3(day.strftime("%a")))
 2445             weekdates.append(leadingzero.sub("", day.strftime("%d")))
 2446             isokey = day.isocalendar()
 2447 
 2448             day = day + ONEDAY
 2449 
 2450         ybeg = weekbeg.year
 2451         yend = weekend.year
 2452         mbeg = weekbeg.month
 2453         mend = weekend.month
 2454         # busy_lst: list of days 0 (monday) - 6 (sunday) where each day is a list of (start minute, end minute, id, summary-time str and file info) tuples
 2455 
 2456         if mbeg == mend:
 2457             header = "{0} - {1}".format(
 2458                 fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%d, %Y'))
 2459         elif ybeg == yend:
 2460             header = "{0} - {1}".format(
 2461                 fmt_dt(weekbeg, '%b %d'), fmt_dt(weekend, '%b %d, %Y'))
 2462         else:
 2463             header = "{0} - {1}".format(
 2464                 fmt_dt(weekbeg, '%b %d, %Y'), fmt_dt(weekend, '%b %d, %Y'))
 2465         header = leadingzero.sub('', header)
 2466         theweek = _("{0} {1}: {2}").format(_("Week"), wn, header)
 2467         return theweek, weekdays, weekdates
 2468 
 2469 
 2470     def closeWeekly(self, event=None):
 2471         self.week_height = self.topwindow.panecget(self.toppane, "height")
 2472         self.topwindow.forget(self.toppane)
 2473         self.weekly = False
 2474         self.tree.pack(fill="both", expand=1, padx=4, pady=0)
 2475         self.update_idletasks()
 2476         for i in range(4, 6):
 2477             self.toolsmenu.entryconfig(i, state="disabled")
 2478 
 2479         self.bind("<Control-f>", self.setFilter)
 2480 
 2481     def showWeekly(self, event=None, chosen_day=None):
 2482         """
 2483         Open the canvas at the current week
 2484         """
 2485         self.custom_box.forget()
 2486         tt = TimeIt(loglevel=2, label="week view")
 2487         logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date))
 2488         if self.weekly:
 2489             # we're in weekview already
 2490             return
 2491         if self.monthly:
 2492             self.closeMonthly()
 2493         self.content.delete("1.0", END)
 2494 
 2495         self.setView(DAY)
 2496 
 2497         self.view = WEEK
 2498 
 2499         if chosen_day is not None:
 2500             self.chosen_date = chosen_day
 2501         elif self.active_date:
 2502             self.chosen_date = self.active_date
 2503         else:
 2504             self.chosen_date = get_current_time().date()
 2505 
 2506         self.topwindow.add(self.toppane, padx=0, pady=0, before=self.botwindow, height=self.week_height)
 2507 
 2508         if self.options['ampm']:
 2509             self.hours = ["{0}am".format(i) for i in range(7, 12)] + ['12pm'] + ["{0}pm".format(i) for i in range(1, 12)]
 2510         else:
 2511             self.hours = ["{0}:00".format(i) for i in range(7, 24)]
 2512         for i in range(4, 6):
 2513             self.toolsmenu.entryconfig(i, state="normal")
 2514         self.showWeek(event=event, week=0)
 2515         self.weekly = True
 2516         self.canvas.focus_set()
 2517         tt.stop()
 2518 
 2519     def priorWeekMonth(self, event=None):
 2520         if self.weekly:
 2521             self.showWeek(event, week=-1)
 2522         elif self.monthly:
 2523             self.showMonth(event, month=-1)
 2524 
 2525     def nextWeekMonth(self, event=None):
 2526         if self.weekly:
 2527             self.showWeek(event=event, week=1)
 2528         elif self.monthly:
 2529             self.showMonth(event=event, month=1)
 2530 
 2531     def showWeek(self, event=None, week=None):
 2532         self.selectedId = None
 2533 
 2534         matching = self.cal_regex is not None and self.default_regex is not None
 2535 
 2536         busy_dates = []
 2537 
 2538         self.current_day = get_current_time().date()
 2539         logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes))
 2540         self.x_win = self.toppane.winfo_width()
 2541         # self.y_win = self.canvas.winfo_height()
 2542         self.y_win = self.toppane.winfo_height()
 2543         logger.debug("win: {0}, {1}".format(self.x_win, self.y_win))
 2544         logger.debug("event: {0}, week: {1}, chosen_day: {2}".format(event, week, self.chosen_date))
 2545         use_active = False
 2546         if week in [-1, 0, 1]:
 2547             if week == 0:
 2548                 day = get_current_time()
 2549             elif week == 1:
 2550                 day = self.next_week
 2551             elif week == -1:
 2552                 day = self.prev_week
 2553             if type(day) is not date:
 2554                 day = day.date()
 2555             self.chosen_date = day
 2556         elif self.active_date:
 2557             use_active = True
 2558             self.year_month = [self.active_date.year, self.active_date.month]
 2559             day = self.chosen_date = self.active_date
 2560         else:
 2561             return
 2562         logger.debug('week active_date: {0}'.format(self.active_date))
 2563         theweek, weekdays, weekdates = self.getWeek(day)
 2564         busy_lst = []
 2565         occasion_lst = []
 2566         weekdaynum = day.isocalendar()[2]
 2567         # reset day to Monday of the current week
 2568         day = day - (weekdaynum - 1) * ONEDAY
 2569         if use_active:
 2570             scrolldate = self.chosen_date
 2571             self.canvas_idpos = weekdaynum - 1
 2572         else:
 2573             scrolldate = day
 2574             self.canvas_idpos = 0
 2575         self.scrollToDate(scrolldate)
 2576         self.OnSelect()
 2577         self.canvas.delete("all")
 2578         l = 5
 2579         r = 5
 2580         t = 22
 2581         b = 5
 2582         if event:
 2583             logger.debug('event: {0}'.format(event))
 2584             w, h = event.width, event.height
 2585             if type(w) is int and type(h) is int:
 2586                 self.canvas_width = w
 2587                 self.canvas_height = h
 2588             else:
 2589                 w = self.canvas.winfo_width()
 2590                 h = self.canvas.winfo_height()
 2591         else:
 2592             w = self.canvas.winfo_width()
 2593             h = self.week_height
 2594         logger.debug("w: {0} {1}; h: {2} {3}, l: {4} {5}, t: {6} {7}".format(w, type(w), h, type(h), l, type(l), t, type(t)))
 2595         self.margins = (w, h, l, r, t, b)
 2596         self.week_x = x_ = Decimal(w - 1 - l - r) / Decimal(7)
 2597         self.week_y = y_ = Decimal(h - 1 - t - b)
 2598         logger.debug("x: {0}, y: {1}".format(x_, y_))
 2599 
 2600         # week
 2601         self.currentView.set(theweek)
 2602         self.busyHsh = {}
 2603 
 2604         # occasions
 2605         busy_ids = set([])
 2606         monthid2date = {}
 2607 
 2608         # weekdays
 2609         intervals = [360, 720, 1080, 1440]
 2610         busywidth = 2
 2611         offset = 6
 2612         indent = 7
 2613 
 2614         nightcolor = self.BUSYBAR
 2615         morningcolor = self.BUSYBAR
 2616         afternooncolor = self.BUSYBAR
 2617         eveningcolor = self.BUSYBAR
 2618 
 2619         conf_ids = []
 2620         self.today_id = None
 2621         self.timeline = None
 2622         self.last_minutes = None
 2623 
 2624 
 2625         for i in range(7):
 2626             fill = self.CURRDATE
 2627             flagcolor = None
 2628             busytimes = 0
 2629             start_x = l + i * x_
 2630             end_x = start_x + x_
 2631             start_y = int(t)
 2632             end_y = start_y + y_
 2633             xy = int(start_x), int(start_y), int(end_x), int(end_y)
 2634             p = int(l + x_ / 2 + x_ * i), int(t + y_ / 2)
 2635             tl_x = bl_x = int(l + x_ * i)
 2636             tl_y = tr_y = int(t)
 2637             tr_x = br_x = int(tl_x + x_)
 2638             bl_y = br_y = int(tl_y + y_)
 2639             w_ = x_ - 12
 2640             h_ = y_ - 12
 2641 
 2642             thisdate = (day + i * ONEDAY)
 2643             isokey = thisdate.isocalendar()
 2644             tags = []
 2645             id = self.canvas.create_rectangle(xy, outline="", width=0)
 2646             busy_ids.add(id)
 2647             monthid2date[id] = thisdate
 2648             today = (thisdate == self.current_day)
 2649             if today:
 2650                 tags.append('current_day')
 2651             if loop.occasions is not None and isokey in loop.occasions:
 2652                 bt = []
 2653                 for item in loop.occasions[isokey]:
 2654                     it = list(item)
 2655                     if matching:
 2656                         if not self.cal_regex.match(it[-1]):
 2657                             continue
 2658                         mtch = (self.default_regex.match(it[-1]) is not None)
 2659                     else:
 2660                         mtch = True
 2661                     it.append(mtch)
 2662                     item = tuple(it)
 2663                     bt.append(item)
 2664                 occasion_lst.append(bt)
 2665                 if bt:
 2666                     if not today:
 2667                         tags.append('occasion')
 2668                     self.busyHsh.setdefault(id, []).extend(["^ {0}".format(x[0]) for x in bt])
 2669             else:
 2670                 occasion_lst.append([])
 2671 
 2672             if loop.busytimes is not None and isokey in loop.busytimes:
 2673                 bt = []
 2674                 overlap = False
 2675                 for item in loop.busytimes[isokey]:
 2676                     it = list(item)
 2677                     if it[0] == it[1]:
 2678                         # skip reminders
 2679                         continue
 2680                     if matching:
 2681                         if not self.cal_regex.match(it[-1]):
 2682                             continue
 2683                         mtch = (self.default_regex.match(it[-1]) is not None)
 2684                     else:
 2685                         mtch = True
 2686                     it.append(mtch)
 2687                     item = tuple(it)
 2688                     bt.append(item)
 2689                 busy_lst.append(bt)
 2690                 busy_dates.append(thisdate.strftime("%a %d"))
 2691                 if bt:
 2692                     lastend = 0
 2693                     for pts in bt:
 2694                         busytimes += pts[1] - pts[0]
 2695                         self.busyHsh.setdefault(id, []).append("* {0}".format(pts[2]))
 2696                         if pts[0] < lastend:
 2697                             overlap = True
 2698                         lastend = pts[1]
 2699                     if overlap:
 2700                         flagcolor = self.CONFLICTFILL
 2701                     tags.append('busy')
 2702 
 2703                     busylines = [[], [], [], []]
 2704                     # each side 360 minutes plus 2 times bar width
 2705 
 2706                     for pts in bt:
 2707                         pt1 = max(0, pts[0])
 2708                         pt2 = min(pts[1], 1440)
 2709                         tmp = [[], [], [], []]
 2710 
 2711                         for ii in range(0, 4):
 2712                             if pt1 >= intervals[ii]:
 2713                                 continue
 2714                             tmp[ii].append(pt1)
 2715                             for jj in range(ii, 4):
 2716                                 if jj > ii:
 2717                                     tmp[jj].append(intervals[jj-1])
 2718                                 if pt2 <= intervals[jj]:
 2719                                     tmp[jj].append(pt2)
 2720                                     break
 2721                                 else:
 2722                                     tmp[jj].append(intervals[jj])
 2723                             break
 2724                         for ii in range(4):
 2725                             if tmp[ii]:
 2726                                 busylines[ii].append(tmp[ii])
 2727 
 2728 
 2729                     if busylines:
 2730                         for side in range(4):
 2731                             lines = busylines[side]
 2732                             if lines:
 2733                                 if side == 0: # left
 2734                                     for line in lines:
 2735                                         bx = ex = bl_x + offset
 2736                                         by = bl_y - indent - int(Decimal((line[0])/360) * h_)
 2737                                         ey = bl_y - indent - int(Decimal((line[1])/360) * h_)
 2738                                         self.canvas.create_line((bx, by, ex, ey), fill=nightcolor, width=busywidth, tag="busy")
 2739                                 elif side == 1: # top
 2740                                     for line in lines:
 2741                                         by = ey = tl_y + offset
 2742                                         bx = tl_x + indent + int(Decimal((line[0]-360)/360) * w_)
 2743                                         ex = tl_x + indent + int(Decimal((line[1]-360)/360) * w_)
 2744                                         self.canvas.create_line((bx, by, ex, ey), fill=morningcolor, width=busywidth, tag="busy")
 2745                                 elif side == 2: # right
 2746                                     for line in lines:
 2747                                         bx = ex = tr_x - offset
 2748                                         by = tr_y + indent + int(Decimal((line[0]-720)/360) * h_)
 2749                                         ey = tr_y + indent + int(Decimal((line[1]-720)/360) * h_)
 2750                                         self.canvas.create_line((bx, by, ex, ey), fill=afternooncolor, width=busywidth, tag="busy")
 2751                                 elif side == 3: # bottom
 2752                                     for line in lines:
 2753                                         by = ey = br_y - offset
 2754                                         bx = br_x - indent - int(Decimal((line[0]-1080)/360) * w_)
 2755                                         ex = br_x - indent - int(Decimal((line[1]-1080)/360) * w_)
 2756                                         self.canvas.create_line((bx, by, ex, ey), fill=eveningcolor, width=busywidth, tag="busy")
 2757 
 2758                         bx = bl_x + offset - 1.5 * busywidth
 2759                         ex = bl_x + offset + .5 * busywidth
 2760                         by = bl_y - indent + 1.5 * busywidth
 2761                         ey = bl_y - indent - .5 * busywidth
 2762                         if flagcolor:
 2763                             self.canvas.create_rectangle((bx, by, ex, ey), fill=flagcolor, outline=flagcolor, tag="busy")
 2764             else:
 2765                 busy_lst.append([])
 2766                 busy_dates.append(thisdate.strftime("%a %d"))
 2767 
 2768             if 'current_day' in tags:
 2769                 self.canvas.itemconfig(id, tag='current_day', fill=self.CURRENTFILL)
 2770             elif 'occasion' in tags:
 2771                 self.canvas.itemconfig(id, tag='occasion', fill=self.OCCASIONFILL)
 2772             elif 'busy' in tags:
 2773                 self.canvas.itemconfig(id, tag='busy', fill=self.BGCOLOR)
 2774             else:
 2775                 self.canvas.itemconfig(id, tag='default', fill=self.BGCOLOR)
 2776 
 2777             # if fill:
 2778             self.canvas.create_text(p, text="{0}".format(weekdates[i]), fill=self.BUSYBAR)
 2779 
 2780         busy_ids = list(busy_ids)
 2781 
 2782         self.conf_ids = conf_ids
 2783 
 2784 
 2785         # border
 2786         # xy = int(l), int(t), int(l + x_ * 7), int(t + y_)
 2787         # self.canvas.create_rectangle(xy, tag="grid")
 2788 
 2789         # verticals
 2790         for i in range(0, 8):
 2791             xy = int(l + x_ * i), int(t-18), int(l + x_ * i), int(t + y_)
 2792             self.canvas.create_line(xy, fill=self.GRIDCOLOR, tag="grid")
 2793 
 2794         for i in range(7):
 2795             p = int(l + x_ / 2 + x_ * i), int(t - 10)
 2796             self.canvas.create_text(p, text="{0}".format(weekdays[i]), fill=self.BUSYBAR)
 2797 
 2798         self.busy_info = (theweek, busy_dates, busy_lst, occasion_lst)
 2799         self.busy_ids = busy_ids
 2800         self.busy_ids.sort()
 2801         for id in self.busy_ids:
 2802             self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item)
 2803             # self.canvas.tag_bind(id, '<Any-Leave>', self.on_leave_item)
 2804         self.canvas_ids = self.busy_ids
 2805         self.monthid2date = monthid2date
 2806 
 2807 
 2808     def closeMonthly(self, event=None):
 2809         self.month_height = self.topwindow.panecget(self.toppane, "height")
 2810         self.topwindow.forget(self.toppane)
 2811         self.monthly = False
 2812         self.tree.pack(fill="both", expand=1, padx=4, pady=0)
 2813         self.update_idletasks()
 2814         for i in range(4, 6):
 2815             self.toolsmenu.entryconfig(i, state="disabled")
 2816         self.bind("<Control-f>", self.setFilter)
 2817 
 2818     def showMonthly(self, event=None, chosen_day=None):
 2819         """
 2820         Open the canvas at the current week
 2821         """
 2822         self.custom_box.forget()
 2823         tt = TimeIt(loglevel=2, label="month view")
 2824         logger.debug("chosen_day: {0}; active_date: {1}".format(chosen_day, self.active_date))
 2825         if self.monthly:
 2826             # we're in month view already
 2827             return
 2828         if self.weekly:
 2829             self.closeWeekly()
 2830         self.content.delete("1.0", END)
 2831         for i in range(4, 6):
 2832             self.toolsmenu.entryconfig(i, state="normal")
 2833 
 2834         self.setView(DAY)
 2835 
 2836         self.view = MONTH
 2837         self.currentView.set(MONTH)
 2838 
 2839         if chosen_day is not None:
 2840             self.chosen_date = chosen_day
 2841         elif self.active_date:
 2842             self.chosen_date = self.active_date
 2843         else:
 2844             self.chosen_date = get_current_time().date()
 2845 
 2846         self.topwindow.add(self.toppane, padx=0, pady=0, before=self.botwindow, height=self.month_height)
 2847 
 2848         self.showMonth(event=event)
 2849         self.monthly = True
 2850         self.canvas.focus_set()
 2851         tt.stop()
 2852 
 2853     def showMonth(self, event=None, month=None):
 2854         # self.canvas.focus_set()
 2855         self.selectedId = None
 2856         matching = self.cal_regex is not None and self.default_regex is not None
 2857         busy_lst = []
 2858         busy_dates = []
 2859         occasion_lst = []
 2860 
 2861         self.current_day = get_current_time().replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
 2862         logger.debug('self.current_day: {0}, minutes: {1}'.format(self.current_day, self.current_minutes))
 2863         self.x_win = self.canvas.winfo_width()
 2864         self.y_win = self.canvas.winfo_height()
 2865         month_day = 1
 2866         use_active = False
 2867         if month in [-1, 0, 1]:
 2868             if month == 0:
 2869                 self.year_month = [self.current_day.year, self.current_day.month]
 2870             elif month == 1:
 2871                 self.year_month[1] += 1
 2872                 if self.year_month[1] > 12:
 2873                     self.year_month[1] -= 12
 2874                     self.year_month[0] += 1
 2875             elif month == -1:
 2876                 self.year_month[1] -= 1
 2877                 if self.year_month[1] < 1:
 2878                     self.year_month[1] += 12
 2879                     self.year_month[0] -= 1
 2880         elif self.active_date:
 2881             use_active = True
 2882             self.year_month = [self.active_date.year, self.active_date.month]
 2883             month_day = self.active_date.day
 2884         else:
 2885             return
 2886         logger.debug('month active_date: {0}'.format(self.active_date))
 2887         day = date(self.year_month[0], self.year_month[1], month_day)
 2888         if use_active:
 2889             scrolldate = self.chosen_date
 2890             self.canvas_idpos = month_day - 1
 2891             # self.canvas_idpos = weekdaynum - 1
 2892         else:
 2893             scrolldate = day
 2894             self.canvas_idpos = 0
 2895         self.scrollToDate(scrolldate)
 2896 
 2897         weeks = self.monthly_calendar.monthdatescalendar(*self.year_month)
 2898         num_weeks = len(weeks)
 2899         weekdays = [s2or3(x.strftime("%a")) for x in weeks[0]]
 2900         themonth = weeks[1][0].strftime("%B %Y")
 2901         self.canvas.delete("all")
 2902         l = 5
 2903         r = 5
 2904         t = 22
 2905         b = 5
 2906         if event:
 2907             logger.debug('event: {0}'.format(event))
 2908             w, h = event.width, event.height
 2909             if type(w) is int and type(h) is int:
 2910                 self.canvas_width = w
 2911                 self.canvas_height = h
 2912             else:
 2913                 w = self.canvas.winfo_width()
 2914                 h = self.canvas.winfo_height()
 2915         else:
 2916             w = self.canvas.winfo_width()
 2917             # h = self.canvas.winfo_height()
 2918             h = self.month_height
 2919         logger.debug("w: {0}, h: {1}, l: {2}, t: {3}".format(w, h, l, t))
 2920 
 2921         self.margins = (w, h, l, r, t, b)
 2922 
 2923         self.month_x = x_ = Decimal(w - 1 - l - r) / Decimal(7)
 2924         self.month_y = y_ = Decimal(h - 1 - t - b) / Decimal(num_weeks)
 2925 
 2926         logger.debug("x: {0}, y: {1}".format(x_, y_))
 2927 
 2928         # month
 2929         p = l + int((w - 1 - l - r) / 2), 20
 2930         self.currentView.set(themonth)
 2931         self.busyHsh = {}
 2932 
 2933         # occasions
 2934         busy_ids = set([])
 2935         monthid2date = {}
 2936 
 2937         # self.canvas.bind('<Escape>', self.on_leave_item)
 2938 
 2939         # monthdays
 2940         intervals = [360, 720, 1080, 1440]
 2941         busywidth = 2
 2942         offset = 6
 2943         indent = 7
 2944 
 2945         nightcolor = self.BUSYBAR
 2946         morningcolor = self.BUSYBAR
 2947         afternooncolor = self.BUSYBAR
 2948         eveningcolor = self.BUSYBAR
 2949 
 2950         for j in range(num_weeks):
 2951             for i in range(7):
 2952                 busytimes = 0
 2953                 flagcolor = None
 2954                 start_x = l + i * x_
 2955                 end_x = start_x + x_
 2956                 start_y = int(t + y_ * j)
 2957                 end_y = start_y + y_
 2958                 xy = int(start_x), int(start_y), int(end_x), int(end_y)
 2959                 p = int(l + x_ / 2 + x_ * i), int(t + y_ * j + y_ / 2)
 2960                 # pp = int(l +  x_ + x_ * i), int(t + y_ * j + y_ )
 2961 
 2962                 tl_x = bl_x = int(l + x_ * i)
 2963                 tl_y = tr_y = int(t + y_ *j)
 2964                 tr_x = br_x = int(tl_x + x_)
 2965                 bl_y = br_y = int(tl_y + y_)
 2966                 w_ = x_ - 12
 2967                 h_ = y_ - 12
 2968 
 2969                 thisdate = weeks[j][i]
 2970                 isokey = thisdate.isocalendar()
 2971                 month = thisdate.month
 2972                 tags = []
 2973                 if (month != self.year_month[1]):
 2974                     fill = self.OTHERDATE
 2975                 else:
 2976                     fill = self.CURRDATE
 2977                     id = self.canvas.create_rectangle(xy, outline="", width=0)
 2978                     busy_ids.add(id)
 2979                     monthid2date[id] = thisdate
 2980                     today = (thisdate == self.current_day.date())
 2981                     bt = []
 2982                     if today:
 2983                         tags.append('current_day')
 2984                     if loop.occasions is not None and isokey in loop.occasions:
 2985                         bt = []
 2986                         for item in loop.occasions[isokey]:
 2987                             it = list(item)
 2988                             if matching:
 2989                                 if not self.cal_regex.match(it[-1]):
 2990                                     continue
 2991                                 mtch = (self.default_regex.match(it[-1]) is not None)
 2992                             else:
 2993                                 mtch = True
 2994                             it.append(mtch)
 2995                             item = tuple(it)
 2996                             bt.append(item)
 2997                         occasion_lst.append(bt)
 2998                         if bt:
 2999                             if not today:
 3000                                 tags.append('occasion')
 3001                             self.busyHsh.setdefault(id, []).extend(["^ {0}".format(x[0]) for x in bt])
 3002                     else:
 3003                         occasion_lst.append([])
 3004 
 3005                     if loop.busytimes is not None and isokey in loop.busytimes:
 3006                         bt = []
 3007                         overlap = False
 3008                         for item in loop.busytimes[isokey]:
 3009                             it = list(item)
 3010                             if it[0] == it[1]:
 3011                                 # skip reminders
 3012                                 continue
 3013                             if matching:
 3014                                 if not self.cal_regex.match(it[-1]):
 3015                                     continue
 3016                                 mtch = (self.default_regex.match(it[-1]) is not None)
 3017                             else:
 3018                                 mtch = True
 3019                             it.append(mtch)
 3020                             item = tuple(it)
 3021                             bt.append(item)
 3022                         busy_lst.append(bt)
 3023                         busy_dates.append(thisdate.strftime("%a %d"))
 3024                         if bt:
 3025                             lastend = 0
 3026                             for pts in bt:
 3027                                 busytimes += pts[1] - pts[0]
 3028                                 self.busyHsh.setdefault(id, []).append("* {0}".format(pts[2]))
 3029                                 if pts[0] < lastend:
 3030                                     overlap = True
 3031                                 lastend = pts[1]
 3032                             if overlap:
 3033                                 flagcolor = self.CONFLICTFILL
 3034                             tags.append('busy')
 3035 
 3036                             busylines = [[], [], [], []]
 3037                             # each side 360 minutes plus 2 times bar width
 3038                             for pts in bt:
 3039                                 pt1 = max(0, pts[0])
 3040                                 pt2 = min(pts[1], 1440)
 3041                                 tmp = [[], [], [], []]
 3042 
 3043                                 for ii in range(0, 4):
 3044                                     if pt1 >= intervals[ii]:
 3045                                         continue
 3046                                     tmp[ii].append(pt1)
 3047                                     for jj in range(ii, 4):
 3048                                         if jj > ii:
 3049                                             tmp[jj].append(intervals[jj-1])
 3050                                         if pt2 <= intervals[jj]:
 3051                                             tmp[jj].append(pt2)
 3052                                             break
 3053                                         else:
 3054                                             tmp[jj].append(intervals[jj])
 3055                                     break
 3056                                 for ii in range(4):
 3057                                     if tmp[ii]:
 3058                                         busylines[ii].append(tmp[ii])
 3059 
 3060                             if busylines:
 3061                                 for side in range(4):
 3062                                     lines = busylines[side]
 3063                                     if lines:
 3064                                         if side == 0: # left
 3065                                             for line in lines:
 3066                                                 bx = ex = bl_x + offset
 3067                                                 by = bl_y - indent - int(Decimal((line[0])/360) * h_)
 3068                                                 ey = bl_y - indent - int(Decimal((line[1])/360) * h_)
 3069                                                 self.canvas.create_line((bx, by, ex, ey), fill=nightcolor, width=busywidth, tag="busy")
 3070                                         elif side == 1: # top
 3071                                             for line in lines:
 3072                                                 by = ey = tl_y + offset
 3073                                                 bx = tl_x + indent + int(Decimal((line[0]-360)/360) * w_)
 3074                                                 ex = tl_x + indent + int(Decimal((line[1]-360)/360) * w_)
 3075                                                 self.canvas.create_line((bx, by, ex, ey), fill=morningcolor, width=busywidth, tag="busy")
 3076                                         elif side == 2: # right
 3077                                             for line in lines:
 3078                                                 bx = ex = tr_x - offset
 3079                                                 by = tr_y + indent + int(Decimal((line[0]-720)/360) * h_)
 3080                                                 ey = tr_y + indent + int(Decimal((line[1]-720)/360) * h_)
 3081                                                 self.canvas.create_line((bx, by, ex, ey), fill=afternooncolor, width=busywidth, tag="busy")
 3082                                         elif side == 3: # bottom
 3083                                             for line in lines:
 3084                                                 by = ey = br_y - offset
 3085                                                 bx = br_x - indent - int(Decimal((line[0]-1080)/360) * w_)
 3086                                                 ex = br_x - indent - int(Decimal((line[1]-1080)/360) * w_)
 3087                                                 self.canvas.create_line((bx, by, ex, ey), fill=eveningcolor, width=busywidth, tag="busy")
 3088 
 3089                                 bx = bl_x + offset - 1.5 * busywidth
 3090                                 ex = bl_x + offset + .5 * busywidth
 3091                                 by = bl_y - indent + 1.5 * busywidth
 3092                                 ey = bl_y - indent - .5 * busywidth
 3093                                 if flagcolor:
 3094                                     self.canvas.create_rectangle((bx, by, ex, ey), fill=flagcolor, outline=flagcolor, tag="busy")
 3095                     else:
 3096                         busy_lst.append([])
 3097                         busy_dates.append(thisdate.strftime("%a %d"))
 3098                 if 'current_day' in tags:
 3099                     self.canvas.itemconfig(id, tag='current_day', fill=self.CURRENTFILL)
 3100                 elif 'occasion' in tags:
 3101                     self.canvas.itemconfig(id, tag='occasion', fill=self.OCCASIONFILL)
 3102                 elif 'busy' in tags:
 3103                     self.canvas.itemconfig(id, tag='busy', fill=self.BGCOLOR)
 3104 
 3105                 if fill:
 3106                     self.canvas.create_text(p, text="{0}".format(weeks[j][i].day), fill=fill)
 3107 
 3108         busy_ids = list(busy_ids)
 3109         for id in busy_ids:
 3110             self.canvas.tag_bind(id, '<Any-Enter>', self.on_enter_item)
 3111             self.canvas.tag_bind(id, '<Any-Leave>', self.on_leave_item)
 3112 
 3113         # border
 3114         # xy = int(l), int(t), int(l + x_ * 7), int(t + y_ * num_weeks + 1)
 3115         # self.canvas.create_rectangle(xy, tag="grid")
 3116 
 3117         # verticals
 3118         for i in range(0, 8):
 3119             xy = int(l + x_ * i), int(t-18), int(l + x_ * i), int(t + y_ * num_weeks)
 3120             self.canvas.create_line(xy, fill=self.GRIDCOLOR, tag="grid")
 3121         # horizontals
 3122         for j in range(0, num_weeks):
 3123             xy = int(l), int(t + y_ * j), int(l + x_ * 7), int(t + y_ * j)
 3124             self.canvas.create_line(xy, fill=self.GRIDCOLOR, tag="grid")
 3125 
 3126         # days
 3127         for i in range(7):
 3128             p = int(l + x_ / 2 + x_ * i), int(t - 10)
 3129             self.canvas.create_text(p, text="{0}".format(weekdays[i]),fill = self.CURRDATE)
 3130 
 3131         self.busy_info = (themonth, busy_dates, busy_lst, occasion_lst)
 3132         self.busy_ids = busy_ids
 3133         self.busy_ids.sort()
 3134         self.canvas_ids = self.busy_ids
 3135         self.monthid2date = monthid2date
 3136 
 3137 
 3138     def selectId(self, event, d=1):
 3139         ids = self.busy_ids
 3140         if self.canvas_idpos is None:
 3141             self.canvas_idpos = 0
 3142             old_id = None
 3143         else:
 3144             if self.canvas_idpos < len(self.canvas_ids):
 3145                 old_id = self.canvas_ids[self.canvas_idpos]
 3146             else:
 3147                 old_id = self.canvas_ids[0]
 3148             if old_id in ids:
 3149                 tags = self.canvas.gettags(old_id)
 3150                 if 'current_day' in tags:
 3151                     self.canvas.itemconfig(old_id, fill=self.CURRENTFILL)
 3152                 elif 'occasion' in tags:
 3153                     self.canvas.itemconfig(old_id, fill=self.OCCASIONFILL)
 3154                 elif self.weekly:
 3155                     self.canvas.itemconfig(old_id, fill=self.BGCOLOR)
 3156             else:
 3157                 self.canvas.itemconfig(old_id, fill=self.OCCASIONFILL)
 3158                 self.canvas.tag_lower(old_id)
 3159         if d == -1:
 3160             self.canvas_idpos -= 1
 3161             if self.canvas_idpos < 0:
 3162                 self.priorWeekMonth(event=event)
 3163                 self.canvas_idpos = len(self.canvas_ids) - 1
 3164         elif d == 1:
 3165             self.canvas_idpos += 1
 3166             if self.canvas_idpos > len(self.canvas_ids) - 1:
 3167                 self.nextWeekMonth(event=event)
 3168                 self.canvas_idpos = 0
 3169 
 3170         if old_id is not None and old_id in self.busy_ids:
 3171             tags = self.canvas.gettags(old_id)
 3172             if 'current_day' in tags:
 3173                 self.canvas.itemconfig(old_id, fill=self.CURRENTFILL)
 3174             elif 'occasion' in tags:
 3175                 self.canvas.itemconfig(old_id, fill=self.OCCASIONFILL)
 3176             elif 'busy' in tags:
 3177                 self.canvas.itemconfig(old_id, fill=self.BGCOLOR)
 3178             else:
 3179                 self.canvas.itemconfig(old_id, fill=self.BGCOLOR)
 3180 
 3181         self.selectedId = id = self.canvas_ids[self.canvas_idpos]
 3182         self.active_date = self.monthid2date[id]
 3183         if type(self.active_date) is not date:
 3184             self.active_date = self.active_date.date()
 3185         self.canvas_date = self.active_date
 3186         self.scrollToDate(self.active_date)
 3187         self.canvas_idpos = self.canvas_ids.index(id)
 3188         if id in self.busy_ids:
 3189             self.canvas.itemconfig(id, fill=self.ACTIVEFILL)
 3190         if id in self.busyHsh:
 3191             txt = "\n".join(self.busyHsh[id])
 3192             self.content.delete("1.0", END)
 3193             self.content.insert("1.0", txt)
 3194         else:
 3195             self.content.delete("1.0", END)
 3196         self.setFocus(e=event)
 3197 
 3198 
 3199     def setFocus(self, e):
 3200         self.canvas.focus()
 3201         self.canvas.focus_set()
 3202 
 3203     def on_enter_item(self, e):
 3204         if self.canvas_idpos is not None:
 3205             old_id = self.canvas_ids[self.canvas_idpos]
 3206             if old_id in self.busy_ids:
 3207                 tags = self.canvas.gettags(old_id)
 3208                 if 'current_day' in tags:
 3209                     self.canvas.itemconfig(old_id, fill=self.CURRENTFILL)
 3210                 elif 'occasion' in tags:
 3211                     self.canvas.itemconfig(old_id, fill=self.OCCASIONFILL)
 3212                 else:
 3213                     self.canvas.itemconfig(old_id, fill=self.BGCOLOR)
 3214         self.selectedId = id = self.canvas.find_withtag(CURRENT)[0]
 3215         self.active_date = self.monthid2date[id]
 3216         self.canvas_date = self.monthid2date[id]
 3217         self.canvas_idpos = self.canvas_ids.index(id)
 3218         if id in self.busy_ids:
 3219             self.canvas.itemconfig(id, fill=self.ACTIVEFILL)
 3220         if id in self.busyHsh:
 3221             txt = "\n".join(self.busyHsh[id])
 3222             self.content.delete("1.0", END)
 3223             self.content.insert("1.0", txt)
 3224         else:
 3225             self.content.delete("1.0", END)
 3226 
 3227     def on_leave_item(self, e):
 3228         self.content.delete("1.0", END)
 3229         id = self.canvas.find_withtag(CURRENT)[0]
 3230         if id in self.busy_ids:
 3231             tags = self.canvas.gettags(id)
 3232             if 'current_date' in tags:
 3233                 self.canvas.itemconfig(id, fill=self.CURRENTFILL)
 3234             elif 'occasion' in tags:
 3235                 self.canvas.itemconfig(id, fill=self.OCCASIONFILL)
 3236             else:
 3237                 self.canvas.itemconfig(id, fill=self.BGCOLOR)
 3238         else:
 3239             self.canvas.itemconfig(id, fill=self.BGCOLOR)
 3240 
 3241     def on_select_item(self, event):
 3242         if self.monthly or self.weekly:
 3243             self.newItem()
 3244         else:
 3245             return "break"
 3246 
 3247     def on_activate_item(self, event):
 3248         if self.monthly or self.weekly:
 3249             self.newItem()
 3250 
 3251     def newEvent(self, event):
 3252         logger.debug("event: {0}".format(event))
 3253         self.canvas.focus_set()
 3254         px = event.x
 3255         py = event.y
 3256         (w, h, l, r, t, b) = self.margins
 3257         x = Decimal(w - 1 - l - r) / Decimal(7)  # x per day intervals
 3258         rx = int(round(Decimal(px - l) / x - Decimal(0.5)))  # number of days
 3259         dt = (self.week_beg + rx * ONEDAY).replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=None)
 3260         dfmt = dt.strftime("%a %b %d")
 3261         s = "*  @s {0}".format(dfmt)
 3262         changed = SimpleEditor(parent=self, master=self.canvas, start=s, options=loop.options).changed
 3263 
 3264         if changed:
 3265             logger.debug('changed, updating alerts, ...')
 3266 
 3267             self.updateAlerts()
 3268             if self.weekly:
 3269                 self.updateDay()
 3270                 self.showWeek(event=event)
 3271             elif self.monthly:
 3272                 self.updateDay()
 3273                 self.showMonth(event=event)
 3274             else:
 3275                 self.showView()
 3276 
 3277     def showCalendar(self, e=None):
 3278         cal_year = 0
 3279         opts = loop.options
 3280         cal_pastcolor = self.YEARPAST
 3281         cal_currentcolor = self.YEARCURRENT
 3282         cal_futurecolor = self.YEARFUTURE
 3283 
 3284         def showYear(x=0):
 3285             global cal_year
 3286             if x:
 3287                 cal_year += x
 3288             else:
 3289                 cal_year = 0
 3290             cal = "\n".join(calyear(cal_year, options=opts))
 3291             if cal_year > 0:
 3292                 col = cal_futurecolor
 3293             elif cal_year < 0:
 3294                 col = cal_pastcolor
 3295             else:
 3296                 col = cal_currentcolor
 3297             t.configure(fg=col)
 3298             t.delete("0.0", END)
 3299             t.insert("0.0", cal)
 3300 
 3301         win = Toplevel(highlightcolor=self.HLCOLOR, background=self.BGCOLOR)
 3302         win.title(_("Calendar"))
 3303         f = Frame(win)
 3304         # pack the button first so that it doesn't disappear with resizing
 3305         b = ttk.Button(win, text=_('OK'), style="bg.TButton",  command=win.destroy, default='active')
 3306         b.pack(side='bottom', fill=tkinter.NONE, expand=0, pady=0)
 3307         win.bind('<Return>', (lambda e, b=b: b.invoke()))
 3308         win.bind('<KP_Enter>', (lambda e, b=b: b.invoke()))
 3309         win.bind('<Escape>', (lambda e, b=b: b.invoke()))
 3310 
 3311         t = ReadOnlyText(f, wrap="word", padx=2, pady=2, bd=2, relief="sunken",
 3312                          font=self.tkfixedfont,
 3313                          takefocus=False,
 3314                          background=self.BGCOLOR,
 3315                          highlightcolor=self.HLCOLOR)
 3316         win.bind('<Left>', (lambda e: showYear(-1)))
 3317         win.bind('<Right>', (lambda e: showYear(1)))
 3318         win.bind('<Home>', (lambda e: showYear()))
 3319         showYear()
 3320         t.pack(side='left', fill=tkinter.BOTH, expand=1, padx=0, pady=0)
 3321         ysb = Scrollbar(f, orient='vertical', command=t.yview, width=8)
 3322         ysb.pack(side='right', fill=tkinter.Y, expand=0, padx=0, pady=0)
 3323 
 3324         t.configure(yscroll=ysb.set)
 3325         f.pack(padx=2, pady=2, fill=tkinter.BOTH, expand=1)
 3326         win.focus_set()
 3327         win.grab_set()
 3328         win.transient(self)
 3329         win.wait_window(win)
 3330 
 3331     def newCommand(self, e=None):
 3332         self.newValue.set(self.newLabel)
 3333 
 3334     def showShortcuts(self, e=None):
 3335         if e and e.char != "?":
 3336             return
 3337         res = self.menutree.showMenu("_")
 3338         self.textWindow(parent=self, title='etm', opts=self.options, prompt=res, modal=False)
 3339 
 3340     def help(self, event=None):
 3341         path = USERMANUAL
 3342         if windoz:
 3343             os.startfile(USERMANUAL)
 3344             return()
 3345         if mac:
 3346             cmd = 'open' + " {0}".format(path)
 3347         else:
 3348             cmd = 'xdg-open' + " {0}".format(path)
 3349         self.check_output(cmd)
 3350         return True
 3351 
 3352     def about(self, event=None):
 3353         res = loop.do_v("")
 3354         self.textWindow(parent=self, title='etm', opts=self.options, prompt=res, modal=False)
 3355 
 3356     def checkForUpdate(self, event=None):
 3357         res = checkForNewerVersion()[1]
 3358         self.textWindow(parent=self, title='etm', prompt=res, opts=self.options)
 3359 
 3360     def showChanges(self, event=None):
 3361         if not loop.options['vcs_system']:
 3362             prompt = """An entry for 'vcs_system' in etmtk.cfg is required but missing."""
 3363             self.textWindow(parent=self, title="etm", prompt=prompt, opts=loop.options)
 3364             return
 3365 
 3366         if self.itemSelected:
 3367             f = self.itemSelected['fileinfo'][0]
 3368             fn = ' {0}"{1}"'.format(self.options['vcs']['file'], os.path.normpath(os.path.join(self.options['datadir'], f)))
 3369             title = _("Showing changes for {0}.").format(f)
 3370 
 3371         else:
 3372             fn = ""
 3373             title = _("Showing changes for all files.")
 3374         logger.debug('fn: {0}'.format(fn))
 3375         prompt = _("""\
 3376 {0}
 3377 
 3378 If an item is selected, changes will be shown for the file containing
 3379 the item. Otherwise, changes will be shown for all files.
 3380 
 3381 Enter an integer number of changes to display
 3382 or 0 to display all changes.""").format(title)
 3383         depth = GetInteger(
 3384             parent=self,
 3385             title=_("Changes"),
 3386             prompt=prompt, opts=[0], default=10).value
 3387         if depth is None:
 3388             return ()
 3389         if depth == 0:
 3390             # all changes
 3391             numstr = ""
 3392         else:
 3393             numstr = "{0} {1}".format(loop.options['vcs']['limit'], depth)
 3394         command = loop.options['vcs']['history'].format(
 3395             repo=loop.options['vcs']['repo'],
 3396             work=loop.options['vcs']['work'],
 3397             numchanges=numstr, rev="{rev}", desc="{desc}", file=fn)
 3398         logger.debug('vcs history command: {0}'.format(command))
 3399         tt = TimeIt(loglevel=2, label="showChanges")
 3400         s = subprocess.check_output(command, shell=True, universal_newlines=True)
 3401         tt.stop()
 3402         p = s2or3(s)
 3403         if not p:
 3404             p = 'no output from command:\n    {0}'.format(command)
 3405 
 3406         p = "\n".join([x for x in p.split('\n') if not (x.startswith('index') or x.startswith('diff') or x.startswith('\ No newline'))])
 3407 
 3408         self.textWindow(parent=self, title=title, prompt=s2or3(p), opts=self.options)
 3409 
 3410     def focus_next_window(self, event):
 3411         event.widget.tk_focusNext().focus()
 3412         return "break"
 3413 
 3414     def goHome(self, event=None):
 3415         today = get_current_time().date()
 3416         if self.weekly:
 3417             self.showWeek(event=event, week=0)
 3418             self.scrollToDate(today)
 3419         elif self.monthly:
 3420             self.showMonth(event=event, month=0)
 3421             self.scrollToDate(today)
 3422         elif self.view == DAY:
 3423             self.scrollToDate(today)
 3424         else:
 3425             self.tree.focus_set()
 3426             self.tree.focus(1)
 3427             self.tree.selection_set(1)
 3428             self.tree.yview(0)
 3429         return
 3430 
 3431     def nextItem(self, e=None):
 3432         item = self.tree.selection()[0]
 3433         if item:
 3434             next = self.tree.next(item)
 3435             if next:
 3436                 next = int(next)
 3437                 next -= 1
 3438                 self.tree.focus(next)
 3439                 self.tree.selection_set(next)
 3440 
 3441     def prevItem(self, e=None):
 3442         item = self.tree.selection()[0]
 3443         if item:
 3444             prev = self.tree.prev(item)
 3445             if prev:
 3446                 prev = int(prev)
 3447                 prev += 1
 3448                 self.tree.focus(prev)
 3449                 self.tree.selection_set(prev)
 3450 
 3451     def OnSelect(self, event=None, uuid=None, dt=None):
 3452         """
 3453         Tree row has gained selection.
 3454         """
 3455         logger.debug("starting OnSelect with uuid: {0}".format(uuid))
 3456         self.content.delete("1.0", END)
 3457         if uuid is None:  # tree view
 3458             if not self.tree.selection():
 3459                 return
 3460             item = self.tree.selection()[0]
 3461             self.rowSelected = int(item)
 3462             logger.debug('rowSelected: {0}'.format(self.rowSelected))
 3463             # type_chr is the actual type, e.g., "-"
 3464             type_chr = self.tree.item(item)['text'][0]
 3465             uuid, dt, hsh = self.getInstance(item)
 3466             logger.debug('tree rowSelected: {0}; {1}; {2} {3}'.format(self.rowSelected, self.tree.item(item)['text'], dt, hsh))
 3467             if self.canvas == self.canvas.focus_get():
 3468                 # canvas has focus
 3469                 logger.debug("using canvas active_date: {0}; {1}".format(self.active_date, self.tree.item(item)['text']))
 3470             elif self.view in [AGENDA, WEEK, MONTH]:
 3471                 if self.rowSelected in self.id2date:
 3472                     if dt is None:
 3473                         # we have the date selected
 3474                         self.active_date = self.id2date[self.rowSelected]
 3475                         logger.debug("active_date from id2date: {0}; {1}".format(self.active_date, self.tree.item(item)['text']))
 3476                     else:
 3477                         # we have an item
 3478                         self.active_date = dt.date()
 3479                         logger.debug('active date from dt: {0}; {1}'.format(self.active_date, self.tree.item(item)))
 3480                 else:
 3481                     if dt:
 3482                         self.active_date = dt.date()
 3483                         logger.debug('active date from dt: {0}; {1}'.format(self.active_date, self.tree.item(item)))
 3484                     else:
 3485                         self.active_date = None
 3486                 logger.debug('active_date: {0}'.format(self.active_date))
 3487             if hsh:
 3488                 type_chr = hsh['itemtype']
 3489         self.update_idletasks()
 3490 
 3491         if uuid is not None:
 3492             isRepeating = ('r' in hsh and dt)
 3493             if isRepeating:
 3494                 logger.debug('selected: {0}, {1}'.format(dt, type(dt)))
 3495                 item = "{0} {1}".format(_('selected'), dt)
 3496                 self.itemmenu.entryconfig(1, label="{0} ...".format(self.em_opts[1]))
 3497                 self.itemmenu.entryconfig(2, label="{0} ...".format(self.em_opts[2]))
 3498             else:
 3499                 self.itemmenu.entryconfig(1, label=self.em_opts[1])
 3500                 self.itemmenu.entryconfig(2, label=self.em_opts[2])
 3501                 item = _('selected')
 3502             isUnfinished = (type_chr in ['-', '+', '%'])
 3503             hasLink = ('g' in hsh and hsh['g'])
 3504             hasUser = ('u' in hsh and hsh['u'])
 3505             l1 = hsh['fileinfo'][1]
 3506             l2 = hsh['fileinfo'][2]
 3507             if l1 == l2:
 3508                 lines = "{0} {1}".format(_('line'), l1)
 3509             else:
 3510                 lines = "{0} {1}-{2}".format(_('lines'), l1, l2)
 3511             self.filetext = filetext = "{0}, {1}".format(hsh['fileinfo'][0], lines)
 3512             if 'errors' in hsh and hsh['errors']:
 3513                 text = "{1}\n\n{2}: {3}\n\n{4}: {5}".format(item, hsh['entry'].lstrip(), _("Errors"), hsh['errors'], _("file"), filetext)
 3514             else:
 3515                 text = "{1}\n\n{2}: {3}".format(item, hsh['entry'].lstrip(), _("file"), filetext)
 3516             for i in [0, 1, 2, 3, 5, 6, 7, 8, 9]:  # everything except finish (4), open link (10) and show user (11)
 3517                 self.itemmenu.entryconfig(i, state='normal')
 3518             if isUnfinished:
 3519                 self.itemmenu.entryconfig(4, state='normal')
 3520             else:
 3521                 self.itemmenu.entryconfig(4, state='disabled')
 3522             if hasLink:
 3523                 self.itemmenu.entryconfig(10, state='normal')
 3524             else:
 3525                 self.itemmenu.entryconfig(10, state='disabled')
 3526             if hasUser:
 3527                 self.itemmenu.entryconfig(11, state='normal')
 3528             else:
 3529                 self.itemmenu.entryconfig(11, state='disabled')
 3530             self.uuidSelected = uuid
 3531             self.itemSelected = hsh
 3532             logger.debug('dt selected: {0}, {1}'.format(dt, type(dt)))
 3533             self.dtSelected = dt
 3534         else:
 3535             text = ""
 3536             for i in range(12):
 3537                 self.itemmenu.entryconfig(i, state='disabled')
 3538             self.itemSelected = None
 3539             self.uuidSelected = None
 3540             self.dtSelected = None
 3541         r = self.tree.identify_row(1)
 3542         if r:
 3543             self.topSelected = int(r)
 3544         else:
 3545             self.topSelected = 1
 3546         logger.debug("row: {0}; uuid: {1}; instance: {2}, {3}; top: {4}".format(self.rowSelected, self.uuidSelected, self.dtSelected, type(self.dtSelected), self.topSelected))
 3547         self.content.insert(INSERT, text)
 3548         self.update_idletasks()
 3549         logger.debug('ending OnSelect')
 3550         return
 3551 
 3552     def OnActivate(self, event):
 3553         """
 3554         Return pressed with tree row selected
 3555         """
 3556         if not self.itemSelected:
 3557             return "break"
 3558         item = self.tree.selection()[0]
 3559         uuid, dt, hsh = self.getInstance(item)
 3560         x = self.winfo_rootx() + 350
 3561         y = self.winfo_rooty() + 50
 3562         logger.debug("id: {0}, coords: {1}, {2}".format(id, x, y))
 3563         self.itemmenu.post(x, y)
 3564         self.itemmenu.focus_set()
 3565         return "break"
 3566 
 3567     def getInstance(self, item):
 3568         instance = self.count2id[item]
 3569         logger.debug('starting getInstance: {0}; {1}'.format(item, instance))
 3570         if instance is not None:
 3571             uuid, dt = self.count2id[item].split("::")
 3572             hsh = loop.uuid2hash[uuid]
 3573             if dt:
 3574                 dt = parse(dt)
 3575             logger.debug('returning uuid: {0}, dt: {1}'.format(uuid, dt))
 3576             return uuid, dt, hsh
 3577         else:
 3578             logger.debug('returning None')
 3579             return None, None, None
 3580 
 3581     def updateTimerStatus(self):
 3582         title, status = self.actionTimer.getStatus()
 3583         self.timerTitle.set(title)
 3584         self.timerStatus.set(status)
 3585 
 3586     def updateClock(self):
 3587         tt = TimeIt(loglevel=2, label="updateClock")
 3588         self.now = get_current_time()
 3589         self.current_minutes = self.now.hour * 60 + self.now.minute
 3590         nxt = (60 - self.now.second) * 1000 - self.now.microsecond // 1000
 3591         nowfmt = "etm  -  {1}  {0}".format(
 3592             s2or3(self.now.strftime("%a %b %d")),
 3593             s2or3(self.now.strftime(loop.options['reprtimefmt']).lower()),
 3594         )
 3595         logger.debug('next update in {0} milliseconds.'.format(nxt))
 3596         self.after(nxt, self.updateClock)
 3597 
 3598         nowfmt = leadingzero.sub("", nowfmt)
 3599         self.currentTime.set("{0}".format(nowfmt))
 3600         self.title(self.currentTime.get())
 3601         today = self.now.date()
 3602         newday = (today != self.today)
 3603         self.today = today
 3604 
 3605         new, modified, deleted = get_changes(
 3606             self.options, loop.file2lastmodified)
 3607 
 3608         if newday or new or modified or deleted:
 3609             if newday:
 3610                 logger.info('newday')
 3611                 self.actionTimer.newDay()
 3612 
 3613                 # update 'bef' using a naive datetime
 3614                 now = datetime.now()
 3615                 year, wn, dn = now.isocalendar()
 3616                 weeks_after = self.options['weeks_after']
 3617                 if dn > 1:
 3618                     days = dn - 1
 3619                 else:
 3620                     days = 0
 3621                 week_beg = now - days * ONEDAY
 3622                 bef = (week_beg + (7 * (weeks_after + 1)) * ONEDAY)
 3623                 self.options['bef'] = bef
 3624 
 3625             logger.info("new: {0}; modified: {1}; deleted: {2}".format(len(new), len(modified), len(deleted)))
 3626             logger.debug('calling loadData')
 3627             loop.loadData()
 3628             # we now have file2uuids ...
 3629 
 3630             if self.weekly:
 3631                 logger.debug('calling showWeek')
 3632                 self.updateDay()
 3633                 self.showWeek()
 3634                 if newday:
 3635                     self.scrollToDate(today)
 3636             elif self.monthly:
 3637                 logger.debug('calling showMonth')
 3638                 self.updateDay()
 3639                 self.showMonth()
 3640                 if newday:
 3641                     self.scrollToDate(today)
 3642             else:
 3643                 logger.debug('calling showView')
 3644                 self.showView()
 3645 
 3646         if self.current_minutes % loop.options['update_minutes'] == 0:
 3647             if loop.do_update:
 3648                 updateCurrentFiles(loop.rows, loop.file2uuids, loop.uuid2hash, loop.options)
 3649                 loop.do_update = False
 3650 
 3651             if loop.options['icssync_folder']:
 3652                 fullpath = os.path.join(loop.options['datadir'], loop.options['icssync_folder'])
 3653                 prefix, files = getAllFiles(fullpath, include="*")
 3654                 base_files = set([])
 3655                 # file_lst = []
 3656                 for tup in files:
 3657                     base, ext = os.path.splitext(tup[0])
 3658                     if ext in [".txt", ".ics"]:
 3659                         base_files.add(base)
 3660                 file_lst = list(base_files)
 3661                 datadir = loop.options['datadir']
 3662                 for file in file_lst:
 3663                     relfile = relpath(file, datadir)
 3664                     logger.debug('calling syncTxt: {0}; {1}'.format(datadir, relfile))
 3665                     syncTxt(self.loop.file2uuids, self.loop.uuid2hash, datadir, relfile)
 3666                 # any updated txt files will be reloaded in the next update
 3667 
 3668         self.updateAlerts()
 3669 
 3670         if self.actionTimer.currentStatus == RUNNING or (self.actionTimer.idleactive and self.actionTimer.showIdle):
 3671             title, status = self.actionTimer.getStatus()
 3672             self.timerTitle.set(title)
 3673             self.timerStatus.set(status)
 3674             if self.actionTimer.currentMinutes >= 1:
 3675                 if (self.options['action_interval'] and self.actionTimer.currentMinutes % loop.options['action_interval'] == 0):
 3676                     logger.debug('action_minutes trigger: {0} {1}'.format(self.actionTimer.currentMinutes, self.actionTimer.currentStatus))
 3677                     if self.actionTimer.currentStatus == 'running':
 3678                         if ('running' in loop.options['action_timer'] and
 3679                                 loop.options['action_timer']['running']):
 3680                             tcmd = loop.options['action_timer']['running']
 3681                             logger.debug('running: {0}'.format(tcmd))
 3682                             self.check_output(tcmd)
 3683 
 3684                     elif self.actionTimer.currentStatus == 'paused':
 3685                         if ('paused' in loop.options['action_timer'] and
 3686                                 loop.options['action_timer']['paused']):
 3687                             tcmd = loop.options['action_timer']['paused']
 3688 
 3689                             logger.debug('paused: {0}'.format(tcmd))
 3690                             self.check_output(tcmd)
 3691         tt.stop()
 3692 
 3693     def check_output(self, cmd):
 3694         try:
 3695             output = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
 3696         except subprocess.CalledProcessError as exc:
 3697             logger.error("command: {0}\n    output: {1}".format(cmd, exc.output))
 3698 
 3699     def updateAlerts(self):
 3700         self.update_idletasks()
 3701         if loop.alerts:
 3702             logger.debug('updateAlerts: {0}'.format(len(loop.alerts)))
 3703         alerts = deepcopy(loop.alerts)
 3704         if alerts and loop.options['calendars']:
 3705             alerts = [x for x in alerts if self.cal_regex.match(x[-1])]
 3706         if alerts:
 3707             # alerts = [(minutes, id, hsh), ...]
 3708             curr_minutes = datetime2minutes(self.now)
 3709             td = -1
 3710             # pop old alerts
 3711             while td < 0 and alerts:
 3712                 td = alerts[0][0] - curr_minutes
 3713                 if td < 0:
 3714                     a = alerts.pop(0)
 3715             # alerts for this minute will have td's < 1.0
 3716             if td < 1.0:
 3717                 if ('alert_wakecmd' in loop.options and
 3718                         loop.options['alert_wakecmd']):
 3719                     cmd = s2or3(loop.options['alert_wakecmd'])
 3720                     self.check_output(cmd)
 3721                 while td < 1.0 and alerts:
 3722                     hsh = alerts[0][2]
 3723                     alerts.pop(0)
 3724                     actions = hsh['_alert_action']
 3725                     if 's' in actions:
 3726                         if ('alert_soundcmd' in self.options and
 3727                                 self.options['alert_soundcmd']):
 3728                             scmd = s2or3(expand_template(
 3729                                 self.options['alert_soundcmd'], hsh))
 3730                             self.check_output(scmd)
 3731                         else:
 3732                             self.textWindow(parent=self, title="etm", prompt=_("""\
 3733 A sound alert failed. The setting for 'alert_soundcmd' is missing from  your etmtk.cfg."""), opts=self.options)
 3734                     if 'd' in actions:
 3735                         if ('alert_displaycmd' in self.options and
 3736                                 self.options['alert_displaycmd']):
 3737                             dcmd = s2or3(expand_template(
 3738                                 self.options['alert_displaycmd'], hsh))
 3739                             self.check_output(dcmd.encode(loop.options['encoding']['gui']))
 3740                         else:
 3741                             self.textWindow(parent=self, title="etm", prompt=_("""\
 3742 A display alert failed. The setting for 'alert_displaycmd' is missing \
 3743 from your etmtk.cfg."""), opts=self.options)
 3744                     if 'v' in actions:
 3745                         if ('alert_voicecmd' in self.options and
 3746                                 self.options['alert_voicecmd']):
 3747                             vcmd = s2or3(expand_template(
 3748                                 self.options['alert_voicecmd'], hsh))
 3749                             self.check_output(vcmd)
 3750                         else:
 3751                             self.textWindow(parent=self, title="etm", prompt=_("""\
 3752 An email alert failed. The setting for 'alert_voicecmd' is missing from \
 3753 your etmtk.cfg."""), opts=self.options)
 3754                     if 'e' in actions:
 3755                         missing = []
 3756                         for field in ['smtp_from', 'smtp_id', 'smtp_pw', 'smtp_server']:
 3757                             if not self.options[field]:
 3758                                     missing.append(field)
 3759                         if missing:
 3760                             self.textWindow(parent=self, title="etm", prompt=_("""\
 3761 An email alert failed. Settings for the following variables are missing \
 3762 from your etmtk.cfg: %s.""" % ", ".join(["'%s'" % x for x in missing])), opts=self.options)
 3763                         else:
 3764                             subject = hsh['summary']
 3765                             message = expand_template(
 3766                                 self.options['email_template'], hsh)
 3767                             arguments = hsh['_alert_argument']
 3768                             recipients = [str(x).strip() for x in arguments[0]]
 3769                             if 'i' in hsh and hsh['i']:
 3770                                 # invitees
 3771                                 for invitee in hsh['i']:
 3772                                     recipients.append(str(invitee).strip())
 3773                             if len(arguments) > 1:
 3774                                 attachments = [str(x).strip()
 3775                                                for x in arguments[1]]
 3776                             else:
 3777                                 attachments = []
 3778                             if subject and message and recipients:
 3779                                 send_mail(
 3780                                     smtp_to=recipients,
 3781                                     subject=subject,
 3782                                     message=message,
 3783                                     files=attachments,
 3784                                     smtp_from=self.options['smtp_from'],
 3785                                     smtp_server=self.options['smtp_server'],
 3786                                     smtp_id=self.options['smtp_id'],
 3787                                     smtp_pw=self.options['smtp_pw'])
 3788                     if 't' in actions:
 3789                         missing = []
 3790                         for field in ['sms_from', 'sms_message', 'sms_phone', 'sms_pw', 'sms_server', 'sms_subject']:
 3791                             if not self.options[field]:
 3792                                 missing.append(field)
 3793                         if missing:
 3794                             self.textWindow(parent=self, title="etm", prompt=_("""\
 3795 A text alert failed. Settings for the following variables are missing \
 3796 from your 'emt.cfg': %s.""" % ", ".join(["'%s'" % x for x in missing])), opts=self.options)
 3797                         else:
 3798                             message = expand_template(
 3799                                 self.options['sms_message'], hsh)
 3800                             subject = expand_template(
 3801                                 self.options['sms_subject'], hsh)
 3802                             arguments = hsh['_alert_argument']
 3803                             if arguments:
 3804                                 sms_phone = ",".join([str(x).strip() for x in
 3805                                                       arguments[0]])
 3806                             else:
 3807                                 sms_phone = self.options['sms_phone']
 3808                             if message:
 3809                                 send_text(
 3810                                     sms_phone=sms_phone,
 3811                                     subject=subject,
 3812                                     message=message,
 3813                                     sms_from=self.options['sms_from'],
 3814                                     sms_server=self.options['sms_server'],
 3815                                     sms_pw=self.options['sms_pw'])
 3816                     if 'p' in actions:
 3817                         arguments = hsh['_alert_argument']
 3818                         proc = str(arguments[0][0]).strip()
 3819                         cmd = s2or3(expand_template(proc, hsh))
 3820                         self.check_output(cmd)
 3821                     if 'm' in actions:
 3822                         # put this last since the internal message window is modal and thus blocking
 3823                         id = hsh['I']
 3824                         if hsh['next'] is None:
 3825                             # last alert for this item - add an alertId
 3826                             self.alertHsh = hsh
 3827                             self.setmessageAlert()
 3828                         else:
 3829                             self.alertMessage = """\
 3830 {0} ({1})
 3831 {2}
 3832 
 3833 ---------------------------------------------------
 3834 Next alert: {3}.\
 3835 """.format(
 3836         expand_template('!summary!', hsh),
 3837         expand_template('!when!', hsh),
 3838         expand_template(self.options['alert_template'], hsh),
 3839         hsh['next'])
 3840 
 3841                             TextDialog(
 3842                                 self,
 3843                                 title=_("alert - {0}".format(fmt_time( self.now, options=loop.options))),
 3844                                 prompt=self.alertMessage,
 3845                                 close=self.options['message_next']*1000
 3846                             )
 3847 
 3848                     if not alerts:
 3849                         break
 3850                     td = alerts[0][0] - curr_minutes
 3851 
 3852         self.itemAlerts = alerts
 3853 
 3854         self.updateAlertList()
 3855 
 3856 
 3857     def updateAlertList(self):
 3858         self.activeAlterts = [(x[2]['at'], x[2]['alert_time'], x[2]['_event_time'], ", ".join(x[2]['_alert_action']), x[2]['summary'][:26]) for x in self.itemAlerts]
 3859 
 3860         for id in self.messageAlerts:
 3861             x = self.messageAlerts[id]
 3862             at = fmt_time(x[1]['at'], seconds=True, options=self.options)
 3863             self.activeAlterts.append((x[1]['at'], at, x[1]['_event_time'], _('snooze'), x[1]['summary'][:26]))
 3864 
 3865         self.activeAlterts.sort()
 3866 
 3867         if self.activeAlterts:
 3868             if len(self.activeAlterts) > 1:
 3869                 self.pendingAlerts.set("{0} +{1}".format(self.activeAlterts[0][1], len(self.activeAlterts) - 1))
 3870                 self.activeAlerts = self.activeAlterts
 3871             else:
 3872                 self.pendingAlerts.set("{0}".format(self.activeAlterts[0][1]))
 3873                 self.activeAlerts = self.activeAlterts
 3874         else:
 3875             self.pendingAlerts.set('~')
 3876             self.activeAlerts = []
 3877 
 3878     def textWindow(self, parent, title=None, prompt=None, opts=None, modal=True):
 3879         TextDialog(parent, title=title, prompt=prompt, opts=opts, modal=modal)
 3880 
 3881     def goToDate(self, e=None):
 3882         if e and e.char != "j":
 3883             return
 3884         prompt = _("""\
 3885 Return an empty string for the current date or a date to be parsed.
 3886 Relative dates and fuzzy parsing are supported.""")
 3887         if self.view not in [DAY, WEEK, MONTH]:
 3888             return
 3889         d = GetDateTime(parent=self, title=_('date'), prompt=prompt)
 3890         day = d.value
 3891 
 3892         logger.debug('day: {0}'.format(day))
 3893         if day is None:
 3894             return
 3895         self.chosen_date = day.date()
 3896         self.active_date = day.date()
 3897         if self.weekly:
 3898             self.showWeek(event=e, week=None)
 3899         elif self.monthly:
 3900             self.showMonth(event=e, month=None)
 3901         self.scrollToDate(day.date())
 3902         return
 3903 
 3904     def setFilter(self, e=None):
 3905         if self.view in [CUSTOM]:
 3906             return
 3907         self.filter_active = True
 3908         # self.motionmenu.entryconfig(6, state="disabled")
 3909         # self.motionmenu.entryconfig(7, state="normal")
 3910         self.fltr.configure(bg=self.BGCOLOR, fg=self.FGCOLOR, state="normal")
 3911         self.fltr.focus_set()
 3912 
 3913     def clearFilter(self, e=None):
 3914         if self.view in [CUSTOM]:
 3915             return
 3916         self.filter_active = False
 3917         # self.motionmenu.entryconfig(6, state="normal")
 3918         # self.motionmenu.entryconfig(7, state="disabled")
 3919         self.filterValue.set('')
 3920         self.fltr.configure(bg=self.BGCOLOR, fg=self.FGCOLOR)
 3921         self.tree.focus_set()
 3922         if self.rowSelected:
 3923             self.tree.focus(self.rowSelected)
 3924             self.tree.selection_set(self.rowSelected)
 3925             self.tree.see(self.rowSelected)
 3926 
 3927     def leaveFilter(self, e=None):
 3928         self.tree.focus_set()
 3929         if self.rowSelected:
 3930             self.tree.focus(self.rowSelected)
 3931             self.tree.selection_set(self.rowSelected)
 3932             self.tree.see(self.rowSelected)
 3933 
 3934 
 3935     def kloneTimer(self, e=None):
 3936         """
 3937         """
 3938         # hack to avoid activating with Ctrl-k
 3939         if e and e.char != "k":
 3940             return
 3941         if not self.uuidSelected:
 3942             return
 3943         hsh = loop.uuid2hash[self.uuidSelected]
 3944         self.timerItem = self.uuidSelected
 3945         logger.debug('item: {0}'.format(hsh))
 3946         name = hsh['_summary']
 3947 
 3948         for k in self.options['action_keys']:
 3949             if k in hsh and hsh[k]:
 3950                 if type(hsh[k]) is list:
 3951                     v = ", ".join(hsh[k])
 3952                 else:
 3953                     v = hsh[k]
 3954                 name += " @{0} {1}".format(k, v)
 3955         self.actionTimer.selectTimer(name=name)
 3956 
 3957     def finishActionTimer(self, e=None):
 3958         if e and e.char != "T":
 3959             return
 3960         thsh = self.actionTimer.finishTimer(e=e)
 3961         if not thsh:
 3962             return
 3963         self.updateTimerStatus()
 3964 
 3965         hsh = {"itemtype": "~", "_summary": thsh['summary'], "s": thsh['start'], "e": thsh['total']}
 3966         changed = SimpleEditor(parent=self, newhsh=hsh, rephsh=None, options=loop.options, title=_("new action"), modified=True).changed
 3967         if changed:
 3968             # clear status and reload
 3969             self.actionTimer.deleteTimer(timer = self.actionTimer.selected)
 3970 
 3971             self.updateAlerts()
 3972             if self.weekly:
 3973                 self.updateDay()
 3974                 self.showWeek()
 3975             elif self.monthly:
 3976                 self.updateDay()
 3977                 self.showMonth()
 3978             else:
 3979                 self.showView(row=self.topSelected)
 3980 
 3981         self.updateTimerStatus()
 3982 
 3983     def gettext(self, event=None):
 3984         s = self.e.get()
 3985         if s is not None:
 3986             return s
 3987         else:
 3988             return ''
 3989 
 3990     def cleartext(self, event=None):
 3991         self.showView()
 3992         return 'break'
 3993 
 3994     def process_input(self, event=None, cmd=None):
 3995         """
 3996         """
 3997 
 3998         if not cmd:
 3999             return True
 4000         if self.mode == 'command':
 4001             cmd = cmd.strip()
 4002             # if cmd[0] in ['a', 'c']:
 4003             if cmd[0] in ['a']:
 4004                 # simple command history for report commands
 4005                 if cmd in self.history:
 4006                     self.history.remove(cmd)
 4007                 self.history.append(cmd)
 4008                 self.index = len(self.history) - 1
 4009             else:
 4010                 parts = cmd.split(' ')
 4011                 if len(parts) == 2:
 4012                     try:
 4013                         i = int(parts[0])
 4014                     except:
 4015                         i = None
 4016                     if i:
 4017                         parts.pop(0)
 4018                         parts.append(str(i))
 4019                         cmd = " ".join(parts)
 4020             try:
 4021                 res = loop.do_command(cmd)
 4022             except:
 4023                 return _('could not process command "{0}"').format(cmd)
 4024 
 4025         elif self.mode == 'delete':
 4026             loop.cmd_do_delete(cmd)
 4027             res = ''
 4028 
 4029         elif self.mode == 'finish':
 4030             loop.cmd_do_finish(cmd)
 4031             res = ''
 4032 
 4033         elif self.mode == 'new_date':
 4034             res = loop.new_date(cmd)
 4035 
 4036         if not res:
 4037             res = _('command "{0}" returned no output').format(cmd)
 4038 
 4039             logger.debug('no output')
 4040 
 4041             self.clearTree()
 4042             return ()
 4043 
 4044         if type(res) == dict:
 4045             self.showTree(res, event=event)
 4046         else:
 4047             # not a hash => not a tree
 4048             self.textWindow(self, title='etm', prompt=res, opts=self.options)
 4049             return 0
 4050 
 4051     def expand2Depth(self, e=None):
 4052         if e and e.char != "o":
 4053             return
 4054         prompt = _("""\
 4055 Enter an integer depth to expand branches
 4056 or 0 to expand all branches completely.""")
 4057         depth = GetInteger(
 4058             parent=self,
 4059             title=_("depth"), prompt=prompt, opts=[0], default=0).value
 4060         if depth is None:
 4061             return ()
 4062         maxdepth = max([k for k in self.depth2id])
 4063         logger.debug('expand2Depth {0}: {1}/{2}'.format(self.view, depth, maxdepth))
 4064         if self.view in [AGENDA, DAY, KEYWORD, NOTE, TAG, PATH, CUSTOM]:
 4065             self.outline_depths[self.view] = depth
 4066             logger.debug('outline_depths: {0}'.format(self.outline_depths))
 4067         if depth == 0:
 4068             # expand all
 4069             for k in self.depth2id:
 4070                 for item in self.depth2id[k]:
 4071                     self.tree.item(item, open=True)
 4072         else:
 4073             depth -= 1
 4074             depth = max(depth, 0)
 4075             logger.debug('using depth: {0}; {1}'.format(depth, maxdepth))
 4076             for i in range(depth):
 4077                 if i in self.depth2id:
 4078                     for item in self.depth2id[i]:
 4079                         try:
 4080                             self.tree.item(item, open=True)
 4081                         except:
 4082                             logger.exception('open: {0}, {1}'.format(i, item))
 4083             for i in range(depth, maxdepth + 1):
 4084                 if i in self.depth2id:
 4085                     for item in self.depth2id[i]:
 4086                         try:
 4087                             self.tree.item(item, open=False)
 4088                         except:
 4089                             logger.exception('open: {0}, {1}'.format(i, item))
 4090 
 4091     def scrollToDate(self, date=None):
 4092         if not loop.prevnext or date is None:
 4093             return
 4094         if self.view not in [DAY, WEEK, MONTH] or date not in loop.prevnext:
 4095             return
 4096         # new: go to the first date on or **after**, i.e., prevnext last
 4097         active_date = loop.prevnext[date][1]
 4098         if active_date not in self.date2id:
 4099             return
 4100         if self.weekly:
 4101             pos = date.isocalendar()[2] - 1
 4102             self.canvas_idpos = pos
 4103         elif self.monthly:
 4104             pos = date.day - 1
 4105             self.canvas_idpos = pos
 4106         uid = self.date2id[active_date]
 4107         self.active_date = date
 4108         self.canvas_date = date
 4109         self.scrollToId(uid)
 4110 
 4111     def scrollToId(self, uid):
 4112         self.update_idletasks()
 4113         # self.tree.focus_set()
 4114         self.tree.focus(uid)
 4115         self.tree.selection_set(uid)
 4116         self.tree.yview(int(uid) - 1)
 4117 
 4118     def showTree(self, tree, event=None):
 4119         self.date2id = {}
 4120         self.id2date = {}
 4121         self.clearTree()
 4122         self.count = 0
 4123         self.count2id = {}
 4124         self.active_tree = tree
 4125         self.depth2id = {}
 4126         self.add2Tree(u'', tree[self.root], tree)
 4127         loop.count2id = self.count2id
 4128         self.tree.tag_configure('treefont', font=self.tktreefont)
 4129 
 4130         self.content.delete("0.0", END)
 4131 
 4132         if event is None:
 4133 
 4134             if self.view in [DAY, WEEK, MONTH] and self.active_date:
 4135                 self.scrollToDate(self.active_date)
 4136             else:
 4137                 if self.view in [AGENDA, TAG, KEYWORD, NOTE, PATH]:
 4138                     if self.filter_active:
 4139                         depth = 0
 4140                     else:
 4141                         depth = self.outline_depths[self.view]
 4142                     if depth == 0:
 4143                         # expand all
 4144                         for k in self.depth2id:
 4145                             for item in self.depth2id[k]:
 4146                                 self.tree.item(item, open=True)
 4147                     else:
 4148                         maxdepth = max([k for k in self.depth2id])
 4149                         depth -= 1
 4150                         depth = max(depth, 0)
 4151                         for i in range(depth):
 4152                             for item in self.depth2id[i]:
 4153                                 self.tree.item(item, open=True)
 4154                         for i in range(depth, maxdepth + 1):
 4155                             for item in self.depth2id[i]:
 4156                                 self.tree.item(item, open=False)
 4157                 self.goHome()
 4158 
 4159     def popupTree(self, e=None):
 4160         # if self.weekly or self.monthly:
 4161         #     return
 4162         if not self.active_tree:
 4163             return
 4164         depth = self.outline_depths[self.view]
 4165         if loop.options:
 4166             if 'report_indent' in loop.options:
 4167                 indent = loop.options['report_indent']
 4168             if 'report_width1' in loop.options:
 4169                 width1 = loop.options['report_width1']
 4170             if 'report_width2' in loop.options:
 4171                 width2 = loop.options['report_width2']
 4172         else:
 4173             indent = 4
 4174             width1 = 43
 4175             width2 = 20
 4176         res = tree2Text(self.active_tree, indent=indent, width1=width1, width2=width2, depth=depth)
 4177         if not res[0][0]:
 4178             res[0].pop(0)
 4179         prompt = "\n".join(res[0])
 4180         self.textWindow(parent=self, title='etm', opts=self.options, prompt=prompt, modal=False)
 4181 
 4182     def printTree(self, e=None):
 4183         if e and e.char != "p":
 4184             return
 4185         if not self.active_tree:
 4186             return
 4187         ans = self.confirm(parent=self.tree, prompt=_("""Print current outline?"""))
 4188         if not ans:
 4189             return False
 4190         depth = self.outline_depths[self.view]
 4191 
 4192         if loop.options:
 4193             if 'report_indent' in loop.options:
 4194                 indent = loop.options['report_indent']
 4195             if 'report_width1' in loop.options:
 4196                 width1 = loop.options['report_width1']
 4197             if 'report_width2' in loop.options:
 4198                 width2 = loop.options['report_width2']
 4199         else:
 4200             indent = 4
 4201             width1 = 43
 4202             width2 = 20
 4203         res = tree2Text(self.active_tree, indent=indent, width1=width1, width2=width2, depth=depth)
 4204         if not res[0][0]:
 4205             res[0].pop(0)
 4206         res[0].append('')
 4207         s = "{0}".format("\n".join(res[0]))
 4208         self.printWithDefault(s)
 4209 
 4210     def clearTree(self):
 4211         """
 4212         Remove all items from the tree
 4213         """
 4214         self.active_tree = {}
 4215         for child in self.tree.get_children():
 4216             self.tree.delete(child)
 4217 
 4218     def add2Tree(self, parent, elements, tree, depth=0):
 4219         max_depth = 100
 4220         for text in elements:
 4221             self.count += 1
 4222             # text is a key in the element (tree) hash
 4223             # these keys are (parent, item) tuples
 4224             if text in tree:
 4225                 # this is a branch
 4226                 item = " {0}".format(text[1])  # this is the label of the parent
 4227                 children = tree[text]  # these are the children tuples of item
 4228                 oid = self.tree.insert(parent, 'end', iid=self.count, text=item,
 4229                                        open=(depth <= max_depth))
 4230                 self.depth2id.setdefault(depth, set([])).add(oid)
 4231                 # recurse to get children
 4232                 self.count2id[oid] = None
 4233                 self.add2Tree(oid, children, tree, depth=depth + 1)
 4234             else:
 4235                 # this is a leaf
 4236                 if len(text[1]) == 4:
 4237                     uuid, item_type, col1, col3 = text[1]
 4238                     dt = ''
 4239                 else:  # len 5 day view with datetime appended
 4240                     uuid, item_type, col1, col3, dt = text[1]
 4241 
 4242                 if item_type:
 4243                     # This hack avoids encoding issues under python 2
 4244                     col1 = "{0} {1}".format(id2Type[item_type], col1)
 4245 
 4246                 if type(col3) == int:
 4247                     col3 = '%s' % col3
 4248                 else:
 4249                     col3 = s2or3(col3)
 4250 
 4251                 # Drop the instance information from the id
 4252                 id = uuid.split(':')[0]
 4253                 if id in loop.uuid2labels:
 4254                     col2 = loop.uuid2labels[id]
 4255                 else:
 4256                     col2 = "***"
 4257                     if item_type not in ["=", "ib"]:
 4258                         logger.warn('Missing key {0} for {1} {2}'.format(id, col1, col3))
 4259                 oid = self.tree.insert(parent, 'end', iid=self.count, text=col1, open=(depth <= max_depth), values=[col2, col3], tags=(item_type, 'treefont'))
 4260                 self.count2id[oid] = "{0}::{1}".format(uuid, dt)
 4261                 if dt:
 4262                     if item_type == 'by':
 4263                         # we want today, not the starting date for this
 4264                         d = get_current_time().date()
 4265                     else:
 4266                         if type(dt) is datetime:
 4267                             d = dt.date()
 4268