mirror of
https://github.com/davatorium/rofi.git
synced 2025-02-24 15:56:25 -05:00
Remove a big chunk of duplicate code by re-ordering.
* Remove the refilter code that was in there twice (and directly squash a small bug) * Pull out the window position calculation in a sub-function.
This commit is contained in:
parent
1fd2f04f0a
commit
ea9090eb50
3 changed files with 180 additions and 187 deletions
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -1,2 +1,13 @@
|
||||||
*.swp
|
*.swp
|
||||||
|
*.*~
|
||||||
build/
|
build/
|
||||||
|
missing
|
||||||
|
install-sh
|
||||||
|
configure
|
||||||
|
config.h.in
|
||||||
|
Makefile.in
|
||||||
|
aclocal.m4
|
||||||
|
autom4te.cache/
|
||||||
|
compile
|
||||||
|
depcomp
|
||||||
|
|
||||||
|
|
294
source/rofi.c
294
source/rofi.c
|
@ -922,6 +922,61 @@ static int levenshtein ( const char *s, const char *t )
|
||||||
return dist ( 0, 0 );
|
return dist ( 0, 0 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param x [out] the calculated x position.
|
||||||
|
* @param y [out] the calculated y position.
|
||||||
|
* @param mon the workarea.
|
||||||
|
* @param h the required height of the window.
|
||||||
|
* @param w the required width of the window.
|
||||||
|
*/
|
||||||
|
static void calculate_window_position ( const workarea *mon, int *x, int *y, int w, int h )
|
||||||
|
{
|
||||||
|
// Default location is center.
|
||||||
|
*y = mon->y + ( mon->h - h ) / 2;
|
||||||
|
*x = mon->x + ( mon->w - w ) / 2;
|
||||||
|
// Determine window location
|
||||||
|
switch ( config.location )
|
||||||
|
{
|
||||||
|
case WL_NORTH_WEST:
|
||||||
|
*x = mon->x;
|
||||||
|
|
||||||
|
case WL_NORTH:
|
||||||
|
*y = mon->y;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WL_NORTH_EAST:
|
||||||
|
*y = mon->y;
|
||||||
|
|
||||||
|
case WL_EAST:
|
||||||
|
*x = mon->x + mon->w - w - config.menu_bw * 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WL_EAST_SOUTH:
|
||||||
|
*x = mon->x + mon->w - w - config.menu_bw * 2;
|
||||||
|
|
||||||
|
case WL_SOUTH:
|
||||||
|
*y = mon->y + mon->h - h - config.menu_bw * 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WL_SOUTH_WEST:
|
||||||
|
*y = mon->y + mon->h - h - config.menu_bw * 2;
|
||||||
|
|
||||||
|
case WL_WEST:
|
||||||
|
*x = mon->x;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WL_CENTER:
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Compensate again for border.
|
||||||
|
*x -= config.menu_bw;
|
||||||
|
*y -= config.menu_bw;
|
||||||
|
// Apply offset.
|
||||||
|
*x += config.x_offset;
|
||||||
|
*y += config.y_offset;
|
||||||
|
}
|
||||||
|
|
||||||
MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shift,
|
MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shift,
|
||||||
menu_match_cb mmc, void *mmc_data, int *selected_line )
|
menu_match_cb mmc, void *mmc_data, int *selected_line )
|
||||||
{
|
{
|
||||||
|
@ -972,10 +1027,9 @@ MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shi
|
||||||
monitor_active ( &mon );
|
monitor_active ( &mon );
|
||||||
|
|
||||||
// Calculate as float to stop silly, big rounding down errors.
|
// Calculate as float to stop silly, big rounding down errors.
|
||||||
int w = config.menu_width < 101 ? ( mon.w / 100.0f ) * ( float ) config.menu_width : config.menu_width;
|
int w = config.menu_width < 101 ? ( mon.w / 100.0f ) * ( float ) config.menu_width : config.menu_width;
|
||||||
int x = mon.x + ( mon.w - w ) / 2;
|
|
||||||
// Compensate for border width.
|
// Compensate for border width.
|
||||||
w -= config.menu_bw*2;
|
w -= config.menu_bw * 2;
|
||||||
|
|
||||||
int element_width = w - ( 2 * ( config.padding ) );
|
int element_width = w - ( 2 * ( config.padding ) );
|
||||||
// Divide by the # columns
|
// Divide by the # columns
|
||||||
|
@ -994,7 +1048,7 @@ MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shi
|
||||||
else{
|
else{
|
||||||
Screen *screen = DefaultScreenOfDisplay ( display );
|
Screen *screen = DefaultScreenOfDisplay ( display );
|
||||||
Window root = RootWindow ( display, XScreenNumberOfScreen ( screen ) );
|
Window root = RootWindow ( display, XScreenNumberOfScreen ( screen ) );
|
||||||
box = XCreateSimpleWindow ( display, root, x, 0, w, 300,
|
box = XCreateSimpleWindow ( display, root, 0, 0, w, 300,
|
||||||
config.menu_bw,
|
config.menu_bw,
|
||||||
color_get ( display, config.menu_bc ),
|
color_get ( display, config.menu_bc ),
|
||||||
color_get ( display, config.menu_bg ) );
|
color_get ( display, config.menu_bg ) );
|
||||||
|
@ -1099,108 +1153,105 @@ MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shi
|
||||||
distance = calloc ( num_lines, sizeof ( int ) );
|
distance = calloc ( num_lines, sizeof ( int ) );
|
||||||
}
|
}
|
||||||
unsigned int filtered_lines = 0;
|
unsigned int filtered_lines = 0;
|
||||||
|
// We want to filter on the first run.
|
||||||
if ( input && *input && strlen ( *input ) > 0 ) {
|
int refilter = TRUE;
|
||||||
char **tokens = tokenize ( *input );
|
int update = FALSE;
|
||||||
|
|
||||||
// input changed
|
|
||||||
for ( i = 0, j = 0; i < num_lines; i++ ) {
|
|
||||||
int match = mmc ( tokens, lines[i], i, mmc_data );
|
|
||||||
|
|
||||||
// If each token was matched, add it to list.
|
|
||||||
if ( match ) {
|
|
||||||
line_map[j] = i;
|
|
||||||
if ( config.levenshtein_sort ) {
|
|
||||||
distance[i] = levenshtein ( *input, lines[i] );
|
|
||||||
}
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( config.levenshtein_sort ) {
|
|
||||||
qsort_r ( line_map, j, sizeof ( int ), lev_sort, distance );
|
|
||||||
}
|
|
||||||
for ( i = 0; i < j; i++ ) {
|
|
||||||
filtered[i] = lines[line_map[i]];
|
|
||||||
}
|
|
||||||
filtered_lines = j;
|
|
||||||
|
|
||||||
tokenize_free ( tokens );
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
int jin = 0;
|
|
||||||
for ( i = 0; i < num_lines; i++ ) {
|
|
||||||
filtered[jin] = lines[i];
|
|
||||||
line_map[jin] = i;
|
|
||||||
jin++;
|
|
||||||
filtered_lines++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// resize window vertically to suit
|
// resize window vertically to suit
|
||||||
// Subtract the margin of the last row.
|
// Subtract the margin of the last row.
|
||||||
int h = line_height * ( max_rows + 1 ) + ( config.padding ) * 2 + LINE_MARGIN;
|
int h = line_height * ( max_rows + 1 ) + ( config.padding ) * 2 + LINE_MARGIN;
|
||||||
|
|
||||||
|
|
||||||
if ( config.hmode == TRUE ) {
|
if ( config.hmode == TRUE ) {
|
||||||
h = line_height + ( config.padding ) * 2;
|
h = line_height + ( config.padding ) * 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default location is center.
|
// Move the window to the correct x,y position.
|
||||||
int y = mon.y + ( mon.h - h ) / 2;
|
int x, y;
|
||||||
|
calculate_window_position ( &mon, &x, &y, w, h );
|
||||||
// Determine window location
|
|
||||||
switch ( config.location )
|
|
||||||
{
|
|
||||||
case WL_NORTH_WEST:
|
|
||||||
x = mon.x;
|
|
||||||
|
|
||||||
case WL_NORTH:
|
|
||||||
y = mon.y;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WL_NORTH_EAST:
|
|
||||||
y = mon.y;
|
|
||||||
|
|
||||||
case WL_EAST:
|
|
||||||
x = mon.x + mon.w - w - config.menu_bw * 2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WL_EAST_SOUTH:
|
|
||||||
x = mon.x + mon.w - w - config.menu_bw * 2;
|
|
||||||
|
|
||||||
case WL_SOUTH:
|
|
||||||
y = mon.y + mon.h - h - config.menu_bw * 2;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WL_SOUTH_WEST:
|
|
||||||
y = mon.y + mon.h - h - config.menu_bw * 2;
|
|
||||||
|
|
||||||
case WL_WEST:
|
|
||||||
x = mon.x;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case WL_CENTER:
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Apply offset.
|
|
||||||
y += config.y_offset;
|
|
||||||
x += config.x_offset;
|
|
||||||
|
|
||||||
|
|
||||||
XMoveResizeWindow ( display, box, x, y, w, h );
|
XMoveResizeWindow ( display, box, x, y, w, h );
|
||||||
|
|
||||||
XMapRaised ( display, box );
|
XMapRaised ( display, box );
|
||||||
|
|
||||||
// if grabbing keyboard failed, fall through
|
// if grabbing keyboard failed, fall through
|
||||||
if ( take_keyboard ( box ) ) {
|
if ( take_keyboard ( box ) ) {
|
||||||
KeySym prev_key = 0;
|
KeySym prev_key = 0;
|
||||||
unsigned int selected = 0;
|
unsigned int selected = 0;
|
||||||
|
|
||||||
for (;; ) {
|
for (;; ) {
|
||||||
int refilter = FALSE;
|
// If something changed, refilter the list. (paste or text entered)
|
||||||
int update = FALSE;
|
if ( refilter ) {
|
||||||
|
if ( strlen ( text->text ) > 0 ) {
|
||||||
|
char **tokens = tokenize ( text->text );
|
||||||
|
|
||||||
|
// input changed
|
||||||
|
for ( i = 0, j = 0; i < num_lines; i++ ) {
|
||||||
|
int match = mmc ( tokens, lines[i], i, mmc_data );
|
||||||
|
|
||||||
|
// If each token was matched, add it to list.
|
||||||
|
if ( match ) {
|
||||||
|
line_map[j] = i;
|
||||||
|
if ( config.levenshtein_sort ) {
|
||||||
|
distance[i] = levenshtein ( text->text, lines[i] );
|
||||||
|
}
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ( config.levenshtein_sort ) {
|
||||||
|
qsort_r ( line_map, j, sizeof ( int ), lev_sort, distance );
|
||||||
|
}
|
||||||
|
// Update the filtered list.
|
||||||
|
for ( i = 0; i < j; i++ ) {
|
||||||
|
filtered[i] = lines[line_map[i]];
|
||||||
|
}
|
||||||
|
for ( i = j; i < num_lines; i++ ) {
|
||||||
|
filtered[i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup + bookkeeping.
|
||||||
|
filtered_lines = j;
|
||||||
|
tokenize_free ( tokens );
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
for ( i = 0; i < num_lines; i++ ) {
|
||||||
|
filtered[i] = lines[i];
|
||||||
|
line_map[i] = i;
|
||||||
|
}
|
||||||
|
filtered_lines = num_lines;
|
||||||
|
}
|
||||||
|
selected = MIN ( selected, j - 1 );
|
||||||
|
|
||||||
|
if ( config.zeltak_mode && filtered_lines == 1 ) {
|
||||||
|
if ( filtered[selected] ) {
|
||||||
|
retv = MENU_OK;
|
||||||
|
*selected_line = line_map[selected];
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
fprintf ( stderr, "We should never hit this." );
|
||||||
|
abort ();
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
refilter = FALSE;
|
||||||
|
}
|
||||||
|
// Update if requested.
|
||||||
|
if ( update ) {
|
||||||
|
menu_hide_arrow_text ( filtered_lines, selected,
|
||||||
|
max_elements, arrowbox_top,
|
||||||
|
arrowbox_bottom );
|
||||||
|
textbox_draw ( text );
|
||||||
|
textbox_draw ( prompt_tb );
|
||||||
|
menu_draw ( boxes, max_elements, num_lines, &last_offset, selected, filtered );
|
||||||
|
menu_set_arrow_text ( filtered_lines, selected,
|
||||||
|
max_elements, arrowbox_top,
|
||||||
|
arrowbox_bottom );
|
||||||
|
update = FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for event.
|
||||||
XEvent ev;
|
XEvent ev;
|
||||||
XNextEvent ( display, &ev );
|
XNextEvent ( display, &ev );
|
||||||
|
|
||||||
|
// Handle event.
|
||||||
if ( ev.type == Expose ) {
|
if ( ev.type == Expose ) {
|
||||||
while ( XCheckTypedEvent ( display, Expose, &ev ) ) {
|
while ( XCheckTypedEvent ( display, Expose, &ev ) ) {
|
||||||
;
|
;
|
||||||
|
@ -1429,75 +1480,6 @@ MenuReturn menu ( char **lines, char **input, char *prompt, Time *time, int *shi
|
||||||
}
|
}
|
||||||
prev_key = key;
|
prev_key = key;
|
||||||
}
|
}
|
||||||
// If something changed, refilter the list. (paste or text entered)
|
|
||||||
if ( refilter ) {
|
|
||||||
if ( strlen ( text->text ) > 0 ) {
|
|
||||||
char **tokens = tokenize ( text->text );
|
|
||||||
|
|
||||||
// input changed
|
|
||||||
for ( i = 0, j = 0; i < num_lines; i++ ) {
|
|
||||||
int match = mmc ( tokens, lines[i], i, mmc_data );
|
|
||||||
|
|
||||||
// If each token was matched, add it to list.
|
|
||||||
if ( match ) {
|
|
||||||
line_map[j] = i;
|
|
||||||
if ( config.levenshtein_sort ) {
|
|
||||||
distance[i] = levenshtein ( text->text, lines[i] );
|
|
||||||
}
|
|
||||||
j++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ( config.levenshtein_sort ) {
|
|
||||||
qsort_r ( line_map, j, sizeof ( int ), lev_sort, distance );
|
|
||||||
}
|
|
||||||
for ( i = 0; i < j; i++ ) {
|
|
||||||
filtered[i] = lines[line_map[i]];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cleanup + bookkeeping.
|
|
||||||
filtered_lines = j;
|
|
||||||
tokenize_free ( tokens );
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
int jin = 0;
|
|
||||||
for ( i = 0; i < num_lines; i++ ) {
|
|
||||||
filtered[jin] = lines[i];
|
|
||||||
line_map[jin] = i;
|
|
||||||
jin++;
|
|
||||||
}
|
|
||||||
filtered_lines = jin;
|
|
||||||
}
|
|
||||||
selected = MIN ( selected, j - 1 );
|
|
||||||
|
|
||||||
for (; j < num_lines; j++ ) {
|
|
||||||
filtered[j] = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( config.zeltak_mode && filtered_lines == 1 ) {
|
|
||||||
if ( filtered[selected] ) {
|
|
||||||
retv = MENU_OK;
|
|
||||||
*selected_line = line_map[selected];
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
fprintf ( stderr, "We should never hit this." );
|
|
||||||
abort ();
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Update if requested.
|
|
||||||
if ( update ) {
|
|
||||||
menu_hide_arrow_text ( filtered_lines, selected,
|
|
||||||
max_elements, arrowbox_top,
|
|
||||||
arrowbox_bottom );
|
|
||||||
textbox_draw ( text );
|
|
||||||
textbox_draw ( prompt_tb );
|
|
||||||
menu_draw ( boxes, max_elements, num_lines, &last_offset, selected, filtered );
|
|
||||||
menu_set_arrow_text ( filtered_lines, selected,
|
|
||||||
max_elements, arrowbox_top,
|
|
||||||
arrowbox_bottom );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
release_keyboard ();
|
release_keyboard ();
|
||||||
|
|
|
@ -58,52 +58,52 @@ typedef struct
|
||||||
* Currently supports string and number.
|
* Currently supports string and number.
|
||||||
*/
|
*/
|
||||||
static XrmOption xrmOptions[] = {
|
static XrmOption xrmOptions[] = {
|
||||||
{ xrm_Number, "opacity", { .num = &config.window_opacity }, NULL },
|
{ xrm_Number, "opacity", { .num = &config.window_opacity }, NULL },
|
||||||
|
|
||||||
{ xrm_Number, "width", { .num = &config.menu_width }, NULL },
|
{ xrm_Number, "width", { .num = &config.menu_width }, NULL },
|
||||||
|
|
||||||
{ xrm_Number, "lines", { .num = &config.menu_lines }, NULL },
|
{ xrm_Number, "lines", { .num = &config.menu_lines }, NULL },
|
||||||
{ xrm_Number, "columns", { .num = &config.menu_columns }, NULL },
|
{ xrm_Number, "columns", { .num = &config.menu_columns }, NULL },
|
||||||
|
|
||||||
{ xrm_String, "font", { .str = &config.menu_font }, NULL },
|
{ xrm_String, "font", { .str = &config.menu_font }, NULL },
|
||||||
/* Foreground color */
|
/* Foreground color */
|
||||||
{ xrm_String, "foreground", { .str = &config.menu_fg }, NULL },
|
{ xrm_String, "foreground", { .str = &config.menu_fg }, NULL },
|
||||||
{ xrm_String, "fg", { .str = &config.menu_fg }, NULL },
|
{ xrm_String, "fg", { .str = &config.menu_fg }, NULL },
|
||||||
|
|
||||||
{ xrm_String, "background", { .str = &config.menu_bg }, NULL },
|
{ xrm_String, "background", { .str = &config.menu_bg }, NULL },
|
||||||
{ xrm_String, "bg", { .str = &config.menu_bg }, NULL },
|
{ xrm_String, "bg", { .str = &config.menu_bg }, NULL },
|
||||||
|
|
||||||
{ xrm_String, "highlightfg", { .str = &config.menu_hlfg }, NULL },
|
{ xrm_String, "highlightfg", { .str = &config.menu_hlfg }, NULL },
|
||||||
{ xrm_String, "hlfg", { .str = &config.menu_hlfg }, NULL },
|
{ xrm_String, "hlfg", { .str = &config.menu_hlfg }, NULL },
|
||||||
|
|
||||||
{ xrm_String, "highlightbg", { .str = &config.menu_hlbg }, NULL },
|
{ xrm_String, "highlightbg", { .str = &config.menu_hlbg }, NULL },
|
||||||
{ xrm_String, "hlbg", { .str = &config.menu_hlbg }, NULL },
|
{ xrm_String, "hlbg", { .str = &config.menu_hlbg }, NULL },
|
||||||
|
|
||||||
{ xrm_String, "bordercolor", { .str = &config.menu_bc }, NULL },
|
{ xrm_String, "bordercolor", { .str = &config.menu_bc }, NULL },
|
||||||
{ xrm_String, "bc", { .str = &config.menu_bc }, NULL },
|
{ xrm_String, "bc", { .str = &config.menu_bc }, NULL },
|
||||||
|
|
||||||
{ xrm_Number, "borderwidth", { .num = &config.menu_bw }, NULL },
|
{ xrm_Number, "borderwidth", { .num = &config.menu_bw }, NULL },
|
||||||
{ xrm_Number, "bw", { .num = &config.menu_bw }, NULL },
|
{ xrm_Number, "bw", { .num = &config.menu_bw }, NULL },
|
||||||
|
|
||||||
{ xrm_Number, "location", { .num = &config.location }, NULL },
|
{ xrm_Number, "location", { .num = &config.location }, NULL },
|
||||||
{ xrm_Number, "loc", { .num = &config.location }, NULL },
|
{ xrm_Number, "loc", { .num = &config.location }, NULL },
|
||||||
|
|
||||||
{ xrm_Number, "padding", { .num = &config.padding }, NULL },
|
{ xrm_Number, "padding", { .num = &config.padding }, NULL },
|
||||||
{ xrm_Number, "yoffset", { .num = &config.y_offset }, NULL },
|
{ xrm_Number, "yoffset", { .num = &config.y_offset }, NULL },
|
||||||
{ xrm_Number, "xoffset", { .num = &config.x_offset }, NULL },
|
{ xrm_Number, "xoffset", { .num = &config.x_offset }, NULL },
|
||||||
{ xrm_Boolean, "fixed-num-lines", { .num = &config.fixed_num_lines }, NULL },
|
{ xrm_Boolean, "fixed-num-lines", { .num = &config.fixed_num_lines }, NULL },
|
||||||
{ xrm_Boolean, "hmode", { .num = &config.hmode }, NULL },
|
{ xrm_Boolean, "hmode", { .num = &config.hmode }, NULL },
|
||||||
|
|
||||||
|
|
||||||
{ xrm_String, "terminal", { .str = &config.terminal_emulator }, NULL },
|
{ xrm_String, "terminal", { .str = &config.terminal_emulator }, NULL },
|
||||||
|
|
||||||
{ xrm_Boolean, "ssh-set-title", { .num = &config.ssh_set_title }, NULL },
|
{ xrm_Boolean, "ssh-set-title", { .num = &config.ssh_set_title }, NULL },
|
||||||
{ xrm_Boolean, "disable-history", { .num = &config.disable_history }, NULL },
|
{ xrm_Boolean, "disable-history", { .num = &config.disable_history }, NULL },
|
||||||
{ xrm_Boolean, "levenshtein-sort",{ .num = &config.levenshtein_sort }, NULL },
|
{ xrm_Boolean, "levenshtein-sort", { .num = &config.levenshtein_sort }, NULL },
|
||||||
/* Key bindings */
|
/* Key bindings */
|
||||||
{ xrm_String, "key", { .str = &config.window_key }, NULL },
|
{ xrm_String, "key", { .str = &config.window_key }, NULL },
|
||||||
{ xrm_String, "rkey", { .str = &config.run_key }, NULL },
|
{ xrm_String, "rkey", { .str = &config.run_key }, NULL },
|
||||||
{ xrm_String, "skey", { .str = &config.ssh_key }, NULL },
|
{ xrm_String, "skey", { .str = &config.ssh_key }, NULL },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue