TEACH RC_DIAL Aaron Sloman August 2000 Updated 26 Aug 2002 LIB RC_DIAL This is a package for creating dials with moving pointers, either directly on window objects or automatically formatted using the rc_control_panel tool. The rc_dial procedure uses several other libraries to provide the low level facilities, for which it provides a convenient high level interface. A closely related high level interface is available using the mechanisms described in HELP rc_control_panel Note: this package was extended and generalised in March 2001. It may take a while for all the documentation files referring to dials to be updated. The latest descriptions are in this file and in HELP RC_DIAL which gives more formal and complete specifications of the procedure rc_dial. This file mainly gives examples. CONTENTS - (Use g to access required sections) -- Introduction -- Load libraries and prepare a window -- Example d1 -- . Adding decorations to d1 -- -- . Adding radial marks -- -- . Adding numeric labels round the dial -- -- . Printing text strings as captions -- Making rc_dial add "decorations" automatically -- Examples of manipulation of dials -- Exploring the behaviour of the dial -- EXERCISE -- A rotated dial -- Definition of rc_dial -- Further examples of rc_dial -- WARNING: changing the sign of rc_yscale -- Future plans -- Introduction ------------------------------------------------------- The RCLIB package provides a variety of tools for producing graphical objects which can be manipulated using mouse and/or keyboard and which can interact with programs. Many examples are described in HELP RCLIB The facilities for creating dials are built up from many other facilities provided in RCLIB and combined using the object oriented approach supported by the Pop-11 Objectclass system. In particular, rclib allows various kinds of movable objects including instances of the class rc_opaque mover, which is used as the basis of the moving pointer on a dial. Separately there are procedures for drawing portions of a circle, including the procedure rc_draw_blob_sector, which can draw a sector of a circle. This is used both to draw the dial panel and also the movable pointer. The mixin rc_constrained_rotater extends the facilities of the rc_constrained_mover, by allowing a mover to have a fixed pivot point around which movement is allowed. It also provides mechanisms for converting between the actual orientation of a rotating object, rc_axis(object), and the orientation used to represent a value, namely rc_virtual_axis(object). The librar file LIB rc_constrained_pointer defines a class which inherits from the classes and mixins already mentioned and also from rc_informant. define :class rc_constrained_pointer; is rc_constrained_rotater rc_opaque_rotatable rc_selectable, rc_informant; This is defined and illustrated in TEACH rc_constrained_pointer The facilities defined there are quite complex and messy to use, so the autoloadable procedure rc_dial, described in this file and also in HELP rc_dial, provides a high level interface. This teach file mainly gives examples, whereas the help file gives more formal definitions. Some examples follow. -- Load libraries and prepare a window -------------------------------- ;;; Load relevant libraries uses rclib uses rc_window_object uses rc_dial ;;; Some utilities for use in examples below. ;;; Declare a variable to hold a window object vars win1; ;;; Here's a procedure to start a new window when needed, and assign ;;; it to win1. This uses standard rc_graphic coordinates, with ;;; rc_yscale = -1, so that y increases upwards define ved_win1(); if rc_islive_window_object(win1) then rc_kill_window_object(win1); endif; ;;; create a window object, with default picture coordinates ;;; including origin at the centre rc_new_window_object( "right", "top", 480, 480, {240 240 1 -1}, 'win1') -> win1; enddefine; ;;; Create a window with the following command ved_win1(); ;;; or, in Ved, do: ENTER win1 ;;; A similar procedure which creates win1 with rc_yscale = 1, ;;; so that y increases downwards instead of upwards. define ved_win2(); if rc_islive_window_object(win1) then rc_kill_window_object(win1); endif; ;;; create a window object, with new default coordinate frame rc_new_window_object( "right", "top", 480, 480, {240 240 1 1}, 'win2') -> win1; enddefine; ;;; This can be invoked as ENTER win2 or ved_win2(); ;;; create a new window, with y going up: ved_win1(); -- Example d1 --------------------------------------------------------- ;;; Using the current window, ;;; create a dial, with centre at location (110, 95), with start ;;; orientation 0 degrees, (start edge horizontal to left), angular ;;; sweep 180 degrees (i.e. a semi-circle), value range going from ;;; 0 to 10, default value 5 (half way) with steps of 1, with pointer ;;; length 80, width at base 15, pointer colour 'yellow', background ;;; 'blue' and associated variable d1val, to be updated whenever the ;;; pointer value is changed. vars d1val; vars d1 = rc_dial( 110, 95, 0, 180, {0 10 5 1}, 80, 15, 'yellow', 'blue', "d1val"); ;;; Check that you can move the yellow pointer with mouse (dragging ;;; it with button 1). ;;; As you do so, check the value associated with d1: rc_pointer_value(d1) => ;;; The pointer ranges from 0 to 10, in steps of 1, with an initial ;;; value of 5, as indicated by the fourth argument to rc_dial, ;;; the range vector (defined precisely in HELP rc_dial ;;; Test how the value of the variable d1val also changes as you move ;;; the pointer with the mouse d1val => ;;; You can also change the value of the pointer by program commands ;;; like these, and see how the pointer moves: 3 -> rc_pointer_value(d1); 9 -> rc_pointer_value(d1); ;;; after each assignment check the associated values rc_pointer_value(d1) => d1val => ;;; Altering the variable does not change the pointer location: 6 -> d1val; ;;; Unlike 6 -> rc_pointer_value(d1); ;;; Unfortunately, without using an "active" variable, or a timer, ;;; you can't make the pointer automatically monitor changes ;;; to the variable. To see how to do it with active variables ;;; see HELP active_variables -- . Adding decorations to d1 ;;; Now add some "decorations" to the dial, each type specified ;;; by a list of vectors. (Details explained later.) -- -- . Adding radial marks ;;; add 10 short red marks (at intervals of 18 degrees), and ;;; three longer 'black' marks at intervals of 90 degrees. rc_draw_dial_marks(d1, [{5 18 2 8 'red'} {8 90 2 10 'black'}]); ;;; rc_draw_dial_marks takes a list of vectors, where each vector ;;; specifies marks to be drawn: how far beyond the dial, the ;;; angular gap, the angular width (the marks are segments of a ;;; circle) the radius of each mark, and the colour.) ;;; You can experiment with variants of the above, as long as you ;;; use vectors with the format ;;; {extra-radius angular-gap mark-width mark-length colour} -- -- . Adding numeric labels round the dial ;;; Now add some numeric labels, red ones showing the angle, and ;;; blue ones, closer to the dial showing the pointer value rc_draw_dial_labels(d1, [{42 18 180 -18 'red' '6x13'} {20 18 0 1 'blue' '6x13'}]); ;;; rc_draw_dial_labels takes a list of vectors where each list ;;; specifies some numbers to be drawn around the dial. Each vector ;;; indicates how far out from the dial, the angular interval, the ;;; first number to be displayed, the increment, the colour and the ;;; font. Change the initial value to a decimal number to print ;;; decimals (e.g. 0.0 instead of 0). ;;; Here each vector has the format ;;; {extra-radius angular-gap initial-value increment colour font} ;;; Not all combinations will work well: the formatting is only ;;; approximately correct and uses the following two global variables to ;;; specify horizontal and vertical offsets. The default values shown ;;; may need to be changed for different fonts and for numbers requiring ;;; more digits. ;;; ;;; rc_dial_label_xoffset = -8; ;;; left a bit ;;; rc_dial_label_yoffset = 5; ;;; down a bit (when rc_yscale < 0) ;;; The algorithm for placing numeric values is not optimised to handle ;;; all cases, so numbers some may be offset slightly from ideal ;;; locations no matter what values you give these variables. -- -- . Printing text strings as captions ;;; Now print some captions below (giving vectors with arguments ;;; to be used by rc_print_a_string) ;;; Define two captions in different colours with different fonts: rc_print_dial_captions(d1, [{-100 -20 'Degrees shown in red' 'red' '9x15'} {-80 -40 'Values in blue' 'blue' '9x15'}]); ;;; Each vector has coordinates relative to the (x, y) coordinates ;;; of the dial, a string to be printed, a colour and a font. The ;;; general format is ;;; {relative-location string colour font} ;;; where the relative-location is two numbers -- Making rc_dial add "decorations" automatically --------------------- The above decorations (marks, labels, captions), can be done within the call of rc_dial, by giving rc_dial some extra arguments in lists with keywords to identify their purpose. [MARKS ....] [LABELS ....] [CAPTIONS ....] (The full set of optional extra arguments allowed is defined in HELP RC_DIAL) The lists starting with the keywords "MARKS", "LABELS", "CAPTIONS", each contain one or more vectors. Each vector is used to compute a set of arguments for the appropriate "decoration procedure", namely one of these procedures illustrated above: rc_draw_dial_marks rc_draw_dial_labels rc_print_dial_captions Here is an example of the use of three such lists, each with two vectors, to produce a dial similar to d1, but slightly smaller (pointer length 60 instead of 80) and located in the lower left of the window (if rc_yscale is negative): vars smalldial = rc_dial( -100, -140, 0, 180, {0 18 5 1}, 60, 15, 'yellow', 'blue', [MARKS ;;; {extra radius, angular gap, mark width, length, colour} {5 10 2 8 'red'} {8 90 2 10 'black'}], [LABELS ;;; {extra radius, angular gap, initial value, increment, ;;; colour font} {50 18 180 -18 'red' '6x13'} {25 10 0 1 'blue' '6x13'}], [CAPTIONS ;;; {relative location, string, colour, font} {-100 -20 'Degrees shown in red' 'red' '9x15'} {-80 -40 'Values in blue' 'blue' '10x20'}] ); ;;; Mark and compile that to see the automatic reformatting produced ;;; to fit the smaller pointer length. ;;; See how the value changes as you move the pointer, then ;;; obey this command (ESC d): rc_pointer_value(smalldial) => ;;; see how the pointer changes as you update the value 3 -> rc_pointer_value(smalldial); 10 -> rc_pointer_value(smalldial); 17 -> rc_pointer_value(smalldial); 5 -> rc_pointer_value(smalldial); 0 -> rc_pointer_value(smalldial); ;;; But you cannot push it beyond the limits specified 34 -> rc_pointer_value(smalldial); -3 -> rc_pointer_value(smalldial); ;;; You can also check the angle: ;;; Repeatedly move the pointer with the mouse and check the axis rc_axis(smalldial) => -- Examples of manipulation of dials ---------------------------------- ;;; move the pointer and compare dial settings with pointer value ;;; The following examples use smalldial but you can also try them ;;; with d1 if that dial is still present. ;;; show the pointer moving vars x; for x from 0 to 18 do x -> rc_pointer_value(smalldial); ;;; pause for 20/100 of a second syssleep(20); endfor; -- Exploring the behaviour of the dial -------------------------------- ;;; These examples use the original dial, d1. If necessary go back ;;; and re-create it. Alternatively use the recently created small ;;; dial, by doing this before testing the commands below: smalldial -> d1; ;;; try moving the pointer and see what happens to the stored value ;;; printed out by this command rc_pointer_value(d1) => ;;; Notice how the movements are "stepped" because of the step value ;;; 1 in the range vector (the fifth argument). ;;; Move it under program control, and check the value after each 4-> rc_pointer_value(d1); 6-> rc_pointer_value(d1); 8-> rc_pointer_value(d1); ;;; You can't exceed the limits under program control either: -3 -> rc_pointer_value(d1); rc_pointer_value(d1) => 12 -> rc_pointer_value(d1); 25 -> rc_pointer_value(d1); rc_pointer_value(d1) => ;;; See the stored range information rc_informant_start(d1)=> rc_informant_end(d1)=> rc_informant_step(d1)=> ;;; compare setting the value and setting the angle: 10 -> rc_pointer_value(d1); 2 -> rc_pointer_value(d1); rc_set_axis(d1, 60, true); rc_axis(d1) => rc_pointer_value(d1) => rc_set_axis(d1, 140, true); rc_axis(d1) => rc_pointer_value(d1) => ;;; the angle will have been coerced to fit the value constraint ;;; (i.e. the value increases in steps of 1, angle step 18 degrees) ;;; The dial is an instance of rc_informant, defined in ;;; LIB rc_informant, so the stored value can be accessed thus: rc_informant_value(d1) => ;;; The end of the pointer can also be moved by specifying new ;;; coordinates, though as it is an instance of rc_constrained_mover, ;;; the possible moves are constrained rc_move_to(d1, 0, 140, true); rc_move_to(d1, 500, 240, true); rc_move_to(d1, 100, 10000, true); rc_pointer_value(d1) => -- EXERCISE ----------------------------------------------------------- Copy and edit the above commands to create dials, and try defining some new dials with different locations, orientations, value ranges, colours, etc. E.g. Try doing all that again with a greater pointer width, e.g. 50 instead of 15, or a different initial orientation, e.g. tilted 45 degrees instead of horizontal. The procedures for decorating the dial should all adjust. -- A rotated dial ----------------------------------------------------- ;;; Create a dial tilted 90 degrees to the right, with an angular ;;; sweep of 140 degrees. It will have values ranging from 0 to 14, ;;; and there will be 14 red marks alongside the dial each labelled ;;; with the corresponding integer in blue. vars d90 = rc_dial( 130, -120, 90, 140, ;;; tilt and angular range {0 14 0 1}, ;;; value range specification 60, 15, ;;; pointer length and base width false, false, ;;; default colour and background [MARKS ;;; {extra radius, angular gap, mark width, length, colour} {5 10 2 8 'red'}], [LABELS {20 10 0 1 'blue' '6x13'}], ); rc_pointer_value(d90) => vars x; for x from 0 to 14 do x -> rc_pointer_value(d90); syssleep(30) endfor; ;;; See what happens if you do the above with a tilt angle of ;;; -90 instead, leaving everything else, except the coordinates ;;; of the dial unchanged. The dial should be tilted in the opposite ;;; direction. vars dminus90 = rc_dial( 110, -140, -90, 140, ;;; tilt and angular range {0 14 0 1}, ;;; value range specification 60, 15, ;;; pointer length and base width false, false, ;;; default colour and background [MARKS ;;; {extra radius, angular gap, mark width, length, colour} {5 10 2 8 'red'}], [LABELS {20 10 0 1 'blue' '6x13'}], ); rc_pointer_value(dminus90) => -- Definition of rc_dial ---------------------------------------------- This section duplicates information in HELP RC_DIAL. If there is any conflict the HELP file is likely to be the accurate one. This procedure has nine required arguments and a number of additional optional arguments, including the optional arguments shown above for specifying "decorations". It returns an instance of the class rc_constrained_pointer, which inherits from various mixins used which give it its properties. These are the required arguments: rc_dial( x, y, orient, angwidth, range, len, width, colour, bg) -> pointer; with the following optional arguments (in any combination), explained below: wid, specs, typespec, win, marks, labels, captions Where (x,y) defines the location of the dial in rc_graphic coordinates. orient is the orientation of the "start" edge of the dial. E.g. 0 means that the leading edge of the dial points to the left if rc_yscale is negative or to the right otherwise. angwidth is the "sweep" angle of the dial from the start edge, which is clockwise if y goes up (rc_yscale is negative), otherwise counter-clockwise range is a number or vector of two to four elements, as in rc_slider. The options are interpreted as follows: - A positive number N: The range of values is then 0 to N, with default value = 0. - A vector containing two, three or four numbers: The first two numbers define the beginning and the end of the interval, and the third number, if present, defines the default value. If there is a fourth number it specifies the minimum step value. E.g. {0 100 50 5} specifies a range from 0 to 100, with minimum steps of 5, and an initial value of 50. Numbers can be positive or negative integers or bigintegers or decimals or ddecimals, and the begging can be larger than the end, if required. If the start and stepvalue are both integers, the stored values are always rounded. len, width The length and base width of the pointer shaped like a sector of a circle (almost a triangle). The units are relative to current rc_graphic scales. colour, bg These two specify the pointer colour and the background "dial-face' colour. They may be false, or strings, and "bg" may be the word "background" representing the default background colour of the window. If false the colours used will be defaults, held in these two global variables, unless re-defined for sub-classes. rc_pointer_colour_def = 'grey30'; rc_pointer_bg_def = 'grey80'; The optional arguments are interpreted as follows: wid A word or identifier, whose valof should be updated whenever the pointer value is changed, either using the mouse or by a program command specs A featurespec vector of the type described in HELP FEATURESPEC If provided this can be used to over-ride some of the class defaults in this individual. typespec This should be an objectclass class key, which is used to determine the sort of instance to create. The default is rc_constrained_pointer_key win This should be an instance of rc_window_object, and if not given defaults to rc_current_window_object. marks A list starting with the word "MARKS" containing one or more vectors specifying gradation marks to be drawn around the dial, where each vector has four numbers and a string {extra radius, angular gap, mark width, length, colour} e.g. {5 10 2 8 'red'}], labels, A list starting with the word "LABELS" containing one or more vectors specifying numeric labels to be drawn around the dial, where each vector has four numbers and two strings: {extra radius, angular gap, initial value, increment, colour, font} e.g. {42 18 180 -18 'red' '6x13'} (Not to be confused with the "label" property of a field in rc_control_panel specification.) captions A list starting with the word "CAPTIONS" containing one or more vectors specifying strings to be printed somewhere near the dial, where each vector has two numbers the string to be printed, a string for colour and a string for font. {relative location, string, colour, font} e.g. {-100 -20 'Degrees shown in red' 'red' '9x15'} -- Further examples of rc_dial ---------------------------------------- ;;; start a new window ved_win1(); ;;; Now try a range that goes in the reverse direction, from ;;; 20 (on left) to 0 (on right), with default 5, and no step value, ;;; using a blue pointer, with width (at its base) 15, and default ;;; background ;;; declare a variable to be associated with the value on the dial: ;;; and give its name as an extra argument to rc_dial vars d2val; vars d2 = rc_dial( 130, -130, 0, 180, {20 0 5}, 80, 15, 'blue', false, ident d2val); ;;; repeatedly check the value of d2val, after moving the pointer with ;;; the mouse or assigning a new value to it. d2val => rc_pointer_value(d2) => 12 -> rc_pointer_value(d2); ;;; Now a dial tilted left, with a red background, using 270 degrees. ;;; The coloured area occupies more than 270 degrees because when the ;;; pointer is at the limits it covers a wider area. The red background ;;; shown indicates the area swept over by the pointer as it moves. ;;; Decimal values this time, instead of integers. ;;; a variable to be associated with it vars d3val; vars d3 = rc_dial( -130, -130, -90, 270, {0.0 100 50 5.0}, 80, 20, false, 'red', "d3val"); rc_pointer_value(d3) => d3val => 12 -> rc_pointer_value(d3); 84 -> rc_pointer_value(d3); ;;; try to increment values by 8 and see what happens when the resulting ;;; stepped values are printed out vars x; for x from 0 by 8 to 110 do x -> rc_pointer_value(d3); d3val => syssleep(30) endfor; ;;; Another with 270 degree sweep, with a range from -100 to 100, using ;;; default pointer and background colours. Decimal values again. vars d4 = rc_dial( -130, 130, 0, 270, {-100.0 100 0 5.0}, 80, 20, false, false); rc_pointer_value(d4) => 12 -> rc_pointer_value(d4); 84 -> rc_pointer_value(d4); -84 -> rc_pointer_value(d4); vars x; for x from -100 by 2 to 110 do x -> rc_pointer_value(d4); rc_pointer_value(d4) => syssleep(5) endfor; ;;; note that the values printed do not go up in steps of 2. ;;; Kill the window rc_kill_window_object(win1); -- WARNING: changing the sign of rc_yscale ---------------------------- All the examples are designed to work with y increasing upwards. If you use ved_win2 and redo the examples you may find some slightly surprising consequences of the change in rc_yscale. In particular the angle associated with the moving pointer (rc_axis(dial)) will increase counter-clockwise instead of clockwise. However rc_dial has been designed to use value converters which make the values go from start to end in the clockwise direction whether rc_yscale is positive or negative. This automatic reversal happens if the global variable rc_dial_counter_clockwise is set true (the default). If it is made false, then when rc_yscale switches sign the direction of increasing value in dials also switches from clockwise to counter clockwise. Another difference concerns the interpretation of the initial angle of a dial. When rc_yscale is negative the angle 0 points to the left, and the angular sweep is interpreted as counter_clockwise, as specified in REF XpwDrawArc, whereas when rc_yscale is positive 0 points to the right and the angular sweep goes in the opposite direction. Since by default rc_control_panel sets rc_yscale = 1, the arguments specified in the DIALS field list will not be the same as those required for an explicit call of rc_dials when rc_yscale = -1. However, a little experimentation, switching between 0 and 180, and changing the angular increment to be positive or negative, should suffice to produce required results. However it may turn out simpler to make the dials and pointer mechanism disregard rc_xscale and rc_yscale. Comments welcome. See also TEACH rc_constrained_pointer, and the following related libraries: LIB rc_draw_arc_segment LIB rc_rotate_vector LIB rc_draw_blob_sector LIB rc_draw_pointer LIB rc_draw_semi_circle -- Future plans ------------------------------------------------------- The panel description language for rc_control_panel may be extended to make it easier to include dials in a panel created by rc_control_panel. Comments criticisms and suggestions welcome. Aaron Sloman: A.Sloman AT cs.bham.ac.uk --- $poplocal/local/rclib/teach/rc_dial --- Copyright University of Birmingham 2002. All rights reserved. ------