unit main;
{$mode objfpc}{$H+}
interface
uses
SysUtils, DateUtils, Forms, Controls, Graphics, ExtCtrls, StdCtrls;
type
PTodoHistoryItem = ^TTodoHistoryItem;
TTodoHistoryItem = record
date : TDate;
done : Boolean;
doneByOthers : Boolean;
end;
PTodoItem = ^TTodoItem;
TTodoItem = record
id : Integer;
name : string;
repeatDays : Integer;
daysSinceDone : Integer;
done : Boolean;
doneByOthers : Boolean;
cancelled : Boolean;
planned : Boolean;
notPlanned : Boolean;
history : array of TTodoHistoryItem;
panel : TPanel;
updating : Boolean;
end;
PTodoList = ^TTodoList;
TTodoList = record
filename : string;
name : string;
nextId : Integer;
date : TDate;
items : array of TTodoItem;
end;
{ TfrmMain }
TfrmMain = class(TForm)
bAdd: TButton;
bNextDay: TButton;
cbPlanned: TCheckBox;
lDate: TLabel;
sbTodo: TScrollBox;
procedure bAddClick(Sender: TObject);
procedure bNextDayClick(Sender: TObject);
procedure cbPlannedChange(Sender: TObject);
procedure FormActivate(Sender: TObject);
procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
procedure OnChangeItem(Sender: TObject);
procedure UpdateItem(var item: TTodoItem);
procedure ClearTodoList();
procedure RebuildTodoList();
private
public
end;
var
frmMain: TfrmMain;
todoList: TTodoList;
implementation
{$R *.lfm}
function BoolToInt(b: Boolean): Integer;
begin
if b then Result := 1 else Result := 0;
end;
function DateToInt(date: TDate): Integer;
var
y, m, d: Word;
begin
DecodeDate(date, y, m, d);
Result := y*10000 + m*100 + d;
end;
function IntToDate(i: Integer): TDate;
begin
Result := EncodeDate(i div 10000, (i div 100) mod 100, i mod 100);
end;
function RoundDate(date: TDate): TDate;
begin
Result := IntToDate( DateToInt(date) );
end;
function isPlanned(var item: TTodoItem): Boolean;
begin
Result := item.planned or ((item.daysSinceDone >= item.repeatDays) and not item.notPlanned);
end;
function isDone(var item: TTodoItem): Boolean;
begin
Result := item.done or item.doneByOthers;
end;
function isNeedToDo(var item: TTodoItem): Boolean;
begin
Result := isPlanned(item) and (not item.cancelled) and not isDone(item);
end;
procedure ClearTodoList(var list: TTodoList);
begin
list.filename := '';
list.name := '';
list.nextId := 0;
setLength(list.items, 0);
end;
function SaveTodoList(var list: TTodoList; filename: string): Boolean;
var
i: Integer;
f: textfile;
begin
Result := False;
AssignFile(f, filename);
Rewrite(f);
Writeln(f, 2); // version
Writeln(f, list.name);
Writeln(f, list.nextId);
Writeln(f, DateToInt(list.date));
Writeln(f, Length(list.items));
for i := 0 to Length(list.items)-1 do begin
Writeln(f, list.items[i].id);
Writeln(f, list.items[i].name);
Writeln(f,
list.items[i].repeatDays, ' ',
list.items[i].daysSinceDone, ' ',
BoolToInt(list.items[i].done), ' ',
BoolToInt(list.items[i].doneByOthers), ' ',
BoolToInt(list.items[i].cancelled), ' ',
BoolToInt(list.items[i].planned), ' ',
BoolToInt(list.items[i].notPlanned) );
end;
CloseFile(f);
Result := True;
end;
function SaveTodoListEx(var list: TTodoList): Boolean;
var
i: Integer;
fo, fn: string;
begin
Result := False;
if FileExists(list.filename) then begin
for i := 0 to 1000 do begin
fo := list.filename + '.old.' + IntToStr(i);
if FileExists(fo) then fo := '' else break;
end;
for i := 0 to 1000 do begin
fn := list.filename + '.new.' + IntToStr(i);
if FileExists(fn) then fn := '' else break;
end;
if (fo = '') or (fn = '') then Exit;
if not SaveTodoList(list, fn) then begin
DeleteFile(fn);
Exit;
end;
if not RenameFile(list.filename, fo) then begin
DeleteFile(fn);
Exit;
end;
if not RenameFile(fn, list.filename) then begin
RenameFile(fo, list.filename);
DeleteFile(fn);
Exit;
end;
DeleteFile(fn);
DeleteFile(fo);
end else
if not SaveTodoList(list, list.filename) then begin
DeleteFile(list.filename);
Exit;
end;
Result := True;
end;
function LoadTodoList(var list: TTodoList; filename: string): Boolean;
var
i, version, count: Integer;
i0, i1, i2, i3, i4: Integer;
f: textfile;
begin
clearTodoList(list);
list.filename := filename;
Result := False;
if not FileExists(filename) then Exit;
AssignFile(f, filename);
Reset(f);
Readln(f, version);
Readln(f, list.name);
Readln(f, list.nextId);
Readln(f, i0);
list.date := IntToDate(i0);
Readln(f, count);
setLength(list.items, count);
for i := 0 to count-1 do begin
Readln(f, list.items[i].id);
Readln(f, list.items[i].name);
if version <= 1 then begin
Readln(f,
list.items[i].repeatDays,
list.items[i].daysSinceDone,
i0, i1, i2, i3 );
list.items[i].done := i0 <> 0;
list.items[i].doneByOthers := i1 <> 0;
list.items[i].cancelled := i3 <> 0;
list.items[i].planned := i2 <> 0;
list.items[i].notPlanned := False;
end else begin
Readln(f,
list.items[i].repeatDays,
list.items[i].daysSinceDone,
i0, i1, i2, i3, i4 );
list.items[i].done := i0 <> 0;
list.items[i].doneByOthers := i1 <> 0;
list.items[i].cancelled := i2 <> 0;
list.items[i].planned := i3 <> 0;
list.items[i].notPlanned := i4 <> 0;
end;
end;
CloseFile(f);
Result := True;
end;
procedure SetTodoListDate(var list: TTodoList; date: TDate);
var
i, d: Integer;
begin
date := RoundDate(date);
d := DaysBetween(date, list.date);
if (date < list.date) and (d > 0) then d := -d;
list.date := date;
if d = 0 then Exit;
for i := 0 to Length(list.items)-1 do begin
Inc(list.items[i].daysSinceDone, d);
if d < 0 then begin
if list.items[i].daysSinceDone < 1 then
list.items[i].daysSinceDone := 1;
end else begin
if isDone(list.items[i]) then begin
list.items[i].daysSinceDone := d;
list.items[i].planned := False;
end;
list.items[i].done := False;
list.items[i].doneByOthers := False;
list.items[i].cancelled := False;
list.items[i].notPlanned := False;
end
end
end;
procedure AddTodoItem(var list: TTodoList);
var
i: Integer;
begin
i := Length(list.items);
SetLength(list.items, i + 1);
list.items[i].id := list.nextId;
list.items[i].repeatDays := 1;
list.items[i].daysSinceDone := 1;
Inc(list.nextId);
end;
procedure RemoveTodoItem(var list: TTodoList; i: Integer);
var
j: Integer;
begin
if (i < 0) or (i >= Length(list.items)) then Exit;
for j := i+1 to Length(list.items)-1 do
list.items[j-1] := list.items[j];
SetLength(list.items, Length(list.items) - 1);
end;
procedure InsertTodoItem(var list: TTodoList; i: Integer; item: TTodoItem);
var
j: Integer;
begin
if i < 0 then i := 0;
if i > Length(list.items) then i := Length(list.items);
SetLength(list.items, Length(list.items) + 1);
for j := Length(list.items)-1 downto i+1 do
list.items[j] := list.items[j-1];
list.items[i] := item;
end;
procedure TfrmMain.bAddClick(Sender: TObject);
begin
AddTodoItem(todoList);
RebuildTodoList();
end;
procedure TfrmMain.bNextDayClick(Sender: TObject);
begin
SetTodoListDate(todoList, IncDay(todoList.date));
RebuildTodoList();
end;
procedure TfrmMain.cbPlannedChange(Sender: TObject);
begin
RebuildTodoList();
end;
procedure TfrmMain.FormActivate(Sender: TObject);
begin
if todoList.filename <> '' then Exit;
LoadTodoList(todoList, 'todo.list');
SetTodoListDate(todoList, Date);
RebuildTodoList();
end;
procedure TfrmMain.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
SaveTodoListEx(todoList);
CloseAction := caFree;
end;
procedure TfrmMain.OnChangeItem(Sender: TObject);
var
p: TPanel;
i: Integer;
n: string;
item: TTodoItem;
function ParseInt(s: string): Integer;
begin
Result := StrToInt(s);
if Result < 1 then Result := 1;
end;
begin
p := TPanel(TControl(Sender).Parent);
i := p.Tag;
n := TControl(Sender).Name;
if n = 'delete' then begin
RemoveTodoItem(todoList, i);
RebuildTodoList();
end else
if n = 'up' then begin
item := todoList.items[i];
RemoveTodoItem(todoList, i);
InsertTodoItem(todoList, i-1, item);
RebuildTodoList();
end else
if n = 'down' then begin
item := todoList.items[i];
RemoveTodoItem(todoList, i);
InsertTodoItem(todoList, i+1, item);
RebuildTodoList();
end else begin
with todoList.items[i] do begin
if updating then Exit;
name := TEdit( p.FindChildControl('name') ).Text;
repeatDays := ParseInt(TEdit( p.FindChildControl('repeatDays') ).Text);
daysSinceDone := ParseInt(TEdit( p.FindChildControl('daysSinceDone') ).Text);
done := TCheckBox( p.FindChildControl('done') ).Checked;
doneByOthers := TCheckBox( p.FindChildControl('doneByOthers') ).Checked;
cancelled := TCheckBox( p.FindChildControl('cancelled') ).Checked;
planned := TCheckBox( p.FindChildControl('planned') ).Checked;
notPlanned := TCheckBox( p.FindChildControl('notPlanned') ).Checked;
end;
UpdateItem(todoList.items[i]);
end;
SaveTodoListEx(todoList);
end;
procedure TfrmMain.UpdateItem(var item: TTodoItem);
begin
item.updating := True;
with item.panel do begin
if item.cancelled then
item.panel.Font.StrikeThrough := True
else
item.panel.Font.StrikeThrough := False;
if isNeedToDo(item) then
item.panel.Font.Color := clDefault
else
item.panel.Font.Color := clGrayText;
TCheckBox( FindChildControl('done') ).Checked := item.done;
TCheckBox( FindChildControl('doneByOthers') ).Checked := item.doneByOthers;
TCheckBox( FindChildControl('cancelled') ).Checked := item.cancelled;
TCheckBox( FindChildControl('planned') ).Checked := item.planned;
TCheckBox( FindChildControl('notPlanned') ).Checked := item.notPlanned;
TEdit( FindChildControl('daysSinceDone') ).Text := IntToStr(item.daysSinceDone);
TEdit( FindChildControl('repeatDays') ).Text := IntToStr(item.repeatDays);
TEdit( FindChildControl('name') ).Text := item.name;
end;
item.updating := False;
end;
procedure TfrmMain.ClearTodoList();
var
i: Integer;
begin
while sbTodo.ControlCount > 0 do
sbTodo.Controls[0].Destroy();
for i := 0 to Length(todoList.items)-1 do
todoList.items[i].panel := nil;
sbTodo.UpdateScrollbars;
end;
procedure TfrmMain.RebuildTodoList();
const
b = 0;
bb = 2;
sb = 16;
var
i, j, h, t, l, ll, r: Integer;
scroll: Integer;
plannedOnly: Boolean;
p: TPanel;
function createCheckBox(name, title: string): TCheckBox;
begin
Result := TCheckBox.Create(p);
Result.Name := name;
Result.Caption := '';
Result.Hint := title;
Result.ShowHint := True;
Result.Top := bb;
Result.Left := l;
Result.OnChange := @OnChangeItem;
p.InsertControl(Result);
Inc(l, Result.Width);
end;
function createButton(name, title: string): TButton;
begin
Result := TButton.Create(p);
Result.Name := name;
Result.Caption := title;
Result.ParentFont := True;
Result.Top := bb;
Result.Left := r - Result.Width;
Result.Anchors := [akTop, akRight];
Result.OnClick := @OnChangeItem;
p.InsertControl(Result);
r := Result.Left;
end;
function createSquareButton(name, title: string): TButton;
begin
Result := createButton(name, title);
Result.Left := Result.Left + Result.Width - Result.Height;
Result.Width := Result.Height;
r := Result.Left;
end;
function createEdit(name, title: string): TEdit;
begin
Result := TEdit.Create(p);
Result.Name := name;
Result.Hint := Title;
Result.ShowHint := True;
Result.AutoSize := False;
Result.BorderStyle := bsNone;
Result.ParentFont := True;
Result.ParentColor := True;
Result.Width := Result.Height*2;
Result.NumbersOnly := True;
Result.Top := bb;
Result.Left := r - Result.Width;
Result.Anchors := [akTop, akRight];
Result.OnEditingDone := @OnChangeItem;
p.InsertControl(Result);
r := Result.Left;
end;
function createNameEdit(name, title: string): TEdit;
begin
Result := createEdit(name, title);
Result.NumbersOnly := False;
Result.Left := l;
Result.Width := Result.Width + r - l;
Result.Anchors := [akLeft, akTop, akRight];
end;
begin
scroll := sbTodo.VertScrollBar.Position;
plannedOnly := cbPlanned.Checked;
ClearTodoList();
lDate.Caption := FormatDateTime('YYYY-MM-DD', todoList.date);
t := b;
for i := 0 to Length(todoList.items)-1 do begin
if plannedOnly and not isPlanned(todoList.items[i]) then
Continue;
p := TPanel.Create(sbTodo);
p.Tag := i;
p.Left := b;
p.Width := sbTodo.Width - b*2 - sb;
p.Top := t;
p.Anchors := [akLeft, akTop, akRight];
p.ParentFont := False;
sbTodo.InsertControl(p);
l := bb;
createCheckBox('done', 'Done');
createCheckBox('doneByOthers', 'Done by others');
createCheckBox('cancelled', 'Cancelled');
ll := l;
Inc(l, bb);
Inc(l, bb);
Inc(l, bb);
createCheckBox('planned', 'Planned manually').Visible := not plannedOnly;
createCheckBox('notPlanned', 'Remove from plan manually').Visible := not plannedOnly;
if plannedOnly then l := ll;
Inc(l, bb);
r := p.ClientWidth - bb;
createButton('delete', 'delete');
Dec(r, bb);
createSquareButton('down', '+');
createSquareButton('up', '-');
Dec(r, bb);
createEdit('daysSinceDone', 'Days since last done');
Dec(r, bb);
createEdit('repeatDays', 'Repeat every N days');
Dec(r, bb);
createNameEdit('name', 'Task name');
h := 0;
for j := 0 to p.ControlCount-1 do
if h < p.Controls[j].Top + p.Controls[j].Height then
h := p.Controls[j].Top + p.Controls[j].Height;
p.Height := h + bb;
Inc(t, p.Height + b);
todoList.items[i].panel := p;
UpdateItem(todoList.items[i]);
end;
sbTodo.UpdateScrollbars;
sbTodo.ScrollBy(0, -scroll);
end;
end.