Changeset 31
- Timestamp:
- 05/26/08 18:12:01 (4 years ago)
- Location:
- trunk
- Files:
-
- 5 added
- 8 edited
-
form_date.cpp (added)
-
form_date.h (added)
-
form_date_ui.cpp (added)
-
form_date_ui.h (added)
-
forum_tree.cpp (modified) (4 diffs)
-
forum_tree.h (modified) (2 diffs)
-
icons/unsubscribe16.png (added)
-
message_tree.cpp (modified) (1 diff)
-
resource.qrc (modified) (1 diff)
-
storage/istorage.h (modified) (2 diffs)
-
storage/mysql_storage.cpp (modified) (2 diffs)
-
storage/mysql_storage.h (modified) (1 diff)
-
sysheaders.h (modified) (3 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/forum_tree.cpp
r30 r31 8 8 //---------------------------------------------------------------------------------------------- 9 9 #include "model/all.h" 10 #include "form_date.h" 10 11 #include "tree_widget_item.h" 11 12 #include "storage/storage_factory.h" … … 39 40 headerItem()->setTextAlignment(1, Qt::AlignCenter); 40 41 42 m_menu = new QMenu(this); 43 44 m_menu_mark_all_as_read = m_menu->addAction(QString::fromUtf8("Пометить все сообщения как прочитанные")); 45 m_menu_mark_all_as_read->setIcon(QIcon(":/icons/markallasread16.png")); 46 47 m_menu_mark_patrial_as_read = m_menu->addAction(QString::fromUtf8("Пометить сообщения до даты как прочитанные")); 48 m_menu_mark_patrial_as_read->setIcon(QIcon(":/icons/markpatrialasread16.png")); 49 50 m_menu->addSeparator(); 51 52 m_menu_mark_all_as_unread = m_menu->addAction(QString::fromUtf8("Пометить все сообщения как не прочитанные")); 53 m_menu_mark_all_as_unread->setIcon(QIcon(":/icons/markallasunread16.png")); 54 55 m_menu_mark_patrial_as_unread = m_menu->addAction(QString::fromUtf8("Пометить сообщения до даты как не прочитанные")); 56 m_menu_mark_patrial_as_unread->setIcon(QIcon(":/icons/markpatrialasunread16.png")); 57 58 m_menu->addSeparator(); 59 60 m_menu_unsubscribe = m_menu->addAction(QString::fromUtf8("Отписаться")); 61 m_menu_unsubscribe->setIcon(QIcon(":/icons/unsubscribe16.png")); 62 41 63 connect(this, SIGNAL(itemSelectionChanged()), this, SLOT(selection_changed())); 42 64 connect(this, SIGNAL(customContextMenuRequested(const QPoint&)), this, SLOT(context_menu_request(const QPoint&))); 65 66 connect(m_menu_mark_all_as_read, SIGNAL(triggered()), this, SLOT(menu_mark_all_as_read_triggered())); 67 connect(m_menu_mark_patrial_as_read, SIGNAL(triggered()), this, SLOT(menu_mark_patrial_as_read_triggered())); 68 connect(m_menu_mark_all_as_unread, SIGNAL(triggered()), this, SLOT(menu_mark_all_as_unread_triggered())); 69 connect(m_menu_mark_patrial_as_unread, SIGNAL(triggered()), this, SLOT(menu_mark_patrial_as_unread_triggered())); 70 connect(m_menu_unsubscribe, SIGNAL(triggered()), this, SLOT(menu_unsubscribe_triggered())); 43 71 44 72 Reload(); … … 229 257 //---------------------------------------------------------------------------------------------- 230 258 259 bool AForumTree::IsGroup (QTreeWidgetItem* item) 260 { 261 return item->childCount() != 0; 262 } 263 //---------------------------------------------------------------------------------------------- 264 265 QTreeWidgetItem* AForumTree::GetSelectedItem () 266 { 267 QList<QTreeWidgetItem*> list = selectedItems(); 268 269 if (list.count() == 0) 270 return NULL; 271 272 return list[0]; 273 } 274 //---------------------------------------------------------------------------------------------- 275 231 276 QTreeWidgetItem* AForumTree::GetSelectedForumItem () 232 277 { 233 Q List<QTreeWidgetItem*> list_selected = selectedItems();234 235 if ( list_selected.count() == 0)278 QTreeWidgetItem* item = GetSelectedItem(); 279 280 if (item == NULL) 236 281 return NULL; 237 282 238 QTreeWidgetItem* item_selected = list_selected[0]; 239 240 bool is_group = item_selected->childCount(); 241 242 if (is_group == true) 283 if (IsGroup(item) == true) 243 284 return NULL; 244 285 245 return item _selected;286 return item; 246 287 } 247 288 //---------------------------------------------------------------------------------------------- … … 295 336 void AForumTree::context_menu_request (const QPoint& pos) 296 337 { 297 QMessageBox::critical(m_parent, QString::fromUtf8("Ошибка!"), QString::fromUtf8("Не выбрано хранилище данных")); 298 (new QMenu("abcd"))->exec(pos); 299 } 300 //---------------------------------------------------------------------------------------------- 338 if (GetSelectedItem() != NULL) 339 m_menu->exec(viewport()->mapToGlobal(pos)); 340 } 341 //---------------------------------------------------------------------------------------------- 342 343 void AForumTree::menu_mark_all_as_read_triggered () 344 { 345 QTreeWidgetItem* item = GetSelectedItem(); 346 347 if (item == NULL) 348 return; 349 350 // получение хранилища 351 std::auto_ptr<IStorage> storage(AStorageFactory::GetStorage()); 352 353 if (storage.get() == NULL) 354 { 355 QMessageBox::critical(m_parent, QString::fromUtf8("Ошибка!"), QString::fromUtf8("Не выбрано хранилище данных")); 356 return; 357 } 358 359 if (IsGroup(item) == true) 360 { 361 int id = (static_cast<GroupTreeWidgetItem*>(item))->PTag()->ID; 362 363 if (storage->SetIDsAsRead(QList<int>() << id, idsGroup, true, QDateTime(), NULL) == false) 364 { 365 storage->ShowError(m_parent); 366 return; 367 } 368 } 369 else 370 { 371 AForumInfo* info = (static_cast<ForumTreeWidgetItem*>(item))->PTag(); 372 373 if (storage->SetIDsAsRead(QList<int>() << info->ID, idsForum, true, QDateTime(), NULL) == false) 374 { 375 storage->ShowError(m_parent); 376 return; 377 } 378 379 // обновление дерева сообщений 380 m_message_tree->ChangeForum(info); 381 } 382 383 // перезагрузка количества непрочитаных 384 ReloadUnread(); 385 } 386 //---------------------------------------------------------------------------------------------- 387 388 void AForumTree::menu_mark_patrial_as_read_triggered () 389 { 390 QTreeWidgetItem* item = GetSelectedItem(); 391 392 if (item == NULL) 393 return; 394 395 std::auto_ptr<FormDate> form(new FormDate(m_parent, true)); 396 397 if (form->exec() == QDialog::Accepted) 398 { 399 // получение хранилища 400 std::auto_ptr<IStorage> storage(AStorageFactory::GetStorage()); 401 402 if (storage.get() == NULL) 403 { 404 QMessageBox::critical(m_parent, QString::fromUtf8("Ошибка!"), QString::fromUtf8("Не выбрано хранилище данных")); 405 return; 406 } 407 408 if (IsGroup(item) == true) 409 { 410 int id = (static_cast<GroupTreeWidgetItem*>(item))->PTag()->ID; 411 412 if (storage->SetIDsAsRead(QList<int>() << id, idsGroup, true, form->SelectedDate(), NULL) == false) 413 { 414 storage->ShowError(m_parent); 415 return; 416 } 417 } 418 else 419 { 420 AForumInfo* info = (static_cast<ForumTreeWidgetItem*>(item))->PTag(); 421 422 if (storage->SetIDsAsRead(QList<int>() << info->ID, idsForum, true, form->SelectedDate(), NULL) == false) 423 { 424 storage->ShowError(m_parent); 425 return; 426 } 427 428 // обновление дерева сообщений 429 m_message_tree->ChangeForum(info); 430 } 431 432 // перезагрузка количества непрочитаных 433 ReloadUnread(); 434 } 435 } 436 //---------------------------------------------------------------------------------------------- 437 438 void AForumTree::menu_mark_all_as_unread_triggered () 439 { 440 QTreeWidgetItem* item = GetSelectedItem(); 441 442 if (item == NULL) 443 return; 444 445 // получение хранилища 446 std::auto_ptr<IStorage> storage(AStorageFactory::GetStorage()); 447 448 if (storage.get() == NULL) 449 { 450 QMessageBox::critical(m_parent, QString::fromUtf8("Ошибка!"), QString::fromUtf8("Не выбрано хранилище данных")); 451 return; 452 } 453 454 if (IsGroup(item) == true) 455 { 456 int id = (static_cast<GroupTreeWidgetItem*>(item))->PTag()->ID; 457 458 if (storage->SetIDsAsRead(QList<int>() << id, idsGroup, false, QDateTime(), NULL) == false) 459 { 460 storage->ShowError(m_parent); 461 return; 462 } 463 } 464 else 465 { 466 AForumInfo* info = (static_cast<ForumTreeWidgetItem*>(item))->PTag(); 467 468 if (storage->SetIDsAsRead(QList<int>() << info->ID, idsForum, false, QDateTime(), NULL) == false) 469 { 470 storage->ShowError(m_parent); 471 return; 472 } 473 474 // обновление дерева сообщений 475 m_message_tree->ChangeForum(info); 476 } 477 478 // перезагрузка количества непрочитаных 479 ReloadUnread(); 480 } 481 //---------------------------------------------------------------------------------------------- 482 483 void AForumTree::menu_mark_patrial_as_unread_triggered () 484 { 485 QTreeWidgetItem* item = GetSelectedItem(); 486 487 if (item == NULL) 488 return; 489 490 std::auto_ptr<FormDate> form(new FormDate(m_parent, false)); 491 492 if (form->exec() == QDialog::Accepted) 493 { 494 // получение хранилища 495 std::auto_ptr<IStorage> storage(AStorageFactory::GetStorage()); 496 497 if (storage.get() == NULL) 498 { 499 QMessageBox::critical(m_parent, QString::fromUtf8("Ошибка!"), QString::fromUtf8("Не выбрано хранилище данных")); 500 return; 501 } 502 503 if (IsGroup(item) == true) 504 { 505 int id = (static_cast<GroupTreeWidgetItem*>(item))->PTag()->ID; 506 507 if (storage->SetIDsAsRead(QList<int>() << id, idsGroup, false, form->SelectedDate(), NULL) == false) 508 { 509 storage->ShowError(m_parent); 510 return; 511 } 512 } 513 else 514 { 515 AForumInfo* info = (static_cast<ForumTreeWidgetItem*>(item))->PTag(); 516 517 if (storage->SetIDsAsRead(QList<int>() << info->ID, idsForum, false, form->SelectedDate(), NULL) == false) 518 { 519 storage->ShowError(m_parent); 520 return; 521 } 522 523 // обновление дерева сообщений 524 m_message_tree->ChangeForum(info); 525 } 526 527 // перезагрузка количества непрочитаных 528 ReloadUnread(); 529 } 530 } 531 //---------------------------------------------------------------------------------------------- 532 533 void AForumTree::menu_unsubscribe_triggered () 534 { 535 } 536 //---------------------------------------------------------------------------------------------- -
trunk/forum_tree.h
r30 r31 38 38 IMessageView* m_message_view; 39 39 40 // проверка элемента на группу 41 bool IsGroup (QTreeWidgetItem* item); 42 43 // возвращает текущий выделенный элемент (форум или группу) 44 QTreeWidgetItem* GetSelectedItem (); 45 40 46 // возвращает текущий выделенный элемент форума или NULL, если группа 41 47 QTreeWidgetItem* GetSelectedForumItem (); 48 49 // высплывающее меню 50 QMenu* m_menu; 51 QAction* m_menu_mark_all_as_read; 52 QAction* m_menu_mark_patrial_as_read; 53 QAction* m_menu_mark_all_as_unread; 54 QAction* m_menu_mark_patrial_as_unread; 55 QAction* m_menu_unsubscribe; 42 56 43 57 // IForumTree … … 55 69 private slots: 56 70 71 // смена выделения 57 72 void selection_changed (); 58 73 74 // запрос всплывающего меню 59 75 void context_menu_request (const QPoint& pos); 76 77 // меню 78 void menu_mark_all_as_read_triggered (); 79 void menu_mark_patrial_as_read_triggered (); 80 void menu_mark_all_as_unread_triggered (); 81 void menu_mark_patrial_as_unread_triggered (); 82 void menu_unsubscribe_triggered (); 60 83 }; 61 84 //---------------------------------------------------------------------------------------------- -
trunk/message_tree.cpp
r30 r31 386 386 } 387 387 388 if (storage->Set MessageAsRead(info->ID, true, NULL) == false)388 if (storage->SetIDsAsRead(QList<int>() << info->ID, idsMessage, true, QDateTime(), NULL) == false) 389 389 { 390 390 storage->ShowError(m_parent); -
trunk/resource.qrc
r28 r31 18 18 <file>icons/help16.png</file> 19 19 20 <file>icons/unsubscribe16.png</file> 21 20 22 <file>icons/synchronize24.png</file> 21 23 -
trunk/storage/istorage.h
r30 r31 11 11 #include "../iprogress.h" 12 12 #include "database_error.h" 13 //---------------------------------------------------------------------------------------------- 14 // типы множеств для обработки 15 //---------------------------------------------------------------------------------------------- 16 typedef enum AIDSet 17 { 18 idsMessage, // сообщение 19 idsTopic, // топик 20 idsForum, // форум 21 idsGroup, // группа 22 idsAll // все 23 }; 13 24 //---------------------------------------------------------------------------------------------- 14 25 // общий интерфейс для всех хранилищ (см. AStorageFactory в storage_factory.h) … … 58 69 virtual bool GetMessageInfo (int id_message, QString& body, IProgress* progress = NULL) = 0; 59 70 60 // пометить сообщение как прочитанное/непрочитанное 61 virtual bool SetMessageAsRead (int id_message, bool read, IProgress* progress = NULL) = 0; 71 // пометить группу сущностей как прочитанное/непрочитанное 72 // если установлена дата, то она учитывается соответственно логике: 73 // read = true - до даты как прочитанные 74 // read = false - после даты как непрочитанные 75 virtual bool SetIDsAsRead (const QList<int>& list, AIDSet type, bool read, QDateTime date, IProgress* progress = NULL) = 0; 62 76 }; 63 77 //---------------------------------------------------------------------------------------------- -
trunk/storage/mysql_storage.cpp
r30 r31 1713 1713 //---------------------------------------------------------------------------------------------- 1714 1714 1715 bool AMySQLStorage::Set MessageAsRead (int id_message, bool read, IProgress* progress)1715 bool AMySQLStorage::SetIDsAsRead (const QList<int>& list, AIDSet type, bool read, QDateTime date, IProgress* progress) 1716 1716 { 1717 1717 if (progress != NULL) 1718 1718 progress->OnProgress(0); 1719 1719 1720 // получение строки с id 1721 QString ids; 1722 1723 for (int i = 0; i < list.count(); i++) 1724 { 1725 ids += QString::number(list[i]); 1726 1727 if (i < list.count() - 1) 1728 ids += ", "; 1729 } 1730 1720 1731 QString sql; 1721 1732 … … 1726 1737 if (read == true) 1727 1738 { 1728 sql += "DELETE FROM\n"; 1729 sql += " `unread`\n"; 1730 sql += "WHERE\n"; 1731 sql += " `id_message` = " + QString::number(id_message); 1739 switch (type) 1740 { 1741 case idsMessage: 1742 1743 sql += "DELETE FROM\n"; 1744 sql += " `unread`\n"; 1745 sql += "WHERE\n"; 1746 sql += " `id_message` IN (" + ids + ")"; 1747 1748 if (date.isValid() == true) 1749 sql += " AND\n `message_date` <= '" + date.toString(Qt::ISODate) + "'"; 1750 1751 break; 1752 1753 case idsTopic: 1754 1755 sql += "DELETE FROM\n"; 1756 sql += " `unread`\n"; 1757 sql += "WHERE\n"; 1758 sql += " `id_topic` IN (" + ids + ")"; 1759 1760 if (date.isValid() == true) 1761 sql += " AND\n `message_date` <= '" + date.toString(Qt::ISODate) + "'"; 1762 1763 break; 1764 1765 case idsForum: 1766 1767 sql += "DELETE FROM\n"; 1768 sql += " `unread`\n"; 1769 sql += "WHERE\n"; 1770 sql += " `id_forum` IN (" + ids + ")"; 1771 1772 if (date.isValid() == true) 1773 sql += " AND\n `message_date` <= '" + date.toString(Qt::ISODate) + "'"; 1774 1775 break; 1776 1777 case idsGroup: 1778 1779 sql += "DELETE FROM\n"; 1780 sql += " `unread`\n"; 1781 sql += "WHERE\n"; 1782 sql += " `id_forum` IN\n"; 1783 sql += " (\n"; 1784 sql += " SELECT\n"; 1785 sql += " `id`\n"; 1786 sql += " FROM\n"; 1787 sql += " `forum`\n"; 1788 sql += " WHERE\n"; 1789 sql += " `id_group` IN (" + ids + ")\n"; 1790 sql += " )"; 1791 1792 if (date.isValid() == true) 1793 sql += " AND\n `message_date` <= '" + date.toString(Qt::ISODate) + "'"; 1794 1795 break; 1796 1797 case idsAll: 1798 1799 sql += "DELETE FROM `unread`"; 1800 1801 if (date.isValid() == true) 1802 sql += "WHERE `message_date` <= '" + date.toString(Qt::ISODate) + "'"; 1803 1804 break; 1805 1806 default: 1807 return ReturnError(QString::fromUtf8("Не указана группа объектов")); 1808 } 1732 1809 } 1733 1810 else 1734 1811 { 1735 sql += "REPLACE INTO `unread`\n"; 1736 sql += "(\n"; 1737 sql += " `id_message`,\n"; 1738 sql += " `id_forum`,\n"; 1739 sql += " `id_topic`,\n"; 1740 sql += " `message_date`\n"; 1741 sql += ")\n"; 1742 sql += "SELECT\n"; 1743 sql += " `id`,\n"; 1744 sql += " `id_forum`,\n"; 1745 sql += " `id_topic`,\n"; 1746 sql += " `message_date`\n"; 1747 sql += "FROM\n"; 1748 sql += " `message`\n"; 1749 sql += "WHERE\n"; 1750 sql += " `id_message` = " + QString::number(id_message); 1812 switch (type) 1813 { 1814 case idsMessage: 1815 1816 sql += "REPLACE INTO `unread`\n"; 1817 sql += "(\n"; 1818 sql += " `id_message`,\n"; 1819 sql += " `id_forum`,\n"; 1820 sql += " `id_topic`,\n"; 1821 sql += " `message_date`\n"; 1822 sql += ")\n"; 1823 sql += "SELECT\n"; 1824 sql += " `id`,\n"; 1825 sql += " `id_forum`,\n"; 1826 sql += " `id_topic`,\n"; 1827 sql += " `message_date`\n"; 1828 sql += "FROM\n"; 1829 sql += " `message`\n"; 1830 sql += "WHERE\n"; 1831 sql += " `id_message` IN (" + ids + ")"; 1832 1833 if (date.isValid() == true) 1834 sql += " AND\n `message_date` >= '" + date.toString(Qt::ISODate) + "'"; 1835 1836 break; 1837 1838 case idsTopic: 1839 1840 sql += "REPLACE INTO `unread`\n"; 1841 sql += "(\n"; 1842 sql += " `id_message`,\n"; 1843 sql += " `id_forum`,\n"; 1844 sql += " `id_topic`,\n"; 1845 sql += " `message_date`\n"; 1846 sql += ")\n"; 1847 sql += "SELECT\n"; 1848 sql += " `id`,\n"; 1849 sql += " `id_forum`,\n"; 1850 sql += " `id_topic`,\n"; 1851 sql += " `message_date`\n"; 1852 sql += "FROM\n"; 1853 sql += " `message`\n"; 1854 sql += "WHERE\n"; 1855 sql += " `id_topic` IN (" + ids + ")"; 1856 1857 if (date.isValid() == true) 1858 sql += " AND\n `message_date` >= '" + date.toString(Qt::ISODate) + "'"; 1859 1860 break; 1861 1862 case idsForum: 1863 1864 sql += "REPLACE INTO `unread`\n"; 1865 sql += "(\n"; 1866 sql += " `id_message`,\n"; 1867 sql += " `id_forum`,\n"; 1868 sql += " `id_topic`,\n"; 1869 sql += " `message_date`\n"; 1870 sql += ")\n"; 1871 sql += "SELECT\n"; 1872 sql += " `id`,\n"; 1873 sql += " `id_forum`,\n"; 1874 sql += " `id_topic`,\n"; 1875 sql += " `message_date`\n"; 1876 sql += "FROM\n"; 1877 sql += " `message`\n"; 1878 sql += "WHERE\n"; 1879 sql += " `id_forum` IN (" + ids + ")"; 1880 1881 if (date.isValid() == true) 1882 sql += " AND\n `message_date` >= '" + date.toString(Qt::ISODate) + "'"; 1883 1884 break; 1885 1886 case idsGroup: 1887 1888 sql += "REPLACE INTO `unread`\n"; 1889 sql += "(\n"; 1890 sql += " `id_message`,\n"; 1891 sql += " `id_forum`,\n"; 1892 sql += " `id_topic`,\n"; 1893 sql += " `message_date`\n"; 1894 sql += ")\n"; 1895 sql += "SELECT\n"; 1896 sql += " `id`,\n"; 1897 sql += " `id_forum`,\n"; 1898 sql += " `id_topic`,\n"; 1899 sql += " `message_date`\n"; 1900 sql += "FROM\n"; 1901 sql += " `message`\n"; 1902 sql += "WHERE\n"; 1903 sql += " `id_forum` IN\n"; 1904 sql += " (\n"; 1905 sql += " SELECT\n"; 1906 sql += " `id`\n"; 1907 sql += " FROM\n"; 1908 sql += " `forum`\n"; 1909 sql += " WHERE\n"; 1910 sql += " `id_group` IN (" + ids + ")\n"; 1911 sql += " )"; 1912 1913 if (date.isValid() == true) 1914 sql += " AND\n `message_date` >= '" + date.toString(Qt::ISODate) + "'"; 1915 1916 break; 1917 1918 case idsAll: 1919 1920 sql += "REPLACE INTO `unread`\n"; 1921 sql += "(\n"; 1922 sql += " `id_message`,\n"; 1923 sql += " `id_forum`,\n"; 1924 sql += " `id_topic`,\n"; 1925 sql += " `message_date`\n"; 1926 sql += ")\n"; 1927 sql += "SELECT\n"; 1928 sql += " `id`,\n"; 1929 sql += " `id_forum`,\n"; 1930 sql += " `id_topic`,\n"; 1931 sql += " `message_date`\n"; 1932 sql += "FROM\n"; 1933 sql += " `message`"; 1934 1935 if (date.isValid() == true) 1936 sql += "\nWHERE\n `message_date` >= '" + date.toString(Qt::ISODate) + "'"; 1937 1938 break; 1939 1940 default: 1941 return ReturnError(QString::fromUtf8("Не указана группа объектов")); 1942 } 1751 1943 } 1752 1944 -
trunk/storage/mysql_storage.h
r30 r31 69 69 bool GetMessageInfo (int id_message, QString& body, IProgress* progress = NULL); 70 70 71 // пометить сообщение как прочитанное/непрочитанное 72 bool SetMessageAsRead (int id_message, bool read, IProgress* progress = NULL); 71 // пометить группу сущностей как прочитанное/непрочитанное 72 // если установлена дата, то она учитывается соответственно логике: 73 // read = true - до даты как прочитанные 74 // read = false - после даты как непрочитанные 75 bool SetIDsAsRead (const QList<int>& list, AIDSet type, bool read, QDateTime date, IProgress* progress = NULL); 73 76 }; 74 77 //---------------------------------------------------------------------------------------------- -
trunk/sysheaders.h
r29 r31 8 8 #define _avalon_sysheaders_h_ 9 9 //---------------------------------------------------------------------------------------------- 10 #include <QFrame> 10 11 #include <QHttp> 11 12 #include <QIcon> … … 23 24 #include <QSqlError> 24 25 #include <QDateTime> 26 #include <QTimeEdit> 27 #include <QGroupBox> 25 28 #include <QStatusBar> 26 29 #include <QTreeWidget> … … 37 40 #include <QSqlDatabase> 38 41 #include <QApplication> 42 #include <QCalendarWidget> 39 43 //---------------------------------------------------------------------------------------------- 40 44 #endif
Note: See TracChangeset
for help on using the changeset viewer.
