diff options
author | Dave Davenport <qball@gmpclient.org> | 2016-02-06 14:27:36 +0100 |
---|---|---|
committer | Dave Davenport <qball@gmpclient.org> | 2016-02-06 14:27:36 +0100 |
commit | adfc83f07d17b6eed67a3e37ca3ced75bb6ec96d (patch) | |
tree | c18281792c3c984ca8daacbc2306f0b1a503e323 /source/view.c | |
parent | 73169af793c59ee1847325d7faa3f3cf6b4c08c0 (diff) |
Restructuring, my biggest joy.
Diffstat (limited to 'source/view.c')
-rw-r--r-- | source/view.c | 1504 |
1 files changed, 1500 insertions, 4 deletions
diff --git a/source/view.c b/source/view.c index 1608c3d7..a7a16468 100644 --- a/source/view.c +++ b/source/view.c @@ -64,13 +64,157 @@ #include "dialogs/dialogs.h" #include "rofi.h" -extern RofiViewState *current_active_menu; -extern Display *display; -extern unsigned int num_modi; -extern Window main_window; +extern RofiViewState *current_active_menu; +extern Display *display; +extern unsigned int num_modi; +Window main_window = None; +extern SnDisplay *sndisplay; +extern SnLauncheeContext *sncontext; +extern GThreadPool *tpool; +extern unsigned int curr_switcher; +extern Mode **modi; +extern unsigned int num_modi; +cairo_surface_t *surface = NULL; +cairo_surface_t *fake_bg = NULL; +cairo_t *draw = NULL; +XIM xim; +XIC xic; +Colormap map = None; +XVisualInfo vinfo; +extern unsigned int normal_window_mode; + +typedef enum _MainLoopEvent +{ + ML_XEVENT, + ML_TIMEOUT +} MainLoopEvent; #include "view.h" +#include "view-internal.h" + +static char * get_matching_state ( void ) +{ + if ( config.case_sensitive ) { + if ( config.levenshtein_sort ) { + return "±"; + } + else { + return "-"; + } + } + else{ + if ( config.levenshtein_sort ) { + return "+"; + } + } + return " "; +} +/** + * Levenshtein Sorting. + */ +static int lev_sort ( const void *p1, const void *p2, void *arg ) +{ + const int *a = p1; + const int *b = p2; + int *distances = arg; + + return distances[*a] - distances[*b]; +} + +/** + * Stores a screenshot of Rofi at that point in time. + */ +static void menu_capture_screenshot ( void ) +{ + const char *outp = g_getenv ( "ROFI_PNG_OUTPUT" ); + if ( surface == NULL ) { + // Nothing to store. + fprintf ( stderr, "There is no rofi surface to store\n" ); + return; + } + const char *xdg_pict_dir = g_get_user_special_dir ( G_USER_DIRECTORY_PICTURES ); + if ( outp == NULL && xdg_pict_dir == NULL ) { + fprintf ( stderr, "XDG user picture directory or ROFI_PNG_OUTPUT is not set. Cannot store screenshot.\n" ); + return; + } + // Get current time. + GDateTime *now = g_date_time_new_now_local (); + // Format filename. + char *timestmp = g_date_time_format ( now, "rofi-%Y-%m-%d-%H%M" ); + char *filename = g_strdup_printf ( "%s.png", timestmp ); + // Build full path + char *fpath = NULL; + if ( outp == NULL ) { + int index = 0; + fpath = g_build_filename ( xdg_pict_dir, filename, NULL ); + while ( g_file_test ( fpath, G_FILE_TEST_EXISTS ) && index < 99 ) { + g_free ( fpath ); + g_free ( filename ); + // Try the next index. + index++; + // Format filename. + filename = g_strdup_printf ( "%s-%d.png", timestmp, index ); + // Build full path + fpath = g_build_filename ( xdg_pict_dir, filename, NULL ); + } + } + else { + fpath = g_strdup ( outp ); + } + fprintf ( stderr, color_green "Storing screenshot %s\n"color_reset, fpath ); + cairo_status_t status = cairo_surface_write_to_png ( surface, fpath ); + if ( status != CAIRO_STATUS_SUCCESS ) { + fprintf ( stderr, "Failed to produce screenshot '%s', got error: '%s'\n", filename, + cairo_status_to_string ( status ) ); + } + g_free ( fpath ); + g_free ( filename ); + g_free ( timestmp ); + g_date_time_unref ( now ); +} +/** + * @param state the state of the View. + * @param mon the work area. + * + * Calculates the window poslition + */ +static void calculate_window_position ( RofiViewState *state ) +{ + // Default location is center. + state->y = state->mon.y + ( state->mon.h - state->h ) / 2; + state->x = state->mon.x + ( state->mon.w - state->w ) / 2; + // Determine window location + switch ( config.location ) + { + case WL_NORTH_WEST: + state->x = state->mon.x; + case WL_NORTH: + state->y = state->mon.y; + break; + case WL_NORTH_EAST: + state->y = state->mon.y; + case WL_EAST: + state->x = state->mon.x + state->mon.w - state->w; + break; + case WL_EAST_SOUTH: + state->x = state->mon.x + state->mon.w - state->w; + case WL_SOUTH: + state->y = state->mon.y + state->mon.h - state->h; + break; + case WL_SOUTH_WEST: + state->y = state->mon.y + state->mon.h - state->h; + case WL_WEST: + state->x = state->mon.x; + break; + case WL_CENTER: + default: + break; + } + // Apply offset. + state->x += config.x_offset; + state->y += config.y_offset; +} void rofi_view_queue_redraw ( void ) { if ( current_active_menu ) { @@ -186,6 +330,824 @@ RofiViewState * rofi_view_state_create ( void ) return g_malloc0 ( sizeof ( RofiViewState ) ); } +typedef struct _thread_state +{ + RofiViewState *state; + char **tokens; + unsigned int start; + unsigned int stop; + unsigned int count; + GCond *cond; + GMutex *mutex; + unsigned int *acount; + void ( *callback )( struct _thread_state *t, gpointer data ); +}thread_state; + +static void filter_elements ( thread_state *t, G_GNUC_UNUSED gpointer user_data ) +{ + // input changed + for ( unsigned int i = t->start; i < t->stop; i++ ) { + int match = mode_token_match ( t->state->sw, t->tokens, + t->state->lines_not_ascii[i], + config.case_sensitive, + i ); + // If each token was matched, add it to list. + if ( match ) { + t->state->line_map[t->start + t->count] = i; + if ( config.levenshtein_sort ) { + // This is inefficient, need to fix it. + char * str = mode_get_completion ( t->state->sw, i ); + t->state->distance[i] = levenshtein ( t->state->text->text, str ); + g_free ( str ); + } + t->count++; + } + } + g_mutex_lock ( t->mutex ); + ( *( t->acount ) )--; + g_cond_signal ( t->cond ); + g_mutex_unlock ( t->mutex ); +} +static void check_is_ascii ( thread_state *t, G_GNUC_UNUSED gpointer user_data ) +{ + for ( unsigned int i = t->start; i < t->stop; i++ ) { + t->state->lines_not_ascii[i] = mode_is_not_ascii ( t->state->sw, i ); + } + g_mutex_lock ( t->mutex ); + ( *( t->acount ) )--; + g_cond_signal ( t->cond ); + g_mutex_unlock ( t->mutex ); +} + +static Window create_window ( Display *display ) +{ + XSetWindowAttributes attr; + attr.colormap = map; + attr.border_pixel = 0; + attr.background_pixel = 0; + + Window box = XCreateWindow ( display, DefaultRootWindow ( display ), 0, 0, 200, 100, 0, vinfo.depth, InputOutput, + vinfo.visual, CWColormap | CWBorderPixel | CWBackPixel, &attr ); + XSelectInput ( + display, + box, + KeyReleaseMask | KeyPressMask | ExposureMask | ButtonPressMask | StructureNotifyMask | FocusChangeMask | + Button1MotionMask ); + + surface = cairo_xlib_surface_create ( display, box, vinfo.visual, 200, 100 ); + // Create a drawable. + draw = cairo_create ( surface ); + cairo_set_operator ( draw, CAIRO_OPERATOR_SOURCE ); + + // Set up pango context. + cairo_font_options_t *fo = cairo_font_options_create (); + // Take font description from xlib surface + cairo_surface_get_font_options ( surface, fo ); + PangoContext *p = pango_cairo_create_context ( draw ); + // Set the font options from the xlib surface + pango_cairo_context_set_font_options ( p, fo ); + // Setup dpi + if ( config.dpi > 0 ) { + PangoFontMap *font_map = pango_cairo_font_map_get_default (); + pango_cairo_font_map_set_resolution ( (PangoCairoFontMap *) font_map, (double) config.dpi ); + } + // Setup font. + PangoFontDescription *pfd = pango_font_description_from_string ( config.menu_font ); + pango_context_set_font_description ( p, pfd ); + pango_font_description_free ( pfd ); + // Tell textbox to use this context. + textbox_set_pango_context ( p ); + // cleanup + g_object_unref ( p ); + cairo_font_options_destroy ( fo ); + + // // make it an unmanaged window + if ( !normal_window_mode && !config.fullscreen ) { + window_set_atom_prop ( display, box, netatoms[_NET_WM_STATE], &netatoms[_NET_WM_STATE_ABOVE], 1 ); + XSetWindowAttributes sattr = { .override_redirect = True }; + XChangeWindowAttributes ( display, box, CWOverrideRedirect, &sattr ); + } + else{ + window_set_atom_prop ( display, box, netatoms[_NET_WM_WINDOW_TYPE], &netatoms[_NET_WM_WINDOW_TYPE_NORMAL], 1 ); + } + if ( config.fullscreen ) { + Atom atoms[] = { + netatoms[_NET_WM_STATE_FULLSCREEN], + netatoms[_NET_WM_STATE_ABOVE] + }; + window_set_atom_prop ( display, box, netatoms[_NET_WM_STATE], atoms, sizeof ( atoms ) / sizeof ( Atom ) ); + } + + xim = XOpenIM ( display, NULL, NULL, NULL ); + xic = XCreateIC ( xim, XNInputStyle, XIMPreeditNothing | XIMStatusNothing, XNClientWindow, + box, XNFocusWindow, box, NULL ); + + // Set the WM_NAME + XStoreName ( display, box, "rofi" ); + + x11_set_window_opacity ( display, box, config.window_opacity ); + return box; +} +/** + * Small wrapper function to create easy workers. + */ +void rofi_view_call_thread ( gpointer data, gpointer user_data ) +{ + thread_state *t = (thread_state *) data; + t->callback ( t, user_data ); +} + +/** + * @param state Internal state of the menu. + * + * Calculate the number of rows, columns and elements to display based on the + * configuration and available data. + */ +static void menu_calculate_rows_columns ( RofiViewState *state ) +{ + state->columns = config.menu_columns; + state->max_elements = MIN ( state->menu_lines * state->columns, state->num_lines ); + + // Calculate the number or rows. We do this by getting the num_lines rounded up to X columns + // (num elements is better name) then dividing by columns. + state->max_rows = MIN ( state->menu_lines, (unsigned int) ( ( state->num_lines + ( state->columns - state->num_lines % + state->columns ) % + state->columns ) / ( state->columns ) ) ); + // Always have at least one row. + state->max_rows = MAX ( 1, state->max_rows ); + + if ( config.fixed_num_lines == TRUE ) { + state->max_elements = state->menu_lines * state->columns; + state->max_rows = state->menu_lines; + // If it would fit in one column, only use one column. + if ( state->num_lines < state->max_elements ) { + state->columns = + ( state->num_lines + ( state->max_rows - state->num_lines % state->max_rows ) % state->max_rows ) / state->max_rows; + state->max_elements = state->menu_lines * state->columns; + } + // Sanitize. + if ( state->columns == 0 ) { + state->columns = 1; + } + } +} + +/** + * @param state Internal state of the menu. + * @param mon the dimensions of the monitor rofi is displayed on. + * + * Calculate the width of the window and the width of an element. + */ +static void menu_calculate_window_and_element_width ( RofiViewState *state, workarea *mon ) +{ + if ( config.menu_width < 0 ) { + double fw = textbox_get_estimated_char_width ( ); + state->w = -( fw * config.menu_width ); + state->w += 2 * state->border + 4; // 4 = 2*SIDE_MARGIN + } + else{ + // Calculate as float to stop silly, big rounding down errors. + state->w = config.menu_width < 101 ? ( mon->w / 100.0f ) * ( float ) config.menu_width : config.menu_width; + } + + if ( state->columns > 0 ) { + state->element_width = state->w - ( 2 * ( state->border ) ); + // Divide by the # columns + state->element_width = ( state->element_width - ( state->columns - 1 ) * config.line_margin ) / state->columns; + } +} + +/** + * Nav helper functions, to avoid duplicate code. + */ +/** + * @param state The current RofiViewState + * + * Move the selection one page down. + * - No wrap around. + * - Clip at top/bottom + */ +inline static void menu_nav_page_next ( RofiViewState *state ) +{ + // If no lines, do nothing. + if ( state->filtered_lines == 0 ) { + return; + } + state->selected += ( state->max_elements ); + if ( state->selected >= state->filtered_lines ) { + state->selected = state->filtered_lines - 1; + } + state->update = TRUE; +} +/** + * @param state The current RofiViewState + * + * Move the selection one page up. + * - No wrap around. + * - Clip at top/bottom + */ +inline static void menu_nav_page_prev ( RofiViewState * state ) +{ + if ( state->selected < state->max_elements ) { + state->selected = 0; + } + else{ + state->selected -= ( state->max_elements ); + } + state->update = TRUE; +} +/** + * @param state The current RofiViewState + * + * Move the selection one column to the right. + * - No wrap around. + * - Do not move to top row when at start. + */ +inline static void menu_nav_right ( RofiViewState *state ) +{ + // If no lines, do nothing. + if ( state->filtered_lines == 0 ) { + return; + } + if ( ( state->selected + state->max_rows ) < state->filtered_lines ) { + state->selected += state->max_rows; + state->update = TRUE; + } + else if ( state->selected < ( state->filtered_lines - 1 ) ) { + // We do not want to move to last item, UNLESS the last column is only + // partially filled, then we still want to move column and select last entry. + // First check the column we are currently in. + int col = state->selected / state->max_rows; + // Check total number of columns. + int ncol = state->filtered_lines / state->max_rows; + // If there is an extra column, move. + if ( col != ncol ) { + state->selected = state->filtered_lines - 1; + state->update = TRUE; + } + } +} +/** + * @param state The current RofiViewState + * + * Move the selection one column to the left. + * - No wrap around. + */ +inline static void menu_nav_left ( RofiViewState *state ) +{ + if ( state->selected >= state->max_rows ) { + state->selected -= state->max_rows; + state->update = TRUE; + } +} +/** + * @param state The current RofiViewState + * + * Move the selection one row up. + * - Wrap around. + */ +inline static void menu_nav_up ( RofiViewState *state ) +{ + // Wrap around. + if ( state->selected == 0 ) { + state->selected = state->filtered_lines; + } + + if ( state->selected > 0 ) { + state->selected--; + } + state->update = TRUE; +} +/** + * @param state The current RofiViewState + * + * Move the selection one row down. + * - Wrap around. + */ +inline static void menu_nav_down ( RofiViewState *state ) +{ + // If no lines, do nothing. + if ( state->filtered_lines == 0 ) { + return; + } + state->selected = state->selected < state->filtered_lines - 1 ? MIN ( state->filtered_lines - 1, state->selected + 1 ) : 0; + state->update = TRUE; +} +/** + * @param state The current RofiViewState + * + * Move the selection to first row. + */ +inline static void menu_nav_first ( RofiViewState * state ) +{ + state->selected = 0; + state->update = TRUE; +} +/** + * @param state The current RofiViewState + * + * Move the selection to last row. + */ +inline static void menu_nav_last ( RofiViewState * state ) +{ + // If no lines, do nothing. + if ( state->filtered_lines == 0 ) { + return; + } + state->selected = state->filtered_lines - 1; + state->update = TRUE; +} +static void menu_draw ( RofiViewState *state, cairo_t *d ) +{ + unsigned int i, offset = 0; + // selected row is always visible. + // If selected is visible do not scroll. + if ( ( ( state->selected - ( state->last_offset ) ) < ( state->max_elements ) ) && ( state->selected >= ( state->last_offset ) ) ) { + offset = state->last_offset; + } + else{ + // Do paginating + int page = ( state->max_elements > 0 ) ? ( state->selected / state->max_elements ) : 0; + offset = page * state->max_elements; + state->last_offset = offset; + if ( page != state->cur_page ) { + state->cur_page = page; + state->rchanged = TRUE; + } + // Set the position + scrollbar_set_handle ( state->scrollbar, page * state->max_elements ); + } + // Re calculate the boxes and sizes, see if we can move this in the menu_calc*rowscolumns + // Get number of remaining lines to display. + unsigned int a_lines = MIN ( ( state->filtered_lines - offset ), state->max_elements ); + + // Calculate number of columns + unsigned int columns = ( a_lines + ( state->max_rows - a_lines % state->max_rows ) % state->max_rows ) / state->max_rows; + columns = MIN ( columns, state->columns ); + + // Update the handle length. + scrollbar_set_handle_length ( state->scrollbar, columns * state->max_rows ); + scrollbar_draw ( state->scrollbar, d ); + // Element width. + unsigned int element_width = state->w - ( 2 * ( state->border ) ); + if ( state->scrollbar != NULL ) { + element_width -= state->scrollbar->widget.w; + } + if ( columns > 0 ) { + element_width = ( element_width - ( columns - 1 ) * config.line_margin ) / columns; + } + + int element_height = state->line_height * config.element_height; + int y_offset = state->top_offset; + int x_offset = state->border; + // Calculate number of visible rows. + unsigned int max_elements = MIN ( a_lines, state->max_rows * columns ); + + if ( state->rchanged ) { + // Move, resize visible boxes and show them. + for ( i = 0; i < max_elements; i++ ) { + unsigned int ex = ( ( i ) / state->max_rows ) * ( element_width + config.line_margin ); + unsigned int ey = ( ( i ) % state->max_rows ) * ( element_height + config.line_margin ); + // Move it around. + textbox_moveresize ( state->boxes[i], ex + x_offset, ey + y_offset, element_width, element_height ); + { + TextBoxFontType type = ( ( ( i % state->max_rows ) & 1 ) == 0 ) ? NORMAL : ALT; + int fstate = 0; + char *text = mode_get_display_value ( state->sw, state->line_map[i + offset], &fstate, TRUE ); + TextBoxFontType tbft = fstate | ( ( i + offset ) == state->selected ? HIGHLIGHT : type ); + textbox_font ( state->boxes[i], tbft ); + textbox_text ( state->boxes[i], text ); + g_free ( text ); + } + textbox_draw ( state->boxes[i], d ); + } + state->rchanged = FALSE; + } + else{ + // Only do basic redrawing + highlight of row. + for ( i = 0; i < max_elements; i++ ) { + TextBoxFontType type = ( ( ( i % state->max_rows ) & 1 ) == 0 ) ? NORMAL : ALT; + int fstate = 0; + mode_get_display_value ( state->sw, state->line_map[i + offset], &fstate, FALSE ); + TextBoxFontType tbft = fstate | ( ( i + offset ) == state->selected ? HIGHLIGHT : type ); + textbox_font ( state->boxes[i], tbft ); + textbox_draw ( state->boxes[i], d ); + } + } +} + +void menu_update ( RofiViewState *state ) +{ + TICK (); + cairo_surface_t * surf = cairo_image_surface_create ( get_format (), state->w, state->h ); + cairo_t *d = cairo_create ( surf ); + cairo_set_operator ( d, CAIRO_OPERATOR_SOURCE ); + if ( config.fake_transparency ) { + if ( fake_bg != NULL ) { + cairo_set_source_surface ( d, fake_bg, + -(double) ( state->x - state->mon.x ), + -(double) ( state->y - state->mon.y ) ); + cairo_paint ( d ); + cairo_set_operator ( d, CAIRO_OPERATOR_OVER ); + color_background ( display, d ); + cairo_paint ( d ); + } + } + else { + // Paint the background. + color_background ( display, d ); + cairo_paint ( d ); + } + TICK_N ( "Background" ); + color_border ( display, d ); + + if ( config.menu_bw > 0 ) { + cairo_save ( d ); + cairo_set_line_width ( d, config.menu_bw ); + cairo_rectangle ( d, + config.menu_bw / 2.0, + config.menu_bw / 2.0, + state->w - config.menu_bw, + state->h - config.menu_bw ); + cairo_stroke ( d ); + cairo_restore ( d ); + } + + // Always paint as overlay over the background. + cairo_set_operator ( d, CAIRO_OPERATOR_OVER ); + if ( state->max_elements > 0 ) { + menu_draw ( state, d ); + } + if ( state->prompt_tb ) { + textbox_draw ( state->prompt_tb, d ); + } + if ( state->text ) { + textbox_draw ( state->text, d ); + } + if ( state->case_indicator ) { + textbox_draw ( state->case_indicator, d ); + } + if ( state->message_tb ) { + textbox_draw ( state->message_tb, d ); + } + color_separator ( display, d ); + + if ( strcmp ( config.separator_style, "none" ) ) { + if ( strcmp ( config.separator_style, "dash" ) == 0 ) { + const double dashes[1] = { 4 }; + cairo_set_dash ( d, dashes, 1, 0.0 ); + } + cairo_move_to ( d, state->border, state->line_height + ( state->border ) * 1 + config.line_margin + 1 ); + cairo_line_to ( d, state->w - state->border, state->line_height + ( state->border ) * 1 + config.line_margin + 1 ); + cairo_stroke ( d ); + if ( state->message_tb ) { + cairo_move_to ( d, state->border, state->top_offset - ( config.line_margin ) - 1 ); + cairo_line_to ( d, state->w - state->border, state->top_offset - ( config.line_margin ) - 1 ); + cairo_stroke ( d ); + } + + if ( config.sidebar_mode == TRUE ) { + cairo_move_to ( d, state->border, state->h - state->line_height - ( state->border ) * 1 - 1 - config.line_margin ); + cairo_line_to ( d, state->w - state->border, state->h - state->line_height - ( state->border ) * 1 - 1 - config.line_margin ); + cairo_stroke ( d ); + } + } + if ( config.sidebar_mode == TRUE ) { + for ( unsigned int j = 0; j < num_modi; j++ ) { + if ( state->modi[j] != NULL ) { + textbox_draw ( state->modi[j], d ); + } + } + } + state->update = FALSE; + + // Draw to actual window. + cairo_set_source_surface ( draw, surf, 0, 0 ); + cairo_paint ( draw ); + // Cleanup + cairo_destroy ( d ); + cairo_surface_destroy ( surf ); + + // Flush the surface. + cairo_surface_flush ( surface ); + TICK (); +} + +/** + * @param state Internal state of the menu. + * @param xse X selection event. + * + * Handle paste event. + */ +static void menu_paste ( RofiViewState *state, XSelectionEvent *xse ) +{ + if ( xse->property == netatoms[UTF8_STRING] ) { + gchar *text = window_get_text_prop ( display, main_window, netatoms[UTF8_STRING] ); + if ( text != NULL && text[0] != '\0' ) { + unsigned int dl = strlen ( text ); + // Strip new line + while ( dl > 0 && text[dl] == '\n' ) { + text[dl] = '\0'; + dl--; + } + // Insert string move cursor. + textbox_insert ( state->text, state->text->cursor, text, dl ); + textbox_cursor ( state->text, state->text->cursor + dl ); + // Force a redraw and refiltering of the text. + state->update = TRUE; + state->refilter = TRUE; + } + g_free ( text ); + } +} + +static void menu_resize ( RofiViewState *state ) +{ + unsigned int sbw = config.line_margin + 8; + widget_move ( WIDGET ( state->scrollbar ), state->w - state->border - sbw, state->top_offset ); + if ( config.sidebar_mode == TRUE ) { + int width = ( state->w - ( 2 * ( state->border ) + ( num_modi - 1 ) * config.line_margin ) ) / num_modi; + for ( unsigned int j = 0; j < num_modi; j++ ) { + textbox_moveresize ( state->modi[j], + state->border + j * ( width + config.line_margin ), state->h - state->line_height - state->border, + width, state->line_height ); + textbox_draw ( state->modi[j], draw ); + } + } + int entrybox_width = state->w - ( 2 * ( state->border ) ) - textbox_get_width ( state->prompt_tb ) + - textbox_get_width ( state->case_indicator ); + textbox_moveresize ( state->text, state->text->widget.x, state->text->widget.y, entrybox_width, state->line_height ); + widget_move ( WIDGET ( state->case_indicator ), state->w - state->border - textbox_get_width ( state->case_indicator ), state->border ); + /** + * Resize in Height + */ + { + unsigned int last_length = state->max_elements; + int element_height = state->line_height * config.element_height + config.line_margin; + // Calculated new number of boxes. + int h = ( state->h - state->top_offset - config.padding ); + if ( config.sidebar_mode == TRUE ) { + h -= state->line_height + config.line_margin; + } + if ( h < 0 ) { + fprintf ( stderr, "Current padding %u (on each side) does not fit within visible window %u.\n", config.padding, state->h ); + h = ( state->h - state->top_offset - state->h / 3 ); + if ( config.sidebar_mode == TRUE ) { + h -= state->line_height + config.line_margin; + } + } + state->max_rows = MAX ( 1, ( h / element_height ) ); + state->max_elements = state->max_rows * config.menu_columns; + // Free boxes no longer needed. + for ( unsigned int i = state->max_elements; i < last_length; i++ ) { + textbox_free ( state->boxes[i] ); + } + // resize array. + state->boxes = g_realloc ( state->boxes, state->max_elements * sizeof ( textbox* ) ); + + int y_offset = state->top_offset; + int x_offset = state->border; + int rstate = 0; + if ( config.markup_rows ) { + rstate = TB_MARKUP; + } + // Add newly added boxes. + for ( unsigned int i = last_length; i < state->max_elements; i++ ) { + state->boxes[i] = textbox_create ( rstate, x_offset, y_offset, + state->element_width, element_height, NORMAL, "" ); + } + scrollbar_resize ( state->scrollbar, -1, ( state->max_rows ) * ( element_height ) - config.line_margin ); + } + + state->rchanged = TRUE; + state->update = TRUE; +} + +/** + * @param state Internal state of the menu. + * @param key the Key being pressed. + * @param modstate the modifier state. + * + * Keyboard navigation through the elements. + */ +static int menu_keyboard_navigation ( RofiViewState *state, KeySym key, unsigned int modstate ) +{ + // pressing one of the global key bindings closes the switcher. This allows fast closing of the + // menu if an item is not selected + if ( locate_switcher ( key, modstate ) != -1 || abe_test_action ( CANCEL, modstate, key ) ) { + state->retv = MENU_CANCEL; + state->quit = TRUE; + return 1; + } + // Up, Ctrl-p or Shift-Tab + else if ( abe_test_action ( ROW_UP, modstate, key ) ) { + menu_nav_up ( state ); + return 1; + } + else if ( abe_test_action ( ROW_TAB, modstate, key ) ) { + if ( state->filtered_lines == 1 ) { + state->retv = MENU_OK; + ( state->selected_line ) = state->line_map[state->selected]; + state->quit = 1; + return 1; + } + + // Double tab! + if ( state->filtered_lines == 0 && key == state->prev_key ) { + state->retv = MENU_NEXT; + ( state->selected_line ) = 0; + state->quit = TRUE; + } + else{ + menu_nav_down ( state ); + } + return 1; + } + // Down, Ctrl-n + else if ( abe_test_action ( ROW_DOWN, modstate, key ) ) { + menu_nav_down ( state ); + return 1; + } + else if ( abe_test_action ( ROW_LEFT, modstate, key ) ) { + menu_nav_left ( state ); + return 1; + } + else if ( abe_test_action ( ROW_RIGHT, modstate, key ) ) { + menu_nav_right ( state ); + return 1; + } + else if ( abe_test_action ( PAGE_PREV, modstate, key ) ) { + menu_nav_page_prev ( state ); + return 1; + } + else if ( abe_test_action ( PAGE_NEXT, modstate, key ) ) { + menu_nav_page_next ( state ); + return 1; + } + else if ( abe_test_action ( ROW_FIRST, modstate, key ) ) { + menu_nav_first ( state ); + return 1; + } + else if ( abe_test_action ( ROW_LAST, modstate, key ) ) { + menu_nav_last ( state ); + return 1; + } + else if ( abe_test_action ( ROW_SELECT, modstate, key ) ) { + // If a valid item is selected, return that.. + if ( state->selected < state->filtered_lines ) { + char *str = mode_get_completion ( state->sw, state->line_map[state->selected] ); + textbox_text ( state->text, str ); + g_free ( str ); + textbox_cursor_end ( state->text ); + state->update = TRUE; + state->refilter = TRUE; + } + return 1; + } + state->prev_key = key; + return 0; +} + +static void menu_mouse_navigation ( RofiViewState *state, XButtonEvent *xbe ) +{ + // Scroll event + if ( xbe->button > 3 ) { + if ( xbe->button == 4 ) { + menu_nav_up ( state ); + } + else if ( xbe->button == 5 ) { + menu_nav_down ( state ); + } + else if ( xbe->button == 6 ) { + menu_nav_left ( state ); + } + else if ( xbe->button == 7 ) { + menu_nav_right ( state ); + } + return; + } + else { + if ( state->scrollbar && widget_intersect ( &( state->scrollbar->widget ), xbe->x, xbe->y ) ) { + state->selected = scrollbar_clicked ( state->scrollbar, xbe->y ); + state->update = TRUE; + return; + } + for ( unsigned int i = 0; config.sidebar_mode == TRUE && i < num_modi; i++ ) { + if ( widget_intersect ( &( state->modi[i]->widget ), xbe->x, xbe->y ) ) { + ( state->selected_line ) = 0; + state->retv = MENU_QUICK_SWITCH | ( i & MENU_LOWER_MASK ); + state->quit = TRUE; + state->skip_absorb = TRUE; + return; + } + } + for ( unsigned int i = 0; i < state->max_elements; i++ ) { + if ( widget_intersect ( &( state->boxes[i]->widget ), xbe->x, xbe->y ) ) { + // Only allow items that are visible to be selected. + if ( ( state->last_offset + i ) >= state->filtered_lines ) { + break; + } + // + state->selected = state->last_offset + i; + state->update = TRUE; + if ( ( xbe->time - state->last_button_press ) < 200 ) { + state->retv = MENU_OK; + ( state->selected_line ) = state->line_map[state->selected]; + // Quit + state->quit = TRUE; + state->skip_absorb = TRUE; + } + state->last_button_press = xbe->time; + break; + } + } + } +} +static void menu_refilter ( RofiViewState *state ) +{ + TICK_N ( "Filter start" ); + if ( strlen ( state->text->text ) > 0 ) { + unsigned int j = 0; + char **tokens = tokenize ( state->text->text, config.case_sensitive ); + /** + * On long lists it can be beneficial to parallelize. + * If number of threads is 1, no thread is spawn. + * If number of threads > 1 and there are enough (> 1000) items, spawn jobs for the thread pool. + * For large lists with 8 threads I see a factor three speedup of the whole function. + */ + unsigned int nt = MAX ( 1, state->num_lines / 500 ); + thread_state states[nt]; + GCond cond; + GMutex mutex; + g_mutex_init ( &mutex ); + g_cond_init ( &cond ); + unsigned int count = nt; + unsigned int steps = ( state->num_lines + nt ) / nt; + for ( unsigned int i = 0; i < nt; i++ ) { + states[i].state = state; + states[i].tokens = tokens; + states[i].start = i * steps; + states[i].stop = MIN ( state->num_lines, ( i + 1 ) * steps ); + states[i].count = 0; + states[i].cond = &cond; + states[i].mutex = &mutex; + states[i].acount = &count; + states[i].callback = filter_elements; + if ( i > 0 ) { + g_thread_pool_push ( tpool, &states[i], NULL ); + } + } + // Run one in this thread. + filter_elements ( &states[0], NULL ); + // No need to do this with only one thread. + |