mainwindow-rector.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. /*
  2. mephi-tasks — a client to NRNU MEPhI Redmine server
  3. Copyright (C) 2015 Dmitry Yu Okunev <dyokunev@ut.mephi.ru> 0x8E30679C
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. */
  15. #include "mainwindow-rector.h"
  16. #include "helpwindow.h"
  17. #include "ui_mainwindow-rector.h"
  18. #include "common.h"
  19. #include <QStringList>
  20. #include <QDesktopServices>
  21. #include <QDateTime>
  22. #include <QMenu>
  23. #include <QList>
  24. #include <QScrollBar>
  25. void MainWindowRector::issuesSetup()
  26. {
  27. QTableWidget *issues = ui->issues;
  28. // Columns:
  29. QStringList columns;
  30. QSize itemSize;
  31. columns << "Название" << "Исполнитель" << "Срок" << "Статус" << "Обновлено";
  32. this->sortColumnAscByIdx[0] = SORT_NAME_ASC;
  33. this->sortColumnAscByIdx[1] = SORT_ASSIGNEE_ASC;
  34. this->sortColumnAscByIdx[2] = SORT_DUE_TO_ASC;
  35. this->sortColumnAscByIdx[3] = SORT_STATUS_POS_ASC;
  36. this->sortColumnAscByIdx[4] = SORT_UPDATED_ON_ASC;
  37. this->sortColumnDescByIdx[0] = SORT_NAME_DESC;
  38. this->sortColumnDescByIdx[1] = SORT_ASSIGNEE_DESC;
  39. this->sortColumnDescByIdx[2] = SORT_DUE_TO_DESC;
  40. this->sortColumnDescByIdx[3] = SORT_STATUS_POS_DESC;
  41. this->sortColumnDescByIdx[4] = SORT_UPDATED_ON_DESC;
  42. issues->setColumnCount ( columns.size() );
  43. issues->horizontalHeader()->setSectionResizeMode ( 0, QHeaderView::Stretch );
  44. issues->horizontalHeader()->setSectionResizeMode ( 1, QHeaderView::Interactive );
  45. issues->horizontalHeader()->resizeSection ( 1, 150 );
  46. issues->horizontalHeader()->setSectionResizeMode ( 2, QHeaderView::Interactive );
  47. issues->horizontalHeader()->resizeSection ( 2, 100 );
  48. issues->horizontalHeader()->setSectionResizeMode ( 3, QHeaderView::Interactive );
  49. issues->horizontalHeader()->resizeSection ( 3, 100 );
  50. issues->horizontalHeader()->setSectionResizeMode ( 4, QHeaderView::Interactive );
  51. issues->horizontalHeader()->resizeSection ( 4, 130 );
  52. issues->setHorizontalHeaderLabels ( columns );
  53. issues->horizontalHeader()->setSortIndicator ( this->sortLogicalIndex, this->sortOrder );
  54. //issues->verticalHeader()->setDefaultSectionSize(18);
  55. issues->verticalHeader()->setSectionResizeMode ( QHeaderView::ResizeToContents );
  56. // Signals:
  57. connect ( issues, SIGNAL ( cellDoubleClicked ( int, int ) ),
  58. this, SLOT ( issues_doubleClick ( int, int ) ) );
  59. connect ( issues->horizontalHeader(), SIGNAL ( sectionClicked ( int ) ),
  60. this, SLOT ( sortColumnSwitch ( int ) ) );
  61. issues->horizontalHeader()->setSortIndicatorShown ( true );
  62. return;
  63. }
  64. void MainWindowRector::sortColumnSwitch ( int columnIdx )
  65. {
  66. enum ESortColumn newSortColumn;
  67. this->sortLogicalIndex = columnIdx;
  68. this->sortOrder = Qt::SortOrder::AscendingOrder;
  69. newSortColumn = this->sortColumnAscByIdx[columnIdx];
  70. if ( this->sortColumn[0] == newSortColumn ) {
  71. this->sortOrder = Qt::SortOrder::DescendingOrder;
  72. newSortColumn = this->sortColumnDescByIdx[columnIdx];
  73. }
  74. this->sortColumn[0] = newSortColumn;
  75. this->issues_display();
  76. }
  77. void MainWindowRector::issues_doubleClick ( int row, int column )
  78. {
  79. ( void ) column;
  80. QString url = QString ( SERVER_URL "/issues/%1" ).arg ( this->issue_row2issue[row]["id"].toInt() );
  81. QDesktopServices::openUrl ( url );
  82. return;
  83. }
  84. MainWindowRector::MainWindowRector ( QWidget *parent ) :
  85. MainWindowCommon ( parent ),
  86. ui ( new Ui::MainWindowRector )
  87. {
  88. ui->setupUi ( this );
  89. /*qDebug("MainWindowRector::MainWindowRector(): %p", this);*/
  90. this->setWindowTitle ( "Система «Задачи» НИЯУ МИФИ: Поручения ректора" );
  91. //Qt::WindowFlags flags = this->windowFlags();
  92. //this->setWindowFlags(flags | Qt::WindowStaysOnTopHint);
  93. connect ( redmine, SIGNAL ( callback_call ( void*, callback_t, QNetworkReply*, QJsonDocument*, void* ) ),
  94. this, SLOT ( callback_dispatcher ( void*, callback_t, QNetworkReply*, QJsonDocument*, void* ) ) );
  95. this->issuesSetup();
  96. this->updateTasks();
  97. this->createIconComboBox();
  98. this->createTrayActions();
  99. this->createTrayIcon();
  100. this->status ( GOOD );
  101. this->setIcon ( GOOD );
  102. this->trayIcon->show();
  103. this->timerUpdateTasks = new QTimer ( this );
  104. connect ( this->timerUpdateTasks, SIGNAL ( timeout() ), this, SLOT ( updateTasks() ) );
  105. this->timerUpdateTasks->start ( 10000 );
  106. return;
  107. }
  108. struct append_assignee_arg {
  109. int pos;
  110. };
  111. void MainWindowRector::append_assignee ( QNetworkReply *reply, QJsonDocument *coassignee_doc, void *_arg )
  112. {
  113. ( void ) reply;
  114. struct append_assignee_arg *arg = ( struct append_assignee_arg * ) _arg;
  115. QJsonObject coassignee = coassignee_doc->object() ["user"].toObject();
  116. QTableWidget *issues = this->ui->issues;
  117. int pos = arg->pos;
  118. /*qDebug("pos: %i; answer is: %s", pos, coassignee_doc->toJson().data());*/
  119. QString firstname = coassignee["firstname"].toString();
  120. QString lastname = coassignee["lastname"].toString();
  121. QString initials = firstname.left ( 1 ) + " " + firstname.left ( firstname.indexOf ( " " ) + 2 ).right ( 1 ); // TODO: Move this to class Redmine
  122. QString fullname = lastname + " " + initials;
  123. QTableWidgetItem *item = issues->item ( pos, 1 );
  124. item->setText ( item->text() + "\n " + fullname );
  125. delete arg;
  126. return;
  127. }
  128. void MainWindowRector::issues_clear()
  129. {
  130. /*qDebug("MainWindowRector::issues_clear(): %p", this);*/
  131. this->issues_list.clear();
  132. return;
  133. }
  134. void MainWindowRector::issue_display_oneissue ( int pos )
  135. {
  136. QTableWidget *issues = this->ui->issues;
  137. QTableWidgetItem *item;
  138. QJsonObject issue = this->issue_row2issue[pos];
  139. QJsonObject issue_status = issue["status"].toObject();
  140. bool isClosed = redmine->get_issue_status ( issue_status["id"].toInt() ) ["is_closed"].toBool();
  141. QColor closedBgColor = QColor ( 192, 255, 192 );
  142. //qDebug("Issue: #%i:\t%s", issue["id"].toInt(), issue["subject"].toString().toUtf8().data());
  143. // New row:
  144. issues->insertRow ( pos );
  145. // Issue name:
  146. item = new QTableWidgetItem ( issue["subject"].toString() );
  147. item->setFlags ( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
  148. if ( isClosed ) item->setBackgroundColor ( closedBgColor );
  149. issues->setItem ( pos, 0, item );
  150. // Assignee:
  151. // Assignee:
  152. QJsonObject assignee_obj = issue["assigned_to"].toObject();
  153. QString assignee_str = assignee_obj["name"].toString();
  154. int assignee_id = assignee_obj["id" ].toInt();
  155. // Setting the assignee value:
  156. item = new QTableWidgetItem ( assignee_str );
  157. item->setFlags ( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
  158. if ( isClosed ) item->setBackgroundColor ( closedBgColor );
  159. issues->setItem ( pos, 1, item );
  160. item = issues->item ( pos, 1 );
  161. // Co-assignees (asynchronous):
  162. QJsonArray customFields = issue["custom_fields"].toArray();
  163. foreach ( const QJsonValue & customField, customFields ) {
  164. if ( customField.toObject() ["name"].toString() == "Соисполнители" ) {
  165. QJsonArray coassignees_id_obj = customField.toObject() ["value"].toArray();
  166. foreach ( const QJsonValue & coassignee_id_obj, coassignees_id_obj ) {
  167. // Don't try to use .toInt() directly, the answer will always be "0":
  168. int coassignee_id = coassignee_id_obj.toString().toInt();
  169. if ( coassignee_id != assignee_id ) {
  170. struct append_assignee_arg *append_assignee_arg_p;
  171. append_assignee_arg_p = new struct append_assignee_arg;
  172. // TODO: fix a memleak if redmine->get_user doesn't success
  173. append_assignee_arg_p->pos = pos;
  174. redmine->get_user ( coassignee_id,
  175. ( Redmine::callback_t ) &MainWindowRector::append_assignee,
  176. ( void * ) append_assignee_arg_p );
  177. }
  178. }
  179. break;
  180. }
  181. }
  182. // Due date:
  183. QString due_date_str = issue["due_date"].toString();
  184. QDateTime now, date;
  185. now = QDateTime::currentDateTime();
  186. date = QDateTime::fromString ( due_date_str, "yyyy-MM-dd" );
  187. item = new QTableWidgetItem ( due_date_str );
  188. item->setFlags ( Qt::ItemIsSelectable | Qt::ItemIsEnabled );
  189. if ( isClosed ) item->setBackgroundColor ( closedBgColor );
  190. if ( ( due_date_str != "" ) && ( now.toTime_t() - ( 3600 * 24 - 1 ) > date.toTime_t() ) && ( !isClosed ) ) {
  191. item->setBackgroundColor ( QColor ( 255, 192, 192 ) );
  192. this->statusWorsenTo ( BAD );
  193. }
  194. issues->setItem ( pos, 2, item );
  195. // Status:
  196. item = new QTableWidgetItem ( issue_status["name"].toString() );
  197. if ( isClosed ) item->setBackgroundColor ( closedBgColor );
  198. issues->setItem ( pos, 3, item );
  199. // Updated on:
  200. QDateTime updated_on = redmine->parseDateTime ( issue["updated_on"] );
  201. item = new QTableWidgetItem ( updated_on.toString ( "yyyy'-'MM'-'dd HH':'MM" ) );
  202. if ( isClosed ) item->setBackgroundColor ( closedBgColor );
  203. issues->setItem ( pos, 4, item );
  204. return;
  205. }
  206. void MainWindowRector::issue_add ( QJsonObject issue )
  207. {
  208. this->issues_list.append ( issue );
  209. return;
  210. }
  211. QList<QJsonObject> MainWindowRector::issues_get()
  212. {
  213. return this->issues_list;
  214. }
  215. void MainWindowRector::issues_display()
  216. {
  217. QTableWidget *uiIssues = this->ui->issues;
  218. QList<QTableWidgetSelectionRange> selected_list = uiIssues->selectedRanges();
  219. QList<QJsonObject> issues_list = this->issues_get();
  220. // Clearing the table
  221. {
  222. int row_count = uiIssues->rowCount();
  223. while ( row_count-- > 0 )
  224. uiIssues->removeRow ( row_count );
  225. this->issue_row2issue.clear();
  226. }
  227. // Filling the table
  228. int issues_count = 0;
  229. if ( this->status() == MainWindowRector::BAD )
  230. this->status ( MainWindowRector::GOOD );
  231. int scrollValue = uiIssues->verticalScrollBar()->value();
  232. qSort ( issues_list.begin(), issues_list.end(), this->sortFunctMap[SORT_STATUS_ISCLOSED_ASC] );
  233. if ( this->sortFunctMap[this->sortColumn[0]] != NULL ) {
  234. QList<QJsonObject>::iterator iterator,
  235. issues_list_end_nonclosed = issues_list.end(),
  236. issues_list_start_closed = issues_list.end();
  237. for ( iterator = issues_list.begin(); iterator != issues_list.end(); iterator++ ) {
  238. bool isClosed = redmine->get_issue_status ( ( *iterator ) ["status"] ) ["is_closed"].toBool();
  239. if ( isClosed ) {
  240. issues_list_end_nonclosed = iterator; // TODO: find out: why here no "-1" in the expression
  241. issues_list_start_closed = iterator;
  242. break;
  243. }
  244. }
  245. if ( issues_list_end_nonclosed != issues_list.begin() )
  246. qSort ( issues_list.begin(), issues_list_end_nonclosed, this->sortFunctMap[this->sortColumn[0]] );
  247. if ( issues_list_end_nonclosed != issues_list.end() )
  248. qSort ( issues_list_start_closed, issues_list.end(), this->sortFunctMap[this->sortColumn[0]] );
  249. }
  250. foreach ( const QJsonObject & issue, issues_list ) {
  251. this->issue_row2issue.insert ( issues_count, issue );
  252. this->issue_display_oneissue ( issues_count );
  253. issues_count++;
  254. }
  255. uiIssues->verticalScrollBar()->setValue ( scrollValue );
  256. foreach ( QTableWidgetSelectionRange range, selected_list )
  257. uiIssues->setRangeSelected (
  258. QTableWidgetSelectionRange (
  259. range.topRow(), range.leftColumn(),
  260. range.bottomRow(), range.rightColumn()
  261. ),
  262. true
  263. );
  264. this->setIcon ( this->status() );
  265. return;
  266. }
  267. void MainWindowRector::get_issues_callback ( QNetworkReply *reply, QJsonDocument *json, void *arg )
  268. {
  269. ( void ) reply;
  270. ( void ) arg;
  271. /*qDebug("MainWindowRector::get_issues_callback(): %p %p", this, arg);*/
  272. //MainWindowRector *win = static_cast<MainWindowRector *>(_win);
  273. QJsonObject answer = json->object();
  274. QJsonArray issues = answer["issues"].toArray();
  275. this->issues_clear();
  276. foreach ( const QJsonValue & issue_val, issues )
  277. this->issue_add ( issue_val.toObject() );
  278. this->issues_display();
  279. return;
  280. }
  281. /* TODO: this function (and related ones) should be replaced with
  282. * MainWindowCommon::updateIssues() [and related]
  283. */
  284. int MainWindowRector::updateTasks()
  285. {
  286. redmine->get_issues ( ( Redmine::callback_t ) &MainWindowRector::get_issues_callback, this );
  287. return 0;
  288. }
  289. MainWindowRector::~MainWindowRector()
  290. {
  291. //saveSettings();
  292. delete this->ui;
  293. delete this->timerUpdateTasks;
  294. }
  295. void MainWindowRector::on_actionExit_triggered()
  296. {
  297. qApp->quit();
  298. }
  299. void MainWindowRector::on_actionHelp_triggered()
  300. {
  301. HelpWindow *win = new HelpWindow();
  302. win->show();
  303. return;
  304. }
  305. void MainWindowRector::showOnTop()
  306. {
  307. #ifdef Q_OS_WIN32
  308. // raise() doesn't work :(
  309. Qt::WindowFlags flags_old = this->windowFlags();
  310. Qt::WindowFlags flags_ontop = flags_old | Qt::WindowStaysOnTopHint;
  311. this->setWindowFlags ( flags_ontop );
  312. this->show();
  313. this->setWindowFlags ( flags_old );
  314. this->show();
  315. #else
  316. this->show();
  317. this->raise();
  318. #endif
  319. return;
  320. }
  321. void MainWindowRector::toggleShowHide()
  322. {
  323. if ( this->isVisible() )
  324. this->hide();
  325. else {
  326. this->showOnTop();
  327. }
  328. return;
  329. }
  330. void MainWindowRector::createTrayActions()
  331. {
  332. showHideAction = new QAction ( tr ( "Показать/Спрятать" ), this );
  333. connect ( showHideAction, SIGNAL ( triggered() ), this, SLOT ( toggleShowHide() ) );
  334. quitAction = new QAction ( tr ( "Завершить" ), this );
  335. connect ( quitAction, SIGNAL ( triggered() ), qApp, SLOT ( quit() ) );
  336. }
  337. void MainWindowRector::iconActivated ( QSystemTrayIcon::ActivationReason reason )
  338. {
  339. switch ( reason ) {
  340. case QSystemTrayIcon::Trigger:
  341. case QSystemTrayIcon::DoubleClick:
  342. this->toggleShowHide();
  343. break;
  344. case QSystemTrayIcon::MiddleClick:
  345. break;
  346. default:
  347. break;
  348. }
  349. }
  350. void MainWindowRector::createTrayIcon()
  351. {
  352. trayIconMenu = new QMenu ( this );
  353. trayIconMenu->addAction ( showHideAction );
  354. trayIconMenu->addAction ( quitAction );
  355. trayIcon = new QSystemTrayIcon ( this );
  356. trayIcon->setContextMenu ( trayIconMenu );
  357. connect ( trayIcon, SIGNAL ( activated ( QSystemTrayIcon::ActivationReason ) ),
  358. this, SLOT ( iconActivated ( QSystemTrayIcon::ActivationReason ) ) );
  359. }
  360. void MainWindowRector::setIcon ( EIcon index )
  361. {
  362. //qDebug("icon: %i", index);
  363. QIcon icon = this->iconComboBox.itemIcon ( index );
  364. this->trayIcon->setIcon ( icon );
  365. this->setWindowIcon ( icon );
  366. this->trayIcon->setToolTip ( this->iconComboBox.itemText ( index ) );
  367. }
  368. void MainWindowRector::createIconComboBox()
  369. {
  370. this->iconComboBox.addItem ( QIcon ( ":/images/good.png" ), tr ( "Просроченных задач нет" ) );
  371. this->iconComboBox.addItem ( QIcon ( ":/images/bad.png" ), tr ( "Есть просроченные задачи" ) );
  372. return;
  373. }
  374. void MainWindowRector::resizeEvent ( QResizeEvent *event )
  375. {
  376. ui->issues->resize ( this->width(), this->height() - 50 );
  377. ui->labelTasksNRNUMEPhI->move ( ( this->width() - ui->labelTasksNRNUMEPhI->width() ) / 2, this->height() - 20 );
  378. QWidget::resizeEvent ( event );
  379. return;
  380. }
  381. #include <QThread>
  382. void MainWindowRector::on_issues_itemSelectionChanged()
  383. {
  384. QTableWidget *issues = this->ui->issues;
  385. int columns_count = issues->columnCount();
  386. int rows_count = issues->rowCount();
  387. QList<QTableWidgetSelectionRange> selected_list = issues->selectedRanges();
  388. foreach ( QTableWidgetSelectionRange range, selected_list ) {
  389. if ( range.leftColumn() != 0 || range.rightColumn() != columns_count - 1 )
  390. issues->setRangeSelected (
  391. QTableWidgetSelectionRange (
  392. range.topRow(), 0,
  393. range.bottomRow(), columns_count - 1
  394. ),
  395. true
  396. );
  397. else
  398. /* Workaround: Drop selection if everything is selected
  399. * it's required to do not select everything on sort switching
  400. */
  401. if ( range.leftColumn() == 0 && range.rightColumn() == columns_count - 1 &&
  402. range.topRow() == 0 && range.bottomRow() == rows_count - 1 ) {
  403. issues->setRangeSelected (
  404. QTableWidgetSelectionRange ( 0, 0, rows_count - 1, columns_count - 1 ),
  405. false
  406. );
  407. break;
  408. }
  409. }
  410. }