diff --git a/alsamixer/Makefile.am b/alsamixer/Makefile.am index 3fe7a74..d1021f9 100644 --- a/alsamixer/Makefile.am +++ b/alsamixer/Makefile.am @@ -10,6 +10,8 @@ alsamixer_SOURCES = card_select.c card_select.h \ die.c die.h \ mainloop.c mainloop.h \ mem.c mem.h \ + menu_widget.c menu_widget.h \ + mixer_clickable.c mixer_clickable.h \ mixer_controls.c mixer_controls.h \ mixer_display.c mixer_display.h \ mixer_widget.c mixer_widget.h \ diff --git a/alsamixer/bindings.h b/alsamixer/bindings.h index 1e9dd6d..fa2e8d3 100644 --- a/alsamixer/bindings.h +++ b/alsamixer/bindings.h @@ -51,7 +51,12 @@ enum mixer_command { CMD_MIXER_TOGGLE_MUTE, CMD_MIXER_TOGGLE_CAPTURE, CMD_MIXER_BALANCE_CONTROL, - CMD_MIXER_REFRESH + CMD_MIXER_REFRESH, + + // Mouse + CMD_MIXER_MOUSE_CLICK_MUTE, + CMD_MIXER_MOUSE_CLICK_VOLUME_BAR, + CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM, }; enum textbox_command { diff --git a/alsamixer/card_select.c b/alsamixer/card_select.c index e970203..6762ecd 100644 --- a/alsamixer/card_select.c +++ b/alsamixer/card_select.c @@ -28,10 +28,10 @@ #include "utils.h" #include "colors.h" #include "widget.h" +#include "menu_widget.h" #include "mixer_widget.h" #include "device_name.h" #include "card_select.h" -#include "bindings.h" struct card { struct card *next; @@ -60,33 +60,15 @@ static void on_key_enter(void) } } -static void on_menu_key(int key) -{ - if (key < ARRAY_SIZE(textbox_bindings)) { - key = textbox_bindings[key]; - if (key >= CMD_TEXTBOX___MIN_MENU_COMMAND && - key <= CMD_TEXTBOX___MAX_MENU_COMMAND) - menu_driver(menu, key + KEY_MAX); - } -} - static void on_handle_key(int key) { - switch (key) { - case 27: - case KEY_CANCEL: - case 'q': - case 'Q': - list_widget.close(); - break; - case 10: - case 13: - case KEY_ENTER: - on_key_enter(); - break; - default: - on_menu_key(key); - break; + switch (menu_widget_handle_key(menu, key)) { + case KEY_ENTER: + on_key_enter(); + break; + case KEY_CANCEL: + list_widget.close(); + break; } } diff --git a/alsamixer/mainloop.c b/alsamixer/mainloop.c index a50109b..e80f232 100644 --- a/alsamixer/mainloop.c +++ b/alsamixer/mainloop.c @@ -50,6 +50,7 @@ void initialize_curses(bool use_color) #endif window_size_changed(); /* update screen_lines/cols */ init_colors(use_color); + mousemask(ALL_MOUSE_EVENTS, NULL); snd_lib_error_set_handler(black_hole_error_handler); } diff --git a/alsamixer/menu_widget.c b/alsamixer/menu_widget.c new file mode 100644 index 0000000..30940a7 --- /dev/null +++ b/alsamixer/menu_widget.c @@ -0,0 +1,63 @@ +#include "menu_widget.h" +#include "colors.h" +#include "utils.h" +#include "bindings.h" + +/* Returns + * - KEY_CANCEL: close is requested + * - KEY_ENTER: item is selected + * - -1: no action + */ +int menu_widget_handle_key(MENU *menu, int key) +{ + MEVENT m; + + switch (key) { + case 27: + case KEY_CANCEL: + case 'q': + case 'Q': + return KEY_CANCEL; + case '\n': + case '\r': + case KEY_ENTER: + return KEY_ENTER; + + case KEY_MOUSE: + switch (menu_driver(menu, KEY_MOUSE)) { + case E_UNKNOWN_COMMAND: + /* If you double-click an item a REQ_TOGGLE_ITEM is generated + * and E_UNKNOWN_COMMAND is returned. (man menu_driver) */ + return KEY_ENTER; + case E_REQUEST_DENIED: + /* If menu did not handle KEY_MOUSE is has to be removed from + * input queue to prevent an infinite loop. */ + key = wgetch(menu_win(menu)); + if (key == KEY_MOUSE) { + if (getmouse(&m) == ERR) + return -1; + if (m.bstate & (BUTTON4_PRESSED|BUTTON4_CLICKED)) + menu_driver(menu, REQ_UP_ITEM); +#if NCURSES_MOUSE_VERSION > 1 + else if (m.bstate & (BUTTON5_PRESSED|BUTTON5_CLICKED)) + menu_driver(menu, REQ_DOWN_ITEM); +#endif + else + return KEY_CANCEL; + } + else if (key > 0) + ungetch(key); + } + return -1; + + default: + if (key < ARRAY_SIZE(textbox_bindings)) { + key = textbox_bindings[key]; + if (key >= CMD_TEXTBOX___MIN_MENU_COMMAND && + key <= CMD_TEXTBOX___MAX_MENU_COMMAND) + menu_driver(menu, key + KEY_MAX); + } + + return -1; + } +} diff --git a/alsamixer/menu_widget.h b/alsamixer/menu_widget.h new file mode 100644 index 0000000..ad3ed0e --- /dev/null +++ b/alsamixer/menu_widget.h @@ -0,0 +1,9 @@ +#ifndef MENU_WIDGET_H_INCLUDED +#define MENU_WIDGET_H_INCLUDED + +#include "widget.h" +#include + +int menu_widget_handle_key(MENU *menu, int key); + +#endif diff --git a/alsamixer/mixer_clickable.c b/alsamixer/mixer_clickable.c new file mode 100644 index 0000000..c772f37 --- /dev/null +++ b/alsamixer/mixer_clickable.c @@ -0,0 +1,136 @@ +#include +#include +#include "mixer_clickable.h" + +extern int screen_cols; +extern int screen_lines; + +static struct clickable_rect *clickable_rects = NULL; +static unsigned int clickable_rects_count = 0; +static unsigned int last_rect = 0; + +/* Using 0 instead of -1 for marking free rectangles allows us to use + * memset for `freeing` all rectangles at once. + * Zero is actually a valid coordinate in ncurses, but since we don't have + * any clickables in the top line this is fine. */ +#define FREE_MARKER 0 +#define RECT_IS_FREE(RECT) ((RECT).y1 == FREE_MARKER) +#define RECT_FREE(RECT) ((RECT).y1 = FREE_MARKER) + +void clickable_set(int y1, int x1, int y2, int x2, command_enum command, int arg1) { + struct clickable_rect* tmp; + unsigned int i; + + for (i = last_rect; i < clickable_rects_count; ++i) { + if (RECT_IS_FREE(clickable_rects[i])) { + last_rect = i; + goto SET_CLICKABLE_DATA; + } + } + + for (i = 0; i < last_rect; ++i) { + if (RECT_IS_FREE(clickable_rects[i])) { + last_rect = i; + goto SET_CLICKABLE_DATA; + } + } + + last_rect = clickable_rects_count; + tmp = realloc(clickable_rects, (clickable_rects_count + 8) * sizeof(*clickable_rects)); + if (!tmp) { + free(clickable_rects); + clickable_rects = NULL; + clickable_rects_count = 0; + last_rect = 0; + return; + } + clickable_rects = tmp; +#if FREE_MARKER == 0 + memset(clickable_rects + clickable_rects_count, 0, 8 * sizeof(*clickable_rects)); +#else + for (i = clickable_rects_count; i < clickable_rects_count + 8; ++i) + RECT_FREE(clickable_rects[i]); +#endif + clickable_rects_count += 8; + +SET_CLICKABLE_DATA: + clickable_rects[last_rect] = (struct clickable_rect) { + .y1 = y1, + .x1 = x1, + .x2 = x2, + .y2 = y2, + .command = command, + .arg1 = arg1 + }; +} + +void clickable_set_relative(WINDOW *win, int y1, int x1, int y2, int x2, command_enum command, int arg1) { + int y, x; + getyx(win, y, x); + y1 = y + y1; + x1 = x + x1; + y2 = y + y2; + x2 = x + x2; + clickable_set(y1, x1, y2, x2, command, arg1); +} + +void clickable_clear(int y1, int x1, int y2, int x2) { +#define IS_IN_RECT(Y, X) (Y >= y1 && Y <= y2 && X >= x1 && X <= x2) + unsigned int i; + + if (x1 == 0 && x2 == -1 && y2 == -1) { + if (y1 == 0) { + // Optimize case: clear all +#if FREE_MARKER == 0 + if (clickable_rects) + memset(clickable_rects, 0, + clickable_rects_count * sizeof(*clickable_rects)); +#else + for (i = 0; i < clickable_rects_count; ++i) + RECT_FREE(clickable_rects[i]); +#endif + } + else { + // Optimize case: clear all lines beyond y1 + for (i = 0; i < clickable_rects_count; ++i) { + if (clickable_rects[i].y2 >= y1) + RECT_FREE(clickable_rects[i]); + } + } + return; + } + + if (y2 < 0) + y2 = screen_lines + y2 + 1; + if (x2 < 0) + x2 = screen_cols + x2 + 1; + + for (i = 0; i < clickable_rects_count; ++i) { + if (!RECT_IS_FREE(clickable_rects[i]) && ( + IS_IN_RECT(clickable_rects[i].y1, clickable_rects[i].x1) || + IS_IN_RECT(clickable_rects[i].y2, clickable_rects[i].x2) + )) + { + RECT_FREE(clickable_rects[i]); + } + } +} + +struct clickable_rect* clickable_find(int y, int x) { + unsigned int i; + + for (i = 0; i < clickable_rects_count; ++i) { + if ( + !RECT_IS_FREE(clickable_rects[i]) && + y >= clickable_rects[i].y1 && + x >= clickable_rects[i].x1 && + y <= clickable_rects[i].y2 && + x <= clickable_rects[i].x2 + ) + { + return &clickable_rects[i]; + } + } + + return NULL; +} diff --git a/alsamixer/mixer_clickable.h b/alsamixer/mixer_clickable.h new file mode 100644 index 0000000..792c711 --- /dev/null +++ b/alsamixer/mixer_clickable.h @@ -0,0 +1,21 @@ +#ifndef MIXER_CLICKABLE_H +#define MIXER_CLICKABLE_H + +#include CURSESINC +#include "bindings.h" + +struct clickable_rect { + short y1; + short x1; + short y2; + short x2; + command_enum command; + int arg1; +}; + +void clickable_set(int y1, int x1, int y2, int x2, command_enum command, int arg1); +void clickable_set_relative(WINDOW *win, int y1, int x1, int y2, int x2, command_enum command, int arg1); +void clickable_clear(int y1, int x1, int y2, int x2); +struct clickable_rect* clickable_find(int y, int x); + +#endif diff --git a/alsamixer/mixer_display.c b/alsamixer/mixer_display.c index b1f79d0..2f45ab3 100644 --- a/alsamixer/mixer_display.c +++ b/alsamixer/mixer_display.c @@ -34,6 +34,7 @@ #include "mixer_widget.h" #include "mixer_controls.h" #include "mixer_display.h" +#include "mixer_clickable.h" enum align { ALIGN_LEFT, @@ -109,6 +110,7 @@ void init_mixer_layout(void) unsigned int label_width_left, label_width_right; unsigned int right_x, i; + clickable_clear(0, 0, -1, -1); screen_too_small = screen_lines < 14 || screen_cols < 12; has_info_items = screen_lines >= 6; if (!has_info_items) @@ -135,9 +137,12 @@ void init_mixer_layout(void) display_string_in_field(1 + i, 2, labels_left[i], label_width_left, ALIGN_RIGHT); if (label_width_right) - for (i = 0; i < 4; ++i) + for (i = 0; i < 4; ++i) { display_string_in_field(1 + i, right_x, labels_right[i], label_width_right, ALIGN_LEFT); + clickable_set(1 + i, right_x, 1 + i, right_x + label_width_right - 1, + CMD_MIXER_HELP + i, -1); + } } void display_card_info(void) @@ -197,6 +202,7 @@ void display_view_mode(void) bool has_view_mode; int i; + clickable_clear(3, 0, 3, 30); if (!has_info_items) return; @@ -204,10 +210,10 @@ void display_view_mode(void) for (i = 0; i < 3; ++i) widths[i] = get_mbs_width(modes[i]); if (4 + widths[0] + 6 + widths[1] + 6 + widths[2] + 1 <= info_items_width) { - wmove(mixer_widget.window, 3, info_items_left); + wmove(mixer_widget.window, 3, info_items_left - 1); wattrset(mixer_widget.window, attr_mixer_text); for (i = 0; i < 3; ++i) { - wprintw(mixer_widget.window, "F%c:", '3' + i); + wprintw(mixer_widget.window, " F%c:", '3' + i); if (has_view_mode && (int)view_mode == i) { wattrset(mixer_widget.window, attr_mixer_active); wprintw(mixer_widget.window, "[%s]", modes[i]); @@ -215,8 +221,8 @@ void display_view_mode(void) } else { wprintw(mixer_widget.window, " %s ", modes[i]); } - if (i < 2) - waddch(mixer_widget.window, ' '); + clickable_set_relative(mixer_widget.window, 0, -(widths[i] + 5), 0, -1, + CMD_WITH_ARG(CMD_MIXER_SET_VIEW_MODE, i), -1); } } else { wattrset(mixer_widget.window, attr_mixer_active); @@ -322,6 +328,7 @@ static void clear_controls_display(void) { int i; + clickable_clear(5, 0, -1, -1); wattrset(mixer_widget.window, attr_mixer_frame); for (i = 5; i < screen_lines - 1; ++i) mvwprintw(mixer_widget.window, i, 1, "%*s", screen_cols - 2, ""); @@ -483,6 +490,8 @@ static void display_control(unsigned int control_index) frame_left + c + 1, ch); } } + clickable_set(base_y - volume_height, frame_left + 1, base_y, frame_left + 2, + CMD_MIXER_MOUSE_CLICK_VOLUME_BAR, control_index); if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, attr_mixer_active); if (!(control->flags & HAS_VOLUME_1)) { @@ -520,6 +529,8 @@ static void display_control(unsigned int control_index) switches[1] ? _("O")[0] | (control->flags & IS_ACTIVE ? attr_ctl_nomute : 0) : _("M")[0] | (control->flags & IS_ACTIVE ? attr_ctl_mute : 0)); + clickable_set(base_y + 1, frame_left + 1, base_y + 1, frame_left + 2, + CMD_MIXER_MOUSE_CLICK_MUTE, control_index); } if (control->flags & TYPE_CSWITCH) { @@ -534,10 +545,14 @@ static void display_control(unsigned int control_index) wattrset(mixer_widget.window, switches[0] ? attr_ctl_capture : attr_ctl_nocapture); /* TRANSLATORS: "left"; no more than two characters */ display_string_in_field(cswitch_y - 1, frame_left - 2, switches[0] ? _("L") : "", 2, ALIGN_RIGHT); + clickable_set(cswitch_y - 1, frame_left - 2, cswitch_y - 1, frame_left - 1, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT), control_index); if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, switches[1] ? attr_ctl_capture : attr_ctl_nocapture); /* TRANSLATORS: "right"; no more than two characters */ display_string_in_field(cswitch_y - 1, frame_left + 4, switches[1] ? _("R") : "", 2, ALIGN_LEFT); + clickable_set(cswitch_y - 1, frame_left + 4, cswitch_y - 1, frame_left + 5, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, RIGHT), control_index); /* TRANSLATORS: no more than eight characters */ s = _("CAPTURE"); if (switches[0] || switches[1]) { @@ -554,6 +569,8 @@ static void display_control(unsigned int control_index) wattrset(mixer_widget.window, attr_ctl_nocapture); display_string_in_field(cswitch_y, frame_left - 2, buf, 8, ALIGN_CENTER); } + clickable_set(cswitch_y, frame_left - 2, cswitch_y, frame_left - 2 + 8, + CMD_WITH_ARG(CMD_MIXER_TOGGLE_CAPTURE, LEFT|RIGHT), control_index); } if (control->flags & TYPE_ENUM) { @@ -566,6 +583,8 @@ static void display_control(unsigned int control_index) if (control->flags & IS_ACTIVE) wattrset(mixer_widget.window, attr_mixer_active); display_string_centered_in_control(base_y, col, buf, control_width); + clickable_set_relative(mixer_widget.window, 0, -control_name_width, 0, -2, + CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM, control_index); } if (control_index == focus_control_index) { @@ -584,6 +603,8 @@ static void display_control(unsigned int control_index) wattrset(mixer_widget.window, attr_ctl_label_inactive); } display_string_centered_in_control(name_y, col, control->name, control_name_width); + clickable_set_relative(mixer_widget.window, -1, -control_name_width, 0, -2, + CMD_WITH_ARG(CMD_MIXER_FOCUS_CONTROL, control_index), -1); if (channel_name_y > name_y) { if (control->flags & IS_MULTICH) { switch (control->flags & MULTICH_MASK) { @@ -630,6 +651,10 @@ static void display_scroll_indicators(void) mvwaddch(mixer_widget.window, y, 0, left); mvwaddch(mixer_widget.window, y, screen_cols - 1, right); } + clickable_set(y0, 0, y1, 0, + CMD_WITH_ARG(CMD_MIXER_PREVIOUS, visible_controls), -1); + clickable_set(y0, screen_cols - 1, y1, screen_cols - 1, + CMD_WITH_ARG(CMD_MIXER_NEXT, visible_controls), -1); } void display_controls(void) diff --git a/alsamixer/mixer_widget.c b/alsamixer/mixer_widget.c index b1bd61e..160124f 100644 --- a/alsamixer/mixer_widget.c +++ b/alsamixer/mixer_widget.c @@ -34,6 +34,7 @@ #include "proc_files.h" #include "card_select.h" #include "volume_mapping.h" +#include "mixer_clickable.h" #include "mixer_controls.h" #include "mixer_display.h" #include "mixer_widget.h" @@ -54,6 +55,9 @@ unsigned int current_control_flags; bool control_values_changed; bool controls_changed; +unsigned int mouse_wheel_step = 1; +bool mouse_wheel_focuses_control = 1; + static int elem_callback(snd_mixer_elem_t *elem, unsigned int mask) { if (mask == SND_CTL_EVENT_MASK_REMOVE) { @@ -462,12 +466,95 @@ static void balance_volumes(void) display_controls(); } +static int on_mouse_key() { + MEVENT m; + command_enum cmd = 0; + unsigned int channels = LEFT | RIGHT; + unsigned int index; + struct control *control; + struct clickable_rect *rect; + + if (getmouse(&m) == ERR) + return 0; + + if (m.bstate & ( + BUTTON1_PRESSED|BUTTON1_RELEASED| + BUTTON2_PRESSED|BUTTON2_RELEASED| + BUTTON3_PRESSED|BUTTON3_RELEASED)) + return 0; + + rect = clickable_find(m.y, m.x); + if (rect) + cmd = rect->command; + +#if NCURSES_MOUSE_VERSION > 1 + if (m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED|BUTTON5_CLICKED|BUTTON5_PRESSED)) { + switch (cmd) { + case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM: + focus_control_index = rect->arg1; + return CMD_WITH_ARG(( + m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED) + ? CMD_MIXER_CONTROL_UP + : CMD_MIXER_CONTROL_DOWN + ), 1); + + case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR: + if (mouse_wheel_focuses_control) + focus_control_index = rect->arg1; + + default: + return CMD_WITH_ARG(( + m.bstate & (BUTTON4_CLICKED|BUTTON4_PRESSED) + ? CMD_MIXER_CONTROL_UP + : CMD_MIXER_CONTROL_DOWN + ), mouse_wheel_step); + } + } +#endif + + /* If the rectangle has got an argument (value != -1) it is used for + * setting `focus_control_index` */ + if (rect && rect->arg1 >= 0) + focus_control_index = rect->arg1; + + switch (cmd) { + case CMD_MIXER_MOUSE_CLICK_VOLUME_BAR: + if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED)) + channels = m.x - rect->x1 + 1; + return CMD_WITH_ARG(CMD_MIXER_CONTROL_SET_PERCENT_LEFT + channels - 1, + (100 * (rect->y2 - m.y) / (rect->y2 - rect->y1)) // volume + ); + + case CMD_MIXER_MOUSE_CLICK_MUTE: + if (m.bstate & (BUTTON3_CLICKED|BUTTON3_DOUBLE_CLICKED|BUTTON3_TRIPLE_CLICKED)) + channels = m.x - rect->x1 + 1; + return CMD_WITH_ARG(CMD_MIXER_TOGGLE_MUTE, channels); + + case CMD_MIXER_MOUSE_CLICK_CONTROL_ENUM: + control = get_focus_control(TYPE_ENUM); + if (control && + (snd_mixer_selem_get_enum_item(control->elem, 0, &index) >= 0)) { + return (index == 0 + ? CMD_WITH_ARG(CMD_MIXER_CONTROL_UP, 100) + : CMD_WITH_ARG(CMD_MIXER_CONTROL_DOWN, 1)); + } + break; + + default: + return cmd; // non-mouse command + } + + return 0; // failed mouse command +} + static void on_handle_key(int key) { int arg; command_enum cmd; - if (key < ARRAY_SIZE(mixer_bindings)) + if (key == KEY_MOUSE) + cmd = on_mouse_key(); + else if (key < ARRAY_SIZE(mixer_bindings)) cmd = mixer_bindings[key]; else return; diff --git a/alsamixer/mixer_widget.h b/alsamixer/mixer_widget.h index 5b1cfcc..3b6f474 100644 --- a/alsamixer/mixer_widget.h +++ b/alsamixer/mixer_widget.h @@ -32,6 +32,9 @@ extern unsigned int current_control_flags; extern bool control_values_changed; extern bool controls_changed; +extern unsigned int mouse_wheel_step; +extern bool mouse_wheel_focuses_control; + void create_mixer_object(struct snd_mixer_selem_regopt *selem_regopt); void create_mixer_widget(void); void mixer_shutdown(void); diff --git a/alsamixer/proc_files.c b/alsamixer/proc_files.c index 9d3fe3f..5ae7ae9 100644 --- a/alsamixer/proc_files.c +++ b/alsamixer/proc_files.c @@ -28,42 +28,26 @@ #include "widget.h" #include "textbox.h" #include "proc_files.h" -#include "bindings.h" +#include "menu_widget.h" static struct widget proc_widget; static ITEM *items[7]; static unsigned int items_count; static MENU *menu; -static void on_menu_key(int key) -{ - if (key < ARRAY_SIZE(textbox_bindings)) { - key = textbox_bindings[key]; - if (key >= CMD_TEXTBOX___MIN_MENU_COMMAND && - key <= CMD_TEXTBOX___MAX_MENU_COMMAND) - menu_driver(menu, key + KEY_MAX); - } -} - static void on_handle_key(int key) { ITEM *item; - switch (key) { - case 27: - case KEY_CANCEL: - proc_widget.close(); - break; - case 10: - case 13: - case KEY_ENTER: - item = current_item(menu); - if (item) - show_textfile(item_name(item)); - break; - default: - on_menu_key(key); - break; + switch (menu_widget_handle_key(menu, key)) { + case KEY_ENTER: + item = current_item(menu); + if (item) + show_textfile(item_name(item)); + break; + case KEY_CANCEL: + proc_widget.close(); + break; } }