1 #include "windows/TimeLine.h"
2 #include "windows/Icons.data.h"
3 #include <wx/bmpbuttn.h>
4 #include <wx/dcbuffer.h>
5
6 NAMESPACE_SPH_BEGIN
7
getSequenceFiles(const Path & inputPath)8 std::map<int, Path> getSequenceFiles(const Path& inputPath) {
9 if (inputPath.empty()) {
10 throw Exception("Sequence for empty path");
11 }
12
13 const Expected<Path> absolutePath = FileSystem::getAbsolutePath(inputPath);
14 if (!absolutePath) {
15 throw Exception("Cannot resolve absolute path of file '" + inputPath.string() + "'");
16 }
17
18 Optional<OutputFile> deducedFile = OutputFile::getMaskFromPath(absolutePath.value());
19 if (!deducedFile) {
20 // just a single file, not part of a sequence (e.g. frag_final.ssf)
21 return { std::make_pair(0, absolutePath.value()) };
22 }
23
24 Path fileMask = deducedFile->getMask();
25 std::map<int, Path> fileMap;
26
27 const Path dir = absolutePath->parentPath();
28 Array<Path> files = FileSystem::getFilesInDirectory(dir);
29
30 for (Path& file : files) {
31 const Optional<OutputFile> deducedMask = OutputFile::getMaskFromPath(dir / file);
32 // check if part of the same sequence
33 if (deducedMask && deducedMask->getMask() == fileMask) {
34 const Optional<Size> index = OutputFile::getDumpIdx(dir / file);
35 SPH_ASSERT(index);
36 fileMap[index.value()] = dir / file;
37 }
38 }
39
40 if (fileMap.empty()) {
41 throw Exception("Cannot open file '" + inputPath.string() + "'");
42 }
43
44 return fileMap;
45 }
46
TimeLinePanel(wxWindow * parent,const Path & inputFile,SharedPtr<ITimeLineCallbacks> callbacks)47 TimeLinePanel::TimeLinePanel(wxWindow* parent, const Path& inputFile, SharedPtr<ITimeLineCallbacks> callbacks)
48 : wxPanel(parent, wxID_ANY)
49 , callbacks(callbacks) {
50
51 this->update(inputFile);
52
53 this->SetBackgroundStyle(wxBG_STYLE_PAINT);
54
55 this->SetMinSize(wxSize(300, 30));
56 this->Connect(wxEVT_PAINT, wxPaintEventHandler(TimeLinePanel::onPaint));
57 this->Connect(wxEVT_MOTION, wxMouseEventHandler(TimeLinePanel::onMouseMotion));
58 this->Connect(wxEVT_LEFT_UP, wxMouseEventHandler(TimeLinePanel::onLeftClick));
59 this->Connect(wxEVT_KEY_UP, wxKeyEventHandler(TimeLinePanel::onKeyUp));
60 }
61
update(const Path & inputFile)62 void TimeLinePanel::update(const Path& inputFile) {
63 CHECK_FUNCTION(CheckFunction::MAIN_THREAD | CheckFunction::NO_THROW);
64 try {
65 fileMap = getSequenceFiles(inputFile);
66 } catch (const Exception& UNUSED(e)) {
67 // we already show one message box in Run, silence the error here
68 fileMap.clear();
69 currentFrame = 0;
70 return;
71 }
72
73 OutputFile of(inputFile);
74 if (!of.hasWildcard()) {
75 currentFrame = OutputFile::getDumpIdx(inputFile).valueOr(0);
76 }
77
78 this->Refresh();
79 }
80
setFrame(const Size newFrame)81 void TimeLinePanel::setFrame(const Size newFrame) {
82 currentFrame = newFrame;
83 this->Refresh();
84 }
85
setPrevious()86 void TimeLinePanel::setPrevious() {
87 auto iter = fileMap.find(currentFrame);
88 SPH_ASSERT(iter != fileMap.end());
89 if (iter != fileMap.begin()) {
90 --iter;
91 currentFrame = iter->first;
92 this->reload();
93 }
94 }
95
startSequence()96 void TimeLinePanel::startSequence() {
97 callbacks->startSequence(fileMap[currentFrame]);
98 }
99
setNext()100 void TimeLinePanel::setNext() {
101 auto iter = fileMap.find(currentFrame);
102 SPH_ASSERT(iter != fileMap.end());
103 ++iter;
104 if (iter != fileMap.end()) {
105 currentFrame = iter->first;
106 this->reload();
107 }
108 }
109
positionToFrame(const wxPoint position) const110 int TimeLinePanel::positionToFrame(const wxPoint position) const {
111 if (fileMap.empty()) {
112 return 0;
113 }
114 wxSize size = this->GetSize();
115 const int firstFrame = fileMap.begin()->first;
116 const int lastFrame = fileMap.rbegin()->first;
117 const int frame = firstFrame + int(roundf(float(position.x) * (lastFrame - firstFrame) / size.x));
118 const auto upperIter = fileMap.upper_bound(frame);
119 if (upperIter == fileMap.begin()) {
120 return upperIter->first;
121 } else {
122 auto lowerIter = upperIter;
123 --lowerIter;
124
125 if (upperIter == fileMap.end()) {
126 return lowerIter->first;
127 } else {
128 // return the closer frame
129 const int lowerDist = frame - lowerIter->first;
130 const int upperDist = upperIter->first - frame;
131 return (upperDist < lowerDist) ? upperIter->first : lowerIter->first;
132 }
133 }
134 }
135
reload()136 void TimeLinePanel::reload() {
137 callbacks->frameChanged(fileMap[currentFrame]);
138 this->Refresh();
139 }
140
onPaint(wxPaintEvent & UNUSED (evt))141 void TimeLinePanel::onPaint(wxPaintEvent& UNUSED(evt)) {
142 wxAutoBufferedPaintDC dc(this);
143 dc.Clear();
144
145 if (fileMap.empty()) {
146 // nothing to do
147 return;
148 }
149 const wxSize size = dc.GetSize();
150 /// \todo deduplicate
151 Rgba backgroundColor = Rgba(this->GetParent()->GetBackgroundColour());
152 wxPen pen = *wxBLACK_PEN;
153 pen.SetWidth(2);
154 wxBrush brush;
155 wxColour fillColor(backgroundColor.darken(0.3f));
156 brush.SetColour(fillColor);
157 pen.SetColour(fillColor);
158
159 dc.SetBrush(brush);
160 dc.SetPen(pen);
161 dc.DrawRectangle(wxPoint(0, 0), size);
162 dc.SetTextForeground(wxColour(255, 255, 255));
163 wxFont font = dc.GetFont();
164 font.MakeSmaller();
165 dc.SetFont(font);
166
167 const int fileCnt = fileMap.size();
168 if (fileCnt == 1) {
169 // nothing to draw
170 return;
171 }
172
173 // ad hoc stepping
174 int step = 1;
175 if (fileCnt > 60) {
176 step = int(fileCnt / 60) * 5;
177 } else if (fileCnt > 30) {
178 step = 2;
179 }
180
181 const int firstFrame = fileMap.begin()->first;
182 const int lastFrame = fileMap.rbegin()->first;
183 int i = 0;
184
185 const bool isLightTheme = backgroundColor.intensity() > 0.5f;
186 if (isLightTheme) {
187 dc.SetTextForeground(wxColour(30, 30, 30));
188 }
189
190 for (auto frameAndPath : fileMap) {
191 const int frame = frameAndPath.first;
192 bool keyframe = (i % step == 0);
193 bool doFull = keyframe;
194 if (frame == currentFrame) {
195 pen.SetColour(wxColour(255, 80, 0));
196 doFull = true;
197 } else if (frame == mouseFrame) {
198 pen.SetColour(wxColour(128, 128, 128));
199 doFull = true;
200 } else {
201 if (isLightTheme) {
202 pen.SetColour(wxColour(30, 30, 30));
203 } else {
204 pen.SetColour(wxColour(backgroundColor));
205 }
206 }
207 dc.SetPen(pen);
208 const int x = (frame - firstFrame) * size.x / (lastFrame - firstFrame);
209 if (doFull) {
210 dc.DrawLine(wxPoint(x, 0), wxPoint(x, size.y));
211 } else {
212 dc.DrawLine(wxPoint(x, 0), wxPoint(x, 5));
213 dc.DrawLine(wxPoint(x, size.y - 5), wxPoint(x, size.y));
214 }
215
216 if (keyframe) {
217 const std::string text = std::to_string(frame);
218 const wxSize extent = dc.GetTextExtent(text);
219 if (x + extent.x + 3 < size.x) {
220 dc.DrawText(text, wxPoint(x + 3, size.y - 20));
221 }
222 }
223 ++i;
224 }
225 }
226
onMouseMotion(wxMouseEvent & evt)227 void TimeLinePanel::onMouseMotion(wxMouseEvent& evt) {
228 mouseFrame = positionToFrame(evt.GetPosition());
229 this->Refresh();
230 }
231
onLeftClick(wxMouseEvent & evt)232 void TimeLinePanel::onLeftClick(wxMouseEvent& evt) {
233 currentFrame = positionToFrame(evt.GetPosition());
234 this->reload();
235 }
236
onKeyUp(wxKeyEvent & evt)237 void TimeLinePanel::onKeyUp(wxKeyEvent& evt) {
238 switch (evt.GetKeyCode()) {
239 case WXK_LEFT:
240 this->setPrevious();
241 break;
242 case WXK_RIGHT:
243 this->setNext();
244 break;
245
246 default:
247 break;
248 }
249 }
250
createButton(wxWindow * parent,const wxBitmap & bitmap)251 static wxBitmapButton* createButton(wxWindow* parent, const wxBitmap& bitmap) {
252 const wxSize buttonSize(60, 40);
253 wxBitmapButton* button = new wxBitmapButton(parent, wxID_ANY, bitmap);
254 button->SetMinSize(buttonSize);
255 return button;
256 }
257
createButton(wxWindow * parent,char ** data)258 static wxBitmapButton* createButton(wxWindow* parent, char** data) {
259 wxBitmap bitmap(data);
260 return createButton(parent, bitmap);
261 }
262
TimeLine(wxWindow * parent,const Path & inputFile,SharedPtr<ITimeLineCallbacks> callbacks)263 TimeLine::TimeLine(wxWindow* parent, const Path& inputFile, SharedPtr<ITimeLineCallbacks> callbacks)
264 : wxPanel(parent, wxID_ANY) {
265 wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL);
266
267 wxImage nextImage = wxBitmap(nextData).ConvertToImage();
268 nextImage = nextImage.Mirror();
269 wxBitmapButton* prevButton = createButton(this, wxBitmap(nextImage, wxBITMAP_SCREEN_DEPTH));
270 sizer->Add(prevButton, 1, wxALL);
271
272 wxBitmapButton* pauseButton = createButton(this, pauseData);
273 sizer->Add(pauseButton, 1, wxALL);
274
275 wxBitmapButton* stopButton = createButton(this, stopData);
276 sizer->Add(stopButton, 1, wxALL);
277
278 wxBitmapButton* playButton = createButton(this, playData);
279 sizer->Add(playButton, 1, wxALL);
280
281 wxBitmapButton* nextButton = createButton(this, nextData);
282 sizer->Add(nextButton, 1, wxALL);
283
284 sizer->AddSpacer(8);
285 timeline = new TimeLinePanel(this, inputFile, callbacks);
286 sizer->Add(timeline, 40, wxALL | wxEXPAND);
287
288 this->SetSizer(sizer);
289 this->Layout();
290
291 prevButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& UNUSED(evt)) { timeline->setPrevious(); });
292 nextButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& UNUSED(evt)) { timeline->setNext(); });
293 playButton->Bind(wxEVT_BUTTON, [this](wxCommandEvent& UNUSED(evt)) { timeline->startSequence(); });
294 stopButton->Bind(wxEVT_BUTTON, [callbacks](wxCommandEvent& UNUSED(evt)) { callbacks->stop(); });
295 pauseButton->Bind(wxEVT_BUTTON, [callbacks](wxCommandEvent& UNUSED(evt)) { callbacks->pause(); });
296 }
297
298 NAMESPACE_SPH_END
299