From 381fa58a03a1382da61c15f406e1f749c329fdbe Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 23 Jan 2019 15:13:59 +0300 Subject: [PATCH 01/55] Button "Pause all" added. --- core.py | 148 ++++++--- elements.py | 97 ++++-- sel_cal.py | 185 +++++++---- tasker.pyw | 929 ++++++++++++++++++++++++++++++++++++---------------- 4 files changed, 935 insertions(+), 424 deletions(-) diff --git a/core.py b/core.py index 3851f5e..ff8e667 100644 --- a/core.py +++ b/core.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 -import os -import time +from collections import OrderedDict as odict import datetime - +import os import sqlite3 -from collections import OrderedDict as odict +import time class DbErrors(Exception): @@ -15,6 +14,7 @@ class DbErrors(Exception): class Db: """Class for interaction with database.""" + def __init__(self): self.db_filename = TABLE_FILE self.connect() @@ -31,7 +31,8 @@ def reconnect(self): self.connect() def exec_script(self, script, *values): - """Custom script execution and commit. Returns lastrowid. Raises DbErrors on database exceptions.""" + """Custom script execution and commit. Returns lastrowid. + Raises DbErrors on database exceptions.""" try: if not values: self.cur.execute(script) @@ -48,7 +49,10 @@ def find_by_clause(self, table, field, value, searchfield, order=None): order_by = '' if order: order_by = ' ORDER BY {0}'.format(order) - self.exec_script('SELECT {3} FROM {0} WHERE {1}="{2}"{4}'.format(table, field, value, searchfield, order_by)) + self.exec_script( + 'SELECT {3} FROM {0} WHERE {1}="{2}"{4}'.format(table, field, + value, searchfield, + order_by)) return self.cur.fetchall() def find_all(self, table, sortfield=None): @@ -56,19 +60,24 @@ def find_all(self, table, sortfield=None): if not sortfield: self.exec_script('SELECT * FROM {0}'.format(table)) else: - self.exec_script('SELECT * FROM {0} ORDER BY {1} ASC'.format(table, sortfield)) + self.exec_script( + 'SELECT * FROM {0} ORDER BY {1} ASC'.format(table, sortfield)) return self.cur.fetchall() def select_task(self, task_id): """Returns tuple of values for given task_id.""" - task = list(self.find_by_clause(searchfield='*', field='id', value=task_id, table='tasks')[0]) + task = list( + self.find_by_clause(searchfield='*', field='id', value=task_id, + table='tasks')[0]) # Adding full spent time: - self.exec_script('SELECT sum(spent_time) FROM activity WHERE task_id=%s' % task_id) + self.exec_script( + 'SELECT sum(spent_time) FROM activity WHERE task_id=%s' % task_id) # Adding spent time on position 3: task.insert(2, self.cur.fetchone()[0]) # Append today's spent time: - self.exec_script('SELECT spent_time FROM activity WHERE task_id={0} AND ' - 'date="{1}"'.format(task_id, date_format(datetime.datetime.now()))) + self.exec_script( + 'SELECT spent_time FROM activity WHERE task_id={0} AND ' + 'date="{1}"'.format(task_id, date_format(datetime.datetime.now()))) today_time = self.cur.fetchone() if today_time: task.append(today_time[0]) @@ -77,41 +86,53 @@ def select_task(self, task_id): return task def insert(self, table, fields, values): - """Insert into fields given values. Fields and values should be tuples of same length.""" + """Insert into fields given values. + Fields and values should be tuples of same length.""" placeholder = "(" + ",".join(["?"] * len(values)) + ")" - return self.exec_script('INSERT INTO {0} {1} VALUES {2}'.format(table, fields, placeholder), *values) + return self.exec_script( + 'INSERT INTO {0} {1} VALUES {2}'.format(table, fields, + placeholder), *values) def insert_task(self, name): """Insert task into database.""" date = date_format(datetime.datetime.now()) try: - rowid = self.insert('tasks', ('name', 'creation_date'), (name, date)) + rowid = self.insert('tasks', ('name', 'creation_date'), + (name, date)) except sqlite3.IntegrityError: raise DbErrors("Task name already exists") else: task_id = self.find_by_clause("tasks", "rowid", rowid, "id")[0][0] - self.insert("activity", ("date", "task_id", "spent_time"), (date, task_id, 0)) + self.insert("activity", ("date", "task_id", "spent_time"), + (date, task_id, 0)) self.insert("tasks_tags", ("tag_id", "task_id"), (1, task_id)) return task_id def update(self, field_id, field, value, table="tasks", updfiled="id"): - """Updates given field in given table with given id using given value :) """ - self.exec_script("UPDATE {0} SET {1}=? WHERE {3}='{2}'".format(table, field, field_id, updfiled), value) + """Updates given field in given table with given id + using given value :) """ + self.exec_script( + "UPDATE {0} SET {1}=? WHERE {3}='{2}'".format(table, field, + field_id, updfiled), + value) def update_task(self, task_id, field="spent_time", value=0): """Updates some fields for given task id.""" if field == 'spent_time': self.exec_script("SELECT rowid FROM activity WHERE task_id={0} " - "AND date='{1}'".format(task_id, date_format(datetime.datetime.now()))) + "AND date='{1}'".format(task_id, date_format( + datetime.datetime.now()))) daterow = self.cur.fetchone()[0] - self.update(daterow, table='activity', updfiled='rowid', field=field, value=value) + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value) else: self.update(task_id, field=field, value=value) def delete(self, table="tasks", **field_values): """Removes several records using multiple "field in (values)" clauses. field_values has to be a dictionary which values can be tuples: - field1=(value1, value), field2=value1, field3=(value1, value2, value3)""" + field1=(value1, value), field2=value1, field3=(value1, value2, value3) + """ clauses = [] for key in field_values: value = field_values[key] @@ -128,7 +149,8 @@ def delete(self, table="tasks", **field_values): self.exec_script("DELETE FROM {0}{1}".format(table, clauses)) def delete_tasks(self, values): - """Removes task and all corresponding records. Values has to be tuple.""" + """Removes task and all corresponding records. Values has to be tuple. + """ self.delete(id=values) self.delete(task_id=values, table="activity") self.delete(task_id=values, table="timestamps") @@ -136,19 +158,26 @@ def delete_tasks(self, values): def tasks_to_export(self, ids): """Prepare tasks list for export.""" - self.exec_script("select name, description, activity.date, activity.spent_time from tasks join activity " - "on tasks.id=activity.task_id where tasks.id in {0} order by tasks.name, activity.date". - format(tuple(ids))) + self.exec_script( + "select name, description, activity.date, activity.spent_time " + "from tasks join activity " + "on tasks.id=activity.task_id where tasks.id in {0} " + "order by tasks.name, activity.date". + format(tuple(ids))) res = self.cur.fetchall() result = odict() for item in res: if item[0] in result: result[item[0]][1].append((item[2], time_format(item[3]))) else: - result[item[0]] = [item[1] if item[1] else '', [(item[2], time_format(item[3]))]] - self.exec_script("select name, fulltime from tasks join (select task_id, sum(spent_time) as fulltime " - "from activity where task_id in {0} group by task_id) as act on tasks.id=act.task_id". - format(tuple(ids))) + result[item[0]] = [item[1] if item[1] else '', + [(item[2], time_format(item[3]))]] + self.exec_script( + "select name, fulltime from tasks join (select task_id, " + "sum(spent_time) as fulltime " + "from activity where task_id in {0} group by task_id) " + "as act on tasks.id=act.task_id". + format(tuple(ids))) res = self.cur.fetchall() for item in res: result[item[0]].append(time_format(item[1])) @@ -156,31 +185,41 @@ def tasks_to_export(self, ids): def dates_to_export(self, ids): """Prepare date-based tasks list for export.""" - self.exec_script("select date, tasks.name, tasks.description, spent_time from activity join tasks " - "on activity.task_id=tasks.id where task_id in {0} order by date, tasks.name". - format(tuple(ids))) + self.exec_script( + "select date, tasks.name, tasks.description, " + "spent_time from activity join tasks " + "on activity.task_id=tasks.id where task_id in {0} " + "order by date, tasks.name". + format(tuple(ids))) res = self.cur.fetchall() result = odict() for item in res: if item[0] in result: - result[item[0]][0].append([item[1], item[2] if item[2] else '', time_format(item[3])]) + result[item[0]][0].append([item[1], item[2] if item[2] else '', + time_format(item[3])]) else: - result[item[0]] = [[[item[1], item[2] if item[2] else '', time_format(item[3])]]] - self.exec_script("select date, sum(spent_time) from activity where task_id in {0} group by date " - "order by date".format(tuple(ids))) + result[item[0]] = [[[item[1], item[2] if item[2] else '', + time_format(item[3])]]] + self.exec_script( + "select date, sum(spent_time) from activity where task_id " + "in {0} group by date order by date".format(tuple(ids))) res = self.cur.fetchall() for item in res: result[item[0]].append(time_format(item[1])) return result def tags_dict(self, taskid): - """Creates a list of tag ids, their values in (0, 1) and their names for given task id. + """Creates a list of tag ids, their values in (0, 1) and their names + for provided task id. Tag has value 1 if a record for given task id exists in tags table. """ - tagnames = self.find_all("tags", sortfield="name") # [(1, tagname), (2, tagname)] - self.exec_script("SELECT t.tag_id FROM tasks_tags AS t JOIN tags ON t.tag_id=tags.id WHERE t.task_id=%d" % taskid) - actual_tags = [x[0] for x in self.cur.fetchall()] # [1, 3, ...] - states_list = [] # [[1, [1, 'tag1']], [2, [0, 'tag2']], [3, [1, 'tag3']]] + # [(1, tagname), (2, tagname)] + tagnames = self.find_all("tags", sortfield="name") + self.exec_script("SELECT t.tag_id FROM tasks_tags AS t JOIN tags " + "ON t.tag_id=tags.id WHERE t.task_id=%d" % taskid) + actual_tags = [x[0] for x in self.cur.fetchall()] # [1, 3, ...] + # [[1, [1, 'tag1']], [2, [0, 'tag2']], [3, [1, 'tag3']]] + states_list = [] for k in tagnames: states_list.append([k[0], [1 if k[0] in actual_tags else 0, k[1]]]) return states_list @@ -189,19 +228,22 @@ def simple_tagslist(self): """Returns tags list just like tags_dict() but every tag value is 0.""" tagslist = self.find_all("tags", sortfield="name") res = [[y, [0, x]] for y, x in tagslist] - res.reverse() # Should be reversed to preserve order like in database. + res.reverse() # Should be reversed to preserve order like in database. return res def simple_dateslist(self): """Returns simple list of all dates of activity without duplicates.""" - self.exec_script('SELECT DISTINCT date FROM activity ORDER BY date DESC') + self.exec_script( + 'SELECT DISTINCT date FROM activity ORDER BY date DESC') return [x[0] for x in self.cur.fetchall()] def timestamps(self, taskid, current_time): """Returns timestamps list in same format as simple_tagslist().""" - timestamps = self.find_by_clause('timestamps', 'task_id', taskid, 'timestamp') + timestamps = self.find_by_clause('timestamps', 'task_id', taskid, + 'timestamp') res = [[x[0], [0, '{0}; {1} spent since that moment'.format( - time_format(x[0]), time_format(current_time - x[0]))]] for x in timestamps] + time_format(x[0]), time_format(current_time - x[0]))]] for x in + timestamps] res.reverse() return res @@ -264,13 +306,15 @@ def patch_database(): if not res: for key in sorted(PATCH_SCRIPTS): apply_script(PATCH_SCRIPTS[key], con) - res = (1, ) + res = (1,) else: for key in sorted(PATCH_SCRIPTS): if int(res[0]) < key: apply_script(PATCH_SCRIPTS[key], con) if res[0] != key: - con.executescript("UPDATE options SET value='{0}' WHERE name='patch_ver';".format(str(key))) + con.executescript( + "UPDATE options SET value='{0}' WHERE name='patch_ver';".format( + str(key))) con.commit() con.close() @@ -320,11 +364,11 @@ def apply_script(scripts_list, db_connection): INSERT INTO options VALUES ('install_time', datetime('now')); """ # PATCH_SCRIPTS = { - # 1: [ - # "INSERT INTO options VALUES ('toggle_tasks', '0');" - # ], - # 2: [ - # "UPDATE options SET value='2.0' WHERE name='version';" - # ] +# 1: [ +# "INSERT INTO options VALUES ('toggle_tasks', '0');" +# ], +# 2: [ +# "UPDATE options SET value='2.0' WHERE name='version';" +# ] # } PATCH_SCRIPTS = {} diff --git a/elements.py b/elements.py index f3ce579..4eba25b 100644 --- a/elements.py +++ b/elements.py @@ -40,9 +40,13 @@ def __init__(self, master=None, **kwargs): class CanvasButton(Text, tk.Canvas): - """Button emulation based on Canvas() widget. Can have text and/or preconfigured image.""" - def __init__(self, master=None, image=None, text=None, variable=None, width=None, height=None, textwidth=None, - textheight=None, fontsize=11, opacity=None, relief='raised', bg=None, bd=2, state='normal', + """Button emulation based on Canvas() widget. Can have text + and/or preconfigured image.""" + + def __init__(self, master=None, image=None, text=None, variable=None, + width=None, height=None, textwidth=None, + textheight=None, fontsize=11, opacity=None, relief='raised', + bg=None, bd=2, state='normal', takefocus=True, command=None): super().__init__(master=master) self.pressed = False @@ -51,16 +55,21 @@ def __init__(self, master=None, image=None, text=None, variable=None, width=None self.bg = bg # configure canvas itself with applicable options: standard_options = {} - for item in ('width', 'height', 'relief', 'bg', 'bd', 'state', 'takefocus'): - if eval(item) is not None: # Such check because value of item can be 0. + for item in ( + 'width', 'height', 'relief', 'bg', 'bd', 'state', 'takefocus'): + if eval( + item) is not None: # Check because value of item can be 0. standard_options[item] = eval(item) super().config(**standard_options) self.bind("", self.press_button) self.bind("", self.release_button) - self.bind("", self._place) # Need to be before call of config_button()! + self.bind("", + self._place) # Need to be before call of config_button()! # Configure widget with specific options: - self.config_button(image=image, text=text, variable=variable, textwidth=textwidth, state=state, - textheight=textheight, fontsize=fontsize, opacity=opacity, bg=bg, command=command) + self.config_button(image=image, text=text, variable=variable, + textwidth=textwidth, state=state, + textheight=textheight, fontsize=fontsize, + opacity=opacity, bg=bg, command=command) # Get items dimensions: items_width = self.bbox('all')[2] - self.bbox('all')[0] items_height = self.bbox('all')[3] - self.bbox('all')[1] @@ -68,7 +77,9 @@ def __init__(self, master=None, image=None, text=None, variable=None, width=None if not width: self.config(width=items_width + items_width / 5 + bdsize * 2) if not height: - self.config(height=items_height + ((items_height / 5) if image else (items_height / 2)) + bdsize * 2) + self.config(height=items_height + ( + (items_height / 5) if image else ( + items_height / 2)) + bdsize * 2) # Place all contents in the middle of the widget: self.move('all', (self.winfo_reqwidth() - items_width) / 2, (self.winfo_reqheight() - items_height) / 2) @@ -100,7 +111,9 @@ def _place(self, event): def config_button(self, **kwargs): """Specific configuration of this widget.""" if 'image' in kwargs and kwargs['image']: - self.add_image(kwargs['image'], opacity='right' if 'opacity' not in kwargs else kwargs['opacity']) + self.add_image(kwargs['image'], + opacity='right' if 'opacity' not in kwargs else + kwargs['opacity']) if 'text' in kwargs and kwargs['text']: text = kwargs['text'] elif 'variable' in kwargs and kwargs['variable']: @@ -113,14 +126,16 @@ def config_button(self, **kwargs): if option in kwargs and kwargs[option]: self.textlabel.config(**{option: kwargs[option]}) if text: - self.add_text(text, **{key: kwargs[key] for key in ('fontsize', 'textwidth', 'textheight', 'bg', 'opacity') + self.add_text(text, **{key: kwargs[key] for key in ( + 'fontsize', 'textwidth', 'textheight', 'bg', 'opacity') if key in kwargs}) if 'command' in kwargs and kwargs['command']: self.command = kwargs['command'] def config(self, **kwargs): default_options = {} - for option in ('width', 'height', 'relief', 'bg', 'bd', 'state', 'takefocus'): + for option in ( + 'width', 'height', 'relief', 'bg', 'bd', 'state', 'takefocus'): if option in kwargs: default_options[option] = kwargs[option] if option not in ('bg', 'state', 'font'): @@ -132,12 +147,16 @@ def add_image(self, image, opacity='right'): """Add image.""" coords = [0, 0] if self.bbox('image'): - coords = self.coords('image') # New image will appear in the same position as previous. + coords = self.coords( + 'image') # New image will appear in the same position as previous. self.delete('image') - self.picture = tk.PhotoImage(file=image) # 'self' need to override garbage collection action. - self.create_image(coords[0], coords[1], image=self.picture, anchor='nw', tag='image') + self.picture = tk.PhotoImage( + file=image) # 'self' need to override garbage collection action. + self.create_image(coords[0], coords[1], image=self.picture, + anchor='nw', tag='image') - def add_text(self, textorvariable, fontsize=None, bg=None, opacity="right", textwidth=None, textheight=None): + def add_text(self, textorvariable, fontsize=None, bg=None, opacity="right", + textwidth=None, textheight=None): """Add text. Text can be tkinter.Variable() or string.""" if fontsize: font = tk.font.Font(size=fontsize) @@ -147,22 +166,29 @@ def add_text(self, textorvariable, fontsize=None, bg=None, opacity="right", text self.bg = bg recreate = False if hasattr(self, 'textlabel'): - coords = self.coords('text') # New text will appear in the same position as previous. + coords = self.coords( + 'text') # New text will appear in the same position as previous. recreate = True self.delete(self.textlabel) if isinstance(textorvariable, tk.Variable): - self.textlabel = tk.Label(self, textvariable=textorvariable, bd=0, bg=self.bg, font=font, justify='center', - state=self.cget('state'), width=textwidth, height=textheight) + self.textlabel = tk.Label(self, textvariable=textorvariable, bd=0, + bg=self.bg, font=font, justify='center', + state=self.cget('state'), + width=textwidth, height=textheight) else: - self.textlabel = tk.Label(self, text=textorvariable, bd=0, bg=self.bg, font=font, justify='center', - state=self.cget('state'), width=textwidth, height=textheight) + self.textlabel = tk.Label(self, text=textorvariable, bd=0, + bg=self.bg, font=font, justify='center', + state=self.cget('state'), + width=textwidth, height=textheight) if self.bbox('image'): x_multiplier = self.bbox('image')[2] - self.bbox('image')[0] x_divider = x_multiplier / 6 - y_multiplier = ((self.bbox('image')[3] - self.bbox('image')[1]) - self.textlabel.winfo_reqheight()) / 2 + y_multiplier = ((self.bbox('image')[3] - self.bbox('image')[ + 1]) - self.textlabel.winfo_reqheight()) / 2 else: x_multiplier = x_divider = y_multiplier = 0 - self.create_window(coords[0] if recreate else x_multiplier + x_divider, coords[1] if recreate else y_multiplier, + self.create_window(coords[0] if recreate else x_multiplier + x_divider, + coords[1] if recreate else y_multiplier, anchor='nw', window=self.textlabel, tags='text') # Swap text and image if needed: if opacity == 'left': @@ -183,37 +209,46 @@ def release_button(self, event): if self.cget('state') == 'normal' and self.pressed: self.config(relief='raised') self.move('all', -1, -1) - if callable(self.command) and event.x_root in range(self.winfo_rootx(), self.winfo_rootx() + - self.winfo_width()) and event.y_root in range(self.winfo_rooty(), self.winfo_rooty() + - self.winfo_height()): + if callable(self.command) and event.x_root in range( + self.winfo_rootx(), self.winfo_rootx() + + self.winfo_width()) and event.y_root \ + in range(self.winfo_rooty(), self.winfo_rooty() + + self.winfo_height()): self.command() self.pressed = False class TaskButton(CanvasButton): """Just a button with some default parameters.""" + def __init__(self, parent, textwidth=8, **kwargs): super().__init__(master=parent, textwidth=textwidth, **kwargs) class ScrolledCanvas(tk.Frame): """Scrollable Canvas. Scroll may be horizontal or vertical.""" + def __init__(self, parent=None, orientation="vertical", bd=2, **options): super().__init__(master=parent, relief='groove', bd=bd) scroller = tk.Scrollbar(self, orient=orientation) self.canvbox = tk.Canvas(self, **options) - scroller.config(command=(self.canvbox.xview if orientation == "horizontal" else self.canvbox.yview)) + scroller.config(command=( + self.canvbox.xview if orientation == "horizontal" + else self.canvbox.yview)) if orientation == "horizontal": self.canvbox.config(xscrollcommand=scroller.set) else: self.canvbox.config(yscrollcommand=scroller.set) - scroller.pack(fill='x' if orientation == 'horizontal' else 'y', expand=1, + scroller.pack(fill='x' if orientation == 'horizontal' else 'y', + expand=1, side='bottom' if orientation == 'horizontal' else 'right', anchor='s' if orientation == 'horizontal' else 'e') self.content_frame = tk.Frame(self.canvbox) - self.canvbox.create_window((0, 0), window=self.content_frame, anchor='nw') + self.canvbox.create_window((0, 0), window=self.content_frame, + anchor='nw') self.canvbox.bind("", self.reconf_canvas) - self.canvbox.pack(fill="x" if orientation == "horizontal" else "both", expand=1) + self.canvbox.pack(fill="x" if orientation == "horizontal" else "both", + expand=1) def reconf_canvas(self, event): """Resizing of canvas scrollable region.""" @@ -224,4 +259,4 @@ def reconf_canvas(self, event): def big_font(unit, size=9): """Font size of a given unit change.""" fontname = tk.font.Font(font=unit['font']).actual()['family'] - unit.config(font=(fontname, size)) \ No newline at end of file + unit.config(font=(fontname, size)) diff --git a/sel_cal.py b/sel_cal.py index d0dd6f1..2f84ed7 100644 --- a/sel_cal.py +++ b/sel_cal.py @@ -34,7 +34,7 @@ from Tkconstants import CENTER, LEFT, N, E, W, S from Tkinter import StringVar -except ImportError: # py3k +except ImportError: # py3k import tkinter as Tkinter import tkinter.font as tkFont import tkinter.ttk as ttk @@ -56,7 +56,12 @@ class Calendar(ttk.Frame): datetime = calendar.datetime.datetime timedelta = calendar.datetime.timedelta - def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MONDAY, locale=None, activebackground='#b1dcfb', activeforeground='black', selectbackground='#003eff', selectforeground='white', command=None, borderwidth=1, relief="solid", on_click_month_button=None): + def __init__(self, master=None, year=None, month=None, + firstweekday=calendar.MONDAY, locale=None, + activebackground='#b1dcfb', activeforeground='black', + selectbackground='#003eff', selectforeground='white', + command=None, borderwidth=1, relief="solid", + on_click_month_button=None): """ WIDGET OPTIONS @@ -84,10 +89,13 @@ def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MON self._selection_is_visible = False self._command = command - ttk.Frame.__init__(self, master, borderwidth=borderwidth, relief=relief) + ttk.Frame.__init__(self, master, borderwidth=borderwidth, + relief=relief) - self.bind("", lambda event:self.event_generate('<>')) - self.bind("", lambda event:self.event_generate('<>')) + self.bind("", + lambda event: self.event_generate('<>')) + self.bind("", + lambda event: self.event_generate('<>')) self._cal = get_calendar(locale, firstweekday) @@ -106,13 +114,16 @@ def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MON # header frame and its widgets hframe = ttk.Frame(self) - lbtn = ttk.Button(hframe, style='L.TButton', command=self._on_press_left_button) + lbtn = ttk.Button(hframe, style='L.TButton', + command=self._on_press_left_button) lbtn.pack(side=LEFT) - self._header = elements.SimpleLabel(hframe, width=15, anchor=CENTER, textvariable=self._header_var) + self._header = elements.SimpleLabel(hframe, width=15, anchor=CENTER, + textvariable=self._header_var) self._header.pack(side=LEFT, padx=12) - rbtn = ttk.Button(hframe, style='R.TButton', command=self._on_press_right_button) + rbtn = ttk.Button(hframe, style='R.TButton', + command=self._on_press_right_button) rbtn.pack(side=LEFT) hframe.grid(columnspan=7, pady=4) @@ -121,15 +132,20 @@ def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MON days_of_the_week = self._cal.formatweekheader(3).split() for i, day_of_the_week in enumerate(days_of_the_week): - elements.SimpleLabel(self, text=day_of_the_week, background='grey90').grid(row=1, column=i, sticky=N+E+W+S) + elements.SimpleLabel(self, text=day_of_the_week, + background='grey90').grid(row=1, column=i, + sticky=N + E + W + S) for i in range(6): for j in range(7): - self._day_labels[i,j] = label = elements.SimpleLabel(self, background = "white") + self._day_labels[i, j] = label = elements.SimpleLabel(self, + background="white") - label.grid(row=i+2, column=j, sticky=N+E+W+S) - label.bind("", lambda event: event.widget.configure(background=self._act_bg, foreground=self._act_fg)) - label.bind("", lambda event: event.widget.configure(background="white")) + label.grid(row=i + 2, column=j, sticky=N + E + W + S) + label.bind("", lambda event: event.widget.configure( + background=self._act_bg, foreground=self._act_fg)) + label.bind("", lambda event: event.widget.configure( + background="white")) label.bind("<1>", self._pressed) @@ -146,7 +162,7 @@ def __init__(self, master=None, year=None, month=None, firstweekday=calendar.MON self._build_calendar(year, month) def _build_calendar(self, year, month): - if not( self._year == year and self._month == month): + if not (self._year == year and self._month == month): self._year = year self._month = month @@ -163,26 +179,31 @@ def _build_calendar(self, year, month): fmt_week = [('%02d' % day) if day else '' for day in week] for j, day_number in enumerate(fmt_week): - self._day_labels[i,j]["text"] = day_number + self._day_labels[i, j]["text"] = day_number if len(cal) < 6: for j in range(7): - self._day_labels[5,j]["text"] = "" + self._day_labels[5, j]["text"] = "" - if self._selected_date is not None and self._selected_date.year == self._year and self._selected_date.month == self._month: + if self._selected_date is not None \ + and self._selected_date.year == self._year \ + and self._selected_date.month == self._month: self._show_selection() def _find_label_coordinates(self, date): - first_weekday_of_the_month = (date.weekday() - date.day) % 7 + first_weekday_of_the_month = (date.weekday() - date.day) % 7 - return divmod((first_weekday_of_the_month - self._cal.firstweekday)%7 + date.day, 7) + return divmod(( + first_weekday_of_the_month - self._cal.firstweekday) + % 7 + date.day, 7 + ) def _show_selection(self): """Show a new selection.""" - i,j = self._find_label_coordinates(self._selected_date) + i, j = self._find_label_coordinates(self._selected_date) - label = self._day_labels[i,j] + label = self._day_labels[i, j] label.configure(background=self._sel_bg, foreground=self._sel_fg) @@ -193,13 +214,15 @@ def _show_selection(self): def _clear_selection(self): """Show a new selection.""" - i,j = self._find_label_coordinates(self._selected_date) + i, j = self._find_label_coordinates(self._selected_date) - label = self._day_labels[i,j] - label.configure(background= "white", foreground="black") + label = self._day_labels[i, j] + label.configure(background="white", foreground="black") - label.bind("", lambda event: event.widget.configure(background=self._act_bg, foreground=self._act_fg)) - label.bind("", lambda event: event.widget.configure(background="white")) + label.bind("", lambda event: event.widget.configure( + background=self._act_bg, foreground=self._act_fg)) + label.bind("", + lambda event: event.widget.configure(background="white")) self._selection_is_visible = False @@ -215,7 +238,8 @@ def _pressed(self, evt): day_number = int(text) - new_selected_date = datetime.datetime(self._year, self._month, day_number) + new_selected_date = datetime.datetime(self._year, self._month, + day_number) if self._selected_date != new_selected_date: if self._selected_date is not None: self._clear_selection() @@ -247,7 +271,8 @@ def select_prev_day(self): self._clear_selection() self._selected_date = self._selected_date - self.timedelta(days=1) - self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar + self._build_calendar(self._selected_date.year, + self._selected_date.month) # reconstruct calendar def select_next_day(self): """Update calendar to show the next day.""" @@ -258,8 +283,8 @@ def select_next_day(self): self._clear_selection() self._selected_date = self._selected_date + self.timedelta(days=1) - self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar - + self._build_calendar(self._selected_date.year, + self._selected_date.month) # reconstruct calendar def select_prev_week_day(self): """Updated calendar to show the previous week.""" @@ -269,7 +294,8 @@ def select_prev_week_day(self): self._clear_selection() self._selected_date = self._selected_date - self.timedelta(days=7) - self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar + self._build_calendar(self._selected_date.year, + self._selected_date.month) # reconstruct calendar def select_next_week_day(self): """Update calendar to show the next week.""" @@ -279,44 +305,50 @@ def select_next_week_day(self): self._clear_selection() self._selected_date = self._selected_date + self.timedelta(days=7) - self._build_calendar(self._selected_date.year, self._selected_date.month) # reconstruct calendar + self._build_calendar(self._selected_date.year, + self._selected_date.month) # reconstruct calendar def select_current_date(self): """Update calendar to current date.""" if self._selection_is_visible: self._clear_selection() self._selected_date = datetime.datetime.now() - self._build_calendar(self._selected_date.year, self._selected_date.month) + self._build_calendar(self._selected_date.year, + self._selected_date.month) def prev_month(self): """Updated calendar to show the previous week.""" if self._selection_is_visible: self._clear_selection() - date = self.datetime(self._year, self._month, 1) - self.timedelta(days=1) - self._build_calendar(date.year, date.month) # reconstuct calendar + date = self.datetime(self._year, self._month, 1) - self.timedelta( + days=1) + self._build_calendar(date.year, date.month) # reconstuct calendar def next_month(self): """Update calendar to show the next month.""" if self._selection_is_visible: self._clear_selection() date = self.datetime(self._year, self._month, 1) + \ - self.timedelta(days=calendar.monthrange(self._year, self._month)[1] + 1) + self.timedelta( + days=calendar.monthrange(self._year, self._month)[1] + 1) - self._build_calendar(date.year, date.month) # reconstuct calendar + self._build_calendar(date.year, date.month) # reconstuct calendar def prev_year(self): """Updated calendar to show the previous year.""" if self._selection_is_visible: self._clear_selection() - self._build_calendar(self._year-1, self._month) # reconstruct calendar + self._build_calendar(self._year - 1, + self._month) # reconstruct calendar def next_year(self): """Update calendar to show the next year.""" if self._selection_is_visible: self._clear_selection() - self._build_calendar(self._year+1, self._month) # reconstruct calendar + self._build_calendar(self._year + 1, + self._month) # reconstruct calendar def get_selection(self): """Return a datetime representing the current selected date.""" @@ -331,13 +363,20 @@ def set_selection(self, date): self._selected_date = date - self._build_calendar(date.year, date.month) # reconstruct calendar + self._build_calendar(date.year, date.month) # reconstruct calendar + # see this URL for date format information: # https://docs.python.org/3/library/datetime.html#strftime-strptime-behavior class Datepicker(elements.SimpleTtkEntry): - def __init__(self, master, current_month=None, current_year=None, entrywidth=None, entrystyle=None, datevar=None, dateformat="%Y-%m-%d", onselect=None, firstweekday=calendar.MONDAY, locale=None, activebackground='#b1dcfb', activeforeground='black', selectbackground='#003eff', selectforeground='white', borderwidth=1, relief="solid"): + def __init__(self, master, current_month=None, current_year=None, + entrywidth=None, entrystyle=None, datevar=None, + dateformat="%Y-%m-%d", onselect=None, + firstweekday=calendar.MONDAY, locale=None, + activebackground='#b1dcfb', activeforeground='black', + selectbackground='#003eff', selectforeground='white', + borderwidth=1, relief="solid"): if datevar is not None: self.date_var = datevar @@ -351,57 +390,79 @@ def __init__(self, master, current_month=None, current_year=None, entrywidth=Non if entrystyle is not None: entry_config["style"] = entrystyle - elements.SimpleTtkEntry.__init__(self, master, textvariable=self.date_var, **entry_config) + elements.SimpleTtkEntry.__init__(self, master, + textvariable=self.date_var, + **entry_config) self.date_format = dateformat self._is_calendar_visible = False self._on_select_date_command = onselect - self.calendar_frame = Calendar(self.winfo_toplevel(), month=current_month, year=current_year, firstweekday=firstweekday, locale=locale, activebackground=activebackground, activeforeground=activeforeground, selectbackground=selectbackground, selectforeground=selectforeground, command=self._on_selected_date, on_click_month_button=lambda: self.focus()) + self.calendar_frame = Calendar(self.winfo_toplevel(), + month=current_month, year=current_year, + firstweekday=firstweekday, + locale=locale, + activebackground=activebackground, + activeforeground=activeforeground, + selectbackground=selectbackground, + selectforeground=selectforeground, + command=self._on_selected_date, + on_click_month_button=lambda: self.focus()) self.bind_all("<1>", self._on_click, "+") self.bind("", lambda event: self._on_entry_focus_out()) self.bind("", lambda event: self.hide_calendar()) - self.calendar_frame.bind("<>", lambda event: self._on_calendar_focus_out()) - + self.calendar_frame.bind("<>", + lambda event: self._on_calendar_focus_out()) # CTRL + PAGE UP: Move to the previous month. - self.bind("", lambda event: self.calendar_frame.prev_month()) + self.bind("", + lambda event: self.calendar_frame.prev_month()) # CTRL + PAGE DOWN: Move to the next month. - self.bind("", lambda event: self.calendar_frame.next_month()) + self.bind("", + lambda event: self.calendar_frame.next_month()) # CTRL + SHIFT + PAGE UP: Move to the previous year. - self.bind("", lambda event: self.calendar_frame.prev_year()) + self.bind("", + lambda event: self.calendar_frame.prev_year()) # CTRL + SHIFT + PAGE DOWN: Move to the next year. - self.bind("", lambda event: self.calendar_frame.next_year()) + self.bind("", + lambda event: self.calendar_frame.next_year()) # CTRL + LEFT: Move to the previous day. - self.bind("", lambda event: self.calendar_frame.select_prev_day()) + self.bind("", + lambda event: self.calendar_frame.select_prev_day()) # CTRL + RIGHT: Move to the next day. - self.bind("", lambda event: self.calendar_frame.select_next_day()) + self.bind("", + lambda event: self.calendar_frame.select_next_day()) # CTRL + UP: Move to the previous week. - self.bind("", lambda event: self.calendar_frame.select_prev_week_day()) + self.bind("", + lambda event: self.calendar_frame.select_prev_week_day()) # CTRL + DOWN: Move to the next week. - self.bind("", lambda event: self.calendar_frame.select_next_week_day()) + self.bind("", + lambda event: self.calendar_frame.select_next_week_day()) # CTRL + END: Close the datepicker and erase the date. self.bind("", lambda event: self.erase()) # CTRL + HOME: Move to the current month. - self.bind("", lambda event: self.calendar_frame.select_current_date()) + self.bind("", + lambda event: self.calendar_frame.select_current_date()) # CTRL + SPACE: Show date on calendar - self.bind("", lambda event: self.show_date_on_calendar()) + self.bind("", + lambda event: self.show_date_on_calendar()) # CTRL + Return: Set to entry current selection - self.bind("", lambda event: self.set_date_from_calendar()) + self.bind("", + lambda event: self.set_date_from_calendar()) def set_date_from_calendar(self): if self.is_calendar_visible: @@ -426,7 +487,8 @@ def current_text(self, text): @property def current_date(self): try: - date = datetime.datetime.strptime(self.date_var.get(), self.date_format) + date = datetime.datetime.strptime(self.date_var.get(), + self.date_format) return date except ValueError: return None @@ -492,7 +554,8 @@ def _on_click(self, event): if not self._is_calendar_visible: self.show_date_on_calendar() else: - if not str_widget.startswith(str(self.calendar_frame)) and self._is_calendar_visible: + if not str_widget.startswith( + str(self.calendar_frame)) and self._is_calendar_visible: self.hide_calendar() @@ -507,10 +570,10 @@ def _on_click(self, event): root = Tk() root.geometry("500x600") - main =Frame(root, pady =15, padx=15) + main = Frame(root, pady=15, padx=15) main.pack(expand=True, fill="both") - Label(main, justify="left", text=__doc__).pack(anchor="w", pady=(0,15)) + Label(main, justify="left", text=__doc__).pack(anchor="w", pady=(0, 15)) Datepicker(main).pack(anchor="w") @@ -518,4 +581,4 @@ def _on_click(self, event): style = ttk.Style() style.theme_use('clam') - root.mainloop() \ No newline at end of file + root.mainloop() diff --git a/tasker.pyw b/tasker.pyw index 453a9b4..fe297ff 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -1,26 +1,30 @@ #!/usr/bin/env python3 -import time -import datetime import copy +import datetime +import time try: import tkinter as tk except ModuleNotFoundError: import sys - sys.exit("Unable to start GUI. Please install Tk for Python: https://docs.python.org/3/library/tkinter.html.") + + sys.exit( + "Unable to start GUI. Please install Tk for Python: " + "https://docs.python.org/3/library/tkinter.html.") from tkinter.filedialog import asksaveasfilename from tkinter.messagebox import askyesno, showinfo from tkinter import ttk from tkinter import TclError -import elements import core +import elements import sel_cal class Window(tk.Toplevel): """Universal class for dialogue windows creation.""" + def __init__(self, master=None, **options): super().__init__(master=master, **options) self.db = core.Db() @@ -34,7 +38,8 @@ class Window(tk.Toplevel): self.wait_window() def on_top_wait(self): - """Allows window to be on the top of others when 'always on top' is enabled.""" + """Allows window to be on the top of others + when 'always on top' is enabled.""" ontop = GLOBAL_OPTIONS['always_on_top'] if ontop == '1': self.wm_attributes("-topmost", 1) @@ -44,15 +49,21 @@ class Window(tk.Toplevel): if parent: stored_xpos = parent.winfo_rootx() self.geometry('+%d+%d' % (stored_xpos, parent.winfo_rooty())) - self.withdraw() # temporary hide window. + self.withdraw() # temporary hide window. self.update_idletasks() - # Check if window will appear inside screen borders and move it if not: - if self.winfo_rootx() + self.winfo_width() > self.winfo_screenwidth(): - stored_xpos = (self.winfo_screenwidth() - self.winfo_width() - 50) + # Check if window will appear inside screen borders + # and move it if not: + if self.winfo_rootx() + self.winfo_width() \ + > self.winfo_screenwidth(): + stored_xpos = ( + self.winfo_screenwidth() - self.winfo_width() - 50) self.geometry('+%d+%d' % (stored_xpos, parent.winfo_rooty())) - if self.winfo_rooty() + self.winfo_height() > self.winfo_screenheight(): - self.geometry('+%d+%d' % (stored_xpos, (self.winfo_screenheight() - self.winfo_height() - 150))) - self.deiconify() # restore hidden window. + if self.winfo_rooty() + self.winfo_height() \ + > self.winfo_screenheight(): + self.geometry('+%d+%d' % (stored_xpos, ( + self.winfo_screenheight() - self.winfo_height() - + 150))) + self.deiconify() # restore hidden window. def destroy(self): self.db.con.close() @@ -64,17 +75,23 @@ class Window(tk.Toplevel): class TaskLabel(elements.SimpleLabel): """Simple sunken text label.""" + def __init__(self, parent, anchor='center', **kwargs): - super().__init__(master=parent, relief='sunken', anchor=anchor, **kwargs) + super().__init__(master=parent, relief='sunken', anchor=anchor, + **kwargs) context_menu = RightclickMenu() self.bind("", context_menu.context_menu_show) class Description(tk.Frame): """Description frame - Text frame with scroll.""" - def __init__(self, parent=None, copy_menu=True, paste_menu=False, state='normal', **options): + + def __init__(self, parent=None, copy_menu=True, paste_menu=False, + state='normal', **options): super().__init__(master=parent) - self.text = elements.SimpleText(self, bg=GLOBAL_OPTIONS["colour"], state=state, wrap='word', bd=2, **options) + self.text = elements.SimpleText(self, bg=GLOBAL_OPTIONS["colour"], + state=state, wrap='word', bd=2, + **options) scroller = tk.Scrollbar(self) scroller.config(command=self.text.yview) self.text.config(yscrollcommand=scroller.set) @@ -83,7 +100,8 @@ class Description(tk.Frame): self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure('all', weight=1) # Context menu for copying contents: - self.context_menu = RightclickMenu(copy_item=copy_menu, paste_item=paste_menu) + self.context_menu = RightclickMenu(copy_item=copy_menu, + paste_item=paste_menu) self.text.bind("", self.context_menu.context_menu_show) def config(self, cnf=None, **kw): @@ -107,6 +125,7 @@ class Description(tk.Frame): class TaskFrame(tk.Frame): """Task frame on application's main screen.""" + def __init__(self, parent=None): super().__init__(master=parent, relief='raised', bd=2) self.db = core.Db() @@ -115,9 +134,10 @@ class TaskFrame(tk.Frame): def create_content(self): """Creates all window elements.""" - self.startstopvar = tk.StringVar() # Text on "Start" button. + self.startstopvar = tk.StringVar() # Text on "Start" button. self.startstopvar.set("Start") - self.task = None # Fake name of running task (which actually is not selected yet). + # Fake name of running task (which actually is not selected yet). + self.task = None self.task_id = None self.description = None if GLOBAL_OPTIONS["compact_interface"] == "0": @@ -125,29 +145,49 @@ class TaskFrame(tk.Frame): # Task name field: self.tasklabel = TaskLabel(self, width=50, anchor='w') elements.big_font(self.tasklabel, size=14) - self.tasklabel.grid(row=1, column=0, columnspan=5, padx=5, pady=5, sticky='w') - self.openbutton = elements.TaskButton(self, text="Task...", command=self.name_dialogue) + self.tasklabel.grid(row=1, column=0, columnspan=5, padx=5, pady=5, + sticky='w') + self.openbutton = elements.TaskButton(self, text="Task...", + command=self.name_dialogue) self.openbutton.grid(row=1, column=5, padx=5, pady=5, sticky='e') - self.startbutton = elements.CanvasButton(self, state='disabled', fontsize=14, command=self.startstopbutton, - variable=self.startstopvar, image='resource/start_disabled.png' if tk.TkVersion >= 8.6 - else 'resource/start_disabled.pgm', opacity='left') + self.startbutton = elements.CanvasButton( + self, state='disabled', + fontsize=14, + command=self.startstopbutton, + variable=self.startstopvar, + image='resource/start_disabled.png' if tk.TkVersion >= 8.6 + else 'resource/start_disabled.pgm', + opacity='left') self.startbutton.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: self.timer_window = TaskLabel(self, width=10, state='disabled') elements.big_font(self.timer_window, size=20) self.timer_window.grid(row=3, column=1, pady=5) - self.add_timestamp_button = elements.CanvasButton(self, text='Add\ntimestamp', state='disabled', command=self.add_timestamp) - self.add_timestamp_button.grid(row=3, sticky='sn', column=2, padx=5) - self.timestamps_window_button = elements.CanvasButton(self, text='View\ntimestamps...', state='disabled', - command=self.timestamps_window) - self.timestamps_window_button.grid(row=3, column=3, sticky='wsn', padx=5) - self.properties = elements.TaskButton(self, text="Properties...", textwidth=9, state='disabled', - command=self.properties_window) + self.add_timestamp_button = elements.CanvasButton( + self, + text='Add\ntimestamp', + state='disabled', + command=self.add_timestamp + ) + self.add_timestamp_button.grid(row=3, sticky='sn', column=2, padx=5) + self.timestamps_window_button = elements.CanvasButton( + self, + text='View\ntimestamps...', + state='disabled', + command=self.timestamps_window + ) + self.timestamps_window_button.grid(row=3, column=3, sticky='wsn', + padx=5) + self.properties = elements.TaskButton(self, text="Properties...", + textwidth=9, state='disabled', + command=self.properties_window) self.properties.grid(row=3, column=4, sticky='e', padx=5) # Clear frame button: - self.clearbutton = elements.TaskButton(self, text='Clear', state='disabled', textwidth=7, command=self.clear) + self.clearbutton = elements.TaskButton(self, text='Clear', + state='disabled', textwidth=7, + command=self.clear) self.clearbutton.grid(row=3, column=5, sticky='e', padx=5) - self.running_time = 0 # Current value of the counter. + self.running_time = 0 # Current value of the counter. self.running = False self.timestamp = 0 @@ -159,7 +199,8 @@ class TaskFrame(tk.Frame): self.l1.grid(row=0, column=1, columnspan=3) # Task description field: self.description = Description(self, width=60, height=3) - self.description.grid(row=2, column=0, columnspan=6, padx=5, pady=6, sticky='we') + self.description.grid(row=2, column=0, columnspan=6, padx=5, pady=6, + sticky='we') if self.task: self.description.update_text(self.task[3]) @@ -175,7 +216,8 @@ class TaskFrame(tk.Frame): def add_timestamp(self): """Adding timestamp to database.""" - self.db.insert('timestamps', ('task_id', 'timestamp'), (self.task_id, self.running_time)) + self.db.insert('timestamps', ('task_id', 'timestamp'), + (self.task_id, self.running_time)) showinfo("Timestamp added", "Timestamp added.") def startstopbutton(self): @@ -226,7 +268,8 @@ class TaskFrame(tk.Frame): def get_restored_task_name(self, taskid): self.task_id = taskid # Preparing new task: - self.prepare_task(self.db.select_task(self.task_id)) # Task parameters from database + self.prepare_task( + self.db.select_task(self.task_id)) # Task parameters from database def prepare_task(self, task): """Prepares frame elements to work with.""" @@ -246,7 +289,8 @@ class TaskFrame(tk.Frame): self.tasklabel.config(text=self.task[1]) self.startbutton.config(state='normal') self.startbutton.config(image='resource/start_normal.png' - if tk.TkVersion >= 8.6 else 'resource/start_normal.pgm') + if tk.TkVersion >= 8.6 + else 'resource/start_normal.pgm') self.properties.config(state='normal') self.clearbutton.config(state='normal') self.timer_window.config(state='normal') @@ -263,14 +307,16 @@ class TaskFrame(tk.Frame): self.running_time = self.task[2] def reload_timer(self): - """Used for task data reloading without explicitly redraw anything but timer.""" + """Used for task data reloading + without explicitly redrawing anything but timer.""" self.timer_stop() self.task = self.db.select_task(self.task_id) self.set_current_time() self.timer_start() def check_date(self): - """Used to check if date has been changed since last timer value save.""" + """Used to check if date has been changed + since last timer value save.""" current_date = core.date_format(datetime.datetime.now()) if current_date != self.current_date: self.current_date = current_date @@ -283,22 +329,27 @@ class TaskFrame(tk.Frame): """Updates time in the database.""" if not self.date_exists: self.db.insert("activity", ("date", "task_id", "spent_time"), - (self.current_date, self.task[0], self.running_today_time)) + (self.current_date, self.task[0], + self.running_today_time)) self.date_exists = True - # self.reload_timer() + # self.reload_timer() else: self.db.update_task(self.task[0], value=self.running_today_time) self.timestamp = self.running_today_time def timer_update(self, counter=0): """Renewal of the counter.""" - interval = 250 # Time interval in milliseconds before next iteration of recursion. + # Time interval in milliseconds + # before next iteration of recursion: + interval = 250 self.running_time = time.time() - self.start_time self.running_today_time = time.time() - self.start_today_time - self.timer_window.config(text=core.time_format(self.running_time if self.running_time < 86400 - else self.running_today_time)) + self.timer_window.config(text=core.time_format( + self.running_time if self.running_time < 86400 + else self.running_today_time)) # Checking if "Stop all" button is pressed: - if not GLOBAL_OPTIONS["stopall"] and GLOBAL_OPTIONS["tasks"][self.task_id]: + if not GLOBAL_OPTIONS["stopall"] and GLOBAL_OPTIONS["tasks"][ + self.task_id]: # Every n seconds counter value is saved in database: if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: self.check_date() @@ -306,7 +357,8 @@ class TaskFrame(tk.Frame): else: counter += interval # self.timer variable becomes ID created by after(): - self.timer = self.timer_window.after(interval, self.timer_update, counter) + self.timer = self.timer_window.after(interval, self.timer_update, + counter) else: self.timer_stop() @@ -324,7 +376,9 @@ class TaskFrame(tk.Frame): self.start_today_time = time.time() - self.task[-1] self.timer_update() self.running = True - self.startbutton.config(image='resource/stop.png' if tk.TkVersion >= 8.6 else 'resource/stop.pgm') + self.startbutton.config( + image='resource/stop.png' if tk.TkVersion >= 8.6 + else 'resource/stop.pgm') self.startstopvar.set("Stop") def timer_stop(self): @@ -341,12 +395,16 @@ class TaskFrame(tk.Frame): self.task[2] = self.running_time self.task[5] = self.running_today_time self.update_description() - self.startbutton.config(image='resource/start_normal.png' if tk.TkVersion >= 8.6 else 'resource/start_normal.pgm') + self.startbutton.config( + image='resource/start_normal.png' if tk.TkVersion >= 8.6 + else 'resource/start_normal.pgm') self.startstopvar.set("Start") def update_description(self): """Update text in "Description" field.""" - self.task[3] = self.db.find_by_clause("tasks", "id", self.task[0], "description")[0][0] + self.task[3] = \ + self.db.find_by_clause("tasks", "id", self.task[0], + "description")[0][0] if self.description: self.description.update_text(self.task[3]) @@ -361,9 +419,10 @@ class TaskFrame(tk.Frame): class TaskList(tk.Frame): """Scrollable tasks table.""" + def __init__(self, columns, parent=None, **options): super().__init__(master=parent, **options) - self.taskslist = ttk.Treeview(self) # A table. + self.taskslist = ttk.Treeview(self) # A table. style = ttk.Style() style.configure(".", font=('Helvetica', 11)) style.configure("Treeview.Heading", font=('Helvetica', 11)) @@ -376,11 +435,14 @@ class TaskList(tk.Frame): self.taskslist.config(columns=tuple([col[0] for col in columns])) for index, col in enumerate(columns): # Configuring columns with given ids: - self.taskslist.column(columns[index][0], width=100, minwidth=100, anchor='center') + self.taskslist.column(columns[index][0], width=100, minwidth=100, + anchor='center') # Configuring headers of columns with given ids: - self.taskslist.heading(columns[index][0], text=columns[index][1], command=lambda c=columns[index][0]: + self.taskslist.heading(columns[index][0], text=columns[index][1], + command=lambda c=columns[index][0]: self.sortlist(c, True)) - self.taskslist.column('#0', anchor='w', width=70, minwidth=50, stretch=0) + self.taskslist.column('#0', anchor='w', width=70, minwidth=50, + stretch=0) self.taskslist.column('taskname', width=600, anchor='w') def sortlist(self, col, reverse): @@ -394,7 +456,8 @@ class TaskList(tk.Frame): shortlist.sort(key=lambda x: x[0], reverse=reverse) for index, value in enumerate(shortlist): self.taskslist.move(value[1], '', index) - self.taskslist.heading(col, command=lambda: self.sortlist(col, not reverse)) + self.taskslist.heading(col, + command=lambda: self.sortlist(col, not reverse)) def _sort(self, position, reverse): l = [] @@ -405,8 +468,9 @@ class TaskList(tk.Frame): return l def insert_tasks(self, tasks): - """Insert rows in the table. Row contents are tuples given in values=.""" - for i, v in enumerate(tasks): # item, number, value: + """Insert rows in the table. Row contents + are tuples provided by 'values='.""" + for i, v in enumerate(tasks): # item, number, value: self.taskslist.insert('', i, text="#%d" % (i + 1), values=v) def update_list(self, tasks): @@ -428,17 +492,22 @@ class TaskList(tk.Frame): class TaskSelectionWindow(Window): """Task selection and creation window.""" + def __init__(self, parent=None, taskvar=None, **options): super().__init__(master=parent, **options) # Variable which will contain selected task id: if taskvar: self.taskidvar = taskvar # Basic script for retrieving tasks from database: - self.main_script = 'SELECT id, name, total_time, description, creation_date FROM tasks JOIN (SELECT task_id, ' \ - 'sum(spent_time) AS total_time FROM activity GROUP BY task_id) AS act ON act.task_id=tasks.id' + self.main_script = 'SELECT id, name, total_time, description, ' \ + 'creation_date FROM tasks JOIN (SELECT task_id, ' \ + 'sum(spent_time) AS total_time FROM activity ' \ + 'GROUP BY task_id) AS act ON act.task_id=tasks.id' self.title("Task selection") self.minsize(width=500, height=350) - elements.SimpleLabel(self, text="New task:").grid(row=0, column=0, sticky='w', pady=5, padx=5) + elements.SimpleLabel(self, text="New task:").grid(row=0, column=0, + sticky='w', pady=5, + padx=5) # New task entry field: self.addentry = elements.SimpleEntry(self, width=50) self.addentry.grid(row=0, column=1, columnspan=3, sticky='we') @@ -447,89 +516,135 @@ class TaskSelectionWindow(Window): self.addentry.focus_set() # Context menu with 'Paste' option: addentry_context_menu = RightclickMenu(paste_item=1, copy_item=0) - self.addentry.bind("", addentry_context_menu.context_menu_show) + self.addentry.bind("", + addentry_context_menu.context_menu_show) # "Add task" button: - self.addbutton = elements.TaskButton(self, text="Add task", command=self.add_new_task, takefocus=False) + self.addbutton = elements.TaskButton(self, text="Add task", + command=self.add_new_task, + takefocus=False) self.addbutton.grid(row=0, column=4, sticky='e', padx=6, pady=5) # Entry for typing search requests: self.searchentry = elements.SimpleEntry(self, width=25) - self.searchentry.grid(row=1, column=1, columnspan=2, sticky='we', padx=5, pady=5) + self.searchentry.grid(row=1, column=1, columnspan=2, sticky='we', + padx=5, pady=5) searchentry_context_menu = RightclickMenu(paste_item=1, copy_item=0) - self.searchentry.bind("", searchentry_context_menu.context_menu_show) + self.searchentry.bind("", + searchentry_context_menu.context_menu_show) # Case sensitive checkbutton: self.ignore_case = tk.IntVar(self, value=1) - elements.SimpleCheckbutton(self, text="Ignore case", takefocus=False, variable=self.ignore_case).grid(row=1, column=0, - padx=6, pady=5, sticky='w') + elements.SimpleCheckbutton(self, text="Ignore case", takefocus=False, + variable=self.ignore_case).grid(row=1, + column=0, + padx=6, + pady=5, + sticky='w') # Search button: - elements.CanvasButton(self, takefocus=False, text='Search', image='resource/magnifier.png' if tk.TkVersion >= 8.6 else - 'resource/magnifier.pgm', command=self.locate_task).grid(row=1, column=3, sticky='w', padx=5, pady=5) + elements.CanvasButton(self, takefocus=False, text='Search', + image='resource/magnifier.png' + if tk.TkVersion >= 8.6 + else 'resource/magnifier.pgm', + command=self.locate_task).grid(row=1, column=3, + sticky='w', + padx=5, pady=5) # Refresh button: - elements.TaskButton(self, takefocus=False, image='resource/refresh.png' if tk.TkVersion >= 8.6 else 'resource/refresh.pgm', - command=self.update_list).grid(row=1, column=4, sticky='e', padx=5, pady=5) + elements.TaskButton(self, takefocus=False, + image='resource/refresh.png' + if tk.TkVersion >= 8.6 else 'resource/refresh.pgm', + command=self.update_list).grid(row=1, column=4, + sticky='e', padx=5, + pady=5) # Naming of columns in tasks list: - columnnames = [('taskname', 'Task name'), ('time', 'Spent time'), ('date', 'Creation date')] + columnnames = [('taskname', 'Task name'), ('time', 'Spent time'), + ('date', 'Creation date')] # Scrollable tasks table: self.listframe = TaskList(columnnames, self, takefocus=True) - self.listframe.grid(row=2, column=0, columnspan=5, pady=10, sticky='news') - elements.SimpleLabel(self, text="Summary time:").grid(row=3, column=0, pady=5, padx=5, sticky='w') + self.listframe.grid(row=2, column=0, columnspan=5, pady=10, + sticky='news') + elements.SimpleLabel(self, text="Summary time:").grid(row=3, column=0, + pady=5, padx=5, + sticky='w') # Summarized time of all tasks in the table: self.fulltime_frame = TaskLabel(self, width=13, anchor='center') self.fulltime_frame.grid(row=3, column=1, padx=6, pady=5, sticky='e') # Selected task description: self.description = Description(self, height=4) - self.description.grid(row=3, column=2, rowspan=2, pady=5, padx=5, sticky='news') + self.description.grid(row=3, column=2, rowspan=2, pady=5, padx=5, + sticky='news') # "Select all" button: - selbutton = elements.TaskButton(self, text="Select all", command=self.select_all) + selbutton = elements.TaskButton(self, text="Select all", + command=self.select_all) selbutton.grid(row=4, column=0, sticky='w', padx=5, pady=5) # "Clear all" button: - clearbutton = elements.TaskButton(self, text="Clear all", command=self.clear_all) + clearbutton = elements.TaskButton(self, text="Clear all", + command=self.clear_all) clearbutton.grid(row=4, column=1, sticky='e', padx=5, pady=5) # Task properties button: - self.editbutton = elements.TaskButton(self, text="Properties...", textwidth=10, command=self.edit) + self.editbutton = elements.TaskButton(self, text="Properties...", + textwidth=10, command=self.edit) self.editbutton.grid(row=3, column=3, sticky='w', padx=5, pady=5) # Remove task button: - self.delbutton = elements.TaskButton(self, text="Remove...", textwidth=10, command=self.delete) + self.delbutton = elements.TaskButton(self, text="Remove...", + textwidth=10, command=self.delete) self.delbutton.grid(row=4, column=3, sticky='w', padx=5, pady=5) # Export button: - self.exportbutton = elements.TaskButton(self, text="Export...", command=self.export) + self.exportbutton = elements.TaskButton(self, text="Export...", + command=self.export) self.exportbutton.grid(row=4, column=4, padx=5, pady=5, sticky='e') # Filter button: - self.filterbutton = elements.TaskButton(self, text="Filter...", command=self.filterwindow) + self.filterbutton = elements.TaskButton(self, text="Filter...", + command=self.filterwindow) self.filterbutton.grid(row=3, column=4, padx=5, pady=5, sticky='e') # Filter button context menu: filter_context_menu = RightclickMenu(copy_item=False) - filter_context_menu.add_command(label='Clear filter', command=self.apply_filter) - self.filterbutton.bind("", filter_context_menu.context_menu_show) + filter_context_menu.add_command(label='Clear filter', + command=self.apply_filter) + self.filterbutton.bind("", + filter_context_menu.context_menu_show) tk.Frame(self, height=40).grid(row=5, columnspan=5, sticky='news') self.grid_columnconfigure(2, weight=1, minsize=50) self.grid_rowconfigure(2, weight=1, minsize=50) - self.update_list() # Fill table contents. - self.current_task = '' # Current selected task. + self.update_list() # Fill table contents. + self.current_task = '' # Current selected task. self.listframe.taskslist.bind("", self.descr_down) self.listframe.taskslist.bind("", self.descr_up) self.listframe.taskslist.bind("", self.descr_click) - self.listframe.bind("", lambda e: self.focus_first_item(forced=False)) - # Need to avoid masquerading of default ttk.Treeview action on Shift+click and Control+click: + self.listframe.bind("", + lambda e: self.focus_first_item(forced=False)) + # Need to avoid masquerading of default ttk.Treeview action + # on Shift+click and Control+click: self.modifier_pressed = False - self.listframe.taskslist.bind("", lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", lambda e: self.shift_control_released()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_pressed()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_pressed()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_pressed()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_pressed()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_released()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_released()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_released()) + self.listframe.taskslist.bind("", + lambda e: self.shift_control_released()) self.searchentry.bind("", lambda e: self.locate_task()) self.bind("", lambda e: self.update_list()) - elements.TaskButton(self, text="Open", command=self.get_task).grid(row=6, column=0, padx=5, pady=5, sticky='w') - elements.TaskButton(self, text="Cancel", command=self.destroy).grid(row=6, column=4, padx=5, pady=5, sticky='e') + elements.TaskButton(self, text="Open", command=self.get_task).grid( + row=6, column=0, padx=5, pady=5, sticky='w') + elements.TaskButton(self, text="Cancel", command=self.destroy).grid( + row=6, column=4, padx=5, pady=5, sticky='e') self.listframe.taskslist.bind("", self.get_task_id) self.listframe.taskslist.bind("", self.get_task_id) self.prepare() def check_row(self, event): - """Check if mouse click is over the row, not another taskslist element.""" - if (event.type == '4' and len(self.listframe.taskslist.identify_row(event.y)) > 0) or (event.type == '2'): + """Check if mouse click is over the row, + not another taskslist element.""" + if (event.type == '4' and len( + self.listframe.taskslist.identify_row(event.y)) > 0) or ( + event.type == '2'): return True def get_task(self): @@ -577,7 +692,8 @@ class TaskSelectionWindow(Window): for key in self.tdict: if searchword.lower() in self.tdict[key][1].lower(): task_items.append(key) - elif self.tdict[key][3]: # Need to be sure that there is non-empty description. + # Need to be sure that there is non-empty description: + elif self.tdict[key][3]: if searchword.lower() in self.tdict[key][3].lower(): task_items.append(key) else: @@ -596,7 +712,9 @@ class TaskSelectionWindow(Window): self.listframe.taskslist.focus(item) self.update_descr(item) else: - showinfo("No results", "No tasks found.\nMaybe need to change filter settings?") + showinfo("No results", + "No tasks found." + "\nMaybe need to change filter settings?") def export(self): """Export all tasks from the table into the file.""" @@ -618,7 +736,9 @@ class TaskSelectionWindow(Window): self.update_descr(item) break else: - showinfo("Task exists", "Task already exists. Change filter configuration to see it.") + showinfo("Task exists", + "Task already exists. " + "Change filter configuration to see it.") else: self.update_list() # If created task appears in the table, highlighting it: @@ -627,10 +747,13 @@ class TaskSelectionWindow(Window): self.listframe.focus_(item) break else: - showinfo("Task created", "Task successfully created. Change filter configuration to see it.") + showinfo("Task created", + "Task successfully created. " + "Change filter configuration to see it.") def filter_query(self): - return self.db.find_by_clause('options', 'name', 'filter', 'value')[0][0] + return self.db.find_by_clause( + 'options', 'name', 'filter', 'value')[0][0] def update_list(self): """Updating table contents using database query.""" @@ -655,11 +778,13 @@ class TaskSelectionWindow(Window): def update_fulltime(self): """Updates value in "fulltime" frame.""" - self.fulltime = core.time_format(sum([self.tdict[x][2] for x in self.tdict])) + self.fulltime = core.time_format( + sum([self.tdict[x][2] for x in self.tdict])) self.fulltime_frame.config(text=self.fulltime) def descr_click(self, event): - """Updates description for the task with item id of the row selected by click.""" + """Updates description for the task with item id of the row + selected by click.""" pos = self.listframe.taskslist.identify_row(event.y) if pos and pos != '#0' and not self.modifier_pressed: self.listframe.focus_(pos) @@ -691,18 +816,24 @@ class TaskSelectionWindow(Window): self.description.update_text(self.tdict[item][3]) def select_all(self): - self.listframe.taskslist.selection_set(self.listframe.taskslist.get_children()) + self.listframe.taskslist.selection_set( + self.listframe.taskslist.get_children()) def clear_all(self): - self.listframe.taskslist.selection_remove(self.listframe.taskslist.get_children()) + self.listframe.taskslist.selection_remove( + self.listframe.taskslist.get_children()) def delete(self): """Remove selected tasks from the database and the table.""" - ids = [self.tdict[x][0] for x in self.listframe.taskslist.selection() if self.tdict[x][0] + ids = [self.tdict[x][0] for x in self.listframe.taskslist.selection() + if self.tdict[x][0] not in GLOBAL_OPTIONS["tasks"]] - items = [x for x in self.listframe.taskslist.selection() if self.tdict[x][0] in ids] + items = [x for x in self.listframe.taskslist.selection() if + self.tdict[x][0] in ids] if ids: - answer = askyesno("Warning", "Are you sure you want to delete selected tasks?", parent=self) + answer = askyesno("Warning", + "Are you sure you want to delete selected tasks?", + parent=self) if answer: self.db.delete_tasks(tuple(ids)) self.listframe.taskslist.delete(*items) @@ -715,7 +846,8 @@ class TaskSelectionWindow(Window): """Show task edit window.""" item = self.listframe.taskslist.focus() try: - id_name = (self.tdict[item][0], self.tdict[item][1]) # Tuple: (selected_task_id, selected_task_name) + id_name = (self.tdict[item][0], self.tdict[item][ + 1]) # Tuple: (selected_task_id, selected_task_name) except KeyError: pass else: @@ -728,8 +860,9 @@ class TaskSelectionWindow(Window): self.tdict[item] = new_task_info self.update_descr(item) # Update data in a table: - self.listframe.taskslist.item(item, values=(new_task_info[1], core.time_format(new_task_info[2]), - new_task_info[4])) + self.listframe.taskslist.item(item, values=( + new_task_info[1], core.time_format(new_task_info[2]), + new_task_info[4])) self.update_fulltime() self.raise_window() @@ -739,17 +872,25 @@ class TaskSelectionWindow(Window): FilterWindow(self, variable=filter_changed) # Update tasks list only if filter parameters have been changed: if filter_changed.get() == 1: - self.apply_filter(GLOBAL_OPTIONS["filter_dict"]['operating_mode'], GLOBAL_OPTIONS["filter_dict"]['script'], - GLOBAL_OPTIONS["filter_dict"]['tags'], GLOBAL_OPTIONS["filter_dict"]['dates']) + self.apply_filter(GLOBAL_OPTIONS["filter_dict"]['operating_mode'], + GLOBAL_OPTIONS["filter_dict"]['script'], + GLOBAL_OPTIONS["filter_dict"]['tags'], + GLOBAL_OPTIONS["filter_dict"]['dates']) self.raise_window() - def apply_filter(self, operating_mode='AND', script=None, tags='', dates=''): + def apply_filter(self, operating_mode='AND', script=None, tags='', + dates=''): """Record filter parameters to database and apply it.""" update = self.filter_query() - self.db.update('filter_operating_mode', field='value', value=operating_mode, table='options', updfiled='name') - self.db.update('filter', field='value', value=script, table='options', updfiled='name') - self.db.update('filter_tags', field='value', value=','.join([str(x) for x in tags]), table='options', updfiled='name') - self.db.update('filter_dates', field='value', value=','.join(dates), table='options', updfiled='name') + self.db.update('filter_operating_mode', field='value', + value=operating_mode, table='options', updfiled='name') + self.db.update('filter', field='value', value=script, table='options', + updfiled='name') + self.db.update('filter_tags', field='value', + value=','.join([str(x) for x in tags]), table='options', + updfiled='name') + self.db.update('filter_dates', field='value', value=','.join(dates), + table='options', updfiled='name') if update != self.filter_query(): self.update_list() @@ -760,47 +901,66 @@ class TaskSelectionWindow(Window): class TaskEditWindow(Window): """Task properties window.""" + def __init__(self, taskid, parent=None, variable=None, **options): super().__init__(master=parent, **options) - # Connected with external IntVar. Needed to avoid unnecessary operations in parent window: + # Connected with external IntVar. + # Needed to avoid unnecessary operations in parent window: self.change = variable # Task information from database: self.task = self.db.select_task(taskid) # List of dates connected with this task: - dates = [x[0] + " - " + core.time_format(x[1]) for x in self.db.find_by_clause('activity', 'task_id', '%s' % - taskid, 'date, spent_time', 'date')] - self.title("Task properties: {}".format(self.db.find_by_clause('tasks', 'id', taskid, 'name')[0][0])) + dates = [x[0] + " - " + core.time_format(x[1]) for x in + self.db.find_by_clause('activity', 'task_id', '%s' % + taskid, 'date, spent_time', 'date')] + self.title("Task properties: {}".format( + self.db.find_by_clause('tasks', 'id', taskid, 'name')[0][0])) self.minsize(width=400, height=300) - elements.SimpleLabel(self, text="Task name:", fontsize=10).grid(row=0, column=0, pady=5, padx=5, sticky='w') + elements.SimpleLabel(self, text="Task name:", fontsize=10).grid( + row=0, column=0, pady=5, padx=5, sticky='w') # Frame containing task name: - TaskLabel(self, width=60, height=1, bg=GLOBAL_OPTIONS["colour"], text=self.task[1], + TaskLabel(self, width=60, height=1, bg=GLOBAL_OPTIONS["colour"], + text=self.task[1], anchor='w').grid(row=1, columnspan=5, sticky='ew', padx=6) tk.Frame(self, height=30).grid(row=2) - elements.SimpleLabel(self, text="Description:", fontsize=10).grid(row=3, column=0, pady=5, padx=5, sticky='w') + elements.SimpleLabel(self, text="Description:", fontsize=10).grid( + row=3, column=0, pady=5, padx=5, sticky='w') # Task description frame. Editable: - self.description = Description(self, paste_menu=True, width=60, height=6) + self.description = Description(self, paste_menu=True, width=60, + height=6) self.description.config(state='normal', bg='white') if self.task[3]: self.description.insert(self.task[3]) self.description.grid(row=4, columnspan=5, sticky='ewns', padx=5) # - elements.SimpleLabel(self, text='Tags:').grid(row=5, column=0, pady=5, padx=5, sticky='nw') + elements.SimpleLabel(self, text='Tags:').grid(row=5, column=0, pady=5, + padx=5, sticky='nw') # Place tags list: self.tags_update() - elements.TaskButton(self, text='Edit tags', textwidth=10, command=self.tags_edit).grid(row=5, column=4, padx=5, pady=5, sticky='e') - elements.SimpleLabel(self, text='Time spent:').grid(row=6, column=0, padx=5, pady=5, sticky='w') + elements.TaskButton(self, text='Edit tags', textwidth=10, + command=self.tags_edit).grid(row=5, column=4, + padx=5, pady=5, + sticky='e') + elements.SimpleLabel(self, text='Time spent:').grid(row=6, column=0, + padx=5, pady=5, + sticky='w') # Frame containing time: - TaskLabel(self, width=11, text='{}'.format(core.time_format(self.task[2]))).grid(row=6, column=1, - pady=5, padx=5, sticky='w') - elements.SimpleLabel(self, text='Dates:').grid(row=6, column=2, sticky='w') + TaskLabel(self, width=11, + text='{}'.format(core.time_format(self.task[2]))).grid( + row=6, column=1, pady=5, padx=5, sticky='w') + elements.SimpleLabel(self, text='Dates:').grid(row=6, column=2, + sticky='w') # Frame containing list of dates connected with current task: datlist = Description(self, height=3, width=30) datlist.update_text('\n'.join(dates)) - datlist.grid(row=6, column=3, rowspan=3, columnspan=2, sticky='ew', padx=5, pady=5) + datlist.grid(row=6, column=3, rowspan=3, columnspan=2, sticky='ew', + padx=5, pady=5) # tk.Frame(self, height=40).grid(row=9) - elements.TaskButton(self, text='Ok', command=self.update_task).grid(row=10, column=0, sticky='sw', padx=5, pady=5) - elements.TaskButton(self, text='Cancel', command=self.destroy).grid(row=10, column=4, sticky='se', padx=5, pady=5) + elements.TaskButton(self, text='Ok', command=self.update_task).grid( + row=10, column=0, sticky='sw', padx=5, pady=5) + elements.TaskButton(self, text='Cancel', command=self.destroy).grid( + row=10, column=4, sticky='se', padx=5, pady=5) self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(3, weight=10) self.grid_rowconfigure(4, weight=1) @@ -818,21 +978,27 @@ class TaskEditWindow(Window): def tags_update(self): """Tags list placing.""" # Tags list. Tags state are saved to database: - self.tags = Tagslist(self.db.tags_dict(self.task[0]), self, orientation='horizontal', width=300, height=30) - self.tags.grid(row=5, column=1, columnspan=3, pady=5, padx=5, sticky='we') + self.tags = Tagslist(self.db.tags_dict(self.task[0]), self, + orientation='horizontal', width=300, height=30) + self.tags.grid(row=5, column=1, columnspan=3, pady=5, padx=5, + sticky='we') def update_task(self): """Update task in database.""" taskdata = self.description.get().rstrip() self.db.update_task(self.task[0], field='description', value=taskdata) # Renew tags list for the task: - existing_tags = [x[0] for x in self.db.find_by_clause('tasks_tags', 'task_id', self.task[0], 'tag_id')] + existing_tags = [x[0] for x in + self.db.find_by_clause('tasks_tags', 'task_id', + self.task[0], 'tag_id')] for item in self.tags.states_list: if item[1][0].get() == 1: if item[0] not in existing_tags: - self.db.insert('tasks_tags', ('task_id', 'tag_id'), (self.task[0], item[0])) + self.db.insert('tasks_tags', ('task_id', 'tag_id'), + (self.task[0], item[0])) else: - self.db.delete(table="tasks_tags", task_id=self.task[0], tag_id=item[0]) + self.db.delete(table="tasks_tags", task_id=self.task[0], + tag_id=item[0]) # Reporting to parent window that task has been changed: if self.change: self.change.set(1) @@ -841,13 +1007,16 @@ class TaskEditWindow(Window): class TagsEditWindow(Window): """Checkbuttons editing window..""" + def __init__(self, parent=None, **options): super().__init__(master=parent, **options) self.parent = parent self.addentry() self.tags_update() - self.closebutton = elements.TaskButton(self, text='Close', command=self.destroy) - self.deletebutton = elements.TaskButton(self, text='Delete', command=self.delete) + self.closebutton = elements.TaskButton(self, text='Close', + command=self.destroy) + self.deletebutton = elements.TaskButton(self, text='Delete', + command=self.delete) self.maxsize(width=500, height=500) self.window_elements_config() self.prepare() @@ -863,7 +1032,8 @@ class TagsEditWindow(Window): """New element addition field""" self.addentry_label = elements.SimpleLabel(self, text="Add tag:") self.addentry_label.grid(row=0, column=0, pady=5, padx=5, sticky='w') - elements.TaskButton(self, text='Add', command=self.add).grid(row=0, column=2, pady=5, padx=5, sticky='e') + elements.TaskButton(self, text='Add', command=self.add).grid( + row=0, column=2, pady=5, padx=5, sticky='e') self.addfield = elements.SimpleEntry(self, width=20) self.addfield.grid(row=0, column=1, sticky='ew') self.addfield.focus_set() @@ -896,13 +1066,16 @@ class TagsEditWindow(Window): if item[1][0].get() == 1: dellist.append(item[0]) if dellist: - answer = askyesno("Really delete?", "Are you sure you want to delete selected items?", parent=self) + answer = askyesno("Really delete?", + "Are you sure you want to delete selected items?", + parent=self) if answer: self.del_record(dellist) self.tags_update() def tags_get(self): - self.tags = Tagslist(self.db.simple_tagslist(), self, width=300, height=300) + self.tags = Tagslist(self.db.simple_tagslist(), self, width=300, + height=300) def add_record(self, tagname): self.db.insert('tags', ('id', 'name'), (None, tagname)) @@ -914,6 +1087,7 @@ class TagsEditWindow(Window): class TimestampsWindow(TagsEditWindow): """Window with timestamps for selected task.""" + def __init__(self, taskid, current_task_time, parent=None, **options): self.taskid = taskid self.current_time = current_task_time @@ -929,10 +1103,17 @@ class TimestampsWindow(TagsEditWindow): def window_elements_config(self): """Configure some window parameters.""" - self.title("Timestamps: {}".format(self.db.find_by_clause('tasks', 'id', self.taskid, 'name')[0][0])) + self.title("Timestamps: {}".format( + self.db.find_by_clause('tasks', 'id', self.taskid, 'name')[0][0])) self.minsize(width=400, height=300) - elements.TaskButton(self, text="Select all", command=self.select_all).grid(row=2, column=0, pady=5, padx=5, sticky='w') - elements.TaskButton(self, text="Clear all", command=self.clear_all).grid(row=2, column=2, pady=5, padx=5, sticky='e') + elements.TaskButton(self, text="Select all", + command=self.select_all).grid(row=2, column=0, + pady=5, padx=5, + sticky='w') + elements.TaskButton(self, text="Clear all", + command=self.clear_all).grid(row=2, column=2, + pady=5, padx=5, + sticky='e') tk.Frame(self, height=40).grid(row=3) self.closebutton.grid(row=4, column=2, pady=5, padx=5, sticky='w') self.deletebutton.grid(row=4, column=0, pady=5, padx=5, sticky='e') @@ -943,16 +1124,20 @@ class TimestampsWindow(TagsEditWindow): def tags_get(self): """Creates timestamps list.""" - self.tags = Tagslist(self.db.timestamps(self.taskid, self.current_time), self, width=400, height=300) + self.tags = Tagslist( + self.db.timestamps(self.taskid, self.current_time), self, + width=400, height=300) def del_record(self, dellist): """Deletes selected timestamps.""" for x in dellist: - self.db.delete(table="timestamps", timestamp=x, task_id=self.taskid) + self.db.delete(table="timestamps", timestamp=x, + task_id=self.taskid) class HelpWindow(Window): """Help window.""" + def __init__(self, parent=None, text='', **options): super().__init__(master=parent, **options) self.title("Help") @@ -964,7 +1149,8 @@ class HelpWindow(Window): main_frame.grid(row=0, column=0, sticky='news', padx=5, pady=5) main_frame.grid_rowconfigure(0, weight=1) main_frame.grid_columnconfigure(0, weight=1) - elements.TaskButton(self, text='OK', command=self.destroy).grid(row=1, column=0, sticky='e', pady=5, padx=5) + elements.TaskButton(self, text='OK', command=self.destroy).grid( + row=1, column=0, sticky='e', pady=5, padx=5) self.grid_columnconfigure(0, weight=1) self.grid_rowconfigure(0, weight=1) self.bind("", lambda e: self.destroy()) @@ -972,8 +1158,11 @@ class HelpWindow(Window): class Tagslist(elements.ScrolledCanvas): - """Tags list. Accepts tagslist: [[tag_id, [state, 'tagname']]], can be 0 or 1.""" - def __init__(self, tagslist, parent=None, orientation="vertical", **options): + """Tags list. Accepts tagslist: [[tag_id, [state, 'tagname']]], + can be 0 or 1.""" + + def __init__(self, tagslist, parent=None, orientation="vertical", + **options): super().__init__(parent=parent, orientation=orientation, **options) self.states_list = tagslist for item in self.states_list: @@ -982,24 +1171,34 @@ class Tagslist(elements.ScrolledCanvas): # Inserting dynamic variable instead of the state: item[1][0] = tk.IntVar() # Connecting new checkbox with this dynamic variable: - cb = elements.SimpleCheckbutton(self.content_frame, text=(item[1][1] + ' ' * 3 if orientation == "horizontal" - else item[1][1]), variable=item[1][0]) - cb.pack(side=('left' if orientation == "horizontal" else 'bottom'), anchor='w') + cb = elements.SimpleCheckbutton(self.content_frame, text=( + item[1][1] + ' ' * 3 if orientation == "horizontal" + else item[1][1]), variable=item[1][0]) + cb.pack(side=('left' if orientation == "horizontal" else 'bottom'), + anchor='w') # Setting dynamic variable value to previously saved state: item[1][0].set(state) class FilterWindow(Window): """Filters window.""" + def __init__(self, parent=None, variable=None, **options): super().__init__(master=parent, **options) self.title("Filter") - self.changed = variable # IntVar instance: used to set 1 if some changes were made. For optimization. - self.operating_mode = tk.StringVar() # Operating mode of the filter: "AND", "OR". + # IntVar instance: used to set 1 if some changes were made. + # For optimization. + self.changed = variable + # Operating mode of the filter: "AND", "OR". + self.operating_mode = tk.StringVar() # Lists of stored filter parameters: - stored_dates = self.db.find_by_clause('options', 'name', 'filter_dates', 'value')[0][0].split(',') - stored_tags = self.db.find_by_clause('options', 'name', 'filter_tags', 'value')[0][0].split(',') - if stored_tags[0]: # stored_tags[0] is string. + stored_dates = \ + self.db.find_by_clause('options', 'name', 'filter_dates', 'value')[0][ + 0].split(',') + stored_tags = \ + self.db.find_by_clause('options', 'name', 'filter_tags', 'value')[0][ + 0].split(',') + if stored_tags[0]: # stored_tags[0] is string. stored_tags = list(map(int, stored_tags)) # Dates list: dates = self.db.simple_dateslist() @@ -1009,26 +1208,48 @@ class FilterWindow(Window): for tag in tags: if tag[0] in stored_tags: tag[1][0] = 1 - elements.SimpleLabel(self, text="Dates").grid(row=0, column=0, sticky='n') - elements.SimpleLabel(self, text="Tags").grid(row=0, column=1, sticky='n') - self.dateslist = Tagslist([[x, [1 if x in stored_dates else 0, x]] for x in dates], self, width=200, height=300) + elements.SimpleLabel(self, text="Dates").grid(row=0, column=0, + sticky='n') + elements.SimpleLabel(self, text="Tags").grid(row=0, column=1, + sticky='n') + self.dateslist = Tagslist( + [[x, [1 if x in stored_dates else 0, x]] for x in dates], self, + width=200, height=300) self.tagslist = Tagslist(tags, self, width=200, height=300) self.dateslist.grid(row=1, column=0, pady=5, padx=5, sticky='news') self.tagslist.grid(row=1, column=1, pady=5, padx=5, sticky='news') - elements.TaskButton(self, text="Select dates...", textwidth=15, command=self.select_dates).grid(row=2, column=0, pady=7, padx=5, sticky='n') - elements.TaskButton(self, text="Clear", command=self.clear_tags).grid(row=2, column=1, pady=7, padx=5, sticky='n') - elements.TaskButton(self, text="Clear", command=self.clear_dates).grid(row=3, column=0, pady=7, padx=5, sticky='n') - tk.Frame(self, height=20).grid(row=5, column=0, columnspan=2, sticky='news') - elements.SimpleLabel(self, text="Filter operating mode:").grid(row=5, columnspan=2, pady=5) + elements.TaskButton(self, text="Select dates...", textwidth=15, + command=self.select_dates).grid(row=2, column=0, + pady=7, padx=5, + sticky='n') + elements.TaskButton(self, text="Clear", command=self.clear_tags).grid( + row=2, column=1, pady=7, padx=5, sticky='n') + elements.TaskButton(self, text="Clear", command=self.clear_dates).grid( + row=3, column=0, pady=7, padx=5, sticky='n') + tk.Frame(self, height=20).grid(row=5, column=0, columnspan=2, + sticky='news') + elements.SimpleLabel(self, text="Filter operating mode:").grid( + row=5, columnspan=2, pady=5) checkframe = tk.Frame(self) checkframe.grid(row=7, columnspan=2, pady=5) - elements.SimpleRadiobutton(checkframe, text="AND", variable=self.operating_mode, value="AND").grid(row=0, column=0, sticky='e') - elements.SimpleRadiobutton(checkframe, text="OR", variable=self.operating_mode, value="OR").grid(row=0, column=1, sticky='w') - self.operating_mode.set(self.db.find_by_clause(table="options", field="name", - value="filter_operating_mode", searchfield="value")[0][0]) - tk.Frame(self, height=20).grid(row=8, column=0, columnspan=2, sticky='news') - elements.TaskButton(self, text="Cancel", command=self.destroy).grid(row=9, column=1, pady=5, padx=5, sticky='e') - elements.TaskButton(self, text='Ok', command=self.apply_filter).grid(row=9, column=0, pady=5, padx=5, sticky='w') + elements.SimpleRadiobutton(checkframe, text="AND", + variable=self.operating_mode, + value="AND").grid(row=0, column=0, + sticky='e') + elements.SimpleRadiobutton(checkframe, text="OR", + variable=self.operating_mode, + value="OR").grid(row=0, column=1, + sticky='w') + self.operating_mode.set( + self.db.find_by_clause(table="options", field="name", + value="filter_operating_mode", + searchfield="value")[0][0]) + tk.Frame(self, height=20).grid(row=8, column=0, columnspan=2, + sticky='news') + elements.TaskButton(self, text="Cancel", command=self.destroy).grid( + row=9, column=1, pady=5, padx=5, sticky='e') + elements.TaskButton(self, text='Ok', command=self.apply_filter).grid( + row=9, column=0, pady=5, padx=5, sticky='w') self.bind("", lambda e: self.apply_filter()) self.minsize(height=350, width=350) self.maxsize(width=750, height=600) @@ -1056,49 +1277,80 @@ class FilterWindow(Window): if correct.get(): for date in self.dateslist.states_list: date[1][0].set(0) - if core.str_to_date(start_date.get()) <= core.str_to_date(date[0]) <= core.str_to_date(end_date.get()): + if core.str_to_date(start_date.get()) <= core.str_to_date( + date[0]) <= core.str_to_date(end_date.get()): date[1][0].set(1) def apply_filter(self): """Create database script based on checkboxes values.""" - dates = list(reversed([x[0] for x in self.dateslist.states_list if x[1][0].get() == 1])) - tags = list(reversed([x[0] for x in self.tagslist.states_list if x[1][0].get() == 1])) + dates = list(reversed( + [x[0] for x in self.dateslist.states_list if x[1][0].get() == 1])) + tags = list(reversed( + [x[0] for x in self.tagslist.states_list if x[1][0].get() == 1])) if not dates and not tags: script = None self.operating_mode.set("AND") else: if self.operating_mode.get() == "OR": - script = 'SELECT id, name, total_time, description, creation_date FROM tasks JOIN activity ON '\ - 'activity.task_id=tasks.id JOIN tasks_tags ON tasks_tags.task_id=tasks.id '\ - 'JOIN (SELECT task_id, sum(spent_time) AS total_time FROM activity GROUP BY task_id) AS act ' \ - 'ON act.task_id=tasks.id WHERE date IN {1} OR tag_id IN {0} ' \ - 'GROUP BY act.task_id'.format("('%s')" % tags[0] if len(tags) == 1 else tuple(tags), - "('%s')" % dates[0] if len(dates) == 1 else tuple(dates)) + script = 'SELECT id, name, total_time, description, ' \ + 'creation_date FROM tasks JOIN activity ' \ + 'ON activity.task_id=tasks.id JOIN tasks_tags ' \ + 'ON tasks_tags.task_id=tasks.id ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time ' \ + 'FROM activity GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id WHERE date IN {1} ' \ + 'OR tag_id IN {0} ' \ + 'GROUP BY act.task_id'.format( + "('%s')" % tags[0] if len(tags) == 1 else tuple(tags), + "('%s')" % dates[0] if len(dates) == 1 else tuple(dates)) else: if dates and tags: - script = 'SELECT DISTINCT id, name, total_time, description, creation_date FROM tasks JOIN (SELECT ' \ - 'task_id, sum(spent_time) AS total_time FROM activity WHERE activity.date IN {0} GROUP BY ' \ - 'task_id) AS act ON act.task_id=tasks.id JOIN (SELECT tt.task_id FROM tasks_tags AS tt WHERE '\ - 'tt.tag_id IN {1} GROUP BY tt.task_id HAVING COUNT(DISTINCT tt.tag_id)={3}) AS x ON ' \ - 'x.task_id=tasks.id JOIN (SELECT act.task_id FROM activity AS act WHERE act.date IN {0} ' \ - 'GROUP BY act.task_id HAVING COUNT(DISTINCT act.date)={2}) AS y ON ' \ - 'y.task_id=tasks.id'.format("('%s')" % dates[0] if len(dates) == 1 else tuple(dates), - "('%s')" % tags[0] if len(tags) == 1 else tuple(tags), - len(dates), len(tags)) + script = 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks JOIN ' \ + '(SELECT task_id, sum(spent_time) AS total_time '\ + 'FROM activity WHERE activity.date IN {0} ' \ + 'GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id JOIN (SELECT tt.task_id'\ + ' FROM tasks_tags AS tt WHERE ' \ + 'tt.tag_id IN {1} GROUP BY tt.task_id ' \ + 'HAVING COUNT(DISTINCT tt.tag_id)={3}) AS x ON ' \ + 'x.task_id=tasks.id JOIN (SELECT act.task_id ' \ + 'FROM activity AS act WHERE act.date IN {0} ' \ + 'GROUP BY act.task_id HAVING ' \ + 'COUNT(DISTINCT act.date)={2}) AS y ON ' \ + 'y.task_id=tasks.id'.format( + "('%s')" % dates[0] if len(dates) == 1 else tuple( + dates), + "('%s')" % tags[0] if len(tags) == 1 else tuple(tags), + len(dates), len(tags)) elif not dates: - script = 'SELECT DISTINCT id, name, total_time, description, creation_date FROM tasks JOIN (SELECT ' \ - 'task_id, sum(spent_time) AS total_time FROM activity GROUP BY ' \ - 'task_id) AS act ON act.task_id=tasks.id JOIN (SELECT tt.task_id FROM tasks_tags AS tt WHERE ' \ - 'tt.tag_id IN {0} GROUP BY tt.task_id HAVING COUNT(DISTINCT tt.tag_id)={1}) AS x ON ' \ - 'x.task_id=tasks.id'.format(tuple(tags) if len(tags) > 1 else "(%s)" % tags[0], len(tags)) + script = 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time FROM activity GROUP BY ' \ + 'task_id) AS act ON act.task_id=tasks.id ' \ + 'JOIN (SELECT tt.task_id FROM tasks_tags ' \ + 'AS tt WHERE tt.tag_id IN {0} GROUP BY ' \ + 'tt.task_id HAVING ' \ + 'COUNT(DISTINCT tt.tag_id)={1}) AS x ON ' \ + 'x.task_id=tasks.id'.format( + tuple(tags) if len(tags) > 1 else "(%s)" % tags[0], + len(tags)) elif not tags: - script = 'SELECT DISTINCT id, name, total_time, description, creation_date FROM tasks JOIN (SELECT ' \ - 'task_id, sum(spent_time) AS total_time FROM activity WHERE activity.date IN {0} GROUP BY ' \ - 'task_id) AS act ON act.task_id=tasks.id JOIN (SELECT act.task_id FROM activity AS act ' \ - 'WHERE act.date IN {0} ' \ - 'GROUP BY act.task_id HAVING COUNT(DISTINCT act.date)={1}) AS y ON ' \ - 'y.task_id=tasks.id'.format(tuple(dates) if len(dates) > 1 else "('%s')" % dates[0], - len(dates)) + script = 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time FROM activity WHERE activity.date'\ + ' IN {0} GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id JOIN (SELECT ' \ + 'act.task_id FROM activity AS act ' \ + 'WHERE act.date IN {0} GROUP BY act.task_id ' \ + 'HAVING COUNT(DISTINCT act.date)={1}) AS y ' \ + 'ON y.task_id=tasks.id'.format( + tuple(dates) if len(dates) > 1 else "('%s')" % dates[ + 0], + len(dates)) GLOBAL_OPTIONS["filter_dict"] = { 'operating_mode': self.operating_mode.get(), 'script': script, @@ -1112,25 +1364,38 @@ class FilterWindow(Window): class CalendarWindow(Window): - def __init__(self, parent=None, correct_data=None, startvar=None, endvar=None, startdate=None, enddate=None, **options): + def __init__(self, parent=None, correct_data=None, startvar=None, + endvar=None, startdate=None, enddate=None, **options): super().__init__(master=parent, **options) self.title("Select dates") self.correct_data = correct_data self.start = startvar self.end = endvar - self.start_date_entry = sel_cal.Datepicker(self, datevar=self.start, - current_month=core.str_to_date(startdate).month, - current_year=core.str_to_date(startdate).year) - self.end_date_entry = sel_cal.Datepicker(self, datevar=self.end, - current_month=core.str_to_date(enddate).month, - current_year=core.str_to_date(enddate).year) - elements.SimpleLabel(self, text="Enter first date:").grid(row=0, column=0, pady=3, padx=5, sticky='w') + self.start_date_entry = sel_cal.Datepicker( + self, datevar=self.start, + current_month=core.str_to_date(startdate).month, + current_year=core.str_to_date(startdate).year) + self.end_date_entry = sel_cal.Datepicker( + self, datevar=self.end, + current_month=core.str_to_date(enddate).month, + current_year=core.str_to_date(enddate).year) + elements.SimpleLabel(self, text="Enter first date:").grid(row=0, + column=0, + pady=3, + padx=5, + sticky='w') self.start_date_entry.grid(row=1, column=0, padx=5, pady=3, sticky='w') - elements.SimpleLabel(self, text="Enter last date:").grid(row=2, column=0, pady=5, padx=5, sticky='w') + elements.SimpleLabel(self, text="Enter last date:").grid(row=2, + column=0, + pady=5, + padx=5, + sticky='w') self.end_date_entry.grid(row=3, column=0, padx=5, pady=3, sticky='w') tk.Frame(self, height=15, width=10).grid(row=4, column=0, columnspan=2) - elements.TaskButton(self, text='OK', command=self.close).grid(row=5, column=0, padx=5, pady=5, sticky='w') - elements.TaskButton(self, text='Cancel', command=self.destroy).grid(row=5, column=1, padx=5, pady=5, sticky='e') + elements.TaskButton(self, text='OK', command=self.close).grid( + row=5, column=0, padx=5, pady=5, sticky='w') + elements.TaskButton(self, text='Cancel', command=self.destroy).grid( + row=5, column=1, padx=5, pady=5, sticky='e') self.bind("", lambda e: self.close()) self.minsize(height=350, width=450) self.maxsize(width=600, height=500) @@ -1157,7 +1422,9 @@ class CalendarWindow(Window): class RightclickMenu(tk.Menu): """Popup menu. By default has one menuitem - "copy".""" - def __init__(self, parent=None, copy_item=True, paste_item=False, **options): + + def __init__(self, parent=None, copy_item=True, paste_item=False, + **options): super().__init__(master=parent, tearoff=0, **options) if copy_item: self.add_command(label="Copy", command=copy_to_clipboard) @@ -1165,13 +1432,15 @@ class RightclickMenu(tk.Menu): self.add_command(label="Paste", command=paste_from_clipboard) def context_menu_show(self, event): - """Function links context menu with current selected widget and pops menu up.""" + """Function links context menu with current selected widget + and pops menu up.""" self.tk_popup(event.x_root, event.y_root) GLOBAL_OPTIONS["selected_widget"] = event.widget class MainFrame(elements.ScrolledCanvas): """Container for all task frames.""" + def __init__(self, parent): super().__init__(parent=parent, bd=2) self.frames_count = 0 @@ -1181,7 +1450,10 @@ class MainFrame(elements.ScrolledCanvas): def clear(self): """Clear all task frames except with opened tasks.""" for w in self.content_frame.winfo_children(): - if self.frames_count == int(GLOBAL_OPTIONS['timers_count']) or self.frames_count == len(GLOBAL_OPTIONS["tasks"]): + if self.frames_count == int(GLOBAL_OPTIONS[ + 'timers_count']) \ + or self.frames_count == len( + GLOBAL_OPTIONS["tasks"]): break if hasattr(w, 'task'): if w.task is None: @@ -1190,7 +1462,8 @@ class MainFrame(elements.ScrolledCanvas): def clear_all(self): """Clear all task frames.""" - answer = askyesno("Really clear?", "Are you sure you want to close all task frames?") + answer = askyesno("Really clear?", + "Are you sure you want to close all task frames?") if answer: for w in self.content_frame.winfo_children(): self.frames_count -= 1 @@ -1211,10 +1484,12 @@ class MainFrame(elements.ScrolledCanvas): def fill(self): """Create contents of the main frame.""" if self.frames_count < int(GLOBAL_OPTIONS['timers_count']): - row_count = range(int(GLOBAL_OPTIONS['timers_count']) - self.frames_count) + row_count = range( + int(GLOBAL_OPTIONS['timers_count']) - self.frames_count) for row_number in row_count: task = TaskFrame(parent=self.content_frame) - task.grid(row=self.rows_counter, pady=5, padx=5, ipady=3, sticky='ew') + task.grid(row=self.rows_counter, pady=5, padx=5, ipady=3, + sticky='ew') if GLOBAL_OPTIONS["preserved_tasks_list"]: task_id = GLOBAL_OPTIONS["preserved_tasks_list"].pop(0) task.get_restored_task_name(task_id) @@ -1222,7 +1497,8 @@ class MainFrame(elements.ScrolledCanvas): self.frames_count += len(row_count) self.content_frame.update() self.canvbox.config(width=self.content_frame.winfo_width()) - elif len(GLOBAL_OPTIONS["tasks"]) < self.frames_count > int(GLOBAL_OPTIONS['timers_count']): + elif len(GLOBAL_OPTIONS["tasks"]) < self.frames_count > int( + GLOBAL_OPTIONS['timers_count']): self.clear() self.content_frame.config(bg='#cfcfcf') @@ -1240,16 +1516,20 @@ class MainFrame(elements.ScrolledCanvas): class MainMenu(tk.Menu): """Main window menu.""" + def __init__(self, parent=None, **options): super().__init__(master=parent, **options) file = tk.Menu(self, tearoff=0) - file.add_command(label="Options...", command=self.options_window, underline=0) + file.add_command(label="Options...", command=self.options_window, + underline=0) file.add_separator() file.add_command(label="Exit", command=self.exit, underline=1) elements.big_font(file, 10) self.add_cascade(label="Main menu", menu=file, underline=0) helpmenu = tk.Menu(self, tearoff=0) - helpmenu.add_command(label="Help...", command=lambda: helpwindow(parent=run, text=core.HELP_TEXT)) + helpmenu.add_command(label="Help...", + command=lambda: helpwindow(parent=run, + text=core.HELP_TEXT)) helpmenu.add_command(label="About...", command=self.aboutwindow) elements.big_font(helpmenu, 10) self.add_cascade(label="Help", menu=helpmenu) @@ -1272,7 +1552,8 @@ class MainMenu(tk.Menu): toggler = tk.IntVar(value=toggle) params = {} accept = tk.BooleanVar() - Options(run, accept, var, ontop, compact_iface, save, show_today, toggler) + Options(run, accept, var, ontop, compact_iface, save, show_today, + toggler) if accept.get(): try: count = var.get() @@ -1306,7 +1587,8 @@ class MainMenu(tk.Menu): run.taskframes.fill() run.taskframes.frames_refill() # Stop all tasks if exclusive run method has been enabled: - if int(GLOBAL_OPTIONS["toggle_tasks"]) and int(GLOBAL_OPTIONS["toggle_tasks"]) != toggle: + if int(GLOBAL_OPTIONS["toggle_tasks"]) and int( + GLOBAL_OPTIONS["toggle_tasks"]) != toggle: GLOBAL_OPTIONS["stopall"] = True run.lift() @@ -1321,8 +1603,11 @@ class MainMenu(tk.Menu): db.con.close() def aboutwindow(self): - showinfo("About Tasker", "Tasker {0}.\nCopyright (c) Alexey Kallistov, {1}".format( - GLOBAL_OPTIONS['version'], datetime.datetime.strftime(datetime.datetime.now(), "%Y"))) + showinfo("About Tasker", + "Tasker {0}.\nCopyright (c) Alexey Kallistov, {1}".format( + GLOBAL_OPTIONS['version'], + datetime.datetime.strftime(datetime.datetime.now(), + "%Y"))) def exit(self): run.destroy() @@ -1330,33 +1615,71 @@ class MainMenu(tk.Menu): class Options(Window): """Options window which can be opened from main menu.""" - def __init__(self, parent, is_applied, counter, on_top, compact, preserve, show_today, toggler, **options): + + def __init__(self, parent, is_applied, counter, on_top, compact, preserve, + show_today, toggler, **options): super().__init__(master=parent, width=300, height=200, **options) self.is_applied = is_applied self.title("Options") self.resizable(height=0, width=0) self.counter = counter - elements.SimpleLabel(self, text="Task frames in main window: ").grid(row=0, column=0, sticky='w') + elements.SimpleLabel(self, text="Task frames in main window: ").grid( + row=0, column=0, sticky='w') counterframe = tk.Frame(self) fontsize = 9 - elements.CanvasButton(counterframe, text='<', command=self.decrease, fontsize=fontsize, height=fontsize * 3).grid(row=0, column=0) - elements.SimpleEntry(counterframe, width=3, textvariable=counter, justify='center').grid(row=0, column=1, sticky='e') - elements.CanvasButton(counterframe, text='>', command=self.increase, fontsize=fontsize, height=fontsize * 3).grid(row=0, column=2) + elements.CanvasButton(counterframe, text='<', command=self.decrease, + fontsize=fontsize, height=fontsize * 3).grid( + row=0, column=0) + elements.SimpleEntry(counterframe, width=3, textvariable=counter, + justify='center').grid(row=0, column=1, + sticky='e') + elements.CanvasButton(counterframe, text='>', command=self.increase, + fontsize=fontsize, height=fontsize * 3).grid( + row=0, column=2) counterframe.grid(row=0, column=1) tk.Frame(self, height=20).grid(row=1) - elements.SimpleLabel(self, text="Always on top: ").grid(row=2, column=0, sticky='w', padx=5) - elements.SimpleCheckbutton(self, variable=on_top).grid(row=2, column=1, sticky='w', padx=5) - elements.SimpleLabel(self, text="Compact interface: ").grid(row=3, column=0, sticky='w', padx=5) - elements.SimpleCheckbutton(self, variable=compact).grid(row=3, column=1, sticky='w', padx=5) - elements.SimpleLabel(self, text="Save tasks on exit: ").grid(row=4, column=0, sticky='w', padx=5) - elements.SimpleCheckbutton(self, variable=preserve).grid(row=4, column=1, sticky='w', padx=5) - elements.SimpleLabel(self, text="Show time for current day only in timer's window: ").grid(row=5, column=0, sticky='w', padx=5) - elements.SimpleCheckbutton(self, variable=show_today).grid(row=5, column=1, sticky='w', padx=5) - elements.SimpleLabel(self, text="Allow to run only one task at a time: ").grid(row=6, column=0, sticky='w', padx=5) - elements.SimpleCheckbutton(self, variable=toggler).grid(row=6, column=1, sticky='w', padx=5) + elements.SimpleLabel(self, text="Always on top: ").grid(row=2, + column=0, + sticky='w', + padx=5) + elements.SimpleCheckbutton(self, variable=on_top).grid(row=2, column=1, + sticky='w', + padx=5) + elements.SimpleLabel(self, text="Compact interface: ").grid(row=3, + column=0, + sticky='w', + padx=5) + elements.SimpleCheckbutton(self, variable=compact).grid(row=3, + column=1, + sticky='w', + padx=5) + elements.SimpleLabel(self, text="Save tasks on exit: ").grid( + row=4, column=0, sticky='w', padx=5) + elements.SimpleCheckbutton(self, variable=preserve).grid(row=4, + column=1, + sticky='w', + padx=5) + elements.SimpleLabel(self, + text="Show time for current day only " + "in timer's window: ").grid( + row=5, column=0, sticky='w', padx=5) + elements.SimpleCheckbutton(self, variable=show_today).grid(row=5, + column=1, + sticky='w', + padx=5) + elements.SimpleLabel(self, + text="Allow to run only " + "one task at a time: ").grid( + row=6, column=0, sticky='w', padx=5) + elements.SimpleCheckbutton(self, variable=toggler).grid(row=6, + column=1, + sticky='w', + padx=5) tk.Frame(self, height=20).grid(row=7) - elements.TaskButton(self, text='OK', command=self.apply).grid(row=8, column=0, sticky='w', padx=5, pady=5) - elements.TaskButton(self, text='Cancel', command=self.destroy).grid(row=8, column=1, sticky='e', padx=5, pady=5) + elements.TaskButton(self, text='OK', command=self.apply).grid( + row=8, column=0, sticky='w', padx=5, pady=5) + elements.TaskButton(self, text='Cancel', command=self.destroy).grid( + row=8, column=1, sticky='e', padx=5, pady=5) self.bind("", lambda e: self.apply()) self.prepare() @@ -1375,17 +1698,25 @@ class Options(Window): class ExportWindow(Window): """Export dialogue window.""" + def __init__(self, parent, data, **options): super().__init__(master=parent, **options) self.title("Export parameters") self.task_ids = [x[0] for x in data.values()] self.operating_mode = tk.IntVar(self) - elements.SimpleLabel(self, text="Export mode", fontsize=10).grid(row=0, column=0, columnspan=2, sticky='ns', pady=5) - elements.SimpleRadiobutton(self, text="Task-based", variable=self.operating_mode, value=0).grid(row=1, column=0) - elements.SimpleRadiobutton(self, text="Date-based", variable=self.operating_mode, value=1).grid(row=1, column=1) + elements.SimpleLabel(self, text="Export mode", fontsize=10).grid( + row=0, column=0, columnspan=2, sticky='ns', pady=5) + elements.SimpleRadiobutton(self, text="Task-based", + variable=self.operating_mode, value=0).grid( + row=1, column=0) + elements.SimpleRadiobutton(self, text="Date-based", + variable=self.operating_mode, value=1).grid( + row=1, column=1) tk.Frame(self, height=15).grid(row=2, column=0) - elements.TaskButton(self, text="Export", command=self.get_data).grid(row=3, column=0, padx=5, pady=5, sticky='ws') - elements.TaskButton(self, text="Cancel", command=self.destroy).grid(row=3, column=1, padx=5, pady=5, sticky='es') + elements.TaskButton(self, text="Export", command=self.get_data).grid( + row=3, column=0, padx=5, pady=5, sticky='ws') + elements.TaskButton(self, text="Cancel", command=self.destroy).grid( + row=3, column=1, padx=5, pady=5, sticky='es') self.minsize(height=150, width=250) self.maxsize(width=450, height=300) self.grid_columnconfigure('all', weight=1) @@ -1393,36 +1724,49 @@ class ExportWindow(Window): self.prepare() def get_data(self): - """Take from the database information to be exported and prepare it. All items should be strings.""" + """Take from the database information to be exported and prepare it. + All items should be strings.""" if self.operating_mode.get() == 0: export_data = self.db.tasks_to_export(self.task_ids) - prepared_data = ['Task,Description,Dates,Time,Summarized working time'] - # Don't try to understand this 'for' loop below if you want to save your mind! + prepared_data = [ + 'Task,Description,Dates,Time,Summarized working time'] + # Don't try to understand this 'for' loop below + # if you want to save your mind! for key in export_data: - temp_list = [key, export_data[key][0], export_data[key][1][0][0], export_data[key][1][0][1], + temp_list = [key, export_data[key][0], + export_data[key][1][0][0], + export_data[key][1][0][1], export_data[key][2]] prepared_data.append(','.join(temp_list)) if len(export_data[key][1]) > 1: for i in range(1, len(export_data[key][1])): - prepared_data.append(','.join(['', '', export_data[key][1][i][0], export_data[key][1][i][1], ''])) + prepared_data.append(','.join( + ['', '', export_data[key][1][i][0], + export_data[key][1][i][1], ''])) i += 1 else: export_data = self.db.dates_to_export(self.task_ids) - prepared_data = ['Date,Tasks,Descriptions,Time,Summarized working time'] + prepared_data = [ + 'Date,Tasks,Descriptions,Time,Summarized working time'] for key in export_data: - temp_list = [key, export_data[key][0][0][0], export_data[key][0][0][1], export_data[key][0][0][2], + temp_list = [key, export_data[key][0][0][0], + export_data[key][0][0][1], + export_data[key][0][0][2], export_data[key][1]] prepared_data.append(','.join(temp_list)) if len(export_data[key][0]) > 1: for i in range(1, len(export_data[key][0])): - prepared_data.append(','.join(['', export_data[key][0][i][0], export_data[key][0][i][1], - export_data[key][0][i][2], ''])) + prepared_data.append(','.join( + ['', export_data[key][0][i][0], + export_data[key][0][i][1], + export_data[key][0][i][2], ''])) i += 1 self.export('\n'.join(prepared_data)) def export(self, data): filename = asksaveasfilename(parent=self, defaultextension=".csv", - filetypes=[("All files", "*.*"), ("Comma-separated texts", "*.csv")]) + filetypes=[("All files", "*.*"), ( + "Comma-separated texts", "*.csv")]) if filename: core.write_to_disk(filename, data) self.destroy() @@ -1444,7 +1788,8 @@ class MainWindow(tk.Tk): if GLOBAL_OPTIONS["compact_interface"] == "0": self.full_interface(True) self.grid_rowconfigure(0, weight=1) - # Make main window always appear in good position and with adequate size: + # Make main window always appear in good position + # and with adequate size: self.update() if self.winfo_height() < self.winfo_screenheight() - 250: window_height = self.winfo_height() @@ -1461,28 +1806,43 @@ class MainWindow(tk.Tk): self.stopall() elif event.keysym in ('Cyrillic_es', 'Cyrillic_ES', 'c', 'C'): self.taskframes.clear_all() - elif event.keysym in ('Cyrillic_shorti', 'Cyrillic_SHORTI', 'q', 'Q', 'Escape'): + elif event.keysym in ( + 'Cyrillic_shorti', 'Cyrillic_SHORTI', 'q', 'Q', 'Escape'): self.destroy() def full_interface(self, firstrun=False): """Create elements which are displayed in full interface mode.""" self.add_frame = tk.Frame(self, height=35) self.add_frame.grid(row=1, columnspan=5) - self.add_stop_button = elements.TaskButton(self, text="Stop all", command=self.stopall) + self.add_stop_button = elements.TaskButton(self, text="Stop all", + command=self.stopall) self.add_stop_button.grid(row=2, column=2, sticky='sn', pady=5, padx=5) - self.add_clear_button = elements.TaskButton(self, text="Clear all", command=self.taskframes.clear_all) - self.add_clear_button.grid(row=2, column=0, sticky='wsn', pady=5, padx=5) - self.add_quit_button = elements.TaskButton(self, text="Quit", command=self.destroy) - self.add_quit_button.grid(row=2, column=4, sticky='sne', pady=5, padx=5) + self.add_clear_button = elements.TaskButton( + self, text="Clear all", + command=self.taskframes.clear_all) + self.add_clear_button.grid(row=2, column=0, sticky='wsn', pady=5, + padx=5) + self.add_pause_button = elements.TaskButton(self, text="Pause all", + command=self.pause_all) + self.add_pause_button.grid(row=2, column=3, sticky='snw', pady=5, + padx=5) + self.add_quit_button = elements.TaskButton(self, text="Quit", + command=self.destroy) + self.add_quit_button.grid(row=2, column=4, sticky='sne', pady=5, + padx=5) if not firstrun: self.taskframes.change_interface('normal') def small_interface(self): """Destroy all additional interface elements.""" - for widget in self.add_frame, self.add_stop_button, self.add_clear_button, self.add_quit_button: + for widget in self.add_frame, self.add_stop_button, \ + self.add_clear_button, self.add_quit_button: widget.destroy() self.taskframes.change_interface('small') + def pause_all(self): + pass + def stopall(self): """Stop all running timers.""" GLOBAL_OPTIONS["stopall"] = True @@ -1493,8 +1853,10 @@ class MainWindow(tk.Tk): db = core.Db() if GLOBAL_OPTIONS["preserve_tasks"] == "1": tasks = ','.join([str(x) for x in GLOBAL_OPTIONS["tasks"]]) - if int(GLOBAL_OPTIONS['timers_count']) < len(GLOBAL_OPTIONS["tasks"]): - db.update(table='options', field='value', value=len(GLOBAL_OPTIONS["tasks"]), + if int(GLOBAL_OPTIONS['timers_count']) < len( + GLOBAL_OPTIONS["tasks"]): + db.update(table='options', field='value', + value=len(GLOBAL_OPTIONS["tasks"]), field_id='timers_count', updfiled='name') else: tasks = '' @@ -1514,19 +1876,24 @@ def copy_to_clipboard(): GLOBAL_OPTIONS["selected_widget"].clipboard_clear() if isinstance(GLOBAL_OPTIONS["selected_widget"], tk.Text): try: - GLOBAL_OPTIONS["selected_widget"].clipboard_append(GLOBAL_OPTIONS["selected_widget"].selection_get()) + GLOBAL_OPTIONS["selected_widget"].clipboard_append( + GLOBAL_OPTIONS["selected_widget"].selection_get()) except tk.TclError: - GLOBAL_OPTIONS["selected_widget"].clipboard_append(GLOBAL_OPTIONS["selected_widget"].get(1.0, 'end')) + GLOBAL_OPTIONS["selected_widget"].clipboard_append( + GLOBAL_OPTIONS["selected_widget"].get(1.0, 'end')) else: - GLOBAL_OPTIONS["selected_widget"].clipboard_append(GLOBAL_OPTIONS["selected_widget"].cget("text")) + GLOBAL_OPTIONS["selected_widget"].clipboard_append( + GLOBAL_OPTIONS["selected_widget"].cget("text")) def paste_from_clipboard(): """Paste text from clipboard.""" if isinstance(GLOBAL_OPTIONS["selected_widget"], tk.Text): - GLOBAL_OPTIONS["selected_widget"].insert(tk.INSERT, GLOBAL_OPTIONS["selected_widget"].clipboard_get()) + GLOBAL_OPTIONS["selected_widget"].insert(tk.INSERT, GLOBAL_OPTIONS[ + "selected_widget"].clipboard_get()) elif isinstance(GLOBAL_OPTIONS["selected_widget"], tk.Entry): - GLOBAL_OPTIONS["selected_widget"].insert(0, GLOBAL_OPTIONS["selected_widget"].clipboard_get()) + GLOBAL_OPTIONS["selected_widget"].insert(0, GLOBAL_OPTIONS[ + "selected_widget"].clipboard_get()) def get_options(): @@ -1539,14 +1906,15 @@ if __name__ == "__main__": # Maximum number of task frames: MAX_TASKS = 10 # Interval between saving time to database: - SAVE_INTERVAL = 10000 # ms + SAVE_INTERVAL = 10000 # ms # Check if tasks database actually exists: core.check_database() # Create options dictionary: GLOBAL_OPTIONS = get_options() # Global tasks ids set. Used for preserve duplicates: if GLOBAL_OPTIONS["tasks"]: - GLOBAL_OPTIONS["tasks"] = dict.fromkeys([int(x) for x in GLOBAL_OPTIONS["tasks"].split(",")], False) + GLOBAL_OPTIONS["tasks"] = dict.fromkeys( + [int(x) for x in GLOBAL_OPTIONS["tasks"].split(",")], False) else: GLOBAL_OPTIONS["tasks"] = dict() # List of preserved tasks which are not open: @@ -1555,7 +1923,8 @@ if __name__ == "__main__": GLOBAL_OPTIONS["stopall"] = False # Widget which is currently connected to context menu: GLOBAL_OPTIONS["selected_widget"] = None - GLOBAL_OPTIONS.update({"MAX_TASKS": MAX_TASKS, "SAVE_INTERVAL": SAVE_INTERVAL}) + GLOBAL_OPTIONS.update( + {"MAX_TASKS": MAX_TASKS, "SAVE_INTERVAL": SAVE_INTERVAL}) # Main window: run = MainWindow() From b17f50bd442456ce2c96618a98d54f76a4415207 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Thu, 14 Feb 2019 21:14:55 +0300 Subject: [PATCH 02/55] Pause all button. --- elements.py | 6 +++--- tasker.pyw | 43 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/elements.py b/elements.py index 4eba25b..e278d64 100644 --- a/elements.py +++ b/elements.py @@ -166,10 +166,10 @@ def add_text(self, textorvariable, fontsize=None, bg=None, opacity="right", self.bg = bg recreate = False if hasattr(self, 'textlabel'): - coords = self.coords( - 'text') # New text will appear in the same position as previous. + # New text will appear in the same position as previous: + coords = self.coords('text') recreate = True - self.delete(self.textlabel) + self.delete('text') if isinstance(textorvariable, tk.Variable): self.textlabel = tk.Label(self, textvariable=textorvariable, bd=0, bg=self.bg, font=font, justify='center', diff --git a/tasker.pyw b/tasker.pyw index fe297ff..1d6801d 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -347,9 +347,7 @@ class TaskFrame(tk.Frame): self.timer_window.config(text=core.time_format( self.running_time if self.running_time < 86400 else self.running_today_time)) - # Checking if "Stop all" button is pressed: - if not GLOBAL_OPTIONS["stopall"] and GLOBAL_OPTIONS["tasks"][ - self.task_id]: + if GLOBAL_OPTIONS["tasks"][self.task_id]: # Every n seconds counter value is saved in database: if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: self.check_date() @@ -1445,6 +1443,8 @@ class MainFrame(elements.ScrolledCanvas): super().__init__(parent=parent, bd=2) self.frames_count = 0 self.rows_counter = 0 + self.frames = [] + self.active_frames = [] self.fill() def clear(self): @@ -1468,6 +1468,7 @@ class MainFrame(elements.ScrolledCanvas): for w in self.content_frame.winfo_children(): self.frames_count -= 1 w.destroy() + self.active_frames.clear() self.fill() def frames_refill(self): @@ -1493,6 +1494,7 @@ class MainFrame(elements.ScrolledCanvas): if GLOBAL_OPTIONS["preserved_tasks_list"]: task_id = GLOBAL_OPTIONS["preserved_tasks_list"].pop(0) task.get_restored_task_name(task_id) + self.frames.append(task) self.rows_counter += 1 self.frames_count += len(row_count) self.content_frame.update() @@ -1513,6 +1515,22 @@ class MainFrame(elements.ScrolledCanvas): except TclError: pass + def pause_all(self): + for frame in self.frames: + if frame.running: + self.active_frames.append(frame) + frame.timer_stop() + + def resume_all(self): + for frame in self.active_frames: + frame.timer_start() + + def stop_all(self): + for frame in self.frames: + if frame.running: + frame.timer_stop() + self.active_frames.clear() + class MainMenu(tk.Menu): """Main window menu.""" @@ -1799,6 +1817,7 @@ class MainWindow(tk.Tk): if GLOBAL_OPTIONS['always_on_top'] == '1': self.wm_attributes("-topmost", 1) self.bind("", self.hotkeys) + self.paused = False def hotkeys(self, event): """Execute corresponding actions for hotkeys.""" @@ -1823,7 +1842,8 @@ class MainWindow(tk.Tk): self.add_clear_button.grid(row=2, column=0, sticky='wsn', pady=5, padx=5) self.add_pause_button = elements.TaskButton(self, text="Pause all", - command=self.pause_all) + command=self.pause_all, + textwidth=10) self.add_pause_button.grid(row=2, column=3, sticky='snw', pady=5, padx=5) self.add_quit_button = elements.TaskButton(self, text="Quit", @@ -1841,11 +1861,20 @@ class MainWindow(tk.Tk): self.taskframes.change_interface('small') def pause_all(self): - pass + if self.paused: + self.add_pause_button.config(text="Pause all") + self.taskframes.resume_all() + self.paused = False + else: + self.add_pause_button.config(text="Resume all") + self.taskframes.pause_all() + self.paused = True def stopall(self): """Stop all running timers.""" - GLOBAL_OPTIONS["stopall"] = True + self.taskframes.stop_all() + self.paused = False + self.add_pause_button.config(text="Pause all") def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") @@ -1919,8 +1948,6 @@ if __name__ == "__main__": GLOBAL_OPTIONS["tasks"] = dict() # List of preserved tasks which are not open: GLOBAL_OPTIONS["preserved_tasks_list"] = list(GLOBAL_OPTIONS["tasks"]) - # If True, all running timers will be stopped: - GLOBAL_OPTIONS["stopall"] = False # Widget which is currently connected to context menu: GLOBAL_OPTIONS["selected_widget"] = None GLOBAL_OPTIONS.update( From 18bc9289b340d29bb6f02fff697c323fa1a376da Mon Sep 17 00:00:00 2001 From: drevoborod Date: Fri, 15 Feb 2019 08:31:13 +0300 Subject: [PATCH 03/55] 'Pause all' issue fixed. --- tasker.pyw | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tasker.pyw b/tasker.pyw index 1d6801d..3e14d13 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -226,6 +226,7 @@ class TaskFrame(tk.Frame): self.timer_stop() else: self.timer_start() + GLOBAL_OPTIONS["paused"].discard(self) def properties_window(self): """Task properties window.""" @@ -1444,7 +1445,6 @@ class MainFrame(elements.ScrolledCanvas): self.frames_count = 0 self.rows_counter = 0 self.frames = [] - self.active_frames = [] self.fill() def clear(self): @@ -1518,18 +1518,19 @@ class MainFrame(elements.ScrolledCanvas): def pause_all(self): for frame in self.frames: if frame.running: - self.active_frames.append(frame) + GLOBAL_OPTIONS["paused"].add(frame) frame.timer_stop() def resume_all(self): - for frame in self.active_frames: + for frame in GLOBAL_OPTIONS["paused"]: frame.timer_start() + GLOBAL_OPTIONS["paused"].clear() def stop_all(self): for frame in self.frames: if frame.running: frame.timer_stop() - self.active_frames.clear() + GLOBAL_OPTIONS["paused"].clear() class MainMenu(tk.Menu): @@ -1950,8 +1951,9 @@ if __name__ == "__main__": GLOBAL_OPTIONS["preserved_tasks_list"] = list(GLOBAL_OPTIONS["tasks"]) # Widget which is currently connected to context menu: GLOBAL_OPTIONS["selected_widget"] = None - GLOBAL_OPTIONS.update( - {"MAX_TASKS": MAX_TASKS, "SAVE_INTERVAL": SAVE_INTERVAL}) + GLOBAL_OPTIONS.update({"MAX_TASKS": MAX_TASKS, + "SAVE_INTERVAL": SAVE_INTERVAL, + "paused": set()}) # Main window: run = MainWindow() From cc972a64ef9421db0c9f8c516095ee8482851eba Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 15 Feb 2019 11:02:39 +0300 Subject: [PATCH 04/55] Hotkey 'P' for "Pause all" added. --- changelog.txt | 6 ++++++ tasker.pyw | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8da3d1c..68aaa7f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,9 @@ +v.1.5.1 +_Добавлено +1. Кнопка Pause all/Resume all. +_Исправления ошибок: + + v.1.5 1. Опция: отображать только сегодняшний день в таймерах. 2. Кнопка "Отмена" в окне редактирования опций. diff --git a/tasker.pyw b/tasker.pyw index 3e14d13..0721fbe 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -1468,7 +1468,7 @@ class MainFrame(elements.ScrolledCanvas): for w in self.content_frame.winfo_children(): self.frames_count -= 1 w.destroy() - self.active_frames.clear() + GLOBAL_OPTIONS["paused"].clear() self.fill() def frames_refill(self): @@ -1829,6 +1829,8 @@ class MainWindow(tk.Tk): elif event.keysym in ( 'Cyrillic_shorti', 'Cyrillic_SHORTI', 'q', 'Q', 'Escape'): self.destroy() + elif event.keysym in ('Cyrillic_ZE', 'Cyrillic_ze', 'p', 'P'): + self.pause_all() def full_interface(self, firstrun=False): """Create elements which are displayed in full interface mode.""" @@ -1856,18 +1858,21 @@ class MainWindow(tk.Tk): def small_interface(self): """Destroy all additional interface elements.""" - for widget in self.add_frame, self.add_stop_button, \ - self.add_clear_button, self.add_quit_button: + for widget in (self.add_frame, self.add_stop_button, + self.add_clear_button, self.add_quit_button, + self.add_pause_button): widget.destroy() self.taskframes.change_interface('small') def pause_all(self): if self.paused: - self.add_pause_button.config(text="Pause all") + if GLOBAL_OPTIONS["compact_interface"] == "0": + self.add_pause_button.config(text="Pause all") self.taskframes.resume_all() self.paused = False else: - self.add_pause_button.config(text="Resume all") + if GLOBAL_OPTIONS["compact_interface"] == "0": + self.add_pause_button.config(text="Resume all") self.taskframes.pause_all() self.paused = True @@ -1875,7 +1880,8 @@ class MainWindow(tk.Tk): """Stop all running timers.""" self.taskframes.stop_all() self.paused = False - self.add_pause_button.config(text="Pause all") + if GLOBAL_OPTIONS["compact_interface"] == "0": + self.add_pause_button.config(text="Pause all") def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") @@ -1954,7 +1960,6 @@ if __name__ == "__main__": GLOBAL_OPTIONS.update({"MAX_TASKS": MAX_TASKS, "SAVE_INTERVAL": SAVE_INTERVAL, "paused": set()}) - # Main window: run = MainWindow() run.mainloop() From b349d7d3c9bc968fc8aac1ccdf5b8479460a93fc Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 23 Feb 2019 10:20:21 +0300 Subject: [PATCH 05/55] Fixed filter issues. --- core.py | 18 +++++++--------- tasker.pyw | 61 +++++++++++++++++++++++++++++------------------------- 2 files changed, 41 insertions(+), 38 deletions(-) diff --git a/core.py b/core.py index ff8e667..2f8f498 100644 --- a/core.py +++ b/core.py @@ -138,9 +138,7 @@ def delete(self, table="tasks", **field_values): value = field_values[key] if type(value) in (list, tuple): value = tuple(value) - if len(value) == 1: - value = "('%s')" % value[0] - clauses.append("{0} in {1}".format(key, value)) + clauses.append("{0} in ({1})".format(key, ",".join((map(str, value))))) else: clauses.append("{0}='{1}'".format(key, value)) clauses = " AND ".join(clauses) @@ -161,9 +159,9 @@ def tasks_to_export(self, ids): self.exec_script( "select name, description, activity.date, activity.spent_time " "from tasks join activity " - "on tasks.id=activity.task_id where tasks.id in {0} " + "on tasks.id=activity.task_id where tasks.id in ({0}) " "order by tasks.name, activity.date". - format(tuple(ids))) + format(",".join(map(str, ids)))) res = self.cur.fetchall() result = odict() for item in res: @@ -175,9 +173,9 @@ def tasks_to_export(self, ids): self.exec_script( "select name, fulltime from tasks join (select task_id, " "sum(spent_time) as fulltime " - "from activity where task_id in {0} group by task_id) " + "from activity where task_id in ({0}) group by task_id) " "as act on tasks.id=act.task_id". - format(tuple(ids))) + format(",".join(map(str, ids)))) res = self.cur.fetchall() for item in res: result[item[0]].append(time_format(item[1])) @@ -188,9 +186,9 @@ def dates_to_export(self, ids): self.exec_script( "select date, tasks.name, tasks.description, " "spent_time from activity join tasks " - "on activity.task_id=tasks.id where task_id in {0} " + "on activity.task_id=tasks.id where task_id in ({0}) " "order by date, tasks.name". - format(tuple(ids))) + format(",".join(map(str, ids)))) res = self.cur.fetchall() result = odict() for item in res: @@ -202,7 +200,7 @@ def dates_to_export(self, ids): time_format(item[3])]]] self.exec_script( "select date, sum(spent_time) from activity where task_id " - "in {0} group by date order by date".format(tuple(ids))) + "in ({0}) group by date order by date".format(",".join(map(str, ids)))) res = self.cur.fetchall() for item in res: result[item[0]].append(time_format(item[1])) diff --git a/tasker.pyw b/tasker.pyw index 0721fbe..b750eca 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -1298,31 +1298,29 @@ class FilterWindow(Window): 'JOIN (SELECT task_id, sum(spent_time) ' \ 'AS total_time ' \ 'FROM activity GROUP BY task_id) AS act ' \ - 'ON act.task_id=tasks.id WHERE date IN {1} ' \ - 'OR tag_id IN {0} ' \ - 'GROUP BY act.task_id'.format( - "('%s')" % tags[0] if len(tags) == 1 else tuple(tags), - "('%s')" % dates[0] if len(dates) == 1 else tuple(dates)) + 'ON act.task_id=tasks.id WHERE date IN ({1}) ' \ + 'OR tag_id IN ({0}) ' \ + 'GROUP BY act.task_id'.\ + format(",".join(map(str, tags)), "'%s'" % "','".join(dates)) else: if dates and tags: script = 'SELECT DISTINCT id, name, total_time, ' \ 'description, creation_date FROM tasks JOIN ' \ '(SELECT task_id, sum(spent_time) AS total_time '\ - 'FROM activity WHERE activity.date IN {0} ' \ + 'FROM activity WHERE activity.date IN ({0}) ' \ 'GROUP BY task_id) AS act ' \ 'ON act.task_id=tasks.id JOIN (SELECT tt.task_id'\ ' FROM tasks_tags AS tt WHERE ' \ - 'tt.tag_id IN {1} GROUP BY tt.task_id ' \ + 'tt.tag_id IN ({1}) GROUP BY tt.task_id ' \ 'HAVING COUNT(DISTINCT tt.tag_id)={3}) AS x ON ' \ 'x.task_id=tasks.id JOIN (SELECT act.task_id ' \ - 'FROM activity AS act WHERE act.date IN {0} ' \ + 'FROM activity AS act WHERE act.date IN ({0}) ' \ 'GROUP BY act.task_id HAVING ' \ 'COUNT(DISTINCT act.date)={2}) AS y ON ' \ - 'y.task_id=tasks.id'.format( - "('%s')" % dates[0] if len(dates) == 1 else tuple( - dates), - "('%s')" % tags[0] if len(tags) == 1 else tuple(tags), - len(dates), len(tags)) + 'y.task_id=tasks.id'.\ + format("'%s'" % "','".join(dates), ",".join(map(str, + tags)), + len(dates), len(tags)) elif not dates: script = 'SELECT DISTINCT id, name, total_time, ' \ 'description, creation_date FROM tasks ' \ @@ -1330,26 +1328,24 @@ class FilterWindow(Window): 'AS total_time FROM activity GROUP BY ' \ 'task_id) AS act ON act.task_id=tasks.id ' \ 'JOIN (SELECT tt.task_id FROM tasks_tags ' \ - 'AS tt WHERE tt.tag_id IN {0} GROUP BY ' \ + 'AS tt WHERE tt.tag_id IN ({0}) GROUP BY ' \ 'tt.task_id HAVING ' \ 'COUNT(DISTINCT tt.tag_id)={1}) AS x ON ' \ - 'x.task_id=tasks.id'.format( - tuple(tags) if len(tags) > 1 else "(%s)" % tags[0], - len(tags)) + 'x.task_id=tasks.id'.\ + format(",".join(map(str, tags)), len(tags)) elif not tags: script = 'SELECT DISTINCT id, name, total_time, ' \ 'description, creation_date FROM tasks ' \ 'JOIN (SELECT task_id, sum(spent_time) ' \ 'AS total_time FROM activity WHERE activity.date'\ - ' IN {0} GROUP BY task_id) AS act ' \ + ' IN ({0}) GROUP BY task_id) AS act ' \ 'ON act.task_id=tasks.id JOIN (SELECT ' \ 'act.task_id FROM activity AS act ' \ - 'WHERE act.date IN {0} GROUP BY act.task_id ' \ + 'WHERE act.date IN ({0}) GROUP BY act.task_id ' \ 'HAVING COUNT(DISTINCT act.date)={1}) AS y ' \ - 'ON y.task_id=tasks.id'.format( - tuple(dates) if len(dates) > 1 else "('%s')" % dates[ - 0], - len(dates)) + 'ON y.task_id=tasks.id'.format("'%s'" % "','" + .join(dates), + len(dates)) GLOBAL_OPTIONS["filter_dict"] = { 'operating_mode': self.operating_mode.get(), 'script': script, @@ -1783,11 +1779,20 @@ class ExportWindow(Window): self.export('\n'.join(prepared_data)) def export(self, data): - filename = asksaveasfilename(parent=self, defaultextension=".csv", - filetypes=[("All files", "*.*"), ( - "Comma-separated texts", "*.csv")]) - if filename: - core.write_to_disk(filename, data) + while True: + filename = asksaveasfilename(parent=self, defaultextension=".csv", + filetypes=[("All files", "*.*"), ( + "Comma-separated texts", "*.csv")]) + if filename: + try: + core.write_to_disk(filename, data) + except PermissionError: + showinfo("Unable to save file", "No permission to save file here!" + "Please select another location.") + else: + break + else: + break self.destroy() From a240ae6c3f0c3f239e581eeaecd2d8d93b297582 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 23 Feb 2019 10:29:34 +0300 Subject: [PATCH 06/55] Update changelog: v.1.5.1. --- changelog.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 68aaa7f..9d8ee71 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,7 +2,10 @@ v.1.5.1 _Добавлено 1. Кнопка Pause all/Resume all. _Исправления ошибок: - +1. Сообщение об ошибке, если нет прав на запись в директорию, выбранную для +экспорта csv. +2. Сбой при попытке экспортировать в csv список длиной в одну задачу. +3. Не работала фильтрация по датам. v.1.5 1. Опция: отображать только сегодняшний день в таймерах. From 1497dd644c3417f34aafc736f84374013ecd6c00 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 23 Feb 2019 11:12:13 +0300 Subject: [PATCH 07/55] Fix stopall after options change. --- tasker.pyw | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tasker.pyw b/tasker.pyw index b750eca..166c403 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -364,7 +364,6 @@ class TaskFrame(tk.Frame): def timer_start(self): """Counter start.""" if not self.running: - GLOBAL_OPTIONS["stopall"] = False if int(GLOBAL_OPTIONS["toggle_tasks"]): for key in GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"][key] = False @@ -1604,7 +1603,7 @@ class MainMenu(tk.Menu): # Stop all tasks if exclusive run method has been enabled: if int(GLOBAL_OPTIONS["toggle_tasks"]) and int( GLOBAL_OPTIONS["toggle_tasks"]) != toggle: - GLOBAL_OPTIONS["stopall"] = True + run.stopall() run.lift() def change_parameter(self, paramdict): From 2c8393bac2adb27dff2a8e5bbf158f2175c94cf9 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 23 Feb 2019 18:22:21 +0300 Subject: [PATCH 08/55] Big refactoring. --- core.py | 103 +++++--- tasker.pyw | 750 +++++++++++++++++++++++++---------------------------- 2 files changed, 426 insertions(+), 427 deletions(-) diff --git a/core.py b/core.py index 2f8f498..7f7400c 100644 --- a/core.py +++ b/core.py @@ -65,24 +65,22 @@ def find_all(self, table, sortfield=None): return self.cur.fetchall() def select_task(self, task_id): - """Returns tuple of values for given task_id.""" - task = list( - self.find_by_clause(searchfield='*', field='id', value=task_id, - table='tasks')[0]) + """Returns dictionary of values for given task_id.""" + res = self.find_by_clause(searchfield='*', field='id', value=task_id, + table='tasks')[0] + task = {key: res[number] for number, key + in enumerate(["id", "name", "descr", "creation_date"])} # Adding full spent time: self.exec_script( 'SELECT sum(spent_time) FROM activity WHERE task_id=%s' % task_id) # Adding spent time on position 3: - task.insert(2, self.cur.fetchone()[0]) + task["spent_total"] = self.cur.fetchone()[0] # Append today's spent time: self.exec_script( 'SELECT spent_time FROM activity WHERE task_id={0} AND ' 'date="{1}"'.format(task_id, date_format(datetime.datetime.now()))) today_time = self.cur.fetchone() - if today_time: - task.append(today_time[0]) - else: - task.append(today_time) + task["spent_today"] = today_time[0] if today_time else 0 return task def insert(self, table, fields, values): @@ -162,23 +160,41 @@ def tasks_to_export(self, ids): "on tasks.id=activity.task_id where tasks.id in ({0}) " "order by tasks.name, activity.date". format(",".join(map(str, ids)))) - res = self.cur.fetchall() - result = odict() - for item in res: - if item[0] in result: - result[item[0]][1].append((item[2], time_format(item[3]))) + db_response = [{"name": item[0], "descr": item[1] if item[1] else '', + "date": item[2], "spent_time": item[3]} + for item in self.cur.fetchall()] + prepared_data = odict() + for item in db_response: + if item["name"] in prepared_data: + prepared_data[item["name"]]["dates"].append( + (item["date"], time_format(item["spent_time"]))) else: - result[item[0]] = [item[1] if item[1] else '', - [(item[2], time_format(item[3]))]] + prepared_data[item["name"]] = { + "descr": item['descr'], + "dates": [(item["date"], + time_format(item["spent_time"]))]} self.exec_script( "select name, fulltime from tasks join (select task_id, " "sum(spent_time) as fulltime " "from activity where task_id in ({0}) group by task_id) " "as act on tasks.id=act.task_id". format(",".join(map(str, ids)))) - res = self.cur.fetchall() - for item in res: - result[item[0]].append(time_format(item[1])) + for item in self.cur.fetchall(): + prepared_data[item[0]]["spent_total"] = time_format(item[1]) + + result = ['Task,Description,Dates,Time,Total working time'] + for key in prepared_data: + temp_list = [key, prepared_data[key]["descr"], + prepared_data[key]["dates"][0][0], + prepared_data[key]["dates"][0][1], + prepared_data[key]["spent_total"]] + result.append(','.join(temp_list)) + if len(prepared_data[key]["dates"]) > 1: + for i in range(1, len(prepared_data[key]["dates"])): + result.append(','.join( + ['', '', prepared_data[key]["dates"][i][0], + prepared_data[key]["dates"][i][1], ''])) + i += 1 return result def dates_to_export(self, ids): @@ -189,21 +205,46 @@ def dates_to_export(self, ids): "on activity.task_id=tasks.id where task_id in ({0}) " "order by date, tasks.name". format(",".join(map(str, ids)))) - res = self.cur.fetchall() - result = odict() - for item in res: - if item[0] in result: - result[item[0]][0].append([item[1], item[2] if item[2] else '', - time_format(item[3])]) + db_response = [{"date": item[0], "name": item[1], + "descr": item[2] if item[2] else '', + "spent_time": item[3]} for item in self.cur.fetchall()] + + prepared_data = odict() + for item in db_response: + if item["date"] in prepared_data: + prepared_data[item["date"]]["tasks"].append({ + "name": item["name"], "descr": item["descr"], + "spent_time": time_format(item["spent_time"])}) else: - result[item[0]] = [[[item[1], item[2] if item[2] else '', - time_format(item[3])]]] + prepared_data[item["date"]] = { + "tasks": [{"name": item["name"], + "descr": item["descr"], + "spent_time": time_format(item["spent_time"])}]} self.exec_script( "select date, sum(spent_time) from activity where task_id " - "in ({0}) group by date order by date".format(",".join(map(str, ids)))) - res = self.cur.fetchall() - for item in res: - result[item[0]].append(time_format(item[1])) + "in ({0}) group by date order by date".format(",".join(map(str, + ids)))) + for item in self.cur.fetchall(): + prepared_data[item[0]]["spent_total"] = (time_format(item[1])) + + result = [ + 'Date,Tasks,Descriptions,Time,Summarized working time'] + for key in prepared_data: + temp_list = [key, + prepared_data[key]["tasks"][0]["name"], + prepared_data[key]["tasks"][0]["descr"], + prepared_data[key]["tasks"][0]["spent_time"], + prepared_data[key]["spent_total"]] + result.append(','.join(temp_list)) + if len(prepared_data[key]["tasks"]) > 1: + for i in range(1, len(prepared_data[key]["tasks"])): + result.append(','.join( + ['', + prepared_data[key]["tasks"][i]["name"], + prepared_data[key]["tasks"][i]["descr"], + prepared_data[key]["tasks"][i]["spent_time"], + ''])) + i += 1 return result def tags_dict(self, taskid): diff --git a/tasker.pyw b/tasker.pyw index 166c403..a8b52b2 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -134,31 +134,29 @@ class TaskFrame(tk.Frame): def create_content(self): """Creates all window elements.""" - self.startstopvar = tk.StringVar() # Text on "Start" button. - self.startstopvar.set("Start") + self.startstop_var = tk.StringVar() # Text on "Start" button. + self.startstop_var.set("Start") # Fake name of running task (which actually is not selected yet). self.task = None - self.task_id = None - self.description = None if GLOBAL_OPTIONS["compact_interface"] == "0": self.normal_interface() # Task name field: - self.tasklabel = TaskLabel(self, width=50, anchor='w') - elements.big_font(self.tasklabel, size=14) - self.tasklabel.grid(row=1, column=0, columnspan=5, padx=5, pady=5, - sticky='w') - self.openbutton = elements.TaskButton(self, text="Task...", - command=self.name_dialogue) - self.openbutton.grid(row=1, column=5, padx=5, pady=5, sticky='e') - self.startbutton = elements.CanvasButton( + self.task_label = TaskLabel(self, width=50, anchor='w') + elements.big_font(self.task_label, size=14) + self.task_label.grid(row=1, column=0, columnspan=5, padx=5, pady=5, + sticky='w') + self.open_button = elements.TaskButton(self, text="Task...", + command=self.name_dialogue) + self.open_button.grid(row=1, column=5, padx=5, pady=5, sticky='e') + self.start_button = elements.CanvasButton( self, state='disabled', fontsize=14, - command=self.startstopbutton, - variable=self.startstopvar, + command=self.start_stop, + variable=self.startstop_var, image='resource/start_disabled.png' if tk.TkVersion >= 8.6 else 'resource/start_disabled.pgm', opacity='left') - self.startbutton.grid(row=3, column=0, sticky='wsn', padx=5) + self.start_button.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: self.timer_window = TaskLabel(self, width=10, state='disabled') elements.big_font(self.timer_window, size=20) @@ -178,16 +176,15 @@ class TaskFrame(tk.Frame): ) self.timestamps_window_button.grid(row=3, column=3, sticky='wsn', padx=5) - self.properties = elements.TaskButton(self, text="Properties...", - textwidth=9, state='disabled', - command=self.properties_window) - self.properties.grid(row=3, column=4, sticky='e', padx=5) + self.properties_button = elements.TaskButton( + self, text="Properties...", textwidth=9, state='disabled', + command=self.properties_window) + self.properties_button.grid(row=3, column=4, sticky='e', padx=5) # Clear frame button: - self.clearbutton = elements.TaskButton(self, text='Clear', - state='disabled', textwidth=7, - command=self.clear) - self.clearbutton.grid(row=3, column=5, sticky='e', padx=5) - self.running_time = 0 # Current value of the counter. + self.clear_button = elements.TaskButton(self, text='Clear', + state='disabled', textwidth=7, + command=self.clear) + self.clear_button.grid(row=3, column=5, sticky='e', padx=5) self.running = False self.timestamp = 0 @@ -198,29 +195,30 @@ class TaskFrame(tk.Frame): elements.big_font(self.l1, size=12) self.l1.grid(row=0, column=1, columnspan=3) # Task description field: - self.description = Description(self, width=60, height=3) - self.description.grid(row=2, column=0, columnspan=6, padx=5, pady=6, - sticky='we') + self.description_area = Description(self, width=60, height=3) + self.description_area.grid(row=2, column=0, columnspan=6, padx=5, + pady=6, sticky='we') if self.task: - self.description.update_text(self.task[3]) + self.description_area.update_text(self.task['descr']) def small_interface(self): """Destroy some interface elements when switching to 'compact' mode.""" - for widget in self.l1, self.description: + for widget in self.l1, self.description_area: widget.destroy() - self.description = None + if hasattr(self, "description_area"): + delattr(self, "description_area") def timestamps_window(self): """Timestamps window opening.""" - TimestampsWindow(self.task_id, self.running_time, run) + TimestampsWindow(self.task["id"], self.spent_current, run) def add_timestamp(self): """Adding timestamp to database.""" self.db.insert('timestamps', ('task_id', 'timestamp'), - (self.task_id, self.running_time)) + (self.task["id"], self.spent_current)) showinfo("Timestamp added", "Timestamp added.") - def startstopbutton(self): + def start_stop(self): """Changes "Start/Stop" button state. """ if self.running: self.timer_stop() @@ -230,9 +228,9 @@ class TaskFrame(tk.Frame): def properties_window(self): """Task properties window.""" - edited = tk.IntVar() - TaskEditWindow(self.task[0], parent=run, variable=edited) - if edited.get() == 1: + edited_var = tk.IntVar() + TaskEditWindow(self.task["id"], parent=run, variable=edited_var) + if edited_var.get() == 1: self.update_description() def clear(self): @@ -240,7 +238,7 @@ class TaskFrame(tk.Frame): self.timer_stop() for w in self.winfo_children(): w.destroy() - GLOBAL_OPTIONS["tasks"].pop(self.task[0]) + GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) self.create_content() def name_dialogue(self): @@ -259,61 +257,48 @@ class TaskFrame(tk.Frame): # Stopping current timer and saving its state: self.timer_stop() # If there is open task, we remove it from running tasks set: - GLOBAL_OPTIONS["tasks"].pop(self.task[0]) + GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) self.get_restored_task_name(task_id) else: # If selected task is already opened in another frame: - if self.task_id != task_id: + if self.task["id"] != task_id: showinfo("Task exists", "Task is already opened.") def get_restored_task_name(self, taskid): - self.task_id = taskid # Preparing new task: self.prepare_task( - self.db.select_task(self.task_id)) # Task parameters from database + self.db.select_task(taskid)) # Task parameters from database def prepare_task(self, task): """Prepares frame elements to work with.""" # Adding task id to set of running tasks: - GLOBAL_OPTIONS["tasks"][task[0]] = False + GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task self.current_date = core.date_format(datetime.datetime.now()) # Set current time, just for this day: - if self.task[-1] is None: - self.date_exists = False - self.task[-1] = 0 - else: - self.date_exists = True + self.date_exists = True if self.task["spent_today"] else False # Taking current counter value from database: self.set_current_time() - self.timer_window.config(text=core.time_format(self.running_time)) - self.tasklabel.config(text=self.task[1]) - self.startbutton.config(state='normal') - self.startbutton.config(image='resource/start_normal.png' + self.timer_window.config(text=core.time_format(self.spent_current)) + self.task_label.config(text=self.task["name"]) + self.start_button.config(state='normal') + self.start_button.config(image='resource/start_normal.png' if tk.TkVersion >= 8.6 else 'resource/start_normal.pgm') - self.properties.config(state='normal') - self.clearbutton.config(state='normal') + self.properties_button.config(state='normal') + self.clear_button.config(state='normal') self.timer_window.config(state='normal') self.add_timestamp_button.config(state='normal') self.timestamps_window_button.config(state='normal') - if self.description: - self.description.update_text(self.task[3]) + if hasattr(self, "description_area"): + self.description_area.update_text(self.task["descr"]) def set_current_time(self): """Set current_time depending on time displaying options value.""" if int(GLOBAL_OPTIONS["show_today"]): - self.running_time = self.task[5] + self.spent_current = self.task["spent_today"] else: - self.running_time = self.task[2] - - def reload_timer(self): - """Used for task data reloading - without explicitly redrawing anything but timer.""" - self.timer_stop() - self.task = self.db.select_task(self.task_id) - self.set_current_time() - self.timer_start() + self.spent_current = self.task["spent_total"] def check_date(self): """Used to check if date has been changed @@ -322,33 +307,32 @@ class TaskFrame(tk.Frame): if current_date != self.current_date: self.current_date = current_date self.date_exists = False - self.running_today_time = self.running_today_time - self.timestamp - self.start_today_time = time.time() - self.running_today_time + self.task["spent_today"] = self.task["spent_today"] - self.timestamp + self.start_today_timestamp = time.time() - self.task["spent_today"] self.task_update() def task_update(self): """Updates time in the database.""" if not self.date_exists: self.db.insert("activity", ("date", "task_id", "spent_time"), - (self.current_date, self.task[0], - self.running_today_time)) + (self.current_date, self.task["id"], + self.task["spent_today"])) self.date_exists = True - # self.reload_timer() else: - self.db.update_task(self.task[0], value=self.running_today_time) - self.timestamp = self.running_today_time + self.db.update_task(self.task["id"], value=self.task["spent_today"]) + self.timestamp = self.task["spent_today"] def timer_update(self, counter=0): """Renewal of the counter.""" # Time interval in milliseconds # before next iteration of recursion: interval = 250 - self.running_time = time.time() - self.start_time - self.running_today_time = time.time() - self.start_today_time + self.spent_current = time.time() - self.start_time + self.task["spent_today"] = time.time() - self.start_today_timestamp self.timer_window.config(text=core.time_format( - self.running_time if self.running_time < 86400 - else self.running_today_time)) - if GLOBAL_OPTIONS["tasks"][self.task_id]: + self.spent_current if self.spent_current < 86400 + else self.task["spent_today"])) + if GLOBAL_OPTIONS["tasks"][self.task["id"]]: # Every n seconds counter value is saved in database: if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: self.check_date() @@ -367,99 +351,99 @@ class TaskFrame(tk.Frame): if int(GLOBAL_OPTIONS["toggle_tasks"]): for key in GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"][key] = False - GLOBAL_OPTIONS["tasks"][self.task_id] = True + GLOBAL_OPTIONS["tasks"][self.task["id"]] = True # Setting current counter value: - self.start_time = time.time() - self.running_time + self.start_time = time.time() - self.spent_current # This value is used to add record to database: - self.start_today_time = time.time() - self.task[-1] + self.start_today_timestamp = time.time() - self.task["spent_today"] self.timer_update() self.running = True - self.startbutton.config( + self.start_button.config( image='resource/stop.png' if tk.TkVersion >= 8.6 else 'resource/stop.pgm') - self.startstopvar.set("Stop") + self.startstop_var.set("Stop") def timer_stop(self): """Stop counter and save its value to database.""" if self.running: # after_cancel() stops execution of callback with given ID. self.timer_window.after_cancel(self.timer) - self.running_time = time.time() - self.start_time - self.running_today_time = time.time() - self.start_today_time + self.spent_current = time.time() - self.start_time + self.task["spent_today"] = time.time() - self.start_today_timestamp self.running = False - GLOBAL_OPTIONS["tasks"][self.task_id] = False + GLOBAL_OPTIONS["tasks"][self.task["id"]] = False # Writing value into database: self.check_date() - self.task[2] = self.running_time - self.task[5] = self.running_today_time + self.task["spent_total"] = self.spent_current self.update_description() - self.startbutton.config( + self.start_button.config( image='resource/start_normal.png' if tk.TkVersion >= 8.6 else 'resource/start_normal.pgm') - self.startstopvar.set("Start") + self.startstop_var.set("Start") def update_description(self): """Update text in "Description" field.""" - self.task[3] = \ - self.db.find_by_clause("tasks", "id", self.task[0], + self.task["descr"] = \ + self.db.find_by_clause("tasks", "id", self.task["id"], "description")[0][0] - if self.description: - self.description.update_text(self.task[3]) + if hasattr(self, "description_area"): + self.description_area.update_text(self.task["descr"]) def destroy(self): """Closes frame and writes counter value into database.""" self.timer_stop() if self.task: - GLOBAL_OPTIONS["tasks"].pop(self.task[0]) + GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) self.db.con.close() tk.Frame.destroy(self) -class TaskList(tk.Frame): +class TaskTable(tk.Frame): """Scrollable tasks table.""" def __init__(self, columns, parent=None, **options): super().__init__(master=parent, **options) - self.taskslist = ttk.Treeview(self) # A table. + self.tasks_table = ttk.Treeview(self) style = ttk.Style() style.configure(".", font=('Helvetica', 11)) style.configure("Treeview.Heading", font=('Helvetica', 11)) scroller = tk.Scrollbar(self) - scroller.config(command=self.taskslist.yview) - self.taskslist.config(yscrollcommand=scroller.set) + scroller.config(command=self.tasks_table.yview) + self.tasks_table.config(yscrollcommand=scroller.set) scroller.pack(side='right', fill='y') - self.taskslist.pack(fill='both', expand=1) + self.tasks_table.pack(fill='both', expand=1) # Creating and naming columns: - self.taskslist.config(columns=tuple([col[0] for col in columns])) - for index, col in enumerate(columns): + self.tasks_table.config(columns=tuple([key for key in columns])) + for name in columns: # Configuring columns with given ids: - self.taskslist.column(columns[index][0], width=100, minwidth=100, - anchor='center') + self.tasks_table.column(name, width=100, minwidth=100, + anchor='center') # Configuring headers of columns with given ids: - self.taskslist.heading(columns[index][0], text=columns[index][1], - command=lambda c=columns[index][0]: - self.sortlist(c, True)) - self.taskslist.column('#0', anchor='w', width=70, minwidth=50, - stretch=0) - self.taskslist.column('taskname', width=600, anchor='w') - - def sortlist(self, col, reverse): + self.tasks_table.heading(name, text=columns[name], + command=lambda c=name: + self.sort_table_contents(c, True)) + self.tasks_table.column('#0', anchor='w', width=70, minwidth=50, + stretch=0) + self.tasks_table.column('taskname', width=600, anchor='w') + + def sort_table_contents(self, col, reverse): """Sorting by click on column header.""" - if col == "time": + if col == "spent_time": shortlist = self._sort(1, reverse) - elif col == "date": + elif col == "creation_date": shortlist = self._sort(2, reverse) else: shortlist = self._sort(0, reverse) shortlist.sort(key=lambda x: x[0], reverse=reverse) for index, value in enumerate(shortlist): - self.taskslist.move(value[1], '', index) - self.taskslist.heading(col, - command=lambda: self.sortlist(col, not reverse)) + self.tasks_table.move(value[1], '', index) + self.tasks_table.heading(col, + command=lambda: + self.sort_table_contents(col, not reverse)) def _sort(self, position, reverse): l = [] - for index, task in enumerate(self.taskslist.get_children()): + for index, task in enumerate(self.tasks_table.get_children()): l.append((self.tasks[index][position], task)) # Sort tasks list by corresponding field to match current sorting: self.tasks.sort(key=lambda x: x[position], reverse=reverse) @@ -469,12 +453,12 @@ class TaskList(tk.Frame): """Insert rows in the table. Row contents are tuples provided by 'values='.""" for i, v in enumerate(tasks): # item, number, value: - self.taskslist.insert('', i, text="#%d" % (i + 1), values=v) + self.tasks_table.insert('', i, text="#%d" % (i + 1), values=v) def update_list(self, tasks): """Refill table contents.""" - for item in self.taskslist.get_children(): - self.taskslist.delete(item) + for item in self.tasks_table.get_children(): + self.tasks_table.delete(item) self.tasks = copy.deepcopy(tasks) for t in tasks: t[1] = core.time_format(t[1]) @@ -482,10 +466,10 @@ class TaskList(tk.Frame): def focus_(self, item): """Focuses on the row with provided id.""" - self.taskslist.see(item) - self.taskslist.selection_set(item) - self.taskslist.focus_set() - self.taskslist.focus(item) + self.tasks_table.see(item) + self.tasks_table.selection_set(item) + self.tasks_table.focus_set() + self.tasks_table.focus(item) class TaskSelectionWindow(Window): @@ -495,7 +479,7 @@ class TaskSelectionWindow(Window): super().__init__(master=parent, **options) # Variable which will contain selected task id: if taskvar: - self.taskidvar = taskvar + self.task_id_var = taskvar # Basic script for retrieving tasks from database: self.main_script = 'SELECT id, name, total_time, description, ' \ 'creation_date FROM tasks JOIN (SELECT task_id, ' \ @@ -507,35 +491,35 @@ class TaskSelectionWindow(Window): sticky='w', pady=5, padx=5) # New task entry field: - self.addentry = elements.SimpleEntry(self, width=50) - self.addentry.grid(row=0, column=1, columnspan=3, sticky='we') + self.add_entry = elements.SimpleEntry(self, width=50) + self.add_entry.grid(row=0, column=1, columnspan=3, sticky='we') # Enter adds new task: - self.addentry.bind('', lambda event: self.add_new_task()) - self.addentry.focus_set() + self.add_entry.bind('', lambda event: self.add_new_task()) + self.add_entry.focus_set() # Context menu with 'Paste' option: addentry_context_menu = RightclickMenu(paste_item=1, copy_item=0) - self.addentry.bind("", - addentry_context_menu.context_menu_show) + self.add_entry.bind("", + addentry_context_menu.context_menu_show) # "Add task" button: - self.addbutton = elements.TaskButton(self, text="Add task", - command=self.add_new_task, - takefocus=False) - self.addbutton.grid(row=0, column=4, sticky='e', padx=6, pady=5) + self.add_button = elements.TaskButton(self, text="Add task", + command=self.add_new_task, + takefocus=False) + self.add_button.grid(row=0, column=4, sticky='e', padx=6, pady=5) # Entry for typing search requests: - self.searchentry = elements.SimpleEntry(self, width=25) - self.searchentry.grid(row=1, column=1, columnspan=2, sticky='we', - padx=5, pady=5) + self.search_entry = elements.SimpleEntry(self, width=25) + self.search_entry.grid(row=1, column=1, columnspan=2, sticky='we', + padx=5, pady=5) searchentry_context_menu = RightclickMenu(paste_item=1, copy_item=0) - self.searchentry.bind("", - searchentry_context_menu.context_menu_show) + self.search_entry.bind("", + searchentry_context_menu.context_menu_show) # Case sensitive checkbutton: - self.ignore_case = tk.IntVar(self, value=1) + self.ignore_case_var = tk.IntVar(self, value=1) elements.SimpleCheckbutton(self, text="Ignore case", takefocus=False, - variable=self.ignore_case).grid(row=1, - column=0, - padx=6, - pady=5, - sticky='w') + variable=self.ignore_case_var).grid(row=1, + column=0, + padx=6, + pady=5, + sticky='w') # Search button: elements.CanvasButton(self, takefocus=False, text='Search', image='resource/magnifier.png' @@ -548,16 +532,16 @@ class TaskSelectionWindow(Window): elements.TaskButton(self, takefocus=False, image='resource/refresh.png' if tk.TkVersion >= 8.6 else 'resource/refresh.pgm', - command=self.update_list).grid(row=1, column=4, - sticky='e', padx=5, - pady=5) + command=self.update_table).grid(row=1, column=4, + sticky='e', padx=5, + pady=5) # Naming of columns in tasks list: - columnnames = [('taskname', 'Task name'), ('time', 'Spent time'), - ('date', 'Creation date')] + column_names = {'taskname': 'Task name', 'spent_time': 'Spent time', + 'creation_date': 'Creation date'} # Scrollable tasks table: - self.listframe = TaskList(columnnames, self, takefocus=True) - self.listframe.grid(row=2, column=0, columnspan=5, pady=10, - sticky='news') + self.table_frame = TaskTable(column_names, self, takefocus=True) + self.table_frame.grid(row=2, column=0, columnspan=5, pady=10, + sticky='news') elements.SimpleLabel(self, text="Summary time:").grid(row=3, column=0, pady=5, padx=5, sticky='w') @@ -565,92 +549,92 @@ class TaskSelectionWindow(Window): self.fulltime_frame = TaskLabel(self, width=13, anchor='center') self.fulltime_frame.grid(row=3, column=1, padx=6, pady=5, sticky='e') # Selected task description: - self.description = Description(self, height=4) - self.description.grid(row=3, column=2, rowspan=2, pady=5, padx=5, - sticky='news') + self.description_area = Description(self, height=4) + self.description_area.grid(row=3, column=2, rowspan=2, pady=5, padx=5, + sticky='news') # "Select all" button: - selbutton = elements.TaskButton(self, text="Select all", + sel_button = elements.TaskButton(self, text="Select all", command=self.select_all) - selbutton.grid(row=4, column=0, sticky='w', padx=5, pady=5) + sel_button.grid(row=4, column=0, sticky='w', padx=5, pady=5) # "Clear all" button: - clearbutton = elements.TaskButton(self, text="Clear all", + clear_button = elements.TaskButton(self, text="Clear all", command=self.clear_all) - clearbutton.grid(row=4, column=1, sticky='e', padx=5, pady=5) + clear_button.grid(row=4, column=1, sticky='e', padx=5, pady=5) # Task properties button: - self.editbutton = elements.TaskButton(self, text="Properties...", - textwidth=10, command=self.edit) - self.editbutton.grid(row=3, column=3, sticky='w', padx=5, pady=5) + self.edit_button = elements.TaskButton(self, text="Properties...", + textwidth=10, command=self.edit) + self.edit_button.grid(row=3, column=3, sticky='w', padx=5, pady=5) # Remove task button: - self.delbutton = elements.TaskButton(self, text="Remove...", - textwidth=10, command=self.delete) - self.delbutton.grid(row=4, column=3, sticky='w', padx=5, pady=5) + self.del_button = elements.TaskButton(self, text="Remove...", + textwidth=10, command=self.delete) + self.del_button.grid(row=4, column=3, sticky='w', padx=5, pady=5) # Export button: - self.exportbutton = elements.TaskButton(self, text="Export...", - command=self.export) - self.exportbutton.grid(row=4, column=4, padx=5, pady=5, sticky='e') + self.export_button = elements.TaskButton(self, text="Export...", + command=self.export) + self.export_button.grid(row=4, column=4, padx=5, pady=5, sticky='e') # Filter button: - self.filterbutton = elements.TaskButton(self, text="Filter...", - command=self.filterwindow) - self.filterbutton.grid(row=3, column=4, padx=5, pady=5, sticky='e') + self.filter_button = elements.TaskButton(self, text="Filter...", + command=self.filterwindow) + self.filter_button.grid(row=3, column=4, padx=5, pady=5, sticky='e') # Filter button context menu: filter_context_menu = RightclickMenu(copy_item=False) filter_context_menu.add_command(label='Clear filter', command=self.apply_filter) - self.filterbutton.bind("", - filter_context_menu.context_menu_show) + self.filter_button.bind("", + filter_context_menu.context_menu_show) tk.Frame(self, height=40).grid(row=5, columnspan=5, sticky='news') self.grid_columnconfigure(2, weight=1, minsize=50) self.grid_rowconfigure(2, weight=1, minsize=50) - self.update_list() # Fill table contents. + self.update_table() # Fill table contents. self.current_task = '' # Current selected task. - self.listframe.taskslist.bind("", self.descr_down) - self.listframe.taskslist.bind("", self.descr_up) - self.listframe.taskslist.bind("", self.descr_click) - self.listframe.bind("", - lambda e: self.focus_first_item(forced=False)) + self.table_frame.tasks_table.bind("", self.descr_down) + self.table_frame.tasks_table.bind("", self.descr_up) + self.table_frame.tasks_table.bind("", self.descr_click) + self.table_frame.bind("", + lambda e: self.focus_first_item(forced=False)) # Need to avoid masquerading of default ttk.Treeview action # on Shift+click and Control+click: self.modifier_pressed = False - self.listframe.taskslist.bind("", - lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_pressed()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_released()) - self.listframe.taskslist.bind("", - lambda e: self.shift_control_released()) - self.searchentry.bind("", lambda e: self.locate_task()) - self.bind("", lambda e: self.update_list()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.tasks_table.bind("", + lambda e: self.shift_control_released()) + self.search_entry.bind("", lambda e: self.locate_task()) + self.bind("", lambda e: self.update_table()) elements.TaskButton(self, text="Open", command=self.get_task).grid( row=6, column=0, padx=5, pady=5, sticky='w') elements.TaskButton(self, text="Cancel", command=self.destroy).grid( row=6, column=4, padx=5, pady=5, sticky='e') - self.listframe.taskslist.bind("", self.get_task_id) - self.listframe.taskslist.bind("", self.get_task_id) + self.table_frame.tasks_table.bind("", self.get_task_id) + self.table_frame.tasks_table.bind("", self.get_task_id) self.prepare() def check_row(self, event): """Check if mouse click is over the row, - not another taskslist element.""" + not another tasks_table element.""" if (event.type == '4' and len( - self.listframe.taskslist.identify_row(event.y)) > 0) or ( + self.table_frame.tasks_table.identify_row(event.y)) > 0) or ( event.type == '2'): return True def get_task(self): """Get selected task id from database and close window.""" # List of selected tasks item id's: - tasks = self.listframe.taskslist.selection() + tasks = self.table_frame.tasks_table.selection() if tasks: - self.taskidvar.set(self.tdict[tasks[0]][0]) + self.task_id_var.set(self.tdict[tasks[0]]["id"]) self.destroy() def get_task_id(self, event): @@ -666,48 +650,48 @@ class TaskSelectionWindow(Window): def focus_first_item(self, forced=True): """Selects first item in the table if no items selected.""" - if self.listframe.taskslist.get_children(): - item = self.listframe.taskslist.get_children()[0] + if self.table_frame.tasks_table.get_children(): + item = self.table_frame.tasks_table.get_children()[0] else: return if forced: - self.listframe.focus_(item) + self.table_frame.focus_(item) self.update_descr(item) else: - if not self.listframe.taskslist.selection(): - self.listframe.focus_(item) + if not self.table_frame.tasks_table.selection(): + self.table_frame.focus_(item) self.update_descr(item) else: - self.listframe.taskslist.focus_set() + self.table_frame.tasks_table.focus_set() def locate_task(self): """Search task by keywords.""" - searchword = self.searchentry.get() + searchword = self.search_entry.get() if searchword: self.clear_all() task_items = [] - if self.ignore_case.get(): + if self.ignore_case_var.get(): for key in self.tdict: - if searchword.lower() in self.tdict[key][1].lower(): + if searchword.lower() in self.tdict[key]["name"].lower(): task_items.append(key) # Need to be sure that there is non-empty description: - elif self.tdict[key][3]: - if searchword.lower() in self.tdict[key][3].lower(): + elif self.tdict[key]["descr"]: + if searchword.lower() in self.tdict[key]["descr"].lower(): task_items.append(key) else: for key in self.tdict: - if searchword in self.tdict[key][1]: + if searchword in self.tdict[key]["name"]: task_items.append(key) - elif self.tdict[key][3]: - if searchword in self.tdict[key][3]: + elif self.tdict[key]["descr"]: + if searchword in self.tdict[key]["descr"]: task_items.append(key) if task_items: for item in task_items: - self.listframe.taskslist.selection_add(item) - item = self.listframe.taskslist.selection()[0] - self.listframe.taskslist.see(item) - self.listframe.taskslist.focus_set() - self.listframe.taskslist.focus(item) + self.table_frame.tasks_table.selection_add(item) + item = self.table_frame.tasks_table.selection()[0] + self.table_frame.tasks_table.see(item) + self.table_frame.tasks_table.focus_set() + self.table_frame.tasks_table.focus(item) self.update_descr(item) else: showinfo("No results", @@ -720,7 +704,7 @@ class TaskSelectionWindow(Window): def add_new_task(self): """Adds new task into the database.""" - task_name = self.addentry.get() + task_name = self.add_entry.get() if task_name: for x in ('"', "'", "`"): task_name = task_name.replace(x, '') @@ -729,8 +713,8 @@ class TaskSelectionWindow(Window): except core.DbErrors: self.db.reconnect() for item in self.tdict: - if self.tdict[item][1] == task_name: - self.listframe.focus_(item) + if self.tdict[item]["name"] == task_name: + self.table_frame.focus_(item) self.update_descr(item) break else: @@ -738,11 +722,11 @@ class TaskSelectionWindow(Window): "Task already exists. " "Change filter configuration to see it.") else: - self.update_list() - # If created task appears in the table, highlighting it: + self.update_table() + # If created task appears in the table, highlight it: for item in self.tdict: - if self.tdict[item][1] == task_name: - self.listframe.focus_(item) + if self.tdict[item]["name"] == task_name: + self.table_frame.focus_(item) break else: showinfo("Task created", @@ -750,25 +734,28 @@ class TaskSelectionWindow(Window): "Change filter configuration to see it.") def filter_query(self): - return self.db.find_by_clause( - 'options', 'name', 'filter', 'value')[0][0] + return self.db.find_by_clause(table='options', field='name', + value='filter', searchfield='value')[0][0] - def update_list(self): + def update_table(self): """Updating table contents using database query.""" # Restoring filter value: query = self.filter_query() if query: - self.filterbutton.config(bg='lightblue') + self.filter_button.config(bg='lightblue') self.db.exec_script(query) else: - self.filterbutton.config(bg=GLOBAL_OPTIONS["colour"]) + self.filter_button.config(bg=GLOBAL_OPTIONS["colour"]) self.db.exec_script(self.main_script) - tlist = self.db.cur.fetchall() - self.listframe.update_list([[f[1], f[2], f[4]] for f in tlist]) + tlist = [{"id": task[0], "name": task[1], "spent_time": task[2], + "descr": task[3], "creation_date": task[4]} + for task in self.db.cur.fetchall()] + self.table_frame.update_list([[f["name"], f["spent_time"], + f["creation_date"]] for f in tlist]) # Dictionary with row ids and tasks info: self.tdict = {} i = 0 - for task_id in self.listframe.taskslist.get_children(): + for task_id in self.table_frame.tasks_table.get_children(): self.tdict[task_id] = tlist[i] i += 1 self.update_descr(None) @@ -777,21 +764,21 @@ class TaskSelectionWindow(Window): def update_fulltime(self): """Updates value in "fulltime" frame.""" self.fulltime = core.time_format( - sum([self.tdict[x][2] for x in self.tdict])) + sum([self.tdict[x]["spent_time"] for x in self.tdict])) self.fulltime_frame.config(text=self.fulltime) def descr_click(self, event): """Updates description for the task with item id of the row selected by click.""" - pos = self.listframe.taskslist.identify_row(event.y) + pos = self.table_frame.tasks_table.identify_row(event.y) if pos and pos != '#0' and not self.modifier_pressed: - self.listframe.focus_(pos) - self.update_descr(self.listframe.taskslist.focus()) + self.table_frame.focus_(pos) + self.update_descr(self.table_frame.tasks_table.focus()) def descr_up(self, event): """Updates description for the item id which is BEFORE selected.""" - item = self.listframe.taskslist.focus() - prev_item = self.listframe.taskslist.prev(item) + item = self.table_frame.tasks_table.focus() + prev_item = self.table_frame.tasks_table.prev(item) if prev_item == '': self.update_descr(item) else: @@ -799,8 +786,8 @@ class TaskSelectionWindow(Window): def descr_down(self, event): """Updates description for the item id which is AFTER selected.""" - item = self.listframe.taskslist.focus() - next_item = self.listframe.taskslist.next(item) + item = self.table_frame.tasks_table.focus() + next_item = self.table_frame.tasks_table.next(item) if next_item == '': self.update_descr(item) else: @@ -809,32 +796,31 @@ class TaskSelectionWindow(Window): def update_descr(self, item): """Filling task description frame.""" if item is None: - self.description.update_text('') + self.description_area.update_text('') elif item != '': - self.description.update_text(self.tdict[item][3]) + self.description_area.update_text(self.tdict[item]["descr"]) def select_all(self): - self.listframe.taskslist.selection_set( - self.listframe.taskslist.get_children()) + self.table_frame.tasks_table.selection_set( + self.table_frame.tasks_table.get_children()) def clear_all(self): - self.listframe.taskslist.selection_remove( - self.listframe.taskslist.get_children()) + self.table_frame.tasks_table.selection_remove( + self.table_frame.tasks_table.get_children()) def delete(self): """Remove selected tasks from the database and the table.""" - ids = [self.tdict[x][0] for x in self.listframe.taskslist.selection() - if self.tdict[x][0] - not in GLOBAL_OPTIONS["tasks"]] - items = [x for x in self.listframe.taskslist.selection() if - self.tdict[x][0] in ids] + ids = [self.tdict[x]["id"] for x in self.table_frame.tasks_table.selection() + if self.tdict[x]["id"] not in GLOBAL_OPTIONS["tasks"]] + items = [x for x in self.table_frame.tasks_table.selection() if + self.tdict[x]["id"] in ids] if ids: answer = askyesno("Warning", "Are you sure you want to delete selected tasks?", parent=self) if answer: self.db.delete_tasks(tuple(ids)) - self.listframe.taskslist.delete(*items) + self.table_frame.tasks_table.delete(*items) for item in items: self.tdict.pop(item) self.update_descr(None) @@ -842,25 +828,26 @@ class TaskSelectionWindow(Window): def edit(self): """Show task edit window.""" - item = self.listframe.taskslist.focus() + item = self.table_frame.tasks_table.focus() try: - id_name = (self.tdict[item][0], self.tdict[item][ - 1]) # Tuple: (selected_task_id, selected_task_name) + id_name = {"id": self.tdict[item]["id"], + "name": self.tdict[item]["name"]} except KeyError: pass else: task_changed = tk.IntVar() - TaskEditWindow(id_name[0], self, variable=task_changed) + TaskEditWindow(id_name["id"], self, variable=task_changed) if task_changed.get() == 1: # Reload task information from database: - new_task_info = self.db.select_task(id_name[0]) + new_task_info = self.db.select_task(id_name["id"]) # Update description: self.tdict[item] = new_task_info self.update_descr(item) # Update data in a table: - self.listframe.taskslist.item(item, values=( - new_task_info[1], core.time_format(new_task_info[2]), - new_task_info[4])) + self.table_frame.tasks_table.item( + item, values=(new_task_info["name"], + core.time_format(new_task_info["spent_total"]), + new_task_info["creation_date"])) self.update_fulltime() self.raise_window() @@ -890,7 +877,7 @@ class TaskSelectionWindow(Window): self.db.update('filter_dates', field='value', value=','.join(dates), table='options', updfiled='name') if update != self.filter_query(): - self.update_list() + self.update_table() def raise_window(self): self.grab_set() @@ -904,7 +891,7 @@ class TaskEditWindow(Window): super().__init__(master=parent, **options) # Connected with external IntVar. # Needed to avoid unnecessary operations in parent window: - self.change = variable + self.change_var = variable # Task information from database: self.task = self.db.select_task(taskid) # List of dates connected with this task: @@ -918,18 +905,18 @@ class TaskEditWindow(Window): row=0, column=0, pady=5, padx=5, sticky='w') # Frame containing task name: TaskLabel(self, width=60, height=1, bg=GLOBAL_OPTIONS["colour"], - text=self.task[1], + text=self.task["name"], anchor='w').grid(row=1, columnspan=5, sticky='ew', padx=6) tk.Frame(self, height=30).grid(row=2) elements.SimpleLabel(self, text="Description:", fontsize=10).grid( row=3, column=0, pady=5, padx=5, sticky='w') # Task description frame. Editable: - self.description = Description(self, paste_menu=True, width=60, - height=6) - self.description.config(state='normal', bg='white') - if self.task[3]: - self.description.insert(self.task[3]) - self.description.grid(row=4, columnspan=5, sticky='ewns', padx=5) + self.description_area = Description(self, paste_menu=True, width=60, + height=6) + self.description_area.config(state='normal', bg='white') + if self.task["descr"]: + self.description_area.insert(self.task["descr"]) + self.description_area.grid(row=4, columnspan=5, sticky='ewns', padx=5) # elements.SimpleLabel(self, text='Tags:').grid(row=5, column=0, pady=5, padx=5, sticky='nw') @@ -944,14 +931,15 @@ class TaskEditWindow(Window): sticky='w') # Frame containing time: TaskLabel(self, width=11, - text='{}'.format(core.time_format(self.task[2]))).grid( + text='{}'.format( + core.time_format(self.task["spent_total"]))).grid( row=6, column=1, pady=5, padx=5, sticky='w') elements.SimpleLabel(self, text='Dates:').grid(row=6, column=2, sticky='w') # Frame containing list of dates connected with current task: - datlist = Description(self, height=3, width=30) - datlist.update_text('\n'.join(dates)) - datlist.grid(row=6, column=3, rowspan=3, columnspan=2, sticky='ew', + date_list = Description(self, height=3, width=30) + date_list.update_text('\n'.join(dates)) + date_list.grid(row=6, column=3, rowspan=3, columnspan=2, sticky='ew', padx=5, pady=5) # tk.Frame(self, height=40).grid(row=9) @@ -962,7 +950,7 @@ class TaskEditWindow(Window): self.grid_columnconfigure(1, weight=1) self.grid_columnconfigure(3, weight=10) self.grid_rowconfigure(4, weight=1) - self.description.text.focus_set() + self.description_area.text.focus_set() self.prepare() def tags_edit(self): @@ -976,45 +964,46 @@ class TaskEditWindow(Window): def tags_update(self): """Tags list placing.""" # Tags list. Tags state are saved to database: - self.tags = Tagslist(self.db.tags_dict(self.task[0]), self, + self.tags = Tagslist(self.db.tags_dict(self.task["id"]), self, orientation='horizontal', width=300, height=30) self.tags.grid(row=5, column=1, columnspan=3, pady=5, padx=5, sticky='we') def update_task(self): """Update task in database.""" - taskdata = self.description.get().rstrip() - self.db.update_task(self.task[0], field='description', value=taskdata) + task_data = self.description_area.get().rstrip() + self.db.update_task(self.task["id"], field='description', + value=task_data) # Renew tags list for the task: existing_tags = [x[0] for x in self.db.find_by_clause('tasks_tags', 'task_id', - self.task[0], 'tag_id')] + self.task["id"], 'tag_id')] for item in self.tags.states_list: if item[1][0].get() == 1: if item[0] not in existing_tags: self.db.insert('tasks_tags', ('task_id', 'tag_id'), - (self.task[0], item[0])) + (self.task["id"], item[0])) else: - self.db.delete(table="tasks_tags", task_id=self.task[0], + self.db.delete(table="tasks_tags", task_id=self.task["id"], tag_id=item[0]) # Reporting to parent window that task has been changed: - if self.change: - self.change.set(1) + if self.change_var: + self.change_var.set(1) self.destroy() class TagsEditWindow(Window): - """Checkbuttons editing window..""" + """Checkbuttons editing window.""" def __init__(self, parent=None, **options): super().__init__(master=parent, **options) self.parent = parent self.addentry() self.tags_update() - self.closebutton = elements.TaskButton(self, text='Close', - command=self.destroy) - self.deletebutton = elements.TaskButton(self, text='Delete', - command=self.delete) + self.close_button = elements.TaskButton(self, text='Close', + command=self.destroy) + self.delete_button = elements.TaskButton(self, text='Delete', + command=self.delete) self.maxsize(width=500, height=500) self.window_elements_config() self.prepare() @@ -1023,8 +1012,8 @@ class TagsEditWindow(Window): """Window additional parameters configuration.""" self.title("Tags editor") self.minsize(width=300, height=300) - self.closebutton.grid(row=2, column=2, pady=5, padx=5, sticky='e') - self.deletebutton.grid(row=2, column=0, pady=5, padx=5, sticky='w') + self.close_button.grid(row=2, column=2, pady=5, padx=5, sticky='e') + self.delete_button.grid(row=2, column=0, pady=5, padx=5, sticky='w') def addentry(self): """New element addition field""" @@ -1087,7 +1076,7 @@ class TimestampsWindow(TagsEditWindow): """Window with timestamps for selected task.""" def __init__(self, taskid, current_task_time, parent=None, **options): - self.taskid = taskid + self.task_id = taskid self.current_time = current_task_time super().__init__(parent=parent, **options) @@ -1102,7 +1091,7 @@ class TimestampsWindow(TagsEditWindow): def window_elements_config(self): """Configure some window parameters.""" self.title("Timestamps: {}".format( - self.db.find_by_clause('tasks', 'id', self.taskid, 'name')[0][0])) + self.db.find_by_clause('tasks', 'id', self.task_id, 'name')[0][0])) self.minsize(width=400, height=300) elements.TaskButton(self, text="Select all", command=self.select_all).grid(row=2, column=0, @@ -1113,8 +1102,8 @@ class TimestampsWindow(TagsEditWindow): pady=5, padx=5, sticky='e') tk.Frame(self, height=40).grid(row=3) - self.closebutton.grid(row=4, column=2, pady=5, padx=5, sticky='w') - self.deletebutton.grid(row=4, column=0, pady=5, padx=5, sticky='e') + self.close_button.grid(row=4, column=2, pady=5, padx=5, sticky='w') + self.delete_button.grid(row=4, column=0, pady=5, padx=5, sticky='e') def addentry(self): """Empty method just for suppressing unnecessary element creation.""" @@ -1123,14 +1112,14 @@ class TimestampsWindow(TagsEditWindow): def tags_get(self): """Creates timestamps list.""" self.tags = Tagslist( - self.db.timestamps(self.taskid, self.current_time), self, + self.db.timestamps(self.task_id, self.current_time), self, width=400, height=300) def del_record(self, dellist): """Deletes selected timestamps.""" for x in dellist: self.db.delete(table="timestamps", timestamp=x, - task_id=self.taskid) + task_id=self.task_id) class HelpWindow(Window): @@ -1140,10 +1129,10 @@ class HelpWindow(Window): super().__init__(master=parent, **options) self.title("Help") main_frame = tk.Frame(self) - self.helptext = Description(main_frame, fontsize=13) - self.helptext.insert(text) - self.helptext.config(state='disabled') - self.helptext.grid(row=0, column=0, sticky='news') + self.help_area = Description(main_frame, fontsize=13) + self.help_area.insert(text) + self.help_area.config(state='disabled') + self.help_area.grid(row=0, column=0, sticky='news') main_frame.grid(row=0, column=0, sticky='news', padx=5, pady=5) main_frame.grid_rowconfigure(0, weight=1) main_frame.grid_columnconfigure(0, weight=1) @@ -1186,9 +1175,9 @@ class FilterWindow(Window): self.title("Filter") # IntVar instance: used to set 1 if some changes were made. # For optimization. - self.changed = variable + self.changed_var = variable # Operating mode of the filter: "AND", "OR". - self.operating_mode = tk.StringVar() + self.operating_mode_var = tk.StringVar() # Lists of stored filter parameters: stored_dates = \ self.db.find_by_clause('options', 'name', 'filter_dates', 'value')[0][ @@ -1210,12 +1199,12 @@ class FilterWindow(Window): sticky='n') elements.SimpleLabel(self, text="Tags").grid(row=0, column=1, sticky='n') - self.dateslist = Tagslist( + self.dates_list = Tagslist( [[x, [1 if x in stored_dates else 0, x]] for x in dates], self, width=200, height=300) - self.tagslist = Tagslist(tags, self, width=200, height=300) - self.dateslist.grid(row=1, column=0, pady=5, padx=5, sticky='news') - self.tagslist.grid(row=1, column=1, pady=5, padx=5, sticky='news') + self.tags_list = Tagslist(tags, self, width=200, height=300) + self.dates_list.grid(row=1, column=0, pady=5, padx=5, sticky='news') + self.tags_list.grid(row=1, column=1, pady=5, padx=5, sticky='news') elements.TaskButton(self, text="Select dates...", textwidth=15, command=self.select_dates).grid(row=2, column=0, pady=7, padx=5, @@ -1228,17 +1217,17 @@ class FilterWindow(Window): sticky='news') elements.SimpleLabel(self, text="Filter operating mode:").grid( row=5, columnspan=2, pady=5) - checkframe = tk.Frame(self) - checkframe.grid(row=7, columnspan=2, pady=5) - elements.SimpleRadiobutton(checkframe, text="AND", - variable=self.operating_mode, + check_frame = tk.Frame(self) + check_frame.grid(row=7, columnspan=2, pady=5) + elements.SimpleRadiobutton(check_frame, text="AND", + variable=self.operating_mode_var, value="AND").grid(row=0, column=0, sticky='e') - elements.SimpleRadiobutton(checkframe, text="OR", - variable=self.operating_mode, + elements.SimpleRadiobutton(check_frame, text="OR", + variable=self.operating_mode_var, value="OR").grid(row=0, column=1, sticky='w') - self.operating_mode.set( + self.operating_mode_var.set( self.db.find_by_clause(table="options", field="name", value="filter_operating_mode", searchfield="value")[0][0]) @@ -1257,11 +1246,11 @@ class FilterWindow(Window): self.prepare() def clear_dates(self): - for x in self.dateslist.states_list: + for x in self.dates_list.states_list: x[1][0].set(0) def clear_tags(self): - for x in self.tagslist.states_list: + for x in self.tags_list.states_list: x[1][0].set(0) def select_dates(self): @@ -1270,10 +1259,10 @@ class FilterWindow(Window): end_date = tk.StringVar(self) correct = tk.DoubleVar(self) CalendarWindow(self, correct, startvar=start_date, endvar=end_date, - startdate=self.dateslist.states_list[-1][0], - enddate=self.dateslist.states_list[0][0]) + startdate=self.dates_list.states_list[-1][0], + enddate=self.dates_list.states_list[0][0]) if correct.get(): - for date in self.dateslist.states_list: + for date in self.dates_list.states_list: date[1][0].set(0) if core.str_to_date(start_date.get()) <= core.str_to_date( date[0]) <= core.str_to_date(end_date.get()): @@ -1282,14 +1271,14 @@ class FilterWindow(Window): def apply_filter(self): """Create database script based on checkboxes values.""" dates = list(reversed( - [x[0] for x in self.dateslist.states_list if x[1][0].get() == 1])) + [x[0] for x in self.dates_list.states_list if x[1][0].get() == 1])) tags = list(reversed( - [x[0] for x in self.tagslist.states_list if x[1][0].get() == 1])) + [x[0] for x in self.tags_list.states_list if x[1][0].get() == 1])) if not dates and not tags: script = None - self.operating_mode.set("AND") + self.operating_mode_var.set("AND") else: - if self.operating_mode.get() == "OR": + if self.operating_mode_var.get() == "OR": script = 'SELECT id, name, total_time, description, ' \ 'creation_date FROM tasks JOIN activity ' \ 'ON activity.task_id=tasks.id JOIN tasks_tags ' \ @@ -1346,14 +1335,14 @@ class FilterWindow(Window): .join(dates), len(dates)) GLOBAL_OPTIONS["filter_dict"] = { - 'operating_mode': self.operating_mode.get(), + 'operating_mode': self.operating_mode_var.get(), 'script': script, 'tags': tags, 'dates': dates } # Reporting to parent window that filter values have been changed: - if self.changed: - self.changed.set(1) + if self.changed_var: + self.changed_var.set(1) self.destroy() @@ -1363,14 +1352,14 @@ class CalendarWindow(Window): super().__init__(master=parent, **options) self.title("Select dates") self.correct_data = correct_data - self.start = startvar - self.end = endvar + self.start_var = startvar + self.end_var = endvar self.start_date_entry = sel_cal.Datepicker( - self, datevar=self.start, + self, datevar=self.start_var, current_month=core.str_to_date(startdate).month, current_year=core.str_to_date(startdate).year) self.end_date_entry = sel_cal.Datepicker( - self, datevar=self.end, + self, datevar=self.end_var, current_month=core.str_to_date(enddate).month, current_year=core.str_to_date(enddate).year) elements.SimpleLabel(self, text="Enter first date:").grid(row=0, @@ -1400,8 +1389,8 @@ class CalendarWindow(Window): def close(self): try: - core.str_to_date(self.start.get()) - core.str_to_date(self.end.get()) + core.str_to_date(self.start_var.get()) + core.str_to_date(self.end_var.get()) except ValueError: self.correct_data.set(False) else: @@ -1552,7 +1541,7 @@ class MainMenu(tk.Menu): def options_window(self): """Open options window.""" # number of main window frames: - var = tk.IntVar(value=int(GLOBAL_OPTIONS['timers_count'])) + timers_count_var = tk.IntVar(value=int(GLOBAL_OPTIONS['timers_count'])) # 'always on top' option: ontop = tk.IntVar(value=int(GLOBAL_OPTIONS['always_on_top'])) # 'compact interface' option @@ -1561,16 +1550,16 @@ class MainMenu(tk.Menu): # 'save tasks on exit' option: save = tk.IntVar(value=int(GLOBAL_OPTIONS['preserve_tasks'])) # 'show current day in timers' option: - show_today = tk.IntVar(value=int(GLOBAL_OPTIONS['show_today'])) + show_today_var = tk.IntVar(value=int(GLOBAL_OPTIONS['show_today'])) toggle = int(GLOBAL_OPTIONS['toggle_tasks']) - toggler = tk.IntVar(value=toggle) + toggler_var = tk.IntVar(value=toggle) params = {} - accept = tk.BooleanVar() - Options(run, accept, var, ontop, compact_iface, save, show_today, - toggler) - if accept.get(): + accept_var = tk.BooleanVar() + Options(run, accept_var, timers_count_var, ontop, compact_iface, save, + show_today_var, toggler_var) + if accept_var.get(): try: - count = var.get() + count = timers_count_var.get() except tk.TclError: pass else: @@ -1592,9 +1581,9 @@ class MainMenu(tk.Menu): # apply value of 'save tasks on exit' option: params['preserve_tasks'] = save.get() # apply value of 'show current day in timers' option: - params['show_today'] = show_today.get() + params['show_today'] = show_today_var.get() # apply value of 'Allow run only one task at a time' option: - params['toggle_tasks'] = toggler.get() + params['toggle_tasks'] = toggler_var.get() # save all parameters to DB: self.change_parameter(params) # redraw taskframes if needed: @@ -1620,8 +1609,7 @@ class MainMenu(tk.Menu): showinfo("About Tasker", "Tasker {0}.\nCopyright (c) Alexey Kallistov, {1}".format( GLOBAL_OPTIONS['version'], - datetime.datetime.strftime(datetime.datetime.now(), - "%Y"))) + datetime.datetime.strftime(datetime.datetime.now(), "%Y"))) def exit(self): run.destroy() @@ -1639,18 +1627,18 @@ class Options(Window): self.counter = counter elements.SimpleLabel(self, text="Task frames in main window: ").grid( row=0, column=0, sticky='w') - counterframe = tk.Frame(self) + counter_frame = tk.Frame(self) fontsize = 9 - elements.CanvasButton(counterframe, text='<', command=self.decrease, + elements.CanvasButton(counter_frame, text='<', command=self.decrease, fontsize=fontsize, height=fontsize * 3).grid( row=0, column=0) - elements.SimpleEntry(counterframe, width=3, textvariable=counter, + elements.SimpleEntry(counter_frame, width=3, textvariable=counter, justify='center').grid(row=0, column=1, sticky='e') - elements.CanvasButton(counterframe, text='>', command=self.increase, + elements.CanvasButton(counter_frame, text='>', command=self.increase, fontsize=fontsize, height=fontsize * 3).grid( row=0, column=2) - counterframe.grid(row=0, column=1) + counter_frame.grid(row=0, column=1) tk.Frame(self, height=20).grid(row=1) elements.SimpleLabel(self, text="Always on top: ").grid(row=2, column=0, @@ -1716,16 +1704,16 @@ class ExportWindow(Window): def __init__(self, parent, data, **options): super().__init__(master=parent, **options) self.title("Export parameters") - self.task_ids = [x[0] for x in data.values()] - self.operating_mode = tk.IntVar(self) + self.task_ids = [x["id"] for x in data.values()] + self.operating_mode_var = tk.IntVar(self) elements.SimpleLabel(self, text="Export mode", fontsize=10).grid( row=0, column=0, columnspan=2, sticky='ns', pady=5) elements.SimpleRadiobutton(self, text="Task-based", - variable=self.operating_mode, value=0).grid( - row=1, column=0) + variable=self.operating_mode_var, + value=0).grid(row=1, column=0) elements.SimpleRadiobutton(self, text="Date-based", - variable=self.operating_mode, value=1).grid( - row=1, column=1) + variable=self.operating_mode_var, + value=1).grid(row=1, column=1) tk.Frame(self, height=15).grid(row=2, column=0) elements.TaskButton(self, text="Export", command=self.get_data).grid( row=3, column=0, padx=5, pady=5, sticky='ws') @@ -1740,41 +1728,10 @@ class ExportWindow(Window): def get_data(self): """Take from the database information to be exported and prepare it. All items should be strings.""" - if self.operating_mode.get() == 0: - export_data = self.db.tasks_to_export(self.task_ids) - prepared_data = [ - 'Task,Description,Dates,Time,Summarized working time'] - # Don't try to understand this 'for' loop below - # if you want to save your mind! - for key in export_data: - temp_list = [key, export_data[key][0], - export_data[key][1][0][0], - export_data[key][1][0][1], - export_data[key][2]] - prepared_data.append(','.join(temp_list)) - if len(export_data[key][1]) > 1: - for i in range(1, len(export_data[key][1])): - prepared_data.append(','.join( - ['', '', export_data[key][1][i][0], - export_data[key][1][i][1], ''])) - i += 1 + if self.operating_mode_var.get() == 0: + prepared_data = self.db.tasks_to_export(self.task_ids) else: - export_data = self.db.dates_to_export(self.task_ids) - prepared_data = [ - 'Date,Tasks,Descriptions,Time,Summarized working time'] - for key in export_data: - temp_list = [key, export_data[key][0][0][0], - export_data[key][0][0][1], - export_data[key][0][0][2], - export_data[key][1]] - prepared_data.append(','.join(temp_list)) - if len(export_data[key][0]) > 1: - for i in range(1, len(export_data[key][0])): - prepared_data.append(','.join( - ['', export_data[key][0][i][0], - export_data[key][0][i][1], - export_data[key][0][i][2], ''])) - i += 1 + prepared_data = self.db.dates_to_export(self.task_ids) self.export('\n'.join(prepared_data)) def export(self, data): @@ -1786,8 +1743,9 @@ class ExportWindow(Window): try: core.write_to_disk(filename, data) except PermissionError: - showinfo("Unable to save file", "No permission to save file here!" - "Please select another location.") + showinfo("Unable to save file", + "No permission to save file here!" + "Please select another location.") else: break else: From 49335d909e47b26f213046a0c5b226e5dfcaa011 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sun, 3 Mar 2019 13:16:47 +0300 Subject: [PATCH 09/55] Timer displaying bug fixed. --- tasker.pyw | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tasker.pyw b/tasker.pyw index a8b52b2..2fcee5f 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -158,9 +158,9 @@ class TaskFrame(tk.Frame): opacity='left') self.start_button.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: - self.timer_window = TaskLabel(self, width=10, state='disabled') - elements.big_font(self.timer_window, size=20) - self.timer_window.grid(row=3, column=1, pady=5) + self.timer_label = TaskLabel(self, width=10, state='disabled') + elements.big_font(self.timer_label, size=20) + self.timer_label.grid(row=3, column=1, pady=5) self.add_timestamp_button = elements.CanvasButton( self, text='Add\ntimestamp', @@ -279,7 +279,7 @@ class TaskFrame(tk.Frame): self.date_exists = True if self.task["spent_today"] else False # Taking current counter value from database: self.set_current_time() - self.timer_window.config(text=core.time_format(self.spent_current)) + self.timer_label.config(text=core.time_format(self.spent_current)) self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') self.start_button.config(image='resource/start_normal.png' @@ -287,7 +287,7 @@ class TaskFrame(tk.Frame): else 'resource/start_normal.pgm') self.properties_button.config(state='normal') self.clear_button.config(state='normal') - self.timer_window.config(state='normal') + self.timer_label.config(state='normal') self.add_timestamp_button.config(state='normal') self.timestamps_window_button.config(state='normal') if hasattr(self, "description_area"): @@ -329,7 +329,7 @@ class TaskFrame(tk.Frame): interval = 250 self.spent_current = time.time() - self.start_time self.task["spent_today"] = time.time() - self.start_today_timestamp - self.timer_window.config(text=core.time_format( + self.timer_label.config(text=core.time_format( self.spent_current if self.spent_current < 86400 else self.task["spent_today"])) if GLOBAL_OPTIONS["tasks"][self.task["id"]]: @@ -340,8 +340,8 @@ class TaskFrame(tk.Frame): else: counter += interval # self.timer variable becomes ID created by after(): - self.timer = self.timer_window.after(interval, self.timer_update, - counter) + self.timer = self.timer_label.after(interval, self.timer_update, + counter) else: self.timer_stop() @@ -367,7 +367,7 @@ class TaskFrame(tk.Frame): """Stop counter and save its value to database.""" if self.running: # after_cancel() stops execution of callback with given ID. - self.timer_window.after_cancel(self.timer) + self.timer_label.after_cancel(self.timer) self.spent_current = time.time() - self.start_time self.task["spent_today"] = time.time() - self.start_today_timestamp self.running = False @@ -1462,7 +1462,7 @@ class MainFrame(elements.ScrolledCanvas): if w.task: state = w.running w.timer_stop() - w.prepare_task(w.db.select_task(w.task_id)) + w.prepare_task(w.db.select_task(w.task["id"])) if state: w.timer_start() From 4e93c24dd5796783d42ebae26c050495627e7a30 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sun, 3 Mar 2019 13:32:51 +0300 Subject: [PATCH 10/55] Moved huge filter query scripts generating code to core module. --- core.py | 58 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ tasker.pyw | 58 ++---------------------------------------------------- 2 files changed, 60 insertions(+), 56 deletions(-) diff --git a/core.py b/core.py index 7f7400c..64d47f1 100644 --- a/core.py +++ b/core.py @@ -287,6 +287,64 @@ def timestamps(self, taskid, current_time): return res +def prepare_filter_query(dates, tags, mode): + """Query to get filtered tasks data from database.""" + if mode == "OR": + return 'SELECT id, name, total_time, description, ' \ + 'creation_date FROM tasks JOIN activity ' \ + 'ON activity.task_id=tasks.id JOIN tasks_tags ' \ + 'ON tasks_tags.task_id=tasks.id ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time ' \ + 'FROM activity GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id WHERE date IN ({1}) ' \ + 'OR tag_id IN ({0}) ' \ + 'GROUP BY act.task_id'. \ + format(",".join(map(str, tags)), "'%s'" % "','".join(dates)) + else: + if dates and tags: + return 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks JOIN ' \ + '(SELECT task_id, sum(spent_time) AS total_time ' \ + 'FROM activity WHERE activity.date IN ({0}) ' \ + 'GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id JOIN (SELECT tt.task_id' \ + ' FROM tasks_tags AS tt WHERE ' \ + 'tt.tag_id IN ({1}) GROUP BY tt.task_id ' \ + 'HAVING COUNT(DISTINCT tt.tag_id)={3}) AS x ON ' \ + 'x.task_id=tasks.id JOIN (SELECT act.task_id ' \ + 'FROM activity AS act WHERE act.date IN ({0}) ' \ + 'GROUP BY act.task_id HAVING ' \ + 'COUNT(DISTINCT act.date)={2}) AS y ON ' \ + 'y.task_id=tasks.id'. \ + format("'%s'" % "','".join(dates), + ",".join(map(str, tags)), len(dates), len(tags)) + elif not dates: + return 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time FROM activity GROUP BY ' \ + 'task_id) AS act ON act.task_id=tasks.id ' \ + 'JOIN (SELECT tt.task_id FROM tasks_tags ' \ + 'AS tt WHERE tt.tag_id IN ({0}) GROUP BY ' \ + 'tt.task_id HAVING ' \ + 'COUNT(DISTINCT tt.tag_id)={1}) AS x ON ' \ + 'x.task_id=tasks.id'. \ + format(",".join(map(str, tags)), len(tags)) + elif not tags: + return 'SELECT DISTINCT id, name, total_time, ' \ + 'description, creation_date FROM tasks ' \ + 'JOIN (SELECT task_id, sum(spent_time) ' \ + 'AS total_time FROM activity WHERE activity.date' \ + ' IN ({0}) GROUP BY task_id) AS act ' \ + 'ON act.task_id=tasks.id JOIN (SELECT ' \ + 'act.task_id FROM activity AS act ' \ + 'WHERE act.date IN ({0}) GROUP BY act.task_id ' \ + 'HAVING COUNT(DISTINCT act.date)={1}) AS y ' \ + 'ON y.task_id=tasks.id'.format("'%s'" % "','" + .join(dates), len(dates)) + + def check_database(): """Check if database file exists.""" if not os.path.exists(TABLE_FILE): diff --git a/tasker.pyw b/tasker.pyw index 2fcee5f..afbf999 100755 --- a/tasker.pyw +++ b/tasker.pyw @@ -1278,62 +1278,8 @@ class FilterWindow(Window): script = None self.operating_mode_var.set("AND") else: - if self.operating_mode_var.get() == "OR": - script = 'SELECT id, name, total_time, description, ' \ - 'creation_date FROM tasks JOIN activity ' \ - 'ON activity.task_id=tasks.id JOIN tasks_tags ' \ - 'ON tasks_tags.task_id=tasks.id ' \ - 'JOIN (SELECT task_id, sum(spent_time) ' \ - 'AS total_time ' \ - 'FROM activity GROUP BY task_id) AS act ' \ - 'ON act.task_id=tasks.id WHERE date IN ({1}) ' \ - 'OR tag_id IN ({0}) ' \ - 'GROUP BY act.task_id'.\ - format(",".join(map(str, tags)), "'%s'" % "','".join(dates)) - else: - if dates and tags: - script = 'SELECT DISTINCT id, name, total_time, ' \ - 'description, creation_date FROM tasks JOIN ' \ - '(SELECT task_id, sum(spent_time) AS total_time '\ - 'FROM activity WHERE activity.date IN ({0}) ' \ - 'GROUP BY task_id) AS act ' \ - 'ON act.task_id=tasks.id JOIN (SELECT tt.task_id'\ - ' FROM tasks_tags AS tt WHERE ' \ - 'tt.tag_id IN ({1}) GROUP BY tt.task_id ' \ - 'HAVING COUNT(DISTINCT tt.tag_id)={3}) AS x ON ' \ - 'x.task_id=tasks.id JOIN (SELECT act.task_id ' \ - 'FROM activity AS act WHERE act.date IN ({0}) ' \ - 'GROUP BY act.task_id HAVING ' \ - 'COUNT(DISTINCT act.date)={2}) AS y ON ' \ - 'y.task_id=tasks.id'.\ - format("'%s'" % "','".join(dates), ",".join(map(str, - tags)), - len(dates), len(tags)) - elif not dates: - script = 'SELECT DISTINCT id, name, total_time, ' \ - 'description, creation_date FROM tasks ' \ - 'JOIN (SELECT task_id, sum(spent_time) ' \ - 'AS total_time FROM activity GROUP BY ' \ - 'task_id) AS act ON act.task_id=tasks.id ' \ - 'JOIN (SELECT tt.task_id FROM tasks_tags ' \ - 'AS tt WHERE tt.tag_id IN ({0}) GROUP BY ' \ - 'tt.task_id HAVING ' \ - 'COUNT(DISTINCT tt.tag_id)={1}) AS x ON ' \ - 'x.task_id=tasks.id'.\ - format(",".join(map(str, tags)), len(tags)) - elif not tags: - script = 'SELECT DISTINCT id, name, total_time, ' \ - 'description, creation_date FROM tasks ' \ - 'JOIN (SELECT task_id, sum(spent_time) ' \ - 'AS total_time FROM activity WHERE activity.date'\ - ' IN ({0}) GROUP BY task_id) AS act ' \ - 'ON act.task_id=tasks.id JOIN (SELECT ' \ - 'act.task_id FROM activity AS act ' \ - 'WHERE act.date IN ({0}) GROUP BY act.task_id ' \ - 'HAVING COUNT(DISTINCT act.date)={1}) AS y ' \ - 'ON y.task_id=tasks.id'.format("'%s'" % "','" - .join(dates), - len(dates)) + script = core.prepare_filter_query(dates, tags, + self.operating_mode_var.get()) GLOBAL_OPTIONS["filter_dict"] = { 'operating_mode': self.operating_mode_var.get(), 'script': script, From b8e0e7b4452e62fdfb5acfae2109f7ad0ebcd120 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Tue, 12 Mar 2019 09:16:07 +0300 Subject: [PATCH 11/55] Source structure changed. --- {resource => dev}/developing_notes.txt | 0 task_generator.py => dev/task_generator.py | 3 +- dev/timer_schema.epgz | Bin 0 -> 14770 bytes core.py => src/core.py | 0 elements.py => src/elements.py | 0 {resource => src/resource}/help.txt | 0 {resource => src/resource}/magnifier.pgm | Bin {resource => src/resource}/magnifier.png | Bin {resource => src/resource}/refresh.pgm | Bin {resource => src/resource}/refresh.png | Bin {resource => src/resource}/start_disabled.pgm | 0 {resource => src/resource}/start_disabled.png | Bin {resource => src/resource}/start_normal.pgm | Bin {resource => src/resource}/start_normal.png | Bin {resource => src/resource}/stop.pgm | Bin {resource => src/resource}/stop.png | Bin sel_cal.py => src/sel_cal.py | 1 + tasker.pyw => src/tasker.pyw | 32 ++++++++++-------- 18 files changed, 20 insertions(+), 16 deletions(-) rename {resource => dev}/developing_notes.txt (100%) rename task_generator.py => dev/task_generator.py (98%) create mode 100644 dev/timer_schema.epgz rename core.py => src/core.py (100%) rename elements.py => src/elements.py (100%) rename {resource => src/resource}/help.txt (100%) rename {resource => src/resource}/magnifier.pgm (100%) rename {resource => src/resource}/magnifier.png (100%) rename {resource => src/resource}/refresh.pgm (100%) rename {resource => src/resource}/refresh.png (100%) rename {resource => src/resource}/start_disabled.pgm (100%) rename {resource => src/resource}/start_disabled.png (100%) rename {resource => src/resource}/start_normal.pgm (100%) rename {resource => src/resource}/start_normal.png (100%) rename {resource => src/resource}/stop.pgm (100%) rename {resource => src/resource}/stop.png (100%) rename sel_cal.py => src/sel_cal.py (99%) rename tasker.pyw => src/tasker.pyw (98%) diff --git a/resource/developing_notes.txt b/dev/developing_notes.txt similarity index 100% rename from resource/developing_notes.txt rename to dev/developing_notes.txt diff --git a/task_generator.py b/dev/task_generator.py similarity index 98% rename from task_generator.py rename to dev/task_generator.py index 669cd0f..b27181f 100644 --- a/task_generator.py +++ b/dev/task_generator.py @@ -8,8 +8,7 @@ ### import random -import core - +from src import core TASK_LIMIT = 50 diff --git a/dev/timer_schema.epgz b/dev/timer_schema.epgz new file mode 100644 index 0000000000000000000000000000000000000000..edf4c71b047b0f911fd57eea0fd8f42a73ad0e57 GIT binary patch literal 14770 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 8.6 - else 'resource/start_disabled.pgm', + image=os.curdir + '/resource/start_disabled.png' + if tk.TkVersion >= 8.6 + else os.curdir + '/resource/start_disabled.pgm', opacity='left') self.start_button.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: @@ -282,9 +284,9 @@ class TaskFrame(tk.Frame): self.timer_label.config(text=core.time_format(self.spent_current)) self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') - self.start_button.config(image='resource/start_normal.png' + self.start_button.config(image=os.curdir + '/resource/start_normal.png' if tk.TkVersion >= 8.6 - else 'resource/start_normal.pgm') + else os.curdir + '/resource/start_normal.pgm') self.properties_button.config(state='normal') self.clear_button.config(state='normal') self.timer_label.config(state='normal') @@ -359,8 +361,8 @@ class TaskFrame(tk.Frame): self.timer_update() self.running = True self.start_button.config( - image='resource/stop.png' if tk.TkVersion >= 8.6 - else 'resource/stop.pgm') + image=os.curdir + '/resource/stop.png' if tk.TkVersion >= 8.6 + else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") def timer_stop(self): @@ -377,8 +379,9 @@ class TaskFrame(tk.Frame): self.task["spent_total"] = self.spent_current self.update_description() self.start_button.config( - image='resource/start_normal.png' if tk.TkVersion >= 8.6 - else 'resource/start_normal.pgm') + image=os.curdir + '/resource/start_normal.png' + if tk.TkVersion >= 8.6 + else os.curdir + '/resource/start_normal.pgm') self.startstop_var.set("Start") def update_description(self): @@ -522,16 +525,17 @@ class TaskSelectionWindow(Window): sticky='w') # Search button: elements.CanvasButton(self, takefocus=False, text='Search', - image='resource/magnifier.png' + image=os.curdir + '/resource/magnifier.png' if tk.TkVersion >= 8.6 - else 'resource/magnifier.pgm', + else os.curdir + '/resource/magnifier.pgm', command=self.locate_task).grid(row=1, column=3, sticky='w', padx=5, pady=5) # Refresh button: elements.TaskButton(self, takefocus=False, - image='resource/refresh.png' - if tk.TkVersion >= 8.6 else 'resource/refresh.pgm', + image=os.curdir + '/resource/refresh.png' + if tk.TkVersion >= 8.6 + else os.curdir + '/resource/refresh.pgm', command=self.update_table).grid(row=1, column=4, sticky='e', padx=5, pady=5) @@ -554,11 +558,11 @@ class TaskSelectionWindow(Window): sticky='news') # "Select all" button: sel_button = elements.TaskButton(self, text="Select all", - command=self.select_all) + command=self.select_all) sel_button.grid(row=4, column=0, sticky='w', padx=5, pady=5) # "Clear all" button: clear_button = elements.TaskButton(self, text="Clear all", - command=self.clear_all) + command=self.clear_all) clear_button.grid(row=4, column=1, sticky='e', padx=5, pady=5) # Task properties button: self.edit_button = elements.TaskButton(self, text="Properties...", From 69e99bb93aec06576a019351b9ad2d35578fad28 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 13 Mar 2019 15:46:46 +0300 Subject: [PATCH 12/55] "TIMER_INTERVAL" added to GLOBAL_OPTIONS. --- dev/timer_schema.epgz | Bin 14770 -> 6512 bytes src/tasker.pyw | 7 +++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/dev/timer_schema.epgz b/dev/timer_schema.epgz index edf4c71b047b0f911fd57eea0fd8f42a73ad0e57..18e02e9c33f441ba2ec045c19bf799e7bb97b426 100644 GIT binary patch literal 6512 zcmV-$8IR^4iwFP!000001MOY=ZzDI7o}cHh==0(h3C<5bs*2~xc@vE;(#e-*$e<-CKV)slL z3r0`Qs%zzwqkE<;$a{pOjg&jN?4IEY+V<@I=_sDaSKjwN(~eBf8*=2~(TI;m`@g?C ze6;Tnc@V(0<8Se7{2hXE&y?2I%J|=VOm1`h$;bc8Z-)8(JJEnnG~np;<<8sD==jCK z!N-pu_xZ!{@crrjhogh<<2yb*j+qaBPQt2n+qe$h{|e)hjOzWw}{ zdNcF&?A6o;3~n|1&#TS3Mfc5XhgV7)_Bb|fuC zr51d&O^|N~hx~otnp;no1$RBoC&R<$?hjAi92il}gVPUhcDmyiqff_j@1NerV{R`?GqV3V9`SDvhadCXcrto%d@{^@ zdb-mM%F7+})w>E#8yz4jNL58piAo8ko_IO4>mud~)kB;Au zejFU>A5I2u21hfuaAqHOm%7D$Dw`|c-?{)x-Qqr#?fm}VEW`2CKRONG2V?QJCg93< zOUd*eFFp+7r5yk`VE(nZ=7@hEO{~vM&7b4xZ!fxkoE}T@ucP58 z9{#fX{Oo0kpMJUBoxdGkxb1)O;RhZKa{Skk*qL}ZJ!y0NyIddfZQFZEP~&y`HiBx^ zzd9VexiHOl^(~a+cr+Lu{VdgIjm$Fj%pL35JJy3c)_eFdJHPksRs3LPsd?Vb9ABP@ zCFxF^s!#QP?{IL$|2G^Qz35Jc?~lqC-MCy{zDb{HWp=yipUcti_l)5>pIfipQ|$<&Ib4Bt5?@<=I$xA~fa;!4udo{ir97?CoMqGN^%1;OX6j8SEo)moSoARO? zYr^zX?M~-qtrdHP^YA3L!EbLI@dd$N8kWm~?LG`n2WfEFzkE9=g^#}I%HZ_)Fn*fc zZRet9oQdK`8Hvft_`9gaZ}ZTU4j~-$?~2IV>BZPp{Yy%!MD&SN&OlJSvgC^=XE2oN zQ7TWf=}s|GqQsY&^5MF{;&H zA3l;Y6>^E8YQfhYya4SK3nmwhvo1OPbarYoimDnGARjH41Sz^0Yd``0k&!D^``F3^ z<7SxTz0*{U(#51?9m zhg?T@)33?E2GA~2s@j2rqG~ACR>u^yC7A^eV65gS*o9v9LfC0ie$hME0s&9>luthJ z#YT_Yn=lw}wNh3U$eKJ^Z;&wO!j#>UE|_Y!YyZuKw{xK??ONCPpz%nFoHgx(BY~AT z!LG07xscPLIIm~bg?d_?mJ4p?S&eyfz9H^Kp1`=1;||}`D>xt3F;no>$;rv^;{=v1 zhK=%}hTPY$2b0x*F*;e>9!!>;#Y>`m=)11bYMocz*PXL6l~4*gr`lhwv6J_`v?p}0 zrzNjDqE@n=eV>@a#WwrW=zV$c=3(jPZ#c_xH9Q)(XCc9N6W{FRufb&rXQl2-IHS5V z-MnPgIb^e1L^VDT*E^rWJ!59AEJvb60>3Fh0SYNq^tL2+mW&N|B7n#2u`!n^Wik0d z%`%n-cSR8QV$E?$HL&-nyFAtWK!XT#1$jl{ds^EEV(djDIHwp1z&G9@vSFLWQFO^h z>t{!?ee3Op@sGxv_n_t1;nCrzEkE9>{x6^2l_LLscvq5qxtehI=B_(aej9x9v`o1# zMHfeb6D3I2c+8=esMvW7G3y(d@*d2g>XaZfu{IsF3wiC^xqtc3+f(JvRBNDQRKz2Br8_|jY*Fr-mLTlzY4iEUvk0R3OVPiP#Df)_*% sYIP4DQiP;MZ|U8BMz-# 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 WkN@`A9^2#3d;C8Y2~4m6+5iA#f$2>E literal 14770 zcmV;jIZehNiwFP!000001MOW2Tus~ezj#Q7Lgwj~%nfHAN+F3JMb8+DGgcaI>Q)qG zXfR}omobq!kH`>_jCsbSNFhUpC}k+A(0`u>kH>rQ%)ft!&pqevd-u6(uf6u#-}ODK zz4c{Z|9~KfgdjOM!zBLBx$=<9U=+h(9KjF>2jwIJp>i4i4P=vfxGNbq@ROf=pUQ?* zo_~^qQ@AJv7p2}LXQ!|B8$cfff&F9uqj+Wi6BrJmundA>6b1eN*?sac$N%&G?`f*w zJe+|A$bkj8xEpENE0wMW`ubj8UV4J3!pXy3&(lTUl5rN?T^UZ$A0oJL4o+IeJ%_j{ zTm?6!gW&$=da~RFe9uUW;gk-Zf;q2c46_`9^Mt_A7>Ti%NRtT5G6+K9C_{)m(o?^( zs_~o4@;fqil_$Bqn;;r#xq{1aRozcAV=aB@Ugh=mD_dv$z4iQ~9v`jN&-jgh8~-qj z69n@2@sFb@R6YK`#nIE%plpzpjeY~yk01Y_-dp4K&AUn;;P6qS|F3EvxP|npWjx$P z>7aBHc)28I6*asZc%{9TF^y4Ge9>NTu(MYJ32_Lj;;YgCF>`PdEX$<%=;2buUEMi1 zg_F}Tg{zh^N?}!e*;}D-2C}~UE&acDK;QD2+_brZ|X~|s; zl#^Y-y@%Q}u7dQImz-?B@*X#HQh0IpjGNNH)lI<(?(SN02i`~vr$mYrF%m{#8e<@g z6(I(LSc2zin!-3#ORjWdT--&4o3oLYGoy5KnA8>4fxtgl2gWHK80uDLIL?BS;Ta_( zUAw;9R8}zwd_{9iU(-Ir`iy}wT!(^eY|2{e`j&$JR!iZbboEdUad3eLyE)i7xV&`> z64AqTKIs;pQyF>h_oXIapLC1Qsf>QO{hy>m(2eEJz{Q=WC>lZWk97+2H#H0ZtaK4L zrNEcjmpL#VQRHYsU^y^E2n<0Wni3hB!Ds?Z0X%|vzg+Z+YS(%JO!l#$4qeneui%bpj9Ii?Sh08Ed{cR`nDfN*5SP%V=^|1d~@5}euU-$la6W4!hs!^4{ zb@X%t-bL;%nR=z&>pD5O2on?zE(UTpg@+4oC@(JuZ|&HwFofvHD_^=*ex|Dc-p@ff z*+34zDJp{?26DtuE{*4k+7zuLM-UXe;CQ$4Pu{L8O}~|31K^jkxfVkpo)l;r;~5?! zc#@?ll3+Ou=R^oWIPsmiw(Qc0Q3_qbWZMn+ZzC;8OFp^mwM-!34>4RzUJiSVv?vG! zNviLvM6&8PawyAev`i3z77-G|7y*Rj7;xMSaN8upkpQiJ9q8~v`O*h2!aIunTetjO zv2srbcL&zNsqC%21J4UChH~D)-PMViTyZz8cP-2&5SoH%n$|D- z6sTM}8I*^r9a$FQ8JuDXh~qE<<0%LwD4rrn1gCkCVkMmcbTwEM-38DcIMhk2ys?Z6 zXRmMr;0eeI+_0RH+>8{z>avWxAerc!&)%22Ty`)cyU2Oa_qv$YJN-End{EJ+6i?zL zLkmDc0fT{j429B|$f6)l!!Z%`DPLwnUK<6>M2=Eg{^RUR%2v2-B)2irG z3L=1^^E4|G9Ec4B79$8!#A%dbSd4?1uWedrg%~UtdI(9=EEs_z0nAL`afpXW0cS{p z;C~v^D!X?2O2)(!Br9Md3}7>bQ2;-)43D!UgODi6lJNKQQx&}kiHHC~0|pg%7GoKb zVMH1SVFV-Kf=Gzy*ES}sTnvPzC=zBMipFt_VFci_fCLaL4MG9LG(U|oeM3Km0;i7C zkSNj=m;iB<5-C)`S%d)oiem}v3z(L!lOQVpn|)PuERDe=L$D%DVkE{xBF&?yND2fE zqXZZQU%**?z`)*VWtkGn0&at{P#Qo$H^E(S^Az3{x?Elsrx6N;Aqa(djDrb^Kq&+# zaE{{zf+N4{gbC@$rT-sC;7byA z|M6N-G5=SMPkz`IR7^TmPm1767IKMLlT(nqubf1|V8TIxoP?xT7{=w`a|*~q%Li4K z43ZOsRPtlCy}}_@?X~~bdp-5-zAWKvDsW)JQn-}MQ#EP$FN`w=Z{e^#R0nnke;A19wTU$6>$pX zS%E|e1f}2RED>A>DB`ntbNmy{0R4=*Uw~}>DwI7?w!pCrLtq#s(FBZu2q*{QI+8^h zl!88kve$)S^n(`kmky+_d|WsFuQBIkpkgnl&e0;zKmtkOG{eFF%9WRaIUJHh0HFbIBgdo* z@Sc*+WpBMdo-8L35k)Yb!y%r+P?%u_8p0XS92y3&1tR}J5**Xfg^_MQJ&yn^|C0z{ zh5#`vz?l*!hcPVAz#KzBC`!{L57hNf4>@26(?RK|Gn3}-zuy}A%X63O{Ec2&y9$qn!lFoEI1HRB3s4~N zsthVJC?dcJL-QD2_OKDh%E%X8>doUu>aB;3??t>{M7~PCfdXz8piv&g*ci%kjDU(b zN~4Svy>cv9CLeGZq zP$d3g#s2l_2UPoINl}8P1VX?B49v0^DFF52IKfD487Y9!h5bkR5yQUgsQ(wj^lefU z*Mt6|H$i;#S8(vtu$qL3@eqnhDZU)W2pr8qq{xB!3u7Q2=eZx46RdoyDe^okhzP{6 zG=O(7OYkHvaEvI57$c%Y1&pk~kA3AZQFehKc;!j@$U=^wu$;yKu&gWsS1=8Ve$3Wa zcq|O~DugBll0yLO01-QfqX;dCI3(~qC5S(8|5f-j5&$R~A~1wx84w|G42A)GKu|DA zQ3NK^75!Jyd%ESlClL^D&gK253qxdi2fisRyZn0>esNm#ojlyCd6|D{3W==uR`Yu1PF}@Y4i#tCPE;v;DIS1IEYpSkt2mF=)AWdy1vKf!T1O0JQ$_k zRwRMyfRgGY+;2%GavpAO(sR(Vr=fBqRVCx@Se=7gorC)W@QOcv4lXGrM=~M~VV3g&nm#A&PukMmd^?2$uY5$ct~8 zgG-W-$gw==L>$9df&yw|1(FvaQN#t|yXWAt6adi-gi|B}Lt$^xhfj9mn3 z0_FVAj(7p)p)apF)3#99&f3AQFW*LBv@M2FAvUlt@4j0s-&JNeuFjm4iz{kia7h zOECyQU@XUyFe6|HFh_)x@^ODqO8Fyma1lMYlG6t&|6BC@ z1Vq+6g%CImM8XJ>#&L=VF^x#U7(zl{ofWJIPY9S2SdM^cOe8?qjIt<+)4-1jD&5fKpyk|7Wl5-<@L8Ij^x1}8-l;!r{gH@^wtEP!Ym;shLc zCMmfTqJYz+If0dEAzFZ|7k&7UpAukP5D*LpMuov7&%!hclLSBwxFF?c{}6;TPO&Vo zb4o-A9wWizLofso8zyOy=1}wt%!6N+a0V)bLG+BXG%ERfmSRZ`;TejT7TO_x_C+5) zk#L6eP%t9Nf1{hFAoHi8o2v$mKP*{WG51%EO@7!GRHipqIU#YswwAc%DI|y{- zP2e1c(I`fK{Uxj`bDVJjm=lMAa7DmS29uB^!=juZh$KlwG{VsW19LF^!%@zxAiyxo)1<&4m>}Xb$FmTP0z}BN7z^P) z8Rd*%?@-PttcPOgU!-$c9Qi)gOzHs#MZtehZGJo6e7_O`zJ~mDcZAe~%LwX?+yADtK(riPb#z|2&WVKI@EN;2%v>JVA39 z3BfGQA`F2@t6)Ms8G6`G}qjlllKW0P1lkNquq?2(LvQr^=+>M~Ep$+()W zu4b#N+3F9o)gU02wg*Efgn%$XT7(QD8J5Bb5kO3a{iiiPzdTzlu%v(hjKRW~AgyW# zytT9@E5{)WNpno~u6Q3xke2qYK?IzJ85pBEnkI1$pi%)x0n`y0{Cl(27z}YZfM~pc ziWnqt6om1dK)@n^kp#wls|0C=VGxSM1SxM7qi})3D2BuZSfnAAqsj032gX0DX~6*Y zqZEaTC{JKCAEy4##i`5*ZpsS&}57uTC&l zuI0e<9LAF@gwrS{A}o#4D1xCdkMpE}(%4U9T3?l|CPknc9OVRWyr%94WrG*f9n4l2^M79Jj zl86XVMD-N-pVvpGd5N zBPOT{j+n$MIAY@c#p!>DrdO^uRHXy{wj114P4@i@NsM7S1Oa_2NlYT3Ch}Pd>_H1xl*bzJ#q{UoB4 zr8rCw0dS%@7J~p_f<#K>aVfisC1Cm=caI|p9sDQS@awZT+p9qRG>wr2LE#w10GlUK zT3}EPm^@2@NSXXM0|N&A^Q-FCs;znM1rGmg5o&}p?HeNQHb~lZArVy zkw4Midv9+|sQUGHlS61+pcq;}M4rbO5vO5RGCr7QIFf=zs_Y>h4Sl!WRF2@ld_-di5MC64 zU*ZuIC0QD0AyOcIg1l5#q%oXCXo3Kai(nxNU^Eci^B5_u@k{;4s}fiKbp}L<3PU7tK@_@y}Vab8Br+2u`< zkcA6-w=|iyaN3-Roz1%K**kooW1MXRSGSZyk9@Xz9^89*bVhncy6u>POUN(Y+hXE2 z?Afzdi<%~%X;Nn+6u5BuLjMNZ^fTHgs-*4=`7ce}+yd{EOl}|E?^x2)ogHO+!H_vMA_FA&y;^Lm!m$y9&jBV7qNV!(;rrR}xXCvKJqx^a+ zpdyp8bFv=v93JG6{aW|9$Azxyp$xW?>oO?5sD8`|?T!A*L|uP%!*S!DS$1_?lhn77 zM-5YD^J4=vI=3{7fQ~>-0%p%@v`)QdWaR47n5%aWZX{MUGW1uUt~1a6!qKM_Ciu0U z-_G>z<#}DZ9%_7Vv%QK%U+((aU8Q?XnZnES{ye4M+pE9f+0d>7q8ltdHvh^C?fe2I zGy2rL7mwpe23x+?p>>nR$u~XICq}PrghuK2W8Fq?j@O#UHg1-1cGc`F@g-Apyz?wH zG!g^W#nf%s-_Z(k)SI@;XMg&`uxaY9bUSFd?tn9`7tG(g`2JN_!yGC(tztKqe{xw$gJtU?5<7$@By?H3VQnMtnT?;XcG2D-uGx~S zes77M>4@C*L;N~L{Mn$Rtar%xyhjOJ0u5HVIGQ=S*SZ+(HXd4WA?fmz*E7!UKX&o* z#-&Ga3dg zo^>d302TrTn=&&PZPlg?I$A9o-*Wa*(^0k-1yoMmLMLC3xe+Z>w(3uvY#iQS|D|!< zk+w!JHl6IHWo|=+xeNW-k?Wa7d6Yr7|V^<1$6EqXcT7(9DR(vwr1LTA+os9U#$eTVk$ zeZHc@&W^1uJ8A_FT&j2O#cye)Mea#2qpr1Cyo`N$=l;gcqqtw6uV1cuTzCKSQ+Xps zjCk%JVHwe5_6x|jBxvw;?Csqnj=f0Hw|!B3{PwQ<&+M)r+?cgjKauQrVr)v_jr)eZ zM!&eLqPfPQwZ`3?8q-hSJ^Z@R)+g%u%$U=cx7+yClsOSsJDSYuu4x)$xhZ?fgAGe9 zpI?<*$pX$X4}&`RZf@S9h27T32`QMaZRlO6nMZC~SXeyL>)svF(9p=r+jswU8&wy| z$9Ho{)=Ssa(1wqxuq-FVk=$)<)}k$wt|tW^TCKNC$7+a1yXU_9YF}^XOkGb8&a7M5 zdlM<|tXIPzu12e-s_ti#CdLeI9X^#pJ8rmSkFAhhlgVtOQrmXuaAWL<5rOAclFxM2 zTaV~-9Zh%I3)@C{1zT$+^f4XW7>_pzIJ2kYg-2%u{Rl})JeA_Zm!;N!#_DAsE~Z!%sZ}e z=YbkZ?^&QYm8PLI;}Y^!x`7WnsdquP?4rw(vVJ1}_Nz3cG@FAh17 zeQVB~IfvF>e%|=O>yo^|yGH3uHks8xHqkaJ=Al=7iH$HVeN*G^H<_^qCrvzbWcQ34_Cgraakq{KRNH!7~7IH4#>ToN1h+bM;=~E}ty{M`&P?kS5I@a-=O2IQ^jCp*Mnv4! zpKh;!A|oTu2V2bXO?yP4dYxh2RE=itX?C}ZW7DmQE_WE`V6fxanI0qlP)Y0e`-&AS z76$|b811r4Ts?01a1Ush$B-BG2DAikr$$9amj1D2%fz}7aYrJzY>5nhaw^(f#p4% zboAK5V=oTZQq%Y|T4(sMVg6I@JSxnX?0)~nZrDe=pPu=kMDse+VvVj^w!QMQu&(d( zl)};;DFrs0L7ROq|6x7u^qDhPZN`i_Q&-hKOHtgWL14|{!@cT8EbIN^u3yiB&`arV zCzn58?AUB((j?cC;;k9I8Z@&GE$pPNy+n3%ZX%WsscJ^|=-&Oc#w@m`a4B+lf#6#@ zBQ&(RkeM)V{*ct<4i?E1YUs$04d`2Np|9G8B}_pFOy0dOZI@k_k;8}MzeMjy zIxOzGzTIJT1KIw~o4am^ja@NvME{+0S064)Pd(H;tf$M$x{CT=wFZL>2c_0ksx)s+07D3*3O( zDN=XrZez(?H@~;+1#>mf%sqeMjVWs%V_Nsq<#S9cQNS;iU@$3<8+B4GHvqW5K~uC0 zuAa}BJAeMXpko>JtwWhSqnT->{^18h&$%9G3s6u{T9ex5#w!)^H`L-&XC7aooOE69 z&KBTCsp6@7D*H9xzfdY>48&?Y_jZ)X6MWvnv|N{5P-$p`JQJF z6?rPX_AW?wrdH&)NeE3a=xWezeuwnAyU(_j9X@0;27sT0dxQKhU@Dq7J!z{kaZS~0 z>TRmkc~@-*>&&&qiMG>6^0lLz5XW}a89tC}H^o+sx1N3qNEWgq-mQ(>(@mP)J2zk9 z?P;PDb7uC`12KJuzH;5))-|nUVJP0do%1<$li-KtzrC{gwc0l`tzvB~>xvn!*+q%C z#j)8P>#8?hI_+wnZScL~b$^~d7oX)WTi7dosP{EJ6*$)Rs$opS=?h{q*SFfmAY)`Q zOP>x8)$;ngd3SDp?U~H`fZ-y5+I+(RYO5(bYn24x>^hf{dBjrxq(^SIkDbeIymVOu z_hFb+Cu2z~`<}ikN$`^$rYC!cHlrTJ&6+>(@Po@@N{6%slICAuzpCD>U483K{f{Np z8gS<9^cVIy7X!JviNf1sfbe{nfF&z(X!@Uhy#2664Tooh%%M>VN^!hK?- zgl=ni&#YIsdyJ-0j}{BHSM{8jA976BBH1-^OZ&)&{rg`v?`mN5Q0-B_-zL;a7`&p+ ztQtn!X)XH|fg5A{+AADpoP1>JHoCLFy1L2O=Q|Fqdz9JD2pfK4%RO)9?mfL#rVpKW za*?qf>3<_|#;uFc!~7R7U#ay`sbL;61S=dWo7UII^HxUCA)iyXx;}n#YeeoH6VsCk zo3%H=mq zA$^;yHE(Tfyzp*-`f4y)%|CzUVoI;Amkh20zel8am_`Fs?Xzt}_KP8@eb$oe-Oa2+ z+eJn`J?O6Dum0QM!3&He@7K^l+cD_a#B?XZuk(--W6wnkXF98B2Hq{G-OtK&hgxZK zy|MO7w7X5c9_z7bW}1G$Ce45+zmK$iU=yp{;&bb0Kte%ws!z?~DR+U_Z82#7E!1zW z<)C(u|hGv)vbjvS8bbh*3sW}ByN)uMIC`RxonTTeSJOL5=e zd)9#-`t-MxgX?9b+`qq@Y~Q%<-nhniTj#?5QK^abHnlp~b$8&DL-$>&=Mz?hwwT45 zEiTDDpkHGen5o#_Bdr|nv~g=$`Z%c9?o-iDF{8I7*Rr}`-?&{nn~lj2GE_X;X&BdZ z>!buTheAcuW%O?QMYQ^(h9_2LU%r|3y1O(Ia6>Zl!fLghnPI7YZ7%l<)g9xhaCFR$ zY%y-iPQ#QvLtdy{ah?r&Azq_Jix?&7h1t_5JR8l=Pdxdgug@({gB^DFMeK6B84+1W z*+1t5)NhfXH)e0nxEeXHOKy~Aw0P*<_24gVA(qZS1brQ#x}BFMIFH>k;nF?X-s@5B z+?iGAwmW8ndhBm3gYrTz72^LG zkzH{2?p?JSo4xjr_3D=BSVwyb@EGIi5AI`Bcp8N zm@5YBXOTKF_0I2!<4=WXg;_3Dlh^7UrykKadADbnW$FRNjYFuRXO3&FeGi{xg5XfG zP@r!OEi@`#;2uK^i|~K8M2N@x_W)O{~e4f_8vcb&Jm!wMX;XQsaDpm zbdJ6A_LCkl88!NixcOusq^EdVWSrh+!K`_Xe$OZOv+^FXZS)=w`M3+>t_KGW9GEw8 z;>0H_HugIey{e|%T4mAZu5)aYcXPh+rCI5F90!k0Zob}1ppH$-i0C)Bfp?s7vJ;d-P~{u-UpsvSGu2*KJ;Zm1P&%`mBQDh#mzy9naLU zkE|1SWZH}4{GW4b_8+JA(zpYd9(x0~-!r$z;O79mA5gzNOFbfOJT$;^Q_~e29em?E zbY5(pos*N_rAwERlH%fnvV+-|w+)T*SgAhsw;@L-b^87H-w&NWecJb=tB$VjC4uLu zbKb9&k05yuRpWx9k|BPRI>^nl9vDnbo4!Wxab!f- zpc>RT?*|)_(Cyj*0SkkkOdu2)Idw0*x;azLcH(||Lw$IY@>Y>w;=w~2whQV8O)&1= z=hVC*8nwCK(Cz6r<}~Z!9XJ|I+M`keH3~Rle>^|irIAM~(^^e-GENRX8^tHqHLq!q zK6*U1!nn2NxPDhp)yZmTHfS|m7sQ2i9gX+#qi58&o35o=w{e?i3 z9j5>J=j#{e^y(}+YGWFZl<9f+l9xCt{dufe;`*K%tF<}@o6K70;j`D+oy~h*BP?rw z&Q$Y-^5yGTgF4X%Q}gi6k>2C&hc`*;1d#82;0b~Y@vf}vvVIW|%pL>QdLys!Sw`)C z!HwIP0pM;B<}xZ_RHxRBIugOAi#oI0H#Q1Qy#H*fTX$uIE5Q~9d&k-8pLsg#Rd~aB zJv%;Ma4GZ2i{ERUpFBXLky*~_Bzx@PKD+e;5VmyGo?XfM&(<%}N|<#W{WV0}J$Jwy zZBSu&h_C`t5(H zjaAH4YNlbPsakBKOyH#Qx45@>-ARwmJiRM*imA%lS~Bwxu_0fSael<&;UR}&CtQj= zy*9;Mxee$PjKPa;8}HaS{JL+gy`rbqbH%G52P(aD>sgI`Y6Nw?_#SsvhFZ0>f=6sE z@YXaH{le>OuCawuBW`#V1Uc7O{3H!3^nokvW28a=e>uDy5|^O zcfI1Hc}8{Yd}9DzpLvhZdk`3r+^3-A*2DwS_=)b){BT}j=CI6(z$N>eqxS&jn`Gn? zxag?KsKAL}l1W;z(a1jLYU%pO3H4=tS(aU_ZGNNHv}OB#nF7YS-htfXGqeWjbdAmO za#tL9_A)W~{@I!8HQU}?cGTiZz{FD_ovdZXF+juPY#%$O&QvpQxx=`nscL`5qsB>b z*Sw?$%5dvl;<>qNw+D2+a?~NQYsr*DKB<3LKbx|-zNTuxjx;oM=~BNwA?ssIZ0cHV z_U{Mc|7;D5WYd2APTU%jYNFAdX>4X5;xcr2!8rgiG=A0WWv8RBAHL0DG>9f0ua6va z`BBcySA$OONV(-X)`M>p!8Cgc)!f(5YRR~9&YJzK8sRCvkCmBge|4tjJT5ut9dk8K z0s33jN3GXOS1?6}wRmVS>u8?=iLF2!nv`A8cF{Z)%?3c}ZR)PcJJDiXPNU%sO>b%Q z%&_4eb*4oQDHyEbV$#gaJoN4s5F=9>5wB+-wH$DC)QAy8y~B%!jRVNz}I<PC>LbsPsb7)Kw$JXkBFFsTCW;8j0bi zi?psVgBEGeX0&62*92NwE#WPa(`Tl2v0AV4e09>rq@*K89g~D#@L+>5JplyE^T$6o z*XTB*IWSwXvB{*>Q=Xn;S`jn5UaalhpshtRD|1ch65;!cb7Gd#V4c7EF?4sEBo+1D zkXir!Z8Z8>&A4=fTc98RU{PM_!kIQ!$5lqR96tO~JD<__@ zarAw9rbUNFX5prNo1NXd#oHn|&?tC;zTZ6`R1sHlB4KEVeB;27^(iSSScCp0#L9C|i=sxSYUuCFTeR=a&II@PnB&?l7oW6P zMS46vzPyz=oL%_n>it&pU(XKgZa;Ep!#Xp&`_F1WfA5*or`?|3zxMp((UtM8VU~)0 zciaa$)!cp{bGrWMh)fb#+4Xq##GKe$n`hd0oiW+*SjRKbPaY0zblz+AiznMM>)$sD zALo{x(nEJsmU3Cq1E1{+S}lI=>BO29p}l;bKI)xnf@$4r;Zum)+^(*H(5GH$_GL@rSa}20d_t(JgDN*|%@fujc0F znR{N}TDo-U{ltR>bF=3t;tPqHr+VJ;R@Ui=bZK^1b?xkCk(08ve%P*_XUIE3+C#JcM}J-+jw!|!jR<)7lsxT zb!+NAbxcOuj;Z^KH6IQJ)qp8kjU79sP=3D6-Mfj~BbV702Lvrf+}OZ9l+0?PdBXAF zCowGsok|Q_7WO*v^yyp6J1x(0o6*`#*0p>0IeOi@{}#D*Yt*F86ZVWAnm75TV^s5V zrKgQR#TxOw(mJf`)Ea~V;v}2%V-hxWa{6QY_T2g-ZeX#sje-H->k<+Yayl|Hve-!D zN(${_2wJ*`=?>1e>>;dnbQTzNeQKj@G@tY0Rx>-fJSe|9bCSUD>|2o(JP^MGZK2 zcKVs5pg*piay9V2RagX~?0F!{{xdo_cfRMowo^ewuVp~AU!M7FVt(&tI!QaHssDLO zpBgtfdF|=ggjxr}C*K~U7N_BV&UD?V3(tc)b?Q_{^LWE(odbgP5HY)r>Vv*EC0z-bS~ZoLrS>=rYKR9X9s1?$9px$7J|q&$Bp^Zi^KIH#CVZyy52; zdx~G7??21p*Tm&`VQ$@}$F;S`c^CoE7G}F(@4Y%y&eWDm$$AEHuS4$ot6#uC!}^Bk zy^6Frxj=5@_)e4`dc_?qnAdA+(VkS>%xR-BCll{w>ERt-=lALm=ACKhw+hiKos-u~Ra1|C>15#h zi}n5@dxy<4BO@g;>Yf1zlhV}=mM+*7+Us?e{`UQiBWkEFHOlO__uww3zf1bE31Laf zp;IP^Q~b`@PV9S>v_-FtengU!Tq^VAyx;L%vNH$J{- z6yy7PZ$|F(qf2tkt(Vr%aHV&=n)fiX#CU9Re$yMisD0SQ%`Nsazf9Yf7k52`?pz!d z*KXTT$?LBtmj&+W6|+w5z>Sx$4EJ0mr#uO;^P9+zGVECFzoVz!!Brhf-A*V}UB-?I zUAThQN!+~J!7$D1@yv#PuaCs*X7bxLnlN#82QS>47ZnvwIGBW6D!R`%|FvP(;SrnW zcTl#?C`wRUJt=3cpBjI_bY^212w|a!d&1ayPWW9b``4xy6AFA7JLutaC&Qh7&m0R& z^H1fy@{*@_j2+RqnVIa$A;Ep>gy5$uZsf%o)9xns_84pA78dPLWIGxbx_B4NO)R}P z@<6Szos-t|JKOtds!UeXQpyDw3!HUiSnYYD z7WHVV-zy`>)a}mgHX^g@bkaJeBJ(NCEpSeMkvQ^M7rsZ*L~f4m&U#Dd+fHc})}>8O zNzAKPrKOJ>Uu`m9&+?Wwdu-BqA4S|w2k#>(dNp8NkM4Nu*E2-BEwt}J?`I3bC*`lb zdn9s!{TeOfVA&3b%wFBQ+~G$ST|2-0ly%dK`Y~tM+T^v5-0GWJyx7k#NB_w71@>|? z)0Q>HpS>Cu}J8v0O zv$r5@xSp5go3?(j+wG8(=O25I-8{L(&(`>$Px5upx<{%WQ%%=hY&y7C$rUxf;_w@) z9_>t(BaZjX?QdFrRC!?0uk;ghyL*kPbs Date: Wed, 13 Mar 2019 17:14:51 +0300 Subject: [PATCH 13/55] Time counter reworked. --- dev/timer_schema.epgz | Bin 6512 -> 20119 bytes src/core.py | 4 ++-- src/tasker.pyw | 29 +++++++++++------------------ 3 files changed, 13 insertions(+), 20 deletions(-) diff --git a/dev/timer_schema.epgz b/dev/timer_schema.epgz index 18e02e9c33f441ba2ec045c19bf799e7bb97b426..a604c871b1a6d8cfd00b9d34e6067c14fa167524 100644 GIT binary patch literal 20119 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�qP$Ys=0!JYvhLa5XoqTX{fgpJiAw>!m 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 KruFy$A^`xl+nCA# literal 6512 zcmV-$8IR^4iwFP!000001MOY=ZzDI7o}cHh==0(h3C<5bs*2~xc@vE;(#e-*$e<-CKV)slL z3r0`Qs%zzwqkE<;$a{pOjg&jN?4IEY+V<@I=_sDaSKjwN(~eBf8*=2~(TI;m`@g?C ze6;Tnc@V(0<8Se7{2hXE&y?2I%J|=VOm1`h$;bc8Z-)8(JJEnnG~np;<<8sD==jCK z!N-pu_xZ!{@crrjhogh<<2yb*j+qaBPQt2n+qe$h{|e)hjOzWw}{ zdNcF&?A6o;3~n|1&#TS3Mfc5XhgV7)_Bb|fuC zr51d&O^|N~hx~otnp;no1$RBoC&R<$?hjAi92il}gVPUhcDmyiqff_j@1NerV{R`?GqV3V9`SDvhadCXcrto%d@{^@ zdb-mM%F7+})w>E#8yz4jNL58piAo8ko_IO4>mud~)kB;Au zejFU>A5I2u21hfuaAqHOm%7D$Dw`|c-?{)x-Qqr#?fm}VEW`2CKRONG2V?QJCg93< zOUd*eFFp+7r5yk`VE(nZ=7@hEO{~vM&7b4xZ!fxkoE}T@ucP58 z9{#fX{Oo0kpMJUBoxdGkxb1)O;RhZKa{Skk*qL}ZJ!y0NyIddfZQFZEP~&y`HiBx^ zzd9VexiHOl^(~a+cr+Lu{VdgIjm$Fj%pL35JJy3c)_eFdJHPksRs3LPsd?Vb9ABP@ zCFxF^s!#QP?{IL$|2G^Qz35Jc?~lqC-MCy{zDb{HWp=yipUcti_l)5>pIfipQ|$<&Ib4Bt5?@<=I$xA~fa;!4udo{ir97?CoMqGN^%1;OX6j8SEo)moSoARO? zYr^zX?M~-qtrdHP^YA3L!EbLI@dd$N8kWm~?LG`n2WfEFzkE9=g^#}I%HZ_)Fn*fc zZRet9oQdK`8Hvft_`9gaZ}ZTU4j~-$?~2IV>BZPp{Yy%!MD&SN&OlJSvgC^=XE2oN zQ7TWf=}s|GqQsY&^5MF{;&H zA3l;Y6>^E8YQfhYya4SK3nmwhvo1OPbarYoimDnGARjH41Sz^0Yd``0k&!D^``F3^ z<7SxTz0*{U(#51?9m zhg?T@)33?E2GA~2s@j2rqG~ACR>u^yC7A^eV65gS*o9v9LfC0ie$hME0s&9>luthJ z#YT_Yn=lw}wNh3U$eKJ^Z;&wO!j#>UE|_Y!YyZuKw{xK??ONCPpz%nFoHgx(BY~AT z!LG07xscPLIIm~bg?d_?mJ4p?S&eyfz9H^Kp1`=1;||}`D>xt3F;no>$;rv^;{=v1 zhK=%}hTPY$2b0x*F*;e>9!!>;#Y>`m=)11bYMocz*PXL6l~4*gr`lhwv6J_`v?p}0 zrzNjDqE@n=eV>@a#WwrW=zV$c=3(jPZ#c_xH9Q)(XCc9N6W{FRufb&rXQl2-IHS5V z-MnPgIb^e1L^VDT*E^rWJ!59AEJvb60>3Fh0SYNq^tL2+mW&N|B7n#2u`!n^Wik0d z%`%n-cSR8QV$E?$HL&-nyFAtWK!XT#1$jl{ds^EEV(djDIHwp1z&G9@vSFLWQFO^h z>t{!?ee3Op@sGxv_n_t1;nCrzEkE9>{x6^2l_LLscvq5qxtehI=B_(aej9x9v`o1# zMHfeb6D3I2c+8=esMvW7G3y(d@*d2g>XaZfu{IsF3wiC^xqtc3+f(JvRBNDQRKz2Br8_|jY*Fr-mLTlzY4iEUvk0R3OVPiP#Df)_*% sYIP4DQiP;MZ|U8BMz-# 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 WkN@`A9^2#3d;C8Y2~4m6+5iA#f$2>E diff --git a/src/core.py b/src/core.py index 64d47f1..a87f81f 100644 --- a/src/core.py +++ b/src/core.py @@ -107,8 +107,8 @@ def insert_task(self, name): return task_id def update(self, field_id, field, value, table="tasks", updfiled="id"): - """Updates given field in given table with given id - using given value :) """ + """Updates provided field in provided table with provided id + using provided value """ self.exec_script( "UPDATE {0} SET {1}=? WHERE {3}='{2}'".format(table, field, field_id, updfiled), diff --git a/src/tasker.pyw b/src/tasker.pyw index b0e23f0..1356df9 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -212,12 +212,12 @@ class TaskFrame(tk.Frame): def timestamps_window(self): """Timestamps window opening.""" - TimestampsWindow(self.task["id"], self.spent_current, run) + TimestampsWindow(self.task["id"], self.task["spent_total"], run) def add_timestamp(self): """Adding timestamp to database.""" self.db.insert('timestamps', ('task_id', 'timestamp'), - (self.task["id"], self.spent_current)) + (self.task["id"], self.task["spent_total"])) showinfo("Timestamp added", "Timestamp added.") def start_stop(self): @@ -309,8 +309,6 @@ class TaskFrame(tk.Frame): if current_date != self.current_date: self.current_date = current_date self.date_exists = False - self.task["spent_today"] = self.task["spent_today"] - self.timestamp - self.start_today_timestamp = time.time() - self.task["spent_today"] self.task_update() def task_update(self): @@ -322,15 +320,14 @@ class TaskFrame(tk.Frame): self.date_exists = True else: self.db.update_task(self.task["id"], value=self.task["spent_today"]) - self.timestamp = self.task["spent_today"] def timer_update(self, counter=0): """Renewal of the counter.""" - # Time interval in milliseconds - # before next iteration of recursion: - interval = GLOBAL_OPTIONS["TIMER_INTERVAL"] - self.spent_current = time.time() - self.start_time - self.task["spent_today"] = time.time() - self.start_today_timestamp + spent = time.time() - self.start_time + self.spent_current += spent + self.task["spent_today"] += spent + self.task["spent_total"] += spent + self.start_time = time.time() self.timer_label.config(text=core.time_format( self.spent_current if self.spent_current < 86400 else self.task["spent_today"])) @@ -340,10 +337,10 @@ class TaskFrame(tk.Frame): self.check_date() counter = 0 else: - counter += interval + counter += GLOBAL_OPTIONS["TIMER_INTERVAL"] # self.timer variable becomes ID created by after(): - self.timer = self.timer_label.after(interval, self.timer_update, - counter) + self.timer = self.timer_label.after( + GLOBAL_OPTIONS["TIMER_INTERVAL"], self.timer_update, counter) else: self.timer_stop() @@ -355,9 +352,8 @@ class TaskFrame(tk.Frame): GLOBAL_OPTIONS["tasks"][key] = False GLOBAL_OPTIONS["tasks"][self.task["id"]] = True # Setting current counter value: - self.start_time = time.time() - self.spent_current + self.start_time = time.time() - self.task["spent_today"] # This value is used to add record to database: - self.start_today_timestamp = time.time() - self.task["spent_today"] self.timer_update() self.running = True self.start_button.config( @@ -370,13 +366,10 @@ class TaskFrame(tk.Frame): if self.running: # after_cancel() stops execution of callback with given ID. self.timer_label.after_cancel(self.timer) - self.spent_current = time.time() - self.start_time - self.task["spent_today"] = time.time() - self.start_today_timestamp self.running = False GLOBAL_OPTIONS["tasks"][self.task["id"]] = False # Writing value into database: self.check_date() - self.task["spent_total"] = self.spent_current self.update_description() self.start_button.config( image=os.curdir + '/resource/start_normal.png' From 9b1d33d3ed6fc29644404fe355340791cfc7934d Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 14 Mar 2019 11:40:55 +0300 Subject: [PATCH 14/55] Timer fix. --- dev/timer_schema.epgz | Bin 20119 -> 16024 bytes src/core.py | 6 ++-- src/tasker.pyw | 76 +++++++++++++++++------------------------- 3 files changed, 34 insertions(+), 48 deletions(-) diff --git a/dev/timer_schema.epgz b/dev/timer_schema.epgz index a604c871b1a6d8cfd00b9d34e6067c14fa167524..806041c3ee3adb39611a14822c753454810f6099 100644 GIT binary patch literal 16024 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 literal 20119 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�qP$Ys=0!JYvhLa5XoqTX{fgpJiAw>!m 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 KruFy$A^`xl+nCA# diff --git a/src/core.py b/src/core.py index a87f81f..95ce968 100644 --- a/src/core.py +++ b/src/core.py @@ -276,13 +276,13 @@ def simple_dateslist(self): 'SELECT DISTINCT date FROM activity ORDER BY date DESC') return [x[0] for x in self.cur.fetchall()] - def timestamps(self, taskid, current_time): + def timestamps(self, taskid, task_total_spent_time): """Returns timestamps list in same format as simple_tagslist().""" timestamps = self.find_by_clause('timestamps', 'task_id', taskid, 'timestamp') res = [[x[0], [0, '{0}; {1} spent since that moment'.format( - time_format(x[0]), time_format(current_time - x[0]))]] for x in - timestamps] + time_format(x[0]), time_format(task_total_spent_time - x[0]))]] + for x in timestamps] res.reverse() return res diff --git a/src/tasker.pyw b/src/tasker.pyw index 1356df9..db62c10 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -179,7 +179,7 @@ class TaskFrame(tk.Frame): self.timestamps_window_button.grid(row=3, column=3, sticky='wsn', padx=5) self.properties_button = elements.TaskButton( - self, text="Properties...", textwidth=9, state='disabled', + self, text="Properties...", textwidth=11, state='disabled', command=self.properties_window) self.properties_button.grid(row=3, column=4, sticky='e', padx=5) # Clear frame button: @@ -188,7 +188,6 @@ class TaskFrame(tk.Frame): command=self.clear) self.clear_button.grid(row=3, column=5, sticky='e', padx=5) self.running = False - self.timestamp = 0 def normal_interface(self): """Creates elements which are visible only in full interface mode.""" @@ -277,16 +276,14 @@ class TaskFrame(tk.Frame): GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task self.current_date = core.date_format(datetime.datetime.now()) - # Set current time, just for this day: - self.date_exists = True if self.task["spent_today"] else False - # Taking current counter value from database: + # Set current time, just for this session: self.set_current_time() self.timer_label.config(text=core.time_format(self.spent_current)) self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') self.start_button.config(image=os.curdir + '/resource/start_normal.png' - if tk.TkVersion >= 8.6 - else os.curdir + '/resource/start_normal.pgm') + if tk.TkVersion >= 8.6 + else os.curdir + '/resource/start_normal.pgm') self.properties_button.config(state='normal') self.clear_button.config(state='normal') self.timer_label.config(state='normal') @@ -302,22 +299,15 @@ class TaskFrame(tk.Frame): else: self.spent_current = self.task["spent_total"] - def check_date(self): - """Used to check if date has been changed - since last timer value save.""" + def task_update(self): + """Updates time in the database.""" current_date = core.date_format(datetime.datetime.now()) if current_date != self.current_date: self.current_date = current_date - self.date_exists = False - self.task_update() - - def task_update(self): - """Updates time in the database.""" - if not self.date_exists: self.db.insert("activity", ("date", "task_id", "spent_time"), (self.current_date, self.task["id"], self.task["spent_today"])) - self.date_exists = True + self.task["spent_today"] = 0 else: self.db.update_task(self.task["id"], value=self.task["spent_today"]) @@ -331,18 +321,15 @@ class TaskFrame(tk.Frame): self.timer_label.config(text=core.time_format( self.spent_current if self.spent_current < 86400 else self.task["spent_today"])) - if GLOBAL_OPTIONS["tasks"][self.task["id"]]: - # Every n seconds counter value is saved in database: - if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: - self.check_date() - counter = 0 - else: - counter += GLOBAL_OPTIONS["TIMER_INTERVAL"] - # self.timer variable becomes ID created by after(): - self.timer = self.timer_label.after( - GLOBAL_OPTIONS["TIMER_INTERVAL"], self.timer_update, counter) + # Every n seconds counter value is saved in database: + if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: + self.task_update() + counter = 0 else: - self.timer_stop() + counter += GLOBAL_OPTIONS["TIMER_INTERVAL"] + # self.timer variable becomes ID created by after(): + self.timer = self.timer_label.after( + GLOBAL_OPTIONS["TIMER_INTERVAL"], self.timer_update, counter) def timer_start(self): """Counter start.""" @@ -351,15 +338,14 @@ class TaskFrame(tk.Frame): for key in GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"][key] = False GLOBAL_OPTIONS["tasks"][self.task["id"]] = True - # Setting current counter value: - self.start_time = time.time() - self.task["spent_today"] - # This value is used to add record to database: - self.timer_update() + # Setting current timestamp: + self.start_time = time.time() self.running = True self.start_button.config( image=os.curdir + '/resource/stop.png' if tk.TkVersion >= 8.6 else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") + self.timer_update() def timer_stop(self): """Stop counter and save its value to database.""" @@ -369,7 +355,7 @@ class TaskFrame(tk.Frame): self.running = False GLOBAL_OPTIONS["tasks"][self.task["id"]] = False # Writing value into database: - self.check_date() + self.task_update() self.update_description() self.start_button.config( image=os.curdir + '/resource/start_normal.png' @@ -1072,9 +1058,9 @@ class TagsEditWindow(Window): class TimestampsWindow(TagsEditWindow): """Window with timestamps for selected task.""" - def __init__(self, taskid, current_task_time, parent=None, **options): + def __init__(self, taskid, task_time, parent=None, **options): self.task_id = taskid - self.current_time = current_task_time + self.task_time = task_time super().__init__(parent=parent, **options) def select_all(self): @@ -1109,7 +1095,7 @@ class TimestampsWindow(TagsEditWindow): def tags_get(self): """Creates timestamps list.""" self.tags = Tagslist( - self.db.timestamps(self.task_id, self.current_time), self, + self.db.timestamps(self.task_id, self.task_time), self, width=400, height=300) def del_record(self, dellist): @@ -1749,11 +1735,11 @@ class MainWindow(tk.Tk): command=self.taskframes.clear_all) self.add_clear_button.grid(row=2, column=0, sticky='wsn', pady=5, padx=5) - self.add_pause_button = elements.TaskButton(self, text="Pause all", - command=self.pause_all, - textwidth=10) - self.add_pause_button.grid(row=2, column=3, sticky='snw', pady=5, - padx=5) + self.pause_button = elements.TaskButton(self, text="Pause all", + command=self.pause_all, + textwidth=10) + self.pause_button.grid(row=2, column=3, sticky='snw', pady=5, + padx=5) self.add_quit_button = elements.TaskButton(self, text="Quit", command=self.destroy) self.add_quit_button.grid(row=2, column=4, sticky='sne', pady=5, @@ -1765,19 +1751,19 @@ class MainWindow(tk.Tk): """Destroy all additional interface elements.""" for widget in (self.add_frame, self.add_stop_button, self.add_clear_button, self.add_quit_button, - self.add_pause_button): + self.pause_button): widget.destroy() self.taskframes.change_interface('small') def pause_all(self): if self.paused: if GLOBAL_OPTIONS["compact_interface"] == "0": - self.add_pause_button.config(text="Pause all") + self.pause_button.config(text="Pause all", anchor="we") self.taskframes.resume_all() self.paused = False else: if GLOBAL_OPTIONS["compact_interface"] == "0": - self.add_pause_button.config(text="Resume all") + self.pause_button.config(text="Resume all", anchor="we") self.taskframes.pause_all() self.paused = True @@ -1786,7 +1772,7 @@ class MainWindow(tk.Tk): self.taskframes.stop_all() self.paused = False if GLOBAL_OPTIONS["compact_interface"] == "0": - self.add_pause_button.config(text="Pause all") + self.pause_button.config(text="Pause all") def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") From 47f093d32fc9e3b22879e1308f5e7f76b1484eac Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 14 Mar 2019 12:24:02 +0300 Subject: [PATCH 15/55] Issue 30 fix. --- src/tasker.pyw | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index db62c10..5990480 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -276,9 +276,7 @@ class TaskFrame(tk.Frame): GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task self.current_date = core.date_format(datetime.datetime.now()) - # Set current time, just for this session: - self.set_current_time() - self.timer_label.config(text=core.time_format(self.spent_current)) + self.timer_label.config(text=core.time_format(self.get_current_time())) self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') self.start_button.config(image=os.curdir + '/resource/start_normal.png' @@ -292,12 +290,12 @@ class TaskFrame(tk.Frame): if hasattr(self, "description_area"): self.description_area.update_text(self.task["descr"]) - def set_current_time(self): - """Set current_time depending on time displaying options value.""" + def get_current_time(self): + """Return current_time depending on time displaying options value.""" if int(GLOBAL_OPTIONS["show_today"]): - self.spent_current = self.task["spent_today"] + return self.task["spent_today"] else: - self.spent_current = self.task["spent_total"] + return self.task["spent_total"] def task_update(self): """Updates time in the database.""" @@ -314,12 +312,12 @@ class TaskFrame(tk.Frame): def timer_update(self, counter=0): """Renewal of the counter.""" spent = time.time() - self.start_time - self.spent_current += spent self.task["spent_today"] += spent self.task["spent_total"] += spent self.start_time = time.time() + current_spent = self.get_current_time() self.timer_label.config(text=core.time_format( - self.spent_current if self.spent_current < 86400 + current_spent if current_spent < 86400 else self.task["spent_today"])) # Every n seconds counter value is saved in database: if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: From 8f38ee511ccb6ade9243d3555210064002dacc4b Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 14 Mar 2019 12:55:40 +0300 Subject: [PATCH 16/55] 'Pause all' button look fix. --- src/tasker.pyw | 73 +++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 5990480..8f83479 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -135,8 +135,7 @@ class TaskFrame(tk.Frame): def create_content(self): """Creates all window elements.""" - self.startstop_var = tk.StringVar() # Text on "Start" button. - self.startstop_var.set("Start") + self.startstop_var = tk.StringVar(value="Start") # Text on "Start" button. # Fake name of running task (which actually is not selected yet). self.task = None if GLOBAL_OPTIONS["compact_interface"] == "0": @@ -211,7 +210,8 @@ class TaskFrame(tk.Frame): def timestamps_window(self): """Timestamps window opening.""" - TimestampsWindow(self.task["id"], self.task["spent_total"], run) + TimestampsWindow(self.task["id"], self.task["spent_total"], + ROOT_WINDOW) def add_timestamp(self): """Adding timestamp to database.""" @@ -230,7 +230,8 @@ class TaskFrame(tk.Frame): def properties_window(self): """Task properties window.""" edited_var = tk.IntVar() - TaskEditWindow(self.task["id"], parent=run, variable=edited_var) + TaskEditWindow(self.task["id"], parent=ROOT_WINDOW, + variable=edited_var) if edited_var.get() == 1: self.update_description() @@ -245,7 +246,7 @@ class TaskFrame(tk.Frame): def name_dialogue(self): """Task selection window.""" var = tk.IntVar() - TaskSelectionWindow(run, taskvar=var) + TaskSelectionWindow(ROOT_WINDOW, taskvar=var) if var.get(): self.get_task_name(var.get()) @@ -1458,7 +1459,7 @@ class MainMenu(tk.Menu): self.add_cascade(label="Main menu", menu=file, underline=0) helpmenu = tk.Menu(self, tearoff=0) helpmenu.add_command(label="Help...", - command=lambda: helpwindow(parent=run, + command=lambda: helpwindow(parent=ROOT_WINDOW, text=core.HELP_TEXT)) helpmenu.add_command(label="About...", command=self.aboutwindow) elements.big_font(helpmenu, 10) @@ -1482,8 +1483,8 @@ class MainMenu(tk.Menu): toggler_var = tk.IntVar(value=toggle) params = {} accept_var = tk.BooleanVar() - Options(run, accept_var, timers_count_var, ontop, compact_iface, save, - show_today_var, toggler_var) + Options(ROOT_WINDOW, accept_var, timers_count_var, ontop, + compact_iface, save, show_today_var, toggler_var) if accept_var.get(): try: count = timers_count_var.get() @@ -1497,14 +1498,14 @@ class MainMenu(tk.Menu): params['timers_count'] = count # apply value of 'always on top' option: params['always_on_top'] = ontop.get() - run.wm_attributes("-topmost", ontop.get()) + ROOT_WINDOW.wm_attributes("-topmost", ontop.get()) # apply value of 'compact interface' option: params['compact_interface'] = compact_iface.get() if compact != compact_iface.get(): if compact_iface.get() == 0: - run.full_interface() + ROOT_WINDOW.full_interface() elif compact_iface.get() == 1: - run.small_interface() + ROOT_WINDOW.small_interface() # apply value of 'save tasks on exit' option: params['preserve_tasks'] = save.get() # apply value of 'show current day in timers' option: @@ -1514,13 +1515,13 @@ class MainMenu(tk.Menu): # save all parameters to DB: self.change_parameter(params) # redraw taskframes if needed: - run.taskframes.fill() - run.taskframes.frames_refill() + ROOT_WINDOW.taskframes.fill() + ROOT_WINDOW.taskframes.frames_refill() # Stop all tasks if exclusive run method has been enabled: if int(GLOBAL_OPTIONS["toggle_tasks"]) and int( GLOBAL_OPTIONS["toggle_tasks"]) != toggle: - run.stopall() - run.lift() + ROOT_WINDOW.stop_all() + ROOT_WINDOW.lift() def change_parameter(self, paramdict): """Change option in the database.""" @@ -1536,10 +1537,11 @@ class MainMenu(tk.Menu): showinfo("About Tasker", "Tasker {0}.\nCopyright (c) Alexey Kallistov, {1}".format( GLOBAL_OPTIONS['version'], - datetime.datetime.strftime(datetime.datetime.now(), "%Y"))) + datetime.datetime.strftime(datetime.datetime.now(), + "%Y"))) def exit(self): - run.destroy() + ROOT_WINDOW.destroy() class Options(Window): @@ -1693,6 +1695,7 @@ class MainWindow(tk.Tk): self.taskframes = MainFrame(self) # Main window content. self.taskframes.grid(row=0, columnspan=5) self.bind("", self.taskframes.reconf_canvas) + self.paused = False if GLOBAL_OPTIONS["compact_interface"] == "0": self.full_interface(True) self.grid_rowconfigure(0, weight=1) @@ -1707,12 +1710,11 @@ class MainWindow(tk.Tk): if GLOBAL_OPTIONS['always_on_top'] == '1': self.wm_attributes("-topmost", 1) self.bind("", self.hotkeys) - self.paused = False def hotkeys(self, event): """Execute corresponding actions for hotkeys.""" if event.keysym in ('Cyrillic_yeru', 'Cyrillic_YERU', 's', 'S'): - self.stopall() + self.stop_all() elif event.keysym in ('Cyrillic_es', 'Cyrillic_ES', 'c', 'C'): self.taskframes.clear_all() elif event.keysym in ( @@ -1725,15 +1727,18 @@ class MainWindow(tk.Tk): """Create elements which are displayed in full interface mode.""" self.add_frame = tk.Frame(self, height=35) self.add_frame.grid(row=1, columnspan=5) - self.add_stop_button = elements.TaskButton(self, text="Stop all", - command=self.stopall) - self.add_stop_button.grid(row=2, column=2, sticky='sn', pady=5, padx=5) - self.add_clear_button = elements.TaskButton( + self.stop_button = elements.TaskButton(self, text="Stop all", + command=self.stop_all) + self.stop_button.grid(row=2, column=2, sticky='sn', pady=5, padx=5) + self.clear_button = elements.TaskButton( self, text="Clear all", command=self.taskframes.clear_all) - self.add_clear_button.grid(row=2, column=0, sticky='wsn', pady=5, - padx=5) - self.pause_button = elements.TaskButton(self, text="Pause all", + self.clear_button.grid(row=2, column=0, sticky='wsn', pady=5, + padx=5) + self.pause_all_var = tk.StringVar(value="Resume all" if self.paused + else "Pause all") + self.pause_button = elements.TaskButton(self, + variable=self.pause_all_var, command=self.pause_all, textwidth=10) self.pause_button.grid(row=2, column=3, sticky='snw', pady=5, @@ -1747,8 +1752,8 @@ class MainWindow(tk.Tk): def small_interface(self): """Destroy all additional interface elements.""" - for widget in (self.add_frame, self.add_stop_button, - self.add_clear_button, self.add_quit_button, + for widget in (self.add_frame, self.stop_button, + self.clear_button, self.add_quit_button, self.pause_button): widget.destroy() self.taskframes.change_interface('small') @@ -1756,21 +1761,21 @@ class MainWindow(tk.Tk): def pause_all(self): if self.paused: if GLOBAL_OPTIONS["compact_interface"] == "0": - self.pause_button.config(text="Pause all", anchor="we") + self.pause_all_var.set("Pause all") self.taskframes.resume_all() self.paused = False else: if GLOBAL_OPTIONS["compact_interface"] == "0": - self.pause_button.config(text="Resume all", anchor="we") + self.pause_all_var.set("Resume all") self.taskframes.pause_all() self.paused = True - def stopall(self): + def stop_all(self): """Stop all running timers.""" self.taskframes.stop_all() self.paused = False if GLOBAL_OPTIONS["compact_interface"] == "0": - self.pause_button.config(text="Pause all") + self.pause_all_var.set("Pause all") def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") @@ -1853,5 +1858,5 @@ if __name__ == "__main__": "SAVE_INTERVAL": SAVE_INTERVAL, "paused": set()}) # Main window: - run = MainWindow() - run.mainloop() + ROOT_WINDOW = MainWindow() + ROOT_WINDOW.mainloop() From 5af0bc60727dd73e62951f62277f82be5fc9c820 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 15 Mar 2019 18:20:20 +0300 Subject: [PATCH 17/55] Changelog update. Version bump. --- changelog.txt | 9 +++++++-- src/core.py | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9d8ee71..f736af5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -2,10 +2,15 @@ v.1.5.1 _Добавлено 1. Кнопка Pause all/Resume all. _Исправления ошибок: -1. Сообщение об ошибке, если нет прав на запись в директорию, выбранную для -экспорта csv. +1. Ошибка, если нет прав на запись в директорию, выбранную для экспорта csv. 2. Сбой при попытке экспортировать в csv список длиной в одну задачу. 3. Не работала фильтрация по датам. +4. ВременнЫе метки добавлялись и отображались относительно текущего дня, + а не всего потраченного на задачу времени, если в настройках отображения + было включено "Отображать время только для текущего дня". +5. Визуальный таймер не сбрасывался в 0:00, если включено + "Отображать время только для текущего дня". + v.1.5 1. Опция: отображать только сегодняшний день в таймерах. diff --git a/src/core.py b/src/core.py index 95ce968..cc952df 100644 --- a/src/core.py +++ b/src/core.py @@ -457,7 +457,7 @@ def apply_script(scripts_list, db_connection): INSERT INTO options VALUES ('toggle_tasks', '0'); INSERT INTO options VALUES ('tasks', ''); INSERT INTO options VALUES ('compact_interface', '0'); - INSERT INTO options VALUES ('version', '1.5'); + INSERT INTO options VALUES ('version', '1.5.1'); INSERT INTO options VALUES ('install_time', datetime('now')); """ # PATCH_SCRIPTS = { From 03b9e626aefd112ab36cc80cfc33fc6828ab3738 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 16 Mar 2019 09:29:05 +0300 Subject: [PATCH 18/55] Creation date format changed. --- src/core.py | 23 ++++++++++++++++------- src/tasker.pyw | 3 ++- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/core.py b/src/core.py index cc952df..d50588f 100644 --- a/src/core.py +++ b/src/core.py @@ -7,6 +7,11 @@ import time +DATE_TEMPLATE = "%Y-%m-%d" +DATE_STORAGE_TEMPLATE = "%Y-%m-%dT%H:%M:%S.%f" +DATE_FULL_HUMAN_READABLE_TEMPLATE = "%Y-%m-%d %H:%M:%S" + + class DbErrors(Exception): """Base class for errors in database operations.""" pass @@ -93,7 +98,7 @@ def insert(self, table, fields, values): def insert_task(self, name): """Insert task into database.""" - date = date_format(datetime.datetime.now()) + date = date_format(datetime.datetime.now(), DATE_STORAGE_TEMPLATE) try: rowid = self.insert('tasks', ('name', 'creation_date'), (name, date)) @@ -102,7 +107,7 @@ def insert_task(self, name): else: task_id = self.find_by_clause("tasks", "rowid", rowid, "id")[0][0] self.insert("activity", ("date", "task_id", "spent_time"), - (date, task_id, 0)) + (table_date_format(date, DATE_TEMPLATE), task_id, 0)) self.insert("tasks_tags", ("tag_id", "task_id"), (1, task_id)) return task_id @@ -356,9 +361,8 @@ def check_database(): def write_to_disk(filename, text): """Creates file and fills it with given text.""" - expfile = open(filename, 'w') - expfile.write(text) - expfile.close() + with open(filename, 'w') as expfile: + expfile.write(text) def time_format(sec): @@ -373,16 +377,21 @@ def time_format(sec): return "{} days".format(day) -def date_format(date, template='%Y-%m-%d'): +def date_format(date, template=DATE_TEMPLATE): """Returns formatted date (str). Accepts datetime.""" return datetime.datetime.strftime(date, template) -def str_to_date(string, template='%Y-%m-%d'): +def str_to_date(string, template=DATE_TEMPLATE): """Returns datetime from string.""" return datetime.datetime.strptime(string, template) +def table_date_format(string, template=DATE_FULL_HUMAN_READABLE_TEMPLATE): + """Formats date stored in database to more human-readable""" + return date_format(str_to_date(string, DATE_STORAGE_TEMPLATE), template) + + def get_help(): """Reading help from the file.""" try: diff --git a/src/tasker.pyw b/src/tasker.pyw index 8f83479..3b2e2d6 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -443,6 +443,7 @@ class TaskTable(tk.Frame): self.tasks = copy.deepcopy(tasks) for t in tasks: t[1] = core.time_format(t[1]) + t[2] = core.table_date_format(t[2]) self.insert_tasks(tasks) def focus_(self, item): @@ -519,7 +520,7 @@ class TaskSelectionWindow(Window): pady=5) # Naming of columns in tasks list: column_names = {'taskname': 'Task name', 'spent_time': 'Spent time', - 'creation_date': 'Creation date'} + 'creation_date': 'Created'} # Scrollable tasks table: self.table_frame = TaskTable(column_names, self, takefocus=True) self.table_frame.grid(row=2, column=0, columnspan=5, pady=10, From 1c39ff5133ff364f24e1b0cc70354d0ca66dfb91 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 16 Mar 2019 12:49:24 +0300 Subject: [PATCH 19/55] Added timestamp comment window. --- changelog.txt | 48 ++++++++++++++++++++------------ src/core.py | 11 ++++++-- src/tasker.pyw | 74 +++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 110 insertions(+), 23 deletions(-) diff --git a/changelog.txt b/changelog.txt index f736af5..453f736 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,12 @@ +v.1.5.2 +_Добавлено +1. В таблице задач отображается не только дата, но и время создания задачи. + + v.1.5.1 _Добавлено 1. Кнопка Pause all/Resume all. -_Исправления ошибок: +_Исправлено: 1. Ошибка, если нет прав на запись в директорию, выбранную для экспорта csv. 2. Сбой при попытке экспортировать в csv список длиной в одну задачу. 3. Не работала фильтрация по датам. @@ -13,10 +18,11 @@ _Исправления ошибок: v.1.5 +_Добавлено 1. Опция: отображать только сегодняшний день в таймерах. 2. Кнопка "Отмена" в окне редактирования опций. 3. Опция: На основном экране при запуске одной задачи можно останавливать остальные. -_Багфиксы: +_Исправлено: 1. Размер текста сделан связанным во всём приложении. 2. Заданы имена окон приложения, отображаемые в панели задач (включая главное). 3. Поправлено поведение календаря в фильтре: при установке диапазона дат сбрасывать ранее установленное. @@ -25,10 +31,10 @@ _Багфиксы: v.1.4 -_Фичи +_Добавлено 1. Отчётность с возможностью выбора, что именно выгружать в отчёт. Есть воможность выбора между датами и задачами. 2. В настройках фильтра теперь можно задавать диапазон дат (с помощью календаря). -_Багфиксы: +_Исправлено: 1. Название новой таски, состоящее только из цифр, некорректно учитывалось при фильтрации списка задач на момент добавления этой таски. 2. Изменён дизайн кнопок увеличения и уменьшения количества отображаемых фреймов в настройках главного окна. @@ -39,40 +45,48 @@ _Багфиксы: v.1.3 -_Фичи: +_Добавлено: 1. Добавлен "компактный режим". 2. Добавлена поддержка кнопок главного окна: 'S' для остановки всех тасок, 'C' для очистки окна, 'Q'/'Esc' для выхода. 3. Добавлена возможность сохранения списка открытых задач при выходе. -_Багфиксы: +_Исправлено: 1. Теперь закрываются ненужные соединения с БД. + v.1.2.2 -_Фичи: +_Добавлено: Теперь в окне с датами в свойствах задачи для каждой даты отображается время, затраченное на данную задачу. + v.1.2.1 -_Багфиксы: +_Исправлено: 1. Если включено "всегда наверху", диалоговое окно не позиционируется внутри границ экрана. 2. Поиск не работает, если у одной из тасок пустое описание. + v1.2 -_Фичи: -1. Сделать кастомные кнопки с изображениями (Refresh, search, start/stop...). -2. Закрытие главного окна крестиком должно работать так же, как и кнопкой "Quit". +_Добавлено: +1. Кастомные кнопки с изображениями (Refresh, search, start/stop...). +2. Закрытие главного окна крестиком работает так же, как и кнопкой "Quit". 3. Опция "всегда наверху" для разных платформ. 4. Закрывать все мелкие окошки по Esc. -_Багфиксы: +_Исправлено: 1. Исправить поведение при прохождении таймера через начало суток. 2. Исправить интерфейс на Mac OS X (кастомные кнопки в этом помогли). 3. Сделать открытие новых окон над предыдущими, а не в дефолтном месте экрана. 4. Поменять местами кнопки Close и Delete в окне редактирования тегов. 5. Окошко настроек не получает фокус. 6. Переходить к найденному по кнопке "поиск". Если найденных много, то к первому. -7. Вставка текста в текстовое поле по правой мыши должна происходить в то место, куда установлен курсор. -8. Копирование через контекстное меню должно копировать только выделенный фрагмент, если он есть. -9. Сортировка по потраченному времени перестаёт работать после применения сортировки по дате. +7. Вставка текста в текстовое поле по правой мыши должна происходить в то место, + куда установлен курсор. +8. Копирование через контекстное меню должно копировать только выделенный фрагмент, + если он есть. +9. Сортировка по потраченному времени перестаёт работать после применения + сортировки по дате. 10. "Ignore case" в поиске не работает для описания таски. -11. В режиме фильтра "OR" применяются только условия "слева", т.е. только соответствие дате. -12. В таблице tasks_tags дублируются записи: если такой тег для такой задачи уже есть, он всё равно добавляется повторно. +11. В режиме фильтра "OR" применяются только условия "слева", т.е. только + соответствие дате. +12. В таблице tasks_tags дублируются записи: если такой тег для такой задачи + уже есть, он всё равно добавляется повторно. diff --git a/src/core.py b/src/core.py index d50588f..be81c16 100644 --- a/src/core.py +++ b/src/core.py @@ -436,6 +436,13 @@ def apply_script(scripts_list, db_connection): HELP_TEXT = get_help() TABLE_FILE = 'tasks.db' +LOG_EVENTS = { + "START": 0, + "STOP": 1, + "PAUSE": 2, + "RESUME": 3, + "CUSTOM": 9 +} TABLE_STRUCTURE = """\ CREATE TABLE tasks (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE, @@ -446,8 +453,8 @@ def apply_script(scripts_list, db_connection): spent_time INT); CREATE TABLE tasks_tags (task_id INT, tag_id INT); - CREATE TABLE timestamps (timestamp INT, - task_id INT); + CREATE TABLE timestamps (timestamp INT, task_id INT, + event_type INT, datetime TEXT, comment TEXT); CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE); CREATE TABLE options (name TEXT UNIQUE, diff --git a/src/tasker.pyw b/src/tasker.pyw index 3b2e2d6..dbad511 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -213,11 +213,25 @@ class TaskFrame(tk.Frame): TimestampsWindow(self.task["id"], self.task["spent_total"], ROOT_WINDOW) - def add_timestamp(self): + def add_timestamp(self, event_type=core.LOG_EVENTS["CUSTOM"], + comment=None): """Adding timestamp to database.""" - self.db.insert('timestamps', ('task_id', 'timestamp'), - (self.task["id"], self.task["spent_total"])) - showinfo("Timestamp added", "Timestamp added.") + # Need to preserve time as it was at the moment of function calling: + timestamp = self.task["spent_total"] + current_time = core.date_format(datetime.datetime.now(), + core.DATE_STORAGE_TEMPLATE) + show_message = False + if comment is None: + show_message = True + comment_var = tk.StringVar() + TimestampCommentWindow(self, variable=comment_var) + comment = comment_var.get() + self.db.insert('timestamps', ('task_id', 'timestamp', 'event_type', + 'datetime', 'comment'), + (self.task["id"], timestamp, event_type, + current_time, comment)) + if show_message: + showinfo("Timestamp added", "Timestamp added.") def start_stop(self): """Changes "Start/Stop" button state. """ @@ -344,6 +358,14 @@ class TaskFrame(tk.Frame): image=os.curdir + '/resource/stop.png' if tk.TkVersion >= 8.6 else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") + if self.task["id"] in [x.task["id"] + for x in GLOBAL_OPTIONS["paused"]]: + event_id = core.LOG_EVENTS["RESUME"] + comment = "Task unpaused." + else: + event_id = core.LOG_EVENTS["START"] + comment = "Task started." + self.add_timestamp(event_id, comment) self.timer_update() def timer_stop(self): @@ -356,6 +378,14 @@ class TaskFrame(tk.Frame): # Writing value into database: self.task_update() self.update_description() + if self.task["id"] in [x.task["id"] + for x in GLOBAL_OPTIONS["paused"]]: + event_id = core.LOG_EVENTS["PAUSE"] + comment = "Task paused." + else: + event_id = core.LOG_EVENTS["STOP"] + comment = "Task stopped." + self.add_timestamp(event_id, comment) self.start_button.config( image=os.curdir + '/resource/start_normal.png' if tk.TkVersion >= 8.6 @@ -379,6 +409,42 @@ class TaskFrame(tk.Frame): tk.Frame.destroy(self) +class TimestampCommentWindow(Window): + """Task properties window.""" + + def __init__(self, parent=None, variable=None, **options): + super().__init__(master=parent, **options) + self.comment_var = variable + self.title("Timestamp comment") + elements.SimpleLabel(self, text="Enter comment:", fontsize=10).grid( + row=0, column=0, columnspan=2, pady=5, padx=5, sticky='we') + + #tk.Frame(self, height=30).grid(row=2) + self.comment_area = Description(self, paste_menu=True, width=60, + height=6) + self.comment_area.config(state='normal', bg='white') + self.comment_area.grid(row=1, column=0, columnspan=2, sticky='we') + + tk.Frame(self, height=40).grid(row=2) + elements.TaskButton(self, text='Ok', command=self.get_comment).grid( + row=3, column=0, sticky='sw', padx=5, pady=5) + elements.TaskButton(self, text='Cancel', command=self.cancel).grid( + row=3, column=1, sticky='se', padx=5, pady=5) + self.grid_columnconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=1) + self.comment_area.text.focus_set() + self.prepare() + + def get_comment(self): + self.comment_var.set(self.comment_area.get()) + self.destroy() + + def cancel(self): + self.comment_var.set("") + self.destroy() + + class TaskTable(tk.Frame): """Scrollable tasks table.""" From ee90f4b47a4c8d9a6d388e1ecab7affeb8422bc9 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sun, 17 Mar 2019 10:31:13 +0300 Subject: [PATCH 20/55] TimestampsWindow added. --- src/core.py | 35 +++-- src/tasker.pyw | 389 +++++++++++++++++++++++++++++-------------------- 2 files changed, 252 insertions(+), 172 deletions(-) diff --git a/src/core.py b/src/core.py index be81c16..1f52140 100644 --- a/src/core.py +++ b/src/core.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from collections import OrderedDict as odict +from collections import OrderedDict import datetime import os import sqlite3 @@ -98,16 +98,15 @@ def insert(self, table, fields, values): def insert_task(self, name): """Insert task into database.""" - date = date_format(datetime.datetime.now(), DATE_STORAGE_TEMPLATE) try: rowid = self.insert('tasks', ('name', 'creation_date'), - (name, date)) + (name, date_format(datetime.datetime.now(), + DATE_STORAGE_TEMPLATE))) except sqlite3.IntegrityError: raise DbErrors("Task name already exists") else: task_id = self.find_by_clause("tasks", "rowid", rowid, "id")[0][0] - self.insert("activity", ("date", "task_id", "spent_time"), - (table_date_format(date, DATE_TEMPLATE), task_id, 0)) + self.insert_task_activity(task_id, 0) self.insert("tasks_tags", ("tag_id", "task_id"), (1, task_id)) return task_id @@ -122,15 +121,25 @@ def update(self, field_id, field, value, table="tasks", updfiled="id"): def update_task(self, task_id, field="spent_time", value=0): """Updates some fields for given task id.""" if field == 'spent_time': - self.exec_script("SELECT rowid FROM activity WHERE task_id={0} " - "AND date='{1}'".format(task_id, date_format( - datetime.datetime.now()))) - daterow = self.cur.fetchone()[0] - self.update(daterow, table='activity', updfiled='rowid', - field=field, value=value) + try: + self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" + " AND date='{1}'".format(task_id, date_format( + datetime.datetime.now()))) + daterow = self.cur.fetchone()[0] + except TypeError: + self.insert_task_activity(task_id, value) + else: + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value) else: self.update(task_id, field=field, value=value) + def insert_task_activity(self, task_id, spent_time): + self.insert("activity", ("date", "task_id", "spent_time"), + (date_format(datetime.datetime.now()), + task_id, + spent_time)) + def delete(self, table="tasks", **field_values): """Removes several records using multiple "field in (values)" clauses. field_values has to be a dictionary which values can be tuples: @@ -168,7 +177,7 @@ def tasks_to_export(self, ids): db_response = [{"name": item[0], "descr": item[1] if item[1] else '', "date": item[2], "spent_time": item[3]} for item in self.cur.fetchall()] - prepared_data = odict() + prepared_data = OrderedDict() for item in db_response: if item["name"] in prepared_data: prepared_data[item["name"]]["dates"].append( @@ -214,7 +223,7 @@ def dates_to_export(self, ids): "descr": item[2] if item[2] else '', "spent_time": item[3]} for item in self.cur.fetchall()] - prepared_data = odict() + prepared_data = OrderedDict() for item in db_response: if item["date"] in prepared_data: prepared_data[item["date"]]["tasks"].append({ diff --git a/src/tasker.pyw b/src/tasker.pyw index dbad511..3df03e0 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -4,6 +4,7 @@ import copy import datetime import os import time +from collections import OrderedDict try: import tkinter as tk @@ -66,6 +67,10 @@ class Window(tk.Toplevel): 150))) self.deiconify() # restore hidden window. + def raise_window(self): + self.grab_set() + self.lift() + def destroy(self): self.db.con.close() if self.master: @@ -222,14 +227,19 @@ class TaskFrame(tk.Frame): core.DATE_STORAGE_TEMPLATE) show_message = False if comment is None: - show_message = True + apply_var = tk.BooleanVar(value=True) comment_var = tk.StringVar() - TimestampCommentWindow(self, variable=comment_var) - comment = comment_var.get() - self.db.insert('timestamps', ('task_id', 'timestamp', 'event_type', - 'datetime', 'comment'), - (self.task["id"], timestamp, event_type, - current_time, comment)) + TimestampCommentWindow(self, comment_var=comment_var, + apply_var=apply_var) + if apply_var.get(): + show_message = True + comment = comment_var.get() + if comment is not None: + self.db.insert('timestamps', + ('task_id', 'timestamp', 'event_type', + 'datetime', 'comment'), + (self.task["id"], timestamp, event_type, + current_time, comment)) if show_message: showinfo("Timestamp added", "Timestamp added.") @@ -314,6 +324,7 @@ class TaskFrame(tk.Frame): def task_update(self): """Updates time in the database.""" + self.db.update_task(self.task["id"], value=self.task["spent_today"]) current_date = core.date_format(datetime.datetime.now()) if current_date != self.current_date: self.current_date = current_date @@ -321,8 +332,6 @@ class TaskFrame(tk.Frame): (self.current_date, self.task["id"], self.task["spent_today"])) self.task["spent_today"] = 0 - else: - self.db.update_task(self.task["id"], value=self.task["spent_today"]) def timer_update(self, counter=0): """Renewal of the counter.""" @@ -412,14 +421,14 @@ class TaskFrame(tk.Frame): class TimestampCommentWindow(Window): """Task properties window.""" - def __init__(self, parent=None, variable=None, **options): + def __init__(self, parent=None, comment_var=None, apply_var=None, + **options): super().__init__(master=parent, **options) - self.comment_var = variable + self.comment_var = comment_var + self.apply_var = apply_var self.title("Timestamp comment") elements.SimpleLabel(self, text="Enter comment:", fontsize=10).grid( row=0, column=0, columnspan=2, pady=5, padx=5, sticky='we') - - #tk.Frame(self, height=30).grid(row=2) self.comment_area = Description(self, paste_menu=True, width=60, height=6) self.comment_area.config(state='normal', bg='white') @@ -441,37 +450,82 @@ class TimestampCommentWindow(Window): self.destroy() def cancel(self): - self.comment_var.set("") + self.apply_var.set(False) self.destroy() -class TaskTable(tk.Frame): - """Scrollable tasks table.""" +class Table(tk.Frame): def __init__(self, columns, parent=None, **options): super().__init__(master=parent, **options) - self.tasks_table = ttk.Treeview(self) + self.table = ttk.Treeview(self) style = ttk.Style() style.configure(".", font=('Helvetica', 11)) style.configure("Treeview.Heading", font=('Helvetica', 11)) scroller = tk.Scrollbar(self) - scroller.config(command=self.tasks_table.yview) - self.tasks_table.config(yscrollcommand=scroller.set) + scroller.config(command=self.table.yview) + self.table.config(yscrollcommand=scroller.set) scroller.pack(side='right', fill='y') - self.tasks_table.pack(fill='both', expand=1) + self.table.pack(fill='both', expand=1) # Creating and naming columns: - self.tasks_table.config(columns=tuple([key for key in columns])) + self.table.config(columns=tuple([key for key in columns])) for name in columns: # Configuring columns with given ids: - self.tasks_table.column(name, width=100, minwidth=100, - anchor='center') + self.table.column(name, width=100, minwidth=100, + anchor='center') # Configuring headers of columns with given ids: - self.tasks_table.heading(name, text=columns[name], - command=lambda c=name: + self.table.heading(name, text=columns[name], + command=lambda c=name: self.sort_table_contents(c, True)) - self.tasks_table.column('#0', anchor='w', width=70, minwidth=50, - stretch=0) - self.tasks_table.column('taskname', width=600, anchor='w') + self.table.column('#0', anchor='w', width=70, minwidth=50, + stretch=0) + + def _sort(self, position, reverse): + l = [] + for index, task in enumerate(self.table.get_children()): + l.append((self.data[index][position], task)) + # Sort tasks list by corresponding field to match current sorting: + self.data.sort(key=lambda x: x[position], reverse=reverse) + return l + + def sort_table_contents(self, col, reverse): + """Should be redefined by successors.""" + pass + + def focus_(self, item): + """Focuses on the row with provided id.""" + self.table.see(item) + self.table.selection_set(item) + self.table.focus_set() + self.table.focus(item) + + def update_data(self, data): + """Update contents of internal data used to fill the table.""" + for item in self.table.get_children(): + self.table.delete(item) + self.data = copy.deepcopy(data) + + def insert_rows(self, data): + """Insert rows in the table. Row contents + are tuples provided by 'values='.""" + for i, v in enumerate(data): # item, number, value: + self.table.insert('', i, text="#%d" % (i + 1), values=v) + + def select_all(self): + self.table.selection_set( + self.table.get_children()) + + def clear_all(self): + self.table.selection_remove( + self.table.get_children()) + + +class TaskTable(Table): + """Scrollable tasks table.""" + + def __init__(self, columns, parent=None, **options): + super().__init__(columns, parent=parent, **options) + self.table.column('taskname', width=600, anchor='w') def sort_table_contents(self, col, reverse): """Sorting by click on column header.""" @@ -483,41 +537,17 @@ class TaskTable(tk.Frame): shortlist = self._sort(0, reverse) shortlist.sort(key=lambda x: x[0], reverse=reverse) for index, value in enumerate(shortlist): - self.tasks_table.move(value[1], '', index) - self.tasks_table.heading(col, - command=lambda: - self.sort_table_contents(col, not reverse)) + self.table.move(value[1], '', index) + self.table.heading(col, command=lambda: + self.sort_table_contents(col, not reverse)) - def _sort(self, position, reverse): - l = [] - for index, task in enumerate(self.tasks_table.get_children()): - l.append((self.tasks[index][position], task)) - # Sort tasks list by corresponding field to match current sorting: - self.tasks.sort(key=lambda x: x[position], reverse=reverse) - return l - - def insert_tasks(self, tasks): - """Insert rows in the table. Row contents - are tuples provided by 'values='.""" - for i, v in enumerate(tasks): # item, number, value: - self.tasks_table.insert('', i, text="#%d" % (i + 1), values=v) - - def update_list(self, tasks): + def update_tasks_list(self, data): """Refill table contents.""" - for item in self.tasks_table.get_children(): - self.tasks_table.delete(item) - self.tasks = copy.deepcopy(tasks) - for t in tasks: + self.update_data(data) + for t in data: t[1] = core.time_format(t[1]) t[2] = core.table_date_format(t[2]) - self.insert_tasks(tasks) - - def focus_(self, item): - """Focuses on the row with provided id.""" - self.tasks_table.see(item) - self.tasks_table.selection_set(item) - self.tasks_table.focus_set() - self.tasks_table.focus(item) + self.insert_rows(data) class TaskSelectionWindow(Window): @@ -585,8 +615,9 @@ class TaskSelectionWindow(Window): sticky='e', padx=5, pady=5) # Naming of columns in tasks list: - column_names = {'taskname': 'Task name', 'spent_time': 'Spent time', - 'creation_date': 'Created'} + column_names = OrderedDict({'taskname': 'Task name', + 'spent_time': 'Spent time', + 'creation_date': 'Created'}) # Scrollable tasks table: self.table_frame = TaskTable(column_names, self, takefocus=True) self.table_frame.grid(row=2, column=0, columnspan=5, pady=10, @@ -603,11 +634,12 @@ class TaskSelectionWindow(Window): sticky='news') # "Select all" button: sel_button = elements.TaskButton(self, text="Select all", - command=self.select_all) + command=self.table_frame.select_all) sel_button.grid(row=4, column=0, sticky='w', padx=5, pady=5) # "Clear all" button: - clear_button = elements.TaskButton(self, text="Clear all", - command=self.clear_all) + clear_button = elements.TaskButton(self, text="Clear selection", + textwidth=12, + command=self.table_frame.clear_all) clear_button.grid(row=4, column=1, sticky='e', padx=5, pady=5) # Task properties button: self.edit_button = elements.TaskButton(self, text="Properties...", @@ -636,52 +668,52 @@ class TaskSelectionWindow(Window): self.grid_rowconfigure(2, weight=1, minsize=50) self.update_table() # Fill table contents. self.current_task = '' # Current selected task. - self.table_frame.tasks_table.bind("", self.descr_down) - self.table_frame.tasks_table.bind("", self.descr_up) - self.table_frame.tasks_table.bind("", self.descr_click) + self.table_frame.table.bind("", self.descr_down) + self.table_frame.table.bind("", self.descr_up) + self.table_frame.table.bind("", self.descr_click) self.table_frame.bind("", lambda e: self.focus_first_item(forced=False)) # Need to avoid masquerading of default ttk.Treeview action # on Shift+click and Control+click: self.modifier_pressed = False - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_pressed()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_pressed()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_pressed()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_pressed()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_released()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_released()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_released()) - self.table_frame.tasks_table.bind("", - lambda e: self.shift_control_released()) + self.table_frame.table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.table.bind("", + lambda e: self.shift_control_pressed()) + self.table_frame.table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.table.bind("", + lambda e: self.shift_control_released()) + self.table_frame.table.bind("", + lambda e: self.shift_control_released()) self.search_entry.bind("", lambda e: self.locate_task()) self.bind("", lambda e: self.update_table()) elements.TaskButton(self, text="Open", command=self.get_task).grid( row=6, column=0, padx=5, pady=5, sticky='w') elements.TaskButton(self, text="Cancel", command=self.destroy).grid( row=6, column=4, padx=5, pady=5, sticky='e') - self.table_frame.tasks_table.bind("", self.get_task_id) - self.table_frame.tasks_table.bind("", self.get_task_id) + self.table_frame.table.bind("", self.get_task_id) + self.table_frame.table.bind("", self.get_task_id) self.prepare() def check_row(self, event): """Check if mouse click is over the row, not another tasks_table element.""" if (event.type == '4' and len( - self.table_frame.tasks_table.identify_row(event.y)) > 0) or ( + self.table_frame.table.identify_row(event.y)) > 0) or ( event.type == '2'): return True def get_task(self): """Get selected task id from database and close window.""" # List of selected tasks item id's: - tasks = self.table_frame.tasks_table.selection() + tasks = self.table_frame.table.selection() if tasks: self.task_id_var.set(self.tdict[tasks[0]]["id"]) self.destroy() @@ -699,25 +731,25 @@ class TaskSelectionWindow(Window): def focus_first_item(self, forced=True): """Selects first item in the table if no items selected.""" - if self.table_frame.tasks_table.get_children(): - item = self.table_frame.tasks_table.get_children()[0] + if self.table_frame.table.get_children(): + item = self.table_frame.table.get_children()[0] else: return if forced: self.table_frame.focus_(item) self.update_descr(item) else: - if not self.table_frame.tasks_table.selection(): + if not self.table_frame.table.selection(): self.table_frame.focus_(item) self.update_descr(item) else: - self.table_frame.tasks_table.focus_set() + self.table_frame.table.focus_set() def locate_task(self): """Search task by keywords.""" searchword = self.search_entry.get() if searchword: - self.clear_all() + self.table_frame.clear_all() task_items = [] if self.ignore_case_var.get(): for key in self.tdict: @@ -736,11 +768,11 @@ class TaskSelectionWindow(Window): task_items.append(key) if task_items: for item in task_items: - self.table_frame.tasks_table.selection_add(item) - item = self.table_frame.tasks_table.selection()[0] - self.table_frame.tasks_table.see(item) - self.table_frame.tasks_table.focus_set() - self.table_frame.tasks_table.focus(item) + self.table_frame.table.selection_add(item) + item = self.table_frame.table.selection()[0] + self.table_frame.table.see(item) + self.table_frame.table.focus_set() + self.table_frame.table.focus(item) self.update_descr(item) else: showinfo("No results", @@ -799,14 +831,12 @@ class TaskSelectionWindow(Window): tlist = [{"id": task[0], "name": task[1], "spent_time": task[2], "descr": task[3], "creation_date": task[4]} for task in self.db.cur.fetchall()] - self.table_frame.update_list([[f["name"], f["spent_time"], - f["creation_date"]] for f in tlist]) + self.table_frame.update_tasks_list( + [[f["name"], f["spent_time"], f["creation_date"]] for f in tlist]) # Dictionary with row ids and tasks info: self.tdict = {} - i = 0 - for task_id in self.table_frame.tasks_table.get_children(): - self.tdict[task_id] = tlist[i] - i += 1 + for n, task_id in enumerate(self.table_frame.table.get_children()): + self.tdict[task_id] = tlist[n] self.update_descr(None) self.update_fulltime() @@ -819,15 +849,15 @@ class TaskSelectionWindow(Window): def descr_click(self, event): """Updates description for the task with item id of the row selected by click.""" - pos = self.table_frame.tasks_table.identify_row(event.y) + pos = self.table_frame.table.identify_row(event.y) if pos and pos != '#0' and not self.modifier_pressed: self.table_frame.focus_(pos) - self.update_descr(self.table_frame.tasks_table.focus()) + self.update_descr(self.table_frame.table.focus()) def descr_up(self, event): """Updates description for the item id which is BEFORE selected.""" - item = self.table_frame.tasks_table.focus() - prev_item = self.table_frame.tasks_table.prev(item) + item = self.table_frame.table.focus() + prev_item = self.table_frame.table.prev(item) if prev_item == '': self.update_descr(item) else: @@ -835,8 +865,8 @@ class TaskSelectionWindow(Window): def descr_down(self, event): """Updates description for the item id which is AFTER selected.""" - item = self.table_frame.tasks_table.focus() - next_item = self.table_frame.tasks_table.next(item) + item = self.table_frame.table.focus() + next_item = self.table_frame.table.next(item) if next_item == '': self.update_descr(item) else: @@ -849,19 +879,11 @@ class TaskSelectionWindow(Window): elif item != '': self.description_area.update_text(self.tdict[item]["descr"]) - def select_all(self): - self.table_frame.tasks_table.selection_set( - self.table_frame.tasks_table.get_children()) - - def clear_all(self): - self.table_frame.tasks_table.selection_remove( - self.table_frame.tasks_table.get_children()) - def delete(self): """Remove selected tasks from the database and the table.""" - ids = [self.tdict[x]["id"] for x in self.table_frame.tasks_table.selection() + ids = [self.tdict[x]["id"] for x in self.table_frame.table.selection() if self.tdict[x]["id"] not in GLOBAL_OPTIONS["tasks"]] - items = [x for x in self.table_frame.tasks_table.selection() if + items = [x for x in self.table_frame.table.selection() if self.tdict[x]["id"] in ids] if ids: answer = askyesno("Warning", @@ -869,7 +891,7 @@ class TaskSelectionWindow(Window): parent=self) if answer: self.db.delete_tasks(tuple(ids)) - self.table_frame.tasks_table.delete(*items) + self.table_frame.table.delete(*items) for item in items: self.tdict.pop(item) self.update_descr(None) @@ -877,7 +899,7 @@ class TaskSelectionWindow(Window): def edit(self): """Show task edit window.""" - item = self.table_frame.tasks_table.focus() + item = self.table_frame.table.focus() try: id_name = {"id": self.tdict[item]["id"], "name": self.tdict[item]["name"]} @@ -893,7 +915,7 @@ class TaskSelectionWindow(Window): self.tdict[item] = new_task_info self.update_descr(item) # Update data in a table: - self.table_frame.tasks_table.item( + self.table_frame.table.item( item, values=(new_task_info["name"], core.time_format(new_task_info["spent_total"]), new_task_info["creation_date"])) @@ -928,10 +950,6 @@ class TaskSelectionWindow(Window): if update != self.filter_query(): self.update_table() - def raise_window(self): - self.grab_set() - self.lift() - class TaskEditWindow(Window): """Task properties window.""" @@ -1121,54 +1139,107 @@ class TagsEditWindow(Window): self.db.delete(tag_id=dellist, table='tasks_tags') -class TimestampsWindow(TagsEditWindow): +class TimestampsTable(Table): + + def __init__(self, columns, parent=None, **options): + super().__init__(columns, parent=parent, **options) + self.table.column('stamp', width=200, anchor='w') + self.table.column('since', width=150, anchor='w') + self.table.column('comment', width=250, anchor='w') + self.table.column('real', width=250, anchor='w') + + def sort_table_contents(self, col, reverse): + """Sorting by click on column header.""" + if col == "stamp": + shortlist = self._sort(0, reverse) + elif col == "real": + shortlist = self._sort(1, reverse) + elif col == "since": + shortlist = self._sort(2, reverse) + else: + return + shortlist.sort(key=lambda x: x[0], reverse=reverse) + for index, value in enumerate(shortlist): + self.table.move(value[1], '', index) + self.table.heading( + col, command=lambda: self.sort_table_contents(col, not reverse)) + + def update_timestamps_list(self, data): + """Refill table contents.""" + self.update_data(data) + for t in data: + t[0] = core.time_format(int(t[0])) + t[1] = core.table_date_format(t[1]) + t[2] = core.time_format(int(t[2])) + self.insert_rows(data) + + +class TimestampsWindow(Window): """Window with timestamps for selected task.""" def __init__(self, taskid, task_time, parent=None, **options): + super().__init__(master=parent, **options) self.task_id = taskid self.task_time = task_time - super().__init__(parent=parent, **options) - - def select_all(self): - for item in self.tags.states_list: - item[1][0].set(1) - - def clear_all(self): - for item in self.tags.states_list: - item[1][0].set(0) - - def window_elements_config(self): - """Configure some window parameters.""" self.title("Timestamps: {}".format( self.db.find_by_clause('tasks', 'id', self.task_id, 'name')[0][0])) - self.minsize(width=400, height=300) + column_names = OrderedDict({"stamp": "Timestamp", + "real": "Date and time", + "since": "Time spent since", + "comment": "Comment"}) + self.stamps_frame = TimestampsTable(column_names, parent=self) + self.stamps_frame.grid(row=0, column=0, columnspan=2) elements.TaskButton(self, text="Select all", - command=self.select_all).grid(row=2, column=0, + command=self.stamps_frame.select_all).grid( + row=1, column=0, pady=5, padx=5, sticky='w') - elements.TaskButton(self, text="Clear all", - command=self.clear_all).grid(row=2, column=2, + elements.TaskButton(self, text="Clear selection", textwidth=12, + command=self.stamps_frame.clear_all).grid( + row=1, column=1, pady=5, padx=5, sticky='e') - tk.Frame(self, height=40).grid(row=3) - self.close_button.grid(row=4, column=2, pady=5, padx=5, sticky='w') - self.delete_button.grid(row=4, column=0, pady=5, padx=5, sticky='e') - - def addentry(self): - """Empty method just for suppressing unnecessary element creation.""" - pass + tk.Frame(self, height=40).grid(row=2) + self.update_table() + elements.TaskButton( + self, text="Delete...", command=self.delete).grid( + row=3, column=0, pady=5, padx=5, sticky='w') + elements.TaskButton(self, text="Close", command=self.destroy).grid( + row=3, column=1, pady=5, padx=5, sticky='e') + self.resizable(height=0, width=0) + self.prepare() - def tags_get(self): - """Creates timestamps list.""" - self.tags = Tagslist( - self.db.timestamps(self.task_id, self.task_time), self, - width=400, height=300) + def update_table(self): + db_contents = self.db.find_by_clause("timestamps", "task_id", + self.task_id, "*") + tlist = [{"stamp": timestamp[0], "datetime": timestamp[3], + "comment": timestamp[4], + "spent_since": self.task_time - timestamp[0]} + for timestamp in db_contents] + self.stamps_frame.update_timestamps_list([[f["stamp"], f["datetime"], + f["spent_since"], + f["comment"]] for f in tlist]) + self.sdict = {} + for n, task_id in enumerate(self.stamps_frame.table.get_children()): + self.sdict[task_id] = tlist[n] - def del_record(self, dellist): + def delete(self): """Deletes selected timestamps.""" - for x in dellist: - self.db.delete(table="timestamps", timestamp=x, - task_id=self.task_id) + ids = self.stamps_frame.table.selection() + dates = [self.sdict[x]["datetime"] for x in ids] + if ids: + answer = askyesno("Warning", + "Are you sure you want to delete " + "selected timestamps?", + parent=self) + if answer: + for x in dates: + self.db.delete(table="timestamps", datetime=x, + task_id=self.task_id) + + self.stamps_frame.table.delete(*ids) + for item in ids: + self.sdict.pop(item) class HelpWindow(Window): From b2370b2c9d41ee53023e725a31cdd3d639f1e4f4 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Mon, 25 Mar 2019 23:46:54 +0300 Subject: [PATCH 21/55] Version bump. Changelog update. --- changelog.txt | 7 +++++++ src/core.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 453f736..7069548 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,13 @@ v.1.5.2 _Добавлено 1. В таблице задач отображается не только дата, но и время создания задачи. +2. При создании временнОй метки появляется окно для ввода комментария. +3. При запуске, остановке, паузе и возобновлении таймера автоматически добавляется + временнАя метка с соответствующим комментарием. +4. Переработано окно отображения временнЫх меток: + - новый табличный вид с возможностью сортировки; + - отображаются дата и время создания метки; + - отображается комментарий. v.1.5.1 diff --git a/src/core.py b/src/core.py index 1f52140..27be8b8 100644 --- a/src/core.py +++ b/src/core.py @@ -482,7 +482,7 @@ def apply_script(scripts_list, db_connection): INSERT INTO options VALUES ('toggle_tasks', '0'); INSERT INTO options VALUES ('tasks', ''); INSERT INTO options VALUES ('compact_interface', '0'); - INSERT INTO options VALUES ('version', '1.5.1'); + INSERT INTO options VALUES ('version', '1.5.2'); INSERT INTO options VALUES ('install_time', datetime('now')); """ # PATCH_SCRIPTS = { From 91a559e6d3addfd88358b5fb652e8a6bafe20fc8 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Tue, 26 Mar 2019 08:58:42 +0300 Subject: [PATCH 22/55] Properties saving when open from tasks selection window fixed. --- src/tasker.pyw | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 3df03e0..709715f 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -912,14 +912,8 @@ class TaskSelectionWindow(Window): # Reload task information from database: new_task_info = self.db.select_task(id_name["id"]) # Update description: - self.tdict[item] = new_task_info + self.tdict[item]["descr"] = new_task_info["descr"] self.update_descr(item) - # Update data in a table: - self.table_frame.table.item( - item, values=(new_task_info["name"], - core.time_format(new_task_info["spent_total"]), - new_task_info["creation_date"])) - self.update_fulltime() self.raise_window() def filterwindow(self): @@ -1145,7 +1139,7 @@ class TimestampsTable(Table): super().__init__(columns, parent=parent, **options) self.table.column('stamp', width=200, anchor='w') self.table.column('since', width=150, anchor='w') - self.table.column('comment', width=250, anchor='w') + self.table.column('comment', width=450, anchor='w') self.table.column('real', width=250, anchor='w') def sort_table_contents(self, col, reverse): @@ -1206,7 +1200,9 @@ class TimestampsWindow(Window): row=3, column=0, pady=5, padx=5, sticky='w') elements.TaskButton(self, text="Close", command=self.destroy).grid( row=3, column=1, pady=5, padx=5, sticky='e') - self.resizable(height=0, width=0) + #self.resizable(height=0, width=0) + self.grid_columnconfigure(2, weight=1, minsize=500) + self.grid_rowconfigure(0, weight=1, minsize=300) self.prepare() def update_table(self): @@ -1580,6 +1576,8 @@ class MainFrame(elements.ScrolledCanvas): for frame in self.frames: if frame.running: frame.timer_stop() + if frame in GLOBAL_OPTIONS["paused"]: + frame.add_timestamp(core.LOG_EVENTS["STOP"], "Task stopped.") GLOBAL_OPTIONS["paused"].clear() From 28c60326772fc00029aaac120db7d096b0636d34 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Tue, 26 Mar 2019 12:05:11 +0300 Subject: [PATCH 23/55] Visual fixes of timestamps and timestamp creation windows. --- src/tasker.pyw | 72 ++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 34 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 709715f..ef9e476 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -227,7 +227,7 @@ class TaskFrame(tk.Frame): core.DATE_STORAGE_TEMPLATE) show_message = False if comment is None: - apply_var = tk.BooleanVar(value=True) + apply_var = tk.BooleanVar() comment_var = tk.StringVar() TimestampCommentWindow(self, comment_var=comment_var, apply_var=apply_var) @@ -353,7 +353,7 @@ class TaskFrame(tk.Frame): self.timer = self.timer_label.after( GLOBAL_OPTIONS["TIMER_INTERVAL"], self.timer_update, counter) - def timer_start(self): + def timer_start(self, log=True): """Counter start.""" if not self.running: if int(GLOBAL_OPTIONS["toggle_tasks"]): @@ -367,17 +367,18 @@ class TaskFrame(tk.Frame): image=os.curdir + '/resource/stop.png' if tk.TkVersion >= 8.6 else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") - if self.task["id"] in [x.task["id"] - for x in GLOBAL_OPTIONS["paused"]]: - event_id = core.LOG_EVENTS["RESUME"] - comment = "Task unpaused." - else: - event_id = core.LOG_EVENTS["START"] - comment = "Task started." - self.add_timestamp(event_id, comment) + if log: + if self.task["id"] in [x.task["id"] + for x in GLOBAL_OPTIONS["paused"]]: + event_id = core.LOG_EVENTS["RESUME"] + comment = "Task unpaused." + else: + event_id = core.LOG_EVENTS["START"] + comment = "Task started." + self.add_timestamp(event_id, comment) self.timer_update() - def timer_stop(self): + def timer_stop(self, log=True): """Stop counter and save its value to database.""" if self.running: # after_cancel() stops execution of callback with given ID. @@ -387,14 +388,15 @@ class TaskFrame(tk.Frame): # Writing value into database: self.task_update() self.update_description() - if self.task["id"] in [x.task["id"] - for x in GLOBAL_OPTIONS["paused"]]: - event_id = core.LOG_EVENTS["PAUSE"] - comment = "Task paused." - else: - event_id = core.LOG_EVENTS["STOP"] - comment = "Task stopped." - self.add_timestamp(event_id, comment) + if log: + if self.task["id"] in [x.task["id"] + for x in GLOBAL_OPTIONS["paused"]]: + event_id = core.LOG_EVENTS["PAUSE"] + comment = "Task paused." + else: + event_id = core.LOG_EVENTS["STOP"] + comment = "Task stopped." + self.add_timestamp(event_id, comment) self.start_button.config( image=os.curdir + '/resource/start_normal.png' if tk.TkVersion >= 8.6 @@ -429,30 +431,32 @@ class TimestampCommentWindow(Window): self.title("Timestamp comment") elements.SimpleLabel(self, text="Enter comment:", fontsize=10).grid( row=0, column=0, columnspan=2, pady=5, padx=5, sticky='we') - self.comment_area = Description(self, paste_menu=True, width=60, - height=6) + # self.comment_area = Description(self, paste_menu=True, width=60, + # height=6) + self.comment_area = elements.SimpleEntry(self) self.comment_area.config(state='normal', bg='white') self.comment_area.grid(row=1, column=0, columnspan=2, sticky='we') tk.Frame(self, height=40).grid(row=2) elements.TaskButton(self, text='Ok', command=self.get_comment).grid( row=3, column=0, sticky='sw', padx=5, pady=5) - elements.TaskButton(self, text='Cancel', command=self.cancel).grid( + elements.TaskButton(self, text='Cancel', command=self.destroy).grid( row=3, column=1, sticky='se', padx=5, pady=5) + context_menu = RightclickMenu(paste_item=1, copy_item=0) + self.comment_area.bind("", context_menu.context_menu_show) + self.comment_area.bind("", lambda e: self.get_comment()) self.grid_columnconfigure(0, weight=1) - self.grid_columnconfigure(0, weight=1) - self.grid_rowconfigure(1, weight=1) - self.comment_area.text.focus_set() + self.grid_columnconfigure(1, weight=1) + self.resizable(height=0, width=1) + self.minsize(width=500, height=10) + self.comment_area.focus_set() self.prepare() def get_comment(self): + self.apply_var.set(True) self.comment_var.set(self.comment_area.get()) self.destroy() - def cancel(self): - self.apply_var.set(False) - self.destroy() - class Table(tk.Frame): @@ -1182,7 +1186,7 @@ class TimestampsWindow(Window): "since": "Time spent since", "comment": "Comment"}) self.stamps_frame = TimestampsTable(column_names, parent=self) - self.stamps_frame.grid(row=0, column=0, columnspan=2) + self.stamps_frame.grid(row=0, column=0, columnspan=2, sticky='news') elements.TaskButton(self, text="Select all", command=self.stamps_frame.select_all).grid( row=1, column=0, @@ -1200,9 +1204,9 @@ class TimestampsWindow(Window): row=3, column=0, pady=5, padx=5, sticky='w') elements.TaskButton(self, text="Close", command=self.destroy).grid( row=3, column=1, pady=5, padx=5, sticky='e') - #self.resizable(height=0, width=0) - self.grid_columnconfigure(2, weight=1, minsize=500) + self.grid_columnconfigure(1, weight=1, minsize=500) self.grid_rowconfigure(0, weight=1, minsize=300) + self.minsize(width=710, height=500) self.prepare() def update_table(self): @@ -1523,10 +1527,10 @@ class MainFrame(elements.ScrolledCanvas): if hasattr(w, 'task'): if w.task: state = w.running - w.timer_stop() + w.timer_stop(log=False) w.prepare_task(w.db.select_task(w.task["id"])) if state: - w.timer_start() + w.timer_start(log=False) def fill(self): """Create contents of the main frame.""" From 9a3cb79870ccc8c48e7b3639cfb06a75851ee732 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Tue, 26 Mar 2019 13:04:00 +0300 Subject: [PATCH 24/55] Timestamp addition on application exit added. --- src/tasker.pyw | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index ef9e476..dda6ce8 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -368,17 +368,16 @@ class TaskFrame(tk.Frame): else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") if log: - if self.task["id"] in [x.task["id"] - for x in GLOBAL_OPTIONS["paused"]]: + if self in GLOBAL_OPTIONS["paused"]: event_id = core.LOG_EVENTS["RESUME"] - comment = "Task unpaused." + comment = "Task resumed." else: event_id = core.LOG_EVENTS["START"] comment = "Task started." self.add_timestamp(event_id, comment) self.timer_update() - def timer_stop(self, log=True): + def timer_stop(self, log=True, log_message=None): """Stop counter and save its value to database.""" if self.running: # after_cancel() stops execution of callback with given ID. @@ -389,13 +388,12 @@ class TaskFrame(tk.Frame): self.task_update() self.update_description() if log: - if self.task["id"] in [x.task["id"] - for x in GLOBAL_OPTIONS["paused"]]: + if self in GLOBAL_OPTIONS["paused"]: event_id = core.LOG_EVENTS["PAUSE"] comment = "Task paused." else: event_id = core.LOG_EVENTS["STOP"] - comment = "Task stopped." + comment = "Task stopped." if not log_message else log_message self.add_timestamp(event_id, comment) self.start_button.config( image=os.curdir + '/resource/start_normal.png' @@ -413,7 +411,11 @@ class TaskFrame(tk.Frame): def destroy(self): """Closes frame and writes counter value into database.""" - self.timer_stop() + message = "Task stopped on application exit." + self.timer_stop(log_message=message) + if self in GLOBAL_OPTIONS["paused"]: + GLOBAL_OPTIONS["paused"].remove(self) + self.add_timestamp(core.LOG_EVENTS["STOP"], message) if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) self.db.con.close() From cfd3c398b896633871aaa4ee777f45932823be6d Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 27 Mar 2019 13:42:09 +0300 Subject: [PATCH 25/55] Activity record duplicating on overnight fixed. --- src/core.py | 30 ++++++++++++++++++++---------- src/tasker.pyw | 14 ++++++-------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/core.py b/src/core.py index 27be8b8..f8f2b5e 100644 --- a/src/core.py +++ b/src/core.py @@ -118,21 +118,31 @@ def update(self, field_id, field, value, table="tasks", updfiled="id"): field_id, updfiled), value) - def update_task(self, task_id, field="spent_time", value=0): + def update_task(self, task_id, field="spent_time", value=0, prev_date=None): """Updates some fields for given task id.""" + res = None if field == 'spent_time': - try: - self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" - " AND date='{1}'".format(task_id, date_format( - datetime.datetime.now()))) - daterow = self.cur.fetchone()[0] - except TypeError: - self.insert_task_activity(task_id, value) + now = datetime.datetime.now() + current_date = date_format(now) + if current_date == prev_date: + req_date = current_date else: - self.update(daterow, table='activity', updfiled='rowid', - field=field, value=value) + self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" + " AND date='{1}'".format(task_id, prev_date)) + secs = datetime.timedelta(hours=now.hour, minutes=now.minute, + seconds=now.second).total_seconds() + self.insert_task_activity(task_id, secs) + req_date = prev_date + value = value - secs + res = {"remainder": secs, "current_date": current_date} + self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" + " AND date='{1}'".format(task_id, req_date)) + daterow = self.cur.fetchone()[0] + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value) else: self.update(task_id, field=field, value=value) + return res def insert_task_activity(self, task_id, spent_time): self.insert("activity", ("date", "task_id", "spent_time"), diff --git a/src/tasker.pyw b/src/tasker.pyw index dda6ce8..a0944ef 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -324,14 +324,12 @@ class TaskFrame(tk.Frame): def task_update(self): """Updates time in the database.""" - self.db.update_task(self.task["id"], value=self.task["spent_today"]) - current_date = core.date_format(datetime.datetime.now()) - if current_date != self.current_date: - self.current_date = current_date - self.db.insert("activity", ("date", "task_id", "spent_time"), - (self.current_date, self.task["id"], - self.task["spent_today"])) - self.task["spent_today"] = 0 + res = self.db.update_task(self.task["id"], + value=self.task["spent_today"], + prev_date=self.current_date) + if res: + self.current_date = res["current_date"] + self.task["spent_today"] = res["remainder"] def timer_update(self, counter=0): """Renewal of the counter.""" From c7f71e6a34ad598e4972fbe037e5d7b036a070c1 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 27 Mar 2019 15:10:43 +0300 Subject: [PATCH 26/55] core.update_task() fixed. --- src/core.py | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/core.py b/src/core.py index f8f2b5e..7582948 100644 --- a/src/core.py +++ b/src/core.py @@ -122,31 +122,32 @@ def update_task(self, task_id, field="spent_time", value=0, prev_date=None): """Updates some fields for given task id.""" res = None if field == 'spent_time': - now = datetime.datetime.now() - current_date = date_format(now) - if current_date == prev_date: - req_date = current_date - else: + try: self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" " AND date='{1}'".format(task_id, prev_date)) - secs = datetime.timedelta(hours=now.hour, minutes=now.minute, - seconds=now.second).total_seconds() - self.insert_task_activity(task_id, secs) - req_date = prev_date - value = value - secs - res = {"remainder": secs, "current_date": current_date} - self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" - " AND date='{1}'".format(task_id, req_date)) - daterow = self.cur.fetchone()[0] - self.update(daterow, table='activity', updfiled='rowid', - field=field, value=value) + daterow = self.cur.fetchone()[0] + except TypeError: + now = datetime.datetime.now() + current_date = date_format(now) + if current_date == prev_date: + self.insert_task_activity(task_id, value, current_date) + else: + secs = datetime.timedelta(hours=now.hour, + minutes=now.minute, + seconds=now.second).total_seconds() + self.insert_task_activity(task_id, value - secs, prev_date) + self.insert_task_activity(task_id, secs, current_date) + res = {"remainder": secs, "current_date": current_date} + else: + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value) else: self.update(task_id, field=field, value=value) return res - def insert_task_activity(self, task_id, spent_time): + def insert_task_activity(self, task_id, spent_time, date=None): self.insert("activity", ("date", "task_id", "spent_time"), - (date_format(datetime.datetime.now()), + (date if date else date_format(datetime.datetime.now()), task_id, spent_time)) From cf5d3937bf5da86d9b5d19b639a2e386a555d1bf Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 28 Mar 2019 12:26:53 +0300 Subject: [PATCH 27/55] core.update_task() fixed. Again. --- src/core.py | 46 +++++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/core.py b/src/core.py index 7582948..c2bff42 100644 --- a/src/core.py +++ b/src/core.py @@ -118,29 +118,41 @@ def update(self, field_id, field, value, table="tasks", updfiled="id"): field_id, updfiled), value) + def check_task_activity_exists(self, task_id, date): + """Returns rowid of row with task activity for provided date + if such activity exists""" + self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" + " AND date='{1}'".format(task_id, date)) + try: + return self.cur.fetchone()[0] + except TypeError: + return + def update_task(self, task_id, field="spent_time", value=0, prev_date=None): """Updates some fields for given task id.""" res = None if field == 'spent_time': - try: - self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" - " AND date='{1}'".format(task_id, prev_date)) - daterow = self.cur.fetchone()[0] - except TypeError: - now = datetime.datetime.now() - current_date = date_format(now) - if current_date == prev_date: - self.insert_task_activity(task_id, value, current_date) + now = datetime.datetime.now() + current_date = date_format(now) + daterow = self.check_task_activity_exists(task_id, prev_date) + if current_date == prev_date: + if daterow: + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value) else: - secs = datetime.timedelta(hours=now.hour, - minutes=now.minute, - seconds=now.second).total_seconds() - self.insert_task_activity(task_id, value - secs, prev_date) - self.insert_task_activity(task_id, secs, current_date) - res = {"remainder": secs, "current_date": current_date} + self.insert_task_activity(task_id, value, prev_date) else: - self.update(daterow, table='activity', updfiled='rowid', - field=field, value=value) + today_secs = datetime.timedelta( + hours=now.hour, minutes=now.minute, + seconds=now.second).total_seconds() + if daterow: + self.update(daterow, table='activity', updfiled='rowid', + field=field, value=value - today_secs) + else: + self.insert_task_activity(task_id, value - today_secs, + prev_date) + self.insert_task_activity(task_id, today_secs, current_date) + res = {"remainder": today_secs, "current_date": current_date} else: self.update(task_id, field=field, value=value) return res From 832b0979e2e89a1002f239c08871941ecfee323b Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 28 Mar 2019 19:16:17 +0300 Subject: [PATCH 28/55] Fixed: not removing task from preserved tasks on exit list on task deletion. --- src/core.py | 6 ++++++ src/tasker.pyw | 36 ++++++++++++++++++++---------------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/core.py b/src/core.py index c2bff42..ef670db 100644 --- a/src/core.py +++ b/src/core.py @@ -163,6 +163,12 @@ def insert_task_activity(self, task_id, spent_time, date=None): task_id, spent_time)) + def update_preserved_tasks(self, tasks): + if type(tasks) is not str: + tasks = ','.join(map(str, tasks)) + self.update(table='options', field='value', value=tasks, + field_id='tasks', updfiled='name') + def delete(self, table="tasks", **field_values): """Removes several records using multiple "field in (values)" clauses. field_values has to be a dictionary which values can be tuples: diff --git a/src/tasker.pyw b/src/tasker.pyw index a0944ef..ccc2f10 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -265,6 +265,8 @@ class TaskFrame(tk.Frame): for w in self.winfo_children(): w.destroy() GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) + if GLOBAL_OPTIONS["preserve_tasks"] == "1": + self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.create_content() def name_dialogue(self): @@ -297,7 +299,7 @@ class TaskFrame(tk.Frame): def prepare_task(self, task): """Prepares frame elements to work with.""" - # Adding task id to set of running tasks: + # Adding task id and state to dictionary of running tasks: GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task self.current_date = core.date_format(datetime.datetime.now()) @@ -314,6 +316,8 @@ class TaskFrame(tk.Frame): self.timestamps_window_button.config(state='normal') if hasattr(self, "description_area"): self.description_area.update_text(self.task["descr"]) + if GLOBAL_OPTIONS["preserve_tasks"] == "1": + self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) def get_current_time(self): """Return current_time depending on time displaying options value.""" @@ -355,8 +359,7 @@ class TaskFrame(tk.Frame): """Counter start.""" if not self.running: if int(GLOBAL_OPTIONS["toggle_tasks"]): - for key in GLOBAL_OPTIONS["tasks"]: - GLOBAL_OPTIONS["tasks"][key] = False + ROOT_WINDOW.stop_all() GLOBAL_OPTIONS["tasks"][self.task["id"]] = True # Setting current timestamp: self.start_time = time.time() @@ -1500,10 +1503,8 @@ class MainFrame(elements.ScrolledCanvas): def clear(self): """Clear all task frames except with opened tasks.""" for w in self.content_frame.winfo_children(): - if self.frames_count == int(GLOBAL_OPTIONS[ - 'timers_count']) \ - or self.frames_count == len( - GLOBAL_OPTIONS["tasks"]): + if self.frames_count == int(GLOBAL_OPTIONS['timers_count']) \ + or self.frames_count == len(GLOBAL_OPTIONS["tasks"]): break if hasattr(w, 'task'): if w.task is None: @@ -1515,10 +1516,12 @@ class MainFrame(elements.ScrolledCanvas): answer = askyesno("Really clear?", "Are you sure you want to close all task frames?") if answer: + self.frames_count = 0 for w in self.content_frame.winfo_children(): - self.frames_count -= 1 w.destroy() GLOBAL_OPTIONS["paused"].clear() + if GLOBAL_OPTIONS["preserve_tasks"] == "1": + self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.fill() def frames_refill(self): @@ -1608,6 +1611,7 @@ class MainMenu(tk.Menu): def options_window(self): """Open options window.""" + self.db = core.Db() # number of main window frames: timers_count_var = tk.IntVar(value=int(GLOBAL_OPTIONS['timers_count'])) # 'always on top' option: @@ -1648,6 +1652,8 @@ class MainMenu(tk.Menu): ROOT_WINDOW.small_interface() # apply value of 'save tasks on exit' option: params['preserve_tasks'] = save.get() + if not params['preserve_tasks']: + self.db.update_preserved_tasks('') # apply value of 'show current day in timers' option: params['show_today'] = show_today_var.get() # apply value of 'Allow run only one task at a time' option: @@ -1665,13 +1671,12 @@ class MainMenu(tk.Menu): def change_parameter(self, paramdict): """Change option in the database.""" - db = core.Db() for parameter_name in paramdict: par = str(paramdict[parameter_name]) - db.update(table='options', field='value', value=par, + self.db.update(table='options', field='value', value=par, field_id=parameter_name, updfiled='name') - GLOBAL_OPTIONS[parameter_name] = par - db.con.close() + GLOBAL_OPTIONS[parameter_name] = str(par) + self.db.con.close() def aboutwindow(self): showinfo("About Tasker", @@ -1922,7 +1927,7 @@ class MainWindow(tk.Tk): if answer: db = core.Db() if GLOBAL_OPTIONS["preserve_tasks"] == "1": - tasks = ','.join([str(x) for x in GLOBAL_OPTIONS["tasks"]]) + tasks = GLOBAL_OPTIONS["tasks"] if int(GLOBAL_OPTIONS['timers_count']) < len( GLOBAL_OPTIONS["tasks"]): db.update(table='options', field='value', @@ -1930,8 +1935,7 @@ class MainWindow(tk.Tk): field_id='timers_count', updfiled='name') else: tasks = '' - db.update(table='options', field='value', value=tasks, - field_id='tasks', updfiled='name') + db.update_preserved_tasks(tasks) db.con.close() super().destroy() @@ -1986,7 +1990,7 @@ if __name__ == "__main__": # Global tasks ids set. Used for preserve duplicates: if GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"] = dict.fromkeys( - [int(x) for x in GLOBAL_OPTIONS["tasks"].split(",")], False) + map(int, GLOBAL_OPTIONS["tasks"].split(",")), False) else: GLOBAL_OPTIONS["tasks"] = dict() # List of preserved tasks which are not open: From 74b3f4e68789987ebd4df474d463154398b8204e Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 28 Mar 2019 19:33:30 +0300 Subject: [PATCH 29/55] Clear all fix. --- src/tasker.pyw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index ccc2f10..08ee3a7 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -419,6 +419,8 @@ class TaskFrame(tk.Frame): self.add_timestamp(core.LOG_EVENTS["STOP"], message) if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) + if GLOBAL_OPTIONS["preserve_tasks"] == "1": + self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.db.con.close() tk.Frame.destroy(self) @@ -1520,8 +1522,6 @@ class MainFrame(elements.ScrolledCanvas): for w in self.content_frame.winfo_children(): w.destroy() GLOBAL_OPTIONS["paused"].clear() - if GLOBAL_OPTIONS["preserve_tasks"] == "1": - self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.fill() def frames_refill(self): From 9e30d7d4578fdbd72ba145cb3c095dfec9f73a35 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Thu, 28 Mar 2019 20:57:31 +0300 Subject: [PATCH 30/55] Clear all fix. Again. --- changelog.txt | 2 ++ src/tasker.pyw | 26 +++++++++++++++----------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7069548..dd7ff3f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -8,6 +8,8 @@ _Добавлено - новый табличный вид с возможностью сортировки; - отображаются дата и время создания метки; - отображается комментарий. +_Исправлено: +1. При удалении задачи она не удалялась из списка сохранённых задач. v.1.5.1 diff --git a/src/tasker.pyw b/src/tasker.pyw index 08ee3a7..83e7dd9 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -208,7 +208,7 @@ class TaskFrame(tk.Frame): def small_interface(self): """Destroy some interface elements when switching to 'compact' mode.""" - for widget in self.l1, self.description_area: + for widget in (self.l1, self.description_area): widget.destroy() if hasattr(self, "description_area"): delattr(self, "description_area") @@ -261,12 +261,17 @@ class TaskFrame(tk.Frame): def clear(self): """Recreation of frame contents.""" - self.timer_stop() + message = "Task frame cleared." + self.timer_stop(log_message=message) + if self in GLOBAL_OPTIONS["paused"]: + GLOBAL_OPTIONS["paused"].remove(self) + self.add_timestamp(core.LOG_EVENTS["STOP"], message) + if self.task: + GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) + if GLOBAL_OPTIONS["preserve_tasks"] == "1": + self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) for w in self.winfo_children(): w.destroy() - GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) - if GLOBAL_OPTIONS["preserve_tasks"] == "1": - self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.create_content() def name_dialogue(self): @@ -376,6 +381,7 @@ class TaskFrame(tk.Frame): event_id = core.LOG_EVENTS["START"] comment = "Task started." self.add_timestamp(event_id, comment) + self.task_update() self.timer_update() def timer_stop(self, log=True, log_message=None): @@ -419,8 +425,6 @@ class TaskFrame(tk.Frame): self.add_timestamp(core.LOG_EVENTS["STOP"], message) if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) - if GLOBAL_OPTIONS["preserve_tasks"] == "1": - self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) self.db.con.close() tk.Frame.destroy(self) @@ -1503,7 +1507,7 @@ class MainFrame(elements.ScrolledCanvas): self.fill() def clear(self): - """Clear all task frames except with opened tasks.""" + """Remove all task frames except with opened tasks.""" for w in self.content_frame.winfo_children(): if self.frames_count == int(GLOBAL_OPTIONS['timers_count']) \ or self.frames_count == len(GLOBAL_OPTIONS["tasks"]): @@ -1518,9 +1522,9 @@ class MainFrame(elements.ScrolledCanvas): answer = askyesno("Really clear?", "Are you sure you want to close all task frames?") if answer: - self.frames_count = 0 for w in self.content_frame.winfo_children(): - w.destroy() + if hasattr(w, 'task'): + w.clear() GLOBAL_OPTIONS["paused"].clear() self.fill() @@ -1540,7 +1544,7 @@ class MainFrame(elements.ScrolledCanvas): if self.frames_count < int(GLOBAL_OPTIONS['timers_count']): row_count = range( int(GLOBAL_OPTIONS['timers_count']) - self.frames_count) - for row_number in row_count: + for _ in row_count: task = TaskFrame(parent=self.content_frame) task.grid(row=self.rows_counter, pady=5, padx=5, ipady=3, sticky='ew') From e845fe7970cefdbc01ed064ab82cb9d4d2a85843 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Fri, 29 Mar 2019 09:25:42 +0300 Subject: [PATCH 31/55] Database 'options' table optimization. --- src/core.py | 23 +++++++++---------- src/tasker.pyw | 61 +++++++++++++++++++++++++------------------------- 2 files changed, 40 insertions(+), 44 deletions(-) diff --git a/src/core.py b/src/core.py index ef670db..93e8830 100644 --- a/src/core.py +++ b/src/core.py @@ -446,7 +446,7 @@ def patch_database(): cur = con.cursor() cur.execute("SELECT value FROM options WHERE name='patch_ver';") res = cur.fetchone() - key = '0' + key = 0 if not res: for key in sorted(PATCH_SCRIPTS): apply_script(PATCH_SCRIPTS[key], con) @@ -457,8 +457,7 @@ def patch_database(): apply_script(PATCH_SCRIPTS[key], con) if res[0] != key: con.executescript( - "UPDATE options SET value='{0}' WHERE name='patch_ver';".format( - str(key))) + "UPDATE options SET value={0} WHERE name='patch_ver';".format(key)) con.commit() con.close() @@ -495,22 +494,20 @@ def apply_script(scripts_list, db_connection): event_type INT, datetime TEXT, comment TEXT); CREATE TABLE tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT UNIQUE); - CREATE TABLE options (name TEXT UNIQUE, - value TEXT); + CREATE TABLE options (name TEXT UNIQUE, value NUMERIC); INSERT INTO tags VALUES (1, 'default'); INSERT INTO options (name) VALUES ('filter'); INSERT INTO options VALUES ('filter_tags', ''); INSERT INTO options VALUES ('filter_dates', ''); INSERT INTO options VALUES ('filter_operating_mode', 'AND'); - INSERT INTO options VALUES ('patch_ver', '0'); - INSERT INTO options VALUES ('timers_count', '3'); - INSERT INTO options VALUES ('minimize_to_tray', '0'); - INSERT INTO options VALUES ('always_on_top', '0'); - INSERT INTO options VALUES ('preserve_tasks', '0'); - INSERT INTO options VALUES ('show_today', '0'); - INSERT INTO options VALUES ('toggle_tasks', '0'); + INSERT INTO options VALUES ('patch_ver', 0); + INSERT INTO options VALUES ('timers_count', 3); + INSERT INTO options VALUES ('always_on_top', 0); + INSERT INTO options VALUES ('preserve_tasks', 0); + INSERT INTO options VALUES ('show_today', 0); + INSERT INTO options VALUES ('toggle_tasks', 0); INSERT INTO options VALUES ('tasks', ''); - INSERT INTO options VALUES ('compact_interface', '0'); + INSERT INTO options VALUES ('compact_interface', 0); INSERT INTO options VALUES ('version', '1.5.2'); INSERT INTO options VALUES ('install_time', datetime('now')); """ diff --git a/src/tasker.pyw b/src/tasker.pyw index 83e7dd9..d859f66 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -43,7 +43,7 @@ class Window(tk.Toplevel): """Allows window to be on the top of others when 'always on top' is enabled.""" ontop = GLOBAL_OPTIONS['always_on_top'] - if ontop == '1': + if ontop: self.wm_attributes("-topmost", 1) def place_window(self, parent): @@ -143,7 +143,7 @@ class TaskFrame(tk.Frame): self.startstop_var = tk.StringVar(value="Start") # Text on "Start" button. # Fake name of running task (which actually is not selected yet). self.task = None - if GLOBAL_OPTIONS["compact_interface"] == "0": + if not GLOBAL_OPTIONS["compact_interface"]: self.normal_interface() # Task name field: self.task_label = TaskLabel(self, width=50, anchor='w') @@ -268,7 +268,7 @@ class TaskFrame(tk.Frame): self.add_timestamp(core.LOG_EVENTS["STOP"], message) if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) - if GLOBAL_OPTIONS["preserve_tasks"] == "1": + if GLOBAL_OPTIONS["preserve_tasks"]: self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) for w in self.winfo_children(): w.destroy() @@ -321,12 +321,12 @@ class TaskFrame(tk.Frame): self.timestamps_window_button.config(state='normal') if hasattr(self, "description_area"): self.description_area.update_text(self.task["descr"]) - if GLOBAL_OPTIONS["preserve_tasks"] == "1": + if GLOBAL_OPTIONS["preserve_tasks"]: self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) def get_current_time(self): """Return current_time depending on time displaying options value.""" - if int(GLOBAL_OPTIONS["show_today"]): + if GLOBAL_OPTIONS["show_today"]: return self.task["spent_today"] else: return self.task["spent_total"] @@ -363,7 +363,7 @@ class TaskFrame(tk.Frame): def timer_start(self, log=True): """Counter start.""" if not self.running: - if int(GLOBAL_OPTIONS["toggle_tasks"]): + if GLOBAL_OPTIONS["toggle_tasks"]: ROOT_WINDOW.stop_all() GLOBAL_OPTIONS["tasks"][self.task["id"]] = True # Setting current timestamp: @@ -1509,7 +1509,7 @@ class MainFrame(elements.ScrolledCanvas): def clear(self): """Remove all task frames except with opened tasks.""" for w in self.content_frame.winfo_children(): - if self.frames_count == int(GLOBAL_OPTIONS['timers_count']) \ + if self.frames_count == GLOBAL_OPTIONS['timers_count'] \ or self.frames_count == len(GLOBAL_OPTIONS["tasks"]): break if hasattr(w, 'task'): @@ -1541,9 +1541,9 @@ class MainFrame(elements.ScrolledCanvas): def fill(self): """Create contents of the main frame.""" - if self.frames_count < int(GLOBAL_OPTIONS['timers_count']): + if self.frames_count < GLOBAL_OPTIONS['timers_count']: row_count = range( - int(GLOBAL_OPTIONS['timers_count']) - self.frames_count) + GLOBAL_OPTIONS['timers_count'] - self.frames_count) for _ in row_count: task = TaskFrame(parent=self.content_frame) task.grid(row=self.rows_counter, pady=5, padx=5, ipady=3, @@ -1556,8 +1556,8 @@ class MainFrame(elements.ScrolledCanvas): self.frames_count += len(row_count) self.content_frame.update() self.canvbox.config(width=self.content_frame.winfo_width()) - elif len(GLOBAL_OPTIONS["tasks"]) < self.frames_count > int( - GLOBAL_OPTIONS['timers_count']): + elif len(GLOBAL_OPTIONS["tasks"]) < self.frames_count > \ + GLOBAL_OPTIONS['timers_count']: self.clear() self.content_frame.config(bg='#cfcfcf') @@ -1617,17 +1617,17 @@ class MainMenu(tk.Menu): """Open options window.""" self.db = core.Db() # number of main window frames: - timers_count_var = tk.IntVar(value=int(GLOBAL_OPTIONS['timers_count'])) + timers_count_var = tk.IntVar(value=GLOBAL_OPTIONS['timers_count']) # 'always on top' option: - ontop = tk.IntVar(value=int(GLOBAL_OPTIONS['always_on_top'])) + ontop = tk.IntVar(value=GLOBAL_OPTIONS['always_on_top']) # 'compact interface' option - compact = int(GLOBAL_OPTIONS['compact_interface']) + compact = GLOBAL_OPTIONS['compact_interface'] compact_iface = tk.IntVar(value=compact) # 'save tasks on exit' option: - save = tk.IntVar(value=int(GLOBAL_OPTIONS['preserve_tasks'])) + save = tk.IntVar(value=GLOBAL_OPTIONS['preserve_tasks']) # 'show current day in timers' option: - show_today_var = tk.IntVar(value=int(GLOBAL_OPTIONS['show_today'])) - toggle = int(GLOBAL_OPTIONS['toggle_tasks']) + show_today_var = tk.IntVar(value=GLOBAL_OPTIONS['show_today']) + toggle = GLOBAL_OPTIONS['toggle_tasks'] toggler_var = tk.IntVar(value=toggle) params = {} accept_var = tk.BooleanVar() @@ -1646,7 +1646,7 @@ class MainMenu(tk.Menu): params['timers_count'] = count # apply value of 'always on top' option: params['always_on_top'] = ontop.get() - ROOT_WINDOW.wm_attributes("-topmost", ontop.get()) + ROOT_WINDOW.wm_attributes("-topmost", params['always_on_top']) # apply value of 'compact interface' option: params['compact_interface'] = compact_iface.get() if compact != compact_iface.get(): @@ -1668,18 +1668,17 @@ class MainMenu(tk.Menu): ROOT_WINDOW.taskframes.fill() ROOT_WINDOW.taskframes.frames_refill() # Stop all tasks if exclusive run method has been enabled: - if int(GLOBAL_OPTIONS["toggle_tasks"]) and int( - GLOBAL_OPTIONS["toggle_tasks"]) != toggle: + if GLOBAL_OPTIONS["toggle_tasks"] and \ + GLOBAL_OPTIONS["toggle_tasks"] != toggle: ROOT_WINDOW.stop_all() ROOT_WINDOW.lift() def change_parameter(self, paramdict): """Change option in the database.""" - for parameter_name in paramdict: - par = str(paramdict[parameter_name]) - self.db.update(table='options', field='value', value=par, - field_id=parameter_name, updfiled='name') - GLOBAL_OPTIONS[parameter_name] = str(par) + for key, value in paramdict.items(): + self.db.update(table='options', field='value', value=value, + field_id=key, updfiled='name') + GLOBAL_OPTIONS[key] = value self.db.con.close() def aboutwindow(self): @@ -1845,7 +1844,7 @@ class MainWindow(tk.Tk): self.taskframes.grid(row=0, columnspan=5) self.bind("", self.taskframes.reconf_canvas) self.paused = False - if GLOBAL_OPTIONS["compact_interface"] == "0": + if not GLOBAL_OPTIONS["compact_interface"]: self.full_interface(True) self.grid_rowconfigure(0, weight=1) # Make main window always appear in good position @@ -1856,7 +1855,7 @@ class MainWindow(tk.Tk): else: window_height = self.winfo_screenheight() - 250 self.geometry('%dx%d+100+50' % (self.winfo_width(), window_height)) - if GLOBAL_OPTIONS['always_on_top'] == '1': + if GLOBAL_OPTIONS['always_on_top']: self.wm_attributes("-topmost", 1) self.bind("", self.hotkeys) @@ -1909,12 +1908,12 @@ class MainWindow(tk.Tk): def pause_all(self): if self.paused: - if GLOBAL_OPTIONS["compact_interface"] == "0": + if not GLOBAL_OPTIONS["compact_interface"]: self.pause_all_var.set("Pause all") self.taskframes.resume_all() self.paused = False else: - if GLOBAL_OPTIONS["compact_interface"] == "0": + if not GLOBAL_OPTIONS["compact_interface"]: self.pause_all_var.set("Resume all") self.taskframes.pause_all() self.paused = True @@ -1923,14 +1922,14 @@ class MainWindow(tk.Tk): """Stop all running timers.""" self.taskframes.stop_all() self.paused = False - if GLOBAL_OPTIONS["compact_interface"] == "0": + if not GLOBAL_OPTIONS["compact_interface"]: self.pause_all_var.set("Pause all") def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") if answer: db = core.Db() - if GLOBAL_OPTIONS["preserve_tasks"] == "1": + if GLOBAL_OPTIONS["preserve_tasks"]: tasks = GLOBAL_OPTIONS["tasks"] if int(GLOBAL_OPTIONS['timers_count']) < len( GLOBAL_OPTIONS["tasks"]): From 73871ea23f59cec1d5b8a64709cc4a2610ddbf6e Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 29 Mar 2019 13:48:15 +0300 Subject: [PATCH 32/55] Fixed issue when preserved tasks list length == 1. --- src/core.py | 2 ++ src/tasker.pyw | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core.py b/src/core.py index 93e8830..7963fab 100644 --- a/src/core.py +++ b/src/core.py @@ -166,6 +166,8 @@ def insert_task_activity(self, task_id, spent_time, date=None): def update_preserved_tasks(self, tasks): if type(tasks) is not str: tasks = ','.join(map(str, tasks)) + if len(tasks) == 1: + tasks += "," self.update(table='options', field='value', value=tasks, field_id='tasks', updfiled='name') diff --git a/src/tasker.pyw b/src/tasker.pyw index d859f66..9bce837 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -42,8 +42,7 @@ class Window(tk.Toplevel): def on_top_wait(self): """Allows window to be on the top of others when 'always on top' is enabled.""" - ontop = GLOBAL_OPTIONS['always_on_top'] - if ontop: + if GLOBAL_OPTIONS['always_on_top']: self.wm_attributes("-topmost", 1) def place_window(self, parent): @@ -109,6 +108,7 @@ class Description(tk.Frame): self.context_menu = RightclickMenu(copy_item=copy_menu, paste_item=paste_menu) self.text.bind("", self.context_menu.context_menu_show) + self.text.bind("", lambda e: self.text.tk_focusNext) def config(self, cnf=None, **kw): """Text configuration method.""" @@ -1993,7 +1993,8 @@ if __name__ == "__main__": # Global tasks ids set. Used for preserve duplicates: if GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"] = dict.fromkeys( - map(int, GLOBAL_OPTIONS["tasks"].split(",")), False) + [int(x) for x in GLOBAL_OPTIONS["tasks"].split(",") if len(x) > 0], + False) else: GLOBAL_OPTIONS["tasks"] = dict() # List of preserved tasks which are not open: @@ -2006,4 +2007,4 @@ if __name__ == "__main__": "paused": set()}) # Main window: ROOT_WINDOW = MainWindow() - ROOT_WINDOW.mainloop() + ROOT_WINDOW.mainloop() \ No newline at end of file From 22b90669652bd5bce7ca4c6433ae8c0445800514 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 29 Mar 2019 13:59:27 +0300 Subject: [PATCH 33/55] About message changed. --- src/core.py | 2 ++ src/tasker.pyw | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/core.py b/src/core.py index 7963fab..2cd1869 100644 --- a/src/core.py +++ b/src/core.py @@ -473,6 +473,8 @@ def apply_script(scripts_list, db_connection): pass +CREATOR_NAME = "Alexey Kallistov" +ABOUT_MESSAGE = "Tasker {0}\nCopyright (c)\n{1},\n{2}" HELP_TEXT = get_help() TABLE_FILE = 'tasks.db' LOG_EVENTS = { diff --git a/src/tasker.pyw b/src/tasker.pyw index 9bce837..e281d3b 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -1683,10 +1683,10 @@ class MainMenu(tk.Menu): def aboutwindow(self): showinfo("About Tasker", - "Tasker {0}.\nCopyright (c) Alexey Kallistov, {1}".format( + core.ABOUT_MESSAGE.format( GLOBAL_OPTIONS['version'], - datetime.datetime.strftime(datetime.datetime.now(), - "%Y"))) + core.CREATOR_NAME, + datetime.datetime.strftime(datetime.datetime.now(), "%Y"))) def exit(self): ROOT_WINDOW.destroy() From 8771538e9bae6c70385da6932df778f7ac536dcf Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 29 Mar 2019 14:34:23 +0300 Subject: [PATCH 34/55] Minor refactoring. --- src/core.py | 2 -- src/tasker.pyw | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/core.py b/src/core.py index 2cd1869..98805df 100644 --- a/src/core.py +++ b/src/core.py @@ -166,8 +166,6 @@ def insert_task_activity(self, task_id, spent_time, date=None): def update_preserved_tasks(self, tasks): if type(tasks) is not str: tasks = ','.join(map(str, tasks)) - if len(tasks) == 1: - tasks += "," self.update(table='options', field='value', value=tasks, field_id='tasks', updfiled='name') diff --git a/src/tasker.pyw b/src/tasker.pyw index e281d3b..8ad43ff 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -1993,8 +1993,7 @@ if __name__ == "__main__": # Global tasks ids set. Used for preserve duplicates: if GLOBAL_OPTIONS["tasks"]: GLOBAL_OPTIONS["tasks"] = dict.fromkeys( - [int(x) for x in GLOBAL_OPTIONS["tasks"].split(",") if len(x) > 0], - False) + map(int, str(GLOBAL_OPTIONS["tasks"]).split(",")), False) else: GLOBAL_OPTIONS["tasks"] = dict() # List of preserved tasks which are not open: From bca9bf61eca27ad2988fbe66111e86fad847729a Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 29 Mar 2019 17:33:42 +0300 Subject: [PATCH 35/55] Removed unnecessary frames_refill(). --- src/tasker.pyw | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 8ad43ff..b0a00d5 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -360,11 +360,12 @@ class TaskFrame(tk.Frame): self.timer = self.timer_label.after( GLOBAL_OPTIONS["TIMER_INTERVAL"], self.timer_update, counter) - def timer_start(self, log=True): + def timer_start(self, log=True, stop_all=True): """Counter start.""" if not self.running: if GLOBAL_OPTIONS["toggle_tasks"]: - ROOT_WINDOW.stop_all() + if stop_all: + ROOT_WINDOW.stop_all() GLOBAL_OPTIONS["tasks"][self.task["id"]] = True # Setting current timestamp: self.start_time = time.time() @@ -1528,17 +1529,6 @@ class MainFrame(elements.ScrolledCanvas): GLOBAL_OPTIONS["paused"].clear() self.fill() - def frames_refill(self): - """Reload data in every task frame with data.""" - for w in self.content_frame.winfo_children(): - if hasattr(w, 'task'): - if w.task: - state = w.running - w.timer_stop(log=False) - w.prepare_task(w.db.select_task(w.task["id"])) - if state: - w.timer_start(log=False) - def fill(self): """Create contents of the main frame.""" if self.frames_count < GLOBAL_OPTIONS['timers_count']: @@ -1580,8 +1570,9 @@ class MainFrame(elements.ScrolledCanvas): def resume_all(self): for frame in GLOBAL_OPTIONS["paused"]: - frame.timer_start() - GLOBAL_OPTIONS["paused"].clear() + frame.timer_start(stop_all=False) + if GLOBAL_OPTIONS["toggle_tasks"]: + GLOBAL_OPTIONS["paused"].clear() def stop_all(self): for frame in self.frames: @@ -1666,11 +1657,14 @@ class MainMenu(tk.Menu): self.change_parameter(params) # redraw taskframes if needed: ROOT_WINDOW.taskframes.fill() - ROOT_WINDOW.taskframes.frames_refill() # Stop all tasks if exclusive run method has been enabled: - if GLOBAL_OPTIONS["toggle_tasks"] and \ - GLOBAL_OPTIONS["toggle_tasks"] != toggle: + if params['toggle_tasks'] and params['toggle_tasks'] != toggle and\ + len([x for x in GLOBAL_OPTIONS["tasks"].values() if x]) > 1: ROOT_WINDOW.stop_all() + if params['toggle_tasks'] and params['toggle_tasks'] != toggle and\ + len(GLOBAL_OPTIONS["paused"]) > 1: + GLOBAL_OPTIONS["paused"].clear() + ROOT_WINDOW.pause_all() ROOT_WINDOW.lift() def change_parameter(self, paramdict): From c9f3681479d2e4f4355e4f6bf8df96ada000b608 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Fri, 29 Mar 2019 17:36:35 +0300 Subject: [PATCH 36/55] Comment removed. --- src/tasker.pyw | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index b0a00d5..66809ce 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -441,8 +441,6 @@ class TimestampCommentWindow(Window): self.title("Timestamp comment") elements.SimpleLabel(self, text="Enter comment:", fontsize=10).grid( row=0, column=0, columnspan=2, pady=5, padx=5, sticky='we') - # self.comment_area = Description(self, paste_menu=True, width=60, - # height=6) self.comment_area = elements.SimpleEntry(self) self.comment_area.config(state='normal', bg='white') self.comment_area.grid(row=1, column=0, columnspan=2, sticky='we') From eb55294543d704a374d70815ede176da7588d71f Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 30 Mar 2019 08:25:05 +0300 Subject: [PATCH 37/55] frames_timer_indicator_update() added. --- src/tasker.pyw | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 66809ce..ac42ab1 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -308,7 +308,7 @@ class TaskFrame(tk.Frame): GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task self.current_date = core.date_format(datetime.datetime.now()) - self.timer_label.config(text=core.time_format(self.get_current_time())) + self.configure_indicator() self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') self.start_button.config(image=os.curdir + '/resource/start_normal.png' @@ -324,12 +324,15 @@ class TaskFrame(tk.Frame): if GLOBAL_OPTIONS["preserve_tasks"]: self.db.update_preserved_tasks(GLOBAL_OPTIONS["tasks"]) - def get_current_time(self): - """Return current_time depending on time displaying options value.""" + def configure_indicator(self): + """Configure timer indicator depending on time displaying options value.""" if GLOBAL_OPTIONS["show_today"]: - return self.task["spent_today"] + current_spent = self.task["spent_today"] else: - return self.task["spent_total"] + current_spent = self.task["spent_total"] + self.timer_label.config(text=core.time_format( + current_spent if current_spent < 86400 + else self.task["spent_today"])) def task_update(self): """Updates time in the database.""" @@ -346,10 +349,7 @@ class TaskFrame(tk.Frame): self.task["spent_today"] += spent self.task["spent_total"] += spent self.start_time = time.time() - current_spent = self.get_current_time() - self.timer_label.config(text=core.time_format( - current_spent if current_spent < 86400 - else self.task["spent_today"])) + self.configure_indicator() # Every n seconds counter value is saved in database: if counter >= GLOBAL_OPTIONS["SAVE_INTERVAL"]: self.task_update() @@ -1527,6 +1527,12 @@ class MainFrame(elements.ScrolledCanvas): GLOBAL_OPTIONS["paused"].clear() self.fill() + def frames_timer_indicator_update(self): + """Explicitly reload timer in every task frame.""" + for frame in self.frames: + if not frame.running: + frame.configure_indicator() + def fill(self): """Create contents of the main frame.""" if self.frames_count < GLOBAL_OPTIONS['timers_count']: @@ -1655,6 +1661,7 @@ class MainMenu(tk.Menu): self.change_parameter(params) # redraw taskframes if needed: ROOT_WINDOW.taskframes.fill() + ROOT_WINDOW.taskframes.frames_timer_indicator_update() # Stop all tasks if exclusive run method has been enabled: if params['toggle_tasks'] and params['toggle_tasks'] != toggle and\ len([x for x in GLOBAL_OPTIONS["tasks"].values() if x]) > 1: From be576dbba6c9a85b9c6c51084624294ea6c270a1 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sat, 30 Mar 2019 10:29:28 +0300 Subject: [PATCH 38/55] Different fixes. --- src/tasker.pyw | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index ac42ab1..c6eb474 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -266,6 +266,8 @@ class TaskFrame(tk.Frame): if self in GLOBAL_OPTIONS["paused"]: GLOBAL_OPTIONS["paused"].remove(self) self.add_timestamp(core.LOG_EVENTS["STOP"], message) + if len(GLOBAL_OPTIONS["paused"]) == 0: + ROOT_WINDOW.pause_all() if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) if GLOBAL_OPTIONS["preserve_tasks"]: @@ -273,6 +275,7 @@ class TaskFrame(tk.Frame): for w in self.winfo_children(): w.destroy() self.create_content() + ROOT_WINDOW.taskframes.fill() def name_dialogue(self): """Task selection window.""" @@ -291,11 +294,20 @@ class TaskFrame(tk.Frame): self.timer_stop() # If there is open task, we remove it from running tasks set: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) + if self in GLOBAL_OPTIONS["paused"]: + GLOBAL_OPTIONS["paused"].remove(self) + self.add_timestamp(core.LOG_EVENTS["STOP"], "Another task opened in the frame.") + if len(GLOBAL_OPTIONS["paused"]) == 0: + ROOT_WINDOW.pause_all() self.get_restored_task_name(task_id) else: # If selected task is already opened in another frame: - if self.task["id"] != task_id: - showinfo("Task exists", "Task is already opened.") + message = "Task exists", "Task is already opened." + if not self.task: + showinfo(*message) + else: + if self.task["id"] != task_id: + showinfo(*message) def get_restored_task_name(self, taskid): # Preparing new task: @@ -326,13 +338,14 @@ class TaskFrame(tk.Frame): def configure_indicator(self): """Configure timer indicator depending on time displaying options value.""" - if GLOBAL_OPTIONS["show_today"]: - current_spent = self.task["spent_today"] - else: - current_spent = self.task["spent_total"] - self.timer_label.config(text=core.time_format( - current_spent if current_spent < 86400 - else self.task["spent_today"])) + if self.task: + if GLOBAL_OPTIONS["show_today"]: + current_spent = self.task["spent_today"] + else: + current_spent = self.task["spent_total"] + self.timer_label.config(text=core.time_format( + current_spent if current_spent < 86400 + else self.task["spent_today"])) def task_update(self): """Updates time in the database.""" From 2a352f0969f3a65ea3b08dd37cfca8bf03b89c8b Mon Sep 17 00:00:00 2001 From: drevoborod Date: Sun, 31 Mar 2019 18:29:04 +0300 Subject: [PATCH 39/55] Time displaying changed. --- src/core.py | 17 +++++++++-------- src/tasker.pyw | 17 ++++++++--------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/core.py b/src/core.py index 98805df..ea9281b 100644 --- a/src/core.py +++ b/src/core.py @@ -405,14 +405,15 @@ def write_to_disk(filename, text): def time_format(sec): """Returns time string in readable format.""" - if sec < 86400: - return time.strftime("%H:%M:%S", time.gmtime(sec)) - else: - day = int(sec // 86400) - if day == 1: - return "1 day" - else: - return "{} days".format(day) + days = int(sec // 86400) + time_ = time.strftime("%H:%M:%S", time.gmtime(sec % 86400)) + prefix = "%d days, " % days + if days == 0: + prefix = '' + elif str(days).endswith("1"): + if days != 11: + prefix = "{} day, ".format(days) + return "{}{}".format(prefix, time_) def date_format(date, template=DATE_TEMPLATE): diff --git a/src/tasker.pyw b/src/tasker.pyw index c6eb474..66fb0a3 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -164,7 +164,7 @@ class TaskFrame(tk.Frame): opacity='left') self.start_button.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: - self.timer_label = TaskLabel(self, width=10, state='disabled') + self.timer_label = TaskLabel(self, width=16, state='disabled') elements.big_font(self.timer_label, size=20) self.timer_label.grid(row=3, column=1, pady=5) self.add_timestamp_button = elements.CanvasButton( @@ -296,7 +296,8 @@ class TaskFrame(tk.Frame): GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) if self in GLOBAL_OPTIONS["paused"]: GLOBAL_OPTIONS["paused"].remove(self) - self.add_timestamp(core.LOG_EVENTS["STOP"], "Another task opened in the frame.") + self.add_timestamp(core.LOG_EVENTS["STOP"], + "Another task opened in the frame.") if len(GLOBAL_OPTIONS["paused"]) == 0: ROOT_WINDOW.pause_all() self.get_restored_task_name(task_id) @@ -340,12 +341,10 @@ class TaskFrame(tk.Frame): """Configure timer indicator depending on time displaying options value.""" if self.task: if GLOBAL_OPTIONS["show_today"]: - current_spent = self.task["spent_today"] + spent = self.task["spent_today"] else: - current_spent = self.task["spent_total"] - self.timer_label.config(text=core.time_format( - current_spent if current_spent < 86400 - else self.task["spent_today"])) + spent = self.task["spent_total"] + self.timer_label.config(text=core.time_format(spent)) def task_update(self): """Updates time in the database.""" @@ -651,7 +650,7 @@ class TaskSelectionWindow(Window): pady=5, padx=5, sticky='w') # Summarized time of all tasks in the table: - self.fulltime_frame = TaskLabel(self, width=13, anchor='center') + self.fulltime_frame = TaskLabel(self, width=16, anchor='center') self.fulltime_frame.grid(row=3, column=1, padx=6, pady=5, sticky='e') # Selected task description: self.description_area = Description(self, height=4) @@ -1016,7 +1015,7 @@ class TaskEditWindow(Window): padx=5, pady=5, sticky='w') # Frame containing time: - TaskLabel(self, width=11, + TaskLabel(self, width=16, text='{}'.format( core.time_format(self.task["spent_total"]))).grid( row=6, column=1, pady=5, padx=5, sticky='w') From 88a11dcbf9c56721af4949dcece163ca598b5fc9 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Mon, 1 Apr 2019 08:56:03 +0300 Subject: [PATCH 40/55] Fixed bug when negative values of spent time were stored to database. --- src/tasker.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index 66fb0a3..bc37be6 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -320,7 +320,6 @@ class TaskFrame(tk.Frame): # Adding task id and state to dictionary of running tasks: GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task - self.current_date = core.date_format(datetime.datetime.now()) self.configure_indicator() self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') @@ -374,6 +373,7 @@ class TaskFrame(tk.Frame): def timer_start(self, log=True, stop_all=True): """Counter start.""" + self.current_date = core.date_format(datetime.datetime.now()) if not self.running: if GLOBAL_OPTIONS["toggle_tasks"]: if stop_all: From 963d884336ef9376045fc597a333fa6035ed8408 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Mon, 1 Apr 2019 18:01:23 +0300 Subject: [PATCH 41/55] Changed logic of timestamps adding. --- src/tasker.pyw | 130 +++++++++++++++++++++++++++---------------------- 1 file changed, 71 insertions(+), 59 deletions(-) diff --git a/src/tasker.pyw b/src/tasker.pyw index bc37be6..83f4893 100755 --- a/src/tasker.pyw +++ b/src/tasker.pyw @@ -192,6 +192,7 @@ class TaskFrame(tk.Frame): command=self.clear) self.clear_button.grid(row=3, column=5, sticky='e', padx=5) self.running = False + self.paused = False def normal_interface(self): """Creates elements which are visible only in full interface mode.""" @@ -249,7 +250,6 @@ class TaskFrame(tk.Frame): self.timer_stop() else: self.timer_start() - GLOBAL_OPTIONS["paused"].discard(self) def properties_window(self): """Task properties window.""" @@ -263,11 +263,10 @@ class TaskFrame(tk.Frame): """Recreation of frame contents.""" message = "Task frame cleared." self.timer_stop(log_message=message) - if self in GLOBAL_OPTIONS["paused"]: - GLOBAL_OPTIONS["paused"].remove(self) + if self.paused: self.add_timestamp(core.LOG_EVENTS["STOP"], message) - if len(GLOBAL_OPTIONS["paused"]) == 0: - ROOT_WINDOW.pause_all() + if len(get_paused_taskframes()) == 0: + ROOT_WINDOW.change_paused_state() if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) if GLOBAL_OPTIONS["preserve_tasks"]: @@ -294,12 +293,12 @@ class TaskFrame(tk.Frame): self.timer_stop() # If there is open task, we remove it from running tasks set: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) - if self in GLOBAL_OPTIONS["paused"]: - GLOBAL_OPTIONS["paused"].remove(self) + if self.paused: + self.paused = False self.add_timestamp(core.LOG_EVENTS["STOP"], "Another task opened in the frame.") - if len(GLOBAL_OPTIONS["paused"]) == 0: - ROOT_WINDOW.pause_all() + if len(get_paused_taskframes()) == 0: + ROOT_WINDOW.change_paused_state() self.get_restored_task_name(task_id) else: # If selected task is already opened in another frame: @@ -373,32 +372,42 @@ class TaskFrame(tk.Frame): def timer_start(self, log=True, stop_all=True): """Counter start.""" - self.current_date = core.date_format(datetime.datetime.now()) if not self.running: - if GLOBAL_OPTIONS["toggle_tasks"]: - if stop_all: - ROOT_WINDOW.stop_all() - GLOBAL_OPTIONS["tasks"][self.task["id"]] = True - # Setting current timestamp: - self.start_time = time.time() - self.running = True self.start_button.config( image=os.curdir + '/resource/stop.png' if tk.TkVersion >= 8.6 else os.curdir + '/resource/stop.pgm') self.startstop_var.set("Stop") + was_paused = False if log: - if self in GLOBAL_OPTIONS["paused"]: + if self.paused: event_id = core.LOG_EVENTS["RESUME"] comment = "Task resumed." + was_paused = True else: event_id = core.LOG_EVENTS["START"] comment = "Task started." self.add_timestamp(event_id, comment) - self.task_update() + if GLOBAL_OPTIONS["toggle_tasks"]: + if stop_all and not was_paused: + ROOT_WINDOW.stop_all() + GLOBAL_OPTIONS["tasks"][self.task["id"]] = True + self.current_date = core.date_format(datetime.datetime.now()) + self.task_update() + # Setting current timestamp: + self.start_time = time.time() + self.running = True + self.paused = False + if not get_paused_taskframes(): + ROOT_WINDOW.change_paused_state() self.timer_update() - def timer_stop(self, log=True, log_message=None): + def timer_stop(self, log=True, log_message=None, paused=False): """Stop counter and save its value to database.""" + event_id = core.LOG_EVENTS["STOP"] + comment = "Task stopped." if not log_message else log_message + if self.paused: + if log: + self.add_timestamp(event_id, comment) if self.running: # after_cancel() stops execution of callback with given ID. self.timer_label.after_cancel(self.timer) @@ -407,19 +416,17 @@ class TaskFrame(tk.Frame): # Writing value into database: self.task_update() self.update_description() - if log: - if self in GLOBAL_OPTIONS["paused"]: - event_id = core.LOG_EVENTS["PAUSE"] - comment = "Task paused." - else: - event_id = core.LOG_EVENTS["STOP"] - comment = "Task stopped." if not log_message else log_message - self.add_timestamp(event_id, comment) + if paused: + event_id = core.LOG_EVENTS["PAUSE"] + comment = "Task paused." self.start_button.config( image=os.curdir + '/resource/start_normal.png' if tk.TkVersion >= 8.6 else os.curdir + '/resource/start_normal.pgm') self.startstop_var.set("Start") + if log: + self.add_timestamp(event_id, comment) + self.paused = paused def update_description(self): """Update text in "Description" field.""" @@ -433,8 +440,7 @@ class TaskFrame(tk.Frame): """Closes frame and writes counter value into database.""" message = "Task stopped on application exit." self.timer_stop(log_message=message) - if self in GLOBAL_OPTIONS["paused"]: - GLOBAL_OPTIONS["paused"].remove(self) + if self.paused: self.add_timestamp(core.LOG_EVENTS["STOP"], message) if self.task: GLOBAL_OPTIONS["tasks"].pop(self.task["id"]) @@ -1536,7 +1542,6 @@ class MainFrame(elements.ScrolledCanvas): for w in self.content_frame.winfo_children(): if hasattr(w, 'task'): w.clear() - GLOBAL_OPTIONS["paused"].clear() self.fill() def frames_timer_indicator_update(self): @@ -1581,22 +1586,16 @@ class MainFrame(elements.ScrolledCanvas): def pause_all(self): for frame in self.frames: if frame.running: - GLOBAL_OPTIONS["paused"].add(frame) - frame.timer_stop() + frame.timer_stop(paused=True) def resume_all(self): - for frame in GLOBAL_OPTIONS["paused"]: - frame.timer_start(stop_all=False) - if GLOBAL_OPTIONS["toggle_tasks"]: - GLOBAL_OPTIONS["paused"].clear() + for frame in self.frames: + if frame.paused: + frame.timer_start(stop_all=False) def stop_all(self): for frame in self.frames: - if frame.running: - frame.timer_stop() - if frame in GLOBAL_OPTIONS["paused"]: - frame.add_timestamp(core.LOG_EVENTS["STOP"], "Task stopped.") - GLOBAL_OPTIONS["paused"].clear() + frame.timer_stop() class MainMenu(tk.Menu): @@ -1675,13 +1674,14 @@ class MainMenu(tk.Menu): ROOT_WINDOW.taskframes.fill() ROOT_WINDOW.taskframes.frames_timer_indicator_update() # Stop all tasks if exclusive run method has been enabled: - if params['toggle_tasks'] and params['toggle_tasks'] != toggle and\ - len([x for x in GLOBAL_OPTIONS["tasks"].values() if x]) > 1: - ROOT_WINDOW.stop_all() - if params['toggle_tasks'] and params['toggle_tasks'] != toggle and\ - len(GLOBAL_OPTIONS["paused"]) > 1: - GLOBAL_OPTIONS["paused"].clear() - ROOT_WINDOW.pause_all() + if params['toggle_tasks'] and params['toggle_tasks'] != toggle: + if len([x for x in GLOBAL_OPTIONS["tasks"].values() if x]) != 1: + ROOT_WINDOW.stop_all() + paused = get_paused_taskframes() + if len(paused) > 1: + ROOT_WINDOW.change_paused_state() + for x in paused: + x.paused = False ROOT_WINDOW.lift() def change_parameter(self, paramdict): @@ -1917,24 +1917,28 @@ class MainWindow(tk.Tk): widget.destroy() self.taskframes.change_interface('small') + def change_paused_state(self, paused=False): + self.paused = paused + if not GLOBAL_OPTIONS["compact_interface"]: + if paused: + title = "Resume all" + else: + title = "Pause all" + self.pause_all_var.set(title) + def pause_all(self): if self.paused: - if not GLOBAL_OPTIONS["compact_interface"]: - self.pause_all_var.set("Pause all") self.taskframes.resume_all() - self.paused = False + self.change_paused_state() else: - if not GLOBAL_OPTIONS["compact_interface"]: - self.pause_all_var.set("Resume all") self.taskframes.pause_all() - self.paused = True + self.change_paused_state(True) def stop_all(self): """Stop all running timers.""" self.taskframes.stop_all() self.paused = False - if not GLOBAL_OPTIONS["compact_interface"]: - self.pause_all_var.set("Pause all") + self.change_paused_state() def destroy(self): answer = askyesno("Quit confirmation", "Do you really want to quit?") @@ -1954,6 +1958,15 @@ class MainWindow(tk.Tk): super().destroy() +def get_paused_taskframes(): + res = [] + for widget in ROOT_WINDOW.taskframes.content_frame.winfo_children(): + if hasattr(widget, "paused"): + if widget.paused: + res.append(widget) + return res + + def helpwindow(parent=None, text=None): """Show simple help window with given text.""" HelpWindow(parent, text) @@ -2013,8 +2026,7 @@ if __name__ == "__main__": GLOBAL_OPTIONS["selected_widget"] = None GLOBAL_OPTIONS.update({"MAX_TASKS": MAX_TASKS, "TIMER_INTERVAL": TIMER_INTERVAL, - "SAVE_INTERVAL": SAVE_INTERVAL, - "paused": set()}) + "SAVE_INTERVAL": SAVE_INTERVAL}) # Main window: ROOT_WINDOW = MainWindow() ROOT_WINDOW.mainloop() \ No newline at end of file From 2fa7f25be8b2d538fa996fd40b1423794786ac62 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Mon, 15 Jul 2019 15:20:13 +0300 Subject: [PATCH 42/55] Possibly fix: if task stopped yesterday, time for today erroneously was stored yesterday --- src/core.py | 6 +++++- src/{tasker.pyw => tracker.pyw} | 9 ++++----- 2 files changed, 9 insertions(+), 6 deletions(-) rename src/{tasker.pyw => tracker.pyw} (99%) diff --git a/src/core.py b/src/core.py index ea9281b..73523cd 100644 --- a/src/core.py +++ b/src/core.py @@ -133,7 +133,7 @@ def update_task(self, task_id, field="spent_time", value=0, prev_date=None): res = None if field == 'spent_time': now = datetime.datetime.now() - current_date = date_format(now) + current_date = today() daterow = self.check_task_activity_exists(task_id, prev_date) if current_date == prev_date: if daterow: @@ -426,6 +426,10 @@ def str_to_date(string, template=DATE_TEMPLATE): return datetime.datetime.strptime(string, template) +def today(): + return date_format(datetime.datetime.now()) + + def table_date_format(string, template=DATE_FULL_HUMAN_READABLE_TEMPLATE): """Formats date stored in database to more human-readable""" return date_format(str_to_date(string, DATE_STORAGE_TEMPLATE), template) diff --git a/src/tasker.pyw b/src/tracker.pyw similarity index 99% rename from src/tasker.pyw rename to src/tracker.pyw index 83f4893..c4e5eef 100755 --- a/src/tasker.pyw +++ b/src/tracker.pyw @@ -9,10 +9,8 @@ from collections import OrderedDict try: import tkinter as tk except ModuleNotFoundError: - import sys - sys.exit( - "Unable to start GUI. Please install Tk for Python: " - "https://docs.python.org/3/library/tkinter.html.") + exit("Unable to start GUI. Please install Tk for Python:\n" + "https://docs.python.org/3/library/tkinter.html.") from tkinter.filedialog import asksaveasfilename from tkinter.messagebox import askyesno, showinfo @@ -316,6 +314,7 @@ class TaskFrame(tk.Frame): def prepare_task(self, task): """Prepares frame elements to work with.""" + self.current_date = core.today() # Adding task id and state to dictionary of running tasks: GLOBAL_OPTIONS["tasks"][task["id"]] = False self.task = task @@ -391,8 +390,8 @@ class TaskFrame(tk.Frame): if stop_all and not was_paused: ROOT_WINDOW.stop_all() GLOBAL_OPTIONS["tasks"][self.task["id"]] = True - self.current_date = core.date_format(datetime.datetime.now()) self.task_update() + self.current_date = core.today() # Setting current timestamp: self.start_time = time.time() self.running = True From 383ea5e93f594eddd5492db00af2c34cae3cbba2 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Mon, 15 Jul 2019 19:37:45 +0300 Subject: [PATCH 43/55] Application renamed --- src/core.py | 3 ++- src/tracker.pyw | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/core.py b/src/core.py index 73523cd..407c710 100644 --- a/src/core.py +++ b/src/core.py @@ -477,7 +477,8 @@ def apply_script(scripts_list, db_connection): CREATOR_NAME = "Alexey Kallistov" -ABOUT_MESSAGE = "Tasker {0}\nCopyright (c)\n{1},\n{2}" +TITLE = "Time tracker" +ABOUT_MESSAGE = "Time tracker {0}\nCopyright (c)\n{1},\n{2}" HELP_TEXT = get_help() TABLE_FILE = 'tasks.db' LOG_EVENTS = { diff --git a/src/tracker.pyw b/src/tracker.pyw index c4e5eef..1fbe675 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -1692,7 +1692,7 @@ class MainMenu(tk.Menu): self.db.con.close() def aboutwindow(self): - showinfo("About Tasker", + showinfo("About %s" % core.TITLE, core.ABOUT_MESSAGE.format( GLOBAL_OPTIONS['version'], core.CREATOR_NAME, @@ -1845,7 +1845,7 @@ class MainWindow(tk.Tk): super().__init__(**options) # Default widget colour: GLOBAL_OPTIONS["colour"] = self.cget('bg') - self.title("Tasker") + self.title(core.TITLE) self.minsize(height=75, width=0) self.resizable(width=0, height=1) main_menu = MainMenu(self) # Create main menu. From 114174f3603dda46fc290afc6a9ffc8b6ad9dae5 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Tue, 16 Jul 2019 12:26:57 +0300 Subject: [PATCH 44/55] Probably fix of incorrect time displaying after starting of task stopped another day --- dev/developing_notes.txt | 38 +++----------------------------------- src/core.py | 18 +++++++++--------- src/tracker.pyw | 32 ++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 58 deletions(-) diff --git a/dev/developing_notes.txt b/dev/developing_notes.txt index ed24d13..3493005 100644 --- a/dev/developing_notes.txt +++ b/dev/developing_notes.txt @@ -1,35 +1,3 @@ -Что нужно в отчёте - -Вариант 1 -- Имя задачи -- Описание задачи -- Даты, в которые происходила работа над ней -- Время, затраченное на задачу в каждую дату -- Суммарное затраченное время - -Вариант 2 -- Дата -- Список задач, над которыми шла работа -- Описания задач -- Время, потраченное на каждую задачу -- Суммарное затраченное время на все задачи за эту дату - - -1) -select name, description, activity.date, activity.spent_time from tasks join activity on tasks.id=activity.task_id where tasks.id in (1,2) order by tasks.name, activity.date; -select name, fulltime from tasks join (select task_id, sum(spent_time) as fulltime from activity where task_id in (1,2, 3) group by task_id) as act on tasks.id=act.task_id; -{'task1': ['description', [('date1': 'time1'), ('date2': 'time2')], 'full time'], 'task2': ...} - -Task,Description,Dates,Time,Summarized working time -Task4,Another task,13.01.17,13:14:00,13:25:00 -,,14.02.17,00:43:12, -Task5,Also a task,13.01.17,00:11:00,00:11:00 - -2) -select date, tasks.name, tasks.description, spent_time from activity join tasks on activity.task_id=tasks.id where task_id in (1,2) order by date, tasks.name -select date, sum(spent_time) from activity where task_id in (1,2) group by date order by date -{'date1': [[['task1', 'description', 'time1'], ['task2', 'description', 'time1']], 'full time'], 'date2': ...} - -Date,Tasks,Description,Time,Summarized working time -13.01.17,Task4,Another task,13:14:00,13:25:00 -,Task5,Also a task,00:11:00, \ No newline at end of file +Варианты ситуаций, когда требуется запись таймера, при условии смены даты: +1. Задача выполнялась, запись при update или stop, вчера прошло больше, чем сегодня. +2. Задача выполнялась, запись при update или stop, вчера прошло меньше, чем сегодня. diff --git a/src/core.py b/src/core.py index 407c710..6fb22b5 100644 --- a/src/core.py +++ b/src/core.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -from collections import OrderedDict +from collections import OrderedDict, namedtuple import datetime import os import sqlite3 @@ -110,17 +110,17 @@ def insert_task(self, name): self.insert("tasks_tags", ("tag_id", "task_id"), (1, task_id)) return task_id - def update(self, field_id, field, value, table="tasks", updfiled="id"): + def update(self, field_id, field, value, table="tasks", updfield="id"): """Updates provided field in provided table with provided id using provided value """ self.exec_script( "UPDATE {0} SET {1}=? WHERE {3}='{2}'".format(table, field, - field_id, updfiled), + field_id, updfield), value) def check_task_activity_exists(self, task_id, date): """Returns rowid of row with task activity for provided date - if such activity exists""" + if such activity exists, otherwise returns None""" self.exec_script("SELECT rowid FROM activity WHERE task_id={0}" " AND date='{1}'".format(task_id, date)) try: @@ -137,7 +137,7 @@ def update_task(self, task_id, field="spent_time", value=0, prev_date=None): daterow = self.check_task_activity_exists(task_id, prev_date) if current_date == prev_date: if daterow: - self.update(daterow, table='activity', updfiled='rowid', + self.update(daterow, table='activity', updfield='rowid', field=field, value=value) else: self.insert_task_activity(task_id, value, prev_date) @@ -146,13 +146,13 @@ def update_task(self, task_id, field="spent_time", value=0, prev_date=None): hours=now.hour, minutes=now.minute, seconds=now.second).total_seconds() if daterow: - self.update(daterow, table='activity', updfiled='rowid', - field=field, value=value - today_secs) + self.update(daterow, table='activity', updfield='rowid', + field=field, value=value - today_secs if value > today_secs else value) else: self.insert_task_activity(task_id, value - today_secs, prev_date) self.insert_task_activity(task_id, today_secs, current_date) - res = {"remainder": today_secs, "current_date": current_date} + res = namedtuple("res", "remained,current_date")(today_secs, current_date) else: self.update(task_id, field=field, value=value) return res @@ -167,7 +167,7 @@ def update_preserved_tasks(self, tasks): if type(tasks) is not str: tasks = ','.join(map(str, tasks)) self.update(table='options', field='value', value=tasks, - field_id='tasks', updfiled='name') + field_id='tasks', updfield='name') def delete(self, table="tasks", **field_values): """Removes several records using multiple "field in (values)" clauses. diff --git a/src/tracker.pyw b/src/tracker.pyw index 1fbe675..079d2ad 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -309,15 +309,18 @@ class TaskFrame(tk.Frame): def get_restored_task_name(self, taskid): # Preparing new task: - self.prepare_task( - self.db.select_task(taskid)) # Task parameters from database + self.set_task_data(taskid) + self.prepare_task() - def prepare_task(self, task): + def set_task_data(self, taskid): + """Get task data from database""" + self.task = self.db.select_task(taskid) # Task parameters from database + + def prepare_task(self): """Prepares frame elements to work with.""" self.current_date = core.today() # Adding task id and state to dictionary of running tasks: - GLOBAL_OPTIONS["tasks"][task["id"]] = False - self.task = task + GLOBAL_OPTIONS["tasks"][self.task["id"]] = False self.configure_indicator() self.task_label.config(text=self.task["name"]) self.start_button.config(state='normal') @@ -349,8 +352,8 @@ class TaskFrame(tk.Frame): value=self.task["spent_today"], prev_date=self.current_date) if res: - self.current_date = res["current_date"] - self.task["spent_today"] = res["remainder"] + self.current_date = res.current_date + self.task["spent_today"] = res.remained def timer_update(self, counter=0): """Renewal of the counter.""" @@ -390,8 +393,9 @@ class TaskFrame(tk.Frame): if stop_all and not was_paused: ROOT_WINDOW.stop_all() GLOBAL_OPTIONS["tasks"][self.task["id"]] = True - self.task_update() self.current_date = core.today() + self.set_task_data(self.task["id"]) + self.configure_indicator() # Setting current timestamp: self.start_time = time.time() self.running = True @@ -962,14 +966,14 @@ class TaskSelectionWindow(Window): """Record filter parameters to database and apply it.""" update = self.filter_query() self.db.update('filter_operating_mode', field='value', - value=operating_mode, table='options', updfiled='name') + value=operating_mode, table='options', updfield='name') self.db.update('filter', field='value', value=script, table='options', - updfiled='name') + updfield='name') self.db.update('filter_tags', field='value', value=','.join([str(x) for x in tags]), table='options', - updfiled='name') + updfield='name') self.db.update('filter_dates', field='value', value=','.join(dates), - table='options', updfiled='name') + table='options', updfield='name') if update != self.filter_query(): self.update_table() @@ -1687,7 +1691,7 @@ class MainMenu(tk.Menu): """Change option in the database.""" for key, value in paramdict.items(): self.db.update(table='options', field='value', value=value, - field_id=key, updfiled='name') + field_id=key, updfield='name') GLOBAL_OPTIONS[key] = value self.db.con.close() @@ -1949,7 +1953,7 @@ class MainWindow(tk.Tk): GLOBAL_OPTIONS["tasks"]): db.update(table='options', field='value', value=len(GLOBAL_OPTIONS["tasks"]), - field_id='timers_count', updfiled='name') + field_id='timers_count', updfield='name') else: tasks = '' db.update_preserved_tasks(tasks) From 463b9e12a0dbe0d2169cf98385b0683c12e219b7 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Tue, 16 Jul 2019 12:46:17 +0300 Subject: [PATCH 45/55] Minor logical issue fix --- src/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.py b/src/core.py index 6fb22b5..c9fdfc8 100644 --- a/src/core.py +++ b/src/core.py @@ -147,7 +147,7 @@ def update_task(self, task_id, field="spent_time", value=0, prev_date=None): seconds=now.second).total_seconds() if daterow: self.update(daterow, table='activity', updfield='rowid', - field=field, value=value - today_secs if value > today_secs else value) + field=field, value=value - today_secs) else: self.insert_task_activity(task_id, value - today_secs, prev_date) From 1159350e3843f5f92a1f9ed283161b752940caab Mon Sep 17 00:00:00 2001 From: Aleksey Date: Tue, 16 Jul 2019 15:56:33 +0300 Subject: [PATCH 46/55] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ece8828..93ec11b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# tasker +# Time tracker __Description__ A program for logging time spent on different tasks. @@ -12,7 +12,7 @@ Filtered tasks list can be exported as .csv. __Installation__ To run application, you need Python 3 with Tkinter framework to be installed. It can be found on https://www.python.org/downloads/ (tkinter is by default included) or in your repository if your operating system is Linux or *BSD. -After Python interpreter installation, just extract archive with the application and execute tasker.pyw. +After Python interpreter installation, just extract archive with the application and execute tracker.pyw. __License__ From 3ee96a413fb3ffc06cb7f65fc3243848bf8d0d4a Mon Sep 17 00:00:00 2001 From: drevoborod Date: Wed, 17 Jul 2019 17:06:19 +0300 Subject: [PATCH 47/55] Added global constant FONT_SIZE --- src/elements.py | 9 ++++++--- src/tracker.pyw | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/elements.py b/src/elements.py index e278d64..c8428a9 100644 --- a/src/elements.py +++ b/src/elements.py @@ -3,8 +3,11 @@ import tkinter as tk +FONTSIZE = 9 + + class Text(tk.Widget): - def __init__(self, fontsize=11, **kwargs): + def __init__(self, fontsize=FONTSIZE, **kwargs): super().__init__(**kwargs) big_font(self, fontsize) @@ -45,7 +48,7 @@ class CanvasButton(Text, tk.Canvas): def __init__(self, master=None, image=None, text=None, variable=None, width=None, height=None, textwidth=None, - textheight=None, fontsize=11, opacity=None, relief='raised', + textheight=None, fontsize=FONTSIZE, opacity=None, relief='raised', bg=None, bd=2, state='normal', takefocus=True, command=None): super().__init__(master=master) @@ -256,7 +259,7 @@ def reconf_canvas(self, event): self.canvbox.config(height=self.content_frame.winfo_height()) -def big_font(unit, size=9): +def big_font(unit, size=FONTSIZE): """Font size of a given unit change.""" fontname = tk.font.Font(font=unit['font']).actual()['family'] unit.config(font=(fontname, size)) diff --git a/src/tracker.pyw b/src/tracker.pyw index 079d2ad..9636547 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -145,7 +145,7 @@ class TaskFrame(tk.Frame): self.normal_interface() # Task name field: self.task_label = TaskLabel(self, width=50, anchor='w') - elements.big_font(self.task_label, size=14) + elements.big_font(self.task_label, size=elements.FONTSIZE + 3) self.task_label.grid(row=1, column=0, columnspan=5, padx=5, pady=5, sticky='w') self.open_button = elements.TaskButton(self, text="Task...", @@ -153,7 +153,7 @@ class TaskFrame(tk.Frame): self.open_button.grid(row=1, column=5, padx=5, pady=5, sticky='e') self.start_button = elements.CanvasButton( self, state='disabled', - fontsize=14, + fontsize=elements.FONTSIZE + 4, command=self.start_stop, variable=self.startstop_var, image=os.curdir + '/resource/start_disabled.png' @@ -163,7 +163,7 @@ class TaskFrame(tk.Frame): self.start_button.grid(row=3, column=0, sticky='wsn', padx=5) # Counter frame: self.timer_label = TaskLabel(self, width=16, state='disabled') - elements.big_font(self.timer_label, size=20) + elements.big_font(self.timer_label, size=elements.FONTSIZE + 8) self.timer_label.grid(row=3, column=1, pady=5) self.add_timestamp_button = elements.CanvasButton( self, @@ -196,7 +196,7 @@ class TaskFrame(tk.Frame): """Creates elements which are visible only in full interface mode.""" # 'Task name' text: self.l1 = tk.Label(self, text='Task name:') - elements.big_font(self.l1, size=12) + elements.big_font(self.l1, size=elements.FONTSIZE + 2) self.l1.grid(row=0, column=1, columnspan=3) # Task description field: self.description_area = Description(self, width=60, height=3) @@ -460,7 +460,7 @@ class TimestampCommentWindow(Window): self.comment_var = comment_var self.apply_var = apply_var self.title("Timestamp comment") - elements.SimpleLabel(self, text="Enter comment:", fontsize=10).grid( + elements.SimpleLabel(self, text="Enter comment:", fontsize=elements.FONTSIZE + 1).grid( row=0, column=0, columnspan=2, pady=5, padx=5, sticky='we') self.comment_area = elements.SimpleEntry(self) self.comment_area.config(state='normal', bg='white') @@ -995,14 +995,14 @@ class TaskEditWindow(Window): self.title("Task properties: {}".format( self.db.find_by_clause('tasks', 'id', taskid, 'name')[0][0])) self.minsize(width=400, height=300) - elements.SimpleLabel(self, text="Task name:", fontsize=10).grid( + elements.SimpleLabel(self, text="Task name:", fontsize=elements.FONTSIZE + 1).grid( row=0, column=0, pady=5, padx=5, sticky='w') # Frame containing task name: TaskLabel(self, width=60, height=1, bg=GLOBAL_OPTIONS["colour"], text=self.task["name"], anchor='w').grid(row=1, columnspan=5, sticky='ew', padx=6) tk.Frame(self, height=30).grid(row=2) - elements.SimpleLabel(self, text="Description:", fontsize=10).grid( + elements.SimpleLabel(self, text="Description:", fontsize=elements.FONTSIZE + 1).grid( row=3, column=0, pady=5, padx=5, sticky='w') # Task description frame. Editable: self.description_area = Description(self, paste_menu=True, width=60, @@ -1278,7 +1278,7 @@ class HelpWindow(Window): super().__init__(master=parent, **options) self.title("Help") main_frame = tk.Frame(self) - self.help_area = Description(main_frame, fontsize=13) + self.help_area = Description(main_frame, fontsize=elements.FONTSIZE + 2) self.help_area.insert(text) self.help_area.config(state='disabled') self.help_area.grid(row=0, column=0, sticky='news') @@ -1611,16 +1611,16 @@ class MainMenu(tk.Menu): underline=0) file.add_separator() file.add_command(label="Exit", command=self.exit, underline=1) - elements.big_font(file, 10) + elements.big_font(file, elements.FONTSIZE + 1) self.add_cascade(label="Main menu", menu=file, underline=0) helpmenu = tk.Menu(self, tearoff=0) helpmenu.add_command(label="Help...", command=lambda: helpwindow(parent=ROOT_WINDOW, text=core.HELP_TEXT)) helpmenu.add_command(label="About...", command=self.aboutwindow) - elements.big_font(helpmenu, 10) + elements.big_font(helpmenu, elements.FONTSIZE + 1) self.add_cascade(label="Help", menu=helpmenu) - elements.big_font(self, 10) + elements.big_font(self, elements.FONTSIZE + 1) def options_window(self): """Open options window.""" @@ -1719,7 +1719,7 @@ class Options(Window): elements.SimpleLabel(self, text="Task frames in main window: ").grid( row=0, column=0, sticky='w') counter_frame = tk.Frame(self) - fontsize = 9 + fontsize = elements.FONTSIZE elements.CanvasButton(counter_frame, text='<', command=self.decrease, fontsize=fontsize, height=fontsize * 3).grid( row=0, column=0) @@ -1797,7 +1797,7 @@ class ExportWindow(Window): self.title("Export parameters") self.task_ids = [x["id"] for x in data.values()] self.operating_mode_var = tk.IntVar(self) - elements.SimpleLabel(self, text="Export mode", fontsize=10).grid( + elements.SimpleLabel(self, text="Export mode", fontsize=elements.FONTSIZE + 1).grid( row=0, column=0, columnspan=2, sticky='ns', pady=5) elements.SimpleRadiobutton(self, text="Task-based", variable=self.operating_mode_var, From 28cb2209e0d336d1db512a1e81a9fd723317a81e Mon Sep 17 00:00:00 2001 From: drevoborod Date: Wed, 17 Jul 2019 17:11:50 +0300 Subject: [PATCH 48/55] FONT_SIZE applied to tables also --- src/tracker.pyw | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tracker.pyw b/src/tracker.pyw index 9636547..fa40ed9 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -493,8 +493,8 @@ class Table(tk.Frame): super().__init__(master=parent, **options) self.table = ttk.Treeview(self) style = ttk.Style() - style.configure(".", font=('Helvetica', 11)) - style.configure("Treeview.Heading", font=('Helvetica', 11)) + style.configure(".", font=('Helvetica', elements.FONTSIZE + 1)) + style.configure("Treeview.Heading", font=('Helvetica', elements.FONTSIZE + 1)) scroller = tk.Scrollbar(self) scroller.config(command=self.table.yview) self.table.config(yscrollcommand=scroller.set) From f156bef4054913ea86f5860f15114397ea425e23 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Thu, 18 Jul 2019 17:08:37 +0300 Subject: [PATCH 49/55] Readme and help updated --- README.md | 4 ++-- src/resource/help.txt | 17 ++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 93ec11b..af0e7d7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ __Description__ A program for logging time spent on different tasks. -Has three stopwatch frames on main screen for visually logging work process. +Has several stopwatch frames on main screen for visually tracking work process. Tasks can be filtered by tags and by dates. Every time the task is opened its state is saved to database and than user can find all tasks on which he had been working at in selected day. @@ -11,7 +11,7 @@ Filtered tasks list can be exported as .csv. __Installation__ -To run application, you need Python 3 with Tkinter framework to be installed. It can be found on https://www.python.org/downloads/ (tkinter is by default included) or in your repository if your operating system is Linux or *BSD. +To run application, you need Python 3 with Tkinter framework to be installed. It can be found on https://www.python.org/downloads/ (tkinter is by default included) or in your OS repository if your operating system is Linux or *BSD. After Python interpreter installation, just extract archive with the application and execute tracker.pyw. diff --git a/src/resource/help.txt b/src/resource/help.txt index 3d8f942..4e28a55 100644 --- a/src/resource/help.txt +++ b/src/resource/help.txt @@ -1,11 +1,6 @@ -На главном экране приложения расположены окошки, в которых может отображаться имя задачи и её описание, и самое главное - таймер. Рядом с таймером находится кнопка “Старт”, при нажатии на которую таймер и запускается. Таким образом, можно постоянно отслеживать потраченное на задачу время. При нажатии на кнопку “стоп” таймер останавливается, а его значение записывается в базу данных. Также туда записывается дата, когда эта запись была произведена. Это нужно для случая, когда над одной и той же задачей работа ведётся несколько дней. Тогда сохраняются все даты, когда над ней велась работа, и по ним можно потом сделать выборку. - -Таких таймеров на главном экране по умолчанию три (в дальнейшем будет добавлена настройка, с помощью которой можно будет менять количество произвольно), то есть можно одновременно следить за тремя задачами, запуская и останавливая их таймеры по отдельности или все сразу. - -Также для каждой задачи есть кнопки добавления и просмотра таймстемпов. Это нужно для случая, когда задача по ходу выполнения разбивается на временнЫе отрезки и требуется отслеживать, сколько времени потрачено на каждый. Ставим таймстемп (метку), а затем, когда потребуется посмотреть, сколько времени прошло с момента её установки, можно открыть окошко со списком таймстемпов и сразу увидеть там эту информацию. Там же можно удалить лишние таймстемпы. - -С помощью кнопки “Задача” (“Task”) с главного экрана можно попасть в окно, где отображается список задач и есть возможность их добавления. Сортировка списка производится по клику в заголовок соответствующей колонки. Кроме того, здесь же, с помощью кнопки “Фильтр” (“Filter”), можно отфильтровать задачи по датам, когда с ними производилась какая-либо работа, и по тегам. - -У каждой задачи есть набор свойств, которые можно увидеть с помощью нажатия на кнопку “Свойства” (“Properties”). При её нажатии открывается окно свойств, где можно отредактировать описание задачи, увидеть все даты, когда с ней производилась работа (на неё было потрачено время), а также задать для неё теги. Теги отображаются в виде списка чекбоксов. Рядом со списком есть кнопка редактирования тегов, открывающая окно, где можно добавлять новые и удалять старые теги. Свойства задачи также можно открыть с главного экрана. - -В окне выбора задач также находится кнопка “Экспорт” (“Export”). Эта кнопка позволяет экспортировать текущую выборку в список в формате .csv. В экспортируемом файле сохраняется название задачи, потраченное на неё время и дата её создания, а также суммарное время, потраченное на все задачи из представленного списка. \ No newline at end of file +Main screen of the application contains pack of timers. These timers can display time spent on different tasks (up to 10 tasks at one time). To set quantity of timers, use Main menu - Options. +To open or add new task, press "Task..." button in any timer frame. Task selection window shall appear. To add new task, just enter new task's name in corresponding field in the top of the window and press "Add task". Or select task from list below and press "Open". +After that, task will appear in timer frame on main screen. Here one can start/stop timer. Any time start/stop button is pressed, new "Timestamp" is created. To view all timestamps, press "View timestamps" button. "Timestamp" and "Time spent since" columns display data in time spent on the task terms, not real time. And "date and time" column displays local time when timestamp created. It's possible also to create your own timestamps using "Add timestamp" button and delete any timestamp from the list. +Every task has its properties - name, description, tags. All of them, except name (it's unique parameter and cannot be changed) can be edited in Properties window (opened by pressing eponymous button in timer frame or in task selection window). Here one can set description and tags. New tags also can be created here, and after that, assigned to any task (by setting checkboxes). +In task selection window filter can be applied. Tasks can be filtered by date ot tag, or both. When filter is enabled, it's button is coloured blue. +Also here is export button. Tasks list can be exported to .csv reports. Csv contains exactly same tasks that are in the list, e.g. if some filter is enabled, exported tasks also will be filtered. Exported data can be date-based or task-based. That means different grouping of tasks: by date (which tasks were spent time on for each date) or by task (what dates had been every task worked on). \ No newline at end of file From 09e88537f34a45682258f6783f91b8d2c94056d0 Mon Sep 17 00:00:00 2001 From: drevoborod Date: Fri, 19 Jul 2019 13:55:48 +0300 Subject: [PATCH 50/55] Task name field increased --- src/tracker.pyw | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tracker.pyw b/src/tracker.pyw index fa40ed9..84e45a9 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -147,7 +147,7 @@ class TaskFrame(tk.Frame): self.task_label = TaskLabel(self, width=50, anchor='w') elements.big_font(self.task_label, size=elements.FONTSIZE + 3) self.task_label.grid(row=1, column=0, columnspan=5, padx=5, pady=5, - sticky='w') + sticky='ew') self.open_button = elements.TaskButton(self, text="Task...", command=self.name_dialogue) self.open_button.grid(row=1, column=5, padx=5, pady=5, sticky='e') From 6fd5e412f46b5a41361f4723f094084088d244b8 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Thu, 24 Mar 2022 22:52:17 +0300 Subject: [PATCH 51/55] fixed bug when single tag was selected in filter --- src/tracker.pyw | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/tracker.pyw b/src/tracker.pyw index 84e45a9..d533fbe 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -1328,12 +1328,10 @@ class FilterWindow(Window): # Operating mode of the filter: "AND", "OR". self.operating_mode_var = tk.StringVar() # Lists of stored filter parameters: - stored_dates = \ - self.db.find_by_clause('options', 'name', 'filter_dates', 'value')[0][ - 0].split(',') - stored_tags = \ - self.db.find_by_clause('options', 'name', 'filter_tags', 'value')[0][ - 0].split(',') + stored_dates = self.db.find_by_clause( + 'options', 'name', 'filter_dates', 'value')[0][0].split(',') + stored_tags = str(self.db.find_by_clause( + 'options', 'name', 'filter_tags', 'value')[0][0]).split(',') if stored_tags[0]: # stored_tags[0] is string. stored_tags = list(map(int, stored_tags)) # Dates list: From 1bfdc155f1a8a4ba5c3ce19a89dcf0a62eedb35b Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 6 Apr 2022 10:57:17 +0300 Subject: [PATCH 52/55] added mouse scroll in main window. --- src/elements.py | 7 +++++++ src/tracker.pyw | 18 +++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/elements.py b/src/elements.py index c8428a9..ab707b0 100644 --- a/src/elements.py +++ b/src/elements.py @@ -258,6 +258,13 @@ def reconf_canvas(self, event): self.canvbox.configure(scrollregion=self.canvbox.bbox('all')) self.canvbox.config(height=self.content_frame.winfo_height()) + def mouse_vertical_scroll(self, event): + if event.num == 4 or event.delta > 0: + delta = -1 + else: + delta = 1 + self.canvbox.yview_scroll(delta, 'units') + def big_font(unit, size=FONTSIZE): """Font size of a given unit change.""" diff --git a/src/tracker.pyw b/src/tracker.pyw index d533fbe..40204d6 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -5,6 +5,7 @@ import datetime import os import time from collections import OrderedDict +from contextlib import suppress try: import tkinter as tk @@ -1572,17 +1573,20 @@ class MainFrame(elements.ScrolledCanvas): GLOBAL_OPTIONS['timers_count']: self.clear() self.content_frame.config(bg='#cfcfcf') + interface_items = [self.content_frame, *get_all_widget_children(self.content_frame, [])] + for item in interface_items: + item.bind("", self.mouse_vertical_scroll) # for Windows/OS X + item.bind("", self.mouse_vertical_scroll) # for Linux + item.bind("", self.mouse_vertical_scroll) def change_interface(self, interface): """Change interface type. Accepts keywords 'normal' and 'small'.""" for widget in self.content_frame.winfo_children(): - try: + with suppress(TclError): if interface == 'normal': widget.normal_interface() elif interface == 'small': widget.small_interface() - except TclError: - pass def pause_all(self): for frame in self.frames: @@ -1959,6 +1963,14 @@ class MainWindow(tk.Tk): super().destroy() +def get_all_widget_children(widget, children_list): + children = widget.winfo_children() + for child in children: + children_list.append(child) + get_all_widget_children(child, children_list) + return children_list + + def get_paused_taskframes(): res = [] for widget in ROOT_WINDOW.taskframes.content_frame.winfo_children(): From cf2edde91aaf47d6118f1ec9496681b21c87ee44 Mon Sep 17 00:00:00 2001 From: "a.kallistov" Date: Wed, 6 Apr 2022 15:12:09 +0300 Subject: [PATCH 53/55] added mouse scroll in tags and dates lists. --- src/elements.py | 8 ++++++-- src/tracker.pyw | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/elements.py b/src/elements.py index ab707b0..c554822 100644 --- a/src/elements.py +++ b/src/elements.py @@ -252,18 +252,22 @@ def __init__(self, parent=None, orientation="vertical", bd=2, **options): self.canvbox.bind("", self.reconf_canvas) self.canvbox.pack(fill="x" if orientation == "horizontal" else "both", expand=1) + self.orientation = orientation def reconf_canvas(self, event): """Resizing of canvas scrollable region.""" self.canvbox.configure(scrollregion=self.canvbox.bbox('all')) self.canvbox.config(height=self.content_frame.winfo_height()) - def mouse_vertical_scroll(self, event): + def mouse_scroll(self, event): if event.num == 4 or event.delta > 0: delta = -1 else: delta = 1 - self.canvbox.yview_scroll(delta, 'units') + if self.orientation == 'vertical': + self.canvbox.yview_scroll(delta, 'units') + else: + self.canvbox.xview_scroll(delta, 'units') def big_font(unit, size=FONTSIZE): diff --git a/src/tracker.pyw b/src/tracker.pyw index 40204d6..c3fd24b 100755 --- a/src/tracker.pyw +++ b/src/tracker.pyw @@ -1315,6 +1315,11 @@ class Tagslist(elements.ScrolledCanvas): anchor='w') # Setting dynamic variable value to previously saved state: item[1][0].set(state) + interface_items = [self.canvbox, *get_all_widget_children(self.content_frame, [])] + for item in interface_items: + item.bind("", self.mouse_scroll) # for Windows/OS X + item.bind("", self.mouse_scroll) # for Linux + item.bind("", self.mouse_scroll) class FilterWindow(Window): @@ -1575,9 +1580,9 @@ class MainFrame(elements.ScrolledCanvas): self.content_frame.config(bg='#cfcfcf') interface_items = [self.content_frame, *get_all_widget_children(self.content_frame, [])] for item in interface_items: - item.bind("", self.mouse_vertical_scroll) # for Windows/OS X - item.bind("", self.mouse_vertical_scroll) # for Linux - item.bind("", self.mouse_vertical_scroll) + item.bind("", self.mouse_scroll) # for Windows/OS X + item.bind("", self.mouse_scroll) # for Linux + item.bind("", self.mouse_scroll) def change_interface(self, interface): """Change interface type. Accepts keywords 'normal' and 'small'.""" From 0c2007f70fe0b5d12c97641f7ea945d704875c39 Mon Sep 17 00:00:00 2001 From: Alexey Kallistov Date: Sun, 10 Apr 2022 19:20:35 +0300 Subject: [PATCH 54/55] Delete changelog.txt --- changelog.txt | 101 -------------------------------------------------- 1 file changed, 101 deletions(-) delete mode 100644 changelog.txt diff --git a/changelog.txt b/changelog.txt deleted file mode 100644 index dd7ff3f..0000000 --- a/changelog.txt +++ /dev/null @@ -1,101 +0,0 @@ -v.1.5.2 -_Добавлено -1. В таблице задач отображается не только дата, но и время создания задачи. -2. При создании временнОй метки появляется окно для ввода комментария. -3. При запуске, остановке, паузе и возобновлении таймера автоматически добавляется - временнАя метка с соответствующим комментарием. -4. Переработано окно отображения временнЫх меток: - - новый табличный вид с возможностью сортировки; - - отображаются дата и время создания метки; - - отображается комментарий. -_Исправлено: -1. При удалении задачи она не удалялась из списка сохранённых задач. - - -v.1.5.1 -_Добавлено -1. Кнопка Pause all/Resume all. -_Исправлено: -1. Ошибка, если нет прав на запись в директорию, выбранную для экспорта csv. -2. Сбой при попытке экспортировать в csv список длиной в одну задачу. -3. Не работала фильтрация по датам. -4. ВременнЫе метки добавлялись и отображались относительно текущего дня, - а не всего потраченного на задачу времени, если в настройках отображения - было включено "Отображать время только для текущего дня". -5. Визуальный таймер не сбрасывался в 0:00, если включено - "Отображать время только для текущего дня". - - -v.1.5 -_Добавлено -1. Опция: отображать только сегодняшний день в таймерах. -2. Кнопка "Отмена" в окне редактирования опций. -3. Опция: На основном экране при запуске одной задачи можно останавливать остальные. -_Исправлено: -1. Размер текста сделан связанным во всём приложении. -2. Заданы имена окон приложения, отображаемые в панели задач (включая главное). -3. Поправлено поведение календаря в фильтре: при установке диапазона дат сбрасывать ранее установленное. -4. Исправлено отображение помощи. -5. Исправлено ошибочное сообщение о том, что задача уже открыта, если она ранее открывалась в другом фрейме. - - -v.1.4 -_Добавлено -1. Отчётность с возможностью выбора, что именно выгружать в отчёт. Есть воможность выбора между датами и задачами. -2. В настройках фильтра теперь можно задавать диапазон дат (с помощью календаря). -_Исправлено: -1. Название новой таски, состоящее только из цифр, некорректно учитывалось при фильтрации списка задач - на момент добавления этой таски. -2. Изменён дизайн кнопок увеличения и уменьшения количества отображаемых фреймов в настройках главного окна. -3. Изменён формат отображения даты и её хранения в БД. Это позволяет избежать лишних шагов по преобразованию - формата при сортировке. -4. Горячие клавиши не работали в главном окне. -5. Размер текста в приложении стандартизован. - - -v.1.3 -_Добавлено: -1. Добавлен "компактный режим". -2. Добавлена поддержка кнопок главного окна: 'S' для остановки всех тасок, 'C' для очистки окна, - 'Q'/'Esc' для выхода. -3. Добавлена возможность сохранения списка открытых задач при выходе. -_Исправлено: -1. Теперь закрываются ненужные соединения с БД. - - -v.1.2.2 -_Добавлено: -Теперь в окне с датами в свойствах задачи для каждой даты отображается время, -затраченное на данную задачу. - - -v.1.2.1 -_Исправлено: -1. Если включено "всегда наверху", диалоговое окно не позиционируется внутри границ экрана. -2. Поиск не работает, если у одной из тасок пустое описание. - - -v1.2 -_Добавлено: -1. Кастомные кнопки с изображениями (Refresh, search, start/stop...). -2. Закрытие главного окна крестиком работает так же, как и кнопкой "Quit". -3. Опция "всегда наверху" для разных платформ. -4. Закрывать все мелкие окошки по Esc. -_Исправлено: -1. Исправить поведение при прохождении таймера через начало суток. -2. Исправить интерфейс на Mac OS X (кастомные кнопки в этом помогли). -3. Сделать открытие новых окон над предыдущими, а не в дефолтном месте экрана. -4. Поменять местами кнопки Close и Delete в окне редактирования тегов. -5. Окошко настроек не получает фокус. -6. Переходить к найденному по кнопке "поиск". Если найденных много, то к первому. -7. Вставка текста в текстовое поле по правой мыши должна происходить в то место, - куда установлен курсор. -8. Копирование через контекстное меню должно копировать только выделенный фрагмент, - если он есть. -9. Сортировка по потраченному времени перестаёт работать после применения - сортировки по дате. -10. "Ignore case" в поиске не работает для описания таски. -11. В режиме фильтра "OR" применяются только условия "слева", т.е. только - соответствие дате. -12. В таблице tasks_tags дублируются записи: если такой тег для такой задачи - уже есть, он всё равно добавляется повторно. From 3245b6aea6ec9d55130b07eae37401b8046857b3 Mon Sep 17 00:00:00 2001 From: Alexey Kallistov Date: Sun, 10 Apr 2022 19:25:22 +0300 Subject: [PATCH 55/55] Version bump --- src/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.py b/src/core.py index c9fdfc8..9bd015f 100644 --- a/src/core.py +++ b/src/core.py @@ -516,7 +516,7 @@ def apply_script(scripts_list, db_connection): INSERT INTO options VALUES ('toggle_tasks', 0); INSERT INTO options VALUES ('tasks', ''); INSERT INTO options VALUES ('compact_interface', 0); - INSERT INTO options VALUES ('version', '1.5.2'); + INSERT INTO options VALUES ('version', '1.6.0'); INSERT INTO options VALUES ('install_time', datetime('now')); """ # PATCH_SCRIPTS = {