Field.pm 16 KB

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