Add GUI control panel (first draft)
[mouse-tracker-for-cubism.git] / src / mouse_cursor_tracker_gui.cpp
1 #include <vector>
2 #include <map>
3 #include <string>
4 #include <algorithm>
5 #include <mutex>
6
7 #include <gtkmm/application.h>
8 #include <gtkmm/builder.h>
9 #include <gtkmm/window.h>
10 #include <gtkmm/comboboxtext.h>
11 #include <gtkmm/button.h>
12 #include <gtkmm/scale.h>
13 #include <gtkmm/adjustment.h>
14 #include <gtkmm/checkbutton.h>
15 #include <gtkmm/expander.h>
16
17 #include "mouse_cursor_tracker.h"
18
19 extern std::vector<std::string> live2dParams;
20 extern std::vector<std::pair<std::string, int> > MCT_motions;
21 extern std::vector<std::string> MCT_expressions;
22 extern std::map<std::string, double> *MCT_overrideMap;
23
24 #define GET_WIDGET_ASSERT(id, widgetPtr) do { \
25 m_builder->get_widget(id, widgetPtr); \
26 if (!widgetPtr) abort(); \
27 } while (0)
28
29 void MouseCursorTracker::onMotionStartButton(void)
30 {
31 std::unique_lock<std::mutex> lock(m_motionMutex, std::defer_lock);
32 lock.lock();
33
34 Gtk::ComboBoxText *cbt;
35 GET_WIDGET_ASSERT("comboBoxMotions", cbt);
36 std::string motionStr = cbt->get_active_text();
37 if (!motionStr.empty())
38 {
39 std::stringstream ss(motionStr);
40 ss >> m_motionGroup >> m_motionNumber;
41 }
42
43 GET_WIDGET_ASSERT("comboBoxMotionPriority", cbt);
44 std::string priority = cbt->get_active_id();
45 if (priority == "priorityForce")
46 {
47 m_motionPriority = MotionPriority::force;
48 }
49 else if (priority == "priorityNormal")
50 {
51 m_motionPriority = MotionPriority::normal;
52 }
53 else if (priority == "priorityIdle")
54 {
55 m_motionPriority = MotionPriority::idle;
56 }
57 else
58 {
59 m_motionPriority = MotionPriority::none;
60 }
61
62 lock.unlock();
63 }
64
65 void MouseCursorTracker::onExpressionStartButton(void)
66 {
67 std::unique_lock<std::mutex> lock(m_motionMutex, std::defer_lock);
68 lock.lock();
69
70 Gtk::ComboBoxText *cbt;
71 GET_WIDGET_ASSERT("comboBoxExpressions", cbt);
72 std::string expStr = cbt->get_active_text();
73 if (!expStr.empty())
74 {
75 m_expression = expStr;
76 }
77
78 lock.unlock();
79 }
80
81 void MouseCursorTracker::onParamUpdateButton(Gtk::Scale *scale, bool isInc)
82 {
83 Glib::RefPtr<Gtk::Adjustment> adj = scale->get_adjustment();
84 double value = adj->get_value();
85 double increment = adj->get_step_increment();
86 adj->set_value(value + increment * (isInc ? 1 : -1));
87 }
88
89 void MouseCursorTracker::onParamValChanged(Glib::RefPtr<Gtk::Adjustment> adj, std::string paramName)
90 {
91 bool isResetting = std::find(
92 m_onClearResettingParams.begin(),
93 m_onClearResettingParams.end(),
94 paramName) != m_onClearResettingParams.end();
95
96 if (isResetting)
97 {
98 // Value changed event caused by onClearButton,
99 // don't change underlying value
100 return;
101 }
102
103 double value = adj->get_value();
104 m_overrideMap["Param" + paramName] = value;
105 /* Use get_object instead of get_widget to avoid
106 * "widget not found" error messages */
107 auto obj = m_builder->get_object("button" + paramName + "Clear");
108 if (obj)
109 {
110 auto button = Glib::RefPtr<Gtk::Button>::cast_dynamic(obj);
111 button->set_visible(true);
112 }
113 }
114
115 void MouseCursorTracker::onClearButton(Glib::RefPtr<Gtk::Button> button, std::string paramName, Glib::RefPtr<Gtk::Adjustment> adj)
116 {
117 m_overrideMap.erase("Param" + paramName);
118
119 // Reset the GtkScale display value to 0, without running the callback.
120 m_onClearResettingParams.push_back(paramName);
121 if (adj)
122 {
123 double value = 0;
124 // Special case for MouthForm: use value from config file
125 if (paramName == "MouthForm")
126 {
127 value = m_cfg.mouthForm;
128 }
129 adj->set_value(value);
130 }
131 m_onClearResettingParams.erase(
132 std::remove(
133 m_onClearResettingParams.begin(),
134 m_onClearResettingParams.end(),
135 paramName),
136 m_onClearResettingParams.end()
137 );
138
139 button->set_visible(false);
140 }
141
142 void MouseCursorTracker::onAutoToggle(Gtk::CheckButton *check, Gtk::Scale *scale,
143 Gtk::Button *buttonDec, Gtk::Button *buttonInc,
144 std::string paramName)
145 {
146 bool active = check->get_active();
147 scale->set_sensitive(!active);
148 buttonDec->set_sensitive(!active);
149 buttonInc->set_sensitive(!active);
150
151 if (active)
152 {
153 m_overrideMap.erase(paramName);
154 }
155 else
156 {
157 auto adj = scale->get_adjustment();
158 double value = adj->get_value();
159 m_overrideMap[paramName] = value;
160 }
161
162 // Special cases for lip sync and auto breath flags
163 if (paramName == "ParamMouthOpenY")
164 {
165 m_cfg.useLipSync = active;
166 }
167 else if (paramName == "ParamBreath")
168 {
169 m_cfg.autoBreath = active;
170 }
171 }
172
173 void MouseCursorTracker::onExpanderChange(Gtk::Window *window)
174 {
175 // Shrink window if enlarged by GtkExpander
176 window->resize(1, 1);
177 }
178
179 void MouseCursorTracker::guiLoop(void)
180 {
181 m_builder = Gtk::Builder::create_from_file("gui.glade");
182
183 // Add motions list to combobox
184 Gtk::ComboBoxText *motionsCbt;
185 GET_WIDGET_ASSERT("comboBoxMotions", motionsCbt);
186 for (auto it = MCT_motions.begin(); it != MCT_motions.end(); ++it)
187 {
188 for (int i = 0; i < it->second; i++)
189 {
190 std::stringstream ss;
191 ss << it->first << " " << i;
192 motionsCbt->append(ss.str());
193 }
194 }
195
196 // Add expressions list to combobox
197 Gtk::ComboBoxText *expsCbt;
198 GET_WIDGET_ASSERT("comboBoxExpressions", expsCbt);
199 for (auto it = MCT_expressions.begin(); it != MCT_expressions.end(); ++it)
200 {
201 expsCbt->append(*it);
202 }
203
204
205 // Add scale tick marks
206 Gtk::Scale *scale;
207
208 std::vector<std::string> ticksAtZero =
209 {
210 "scaleAngleX", "scaleAngleY", "scaleAngleZ",
211 "scaleEyeBallX", "scaleEyeBallY", "scaleEyeBallForm",
212 "scaleMouthForm",
213 "scaleBrowLX", "scaleBrowLY", "scaleBrowLAngle",
214 "scaleBrowLForm",
215 "scaleBrowRX", "scaleBrowRY", "scaleBrowRAngle",
216 "scaleBrowRForm",
217 "scaleHairFront", "scaleHairSide", "scaleHairBack",
218 "scaleBodyAngleX", "scaleBodyAngleY", "scaleBodyAngleZ",
219 "scaleBustX", "scaleBustY", "scaleBaseX", "scaleBaseY",
220 "scaleArmLA", "scaleArmLB", "scaleArmRA", "scaleArmRB",
221 "scaleHandL", "scaleHandR"
222 };
223
224 for (auto it = ticksAtZero.begin(); it != ticksAtZero.end(); ++it)
225 {
226 GET_WIDGET_ASSERT(*it, scale);
227 scale->add_mark(0, Gtk::PositionType::POS_BOTTOM, "");
228 }
229
230 GET_WIDGET_ASSERT("scaleEyeLOpen", scale);
231 scale->add_mark(0, Gtk::PositionType::POS_BOTTOM, "");
232 scale->add_mark(1, Gtk::PositionType::POS_BOTTOM, "");
233 GET_WIDGET_ASSERT("scaleEyeROpen", scale);
234 scale->add_mark(0, Gtk::PositionType::POS_BOTTOM, "");
235 scale->add_mark(1, Gtk::PositionType::POS_BOTTOM, "");
236
237 GET_WIDGET_ASSERT("scaleMouthOpenY", scale);
238 scale->add_mark(1, Gtk::PositionType::POS_BOTTOM, "");
239
240 // Bind button handlers
241 Gtk::Button *button;
242 GET_WIDGET_ASSERT("buttonStartMotion", button);
243 button->signal_clicked().connect(sigc::mem_fun(*this, &MouseCursorTracker::onMotionStartButton));
244
245 GET_WIDGET_ASSERT("buttonStartExpression", button);
246 button->signal_clicked().connect(sigc::mem_fun(*this, &MouseCursorTracker::onExpressionStartButton));
247
248 // Bind button handlers for increment / decrement buttons
249 for (auto it = live2dParams.begin(); it != live2dParams.end(); ++it)
250 {
251 std::string paramName = *it; // e.g. ParamAngleX
252 paramName.erase(0, 5); // e.g. AngleX
253 std::string buttonDecId = "button" + paramName + "Dec";
254 std::string buttonIncId = "button" + paramName + "Inc";
255 std::string buttonClrId = "button" + paramName + "Clear";
256 std::string scaleId = "scale" + paramName;
257
258 m_builder->get_widget(scaleId, scale);
259
260 m_builder->get_widget(buttonDecId, button);
261 if (button && scale)
262 {
263 button->signal_clicked().connect(
264 sigc::bind<Gtk::Scale *, bool>(
265 sigc::mem_fun(*this, &MouseCursorTracker::onParamUpdateButton),
266 scale, false
267 )
268 );
269 }
270
271 m_builder->get_widget(buttonIncId, button);
272 if (button && scale)
273 {
274 button->signal_clicked().connect(
275 sigc::bind<Gtk::Scale *, bool>(
276 sigc::mem_fun(*this, &MouseCursorTracker::onParamUpdateButton),
277 scale, true
278 )
279 );
280 }
281
282 Glib::RefPtr<Gtk::Adjustment> adj;
283 if (scale)
284 {
285 adj = scale->get_adjustment();
286 if (!adj) abort();
287 adj->signal_value_changed().connect(
288 sigc::bind<Glib::RefPtr<Gtk::Adjustment>, std::string>(
289 sigc::mem_fun(*this, &MouseCursorTracker::onParamValChanged),
290 adj, paramName
291 )
292 );
293 }
294
295 /* Use get_object instead of get_widget to avoid
296 * "widget not found" error messages */
297 auto obj = m_builder->get_object(buttonClrId);
298 if (obj)
299 {
300 auto buttonClr = Glib::RefPtr<Gtk::Button>::cast_dynamic(obj);
301 buttonClr->signal_clicked().connect(
302 sigc::bind<Glib::RefPtr<Gtk::Button>, std::string, Glib::RefPtr<Gtk::Adjustment> >(
303 sigc::mem_fun(*this, &MouseCursorTracker::onClearButton),
304 buttonClr, paramName, adj
305 )
306 );
307 }
308 }
309
310 // Bind handlers for auto params check boxes
311 Gtk::CheckButton *check;
312 Gtk::Button *buttonInc;
313 Gtk::Button *buttonDec;
314
315 std::vector<std::string> autoTracked =
316 {
317 "AngleX", "AngleY", "EyeLOpen", "EyeROpen", "MouthOpenY", "Breath"
318 };
319
320 for (auto it = autoTracked.begin(); it != autoTracked.end(); ++it)
321 {
322 GET_WIDGET_ASSERT("check" + *it, check);
323 GET_WIDGET_ASSERT("scale" + *it, scale);
324 GET_WIDGET_ASSERT("button" + *it + "Inc", buttonInc);
325 GET_WIDGET_ASSERT("button" + *it + "Dec", buttonDec);
326 check->signal_toggled().connect(
327 sigc::bind<Gtk::CheckButton *, Gtk::Scale *, Gtk::Button *, Gtk::Button *, std::string>(
328 sigc::mem_fun(*this, &MouseCursorTracker::onAutoToggle),
329 check, scale, buttonDec, buttonInc, "Param" + *it
330 )
331 );
332 }
333
334 // Set some values from config file
335 GET_WIDGET_ASSERT("checkMouthOpenY", check);
336 check->set_active(m_cfg.useLipSync);
337 GET_WIDGET_ASSERT("checkBreath", check);
338 check->set_active(m_cfg.autoBreath);
339 GET_WIDGET_ASSERT("scaleMouthForm", scale);
340 auto adj = scale->get_adjustment();
341 // Don't trigger value changed event
342 m_onClearResettingParams.push_back("MouthForm");
343 adj->set_value(m_cfg.mouthForm);
344 m_onClearResettingParams.erase(
345 std::remove(
346 m_onClearResettingParams.begin(),
347 m_onClearResettingParams.end(),
348 "MouthForm"),
349 m_onClearResettingParams.end()
350 );
351
352 Gtk::Window *window;
353 GET_WIDGET_ASSERT("windowMain", window);
354
355 std::vector<std::string> expanders =
356 {
357 "expanderHead", "expanderEyes", "expanderEyebrows",
358 "expanderMouthFace", "expanderHair", "expanderBody",
359 "expanderArmsHands"
360 };
361 Gtk::Expander *expander;
362 for (auto it = expanders.begin(); it != expanders.end(); ++it)
363 {
364 m_builder->get_widget(*it, expander);
365 if (expander)
366 {
367 expander->property_expanded().signal_changed().connect(
368 sigc::bind<Gtk::Window *>(
369 sigc::mem_fun(*this, &MouseCursorTracker::onExpanderChange),
370 window
371 )
372 );
373 }
374 }
375
376 m_gtkapp->run(*window);
377 }
378