Field.pm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  1. # An input field class prototype.
  2. package Slic3r::GUI::OptionsGroup::Field;
  3. use Moo;
  4. # This is a base class for option fields.
  5. has 'parent' => (is => 'ro', required => 1);
  6. # Slic3r::GUI::OptionsGroup::Option
  7. has 'option' => (is => 'ro', required => 1);
  8. # On change callback
  9. has 'on_change' => (is => 'rw', default => sub { sub {} });
  10. has 'on_kill_focus' => (is => 'rw', default => sub { sub {} });
  11. # If set, the callback $self->on_change is not called.
  12. # This is used to avoid recursive invocation of the field change/update by wxWidgets.
  13. has 'disable_change_event' => (is => 'rw', default => sub { 0 });
  14. # This method should not fire the on_change event
  15. sub set_value {
  16. my ($self, $value) = @_;
  17. die "Method not implemented";
  18. }
  19. sub get_value {
  20. my ($self) = @_;
  21. die "Method not implemented";
  22. }
  23. sub set_tooltip {
  24. my ($self, $tooltip) = @_;
  25. $self->SetToolTipString($tooltip)
  26. if $tooltip && $self->can('SetToolTipString');
  27. }
  28. sub toggle {
  29. my ($self, $enable) = @_;
  30. $enable ? $self->enable : $self->disable;
  31. }
  32. sub _on_change {
  33. my ($self, $opt_id) = @_;
  34. $self->on_change->($opt_id, $self->get_value)
  35. unless $self->disable_change_event;
  36. }
  37. sub _on_kill_focus {
  38. my ($self, $opt_id, $s, $event) = @_;
  39. # Without this, there will be nasty focus bugs on Windows.
  40. # Also, docs for wxEvent::Skip() say "In general, it is recommended to skip all
  41. # non-command events to allow the default handling to take place."
  42. $event->Skip(1);
  43. $self->on_kill_focus->($opt_id);
  44. }
  45. package Slic3r::GUI::OptionsGroup::Field::wxWindow;
  46. use Moo;
  47. extends 'Slic3r::GUI::OptionsGroup::Field';
  48. has 'wxWindow' => (is => 'rw', trigger => 1); # wxWindow object
  49. sub _default_size {
  50. my ($self) = @_;
  51. # default width on Windows is too large
  52. return Wx::Size->new($self->option->width || 60, $self->option->height || -1);
  53. }
  54. sub _trigger_wxWindow {
  55. my ($self) = @_;
  56. $self->wxWindow->SetToolTipString($self->option->tooltip)
  57. if $self->option->tooltip && $self->wxWindow->can('SetToolTipString');
  58. }
  59. sub set_value {
  60. my ($self, $value) = @_;
  61. $self->disable_change_event(1);
  62. $self->wxWindow->SetValue($value);
  63. $self->disable_change_event(0);
  64. }
  65. sub get_value {
  66. my ($self) = @_;
  67. return $self->wxWindow->GetValue;
  68. }
  69. sub enable {
  70. my ($self) = @_;
  71. $self->wxWindow->Enable;
  72. $self->wxWindow->Refresh;
  73. }
  74. sub disable {
  75. my ($self) = @_;
  76. $self->wxWindow->Disable;
  77. $self->wxWindow->Refresh;
  78. }
  79. package Slic3r::GUI::OptionsGroup::Field::Checkbox;
  80. use Moo;
  81. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  82. use Wx qw(:misc);
  83. use Wx::Event qw(EVT_CHECKBOX);
  84. sub BUILD {
  85. my ($self) = @_;
  86. my $field = Wx::CheckBox->new($self->parent, -1, "");
  87. $self->wxWindow($field);
  88. $field->SetValue($self->option->default);
  89. $field->Disable if $self->option->readonly;
  90. EVT_CHECKBOX($self->parent, $field, sub {
  91. $self->_on_change($self->option->opt_id);
  92. });
  93. }
  94. package Slic3r::GUI::OptionsGroup::Field::SpinCtrl;
  95. use Moo;
  96. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  97. use Wx qw(:misc);
  98. use Wx::Event qw(EVT_SPINCTRL EVT_TEXT EVT_KILL_FOCUS);
  99. has 'tmp_value' => (is => 'rw');
  100. sub BUILD {
  101. my ($self) = @_;
  102. my $field = Wx::SpinCtrl->new($self->parent, -1, $self->option->default, wxDefaultPosition, $self->_default_size,
  103. 0, $self->option->min || 0, $self->option->max || 2147483647, $self->option->default);
  104. $self->wxWindow($field);
  105. EVT_SPINCTRL($self->parent, $field, sub {
  106. $self->tmp_value(undef);
  107. $self->_on_change($self->option->opt_id);
  108. });
  109. EVT_TEXT($self->parent, $field, sub {
  110. my ($s, $event) = @_;
  111. # On OSX/Cocoa, wxSpinCtrl::GetValue() doesn't return the new value
  112. # when it was changed from the text control, so the on_change callback
  113. # gets the old one, and on_kill_focus resets the control to the old value.
  114. # As a workaround, we get the new value from $event->GetString and store
  115. # here temporarily so that we can return it from $self->get_value
  116. $self->tmp_value($event->GetString) if $event->GetString =~ /^\d+$/;
  117. $self->_on_change($self->option->opt_id);
  118. # We don't reset tmp_value here because _on_change might put callbacks
  119. # in the CallAfter queue, and we want the tmp value to be available from
  120. # them as well.
  121. });
  122. EVT_KILL_FOCUS($field, sub {
  123. $self->tmp_value(undef);
  124. $self->_on_kill_focus($self->option->opt_id, @_);
  125. });
  126. }
  127. sub get_value {
  128. my ($self) = @_;
  129. return $self->tmp_value // $self->wxWindow->GetValue;
  130. }
  131. package Slic3r::GUI::OptionsGroup::Field::TextCtrl;
  132. use Moo;
  133. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  134. use Wx qw(:misc :textctrl);
  135. use Wx::Event qw(EVT_TEXT EVT_KILL_FOCUS);
  136. sub BUILD {
  137. my ($self) = @_;
  138. my $style = 0;
  139. $style = wxTE_MULTILINE if $self->option->multiline;
  140. my $field = Wx::TextCtrl->new($self->parent, -1, $self->option->default, wxDefaultPosition,
  141. $self->_default_size, $style);
  142. $self->wxWindow($field);
  143. # TODO: test loading a config that has empty string for multi-value options like 'wipe'
  144. EVT_TEXT($self->parent, $field, sub {
  145. $self->_on_change($self->option->opt_id);
  146. });
  147. EVT_KILL_FOCUS($field, sub {
  148. $self->_on_kill_focus($self->option->opt_id, @_);
  149. });
  150. }
  151. sub enable {
  152. my ($self) = @_;
  153. $self->wxWindow->Enable;
  154. $self->wxWindow->SetEditable(1);
  155. }
  156. sub disable {
  157. my ($self) = @_;
  158. $self->wxWindow->Disable;
  159. $self->wxWindow->SetEditable(0);
  160. }
  161. package Slic3r::GUI::OptionsGroup::Field::Choice;
  162. use Moo;
  163. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  164. use List::Util qw(first);
  165. use Wx qw(:misc :combobox);
  166. use Wx::Event qw(EVT_COMBOBOX EVT_TEXT);
  167. sub BUILD {
  168. my ($self) = @_;
  169. my $style = 0;
  170. $style |= wxCB_READONLY if defined $self->option->gui_type && $self->option->gui_type ne 'select_open';
  171. my $field = Wx::ComboBox->new($self->parent, -1, "", wxDefaultPosition, $self->_default_size,
  172. $self->option->labels || $self->option->values || [], $style);
  173. $self->wxWindow($field);
  174. $self->set_value($self->option->default);
  175. EVT_COMBOBOX($self->parent, $field, sub {
  176. $self->_on_change($self->option->opt_id);
  177. });
  178. EVT_TEXT($self->parent, $field, sub {
  179. $self->_on_change($self->option->opt_id);
  180. });
  181. }
  182. sub set_value {
  183. my ($self, $value) = @_;
  184. $self->disable_change_event(1);
  185. my $idx;
  186. if ($self->option->values) {
  187. $idx = first { $self->option->values->[$_] eq $value } 0..$#{$self->option->values};
  188. # if value is not among indexes values we use SetValue()
  189. }
  190. if (defined $idx) {
  191. $self->wxWindow->SetSelection($idx);
  192. } else {
  193. $self->wxWindow->SetValue($value);
  194. }
  195. $self->disable_change_event(0);
  196. }
  197. sub set_values {
  198. my ($self, $values) = @_;
  199. $self->disable_change_event(1);
  200. # it looks that Clear() also clears the text field in recent wxWidgets versions,
  201. # but we want to preserve it
  202. my $ww = $self->wxWindow;
  203. my $value = $ww->GetValue;
  204. $ww->Clear;
  205. $ww->Append($_) for @$values;
  206. $ww->SetValue($value);
  207. $self->disable_change_event(0);
  208. }
  209. sub get_value {
  210. my ($self) = @_;
  211. if ($self->option->values) {
  212. my $idx = $self->wxWindow->GetSelection;
  213. if ($idx != &Wx::wxNOT_FOUND) {
  214. return $self->option->values->[$idx];
  215. }
  216. }
  217. return $self->wxWindow->GetValue;
  218. }
  219. package Slic3r::GUI::OptionsGroup::Field::NumericChoice;
  220. use Moo;
  221. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  222. use List::Util qw(first);
  223. use Wx qw(wxTheApp :misc :combobox);
  224. use Wx::Event qw(EVT_COMBOBOX EVT_TEXT);
  225. # if option has no 'values', indices are values
  226. # if option has no 'labels', values are labels
  227. sub BUILD {
  228. my ($self) = @_;
  229. my $field = Wx::ComboBox->new($self->parent, -1, $self->option->default, wxDefaultPosition, $self->_default_size,
  230. $self->option->labels || $self->option->values);
  231. $self->wxWindow($field);
  232. $self->set_value($self->option->default);
  233. EVT_COMBOBOX($self->parent, $field, sub {
  234. my $disable_change_event = $self->disable_change_event;
  235. $self->disable_change_event(1);
  236. my $idx = $field->GetSelection; # get index of selected value
  237. my $label;
  238. if ($self->option->labels && $idx <= $#{$self->option->labels}) {
  239. $label = $self->option->labels->[$idx];
  240. } elsif ($self->option->values && $idx <= $#{$self->option->values}) {
  241. $label = $self->option->values->[$idx];
  242. } else {
  243. $label = $idx;
  244. }
  245. # The MSW implementation of wxComboBox will leave the field blank if we call
  246. # SetValue() in the EVT_COMBOBOX event handler, so we postpone the call.
  247. wxTheApp->CallAfter(sub {
  248. my $dce = $self->disable_change_event;
  249. $self->disable_change_event(1);
  250. # ChangeValue() is not exported in wxPerl
  251. $field->SetValue($label);
  252. $self->disable_change_event($dce);
  253. });
  254. $self->disable_change_event($disable_change_event);
  255. $self->_on_change($self->option->opt_id);
  256. });
  257. EVT_TEXT($self->parent, $field, sub {
  258. $self->_on_change($self->option->opt_id);
  259. });
  260. }
  261. sub set_value {
  262. my ($self, $value) = @_;
  263. $self->disable_change_event(1);
  264. my $field = $self->wxWindow;
  265. if ($self->option->gui_flags =~ /\bshow_value\b/) {
  266. $field->SetValue($value);
  267. } else {
  268. if ($self->option->values) {
  269. # check whether we have a value index
  270. my $value_idx = first { $self->option->values->[$_] eq $value } 0..$#{$self->option->values};
  271. if (defined $value_idx) {
  272. $field->SetSelection($value_idx);
  273. $self->disable_change_event(0);
  274. return;
  275. }
  276. } elsif ($self->option->labels && $value <= $#{$self->option->labels}) {
  277. # if we have no values, we expect value to be an index
  278. $field->SetValue($self->option->labels->[$value]);
  279. $self->disable_change_event(0);
  280. return;
  281. }
  282. $field->SetValue($value);
  283. }
  284. $self->disable_change_event(0);
  285. }
  286. sub get_value {
  287. my ($self) = @_;
  288. my $label = $self->wxWindow->GetValue;
  289. if ($self->option->labels) {
  290. my $value_idx = first { $self->option->labels->[$_] eq $label } 0..$#{$self->option->labels};
  291. if (defined $value_idx) {
  292. if ($self->option->values) {
  293. return $self->option->values->[$value_idx];
  294. }
  295. return $value_idx;
  296. }
  297. }
  298. return $label;
  299. }
  300. package Slic3r::GUI::OptionsGroup::Field::ColourPicker;
  301. use Moo;
  302. extends 'Slic3r::GUI::OptionsGroup::Field::wxWindow';
  303. use Wx qw(:misc :colour);
  304. use Wx::Event qw(EVT_COLOURPICKER_CHANGED);
  305. sub BUILD {
  306. my ($self) = @_;
  307. my $field = Wx::ColourPickerCtrl->new($self->parent, -1,
  308. $self->_string_to_colour($self->option->default), wxDefaultPosition,
  309. $self->_default_size);
  310. $self->wxWindow($field);
  311. EVT_COLOURPICKER_CHANGED($self->parent, $field, sub {
  312. $self->_on_change($self->option->opt_id);
  313. });
  314. }
  315. sub set_value {
  316. my ($self, $value) = @_;
  317. $self->disable_change_event(1);
  318. $self->wxWindow->SetColour($self->_string_to_colour($value));
  319. $self->disable_change_event(0);
  320. }
  321. sub get_value {
  322. my ($self) = @_;
  323. return $self->wxWindow->GetColour->GetAsString(wxC2S_HTML_SYNTAX);
  324. }
  325. sub _string_to_colour {
  326. my ($self, $string) = @_;
  327. $string =~ s/^#//;
  328. # If the color is in an invalid format, set it to white.
  329. $string = 'FFFFFF' if ($string !~ m/^[[:xdigit:]]{6}/);
  330. return Wx::Colour->new(unpack 'C*', pack 'H*', $string);
  331. }
  332. package Slic3r::GUI::OptionsGroup::Field::wxSizer;
  333. use Moo;
  334. extends 'Slic3r::GUI::OptionsGroup::Field';
  335. has 'wxSizer' => (is => 'rw'); # wxSizer object
  336. package Slic3r::GUI::OptionsGroup::Field::Point;
  337. use Moo;
  338. extends 'Slic3r::GUI::OptionsGroup::Field::wxSizer';
  339. has 'x_textctrl' => (is => 'rw');
  340. has 'y_textctrl' => (is => 'rw');
  341. use Slic3r::Geometry qw(X Y);
  342. use Wx qw(:misc :sizer);
  343. use Wx::Event qw(EVT_TEXT);
  344. sub BUILD {
  345. my ($self) = @_;
  346. my $sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  347. $self->wxSizer($sizer);
  348. my $field_size = Wx::Size->new(40, -1);
  349. $self->x_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[X], wxDefaultPosition, $field_size));
  350. $self->y_textctrl(Wx::TextCtrl->new($self->parent, -1, $self->option->default->[Y], wxDefaultPosition, $field_size));
  351. my @items = (
  352. Wx::StaticText->new($self->parent, -1, "x:"),
  353. $self->x_textctrl,
  354. Wx::StaticText->new($self->parent, -1, " y:"),
  355. $self->y_textctrl,
  356. );
  357. $sizer->Add($_, 0, wxALIGN_CENTER_VERTICAL, 0) for @items;
  358. if ($self->option->tooltip) {
  359. foreach my $item (@items) {
  360. $item->SetToolTipString($self->option->tooltip)
  361. if $item->can('SetToolTipString');
  362. }
  363. }
  364. EVT_TEXT($self->parent, $_, sub {
  365. $self->_on_change($self->option->opt_id);
  366. }) for $self->x_textctrl, $self->y_textctrl;
  367. }
  368. sub set_value {
  369. my ($self, $value) = @_;
  370. $self->disable_change_event(1);
  371. $self->x_textctrl->SetValue($value->[X]);
  372. $self->y_textctrl->SetValue($value->[Y]);
  373. $self->disable_change_event(0);
  374. }
  375. sub get_value {
  376. my ($self) = @_;
  377. return [
  378. $self->x_textctrl->GetValue,
  379. $self->y_textctrl->GetValue,
  380. ];
  381. }
  382. sub enable {
  383. my ($self) = @_;
  384. $self->x_textctrl->Enable;
  385. $self->y_textctrl->Enable;
  386. }
  387. sub disable {
  388. my ($self) = @_;
  389. $self->x_textctrl->Disable;
  390. $self->y_textctrl->Disable;
  391. }
  392. package Slic3r::GUI::OptionsGroup::Field::Slider;
  393. use Moo;
  394. extends 'Slic3r::GUI::OptionsGroup::Field::wxSizer';
  395. has 'scale' => (is => 'rw', default => sub { 10 });
  396. has 'slider' => (is => 'rw');
  397. has 'textctrl' => (is => 'rw');
  398. use Wx qw(:misc :sizer);
  399. use Wx::Event qw(EVT_SLIDER EVT_TEXT EVT_KILL_FOCUS);
  400. sub BUILD {
  401. my ($self) = @_;
  402. my $sizer = Wx::BoxSizer->new(wxHORIZONTAL);
  403. $self->wxSizer($sizer);
  404. my $slider = Wx::Slider->new(
  405. $self->parent, -1,
  406. ($self->option->default // $self->option->min) * $self->scale,
  407. ($self->option->min // 0) * $self->scale,
  408. ($self->option->max // 100) * $self->scale,
  409. wxDefaultPosition,
  410. [ $self->option->width // -1, $self->option->height // -1 ],
  411. );
  412. $self->slider($slider);
  413. my $textctrl = Wx::TextCtrl->new($self->parent, -1, $slider->GetValue/$self->scale,
  414. wxDefaultPosition, [50,-1]);
  415. $self->textctrl($textctrl);
  416. $sizer->Add($slider, 1, wxALIGN_CENTER_VERTICAL, 0);
  417. $sizer->Add($textctrl, 0, wxALIGN_CENTER_VERTICAL, 0);
  418. EVT_SLIDER($self->parent, $slider, sub {
  419. if (! $self->disable_change_event) {
  420. $self->textctrl->SetLabel($self->get_value);
  421. $self->_on_change($self->option->opt_id);
  422. }
  423. });
  424. EVT_TEXT($self->parent, $textctrl, sub {
  425. my $value = $textctrl->GetValue;
  426. if ($value =~ /^-?\d+(\.\d*)?$/) {
  427. $self->disable_change_event(1);
  428. $self->slider->SetValue($value*$self->scale);
  429. $self->disable_change_event(0);
  430. $self->_on_change($self->option->opt_id);
  431. }
  432. });
  433. EVT_KILL_FOCUS($textctrl, sub {
  434. $self->_on_kill_focus($self->option->opt_id, @_);
  435. });
  436. }
  437. sub set_value {
  438. my ($self, $value) = @_;
  439. $self->disable_change_event(1);
  440. $self->slider->SetValue($value*$self->scale);
  441. $self->textctrl->SetLabel($self->get_value);
  442. $self->disable_change_event(0);
  443. }
  444. sub get_value {
  445. my ($self) = @_;
  446. return $self->slider->GetValue/$self->scale;
  447. }
  448. sub enable {
  449. my ($self) = @_;
  450. $self->slider->Enable;
  451. $self->textctrl->Enable;
  452. $self->textctrl->SetEditable(1);
  453. }
  454. sub disable {
  455. my ($self) = @_;
  456. $self->slider->Disable;
  457. $self->textctrl->Disable;
  458. $self->textctrl->SetEditable(0);
  459. }
  460. 1;