alsamixer: added mouse support

Mouse support has been added for mixer_widget.c, card_select.c and
proc_files.c.

In the mixer widget the mouse is handled as follows:
- After an element has been printed in mixer_display.c, a call to
  clickable_set() will store the coordinates of the drawn area plus the
  command enum that should be executed on click. An optional argument
  holds an index which points to the selected mixer control.
- on_mouse_click() searches for a matching rectangle, focuses the mixer
  control and returns the command enum.

In the menu widgets, the menu_driver() function handles mouse input.

Signed-off-by: Benjamin Abendroth <braph93@gmx.de>
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
This commit is contained in:
braph 2019-09-27 01:25:56 +02:00 committed by Jaroslav Kysela
parent 6e9e84ba60
commit ba1c5357a1
12 changed files with 377 additions and 59 deletions

View file

@ -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 \

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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);
}

63
alsamixer/menu_widget.c Normal file
View file

@ -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;
}
}

9
alsamixer/menu_widget.h Normal file
View file

@ -0,0 +1,9 @@
#ifndef MENU_WIDGET_H_INCLUDED
#define MENU_WIDGET_H_INCLUDED
#include "widget.h"
#include <menu.h>
int menu_widget_handle_key(MENU *menu, int key);
#endif

136
alsamixer/mixer_clickable.c Normal file
View file

@ -0,0 +1,136 @@
#include <stdlib.h>
#include <string.h>
#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;
}

View file

@ -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

View file

@ -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)

View file

@ -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;

View file

@ -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);

View file

@ -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;
}
}