ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra-Client/DC/Macro.pm
Revision: 1.15
Committed: Wed Dec 26 20:46:39 2007 UTC (16 years, 5 months ago) by root
Branch: MAIN
Changes since 1.14: +62 -62 lines
Log Message:
*** empty log message ***

File Contents

# User Rev Content
1 root 1.15 package dc::Macro;
2 root 1.1
3     use strict;
4    
5 root 1.9 use List::Util ();
6 root 1.15 use dc::UI;
7 root 1.1
8     our $REFRESH_MACRO_LIST;
9    
10 root 1.9 our %DEFAULT_KEYMAP = (
11 root 1.10 (map +("($_)" => "!completer $_"), "a" .. "z"),
12 root 1.9 "(!)" => "!completer shout ",
13     "(\")" => "!completer say ",
14     "(')" => "!completer",
15    
16     "LShift-tab" => "!toggle-messagewindow",
17     "RShift-tab" => "!toggle-messagewindow",
18     "tab" => "!toggle-playerbook",
19     "f1" => "!toggle-help",
20     "f2" => "!toggle-stats",
21     "f3" => "!toggle-skills",
22     "f4" => "!toggle-spells",
23     "f5" => "!toggle-inventory",
24     "f9" => "!toggle-setup",
25     (map +("LAlt-$_" => "!switch-tab $_"), 0..9),
26     (map +("RAlt-$_" => "!switch-tab $_"), 0..9),
27 elmex 1.13 "LAlt-x" => "!close-current-tab",
28 root 1.9 "return" => "!activate-chat",
29     "." => "!repeat-command",
30    
31     "," => "take",
32     "space" => "apply",
33 root 1.11 "enter" => "examine",
34 root 1.9 "[+]" => "rotateshoottype +",
35     "[-]" => "rotateshoottype -",
36 root 1.14 "LAlt-e" => "examine",
37 root 1.10 "LAlt-s" => "ready_skill find traps",
38     "LAlt-d" => "ready_skill disarm traps",
39     "LAlt-p" => "ready_skill praying",
40 root 1.9 );
41    
42 root 1.1 # allowed modifiers
43     our %MODIFIER = (
44 root 1.15 "LShift" => dc::KMOD_LSHIFT,
45     "RShift" => dc::KMOD_RSHIFT,
46     # "Shift" => dc::KMOD_LSHIFT | dc::KMOD_RSHIFT,
47     "LCtrl" => dc::KMOD_LCTRL,
48     "RCtrl" => dc::KMOD_RCTRL,
49     # "Ctrl" => dc::KMOD_LCTRL | dc::KMOD_RCTRL,
50     "LAlt" => dc::KMOD_LALT,
51     "RAlt" => dc::KMOD_RALT,
52     # "Alt" => dc::KMOD_LALT | dc::KMOD_RALT,
53     "LMeta" => dc::KMOD_LMETA,
54     "RMeta" => dc::KMOD_RMETA,
55     # "Meta" => dc::KMOD_LMETA | dc::KMOD_RMETA,
56 root 1.1 );
57    
58     # allowed modifiers
59     our $MODIFIER_MASK |= $_ for values %MODIFIER;
60    
61     # can bind to these without any modifier
62     our @DIRECT_CHARS = qw(0 1 2 3 4 5 6 7 8 9);
63    
64     our @DIRECT_KEYS = (
65 root 1.15 dc::SDLK_F1,
66     dc::SDLK_F2,
67     dc::SDLK_F3,
68     dc::SDLK_F4,
69     dc::SDLK_F5,
70     dc::SDLK_F6,
71     dc::SDLK_F7,
72     dc::SDLK_F8,
73     dc::SDLK_F9,
74     dc::SDLK_F10,
75     dc::SDLK_F11,
76     dc::SDLK_F12,
77     dc::SDLK_F13,
78     dc::SDLK_F14,
79     dc::SDLK_F15,
80 root 1.1 );
81    
82 root 1.9 our %MACRO_FUNCTION = (
83     "toggle-messagewindow" => sub { $::MESSAGE_WINDOW->toggle_visibility },
84     "toggle-playerbook" => sub { $::PL_WINDOW->toggle_visibility },
85     "toggle-help" => sub { $::HELP_WINDOW->toggle_visibility },
86     "toggle-stats" => sub { ::toggle_player_page ($::STATS_PAGE) },
87     "toggle-skills" => sub { ::toggle_player_page ($::SKILL_PAGE) },
88     "toggle-spells" => sub { ::toggle_player_page ($::SPELL_PAGE) },
89     "toggle-inventory" => sub { ::toggle_player_page ($::INVENTORY_PAGE) },
90     "toggle-pickup" => sub { ::toggle_player_page ($::PICKUP_PAGE) },
91     "toggle-setup" => sub { $::SETUP_DIALOG->toggle_visibility },
92     "toggle-setup" => sub { $::SETUP_DIALOG->toggle_visibility },
93     "switch-tab" => sub { $::MESSAGE_WINDOW->user_switch_to_page (0 + shift) },
94 elmex 1.13 "close-current-tab" => sub { $::MESSAGE_WINDOW->close_current_tab },
95 root 1.9 "activate-chat" => sub { $::MESSAGE_WINDOW->activate_current },
96     "repeat-command" => sub {
97     $::CONN->user_send ($::COMPLETER->{last_command})
98     if $::CONN && exists $::COMPLETER->{last_command};
99     },
100     "completer" => sub {
101     if ($::CONN) {
102     $::COMPLETER->set_prefix (shift);
103     $::COMPLETER->show;
104     }
105     },
106     );
107    
108     our $DEFAULT_KEYMAP;
109    
110     sub init {
111     $DEFAULT_KEYMAP ||= do {
112 root 1.15 my %sym = map +(dc::SDL_GetKeyName $_, $_), dc::SDLK_FIRST .. dc::SDLK_LAST;
113 root 1.9 my $map;
114    
115     while (my ($k, $v) = each %DEFAULT_KEYMAP) {
116     if ($k =~ /^\((.)\)$/) {
117     $map->{U}{ord $1} = $v;
118     } else {
119     my @mod = split /-/, $k;
120     my $sym = $sym{pop @mod}
121     or warn "unknown keysym $k\n";
122    
123     my $mod = 0; $mod |= $MODIFIER{$_} for @mod;
124    
125 root 1.15 $map->{K}[dc::popcount $mod]{$mod}{$sym} = $v;
126 root 1.9 }
127     }
128    
129     %DEFAULT_KEYMAP = ();
130     $map
131     };
132     }
133    
134 root 1.1 sub accelkey_to_string($) {
135     join "-",
136 root 1.9 (grep $_[0][0] & $MODIFIER{$_}, keys %MODIFIER),
137 root 1.15 dc::SDL_GetKeyName $_[0][1]
138 root 1.1 }
139    
140     sub trigger_to_string($) {
141     my ($macro) = @_;
142    
143     $macro->{accelkey}
144     ? accelkey_to_string $macro->{accelkey}
145     : "(none)"
146     }
147    
148     sub macro_to_text($) {
149     my ($macro) = @_;
150    
151     join "", map "$_\n", @{ $macro->{action} }
152     }
153    
154     sub macro_from_text($$) {
155     my ($macro, $text) = @_;
156    
157     $macro->{action} = [
158     grep /\S/, $text =~ /^\s*(.*?)\s*$/mg
159     ];
160     }
161    
162     sub trigger_edit {
163     my ($macro, $end_cb) = @_;
164    
165     my $window;
166    
167     my $done = sub {
168     $window->disconnect_all ("delete");
169     $window->disconnect_all ("focus_out");
170     $window->destroy;
171     &$end_cb;
172     };
173    
174 root 1.15 $window = new dc::UI::Toplevel
175 root 1.1 title => "Edit Macro Trigger",
176     x => "center",
177     y => "center",
178     z => 1000,
179     can_events => 1,
180     can_focus => 1,
181     has_close_button => 1,
182     on_delete => sub {
183     $done->(0);
184     1
185     },
186     on_focus_out => sub {
187     $done->(0);
188     1
189     },
190     ;
191    
192 root 1.15 $window->add (my $vb = new dc::UI::VBox);
193 root 1.1
194 root 1.15 $vb->add (new dc::UI::Label
195 root 1.1 text => "To bind the macro to a key,\n"
196     . "press a modifier (Ctrl, Alt\n"
197     . "and/or Shift) and a key, or\n"
198     . "0-9 and F1-F15 without any modifier\n\n"
199     . "To cancel press Escape or close this.\n\n"
200     . "Accelerator key combo:",
201     ellipsise => 0,
202     );
203    
204 root 1.15 $vb->add (my $entry = new dc::UI::Label
205 root 1.1 fg => [0, 0, 0, 1],
206     bg => [1, 1, 0, 1],
207     );
208    
209     my $key_cb = sub {
210     my (undef, $ev) = @_;
211    
212     my $mod = $ev->{cmod} & $MODIFIER_MASK;
213     my $sym = $ev->{sym};
214    
215     if ($sym == 27) {
216     $done->(0);
217     return 1;
218     }
219    
220     $entry->set_text (
221     join "",
222     map "$_-",
223     grep $mod & $MODIFIER{$_},
224     keys %MODIFIER
225     );
226    
227 root 1.15 return if $sym >= dc::SDLK_MODIFIER_MIN
228     && $sym <= dc::SDLK_MODIFIER_MAX;
229 root 1.1
230     if ($mod
231     || ((grep $_ eq chr $ev->{unicode}, @DIRECT_CHARS)
232     || (grep $_ == $sym, @DIRECT_KEYS)))
233     {
234     $macro->{accelkey} = [$mod, $sym];
235     $done->(1);
236     } else {
237 root 1.15 $entry->set_text ("cannot bind " . (dc::SDL_GetKeyName $sym) . " without modifier.");
238 root 1.1 }
239     1
240     };
241    
242     $window->connect (key_up => $key_cb);
243     $window->connect (key_down => $key_cb);
244    
245     $window->grab_focus;
246     $window->show;
247     }
248    
249 root 1.9 sub find_default($) {
250     my ($ev) = @_;
251    
252     for my $m (reverse grep $_, @{ $DEFAULT_KEYMAP->{K} }) {
253     for (keys %$m) {
254     if ($_ == ($ev->{mod} & $_)) {
255     if (defined (my $cmd = $m->{$_}{$ev->{sym}})) {
256     return $cmd;
257     }
258     }
259     }
260     }
261    
262 root 1.12 if (my $cmd = $DEFAULT_KEYMAP->{U}{$ev->{unicode}}) {
263     return $cmd;
264     }
265    
266 root 1.9 ()
267     }
268    
269 root 1.2 # find macro by event
270 root 1.9 sub find($) {
271 root 1.2 my ($ev) = @_;
272    
273 root 1.9 # try user-defined macros
274     if (my @user =
275     grep {
276     if (my $key = $_->{accelkey}) {
277     $key->[1] == $ev->{sym}
278     && $key->[0] == ($ev->{mod} & $MODIFIER_MASK)
279     } else {
280     0
281     }
282     } @{ $::PROFILE->{macro} || [] }
283     ) {
284     return @user;
285     }
286    
287     # now try default keymap
288     if (defined (my $def = find_default $ev)) {
289     return {
290     action => [$def],
291     };
292     }
293    
294     ()
295     }
296    
297     sub execute {
298     my ($macro) = @_;
299    
300     for (@{ $macro->{action} }) {
301     if (/^\!(\S+)\s?(.*)$/) {
302     $MACRO_FUNCTION{$1}->($2)
303     if exists $MACRO_FUNCTION{$1};
304 root 1.4 } else {
305 root 1.9 $::CONN->send_command ($_)
306     if $::CONN;
307 root 1.4 }
308 root 1.9 }
309 root 1.2 }
310    
311 root 1.1 sub keyboard_setup {
312 root 1.15 my $kbd_setup = new dc::UI::VBox;
313 root 1.1
314 root 1.15 $kbd_setup->add (my $list = new dc::UI::VBox);
315 root 1.1
316 root 1.15 $list->add (new dc::UI::FancyFrame
317 root 1.1 label => "Options",
318 root 1.15 child => (my $hb = new dc::UI::HBox),
319 root 1.1 );
320 root 1.15 $hb->add (new dc::UI::Label text => "only shift-up stops fire");
321     $hb->add (new dc::UI::CheckBox
322 root 1.1 expand => 1,
323     state => $::CFG->{shift_fire_stop},
324     tooltip => "If this checkbox is enabled you will stop fire only if you stop pressing shift.",
325     on_changed => sub {
326     my ($cbox, $value) = @_;
327     $::CFG->{shift_fire_stop} = $value;
328     0
329     },
330     );
331    
332 root 1.15 $list->add (new dc::UI::FancyFrame
333 root 1.5 label => "Macros",
334 root 1.15 child => (my $macros = new dc::UI::VBox),
335 root 1.1 );
336    
337     my $refresh;
338    
339     my $tooltip_common = "\n\n<small>Left click - edit macro\nMiddle click - invoke macro\nRight click - further options</small>";
340     my $tooltip_trigger = "The event that triggers execution of this macro, usually a key combination.";
341     my $tooltip_commands = "The commands that comprise the macro.";
342    
343     my $edit_macro = sub {
344     my ($macro) = @_;
345    
346     $kbd_setup->clear;
347 root 1.15 $kbd_setup->add (new dc::UI::Button
348 root 1.1 text => "Return",
349     tooltip => "Return to the macro list.",
350     on_activate => sub {
351     $kbd_setup->clear;
352     $kbd_setup->add ($list);
353     $refresh->();
354     1
355     },
356     );
357 root 1.15 $kbd_setup->add (new dc::UI::FancyFrame
358 root 1.1 label => "Edit Macro",
359 root 1.15 child => (my $editor = new dc::UI::Table col_expand => [0, 1]),
360 root 1.1 );
361    
362 root 1.15 $editor->add_at (0, 1, new dc::UI::Label
363 root 1.1 text => "Trigger",
364     tooltip => $tooltip_trigger,
365     can_hover => 1,
366     can_events => 1,
367     );
368 root 1.15 $editor->add_at (0, 2, new dc::UI::Label
369 root 1.1 text => "Actions",
370     tooltip => $tooltip_commands,
371     can_hover => 1,
372     can_events => 1,
373     );
374    
375 root 1.15 $editor->add_at (1, 2, my $textedit = new dc::UI::TextEdit
376 root 1.1 text => macro_to_text $macro,
377     tooltip => $tooltip_commands,
378     on_changed => sub {
379     $macro->{action} = macro_from_text $macro, $_[1];
380     },
381     );
382    
383 root 1.15 $editor->add_at (1, 1, my $accel = new dc::UI::Button
384 root 1.1 text => trigger_to_string $macro,
385     tooltip => "To change the trigger for a macro, activate this button.",
386     on_activate => sub {
387     my ($accel) = @_;
388     trigger_edit $macro, sub {
389     $accel->set_text (trigger_to_string $macro);
390     };
391     1
392     },
393     );
394    
395     my $recording;
396 root 1.15 $editor->add_at (1, 3, new dc::UI::Button
397 root 1.1 text => "Start Recording",
398     tooltip => "Start/Stop command recording: when recording, "
399     . "actions and commands you invoke are appended to this macro. "
400     . "You can only record when you are logged in.",
401     on_destroy => sub {
402     $::CONN->record if $::CONN;
403     },
404     on_activate => sub {
405     my ($widget) = @_;
406    
407     $recording = $::CONN && !$recording;
408     if ($recording) {
409     $widget->set_text ("Stop Recording");
410     $::CONN->record (sub {
411 elmex 1.6 push @{ $macro->{action} }, $_[0];
412 root 1.1 $textedit->set_text (macro_to_text $macro);
413     }) if $::CONN;
414     } else {
415     $widget->set_text ("Start Recording");
416     $::CONN->record if $::CONN;
417     }
418     },
419     );
420     };
421    
422 root 1.15 $macros->add (new dc::UI::Button
423 root 1.5 text => "New Macro",
424     tooltip => "Creates a new, empty, macro you can edit.",
425     on_activate => sub {
426     my $macro = { };
427     push @{ $::PROFILE->{macro} }, $macro;
428     $edit_macro->($macro);
429     },
430     );
431    
432 root 1.15 $macros->add (my $macrolist = new dc::UI::Table col_expand => [0, 1]);
433 root 1.5
434 root 1.1 $REFRESH_MACRO_LIST = $refresh = sub {
435 root 1.5 $macrolist->clear;
436 root 1.1
437 root 1.15 $macrolist->add_at (0, 1, new dc::UI::Label
438 root 1.1 text => "Trigger",
439     align => 0,
440     tooltip => $tooltip_trigger . $tooltip_common,
441     );
442 root 1.15 $macrolist->add_at (1, 1, new dc::UI::Label
443 root 1.1 text => "Commands",
444     tooltip => $tooltip_commands . $tooltip_common,
445     );
446    
447     for my $idx (0 .. $#{$::PROFILE->{macro} || []}) {
448     my $macro = $::PROFILE->{macro}[$idx];
449 root 1.5 my $y = $idx + 2;
450 root 1.1
451     my $macro_cb = sub {
452     my ($widget, $ev) = @_;
453    
454     if ($ev->{button} == 1) {
455     $edit_macro->($macro),
456     } elsif ($ev->{button} == 2) {
457     $::CONN->macro_send ($macro) if $::CONN;
458     } elsif ($ev->{button} == 3) {
459 root 1.15 (new dc::UI::Menu
460 root 1.1 items => [
461     ["Edit" => sub { $edit_macro->($macro) }],
462     ["Invoke" => sub { $::CONN->macro_send ($macro) if $::CONN }],
463     ["Delete" => sub {
464     # might want to use grep instead
465     splice @{$::PROFILE->{macro}}, $idx, 1, ();
466     $refresh->();
467     }],
468     ],
469     )->popup ($ev);
470     } else {
471     return 0;
472     }
473    
474     1
475     };
476    
477 root 1.15 $macrolist->add_at (0, $y, new dc::UI::Label
478 root 1.1 text => trigger_to_string $macro,
479     tooltip => $tooltip_trigger . $tooltip_common,
480     align => 0,
481     can_hover => 1,
482     can_events => 1,
483     on_button_down => $macro_cb,
484     );
485    
486 root 1.15 $macrolist->add_at (1, $y, new dc::UI::Label
487 root 1.3 text => (join "; ", @{ $macro->{action} || [] }),
488 root 1.1 tooltip => $tooltip_commands . $tooltip_common,
489     expand => 1,
490     ellipsise => 3,
491     can_hover => 1,
492     can_events => 1,
493     on_button_down => $macro_cb,
494     );
495     }
496     };
497    
498     $refresh->();
499    
500     $kbd_setup
501     }
502    
503     # this is a shortcut method that asks for a binding
504     # and then just binds it.
505     sub quick_macro {
506 root 1.3 my ($cmds, $end_cb) = @_;
507 root 1.1
508     my $macro = {
509     action => $cmds,
510     };
511    
512     trigger_edit $macro, sub {
513     if ($_[0]) {
514     push @{ $::PROFILE->{macro} }, $macro;
515     $REFRESH_MACRO_LIST->();
516     }
517    
518     &$end_cb if $end_cb;
519     };
520     }
521    
522 root 1.9 1