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