Field.pm 17 KB

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