1 // Copyright (c) 2006 by Mike Sharov <msharov@users.sourceforge.net>
11 //----------------------------------------------------------------------
13 static const uint32_t c_RootItemId
= 0;
14 static const iff::fmt_t fmt_TodoList
= IFF_SFMT("TODO");
15 static const iff::fmt_t fmt_Item
= IFF_SFMT("TITM");
16 static const iff::fmt_t fmt_Dep
= IFF_SFMT("TDEP");
18 static const uint32_t c_CurrentVersion
= 1;
20 //----------------------------------------------------------------------
22 class DLL_LOCAL CTodoHeader
{
24 inline CTodoHeader (void) : m_Version (c_CurrentVersion
), m_Flags (0) { }
25 inline void read (istream
& is
) { is
>> m_Version
>> m_Flags
; }
26 inline void write (ostream
& os
) const { os
<< m_Version
<< m_Flags
; }
27 inline size_t stream_size (void) const { return (stream_size_of (m_Version
) + stream_size_of (m_Flags
)); }
29 uint32_t m_Version
; ///< File format's version number.
30 bitset
<32> m_Flags
; ///< Various flags. None for now.
33 STD_STREAMABLE (CTodoHeader
)
35 //----------------------------------------------------------------------
37 /// Default constructor.
38 CTodoDocument::CTodoDocument (void)
46 m_Todos
.insert (CTodoEntry());
47 m_Stack
.push_back (c_RootItemId
);
48 m_Selection
.push_back (0);
51 //--{ Internal data accessors }-----------------------------------------
53 /// Returns the location of item \p id in m_Todos.
54 inline CTodoDocument::iitem_t
CTodoDocument::FindItem (itemid_t id
)
56 return (m_Todos
.find (id
));
59 /// Returns the location of item \p id in m_Todos.
60 inline CTodoDocument::icitem_t
CTodoDocument::FindItem (itemid_t id
) const
62 return (m_Todos
.find (id
));
65 /// Returns the pointer to the currently selected item in m_Todos
66 inline CTodoDocument::iitem_t
CTodoDocument::FindCurrentItem (void)
68 assert (Selection() < m_List
.size());
69 return (FindItem (m_List
[Selection()].Id()));
72 /// Returns the range of dependencies for the current item.
73 /// This is the same as the list of items in m_List.
74 inline CTodoDocument::deprange_t
CTodoDocument::CurrentItemDeps (void)
76 return (equal_range (m_Deps
, CTodoDep(ItemId())));
79 CTodoDocument::rcentry_t
CTodoDocument::CurrentEntry (void) const
81 static const CTodoEntry s_NullEntry
;
82 return (Selection() < m_List
.size() ? m_List
[Selection()] : s_NullEntry
);
85 /// Returns the next available item id.
86 inline CTodoDocument::itemid_t
CTodoDocument::GetNextItemId (void) const
88 return (m_Todos
.back().Id() + 1);
91 /// Returns true if the current item is a directory under the current item.
92 bool CTodoDocument::ItemNameIsADirectory (rcfname_t name
) const
94 // Build the full path name of the supposed directory.
96 if (Filename()[0] != '/') {
98 if (!getcwd (VectorBlock (cwd
)))
99 throw libc_exception ("getcwd");
104 // Remove the trailing /.todo
105 dir
.erase (dir
.iat (dir
.rfind ('/')), dir
.end());
106 // Add the current entry stack (skip the root entry which is empty)
107 for (itstack_t::const_iterator i
= m_Stack
.begin()+1; i
< m_Stack
.end(); ++i
) {
108 icitem_t ii
= FindItem (*i
);
109 if (ii
->Priority() != CTodoEntry::priority_Directory
)
110 return (false); // The full path must be a directory
116 return (access (dir
, F_OK
) == 0);
119 //--{ Serialization }---------------------------------------------------
121 /// Reads the object from stream \p is.
122 void CTodoDocument::read (istream
& is
)
125 iff::ReadChunk (is
, h
, fmt_TodoList
);
126 iff::ReadVector (is
, m_Todos
, fmt_Item
);
127 iff::ReadVector (is
, m_Deps
, fmt_Dep
);
129 SetList (c_RootItemId
);
134 /// Writes the object to stream \p os.
135 void CTodoDocument::write (ostream
& os
) const
138 iff::WriteChunk (os
, h
, fmt_TodoList
);
139 iff::WriteVector (os
, m_Todos
, fmt_Item
);
140 iff::WriteVector (os
, m_Deps
, fmt_Dep
);
143 /// Returns the size of the written object.
144 size_t CTodoDocument::stream_size (void) const
146 return (iff::chunk_size_of (CTodoHeader()) +
147 iff::vector_size_of (m_Todos
) +
148 iff::vector_size_of (m_Deps
));
151 /// Opens \p filename and reads todo entries from it.
152 void CTodoDocument::Open (const string
& filename
)
154 CDocument::Open (filename
);
155 if (access (filename
, F_OK
)) // Check if it's a new document.
157 if (access (filename
, W_OK
)) // Check if it is read-only.
158 SetFlag (f_ReadOnly
);
160 buf
.read_file (filename
);
163 SetFlag (f_Changed
, false);
166 /// Saves the data to the currently open file.
167 void CTodoDocument::Save (void)
169 if (Flag (f_ReadOnly
) && access (Filename(), W_OK
)) {
170 MessageBox ("Can't save: this file is marked as read-only");
173 memblock
buf (stream_size_of (*this));
176 buf
.write_file (Filename());
177 SetFlag (f_Changed
, false);
180 /// Verifies and corrects any defects in the data.
181 void CTodoDocument::VerifyData (void)
183 foreach (tddepmap_t::iterator
, i
, m_Deps
) {
184 if (FindItem (i
->m_ItemId
) == m_Todos
.end() || FindItem (i
->m_DepId
) == m_Todos
.end()) {
185 assert (!"Found a dependency pointing to a nonexistent item!");
186 --(i
= m_Deps
.erase (i
));
191 /// Uses the current working directory to find its sublist in the tree.
192 void CTodoDocument::DescendByCwd (void)
194 if (ItemId() != c_RootItemId
) // Only autodescend from root
196 // Get current working directory
197 char cwdbuf
[PATH_MAX
];
198 if (!getcwd (VectorBlock (cwdbuf
)))
199 throw libc_exception ("getcwd");
201 cwd
.link (cwdbuf
, strlen (cwdbuf
));
202 // Determine the path to descend down
203 uoff_t pathBase
= cwd
.size();
204 const string
& fn (Filename());
205 string dir
; // Parse fn for ../../../.todo
206 for (uoff_t i
= 0; i
< fn
.size(); i
+= dir
.size() + 1) {
207 dir
.relink (fn
.iat(i
), min (fn
.find ('/', i
), fn
.size()) - i
);
209 pathBase
= cwd
.rfind ('/', pathBase
-1);
210 else if (dir
!= ".todo") // If the user specified some .todo explicitly,
211 return; // ... then don't descend.
213 // Now for each dir in the path see if there is a matching entry
214 for (uoff_t i
= pathBase
+ 1; i
< cwd
.size(); i
+= dir
.size() + 1) {
215 dir
.relink (cwd
.iat(i
), min (cwd
.find ('/', i
), cwd
.size()) - i
);
216 deprange_t r
= CurrentItemDeps();
217 for (idep_t j
= r
.first
; j
< r
.second
; ++j
) {
218 iitem_t ii
= FindItem (j
->m_DepId
);
219 if (ii
->Complete()) // Directory items are never complete
220 return; // ... so stop looking at that point.
221 if (ii
->Text() == dir
) { // Found the proper entry, so descend
222 SetSelection (distance (r
.first
, j
));
230 //--{ List manipulation }-----------------------------------------------
232 /// Sets the current list to \p lid
233 void CTodoDocument::SetList (itemid_t lid
)
236 m_Stack
.push_back (lid
);
238 m_Selection
.push_back (0);
242 /// Reloads the currently visible list from the overall data.
243 void CTodoDocument::ReloadList (void)
246 // Find the list of dependencies for item lid
247 deprange_t r
= CurrentItemDeps();
248 m_List
.reserve (distance (r
.first
, r
.second
));
249 const time_t c_OneDay
= 24 * 60 * 60;
250 const time_t tooOld (time(NULL
) - c_OneDay
);
251 // Insert all the items dependent on m_ItemId into m_List.
252 for (; r
.first
< r
.second
; ++r
.first
) {
253 iitem_t ii
= FindItem (r
.first
->m_DepId
);
254 if (!Flag (f_CompleteVisible
) && ii
->Complete() && ii
->Done() < tooOld
)
256 m_List
.push_back (*ii
);
258 SetSelection (min (Selection(), uoff_t (m_List
.size() - 1)));
261 /// Returns the true if \p i1 ought to appear before \p i2 in the visible list.
262 bool CTodoDocument::VisibleOrderLess (itemid_t id1
, itemid_t id2
) const
264 const icitem_t
i1 (FindItem (id1
)), i2 (FindItem (id2
));
265 // Sort to put completed items at the end, then sort by priority, and then by creation date.
266 return (i1
->Complete() < i2
->Complete() ||
267 (!i1
->Complete() && !i2
->Complete() && (i1
->Priority() < i2
->Priority() ||
268 (i1
->Priority() == i2
->Priority() && *i1
< *i2
))) ||
269 (i1
->Complete() && i2
->Complete() && (i1
->Done() > i2
->Done() ||
270 (i1
->Done() == i2
->Done() && *i1
< *i2
))));
273 /// Reorders the items in the list by canonical sort order.
274 void CTodoDocument::ResortVisibleList (void)
276 // Optimize for the already sorted list; hence insertion sort.
277 deprange_t r
= CurrentItemDeps();
278 for (idep_t j
, i
= r
.first
+ 1; i
< r
.second
; ++i
) {
279 for (j
= i
; j
-- > r
.first
&& !VisibleOrderLess (j
->m_DepId
, i
->m_DepId
););
280 rotate (++j
, i
, i
+ 1);
285 //--{ Entry editing }---------------------------------------------------
287 /// Verifies that the entry is or is not a directory as it says.
288 void CTodoDocument::UpdateEntryDirectoryFlag (iitem_t icuri
) const
290 if (ItemNameIsADirectory (icuri
->Text()))
291 icuri
->SetPriority (CTodoEntry::priority_Directory
);
292 else if (icuri
->Priority() == CTodoEntry::priority_Directory
)
293 icuri
->SetPriority (CTodoEntry::priority_Medium
);
296 /// Sets the current entry to \p e
297 void CTodoDocument::UpdateCurrentEntry (rcentry_t e
)
299 iitem_t
icuri (FindCurrentItem());
301 UpdateEntryDirectoryFlag (icuri
);
302 m_List
[Selection()] = *icuri
;
307 /// Removes the current entry.
308 void CTodoDocument::RemoveCurrentEntry (void)
310 iitem_t ii
= FindCurrentItem(); // Cache master list iterator while we still have the item to look for.
311 // Remove from dependency list.
312 deprange_t r
= CurrentItemDeps();
313 m_Deps
.erase (r
.first
+ Selection());
315 if (r
.first
== r
.second
) // Can't be cleanly done in UpdateItemProgress.
316 FindItem (ItemId())->SetHasSublist (false);
317 // Update the item in the master list.
318 if (!ii
->DelRef()) // If no more refs, then delete.
321 // Update visible list.
322 UpdateItemProgress (r
.first
, r
.second
);
326 void CTodoDocument::AppendEntry (void)
328 CTodoEntry
e (GetNextItemId());
329 assert (FindItem (e
.Id()) == m_Todos
.end());
330 UpdateEntryDirectoryFlag (&e
);
331 // To the complete list
333 // And create link to the current entry.
334 PasteLinkToEntry (e
.Id());
337 /// Marks the currently selected entry as 100% complete.
338 void CTodoDocument::MarkEntryComplete (void)
340 iitem_t
icuri (FindCurrentItem());
341 icuri
->MarkComplete (100 * !icuri
->Complete());
343 UpdateCompleteStatus (icuri
->Id());
347 /// Sets the priority of the currently selected entry to \p p.
348 void CTodoDocument::SetCurrentEntryPriority (CTodoEntry::EPriority p
)
350 iitem_t
icuri (FindCurrentItem());
351 if (icuri
->Priority() == CTodoEntry::priority_Directory
)
353 icuri
->SetPriority (p
);
358 /// Sets the currently active item as the current list.
359 void CTodoDocument::EnterCurrentEntry (void)
361 rcentry_t
e (CurrentEntry());
364 m_Stack
.push_back (e
.Id());
365 m_Selection
.push_back (0);
369 /// Goes up to the parent list of the current list.
370 void CTodoDocument::LeaveCurrentEntry (void)
372 if (m_Stack
.size() <= 1)
375 m_Selection
.pop_back();
379 /// Makes \p id a dependency of the current item.
380 void CTodoDocument::PasteLinkToEntry (itemid_t id
)
382 // Create a dependency link to it from the current entry.
383 deprange_t ir
= CurrentItemDeps();
384 for (idep_t d
= ir
.first
; d
< ir
.second
; ++d
)
385 if (d
->m_DepId
== id
)
386 return; // Already in this list
387 // Find the item in the main list and add a reference.
388 iitem_t i
= FindItem (id
);
389 if (i
== m_Todos
.end())
390 throw runtime_error ("the pasted item no longer exists");
391 m_Deps
.insert (ir
.second
, CTodoDep (ItemId(), i
->Id()));
394 // Update the new item's status.
395 FindItem(ItemId())->SetHasSublist (true);
396 UpdateCompleteStatus (i
->Id());
397 // Update the visible list.
399 // And select the new item.
400 for (m_Selection
.back() = 0; m_List
[Selection()].Id() != i
->Id(); ++m_Selection
.back());
404 //--{ Item progress recursive updating }--------------------------------
406 /// Updates the progress of the item given by dependency range [first, last)
407 void CTodoDocument::UpdateItemProgress (icdep_t first
, icdep_t last
)
409 const uint32_t nItems
= distance (first
, last
);
412 const itemid_t
rangeId (first
->m_ItemId
);
413 uint32_t progress
= 0;
414 for (; first
< last
; ++first
)
415 progress
+= FindItem(first
->m_DepId
)->Progress();
416 // +1 treats the parent item as part of the list.
417 progress
= min (progress
/ (nItems
+ 1), 100U);
418 iitem_t ii
= FindItem (rangeId
);
419 if (ii
== m_Todos
.end() || ii
->Progress() == progress
)
421 ii
->MarkComplete (progress
);
422 UpdateCompleteStatus (rangeId
); // Recurse to propagate the change up.
425 /// Updates progress values of item dependent on \p dep
426 void CTodoDocument::UpdateCompleteStatus (itemid_t dep
)
428 // Each item has a range of dependencies; ranges are sequential in m_Deps.
429 icdep_t rangeStart
= m_Deps
.begin(); // The start of the current range.
430 const icdep_t iEnd
= m_Deps
.end();
431 bool hasDep
= false; // Does current range have dep?
432 for (icdep_t i
= rangeStart
; i
< iEnd
; ++i
) {
433 if (i
->m_ItemId
!= rangeStart
->m_ItemId
) {
435 UpdateItemProgress (rangeStart
, i
);
439 hasDep
= hasDep
|| i
->m_DepId
== dep
;
442 UpdateItemProgress (rangeStart
, iEnd
);
445 /// Executes command \p c.
446 void CTodoDocument::OnCommand (cmd_t c
)
448 CDocument::OnCommand (c
);
450 case cmd_Item_Complete
: MarkEntryComplete(); break;
451 case cmd_List_Leave
: LeaveCurrentEntry(); break;
452 case cmd_List_Enter
: EnterCurrentEntry(); break;
453 case cmd_List_OldItems
: ToggleCompleteVisible(); break;
454 case cmd_Item_Delete
: RemoveCurrentEntry(); break;
455 case cmd_File_Save
: Save(); break;
456 case cmd_Item_Priority_Highest
:
457 case cmd_Item_Priority_High
:
458 case cmd_Item_Priority_Medium
:
459 case cmd_Item_Priority_Low
:
460 case cmd_Item_Priority_Lowest
:
461 SetCurrentEntryPriority (CTodoEntry::EPriority (c
- cmd_Item_Priority_Highest
+ 1));
464 if (Flag (f_Changed
)) {
465 int rv
= MessageBox ("There are unsaved changes. Save now?", MB_YesNoCancel
);
466 if (rv
== mbrv_Cancel
)
468 else if (rv
!= mbrv_No
)
471 CRootWindow::Instance().Close();
476 /// Updates visible flags of command \p rc.
477 void CTodoDocument::OnUpdateCommandUI (rcmd_t rc
) const
479 CDocument::OnUpdateCommandUI (rc
);
480 const bool bHaveSelection (Selection() < ListSize());
481 const bool bReadOnly (Flag (f_ReadOnly
));
484 case cmd_File_Save
: bActive
= !bReadOnly
&& Flag(f_Changed
);break;
485 case cmd_List_Leave
: bActive
= NestingDepth(); break;
486 case cmd_List_OldItems
:
487 rc
.SetFlag (SCmd::cf_Checked
, Flag (f_CompleteVisible
)); break;
488 case cmd_List_Enter
: bActive
= bHaveSelection
; break;
489 case cmd_Item_Delete
: bActive
= bHaveSelection
&& !bReadOnly
; break;
490 case cmd_Item_Complete
: bActive
= bHaveSelection
&& !bReadOnly
; break;
491 case cmd_Item_Priority
: bActive
= bHaveSelection
; break;
492 case cmd_Item_Priority_Highest
:
493 case cmd_Item_Priority_High
:
494 case cmd_Item_Priority_Medium
:
495 case cmd_Item_Priority_Low
:
496 case cmd_Item_Priority_Lowest
:
497 bActive
= bHaveSelection
&& !bReadOnly
;
498 rc
.SetFlag (SCmd::cf_Checked
, rc
.cmd
== CurrentEntry().Priority() + cmd_Item_Priority_Highest
- 1);
501 bActive
= !(rc
.flags
& SCmd::cf_Grayed
);
504 rc
.SetFlag (SCmd::cf_Grayed
, !bActive
);