1 /*
2     Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3 
4     This file is part of CopyQ.
5 
6     CopyQ is free software: you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation, either version 3 of the License, or
9     (at your option) any later version.
10 
11     CopyQ is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15 
16     You should have received a copy of the GNU General Public License
17     along with CopyQ.  If not, see <http://www.gnu.org/licenses/>.
18 */
19 
20 #include "actiontablemodel.h"
21 
22 #include "common/action.h"
23 #include "common/actionhandlerenums.h"
24 
25 #include <QColor>
26 
27 #include <algorithm>
28 
29 namespace {
30 
31 constexpr auto dateTimeFormat = "yyyy-MM-dd HH:mm:ss.zzz";
32 
actionStateToString(ActionState state)33 QString actionStateToString(ActionState state)
34 {
35     switch (state) {
36     case ActionState::Error: return "Error";
37     case ActionState::Finished: return "Finished";
38     case ActionState::Running: return "Running";
39     case ActionState::Starting: return "Starting";
40     }
41     return QString();
42 }
43 
actionStateOrder(ActionState state)44 int actionStateOrder(ActionState state)
45 {
46     switch (state) {
47     case ActionState::Error: return 1;
48     case ActionState::Finished: return 0;
49     case ActionState::Running: return 2;
50     case ActionState::Starting: return 3;
51     }
52     return -1;
53 }
54 
55 } // namespace
56 
ActionTableModel(uint maxRowCount,QObject * parent)57 ActionTableModel::ActionTableModel(uint maxRowCount, QObject *parent)
58     : QAbstractTableModel(parent)
59     , m_maxRowCount(maxRowCount)
60 {
61 }
62 
actionAboutToStart(Action * action)63 int ActionTableModel::actionAboutToStart(Action *action)
64 {
65     ActionData actionData;
66     actionData.id = m_actions.empty() ? 1 : m_actions[m_actions.size() - 1].id + 1;
67     actionData.name = action->name();
68     if ( actionData.name.isEmpty() )
69         actionData.name = action->commandLine();
70     actionData.finished = -1;
71 
72     limitItems();
73 
74     beginInsertRows(QModelIndex(), actionCount(), actionCount());
75     m_actions.push_back(actionData);
76     endInsertRows();
77 
78     return actionData.id;
79 }
80 
actionStarted(Action * action)81 void ActionTableModel::actionStarted(Action *action)
82 {
83     const int row = rowFor(action);
84     actionData(row).started = QDateTime::currentDateTime();
85     for (const int column : { ActionHandlerColumn::started, ActionHandlerColumn::status }) {
86         const auto index = this->index(row, column);
87         emit dataChanged(index, index);
88     }
89 }
90 
actionFailed(Action * action,const QString & error)91 void ActionTableModel::actionFailed(Action *action, const QString &error)
92 {
93     const int row = rowFor(action);
94     actionData(row).error = error;
95     for (const int column : { ActionHandlerColumn::error, ActionHandlerColumn::status }) {
96         const auto index = this->index(row, column);
97         emit dataChanged(index, index);
98     }
99 }
100 
actionFinished(Action * action)101 void ActionTableModel::actionFinished(Action *action)
102 {
103     const int row = rowFor(action);
104     ActionData &data = actionData(row);
105     data.finished = data.started.msecsTo(QDateTime::currentDateTime());
106     for (const int column : { ActionHandlerColumn::finished, ActionHandlerColumn::status }) {
107         const auto index = this->index(row, column);
108         emit dataChanged(index, index);
109     }
110 }
111 
actionFinished(const QString & name)112 void ActionTableModel::actionFinished(const QString &name)
113 {
114     ActionData actionData;
115     actionData.id = m_actions.empty() ? 1 : m_actions.end()->id + 1;
116     actionData.name = name;
117     actionData.started = QDateTime::currentDateTime();
118     actionData.finished = 0;
119 
120     limitItems();
121 
122     beginInsertRows(QModelIndex(), actionCount(), actionCount());
123     m_actions.push_back(actionData);
124     endInsertRows();
125 }
126 
headerData(int section,Qt::Orientation orientation,int role) const127 QVariant ActionTableModel::headerData(int section, Qt::Orientation orientation, int role) const
128 {
129     if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
130         switch (section) {
131         case ActionHandlerColumn::id:
132             return "ID";
133         case ActionHandlerColumn::name:
134             return "Name";
135         case ActionHandlerColumn::status:
136             return "Status";
137         case ActionHandlerColumn::started:
138             return "Started";
139         case ActionHandlerColumn::finished:
140             return "Finished";
141         case ActionHandlerColumn::error:
142             return "Error";
143         }
144     }
145 
146     return QVariant();
147 }
148 
rowCount(const QModelIndex & parent) const149 int ActionTableModel::rowCount(const QModelIndex &parent) const
150 {
151     if (parent.isValid())
152         return 0;
153 
154     return actionCount();
155 }
156 
columnCount(const QModelIndex & parent) const157 int ActionTableModel::columnCount(const QModelIndex &parent) const
158 {
159     if (parent.isValid())
160         return 0;
161 
162     return ActionHandlerColumn::count;
163 }
164 
data(const QModelIndex & index,int role) const165 QVariant ActionTableModel::data(const QModelIndex &index, int role) const
166 {
167     if (!index.isValid())
168         return QVariant();
169 
170     if (role == Qt::DisplayRole || role == Qt::EditRole) {
171         const int row = index.row();
172         const int column = index.column();
173         const ActionData &data = actionData(row);
174         switch (column) {
175         case ActionHandlerColumn::id:
176             return row;
177         case ActionHandlerColumn::name:
178             return data.name;
179         case ActionHandlerColumn::status:
180             return actionStateToString(actionState(data));
181         case ActionHandlerColumn::started:
182             return data.started.toString(dateTimeFormat);
183         case ActionHandlerColumn::finished:
184             return data.finished == -1 ? QString() : data.started.addMSecs(data.finished).toString(dateTimeFormat);
185         case ActionHandlerColumn::error:
186             return data.error;
187         }
188     } else if (role == Qt::ToolTipRole) {
189         const int row = index.row();
190         const int column = index.column();
191         const ActionData &data = actionData(row);
192         if (column == ActionHandlerColumn::name)
193             return data.name;
194         if (column == ActionHandlerColumn::error)
195             return data.error;
196     } else if (role == Qt::DecorationRole) {
197             const int column = index.column();
198             if (column == ActionHandlerColumn::status) {
199                 const int row = index.row();
200                 const ActionData &data = actionData(row);
201                 switch (actionState(data)) {
202                 case ActionState::Error: return QColor(0xe56950);
203                 case ActionState::Running: return QColor(0xcce550);
204                 case ActionState::Starting: return QColor(0xe5b350);
205                 case ActionState::Finished: break;
206                 }
207             }
208     } else if (role >= Qt::UserRole) {
209         if (role == ActionHandlerRole::sort) {
210             const int row = index.row();
211             const int column = index.column();
212             const ActionData &data = actionData(row);
213             switch (column) {
214             case ActionHandlerColumn::id:
215                 return data.id;
216             case ActionHandlerColumn::name:
217                 return data.name;
218             case ActionHandlerColumn::status:
219                 return actionStateOrder(actionState(data));
220             case ActionHandlerColumn::started:
221                 return data.started;
222             case ActionHandlerColumn::finished:
223                 return data.finished;
224             case ActionHandlerColumn::error:
225                 return data.error;
226             }
227         } else if (role == ActionHandlerRole::status) {
228             const int row = index.row();
229             const ActionData &data = actionData(row);
230             return static_cast<int>(actionState(data));
231         } else if (role == ActionHandlerRole::id) {
232             const int row = index.row();
233             const ActionData &data = actionData(row);
234             return data.id;
235         }
236     }
237 
238     return QVariant();
239 }
240 
actionState(const ActionTableModel::ActionData & data)241 ActionState ActionTableModel::actionState(const ActionTableModel::ActionData &data)
242 {
243     if ( !data.error.isEmpty() )
244         return ActionState::Error;
245     if ( data.finished != -1 )
246         return ActionState::Finished;
247     if ( data.started.isValid() )
248         return ActionState::Running;
249     return ActionState::Starting;
250 }
251 
rowFor(const Action * action) const252 int ActionTableModel::rowFor(const Action *action) const
253 {
254     const auto found = std::lower_bound(
255         std::begin(m_actions), std::end(m_actions), action->id(),
256         [](const ActionData &data, int id) {
257             return data.id < id;
258         });
259     const auto row = std::distance(std::begin(m_actions), found);
260     return static_cast<int>(row);
261 }
262 
limitItems()263 void ActionTableModel::limitItems()
264 {
265     if (m_actions.size() < m_maxRowCount * 4 / 3)
266         return;
267 
268     while (m_actions.size() >= m_maxRowCount) {
269         const auto found = std::find_if(
270             std::begin(m_actions), std::end(m_actions),
271             [](const ActionData &data) {
272                 const auto state = actionState(data);
273                 return state == ActionState::Finished || state == ActionState::Error;
274             });
275 
276         if ( found == std::end(m_actions) )
277             break;
278 
279         const auto row = static_cast<int>( std::distance(std::begin(m_actions), found) );
280         beginRemoveRows(QModelIndex(), row, row);
281         m_actions.erase(found);
282         endRemoveRows();
283     }
284 }
285