1# contains:
2#   SpinButton
3#   AltSpinButton
4#   SpinEdit
5#   Gauge
6#   Slider
7#   CircularSlider
8
9package Prima::Sliders;
10
11use strict;
12use warnings;
13use Prima::Const;
14use Prima::Classes;
15use Prima::IntUtils;
16
17package Prima::AbstractSpinButton;
18use vars qw(@ISA);
19@ISA = qw(Prima::Widget Prima::MouseScroller);
20
21{
22my %RNT = (
23	%{Prima::Widget-> notification_types()},
24	Increment  => nt::Default,
25	TrackEnd   => nt::Default,
26);
27sub notification_types { return \%RNT; }
28}
29
30sub profile_default
31{
32	return {
33		%{$_[ 0]-> SUPER::profile_default},
34		ownerBackColor => 1,
35		color          => cl::Black,
36		selectable     => 0,
37		tabStop        => 0,
38		widgetClass    => wc::Button,
39	}
40}
41
42sub init
43{
44	my $self = shift;
45	my %profile = $self-> SUPER::init( @_);
46	$self-> { pressState} = 0;
47	return %profile;
48}
49
50sub on_mouseclick
51{
52	my $self = shift;
53	$self-> clear_event;
54	return unless pop;
55	$self-> clear_event unless $self-> notify( "MouseDown", @_);
56}
57
58sub state         {($#_)?$_[0]-> set_state ($_[1]):return $_[0]-> {pressState}}
59
60#sub on_trackend   {}
61#sub on_increment  {
62#  my ( $self, $increment) = @_;
63#}
64
65
66package Prima::SpinButton;
67use vars qw(@ISA);
68@ISA = qw(Prima::AbstractSpinButton);
69
70sub profile_default
71{
72	return {
73		%{$_[ 0]-> SUPER::profile_default},
74		width        => 17 * $::application-> uiScaling,
75		height       => 24 * $::application-> uiScaling,
76	}
77}
78
79sub on_mousedown
80{
81	my ( $self, $btn, $mod, $x, $y) = @_;
82	return if $self-> {mouseTransaction};
83	return if $btn != mb::Left;
84	my $h = $self-> height;
85	if ( $y >= $h * 0.6) {
86		$self-> { mouseTransaction} = 1;
87	} elsif ( $y <  $h * 0.4) {
88		$self-> { mouseTransaction} = 2;
89	} else {
90		$self-> { mouseTransaction} = 3;
91	}
92	delete $self->{prelight};
93	$self-> { lastMouseOver}  = 1;
94	$self-> { startMouseY  }  = $y;
95	$self-> state( $self-> { mouseTransaction});
96	$self-> capture(1);
97	$self-> clear_event;
98	$self-> {increment} = 0;
99	if ( $self-> { mouseTransaction} != 3) {
100		$self-> notify( 'Increment', $self-> { mouseTransaction} == 1 ? 1 : -1);
101		$self-> scroll_timer_start;
102		$self-> scroll_timer_semaphore(0);
103	} else {
104		$self-> {pointerSave} = $self-> pointer;
105		$self-> pointer( cr::SizeWE);
106	}
107}
108
109sub on_mouseup
110{
111	my ( $self, $btn, $mod, $x, $y) = @_;
112	return if $btn != mb::Left;
113	return unless $self-> {mouseTransaction};
114	my $mt  = $self-> {mouseTransaction};
115	my $inc = $mt != 2 ? 1 : -1;
116
117	$self-> {mouseTransaction} = undef;
118	$self-> {spaceTransaction} = undef;
119	$self-> {lastMouseOver}    = undef;
120	$self-> capture(0);
121	$self-> scroll_timer_stop;
122	$self-> state( 0);
123	$self-> pointer( $self-> {pointerSave}), $self-> {pointerSave} = undef
124		if $mt == 3;
125	$self-> {increment} = 0;
126	$self-> notify( 'TrackEnd');
127}
128
129sub on_mousemove
130{
131	my ( $self, $mod, $x, $y) = @_;
132	unless ( $self-> {mouseTransaction}) {
133		my $h = $self-> height;
134		my $prelight;
135		if ( $self-> enabled ) {
136			if ( $y >= $h * 0.6) {
137				$prelight = 'lower';
138				$self-> pointer(cr::Default);
139			} elsif ($y < $h * 0.4 ) {
140				$prelight = 'upper';
141				$self-> pointer(cr::Default);
142			} else {
143				$prelight = 'middle';
144				$self-> pointer(cr::SizeWE);
145			}
146			if (( $prelight // '') ne ($self->{prelight} // '')) {
147				$self->{prelight} = $prelight;
148				$self-> repaint;
149			}
150		}
151		return;
152	}
153	my @size = $self-> size;
154	my $mouseOver = $x > 0 && $y > 0 && $x < $size[0] && $y < $size[1];
155	$self-> state( $self-> {pressState} ? 0 : $self-> {mouseTransaction})
156		if $self-> { lastMouseOver} != $mouseOver && $self-> {pressState} != 3;
157	$self-> { lastMouseOver} = $mouseOver;
158
159	if ( $self-> {pressState} == 3) {
160		my $d  = ( $self-> {startMouseY} - $y) / 3; # 2 is mouse sensitivity
161		$self-> notify( 'Increment', int($self-> {increment}) - int($d))
162			if int( $self-> {increment}) != int( $d);
163		$self-> {increment}  = $d;
164	} elsif ( $self-> {pressState} > 0) {
165		$self-> scroll_timer_start unless $self-> scroll_timer_active;
166		return unless $self-> scroll_timer_semaphore;
167		$self-> scroll_timer_semaphore(0);
168		$self-> notify( 'Increment', $self-> {mouseTransaction} == 1 ? 1 : -1);
169	} else {
170		$self-> scroll_timer_stop;
171	}
172}
173
174sub on_mouseleave
175{
176	my $self = shift;
177	$self-> repaint if defined( delete $self->{prelight} );
178}
179
180sub on_paint
181{
182	my ( $self, $canvas) = @_;
183	my @clr;
184	my ($prelightPart, $prelightColor) = ('');
185	if ( $self-> enabled) {
186		@clr = ($self-> color, $self-> backColor);
187		if ($self->{prelight}) {
188			$prelightColor = $self-> prelight_color($clr[1], 1.5);
189			$prelightPart  = $self->{prelight};
190		}
191	} else {
192		@clr = ( $self-> disabledColor, $self-> disabledBackColor);
193	}
194	my @c3d  = ( $self-> light3DColor, $self-> dark3DColor);
195	my @size = $canvas-> size;
196	my $p = $self-> {pressState};
197
198	$canvas-> rect3d( 0, 0, $size[0] - 1, $size[1] * 0.4 - 1, 2,
199		(($p != 2) ? @c3d : reverse @c3d), ($prelightPart eq 'upper') ? $prelightColor : $clr[1]);
200	$canvas-> rect3d( 0, $size[1] * 0.4, $size[0] - 1, $size[1] * 0.6 - 1, 2,
201		(($p != 3) ? @c3d : reverse @c3d), ($prelightPart eq 'middle') ? $prelightColor : $clr[1]);
202	$canvas-> rect3d( 0, $size[1] * 0.6, $size[0] - 1, $size[1] - 1, 2,
203		(($p != 1) ? @c3d : reverse @c3d), ($prelightPart eq 'lower') ? $prelightColor : $clr[1]);
204
205	$canvas-> color( $clr[0]);
206	my $p1 = ( $p == 1) ? 1 : 0;
207	$canvas-> fillpoly( [
208		$size[0] * 0.3 + $p1, $size[1] * 0.73 - $p1,
209		$size[0] * 0.5 + $p1, $size[1] * 0.87 - $p1,
210		$size[0] * 0.7 + $p1, $size[1] * 0.73 - $p1
211	]);
212	$p1 = ( $p == 2) ? 1 : 0;
213	$canvas-> fillpoly( [
214		$size[0] * 0.3 + $p1, $size[1] * 0.27 - $p1,
215		$size[0] * 0.5 + $p1, $size[1] * 0.13 - $p1,
216		$size[0] * 0.7 + $p1, $size[1] * 0.27 - $p1
217	]);
218}
219
220sub set_state
221{
222	my ( $self, $s) = @_;
223	$s = 0 if $s > 3;
224	return if $s == $self-> {pressState};
225	$self-> {pressState} = $s;
226	$self-> repaint;
227}
228
229package Prima::AltSpinButton;
230use vars qw(@ISA);
231@ISA = qw(Prima::AbstractSpinButton);
232
233sub profile_default
234{
235	return {
236		%{$_[ 0]-> SUPER::profile_default},
237		width        => 18 * $::application-> uiScaling,
238		height       => 18 * $::application-> uiScaling,
239	}
240}
241
242sub profile_check_in
243{
244	my ( $self, $p, $default) = @_;
245	$p-> {height} = $p-> {width}  if !exists( $p-> {height}) && exists( $p-> {width});
246	$p-> {width}  = $p-> {height} if exists( $p-> {height}) && !exists( $p-> {width});
247	$self-> SUPER::profile_check_in( $p, $default);
248}
249
250sub on_mousedown
251{
252	my ( $self, $btn, $mod, $x, $y) = @_;
253	return if $self-> {mouseTransaction};
254	return if $btn != mb::Left;
255	$self-> { mouseTransaction} =
256		(( $x * $self-> height / ( $self-> width || 1)) > $y) ?
257			2 : 1;
258	$self-> { lastMouseOver}  = 1;
259	delete $self->{prelight};
260	$self-> state( $self-> { mouseTransaction});
261	$self-> capture(1);
262	$self-> clear_event;
263	$self-> notify( 'Increment', $self-> { mouseTransaction} == 1 ? 1 : -1);
264	$self-> scroll_timer_start;
265	$self-> scroll_timer_semaphore(0);
266}
267
268sub on_mouseup
269{
270	my ( $self, $btn, $mod, $x, $y) = @_;
271	return if $btn != mb::Left;
272	return unless $self-> {mouseTransaction};
273	$self-> {mouseTransaction} = undef;
274	$self-> {spaceTransaction} = undef;
275	$self-> {lastMouseOver}    = undef;
276	$self-> capture(0);
277	$self-> scroll_timer_stop;
278	$self-> state( 0);
279	$self-> notify( 'TrackEnd');
280}
281
282sub on_mousemove
283{
284	my ( $self, $mod, $x, $y) = @_;
285	unless ($self-> {mouseTransaction}) {
286		if ( $self-> enabled ) {
287			my $prelight = (( $x * $self-> height / ( $self-> width || 1)) > $y) ?  2 : 1;
288			if (( $self->{prelight} // 0 ) != $prelight) {
289				$self->{prelight} = $prelight;
290				$self->repaint;
291			}
292		}
293		return;
294	}
295	my @size = $self-> size;
296	my $mouseOver = $x > 0 && $y > 0 && $x < $size[0] && $y < $size[1];
297	$self-> state( $self-> {pressState} ? 0 : $self-> {mouseTransaction})
298		if $self-> { lastMouseOver} != $mouseOver;
299	$self-> { lastMouseOver} = $mouseOver;
300	if ( $self-> {pressState}) {
301		$self-> scroll_timer_start unless $self-> scroll_timer_active;
302		return unless $self-> scroll_timer_semaphore;
303		$self-> scroll_timer_semaphore(0);
304		$self-> notify( 'Increment', $self-> {mouseTransaction} == 1 ? 1 : -1);
305	} else {
306		$self-> scroll_timer_stop;
307	}
308}
309
310sub on_mouseleave
311{
312	my $self = shift;
313	$self-> repaint if defined( delete $self->{prelight} );
314}
315
316sub fix_triangle
317{
318	my @spot = map { int($_ + .5) } @_;
319	my $dx = $spot[4] - $spot[0];
320	my $dy = $spot[3] - $spot[1];
321	if ($dx % 2) {
322		$spot[2] = $spot[0] + ($dx - 1) / 2;
323		$spot[4]--;
324		$dx--;
325	}
326	if ( $dx == 2 ) {
327		$spot[4]++;
328		$spot[0]--;
329		$dx += 2;
330	}
331	$spot[3] -= ($dy > 0) ? 1 : -1 if abs($dy) > $dx / 2;
332	return \@spot;
333}
334
335
336sub on_paint
337{
338	my ( $self, $canvas) = @_;
339	my @clr  = ( $self-> color, $self-> backColor);
340	@clr = ( $self-> hiliteColor, $self-> hiliteBackColor)     if $self-> { default};
341	@clr = ( $self-> disabledColor, $self-> disabledBackColor) if !$self-> enabled;
342	my ($prelightPart, $prelightColor) = (0);
343	if ($self->{prelight}) {
344		$prelightColor = $self-> prelight_color($clr[1], 1.5);
345		$prelightPart  = $self->{prelight};
346	}
347	my @c3d  = ( $self-> light3DColor, $self-> dark3DColor);
348	my @size = $canvas-> size;
349	$canvas-> color( $clr[ 1]);
350	$canvas-> bar( 0, 0, $size[0]-1, $size[1]-1);
351	my $p = $self-> {pressState};
352
353	if ( $prelightPart == 1 && $size[1] > 4 && $size[0] > 4 ) {
354		$canvas->color( $prelightColor );
355		$canvas->fillpoly([
356			2, 2,
357			2, $size[1] - 3,
358			$size[0] - 3, $size[1] - 3,
359		]);
360	}
361	$canvas-> color( $p == 1 ? 0x404040 : $c3d[1]);
362	$canvas-> polyline( [0, 0, 0, $size[1] - 1, $size[0] - 2, $size[1] - 1]);
363	$canvas-> color( $p == 1 ? $c3d[1]  : $c3d[0]);
364	$canvas-> polyline( [1, 1, 1, $size[1] - 2, $size[0] - 3, $size[1] - 2]);
365
366	if ( $prelightPart == 2 && $size[1] > 4 && $size[0] > 4 ) {
367		$canvas->color( $prelightColor );
368		$canvas->fillpoly([
369			2, 2,
370			$size[0] - 3, $size[1] - 3,
371			$size[0] - 3, 2,
372		]);
373	}
374	$canvas-> color( $p == 2 ? $c3d[0] : $c3d[1]);
375	$canvas-> polyline([2, 1, $size[0] - 2, 1, $size[0] - 2, $size[1] - 2]);
376	$canvas-> color( $p == 2 ? $c3d[1] : 0x404040);
377	$canvas-> polyline([1, 0, $size[0] - 1, 0, $size[0] - 1, $size[1] - 1]);
378
379	$canvas-> color( $p == 1 ? $c3d[ 0] : $c3d[ 1]);
380	$canvas-> line( -1, 0, $size[0] - 2, $size[1] - 1);
381	$canvas-> color( 0x404040);
382	$canvas-> line( 0, 0, $size[0] - 1, $size[1] - 1);
383	$canvas-> color( $p == 2 ? $c3d[ 1] : $c3d[ 0]);
384	$canvas-> line( 1, 0, $size[0], $size[1] - 1);
385
386	$canvas-> color( $clr[0]);
387	my $p1 = ( $p == 1) ? 1 : 0;
388	$canvas-> fillpoly( fix_triangle(
389		$size[0] * 0.2 + $p1, $size[1] * 0.65 - $p1,
390		$size[0] * 0.3 + $p1, $size[1] * 0.77 - $p1,
391		$size[0] * 0.4 + $p1, $size[1] * 0.65 - $p1
392	));
393	$p1 = ( $p == 2) ? 1 : 0;
394	$canvas-> fillpoly( fix_triangle(
395		$size[0] * 0.59 + $p1, $size[1] * 0.35 - $p1,
396		$size[0] * 0.69 + $p1, $size[1] * 0.23 - $p1,
397		$size[0] * 0.79 + $p1, $size[1] * 0.35 - $p1
398	));
399}
400
401sub set_state
402{
403	my ( $self, $s) = @_;
404	$s = 0 if $s > 2;
405	return if $s == $self-> {pressState};
406	$self-> {pressState} = $s;
407	$self-> repaint;
408}
409
410package Prima::SpinEdit;
411use vars qw(@ISA %editProps %spinDynas);
412use Prima::InputLine;
413@ISA = qw(Prima::Widget);
414
415
416%editProps = (
417	alignment      => 1, autoScroll  => 1, text        => 1,
418	charOffset     => 1, maxLen      => 1, insertMode  => 1, firstChar   => 1,
419	selection      => 1, selStart    => 1, selEnd      => 1, writeOnly   => 1,
420	copy           => 1, cut         => 1, 'delete'    => 1, paste       => 1,
421	wordDelimiters => 1, readOnly    => 1, passwordChar=> 1, focus       => 1,
422	select_all     => 1,
423);
424
425%spinDynas = ( onIncrement => 1, onTrackEnd => 1,);
426
427for ( keys %editProps) {
428	eval <<GENPROC;
429   sub $_ { return shift-> {edit}-> $_(\@_); }
430   sub Prima::SpinEdit::DummyEdit::$_ { }
431GENPROC
432}
433
434sub profile_default
435{
436	my $font = $_[ 0]-> get_default_font;
437	my $fh   = $font-> {height} + 2;
438	return {
439		%{Prima::InputLine-> profile_default},
440		%{$_[ 0]-> SUPER::profile_default},
441		autoEnableChildren => 1,
442		ownerBackColor => 1,
443		selectable     => 0,
444		scaleChildren  => 0,
445		min            => 0,
446		max            => 100,
447		step           => 1,
448		pageStep       => 10,
449		value          => 0,
450		circulate      => 0,
451		height         => $fh < 20 ? 20 : $fh,
452		editClass      => 'Prima::InputLine',
453		spinClass      => 'Prima::AltSpinButton',
454		editProfile    => {},
455		spinProfile    => {},
456		editDelegations=> [qw(KeyDown Change MouseWheel Enter Leave DragEnd)],
457		spinDelegations=> [qw(Increment)],
458	}
459}
460
461sub init
462{
463	my $self = shift;
464	my %profile = @_;
465	my $visible = $profile{visible};
466	$profile{visible} = 0;
467	for (qw( min max step circulate pageStep)) {$self-> {$_} = 1;};
468	$self-> {edit} = bless [], q\Prima::SpinEdit::DummyEdit\;
469	%profile = $self-> SUPER::init(%profile);
470	my ( $w, $h) = ( $self-> size);
471	$self-> {spin} = $self-> insert( $profile{spinClass} =>
472		ownerBackColor => 1,
473		name           => 'Spin',
474		bottom         => 1,
475		right          => $w - 1,
476		height         => $h - 1 * 2,
477		growMode       => gm::Right,
478		delegations    => $profile{spinDelegations},
479		(map { $_ => $profile{$_}} grep { exists $profile{$_} ? 1 : 0} keys %spinDynas),
480		%{$profile{spinProfile}},
481	);
482	$self-> {edit} = $self-> insert( $profile{editClass} =>
483		name         => 'InputLine',
484		origin      => [ 1, 1],
485		size        => [ $w - $self-> {spin}-> width - 1 * 2, $h - 1 * 2],
486		growMode    => gm::GrowHiX|gm::GrowHiY,
487		selectable  => 1,
488		tabStop     => 1,
489		borderWidth => 0,
490		current     => 1,
491		delegations => $profile{editDelegations},
492		(map { $_ => $profile{$_}} keys %editProps),
493		%{$profile{editProfile}},
494		text        => $profile{value},
495	);
496	for (qw( min max step value circulate pageStep)) {$self-> $_($profile{$_});};
497	$self-> visible( $visible);
498	return %profile;
499}
500
501sub on_paint
502{
503	my ( $self, $canvas) = @_;
504	my @s = $canvas-> size;
505	$canvas-> rect3d( 0, 0, $s[0]-1, $s[1]-1, 1, $self-> dark3DColor, $self-> light3DColor);
506}
507
508sub InputLine_MouseWheel
509{
510	my ( $self, $edit, $mod, $x, $y, $z) = @_;
511	$z = (abs($z) > 120) ? int($z/120) : (($z > 0) ? 1 : -1);
512	$z *= $self-> {pageStep} if $mod & km::Ctrl;
513	my $value = $self-> value;
514	$self-> value( $value + $z * $self-> {step});
515	$self-> value( $z > 0 ? $self-> min : $self-> max)
516		if $self-> {circulate} && ( $self-> value == $value);
517	$edit-> clear_event;
518}
519
520sub InputLine_DragEnd
521{
522	my ( $self, $edit, $clipboard, $action, $mod, $x, $y, $ref ) = @_;
523	return unless $clipboard;
524	my $text = $clipboard->text;
525	return unless defined $text;
526	$text =~ s/^\s+//;
527	$text =~ s/\s+$//;
528	return if $text =~ /^-?\d+(\.\d+)?$/ and $text >= $self->min and $text <= $self->max;
529	$edit->clear_event;
530	$edit->on_dragend(undef, $action, $mod, $x, $y, $ref);
531	$ref->{allow} = 0;
532}
533
534sub Spin_Increment
535{
536	my ( $self, $spin, $increment) = @_;
537	my $value = $self-> value;
538	$self-> value( $value + $increment * $self-> {step});
539	$self-> value( $increment > 0 ? $self-> min : $self-> max)
540		if $self-> {circulate} && ( $self-> value == $value);
541}
542
543sub InputLine_KeyDown
544{
545	my ( $self, $edit, $code, $key, $mod) = @_;
546	$edit-> clear_event, return if
547		$key == kb::NoKey && !($mod & (km::Alt | km::Ctrl)) &&
548		chr($code) !~ /^[.\d+-]$/;
549	if ( $key == kb::Up || $key == kb::Down || $key == kb::PgDn || $key == kb::PgUp) {
550		my ($s,$pgs) = ( $self-> step, $self-> pageStep);
551		my $z = ( $key == kb::Up) ? $s : (( $key == kb::Down) ? -$s :
552			(( $key == kb::PgUp) ? $pgs : -$pgs));
553		if (( $mod & km::Ctrl) && ( $key == kb::PgDn || $key == kb::PgUp)) {
554			$self-> value( $key == kb::PgDn ? $self-> min : $self-> max);
555		} else {
556			my $value = $self-> value;
557			$self-> value( $value + $z);
558			$self-> value( $z > 0 ? $self-> min : $self-> max)
559				if $self-> {circulate} && ( $self-> value == $value);
560		}
561		$edit-> clear_event;
562		return;
563	}
564	if ($key == kb::Enter) {
565		my $value = $edit-> text;
566		$self-> value( $value);
567		$edit-> clear_event if $value ne $self-> value;
568		return;
569	}
570}
571
572sub InputLine_Change
573{
574	my ( $self, $edit) = @_;
575	$self-> notify(q(Change));
576}
577
578sub InputLine_Enter
579{
580	my ( $self, $edit) = @_;
581	$self-> notify(q(Enter));
582}
583
584sub InputLine_Leave
585{
586	my ( $self, $edit) = @_;
587	$self-> notify(q(Leave));
588}
589
590sub set_bounds
591{
592	my ( $self, $min, $max) = @_;
593	$max = $min if $max < $min;
594	( $self-> { min}, $self-> { max}) = ( $min, $max);
595	my $oldValue = $self-> value;
596	$self-> value( $max) if $max < $self-> value;
597	$self-> value( $min) if $min > $self-> value;
598}
599
600sub set_step
601{
602	my ( $self, $step) = @_;
603	$step  = 0 if $step < 0;
604	$self-> {step} = $step;
605}
606
607sub circulate
608{
609	return $_[0]-> {circulate} unless $#_;
610	$_[0]-> {circulate} = $_[1];
611}
612
613sub pageStep
614{
615	return $_[0]-> {pageStep} unless $#_;
616	$_[0]-> {pageStep} = $_[1];
617}
618
619
620sub min          {($#_)?$_[0]-> set_bounds($_[1], $_[0]-> {'max'})      : return $_[0]-> {min};}
621sub max          {($#_)?$_[0]-> set_bounds($_[0]-> {'min'}, $_[1])      : return $_[0]-> {max};}
622sub step         {($#_)?$_[0]-> set_step         ($_[1]):return $_[0]-> {step}}
623sub value
624{
625	if ($#_) {
626		my ( $self, $value) = @_;
627		if ( $value =~ m/^\s*([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\s*$/) {
628			$value = $self-> {min} if $value < $self-> {min};
629			$value = $self-> {max} if $value > $self-> {max};
630		} else {
631			$value = $self-> {min};
632		}
633		return if $value eq $self-> {edit}-> text;
634		$self-> {edit}-> text( $value);
635	} else {
636		my $self = $_[0];
637		my $value = $self-> {edit}-> text;
638		if ( $value =~ m/^\s*([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?\s*$/) {
639			$value = $self-> {min} if $value < $self-> {min};
640			$value = $self-> {max} if $value > $self-> {max};
641		} else {
642			$value = $self-> {min};
643		}
644		return $value;
645	}
646}
647
648
649# gauge reliefs
650package
651    gr;
652use constant Sink         =>  -1;
653use constant Border       =>  0;
654use constant Raise        =>  1;
655
656
657package Prima::Gauge;
658use vars qw(@ISA);
659@ISA = qw(Prima::Widget);
660
661{
662my %RNT = (
663	%{Prima::Widget-> notification_types()},
664	Stringify => nt::Action,
665);
666
667sub notification_types { return \%RNT; }
668}
669
670sub profile_default
671{
672	return {
673		%{$_[ 0]-> SUPER::profile_default},
674		indent         => 1,
675		relief         => gr::Sink,
676		ownerBackColor => 1,
677		hiliteBackColor=> cl::Blue,
678		hiliteColor    => cl::White,
679		min            => 0,
680		max            => 100,
681		value          => 0,
682		threshold      => 0,
683		vertical       => 0,
684		# additional properties for indeterminate mode
685		indeterminate	=> '1',
686		sliderLength	=> 30,
687	}
688}
689
690sub init
691{
692	my $self = shift;
693	my %profile = $self-> SUPER::init(@_);
694	for (qw( relief value indent min max threshold vertical))
695	{$self-> {$_} = 0}
696	$self-> {string} = '';
697	for (qw( vertical threshold min max relief indent value))
698	{$self-> $_($profile{$_}); }
699
700	# additional properties for indeterminate mode
701	$self->{direction} = 1;
702	for (qw( indeterminate sliderLength))
703	{$self-> $_($profile{$_}); }
704	# If indeterminate is true, the start value must be > sliderLength
705	$self->value($self->{sliderLength}) if ($self->indeterminate);
706
707	return %profile;
708}
709
710sub setup
711{
712	$_[0]-> SUPER::setup;
713	$_[0]-> value($_[0]-> {value});
714}
715
716sub on_paint
717{
718	my ($self,$canvas) = @_;
719	my ($x, $y) = $canvas-> size;
720
721	$canvas->clear();
722	my $i = $self-> indent;
723	my ($clComplete,$clBack,$clFore,$clHilite) = ($self-> hiliteBackColor, $self-> backColor, $self-> color, $self-> hiliteColor);
724	my $v = $self-> {vertical};
725	my $complete = $v ? $y : $x;
726	my $range = ($self-> {max} - $self-> {min}) || 1;
727	$complete = int(($complete - $i*2) * $self-> {value} / $range + 0.5);
728	my ( $l3, $d3) = ( $self-> light3DColor, $self-> dark3DColor);
729	$canvas-> color( $clComplete);
730
731	# INDETERMINATE STYLE HACK
732	my $left_bound =
733		$self->indeterminate ?
734			$complete - ($self->{sliderLength} * ($v ? $y : $x) / $range + 0.5) :
735			$i;
736	$canvas-> bar ( $v ?
737		($i, $left_bound, $x-$i-1, $i+$complete) :
738		($left_bound, $i, $i + $complete, $y-$i-1));
739
740	$canvas-> color( $clBack);
741	$canvas-> bar ( $v ? ($i, $i+$complete+1, $x-$i-1, $y-$i-1) : ( $i+$complete+1, $i, $x-$i-1, $y-$i-1));
742
743	# draw the border
744	my $relief = $self-> relief;
745	$canvas-> color(( $relief == gr::Sink) ? $d3 : (( $relief == gr::Border) ? cl::Black : $l3));
746	for ( my $j = 0; $j < $i; $j++)
747	{
748		$canvas-> line( $j, $j, $j, $y - $j - 1);
749		$canvas-> line( $j, $y - $j - 1, $x - $j - 1, $y - $j - 1);
750	}
751	$canvas-> color(( $relief == gr::Sink) ? $l3 : (( $relief == gr::Border) ? cl::Black : $d3));
752	for ( my $j = 0; $j < $i; $j++)
753	{
754		$canvas-> line( $j + 1, $j, $x - $j - 1, $j);
755		$canvas-> line( $x - $j - 1, $j, $x - $j - 1, $y - $j - 1);
756	}
757
758
759	# draw the text, if neccessary
760	my $s = $self-> {string};
761	if ( $s ne '')
762	{
763		my ($fw, $fh) = ( $canvas-> get_text_width( $s), $canvas-> font-> height);
764		my $xBeg = int(( $x - $fw) / 2 + 0.5);
765		my $xEnd = $xBeg + $fw;
766		my $yBeg = int(( $y - $fh) / 2 + 0.5);
767		my $yEnd = $yBeg + $fh;
768		my ( $zBeg, $zEnd) = $v ? ( $yBeg, $yEnd) : ( $xBeg, $xEnd);
769		if ( $zBeg > $i + $complete) {
770			$canvas-> color( $clFore);
771			$canvas-> text_shape_out( $s, $xBeg, $yBeg);
772		} elsif ( $zEnd < $i + $complete + 1) {
773			$canvas-> color( $clHilite);
774			$canvas-> text_shape_out( $s, $xBeg, $yBeg);
775		} else {
776			$canvas-> clipRect( $v ?
777				( 0, 0, $x, $i + $complete) :
778				( 0, 0, $i + $complete, $y)
779			);
780			$canvas-> color( $clHilite);
781			$canvas-> text_shape_out( $s, $xBeg, $yBeg);
782			$canvas-> clipRect( $v ?
783				( 0, $i + $complete + 1, $x, $y) :
784				( $i + $complete + 1, 0, $x, $y)
785			);
786			$canvas-> color( $clFore);
787			$canvas-> text_shape_out( $s, $xBeg, $yBeg);
788		}
789	}
790}
791
792sub set_bounds
793{
794	my ( $self, $min, $max) = @_;
795	$max = $min if $max < $min;
796	( $self-> { min}, $self-> { max}) = ( $min, $max);
797	my $oldValue = $self-> {value};
798	$self-> value( $max) if $self-> {value} > $max;
799	$self-> value( $min) if $self-> {value} < $min;
800}
801
802sub value {
803	return $_[0]-> {value} unless $#_;
804	my $v = $_[1] < $_[0]-> {min} ? $_[0]-> {min} : ($_[1] > $_[0]-> {max} ? $_[0]-> {max} : $_[1]);
805	$v -= $_[0]-> {min};
806	if ($_[0]->indeterminate) {
807		$_[0]-> {value} = $v;
808		$_[0]-> repaint;
809	}
810	else {
811		my $old = $_[0]-> {value};
812		if (abs($old - $v) >= $_[0]-> {threshold}) {
813			my ($x, $y) = $_[0]-> size;
814			my $i = $_[0]-> {indent};
815			my $range = ( $_[0]-> {max} - $_[0]-> {min}) || 1;
816			my $x1 = $i + ($x - $i*2) * $old / $range;
817			my $x2 = $i + ($x - $i*2) * $v   / $range;
818			($x1, $x2) = ( $x2, $x1) if $x1 > $x2;
819			my $s = $_[0]-> {string};
820			$_[0]-> {value} = $v;
821			$_[0]-> notify(q(Stringify), $v, \$_[0]-> {string});
822			( $_[0]-> {string} eq $s) ?
823				$_[0]-> invalidate_rect( $x1, 0, $x2+1, $y) :
824				$_[0]-> repaint;
825		}
826	}
827}
828
829sub on_stringify
830{
831	my ( $self, $value, $sref) = @_;
832	$$sref = sprintf( "%2d%%", $value * 100.0 / (($_[0]-> {max} - $_[0]-> {min})||1));
833	$self-> clear_event;
834}
835
836sub indent    {($#_)?($_[0]-> {indent} = $_[1],$_[0]-> repaint)  :return $_[0]-> {indent};}
837sub relief    {($#_)?($_[0]-> {relief} = $_[1],$_[0]-> repaint)  :return $_[0]-> {relief};}
838sub vertical  {($#_)?($_[0]-> {vertical} = $_[1],$_[0]-> repaint):return $_[0]-> {vertical};}
839sub min       {($#_)?$_[0]-> set_bounds($_[1], $_[0]-> {'max'})  : return $_[0]-> {min};}
840sub max       {($#_)?$_[0]-> set_bounds($_[0]-> {'min'}, $_[1])  : return $_[0]-> {max};}
841sub threshold {($#_)?($_[0]-> {threshold} = $_[1]):return $_[0]-> {threshold};}
842
843sub indeterminate    {
844	my ($self, $indeterminate) = @_;
845	return $self-> {indeterminate} unless $#_;
846
847	# Create the timer for the motion in indeterminate mode
848	# if it is not still created
849	unless ( $self->{timer} ) {
850		$self->{timer} = $self->insert( Timer =>
851		name	    => 'Timer',
852		timeout     => 25,
853		delegations => ['Tick'],
854		);
855	}
856
857	# When the style property is changed, reset the timer frequency
858	# and the start_angle and for style circle the end_angle, too
859	if ( $indeterminate) {
860		$self->{timer}->start;
861	}
862
863	else {
864		$self->{timer}->stop;
865	}
866	$self->{indeterminate} = $indeterminate;
867
868}
869
870sub Timer_Tick
871{
872	my $self = shift;
873	my $newval = $self->value;
874	my $sliderLength = $self->sliderLength;
875	$newval = $newval+1 if ($self->direction == 1);
876	$newval = $newval-1 if ($self->direction == 0);
877	$self->value($newval);
878	$self->direction(0) if ($newval == 100);
879	$self->direction(1) if ($newval == $sliderLength);
880	$self->repaint;
881}
882
883sub direction    {($#_)?($_[0]-> {direction} = $_[1])  :return $_[0]-> {direction};}
884sub sliderLength    {($#_)?($_[0]-> {sliderLength} = $_[1])  :return $_[0]-> {sliderLength};}
885
886# slider standard schemes
887package
888    ss;
889use constant Gauge        => 0;
890use constant Axis         => 1;
891use constant Thermometer  => 2;
892use constant StdMinMax    => 3;
893
894package Prima::AbstractSlider;
895use vars qw(@ISA);
896@ISA = qw(Prima::Widget);
897
898{
899my %RNT = (
900	%{Prima::Widget-> notification_types()},
901	Track   => nt::Default,
902);
903sub notification_types { return \%RNT; }
904}
905
906sub profile_default
907{
908	return {
909		%{$_[ 0]-> SUPER::profile_default},
910		autoHeight     => 0,
911		autoWidth      => 0,
912		autoTrack      => 1,
913		increment      => 10,
914		min            => 0,
915		max            => 100,
916		ownerBackColor => 1,
917		readOnly       => 0,
918		scheme         => undef,
919		selectable     => 1,
920		snap           => 0,
921		step           => 1,
922		ticks          => undef,
923		value          => 0,
924		widgetClass    => wc::Slider,
925	}
926}
927
928
929sub init
930{
931	my $self = shift;
932	for ( qw( min max readOnly snap value autoTrack autoWidth autoHeight))
933		{$self-> {$_}=0}
934	for ( qw( tickVal tickLen tickTxt )) { $self-> {$_} = [] };
935	my %profile = $self-> SUPER::init( @_);
936	for ( qw( step min max increment readOnly ticks snap value autoTrack autoHeight autoWidth))
937	{$self-> $_($profile{$_});}
938	$self-> scheme( $profile{scheme}) if defined $profile{scheme};
939	return %profile;
940}
941
942sub autoTrack  { $#_ ? $_[0]-> {autoTrack}  = $_[1] : $_[0]-> {autoTrack}  }
943sub autoWidth  { $#_ ? $_[0]-> {autoWidth}  = $_[1] : $_[0]-> {autoWidth}  }
944sub autoHeight { $#_ ? $_[0]-> {autoHeight} = $_[1] : $_[0]-> {autoHeight} }
945
946sub on_mouseclick
947{
948	my $self = shift;
949	$self-> clear_event;
950	return unless pop;
951	$self-> clear_event unless $self-> notify( "MouseDown", @_);
952}
953
954sub on_mousewheel
955{
956	my ( $self, $mod, $x, $y, $z) = @_;
957	$self-> set_next_value( $self-> {step} * $z / 120);
958	$self-> clear_event;
959}
960
961sub set_next_value
962{
963	my ( $self, $dir) = @_;
964	$dir *= -1 if $self-> {min} > $self-> {max};
965	if ( $self-> snap) {
966		my $v = $self-> value;
967		my $w = $v;
968		return if ( $v + $dir > $self-> {min} and $v + $dir > $self-> {max}) or
969			( $v + $dir < $self-> {min} and $v + $dir < $self-> {max});
970		$self-> value( $v += $dir) while $self-> {value} == $w;
971	} else {
972		$self-> value( $self-> value + $dir);
973	}
974}
975
976sub update_geom_sizes {}
977
978sub set_read_only
979{
980	$_[0]-> {readOnly} = $_[1];
981	$_[0]-> repaint;
982	$_[0]-> notify(q(MouseUp),0,0,0) if defined $_[0]-> {mouseTransaction};
983}
984
985
986sub set_snap
987{
988	$_[0]-> {snap} = $_[1];
989	$_[0]-> value( $_[0]-> value) if $_[1];
990}
991
992sub set_step
993{
994	my $i = $_[1];
995	$i = 1 if $i == 0;
996	$_[0]-> {step} = $i;
997}
998
999sub get_ticks
1000{
1001	my $self  =  $_[0];
1002	my $i;
1003	my ( $tv, $tl, $tt) = ($self-> {tickVal}, $self-> {tickLen}, $self-> {tickTxt});
1004	my @t;
1005	for ( $i = 0; $i < scalar @{$tv}; $i++) {
1006		push ( @t, {
1007			value => $$tv[$i],
1008			height => $$tl[$i],
1009			text => $$tt[$i]
1010		});
1011	}
1012	return @t;
1013}
1014
1015sub set_ticks
1016{
1017	my $self  = shift;
1018	return unless defined $_[0];
1019	my @ticks = (@_ == 1 and ref($_[0]) eq q(ARRAY)) ? @{$_[0]} : @_;
1020	my @val;
1021	my @len;
1022	my @txt;
1023	for ( @ticks) {
1024		next unless exists $$_{value};
1025		push( @val, $$_{value});
1026		push( @len, exists($$_{height})    ? $$_{height}    : 0);
1027		push( @txt, exists($$_{text})      ? $$_{text}      : undef);
1028	}
1029	$self-> {tickVal} = \@val;
1030	$self-> {tickLen} = \@len;
1031	$self-> {tickTxt} = \@txt;
1032	$self-> {scheme}  = undef;
1033	$self-> update_geom_sizes;
1034	$self-> value( $self-> value);
1035	$self-> repaint;
1036}
1037
1038sub set_bound
1039{
1040	my ( $self, $val, $bound) = @_;
1041	$self-> {$bound} = $val;
1042	$self-> scheme($self-> {scheme}) if defined $self-> {scheme};
1043	$self-> repaint;
1044}
1045
1046sub set_scheme
1047{
1048	my ( $self, $s) = @_;
1049	unless ( defined $s) {
1050		$self-> {scheme} = undef;
1051		return;
1052	}
1053	my ( $max, $min) = ( $self-> {max}, $self-> {min});
1054	$self-> ticks([]), return if $max == $min;
1055
1056	my @t;
1057	my $i;
1058	my $inc = $self-> {increment};
1059	if ( $s == ss::Gauge) {
1060		for ( $i = $min; $i <= $max; $i += $inc) {
1061			push ( @t, { value => $i, height => 4, text => $i });
1062		}
1063	} elsif ( $s == ss::Axis) {
1064		for ( $i = $min; $i <= $max; $i += $inc) {
1065			push ( @t, { value => $i, height => 6,   text => $i });
1066			if ( $i < $max) {
1067				for ( 1..4) {
1068					my $v = $i + $inc / 5 * $_;
1069					last if $v > $max;
1070					push ( @t, { value => $v, height => 3 });
1071				}
1072			}
1073		}
1074		push ( @t, { value => $max, height => 6,   text => $max }) if $i != $max;
1075	} elsif ( $s == ss::StdMinMax) {
1076		push ( @t, { value => $min, height => 6,   text => "Min" });
1077		push ( @t, { value => $max, height => 6,   text => "Max" });
1078	} elsif ( $s == ss::Thermometer ) {
1079		for ( $i = $min; $i <= $max; $i += $inc) {
1080			push ( @t, {
1081				value => $i,
1082				height => 6,
1083				text => $i
1084			});
1085			if ( $i < $max) {
1086				my $j;
1087				for ( $j = 1; $j < 10; $j++) {
1088					my $v = $i + $inc / 10 * $j;
1089					last if $v > $max;
1090					push ( @t, {
1091						value => $v,
1092						height => $j == 5 ? 5 : 3
1093					});
1094				}
1095			}
1096		}
1097		push ( @t, { value => $max, height => 6,   text => $max }) if $i != $max;
1098	}
1099	$self-> ticks( @t);
1100	$self-> {scheme} = $s;
1101}
1102
1103sub increment
1104{
1105	return $_[0]-> {increment} unless $#_;
1106	my ( $self, $increment) = @_;
1107	$self-> {increment} = $increment;
1108	if ( defined $self-> {scheme}) {
1109		$self-> scheme( $self-> {scheme});
1110		$self-> repaint;
1111	}
1112}
1113sub readOnly    {($#_)?$_[0]-> set_read_only   ($_[1]):return $_[0]-> {readOnly};}
1114sub ticks       {($#_)?shift-> set_ticks          (@_):return $_[0]-> get_ticks;}
1115sub snap        {($#_)?$_[0]-> set_snap        ($_[1]):return $_[0]-> {snap};}
1116sub step        {($#_)?$_[0]-> set_step        ($_[1]):return $_[0]-> {step};}
1117sub scheme      {($#_)?shift-> set_scheme         (@_):return $_[0]-> {scheme}}
1118sub value       {($#_)?$_[0]-> {value} =       $_[1]  :return $_[0]-> {value};}
1119sub min         {($#_)?$_[0]-> set_bound($_[1],q(min)):return $_[0]-> {min};}
1120sub max         {($#_)?$_[0]-> set_bound($_[1],q(max)):return $_[0]-> {max};}
1121
1122
1123# linear slider tick alignment
1124package
1125    tka;
1126use constant Normal      => 0;
1127use constant Alternative => 1;
1128use constant Dual        => 2;
1129
1130package Prima::Slider;
1131use vars qw(@ISA);
1132@ISA = qw(Prima::AbstractSlider);
1133
1134sub profile_default
1135{
1136	return {
1137		%{$_[ 0]-> SUPER::profile_default},
1138		borderWidth    => 0,
1139		ribbonStrip    => 0,
1140		shaftBreadth   => 6,
1141		knobBreadth    => 12,
1142		tickAlign      => tka::Normal,
1143		vertical       => 0,
1144		scheme         => ss::Gauge,
1145	}
1146}
1147
1148sub profile_check_in
1149{
1150	my ( $self, $p, $default) = @_;
1151	$p-> { autoWidth} = 1
1152		if !exists $p->{autoWidth} and (($p->{vertical} // $default->{vertical}) == 1);
1153	$p-> { autoHeight} = 1
1154		if !exists $p->{autoHeight} and (($p->{vertical} // $default->{vertical}) == 0);
1155	$p-> { autoHeight} = 0
1156		if exists $p-> {height} || exists $p-> {size} || exists $p-> {rect} ||
1157			( exists $p-> {top} && exists $p-> {bottom});
1158	$p-> { autoWidth} = 0
1159		if exists $p-> {width} || exists $p-> {size} || exists $p-> {rect} ||
1160			( exists $p-> {left} && exists $p-> {right});
1161	my $sc = $::application->uiScaling;
1162	$p->{$_} = ( exists($p->{$_}) ? $p->{$_} : $default->{$_} ) * $sc for qw(shaftBreadth knobBreadth );
1163	$self-> SUPER::profile_check_in( $p, $default);
1164}
1165
1166sub init
1167{
1168	my $self = shift;
1169	$self-> {$_} = 0
1170		for qw( vertical tickAlign ribbonStrip shaftBreadth borderWidth knobBreadth);
1171	my %profile = $self-> SUPER::init( @_);
1172	$self-> $_($profile{$_})
1173		for qw( vertical tickAlign ribbonStrip shaftBreadth borderWidth knobBreadth);
1174	return %profile;
1175}
1176
1177sub on_paint
1178{
1179	my ( $self, $canvas) = @_;
1180	my @clr;
1181	my $prelight;
1182
1183	my $enabled = $self->enabled;
1184
1185	if ( $enabled ) {
1186		@clr  = ( $self-> color, $self-> backColor);
1187		$prelight = $self-> prelight_color($clr[1], 1.5) if $self->{prelight};
1188	} else {
1189		@clr = ( $self-> disabledColor, $self-> disabledBackColor)
1190	}
1191	my @c3d  = ( $self-> dark3DColor, $self-> light3DColor);
1192	my @cht  = ( $self-> hiliteColor, $self-> hiliteBackColor);
1193	my @glyph_deltas = ([$clr[0], 0, 0]);
1194	unshift @glyph_deltas, [cl::White, 1, -1] unless $enabled;
1195
1196	my @size = $canvas-> size;
1197	my (
1198		$sb, $v,
1199		$range, $min,
1200		$tval, $tlen, $ttxt,
1201		$ta, $kb
1202	) = (
1203		$self-> {shaftBreadth}, $self-> {vertical},
1204		abs($self-> {max} - $self-> {min}) || 1, $self-> {min},
1205		$self-> {tickVal}, $self-> {tickLen}, $self-> {tickTxt},
1206		$self-> {tickAlign}, $self->{knobBreadth},
1207	);
1208	if ( $ta == tka::Normal) {
1209		$ta = 1;
1210	} elsif ( $ta == tka::Alternative) {
1211		$ta = 2;
1212	} else {
1213		$ta = 3;
1214	}
1215
1216	unless ( $self-> transparent) {
1217		$canvas-> color( $clr[1]);
1218		$canvas-> bar(0,0,@size);
1219	}
1220	$sb = ( $v ? $size[0] : $size[1]) / 6 unless $sb;
1221	$sb = 2 unless $sb;
1222
1223	my ($br, $bh, $mw, $bw);
1224	if ( $v ) {
1225		$bh = $canvas-> font-> height;
1226		$br = $size[1] - 2 * $bh - 2;
1227	} else {
1228		$mw = $canvas-> font-> width;
1229		$bw = $mw + $self-> {borderWidth};
1230		$br  = $size[0] - 2 * $bw - 2;
1231	}
1232
1233	# do we have to remove small dashes?
1234	my $remove_dashes_shorter_than = 0;
1235	my $check_dashes = sub {
1236		my ( $height, $set_threshold ) = @_;
1237		my $lastval = -1_000_000;
1238		for ( my $i = 1; $i < scalar @{$tval} - 1; $i++) {
1239			next if $$tlen[$i] > $height || $$tlen[$i] < $remove_dashes_shorter_than;
1240			my $val = int( abs( $$tval[$i] - $min) * ( $br - 3) / $range + .5);
1241			$remove_dashes_shorter_than = $set_threshold, last if abs($val - $lastval) < 4;
1242			$lastval = $val;
1243		}
1244	};
1245	if ( $self->{scheme} == ss::Thermometer || $self->{scheme} == ss::Axis ) {
1246		$check_dashes->(5, 5);
1247		$check_dashes->(12, 12);
1248	} elsif ( $self->{scheme} == ss::Axis ) {
1249		$check_dashes->(3, 3);
1250	} else {
1251		$check_dashes->(12, 12);
1252	}
1253
1254	if ( $v) {
1255		my $bw  = ( $size[0] - $sb) / 2;
1256		return if $size[1] <= $kb * ($self-> {readOnly} ? 1 : 0) + 2 * $bh + 2;
1257
1258		$canvas-> translate((( $ta == 1) ? 1 : -1) * ( $bw - $sb - $kb), 0)
1259			if $ta < 3;
1260		$canvas-> rect3d(
1261			$bw, $bh, $bw + $sb - 1, $bh + $br - 1, 1,
1262			@c3d, $cht[1]
1263		), return unless $range;
1264
1265		my $val = $bh + 1 + abs( $self-> {value} - $min) * ( $br - 3) / $range;
1266		if ( $self-> {ribbonStrip}) {
1267			$canvas-> rect3d( $bw, $bh, $bw + $sb - 1, $bh + $br - 1, 1, @c3d);
1268			$canvas-> color( $cht[0]);
1269			$canvas-> bar( $bw + 1, $bh + 1, $bw + $sb - 2, $val);
1270			$canvas-> color( $cht[1]);
1271			$canvas-> bar( $bw + 1, $val + 1, $bw + $sb - 2, $bh + $br - 2);
1272		} else {
1273			$canvas-> rect3d( $bw, $bh, $bw + $sb - 1, $bh + $br - 1,
1274				1, @c3d, $cht[1]);
1275			$canvas-> color( $clr[0]);
1276			$canvas-> line( $bw + 1, $val, $bw + $sb - 2, $val)
1277				if $self-> {readOnly};
1278		}
1279		my $i;
1280		my @tr = $self->translate;
1281		my @texts;
1282		for my $glyph_delta ( @glyph_deltas ) {
1283			my ( $color, $delta_x, $delta_y ) = @$glyph_delta;
1284			$canvas-> color( $color );
1285			$self->translate($tr[0] + $delta_x, $tr[1] + $delta_y);
1286			for ( $i = 0; $i < scalar @{$tval}; $i++) {
1287				my $val = $bh + 1 + abs( $$tval[$i] - $min) * ( $br - 3) / $range;
1288				if ( $$tlen[ $i]) {
1289					next if
1290						defined($remove_dashes_shorter_than) &&
1291						$remove_dashes_shorter_than > $$tlen[$i] &&
1292						$i != 0 && $i != $#$tval;
1293					$canvas-> line(
1294						$bw + $sb + 3, $val,
1295						$bw + $sb + $$tlen[ $i] + 3, $val
1296					) if $ta & 2;
1297					$canvas-> line(
1298						$bw - 4, $val,
1299						$bw - 4 - $$tlen[ $i], $val
1300					) if $ta & 1;
1301				}
1302				push @texts, [
1303					$$ttxt[ $i],
1304					( $ta == 2) ?
1305						$bw + $sb + $$tlen[ $i] + 5 :
1306						$bw - $$tlen[ $i] - 5 - $canvas-> get_text_width( $$ttxt[ $i]),
1307					$val - $bh / 2
1308				] if defined $$ttxt[ $i];
1309			}
1310			my $size = $size[1] - $bh - 2;
1311			my $fh   = $bh + 1;
1312			if ($size < $fh) {
1313				@texts = ();
1314			} elsif ( $size < $fh * 2 || @texts == 1) {
1315				@texts = ($texts[0]);
1316			} elsif ( $size < $fh * 3 || @texts == 2) {
1317				@texts = @texts[0,-1];
1318			} else {
1319				my @t = ($texts[0]);
1320				$size -= $fh * 1.5;
1321				my $y = $texts[0][2] + $fh;
1322				for my $t ( @texts[1 .. $#texts - 1] ) {
1323					next if $t->[2] < $y;
1324					last if $t->[2] > $size;
1325					push @t, $t;
1326					$y = $t->[2] + $fh;
1327				}
1328				@texts = (@t, $texts[-1]);
1329			}
1330			$canvas->text_shape_out(@$_) for @texts;
1331		}
1332		unless ( $self-> {readOnly}) {
1333			my @jp = map { int( $_ + .5 ) } (
1334				$bw - 4,       $val - $kb / 2,
1335				$bw - 4,       $val + $kb / 2,
1336				$bw + $sb + 1, $val + $kb / 2,
1337				$bw + $sb + 1 + $kb/2, $val,
1338				$bw + $sb + 1, $val - $kb / 2,
1339			);
1340			my $rgn = Prima::Region->new( polygon => \@jp);
1341			$rgn->offset( $canvas->translate );
1342			$canvas-> region( $rgn );
1343			$canvas-> new_gradient(
1344				palette  => [ $c3d[0], ($self->{prelight} ? $prelight : $clr[1]) ],
1345				poly     => [0,0,0.3,0.7,1,1],
1346				vertical => 0,
1347			)-> bar( $jp[0]+2,$jp[1]+2,$jp[6]-2,$jp[3]-2);
1348			$canvas-> color( 0x404040);
1349			$canvas-> polyline([@jp[6..9,0,1]]);
1350			$canvas-> color( $c3d[1]);
1351			$canvas-> polyline([$jp[0]+1,$jp[1]+1,$jp[2]+1,$jp[3]-1,$jp[4],$jp[5]-1,$jp[6]-1,$jp[7]]);
1352			$canvas-> line($jp[0]+2, $jp[7]-1, $jp[6]-2, $jp[7]-1);
1353			$canvas-> color( $c3d[0]);
1354			$canvas-> polyline([$jp[6]-1,@jp[7,8],$jp[9]+1,$jp[0],$jp[1]+1,@jp[2..7]]);
1355			$canvas-> line($jp[0]+2, $jp[7]+1, $jp[6]-1, $jp[7]+1);
1356		}
1357	} else {
1358		my $bh  = ( $size[1] - $sb) / 2;
1359		my $fh = $canvas-> font-> height;
1360		return if $size[0] <= $kb * ($self-> {readOnly} ? 1 : 0) + 2 * $bw + 2;
1361
1362		$canvas-> translate( 0, (( $ta == 1) ? -1 : 1) * ( $bh - $sb - $kb))
1363			if $ta < 3;
1364		$canvas-> rect3d( $bw, $bh, $bw + $br - 1, $bh + $sb - 1, 1, @c3d, $cht[1]), return
1365			unless $range;
1366		my $val = $bw + 1 + abs( $self-> {value} - $min) * ( $br - 3) / $range;
1367
1368		if ( $self-> {ribbonStrip}) {
1369			$canvas-> rect3d( $bw, $bh, $bw + $br - 1, $bh + $sb - 1, 1, @c3d);
1370			$canvas-> color( $cht[0]);
1371			$canvas-> bar( $bw+1, $bh+1, $val, $bh + $sb - 2);
1372			$canvas-> color( $cht[1]);
1373			$canvas-> bar( $val+1, $bh+1, $bw + $br - 2, $bh + $sb - 2);
1374		} else  {
1375			$canvas-> rect3d( $bw, $bh, $bw + $br - 1, $bh + $sb - 1, 1, @c3d, $cht[1]);
1376			$canvas-> color( $clr[0]);
1377			$canvas-> line( $val, $bh+1, $val, $bh + $sb - 2) if $self-> {readOnly};
1378		}
1379		my $i;
1380
1381		my @texts;
1382		my @tr = $self->translate;
1383		for my $glyph_delta ( @glyph_deltas ) {
1384			my ( $color, $delta_x, $delta_y ) = @$glyph_delta;
1385			$canvas-> color( $color );
1386			$self->translate($tr[0] + $delta_x, $tr[1] + $delta_y);
1387			for ( $i = 0; $i < scalar @{$tval}; $i++) {
1388				my $val = int( 1 + $bw + abs( $$tval[$i] - $min) * ( $br - 3) / $range + .5);
1389				if ( $$tlen[ $i]) {
1390					next if
1391						defined($remove_dashes_shorter_than) &&
1392						$remove_dashes_shorter_than > $$tlen[$i] &&
1393						$i != 0 && $i != $#$tval;
1394					$canvas-> line( $val, $bh + $sb + 3, $val, $bh + $sb + $$tlen[ $i] + 3)
1395						if $ta & 1;
1396					$canvas-> line( $val, $bh - 4, $val, $bh - 4 - $$tlen[ $i])
1397						if $ta & 2;
1398				}
1399
1400				next unless defined $$ttxt[ $i];
1401				my $tw = int( $canvas-> get_text_width( $$ttxt[ $i]) / 2 + .5);
1402				my $x = $val - $tw;
1403				next if $x >= $size[0] or $val + $tw < 0;
1404				push @texts, [
1405					$$ttxt[$i], $val, $tw,
1406					( $ta == 2) ? $bh - $$tlen[ $i] - 5 - $fh : $bh + $sb + $$tlen[ $i] + 5,
1407					$size[0]
1408				];
1409			}
1410
1411
1412			if ( @texts) {
1413				# see that leftmost val fits
1414				if ( $texts[0]->[1] - $texts[0]->[2] < 0) {
1415					$texts[0]->[1] = $texts[0]->[2];
1416					shift @texts
1417						if $texts[0]->[1] + $texts[0]->[2] > $size[0];
1418					goto NO_LABELS unless @texts;
1419				}
1420
1421				# see that rightmost text fits
1422				my ( $rightmost_val, $rightmost_label_width) = (
1423					$texts[-1]->[1], $texts[-1]->[2]);
1424				$rightmost_val = $size[0] - 1 - $rightmost_label_width
1425					if $rightmost_val > $size[0] - 1 - $rightmost_label_width;
1426				if ( 1 < @texts and $rightmost_val < 0) {
1427					# skip it
1428					pop @texts;
1429					goto NO_LABELS unless @texts;
1430				} else {
1431					$texts[-1]->[1] = $rightmost_val;
1432					my $lv = 2 * $rightmost_label_width + $mw;
1433					$$_[-1] -= $lv for @texts[0..$#texts-1];
1434					$texts[-1][-1] += $mw;
1435				}
1436
1437				# draw labels
1438				my $lastx = 0;
1439				for ( @texts) {
1440					my ( $text, $val, $half_width, $y, $xlim) = @$_;
1441					my $x = $val - $half_width;
1442					next if $x < $lastx or $x < 0 or $val + $half_width >= $xlim;
1443					$lastx = $val + $half_width + $mw;
1444					$canvas-> text_shape_out( $text, $x, $y);
1445				}
1446			}
1447			NO_LABELS:
1448		}
1449
1450		unless ( $self-> {readOnly}) {
1451			my @jp = map { int($_ + .5) } (
1452				$val - $kb / 2, $bh - 2,
1453				$val - $kb / 2, $bh + $sb + 3,
1454				$val + $kb / 2, $bh + $sb + 3,
1455				$val + $kb / 2, $bh - 2,
1456				$val,           $bh - $kb / 2 - 2,
1457			);
1458			my $rgn = Prima::Region->new( polygon => \@jp);
1459			$rgn->offset( $canvas->translate );
1460			$canvas-> region( $rgn );
1461			$canvas-> new_gradient(
1462				palette  => [ ($self->{prelight} ? $prelight : $clr[1]), $c3d[0] ],
1463				poly     => [0,0,0.7,0.3,1,1],
1464				vertical => 1,
1465			)-> bar( $jp[0]+2,$jp[9],$jp[4]-2,$jp[3]);
1466			$canvas-> color( 0x404040 );
1467			$canvas-> polyline([@jp[4..9]]);
1468			$canvas-> color( $c3d[0]);
1469			$canvas-> polyline([
1470				@jp[8,9,0..3],$jp[4]-1,$jp[5],
1471				$jp[6]-1,$jp[7],$jp[8],$jp[9]+1
1472			]);
1473			$canvas-> line($jp[8]-1,$jp[3]-2,$jp[8]-1,$jp[9]) if $kb > 10;
1474			$canvas-> color( $c3d[1]);
1475			$canvas-> polyline([$jp[8],$jp[9]+1,$jp[0]+1,$jp[1],$jp[2]+1,$jp[3]-1,$jp[4]-2,$jp[5]-1]);
1476			$canvas-> line($jp[8]+1,$jp[3]-2,$jp[8]+1,$jp[9]) if $kb > 10;
1477		}
1478	}
1479}
1480
1481sub on_fontchanged
1482{
1483	my $self = shift;
1484	$self->update_geom_sizes;
1485	$self->repaint;
1486}
1487
1488sub update_geom_sizes
1489{
1490	my $self = shift;
1491	my $maxtlen = 0;
1492	for ( @{ $self->{tickLen}}) {
1493		$maxtlen = $_ if $maxtlen < $_;
1494	}
1495	$maxtlen *= 2 if $self->tickAlign == tka::Dual;
1496	if ( $self->vertical ) {
1497		return unless $self->autoWidth;
1498		my $maxtwid = 0;
1499		$self->begin_paint_info;
1500		for ( grep { defined } @{ $self->{tickTxt}}) {
1501			my $w = $self->get_text_width($_);
1502			$maxtwid = $w if $maxtwid < $w;
1503		}
1504		$self->end_paint_info;
1505		my $x = $maxtlen + $maxtwid * 2 + $self->shaftBreadth + $self->borderWidth + 5 + $self->knobBreadth;
1506		$self->geomWidth($x);
1507	} else {
1508		return unless $self->autoHeight;
1509		my $y = $maxtlen + $self->font->height * 2 + $self->shaftBreadth + $self->borderWidth + 5 + $self->knobBreadth;
1510		$self->geomHeight($y);
1511	}
1512}
1513
1514sub pos2info
1515{
1516	my ( $self, $x, $y) = @_;
1517	my @size = $self-> size;
1518	return if $self-> {max} == $self-> {min};
1519	if ( $self-> {vertical}) {
1520		my $bh  = $self-> font-> height;
1521		my $val =
1522			$bh +
1523			1 +
1524			abs( $self-> {value} - $self-> {min}) *
1525				( $size[1] - 2 * $bh - 5) /
1526				( abs($self-> {max} - $self-> {min}) || 1);
1527		my $ret1 =
1528			$self-> {min} +
1529			( $y - $bh - 1) *
1530				abs($self-> {max} - $self-> {min}) /
1531				(( $size[1] - 2 * $bh - 5) || 1);
1532
1533		if ( $y < $val - $self->knobBreadth / 2 or $y >= $val + $self->knobBreadth / 2) {
1534			return 0, $ret1;
1535		} else {
1536			return 1, $ret1, $y - $val;
1537		}
1538	} else {
1539		my $bw = $self-> font-> width + $self->{borderWidth};
1540		my $val =
1541			$bw +
1542			1 +
1543			abs( $self-> {value} - $self-> {min}) *
1544				( $size[0] - 2 * $bw - 5) /
1545				(abs($self-> {max} - $self-> {min}) || 1);
1546		my $ret1 =
1547			$self-> {min} +
1548			( $x - $bw - 1) *
1549				abs($self-> {max} - $self-> {min}) /
1550				(( $size[0] - 2 * $bw - 5) || 1);
1551
1552		if ( $x < $val - $self->knobBreadth / 2 or $x >= $val + $self->knobBreadth / 2) {
1553			return 0, $ret1;
1554		} else {
1555			return 1, $ret1, $x - $val;
1556		}
1557	}
1558}
1559
1560sub on_mousedown
1561{
1562	my ( $self, $btn, $mod, $x, $y) = @_;
1563	return if $self-> {readOnly};
1564	return if $self-> {mouseTransaction};
1565	return if $btn != mb::Left;
1566	my ($info, $pos, $ap) = $self-> pos2info( $x, $y);
1567	return unless defined $info;
1568	delete $self->{prelight};
1569	if ( $info == 0) {
1570		$self-> value( $pos);
1571		return;
1572	}
1573	$self-> {aperture} = $ap;
1574	$self-> {mouseTransaction} = 1;
1575	$self-> capture(1);
1576	$self-> clear_event;
1577}
1578
1579sub on_mouseup
1580{
1581	my ( $self, $btn, $mod, $x, $y) = @_;
1582	return if $btn != mb::Left;
1583	return unless $self-> {mouseTransaction};
1584	$self-> {mouseTransaction} = undef;
1585	$self-> capture(0);
1586	$self-> notify( 'Change') unless $self-> {autoTrack};
1587}
1588
1589sub on_mousemove
1590{
1591	my ( $self, $mod, $x, $y) = @_;
1592	unless ($self-> {mouseTransaction}) {
1593		if ( $self-> enabled ) {
1594			my ($prelight) = $self-> pos2info( $x, $y);
1595			$prelight = (!defined($prelight) || ($prelight != 1)) ? undef : 1;
1596			if (($prelight // 0) != ($self->{prelight} // 0)) {
1597				$self->{prelight} = $prelight;
1598				$self->repaint;
1599			}
1600		}
1601		return;
1602	}
1603	$self-> {vertical} ? $y : $x   -= $self-> {aperture};
1604	my ( $info, $pos) = $self-> pos2info( $x, $y);
1605	return unless defined $info;
1606	my $ov = $self-> {value};
1607	$self-> {suppressNotify} = 1 unless $self-> {autoTrack};
1608	$self-> value( $pos);
1609	$self-> {suppressNotify} = 0;
1610	$self-> notify(q(Track)) if !$self-> {autoTrack} && $ov != $self-> {value};
1611}
1612
1613sub on_mouseleave
1614{
1615	my $self = shift;
1616	$self-> repaint if defined( delete $self->{prelight} );
1617}
1618
1619sub on_keydown
1620{
1621	my ( $self, $code, $key, $mod) = @_;
1622	return if $self-> {readOnly};
1623	if ( $key == kb::Home || $key == kb::PgUp) {
1624		$self-> value( $self-> {vertical} ? $self-> {max} : $self-> {min});
1625		$self-> clear_event;
1626		return;
1627	}
1628	if ( $key == kb::End || $key == kb::PgDn) {
1629		$self-> value( $self-> {vertical} ? $self-> {min} : $self-> {max});
1630		$self-> clear_event;
1631		return;
1632	}
1633	if ( $key == kb::Left || $key == kb::Right || $key == kb::Up || $key == kb::Down) {
1634		my $s = $self-> {step};
1635		$self-> clear_event;
1636		$self-> set_next_value(( $key == kb::Left || $key == kb::Down) ? -$s : $s);
1637	}
1638}
1639
1640sub set_vertical
1641{
1642	$_[0]-> {vertical} = $_[1];
1643	$_[0]-> update_geom_sizes;
1644	$_[0]-> repaint;
1645}
1646
1647sub set_tick_align
1648{
1649	my ( $self, $ta) = @_;
1650	$ta = tka::Normal if $ta != tka::Alternative and $ta != tka::Dual;
1651	return if $ta == $self-> {tickAlign};
1652	$self-> {tickAlign} = $ta;
1653	$self-> update_geom_sizes;
1654	$self-> repaint;
1655}
1656
1657sub set_ribbon_strip
1658{
1659	$_[0]-> {ribbonStrip} = $_[1];
1660	$_[0]-> repaint;
1661}
1662
1663sub set_shaft_breadth
1664{
1665	my ( $self, $sb) = @_;
1666	$sb = 0 if $sb < 0;
1667	return if $sb == $self-> {shaftBreadth};
1668	$self-> {shaftBreadth} = $sb;
1669	$self-> update_geom_sizes;
1670	$self-> repaint;
1671}
1672
1673sub set_bound
1674{
1675	my ( $self, $val, $bound) = @_;
1676	$self-> {$bound} = $val;
1677	$self-> scheme($self-> {scheme}) if defined $self-> {scheme};
1678	$self-> repaint;
1679}
1680
1681sub value
1682{
1683	if ($#_) {
1684		my ( $self, $value) = @_;
1685		my ( $min, $max) = ( $self-> {min}, $self-> {max});
1686		my $old = $self-> {value};
1687		if ( $self-> {snap}) {
1688			my ( $minDist, $thatVal, $i) = ( abs( $min - $max));
1689			my $tval = $self-> {tickVal};
1690			for ( $i = 0; $i < scalar @{$tval}; $i++) {
1691				my $j = $$tval[ $i];
1692				$minDist = abs(($thatVal = $j) - $value)
1693					if abs( $j - $value) < $minDist;
1694			}
1695			$value = $thatVal if defined $thatVal;
1696		} elsif ( $self-> {step} != 0 ) {
1697			$value = int ( $value / $self-> {step} ) * $self-> {step};
1698		}
1699		$value = $min if $value < $min;
1700		$value = $max if $value > $max;
1701		return if $old == $value;
1702		$self-> {value} = $value;
1703		my @size = $self-> size;
1704		my $sb = $self-> {shaftBreadth};
1705		if ( $self-> {vertical}) {
1706			$sb = $size[0] / 6 unless $sb;
1707			$sb = 2 unless $sb;
1708			my $bh = $self-> font-> height;
1709			my $bw  = ( $size[0] - $sb) / 2;
1710			my $v1  = $bh + 1 + abs( $self-> {value} - $self-> {min}) *
1711				( $size[1] - 2 * $bh - 5) / (abs($self-> {max} - $self-> {min})||1);
1712			my $v2  = $bh + 1 + abs( $old - $self-> {min}) *
1713				( $size[1] - 2 * $bh - 5) / (abs($self-> {max} - $self-> {min})||1);
1714			( $v2, $v1) = ( $v1, $v2) if $v1 > $v2;
1715			my $kb = $self-> knobBreadth / 2;
1716			my $xd = 0;
1717			$xd = (( $self-> {tickAlign} == tka::Normal) ? 1 : -1) *
1718			( $bw - $sb - $self->knobBreadth) if $self-> {tickAlign} != tka::Dual;
1719			$self-> invalidate_rect(
1720				map { int($_ + .5) }
1721				$bw - 4 + $xd, $v1 - $kb, $bw + $sb * 2 + 3 + $xd, $v2 + $kb + 1
1722			);
1723		} else {
1724			$sb = $size[1] / 6 unless $sb;
1725			$sb = 2 unless $sb;
1726			my $bw = $self-> font-> width + $self-> {borderWidth};
1727			my $bh  = ( $size[1] - $sb) / 2;
1728			my $v1  = $bw + 1 + abs( $self-> {value} - $self-> {min}) *
1729				( $size[0] - 2 * $bw - 5) / (abs($self-> {max} - $self-> {min})||1);
1730			my $v2  = $bw + 1 + abs( $old - $self-> {min}) *
1731				( $size[0] - 2 * $bw - 5) / (abs($self-> {max} - $self-> {min})||1);
1732			( $v2, $v1) = ( $v1, $v2) if $v1 > $v2;
1733			my $kb = $self-> knobBreadth / 2;
1734			my $yd = 0;
1735			$yd = (( $self-> {tickAlign} == tka::Normal) ? -1 : 1) *
1736			( $bh - $sb - $self->knobBreadth) if $self-> {tickAlign} != tka::Dual;
1737			$self-> invalidate_rect(
1738				map { int($_ + .5) }
1739				$v1 - $kb, $bh - $kb - 2 + $yd,
1740				$v2 + $kb + 1, $bh + $sb + 5 + $yd
1741			);
1742		}
1743		$self-> notify(q(Change)) unless $self-> {suppressNotify};
1744	} else {
1745		return $_[0]-> {value};
1746	}
1747}
1748sub vertical    {($#_)?$_[0]-> set_vertical    ($_[1]):return $_[0]-> {vertical};}
1749sub tickAlign   {($#_)?$_[0]-> set_tick_align  ($_[1]):return $_[0]-> {tickAlign};}
1750sub ribbonStrip {($#_)?$_[0]-> set_ribbon_strip($_[1]):return $_[0]-> {ribbonStrip};}
1751sub shaftBreadth{($#_)?$_[0]-> set_shaft_breadth($_[1]):return $_[0]-> {shaftBreadth};}
1752
1753sub knobBreadth
1754{
1755	return $_[0]->{knobBreadth} unless $#_;
1756	my ( $self, $kb) = @_;
1757	$kb = 4 if $kb < 4;
1758	$kb &= ~1; # must divide by 2
1759	return if $kb == $self-> {knobBreadth};
1760	$self-> {knobBreadth} = $kb;
1761	$self-> update_geom_sizes;
1762	$self-> repaint;
1763}
1764
1765sub borderWidth
1766{
1767	return $_[0]-> {borderWidth} unless $#_;
1768	my ( $self, $bw) = @_;
1769	$bw = 0 if $bw < 0;
1770	$self-> {borderWidth} = $bw;
1771	$self-> update_geom_sizes;
1772	$self-> repaint;
1773}
1774
1775package Prima::CircularSlider;
1776use vars qw(@ISA);
1777@ISA = qw(Prima::AbstractSlider Prima::MouseScroller);
1778
1779{
1780my %RNT = (
1781	%{Prima::AbstractSlider-> notification_types()},
1782	Stringify  => nt::Action,
1783);
1784sub notification_types { return \%RNT; }
1785}
1786
1787sub profile_default
1788{
1789	return {
1790		%{$_[ 0]-> SUPER::profile_default},
1791		buttons        => 1,
1792		stdPointer     => 0,
1793		buttonWidth    => 10,
1794	}
1795}
1796
1797sub profile_check_in
1798{
1799	my ( $self, $p, $default) = @_;
1800
1801	my $sc = $::application->uiScaling;
1802	$p->{$_} = ( exists($p->{$_}) ? $p->{$_} : $default->{$_} ) * $sc for qw(buttonWidth);
1803	$self-> SUPER::profile_check_in( $p, $default);
1804}
1805
1806sub init
1807{
1808	my $self = shift;
1809	$self-> {$_}=0 for qw( buttons pressState circX circY br butt1X butt1Y butt2X buttonWidth);
1810	$self-> {string} = '';
1811	my %profile = $self-> SUPER::init( @_);
1812	$self-> $_($profile{$_}) for qw( buttons stdPointer buttonWidth);
1813	$self-> reset;
1814	return %profile;
1815}
1816
1817sub setup
1818{
1819	$_[0]-> SUPER::setup;
1820	$_[0]-> notify(q(Stringify), $_[0]-> {value}, \$_[0]-> {string});
1821	$_[0]-> repaint;
1822}
1823
1824sub set_text
1825{
1826	my ( $self, $caption) = @_;
1827	$self-> SUPER::set_text( $caption );
1828	$self-> {accel} = lc($1) if $caption =~ /~([a-z0-9])/i;
1829	$self-> repaint;
1830}
1831
1832sub reset
1833{
1834	my $self = $_[0];
1835	my @size = $self-> size;
1836	my $fh  = $self-> font-> height;
1837	my $bw  = $self->buttonWidth;
1838	my $bw_fh = ( $bw > $fh ) ? $bw : $fh;
1839	my $br  = ($size[0] > ( $size[1] - $bw_fh)) ? ( $size[1] - $bw_fh) : $size[0];
1840	$self->begin_paint_info;
1841
1842	# first calculate a minimum viable dial radius
1843	my $tx1 = $self->get_text_width( $self-> min, 1 );
1844	my $tx2 = $self->get_text_width( $self-> max, 1 );
1845	$tx1 = $tx2 if $tx1 < $tx2;
1846	$tx1 = $fh if $tx1 < $fh;
1847	$tx1 /= 2;
1848	$tx1 += 4 + 10;
1849	my $min_viable_rad = $tx1;
1850	my $rad = $self-> {radius} = ($tx1 < ($br * 0.5)) ? $tx1 : ($br * 0.5);
1851
1852	# circle center
1853	$self-> {br}        = $br;
1854	$self-> {circX}     = int($size[0]/2 + .5);
1855	$self-> {circY}     = int(($size[1] + $bw_fh) / 2 + .5);
1856
1857	my $i;
1858	my ( $tval, $tlen, $ttxt) = ( $self-> {tickVal}, $self-> {tickLen}, $self-> {tickTxt});
1859	my @ext = (0,0,0,0);
1860	for ( $i = 0; $i < scalar @{$tval}; $i++) {
1861		my $r = $rad + 3 + $$tlen[ $i];
1862		my ( $cos, $sin) = $self-> offset2data( $$tval[$i]);
1863		if ( $$tlen[$i]) {
1864			my @outer = ($r * $cos, $r * $sin);
1865			$ext[0] = $outer[0] if $ext[0] > $outer[0];
1866			$ext[1] = $outer[1] if $ext[1] > $outer[1];
1867			$ext[2] = $outer[0] if $ext[2] < $outer[0];
1868			$ext[3] = $outer[1] if $ext[3] < $outer[1];
1869		}
1870		$r += 3;
1871		if ( defined $$ttxt[ $i]) {
1872			my $w = $self-> get_text_width( $$ttxt[ $i], 1);
1873			my $y = $r * $sin - $fh / 2 * ( 1 - $sin);
1874			my $x = $r * $cos - ( 1 - $cos) * $w / 2;
1875			my $r = $x + $w;
1876			my $t = $y + $fh;
1877			$ext[0] = $x if $ext[0] > $x;
1878			$ext[1] = $y if $ext[1] > $y;
1879			$ext[2] = $r if $ext[2] < $r;
1880			$ext[3] = $t if $ext[3] < $t;
1881		}
1882	}
1883	$ext[$_] = int($ext[$_] - .5) for 0,1;
1884	$ext[$_] = int($ext[$_] + .5) for 2,3;
1885
1886	my @sz = ( $ext[2] - $ext[0], $ext[3] - $ext[1] );
1887	my @d = (
1888		$self->{circX} + $ext[0],
1889		$self->{circY} + $ext[1],
1890		$size[0] - $self->{circX} - $ext[2],
1891		$size[1] - $self->{circY} - $ext[3],
1892	);
1893	$self-> {show_scale} = ! grep { $_ < 0 } @d;
1894
1895GROW_CIRCLE:
1896	@ext = (0,0,0,0) unless $self->{show_scale};
1897
1898	# can grow the circle?
1899	$ext[0] = -$rad if $ext[0] > -$rad;
1900	$ext[1] = -$rad if $ext[1] > -$rad;
1901	$ext[2] =  $rad if $ext[2] <  $rad;
1902	$ext[3] =  $rad if $ext[3] <  $rad;
1903	$ext[$_] -= 2 for 0,1;
1904	$ext[$_] += 2 for 2,3;
1905	@sz = ( $ext[2] - $ext[0], $ext[3] - $ext[1] );
1906
1907	if ( $sz[0] < $size[0] && $sz[1] < $size[1] - $bw - $fh) {
1908		my @d = (
1909			$self->{circX} + $ext[0],
1910			$self->{circY} + $ext[1],
1911			$size[0] - $self->{circX} - $ext[2],
1912			$size[1] - $self->{circY} - $ext[3],
1913		);
1914		my $min = $d[0];
1915		for ( @d ) {
1916			$min = $_ if $min > $_;
1917		}
1918		$min--;
1919		if ( $min > 0 ) {
1920			$self->{radius} += $min;
1921			$ext[$_] -= $min for 0,1;
1922			$ext[$_] += $min for 2,3;
1923		}
1924	}
1925
1926	# buttons X location
1927	$self-> {butt1X}    = int( $size[0] / 2 - $self->{radius} - $bw / 2 + .5);
1928	$self-> {butt2X}    = int( $size[0] / 2 + $self->{radius} - $bw / 2 + .5);
1929	if ($self->{butt1X} < 1) {
1930		$self->{butt2X} += $self->{butt1X} - 2;
1931		$self->{butt1X} = 1;
1932	}
1933	if ( $self->{butt1X} + $bw + 1 > $self->{butt2X} ) {
1934		my $d = $bw - $self->{butt2X} + $self->{butt1X};
1935		$self->{butt1X} -= $d / 2 + 1;
1936		$self->{butt2X} += $d / 2 + 1;
1937	}
1938
1939	# Y location for title and buttons
1940	my $lowest = $self->{circY} + $ext[1];
1941	my $fd     = $self->font->descent;
1942	$self-> {textY} = ($lowest > $fh + 2) ? ($lowest - $fh) / 2 + 1 : 2;
1943	$self-> {textY} += $fd;
1944	$self-> {butt1Y} = ($lowest > $bw + 2) ? ($lowest - $bw) / 2 + 1 : 2;
1945	$self-> {show_text} = 1;
1946	my $title_width = $self->get_text_width($self->text, 1);
1947	if ( $title_width > $size[0] ) {
1948		$self->{show_text} = 0;
1949	} elsif ( $title_width > $self->{butt2X} - $self->{butt1X} - $bw - 2 ) {
1950		if ( $title_width + 2 + $bw * 2 < $size[0] ) {
1951			# move buttons aparts by x to accomodate title
1952			$self->{butt1X} = ($size[0] - $title_width) / 2 - $bw - 1;
1953			$self->{butt2X} = ($size[0] + $title_width) / 2 + 1;
1954		} elsif ( $lowest > $fh + $bw + 2 ) {
1955			# draw buttons and title on separate lines
1956			my $d = ($bw_fh + 1 ) / 2;
1957			$self->{textY} -= $d;
1958			$self->{butt1Y} += $d;
1959		} else {
1960			$self->{show_text} = 0;
1961		}
1962	}
1963
1964	$self->{show_dial} = $self->{radius} >= $min_viable_rad;
1965	if ( !$self->{show_dial} && $self->{show_scale} ) {
1966		# try to grow the circle again
1967		$self->{show_scale} = 0;
1968		goto GROW_CIRCLE;
1969	}
1970
1971	$self->end_paint_info;
1972
1973	# hints
1974	if (
1975		$self->{show_text} &&
1976		!$self->{show_scale} &&
1977		$self->{show_dial} &&
1978		$self->{textY} + $fh > $self->{circY} - $self->{radius}
1979	) {
1980		# if text is over the expanded dial ( when @ext is empty ), move it or hide
1981		$self->{textY} = $fd;
1982		if ( $self->{textY} + $fh > $self->{circY} - $self->{radius}) {
1983			$self->{show_text} = 0;
1984		}
1985	}
1986	if ( !$self->{show_text} && $self->{butt1Y} + $bw > $size[1] ) {
1987		# no text, don't need to fit buttons together with the text
1988		$self->{butt1Y} = ( $size[1] - $bw ) / 2;
1989	}
1990	if ( !$self->{show_dial} ) {
1991		# do not obscure the value as much as possible
1992		$self->{show_text} = 0
1993			if $self->{show_text} && $fh + $bw + $fh > $size[1];
1994		$self->{butt1Y} = $self->{textY} = 0;
1995	}
1996	$self->{valueY} = $self->{circY} - $fh / 2;
1997	if ($self->{valueY} + $fh - 2 > $size[1]) {
1998		$self->{valueY} = 0;
1999		if ( $self->{butt1X} + $bw + 2 >= $self->{butt2X} ) {
2000			$self->{butt1X} = 1;
2001			$self->{butt2X} = $size[0] - $bw - 1;
2002		}
2003	}
2004}
2005
2006sub offset2pt
2007{
2008	my ( $self, $width, $height, $value, $radius) = @_;
2009	my $a = 225 * 3.14159 / 180 - ( 270 * 3.14159 / 180) * ( $value - $self-> {min}) /
2010		(abs( $self-> {min} - $self-> {max})||1);
2011	return $width + $radius * cos($a), $height + $radius * sin($a);
2012}
2013
2014sub offset2data
2015{
2016	my ( $self, $value) = @_;
2017	my $a = 225 * 3.14159 / 180 - ( 270 * 3.14159 / 180) * abs( $value - $self-> {min})/
2018		(abs( $self-> {min} - $self-> {max})||1);
2019	return cos($a), sin($a);
2020}
2021
2022sub on_paint
2023{
2024	my ( $self, $canvas) = @_;
2025	my @clr;
2026	my $prelight;
2027	if ( $self->enabled ) {
2028		@clr = ( $self-> color, $self-> backColor);
2029		$prelight = $self->prelight_color($clr[1]) if $self->{prelight};
2030	} else {
2031		@clr = ( $self-> disabledColor, $self-> disabledBackColor);
2032	}
2033	my @c3d  = ( $self-> dark3DColor, $self-> light3DColor);
2034	my @cht  = ( $self-> hiliteColor, $self-> hiliteBackColor);
2035	my @size = $canvas-> size;
2036	my ( $range, $min, $tval, $tlen, $ttxt, $bw) =
2037	( abs($self-> {max} - $self-> {min}), $self-> {min}, $self-> {tickVal},
2038	$self-> {tickLen}, $self-> {tickTxt}, $self->{buttonWidth} );
2039
2040	if ( defined $self-> {singlePaint}) {
2041		my @clip1 = @{$self-> {expectedClip}};
2042		my @clip2 = $self-> clipRect;
2043		my $i;
2044		for ( $i = 0; $i < 4; $i++) {
2045			$self-> {singlePaint} = undef, last if $clip1[$i] != $clip2[$i];
2046		}
2047	}
2048
2049	$canvas-> color( $clr[1]);
2050	$canvas-> bar( 0, 0, @size) if !$self-> transparent && !defined $self-> {singlePaint};
2051	my $fh  = $canvas-> font-> height;
2052	my $br  = $self-> {br};
2053	my $rad = $self-> {radius};
2054	my @cpt = ( $self-> {circX}, $self-> {circY}, $rad*2+1, $rad*2+1);
2055
2056	goto AFTER_DIAL unless $self->{show_dial};
2057	if ( defined $self-> {singlePaint}) {
2058		my $drad = 5;
2059		my $radx = $rad;
2060		for my $lw ( 2..4) {
2061			$radx -= 100;
2062			last if $radx < 0;
2063			$drad++;
2064		}
2065		$canvas-> color( $prelight ) if $self->{prelight};
2066		$canvas-> fill_ellipse( @cpt[0..1], $rad*2-$drad, $rad*2-$drad);
2067		$canvas-> color( $clr[0]);
2068	} else {
2069		if ($self->{prelight}) {
2070			$canvas-> color( $prelight );
2071			$canvas-> fill_ellipse( @cpt[0..1], $rad*2-5, $rad*2-5);
2072		}
2073
2074		my $radx = $rad;
2075		my $da   = 0;
2076		my $dp   = 2;
2077		$canvas-> lineWidth(2);
2078		for my $lw (2..4) {
2079			$canvas-> color( $c3d[1]);
2080			$canvas-> arc( @cpt[0..1], $cpt[2]-$dp, $cpt[3]-$dp, 65 + $da, 235 - $da);
2081			$canvas-> color( $c3d[0]);
2082			$canvas-> arc( @cpt[0..1], $cpt[2]-$dp, $cpt[3]-$dp, 255 + $da, 405 - $da);
2083			$radx -= 100;
2084			$da += 20;
2085			$dp++;
2086			last if $radx < 0;
2087		}
2088		$canvas-> lineWidth(0);
2089		$canvas-> color( $clr[0]);
2090		$canvas-> ellipse( @cpt);
2091	}
2092
2093	if ( $self-> {stdPointer}) {
2094		my $dev = $range * 0.03;
2095		my @j = (
2096			$self-> offset2pt( @cpt[0,1], $self-> {value}, $rad * 0.8),
2097			$self-> offset2pt( @cpt[0,1], $self-> {value} + $dev, $rad * 0.6),
2098			$self-> offset2pt( @cpt[0,1], $self-> {value} - $dev, $rad * 0.6),
2099		);
2100		$self-> fillpoly( \@j);
2101	} else {
2102		my @cxt = ( $self-> offset2pt( @cpt[0,1], $self-> {value}, $rad - 10), 4, 4);
2103		my $knob = $::application->uiScaling * 3;
2104		$canvas-> lineWidth(2);
2105		$canvas-> color( $c3d[0]);
2106		$canvas-> arc( @cxt[0..1], $knob, $knob, 65, 235);
2107		$canvas-> color( $c3d[1]);
2108		$canvas-> arc( @cxt[0..1], $knob, $knob, 255, 405);
2109		$canvas-> lineWidth(0);
2110	}
2111AFTER_DIAL:
2112	$canvas-> color( $clr[0]);
2113
2114	if ( $self-> {show_scale} && !defined $self-> {singlePaint}) {
2115		my $i;
2116		for ( $i = 0; $i < scalar @{$tval}; $i++) {
2117			my $r = $rad + 3 + $$tlen[ $i];
2118			my ( $cos, $sin) = $self-> offset2data( $$tval[$i]);
2119			$canvas-> line( $self-> offset2pt( @cpt[0,1], $$tval[$i], $rad + 3),
2120				$cpt[0] + $r * $cos, $cpt[1] + $r * $sin
2121			) if $$tlen[ $i];
2122			$r += 3;
2123			if ( defined $$ttxt[ $i]) {
2124				my $y = $cpt[1] + $r * $sin - $fh / 2 * ( 1 - $sin);
2125				my $x = $cpt[0] + $r * $cos -
2126					( 1 - $cos) *
2127					$canvas-> get_text_width( $$ttxt[ $i], 1) / 2;
2128				$canvas-> text_shape_out( $$ttxt[ $i], $x, $y);
2129			}
2130		}
2131	}
2132
2133	my $ttw = $canvas-> get_text_width( $self-> {string}, 1);
2134	$canvas-> text_shape_out( $self-> {string}, ( $size[0] - $ttw) / 2, $self->{valueY});
2135	return if defined $self-> {singlePaint};
2136
2137	my $text = $self->text;
2138	$text =~ s/\~//;
2139	$ttw = $canvas-> get_text_width( $text, 1);
2140	$canvas-> draw_text( $self->text,
2141		( $size[0] - $ttw) / 2, $self->{textY},
2142		( $size[0] + $ttw) / 2, $self->{textY} + $fh,
2143		dt::DrawMnemonic|dt::NoWordWrap|dt::Default)
2144			if $self->{show_text};
2145
2146	if ( $self-> {buttons}) {
2147		my $s = $self-> {pressState};
2148		my @cbd = reverse @c3d;
2149		my $at  = 0;
2150		$at = 1, @cbd = reverse @cbd if $s & 1;
2151
2152		$canvas-> rect3d(
2153			$self-> { butt1X}, $self-> { butt1Y}, $self-> { butt1X} + $bw,
2154			$self-> { butt1Y} + $bw, 1, @cbd, $clr[1]
2155		);
2156		$canvas-> line(
2157			$self-> { butt1X} + 2 + $at, $self-> { butt1Y} + $bw / 2 - $at,
2158			$self-> { butt1X} - 2 + + $bw + $at, $self-> {butt1Y} + $bw / 2 - $at
2159		);
2160
2161		@cbd = reverse @c3d; $at = 0;
2162		$at = 1, @cbd = reverse @cbd if $s & 2;
2163		$canvas-> rect3d(
2164			$self-> { butt2X}, $self-> { butt1Y}, $self-> { butt2X} + $bw,
2165			$self-> { butt1Y} + $bw, 1, @cbd, $clr[1]
2166		);
2167		$canvas-> line(
2168			$self-> { butt2X} + 2 + $at, $self-> { butt1Y} + $bw / 2 - $at,
2169			$self-> { butt2X} - 2 + + $bw + $at, $self-> {butt1Y} + $bw / 2 - $at
2170		);
2171		$canvas-> line(
2172			$self-> { butt2X} + $bw / 2 + $at, $self-> { butt1Y} + 2 - $at,
2173			$self-> { butt2X} + $bw / 2 + $at, $self-> { butt1Y} - 2 - $at + $bw
2174		);
2175	}
2176
2177	$canvas-> rect_focus(
2178		( $size[0] - $ttw) / 2 - 1, $self->{textY} - 1,
2179		( $size[0] + $ttw) / 2 + 1, $self->{textY} + $fh + 1
2180	) if $self->{show_text} && $self-> focused && ( length( $self-> text) > 0);
2181}
2182
2183sub on_keydown
2184{
2185	my ( $self, $code, $key, $mod) = @_;
2186	return if $self-> {readOnly};
2187	if ( $key == kb::Home || $key == kb::PgUp) {
2188		$self-> value( $self-> {min});
2189		$self-> clear_event;
2190		return;
2191	}
2192	if ( $key == kb::End || $key == kb::PgDn) {
2193		$self-> value( $self-> {max});
2194		$self-> clear_event;
2195		return;
2196	}
2197	if ( $key == kb::Left || $key == kb::Right || $key == kb::Up || $key == kb::Down) {
2198		my $s = $self-> {step};
2199		$self-> clear_event;
2200		$self-> set_next_value(( $key == kb::Left || $key == kb::Down) ? -$s : $s);
2201	}
2202}
2203
2204sub on_translateaccel
2205{
2206	my ( $self, $code, $key, $mod) = @_;
2207	if (
2208		defined $self-> {accel} &&
2209		($key == kb::NoKey) &&
2210		lc chr $code eq $self-> { accel}
2211	) {
2212		$self-> clear_event;
2213		$self-> select;
2214	}
2215}
2216
2217sub xy2val
2218{
2219	my ( $self, $x, $y) = @_;
2220	$x -= $self-> {circX};
2221	$y -= $self-> {circY};
2222	my $a  = atan2( $y, $x);
2223	my $pi = atan2( 0, -1);
2224	$a += $pi / 2;
2225	$a += $pi * 2 if $a < 0;
2226	$a = $self-> {min} + abs( $self-> {max} - $self-> {min}) * ( $pi * 1.75 - $a) * 2 / ( 3 * $pi);
2227	my $s = $self-> {step};
2228	$a = int( $a) if int( $s) - $s == 0;
2229	my $inCircle = ( abs($x) < $self-> {radius} + 3 and abs($y) < $self-> {radius} + 3);
2230	return $a, $inCircle;
2231}
2232
2233sub on_mousedown
2234{
2235	my ( $self, $btn, $mod, $x, $y) = @_;
2236	return if $self-> {readOnly};
2237	return if $self-> {mouseTransaction};
2238	return if $btn != mb::Left;
2239	my @butt = (
2240		$self-> {butt1X}, $self-> {butt1Y},
2241		$self-> {butt2X}, $self-> {butt1X} + $self->buttonWidth,
2242		$self-> {butt1Y} + $self->buttonWidth, $self-> {butt2X} + $self->buttonWidth
2243	);
2244	if ( $self-> {buttons} and $y >= $butt[1] and $y < $butt[4]) {
2245		if ( $x >= $butt[0] and $x < $butt[3]) {
2246			$self-> {pressState} = 1;
2247			$self-> invalidate_rect( @butt[0..1], $butt[3] + 1, $butt[4] + 1);
2248		}
2249		if ( $x >= $butt[2] and $x < $butt[5]) {
2250			$self-> {pressState} = 2;
2251			$self-> invalidate_rect( $butt[2], $butt[1], $butt[5] + 1, $butt[4] + 1);
2252		}
2253		if ( $self-> {pressState} > 0) {
2254			$self-> {mouseTransaction} = $self-> {pressState};
2255			$self-> update_view;
2256			$self-> capture(1);
2257			$self-> scroll_timer_start;
2258			$self-> scroll_timer_semaphore(0);
2259			$self-> value( $self-> value +
2260				$self-> step * (($self-> {pressState} == 1) ? -1 : 1));
2261			return;
2262		}
2263	}
2264	return unless $self->{show_dial};
2265
2266	my ( $val, $inCircle) = $self-> xy2val( $x, $y);
2267	return unless $inCircle;
2268	$self-> {mouseTransaction} = 3;
2269	$self-> capture(1);
2270	$self-> value( $val);
2271	$self-> clear_event;
2272}
2273
2274sub on_mouseup
2275{
2276	my ( $self, $btn, $mod, $x, $y) = @_;
2277	return if $btn != mb::Left;
2278	return unless $self-> {mouseTransaction};
2279	my @butt = (
2280		$self-> {butt1X}, $self-> {butt1Y}, $self-> {butt2X},
2281		$self-> {butt1X} + $self->buttonWidth, $self-> {butt1Y} + $self->buttonWidth,
2282		$self-> {butt2X} + $self->buttonWidth
2283	);
2284	$self-> scroll_timer_stop;
2285	$self-> {pressState} = 0;
2286	if ( $self-> {mouseTransaction} == 1) {
2287		$self-> invalidate_rect( @butt[0..1], $butt[3] + 1, $butt[4] + 1);
2288		$self-> update_view;
2289	}
2290	if ( $self-> {mouseTransaction} == 2) {
2291		$self-> invalidate_rect( $butt[2], $butt[1], $butt[5] + 1, $butt[4] + 1);
2292		$self-> update_view;
2293	}
2294	my $mt = $self-> {mouseTransaction};
2295	$self-> {mouseTransaction} = undef;
2296	$self-> capture(0);
2297	$self-> notify( 'Change') if $mt == 3 && !$self-> {autoTrack};
2298}
2299
2300sub on_mousemove
2301{
2302	my ( $self, $mod, $x, $y) = @_;
2303	unless ($self-> {mouseTransaction}) {
2304		if ( $self-> enabled ) {
2305			my ( undef, $prelight) = $self-> xy2val( $x, $y);
2306			if (($prelight // 0) != ($self->{prelight} // 0)) {
2307				$self->{prelight} = $prelight;
2308				$self->repaint_circle;
2309			}
2310		}
2311		return;
2312	}
2313	if ( $self-> {mouseTransaction} == 3) {
2314		my $ov = $self-> {value};
2315		$self-> {suppressNotify} = 1 unless $self-> {autoTrack};
2316		$self-> value( $self-> xy2val( $x, $y));
2317		$self-> {suppressNotify} = 0;
2318		$self-> notify(q(Track)) if !$self-> {autoTrack} && $ov != $self-> {value};
2319	} elsif ( $self-> {pressState} > 0) {
2320		$self-> scroll_timer_start unless $self-> scroll_timer_active;
2321		return unless $self-> scroll_timer_semaphore;
2322		$self-> scroll_timer_semaphore(0);
2323		$self-> value( $self-> value +
2324			$self-> step * (( $self-> {mouseTransaction} == 1) ? -1 : 1));
2325	} else {
2326		$self-> scroll_timer_stop;
2327	}
2328}
2329
2330sub on_mouseleave
2331{
2332	my $self = shift;
2333	$self-> repaint_circle if defined( delete $self->{prelight} );
2334}
2335
2336sub on_mouseclick
2337{
2338	my $self = shift;
2339	$self-> clear_event;
2340	return unless pop;
2341	$self-> clear_event unless $self-> notify( "MouseDown", @_);
2342}
2343
2344sub on_size        { $_[0]-> reset; }
2345sub on_fontchanged { $_[0]-> reset; }
2346sub on_enter { $_[0]-> repaint; }
2347sub on_leave { $_[0]-> repaint; }
2348
2349sub on_stringify
2350{
2351	my ( $self, $value, $sref) = @_;
2352	$$sref = $value;
2353	$self-> clear_event;
2354}
2355
2356sub set_buttons
2357{
2358	$_[0]-> {buttons} = $_[1];
2359	$_[0]-> repaint;
2360}
2361
2362sub set_std_pointer
2363{
2364	$_[0]-> {stdPointer} = $_[1];
2365	$_[0]-> repaint;
2366}
2367
2368sub stdPointer  {($#_)?$_[0]-> set_std_pointer ($_[1]):return $_[0]-> {stdPointer};}
2369sub buttons     {($#_)?$_[0]-> set_buttons     ($_[1]):return $_[0]-> {buttons};}
2370
2371sub repaint_circle
2372{
2373	my $self = shift;
2374	$self-> {singlePaint} = 1;
2375	my $radius = $self->{radius} // 0;
2376	my @clip = (
2377		int( $self-> {circX} - $radius),
2378		int( $self-> {circY} - $radius),
2379		int( $self-> {circX} + $radius),
2380		int( $self-> {circY} + $radius),
2381	);
2382	$self-> {expectedClip} = \@clip;
2383	$self-> invalidate_rect( @clip[0..1], $clip[2]+1, $clip[3]+1);
2384	$self-> update_view;
2385	$self-> {singlePaint} = undef;
2386}
2387
2388sub value
2389{
2390	return $_[0]-> {value} unless $#_;
2391	my ( $self, $value) = @_;
2392	my ( $min, $max) = ( $self-> {min}, $self-> {max});
2393	my $old = $self-> {value};
2394	$value = $min if $value < $min;
2395	$value = $max if $value > $max;
2396	if ( $self-> {snap}) {
2397		my ( $minDist, $thatVal, $i) = ( abs( $min - $max));
2398		my $tval = $self-> {tickVal};
2399		for ( $i = 0; $i < scalar @{$tval}; $i++) {
2400			my $j = $$tval[ $i];
2401			$minDist = abs(($thatVal = $j) - $value) if abs( $j - $value) < $minDist;
2402		}
2403		$value = $thatVal if defined $thatVal;
2404	} elsif ( $self-> {step} != 0 ) {
2405		$value = int ( $value / $self-> {step} ) * $self-> {step};
2406	}
2407	return if $old == $value;
2408
2409	$self-> {value} = $value;
2410	$self-> notify(q(Stringify), $value, \$self-> {string});
2411	$self-> repaint_circle;
2412	$self-> notify(q(Change)) unless $self-> {suppressNotify};
2413}
2414
2415sub buttonWidth
2416{
2417	return $_[0]->{buttonWidth} unless $#_;
2418	my ( $self, $bw) = @_;
2419	$bw = 1 if $bw < 1;
2420	return if $bw == $self-> {buttonWidth};
2421	$self-> {buttonWidth} = $bw;
2422	$self-> repaint;
2423}
2424
2425package Prima::ProgressBar;
2426use vars qw(@ISA);
2427@ISA = qw(Prima::Widget);
2428
2429my $TIMER_SILENT_PERIOD = 2000;
2430my $TIMER_ACTIVE_PERIOD = 10;
2431my $TAB_STEP            = 10;
2432my $INDENT              = 1;
2433my $USE_ANIMATION;
2434
2435sub profile_default
2436{
2437	return {
2438		%{$_[ 0]-> SUPER::profile_default},
2439		buffered  => 1,
2440		color     => cl::Green,
2441		max       => 100,
2442		min       => 0,
2443		value     => 0,
2444	}
2445}
2446
2447sub init
2448{
2449	my $self = shift;
2450	$self->{$_} = 0 for qw( value min max );
2451	$self->{cache} = {
2452		size    => [0,0],
2453	};
2454	$self->{tabmode} = 'silent';
2455	$self->{tabpos}  = 0;
2456	my %profile = $self-> SUPER::init(@_);
2457	$self->$_($profile{$_}) for qw( min max value);
2458
2459	$USE_ANIMATION //= $self->can_draw_alpha;
2460
2461	$self->insert( 'Prima::Timer' =>
2462		name        => 'Timer',
2463		delegations => ['Tick'],
2464	) if $USE_ANIMATION;
2465	$self-> next_tick if $self-> visible;
2466
2467	return %profile;
2468}
2469
2470sub mask2icon
2471{
2472	my ( $mask, $color ) = @_;
2473	my $bits = Prima::Image->new(
2474		size => [ $mask-> size ],
2475		type => im::Byte,
2476		backColor => $color,
2477	);
2478	$bits-> clear;
2479	my $icon = Prima::Icon-> create_combined($bits, $mask);
2480	$icon->premultiply_alpha;
2481	return $icon;
2482}
2483
2484sub create_tab
2485{
2486	my ( $self, $x, $y ) = @_;
2487
2488	my $tab_mask = Prima::Image->new(
2489		size      => [ int($x / 5 + .5), 1 ],
2490		type      => im::Byte,
2491		backColor => cl::White,
2492	);
2493	$tab_mask-> clear;
2494	$tab_mask-> put_image(0,0,$tab_mask,rop::SrcOut | rop::DstAlpha | ( 128 << rop::DstAlphaShift ) );
2495
2496	my $tabend_mask = Prima::Image->new(
2497		size => [ $y * 2, 1 ],
2498		type => im::Byte,
2499		color => cl::Black,
2500	);
2501	$tabend_mask->new_gradient(
2502		palette => [cl::Black, cl::White, cl::Black],
2503	)->bar(0, 0, $y * 2, 1, 1);
2504	my $tabend = mask2icon( $tabend_mask, cl::Black );
2505
2506	$tab_mask-> put_image_indirect( $tabend_mask, 0, 0, $y, 0, $y, 1, $y, 1, rop::SrcOver | rop::ConstantColor | rop::Premultiply);
2507	$tab_mask-> put_image_indirect( $tabend_mask, $tab_mask-> width - $y, 0, 0, 0, $y, 1, $y, 1, rop::SrcOver | rop::ConstantColor | rop::Premultiply);
2508
2509	$self->{cache}->{tab}  = mask2icon( $tab_mask, cl::White )-> bitmap;
2510	$self->{cache}->{tabx} = $tab_mask-> width;
2511}
2512
2513sub recalc_images
2514{
2515	my ( $self, $x, $y ) = @_;
2516
2517	$x ||= 1;
2518	$y ||= 1;
2519	my $cache = $self->{cache};
2520	return if $cache->{size}->[0] == $x && $cache->{size}->[1] == $y;
2521
2522	my $recalc_x = $cache->{size}->[1] != $y;
2523	if ( !$recalc_x && $cache->{size}->[0] != $x ) {
2524		my $tabx = $x / 5;
2525		my $diff = abs( $cache->{tabx} - $tabx ) / $tabx;
2526		$recalc_x = 1 if $diff < 0.8 || $diff > 1.2;
2527	}
2528
2529	$self->create_tab( $x, $y ) if $recalc_x;
2530}
2531
2532sub next_tick
2533{
2534	my $self = shift;
2535
2536	return unless $USE_ANIMATION;
2537
2538	my $timer = $self-> Timer;
2539	if ( $self->{tabmode} eq 'silent' ) {
2540		if ( $timer-> get_active ) {
2541			$self->{tabmode} = 'show';
2542			$self->{tabpos}  = - $self->{cache}->{tabx};
2543			$timer->timeout( $TIMER_ACTIVE_PERIOD );
2544		} else {
2545			$timer->timeout( $TIMER_SILENT_PERIOD );
2546		}
2547		$timer->start;
2548	} elsif ( $self->{tabpos} < $self-> width ) {
2549		$self->{tabpos} += $TAB_STEP;
2550		$self->repaint;
2551	} else {
2552		$self->{tabmode} = 'silent';
2553		$timer->timeout( $TIMER_SILENT_PERIOD );
2554		$timer->start;
2555		$self->repaint;
2556	}
2557}
2558
2559sub on_size
2560{
2561	my ( $self, $ox, $oy, $x, $y ) = @_;
2562	$self->recalc_images( $x, $y );
2563}
2564
2565sub on_hide
2566{
2567	my $self = shift;
2568	$self->Timer1->stop;
2569	$self->{tabmode} = 'silent';
2570}
2571
2572sub on_show
2573{
2574	shift->next_tick;
2575}
2576
2577sub on_paint
2578{
2579	my ($self,$canvas) = @_;
2580
2581	my ($xa1, $xa2, $xb1, $xb2, $y1, $y2);
2582
2583	my @sz = $self-> size;
2584	my $indent = $INDENT;
2585	my $range  = $self->{max} - $self->{min};
2586	$y1 = $indent;
2587	$y2 = $sz[1] - 1;
2588	if ( $self->{value} == $self->{min} || $sz[0] == 0 || $sz[1] == 0 || $range == 0) {
2589		$xa1 = $xa2 = -1;
2590	} else {
2591		$xa1 = $indent;
2592		$xa2 = ( $self->{value} == $self->{max} ) ?
2593			$sz[0] - $indent :
2594			(( $sz[0] - $indent * 2 ) * $self->{value} / $range + $indent);
2595	}
2596	if ( $self->{value} == $self->{max} || $sz[0] == 0 || $sz[1] == 0 || $range == 0) {
2597		$xb1 = $xb2 = -1;
2598	} else {
2599		$xb1 = ( $xa2 < 0 ) ? $indent : ( $xa2 + 1 );
2600		$xb2 = $sz[0] - $indent;
2601	}
2602
2603	$canvas-> new_gradient(
2604		palette => [ cl::Black, $self->color, cl::White ],
2605		poly    => [ 0, 0.25, 1, 0.75, 0.75, 0.25 + 0.5 * 0.75 ],
2606	)-> bar( $xa1, $y1, $xa2, $y2 ) if $xa1 > 0;
2607
2608	$canvas-> new_gradient(
2609		palette => [ cl::Black, cl::Gray, cl::White ],
2610		poly    => [ 0, 0.25, 1, 0.75, 0.75, 0.25 + 0.5 * 0.75 ],
2611	)-> bar( $xb1, $y1, $xb2, $y2 ) if $xb1 > 0;
2612
2613	$canvas-> color(cl::Gray);
2614	$canvas-> rectangle( 0, 0, $sz[0] - 1, $sz[1] - 1);
2615
2616	if ( $self->{tabmode} eq 'show' && $xa1 > 0) {
2617		$canvas->clipRect(0, 0, $xa2, $sz[1]);
2618		$canvas->put_image( $self->{tabpos}, $_, $self->{cache}->{tab}, rop::SrcOver ) for 0 .. $sz[1];
2619	}
2620}
2621
2622sub set_bounds
2623{
2624	my ( $self, $min, $max) = @_;
2625	$max = $min if $max < $min;
2626	( $self-> { min}, $self-> { max}) = ( $min, $max);
2627	$self-> value( $max) if $self-> {value} > $max;
2628	$self-> value( $min) if $self-> {value} < $min;
2629}
2630
2631sub value
2632{
2633	return $_[0]-> {value} unless $#_;
2634	my $v = $_[1] < $_[0]-> {min} ? $_[0]-> {min} : ($_[1] > $_[0]-> {max} ? $_[0]-> {max} : $_[1]);
2635	return if $v == $_[0]->{value};
2636	$_[0]-> {value} = $v;
2637	$_[0]-> repaint;
2638}
2639
2640sub min       {($#_)?$_[0]-> set_bounds($_[1], $_[0]-> {'max'})  : return $_[0]-> {min};}
2641sub max       {($#_)?$_[0]-> set_bounds($_[0]-> {'min'}, $_[1])  : return $_[0]-> {max};}
2642
2643sub Timer_Tick { shift-> next_tick }
2644
26451;
2646
2647=pod
2648
2649=head1 NAME
2650
2651Prima::Sliders - sliding bars, spin buttons and input lines, dial widget etc.
2652
2653=head1 DESCRIPTION
2654
2655The module is a set of widget classes, with one
2656common property; - all of these provide input and / or output of an integer value.
2657This property unites the following set of class hierarchies:
2658
2659	Prima::AbstractSpinButton
2660		Prima::SpinButton
2661		Prima::AltSpinButton
2662
2663	Prima::SpinEdit
2664
2665	Prima::Gauge
2666	Prima::PrigressBar
2667
2668	Prima::AbstractSlider
2669		Prima::Slider
2670		Prima::CircularSlider
2671
2672=head1 Prima::AbstractSpinButton
2673
2674Provides a generic interface to spin-button class functionality, which includes
2675range definition properties and events. Neither C<Prima::AbstractSpinButton>, nor
2676its descendants store the integer value. These provide a mere possibility for
2677the user to send incrementing or decrementing commands.
2678
2679The class is not usable directly.
2680
2681=head2 Properties
2682
2683=over
2684
2685=item state INTEGER
2686
2687Internal state, reflects widget modal state, for example,
2688is set to non-zero when the user performs a mouse drag action. The exact meaning of C<state>
2689is defined in the descendant classes.
2690
2691=back
2692
2693=head2 Events
2694
2695=over
2696
2697=item Increment DELTA
2698
2699Called when the user presses a part of a widget that is responsible for
2700incrementing or decrementing commands. DELTA is an integer value,
2701indicating how the associated value must be modified.
2702
2703=item TrackEnd
2704
2705Called when the user finished the mouse transaction.
2706
2707=back
2708
2709=head1 Prima::SpinButton
2710
2711=for podview <img src="spinbutton.gif">
2712
2713=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/spinbutton.gif">
2714
2715A rectangular spin button, consists of three parts, divided horizontally.
2716The upper and the lower parts are push-buttons associated with singular
2717increment and decrement commands. The middle part, when dragged by mouse,
2718fires C<Increment> events with delta value, based on a vertical position
2719of the mouse pointer.
2720
2721=head1 Prima::AltSpinButton
2722
2723=for podview <img src="altspinbutton.gif">
2724
2725=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/altspinbutton.gif">
2726
2727A rectangular spin button, consists of two push-buttons, associated
2728with singular increment and decrement command. Comparing to C<Prima::SpinButton>,
2729the class is less functional but has more stylish look.
2730
2731=head1 Prima::SpinEdit
2732
2733=for podview <img src="spinedit.gif">
2734
2735=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/spinedit.gif">
2736
2737The class is a numerical input line, paired with a spin button.
2738The input line value can be change three ways - either as a direct
2739traditional keyboard input, or as spin button actions, or as mouse
2740wheel response. The class provides value storage and range
2741selection properties.
2742
2743=head2 Properties
2744
2745=over
2746
2747=item circulate BOOLEAN
2748
2749Selects the value modification rule when the increment or decrement
2750action hits the range. If 1, the value is changed to the opposite limit
2751value ( for example, if value is 100 in range 2-100, and the user
2752clicks on 'increment' button, the value is changed to 2 ).
2753
2754If 0, the value does not change.
2755
2756Default value: 0
2757
2758=item editClass STRING
2759
2760Assigns an input line class.
2761
2762Create-only property.
2763
2764Default value: C<Prima::InputLine>
2765
2766=item editDelegations ARRAY
2767
2768Assigns the input line list of delegated notifications.
2769
2770Create-only property.
2771
2772=item editProfile HASH
2773
2774Assigns hash of properties, passed to the input line during the creation.
2775
2776Create-only property.
2777
2778=item max INTEGER
2779
2780Sets the upper limit for C<value>.
2781
2782Default value: 100.
2783
2784=item min INTEGER
2785
2786Sets the lower limit for C<value>.
2787
2788Default value: 0
2789
2790=item pageStep INTEGER
2791
2792Determines the multiplication factor for incrementing/decrementing
2793actions of the mouse wheel.
2794
2795Default value: 10
2796
2797=item spinClass STRING
2798
2799Assigns a spin-button class.
2800
2801Create-only property.
2802
2803Default value: C<Prima::AltSpinButton>
2804
2805=item spinProfile ARRAY
2806
2807Assigns the spin-button list of delegated notifications.
2808
2809Create-only property.
2810
2811=item spinDelegations HASH
2812
2813Assigns hash of properties, passed to the spin-button during the creation.
2814
2815Create-only property.
2816
2817=item step INTEGER
2818
2819Determines the multiplication factor for incrementing/decrementing
2820actions of the spin-button.
2821
2822Default value: 1
2823
2824=item value INTEGER
2825
2826Selects integer value in range from C<min> to C<max>, reflected in the input line.
2827
2828Default value: 0.
2829
2830=back
2831
2832=head2 Methods
2833
2834=over
2835
2836=item set_bounds MIN, MAX
2837
2838Simultaneously sets both C<min> and C<max> values.
2839
2840=back
2841
2842=head2 Events
2843
2844=over
2845
2846=item Change
2847
2848Called when C<value> is changed.
2849
2850=back
2851
2852=head1 Prima::Gauge
2853
2854=for podview <img src="gauge.gif">
2855
2856=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/gauge.gif">
2857
2858An output-only widget class, displays a progress bar and an eventual percentage string.
2859Useful as a progress indicator.
2860
2861=head2 Properties
2862
2863=over
2864
2865=item indent INTEGER
2866
2867Selects width of a border around the widget.
2868
2869Default value: 1
2870
2871=item max INTEGER
2872
2873Sets the upper limit for C<value>.
2874
2875Default value: 100.
2876
2877=item min INTEGER
2878
2879Sets the lower limit for C<value>.
2880
2881Default value: 0
2882
2883=item relief INTEGER
2884
2885Selects the style of a border around the widget. Can be one of the
2886following C<gr::XXX> constants:
2887
2888	gr::Sink    - 3d sunken look
2889	gr::Border  - uniform black border
2890	gr::Raise   - 3d risen look
2891
2892Default value: C<gr::Sink>.
2893
2894=item threshold INTEGER
2895
2896Selects the threshold value used to determine if the changes to C<value>
2897are reflected immediately or deferred until the value is changed more
2898significantly. When 0, all calls to C<value> result in an immediate
2899repaint request.
2900
2901Default value: 0
2902
2903=item value INTEGER
2904
2905Selects integer value between C<min> and C<max>, reflected in the progress bar and
2906eventual text.
2907
2908Default value: 0.
2909
2910=item vertical BOOLEAN
2911
2912If 1, the widget is drawn vertically, and the progress bar moves from bottom to top.
2913If 0, the widget is drawn horizontally, and the progress bar moves from left to right.
2914
2915Default value: 0
2916
2917=back
2918
2919=head2 Methods
2920
2921=over
2922
2923=item set_bounds MIN, MAX
2924
2925Simultaneously sets both C<min> and C<max> values.
2926
2927=back
2928
2929=head2 Events
2930
2931=over
2932
2933=item Stringify VALUE, REF
2934
2935Converts integer VALUE into a string format and puts into REF scalar reference.
2936Default stringifying conversion is identical to C<sprintf("%2d%%")> one.
2937
2938=back
2939
2940=head1 Prima::ProgressBar
2941
2942=for podview <img src="progressbar.png">
2943
2944=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/progressbar.png">
2945
2946Displays a progress bar
2947
2948=head2 Properties
2949
2950=over
2951
2952=item max INTEGER
2953
2954Sets the upper limit for C<value>.
2955
2956Default value: 100.
2957
2958=item min INTEGER
2959
2960Sets the lower limit for C<value>.
2961
2962Default value: 0
2963
2964=item value INTEGER
2965
2966Selects integer value between C<min> and C<max>, reflected in the progress bar and
2967eventual text.
2968
2969Default value: 0.
2970
2971=back
2972
2973=head2 Methods
2974
2975=over
2976
2977=item set_bounds MIN, MAX
2978
2979Simultaneously sets both C<min> and C<max> values.
2980
2981=back
2982
2983=head1 Prima::AbstractSlider
2984
2985The class provides basic functionality of a sliding bar, equipped with
2986tick marks. Tick marks are supposed to be drawn alongside the main sliding axis or
2987circle and provide visual feedback for the user.
2988
2989The class is not usable directly.
2990
2991=head2 Properties
2992
2993=over
2994
2995=item autoTrack BOOLEAN
2996
2997A boolean flag, selects the way notifications execute when the user mouse-drags
2998the sliding bar. If 1, C<Change> notification is executed as soon as C<value>
2999is changed. If 0, C<Change> is deferred until the user finished the mouse drag;
3000instead, C<Track> notification is executed when the bar is moved.
3001
3002This property can be used when the action, called on C<Change> performs very
3003slow, so the eventual fast mouse interactions would not thrash down the program.
3004
3005Default value: 1
3006
3007=item increment INTEGER
3008
3009A step range value, used in C<scheme> for marking the key ticks.
3010See L<scheme> for details.
3011
3012Default value: 10
3013
3014=item max INTEGER
3015
3016Sets the upper limit for C<value>.
3017
3018Default value: 100.
3019
3020=item min INTEGER
3021
3022Sets the lower limit for C<value>.
3023
3024Default value: 0
3025
3026=item readOnly BOOLEAN
3027
3028If 1, the use cannot change the value by moving the bar or otherwise.
3029
3030Default value: 0
3031
3032=item ticks ARRAY
3033
3034Selects the tick marks representation along the sliding axis or circle.
3035ARRAY consists of hashes, each for one tick. The hash must contain
3036at least C<value> key, with integer value. The two additional keys,
3037C<height> and C<text>, select the height of a tick mark in pixels
3038and the text drawn near the mark, correspondingly.
3039
3040If ARRAY is C<undef>, no ticks are drawn.
3041
3042=item scheme INTEGER
3043
3044C<scheme> is a property, that creates a set of tick marks
3045using one of the predefined scale designs, selected by C<ss::XXX> constants.
3046Each constant produces different scale; some make use of C<increment> integer
3047property, which selects a step by which the additional
3048text marks are drawn. As an example, C<ss::Thermometer> design with
3049default C<min>, C<max>, and C<increment> values would look like that:
3050
3051	0   10   20        100
3052	|    |    |          |
3053	|||||||||||||||....|||
3054
3055The module defines the following constants:
3056
3057	ss::Axis           - 5 minor ticks per increment
3058	ss::Gauge          - 1 tick per increment
3059	ss::StdMinMax      - 2 ticks at the ends of the bar
3060	ss::Thermometer    - 10 minor ticks per increment, longer text ticks
3061
3062When C<tick> property is set, C<scheme> is reset to C<undef>.
3063
3064=item snap BOOLEAN
3065
3066If 1, C<value> cannot accept values that are not on the tick scale.
3067When set such a value, it is rounded to the closest tick mark.
3068If 0, C<value> can accept any integer value in range from C<min> to C<max>.
3069
3070Default value: 0
3071
3072=item step INTEGER
3073
3074Integer delta for singular increment / decrement commands and
3075a threshold for C<value> when C<snap> value is 0.
3076
3077Default value: 1
3078
3079=item value INTEGER
3080
3081Selects integer value between C<min> and C<max> and the corresponding sliding bar
3082position.
3083
3084Default value: 0.
3085
3086=back
3087
3088=head2 Events
3089
3090=over
3091
3092=item Change
3093
3094Called when C<value> value is changed, with one exception:
3095if the user moves the sliding bar while C<autoTrack> is 0, C<Track>
3096notification is called instead.
3097
3098=item Track
3099
3100Called when the user moves the sliding bar while C<autoTrack> value is 0;
3101this notification is a substitute to C<Change>.
3102
3103=back
3104
3105=head1 Prima::Slider
3106
3107=for podview <img src="slider.gif">
3108
3109=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/slider.gif">
3110
3111Presents a linear sliding bar, movable along a linear shaft.
3112
3113=head2 Properties
3114
3115=over
3116
3117=item borderWidth INTEGER
3118
3119In horizontal mode, sets extra margin space between the slider line and
3120the widget boundaries. Can be used for fine tuning of displaying text
3121labels from <ticks()>, where the default spacing (0) or spacing procedure
3122(drop overlapping labels) is not enough.
3123
3124=item ribbonStrip BOOLEAN
3125
3126If 1, the parts of shaft are painted with different colors, to increase
3127visual feedback. If 0, the shaft is painted with single default background color.
3128
3129Default value: 0
3130
3131=item shaftBreadth INTEGER
3132
3133Breadth of the shaft in pixels.
3134
3135Default value: 6
3136
3137=item tickAlign INTEGER
3138
3139One of C<tka::XXX> constants, that correspond to the situation of tick marks:
3140
3141	tka::Normal        - ticks are drawn on the left or on the top of the shaft
3142	tka::Alternative   - ticks are drawn on the right or at the bottom of the shaft
3143	tka::Dual          - ticks are drawn both ways
3144
3145The ticks orientation ( left or top, right or bottom ) is dependant on C<vertical>
3146property value.
3147
3148Default value: C<tka::Normal>
3149
3150=item vertical BOOLEAN
3151
3152If 1, the widget is drawn vertically, and the slider moves from bottom to top.
3153If 0, the widget is drawn horizontally, and the slider moves from left to right.
3154
3155Default value: 0
3156
3157=back
3158
3159=head2 Methods
3160
3161=over
3162
3163=item pos2info X, Y
3164
3165Translates integer coordinates pair ( X, Y ) into the value corresponding to the scale,
3166and returns three scalars:
3167
3168=over
3169
3170=item info INTEGER
3171
3172If C<undef>, the user-driven positioning is not possible ( C<min> equals to C<max> ).
3173
3174If 1, the point is located on the slider.
3175
3176If 0, the point is outside the slider.
3177
3178=item value INTEGER
3179
3180If C<info> is 0 or 1, contains the corresponding C<value>.
3181
3182=item aperture INTEGER
3183
3184Offset in pixels along the shaft axis.
3185
3186=back
3187
3188=back
3189
3190=head1 Prima::CircularSlider
3191
3192=for podview <img src="circularslider.gif">
3193
3194=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/circularslider.gif">
3195
3196Presents a slider widget with the dial and two increment / decrement buttons.
3197The tick marks are drawn around the perimeter of the dial; current value
3198is displayed in the center of the dial.
3199
3200=head2 Properties
3201
3202=over
3203
3204=item buttons BOOLEAN
3205
3206If 1, the increment / decrement buttons are shown at the bottom of the dial,
3207and the user can change the value either by the dial or by the buttons.
3208If 0, the buttons are not shown.
3209
3210Default values: 0
3211
3212=item stdPointer BOOLEAN
3213
3214Determines the style of a value indicator ( pointer ) on the dial.
3215If 1, it is drawn as a black triangular mark.
3216If 0, it is drawn as a small circular knob.
3217
3218Default value: 0
3219
3220=back
3221
3222=head2 Methods
3223
3224=over
3225
3226=item offset2data VALUE
3227
3228Converts integer value in range from C<min> to C<max> into
3229the corresponding angle, and return two real values:
3230cosine and sine of the angle.
3231
3232=item offset2pt X, Y, VALUE, RADIUS
3233
3234Converts integer value in range from C<min> to C<max> into the
3235point coordinates, with the RADIUS and dial center coordinates
3236X and Y. Return the calculated point coordinates
3237as two integers in (X,Y) format.
3238
3239=item xy2val X, Y
3240
3241Converts widget coordinates X and Y into value in range from C<min>
3242to C<max>, and return two scalars: the value and the boolean flag,
3243which is set to 1 if the (X,Y) point is inside the dial circle,
3244and 0 otherwise.
3245
3246=back
3247
3248=head2 Events
3249
3250=over
3251
3252=item Stringify VALUE, REF
3253
3254Converts integer VALUE into a string format and puts into REF scalar reference.
3255The resulting string is displayed in the center of the dial.
3256
3257Default conversion routine simply copies VALUE to REF as is.
3258
3259=back
3260
3261=head1 AUTHOR
3262
3263Dmitry Karasik, E<lt>dmitry@karasik.eu.orgE<gt>,
3264Anton Berezin E<lt>tobez@tobez.orgE<gt>.
3265
3266=head1 SEE ALSO
3267
3268L<Prima>, F<examples/fontdlg.pl>
3269
3270=cut
3271