Moved all document-related commands into the document
[ttodo.git] / tddoc.cc
blob412c0dedfb8bb71b1a11348ad01551c3d69d99a6
1 // Copyright (c) 2006 by Mike Sharov <msharov@users.sourceforge.net>
2 //
3 // tddoc.cc
4 //
6 #include "tddoc.h"
7 #include <iff.h>
8 #include <time.h>
9 #include <unistd.h>
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 {
23 public:
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)); }
28 public:
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)
39 : CDocument (),
40 m_Todos (),
41 m_List (),
42 m_Deps (),
43 m_Stack (),
44 m_Selection ()
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, CDependency(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.
95 string dir;
96 if (Filename()[0] != '/') {
97 char cwd [PATH_MAX];
98 if (!getcwd (VectorBlock (cwd)))
99 throw libc_exception ("getcwd");
100 dir = cwd;
102 dir += '/';
103 dir += Filename();
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
111 dir += '/';
112 dir += ii->Text();
114 dir += '/';
115 dir += name;
116 return (access (dir, F_OK) == 0);
119 //--{ Serialization }---------------------------------------------------
121 /// Reads the object from stream \p is.
122 void CTodoDocument::read (istream& is)
124 CTodoHeader h;
125 iff::ReadChunk (is, h, fmt_TodoList);
126 iff::ReadVector (is, m_Todos, fmt_Item);
127 iff::ReadVector (is, m_Deps, fmt_Dep);
128 VerifyData();
129 SetList (c_RootItemId);
130 SetSelection (0);
131 DescendByCwd();
134 /// Writes the object to stream \p os.
135 void CTodoDocument::write (ostream& os) const
137 CTodoHeader h;
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.
156 return;
157 if (access (filename, W_OK)) // Check if it is read-only.
158 SetFlag (f_ReadOnly);
159 memblock buf;
160 buf.read_file (filename);
161 istream is (buf);
162 read (is);
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");
171 return;
173 memblock buf (stream_size_of (*this));
174 ostream os (buf);
175 write (os);
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 (depmap_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
195 return;
196 // Get current working directory
197 char cwdbuf [PATH_MAX];
198 if (!getcwd (VectorBlock (cwdbuf)))
199 throw libc_exception ("getcwd");
200 string cwd;
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);
208 if (dir == "..")
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));
223 EnterCurrentEntry();
224 break;
230 //--{ List manipulation }-----------------------------------------------
232 /// Sets the current list to \p lid
233 void CTodoDocument::SetList (itemid_t lid)
235 m_Stack.clear();
236 m_Stack.push_back (lid);
237 m_Selection.clear();
238 m_Selection.push_back (0);
239 ReloadList();
242 /// Reloads the currently visible list from the overall data.
243 void CTodoDocument::ReloadList (void)
245 m_List.clear();
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)
255 break;
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);
282 ReloadList();
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());
300 *icuri = e;
301 UpdateEntryDirectoryFlag (icuri);
302 m_List[Selection()] = *icuri;
303 SetFlag (f_Changed);
304 ResortVisibleList();
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());
314 -- r.second;
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.
319 m_Todos.erase (ii);
320 SetFlag (f_Changed);
321 // Update visible list.
322 UpdateItemProgress (r.first, r.second);
323 ReloadList();
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
332 m_Todos.insert (e);
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());
342 SetFlag (f_Changed);
343 UpdateCompleteStatus (icuri->Id());
344 ResortVisibleList();
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)
352 return;
353 icuri->SetPriority (p);
354 SetFlag (f_Changed);
355 ResortVisibleList();
358 /// Sets the currently active item as the current list.
359 void CTodoDocument::EnterCurrentEntry (void)
361 rcentry_t e (CurrentEntry());
362 if (!e.Id())
363 return;
364 m_Stack.push_back (e.Id());
365 m_Selection.push_back (0);
366 ReloadList();
369 /// Goes up to the parent list of the current list.
370 void CTodoDocument::LeaveCurrentEntry (void)
372 if (m_Stack.size() <= 1)
373 return;
374 m_Stack.pop_back();
375 m_Selection.pop_back();
376 ReloadList();
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, CDependency (ItemId(), i->Id()));
392 i->AddRef();
393 SetFlag (f_Changed);
394 // Update the new item's status.
395 FindItem(ItemId())->SetHasSublist (true);
396 UpdateCompleteStatus (i->Id());
397 // Update the visible list.
398 ResortVisibleList();
399 // And select the new item.
400 for (m_Selection.back() = 0; m_List[Selection()].Id() != i->Id(); ++m_Selection.back());
401 UpdateAllViews();
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);
410 if (!nItems)
411 return;
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)
420 return;
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) {
434 if (hasDep)
435 UpdateItemProgress (rangeStart, i);
436 rangeStart = i;
437 hasDep = false;
439 hasDep = hasDep || i->m_DepId == dep;
441 if (hasDep)
442 UpdateItemProgress (rangeStart, iEnd);
445 /// Executes command \p c.
446 void CTodoDocument::OnCommand (cmd_t c)
448 CDocument::OnCommand (c);
449 switch (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));
462 break;
463 case cmd_File_Quit:
464 if (Flag (f_Changed)) {
465 int rv = MessageBox ("There are unsaved changes. Save now?", MB_YesNoCancel);
466 if (rv == mbrv_Cancel)
467 return;
468 else if (rv != mbrv_No)
469 Save();
471 CRootWindow::Instance().Close();
472 break;
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));
482 bool bActive = true;
483 switch (rc.cmd) {
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);
499 break;
500 default:
501 bActive = !(rc.flags & SCmd::cf_Grayed);
502 break;
504 rc.SetFlag (SCmd::cf_Grayed, !bActive);