/* DeaDBeeF - ultimate music player for GNU/Linux systems with X11 Copyright (C) 2009 Alexey Yakovenko This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef HAVE_CONFIG_H # include #endif #include #include #include #include #include #include #include #include #include "gtkplaylist.h" #include "callbacks.h" #include "interface.h" #include "support.h" #include "playlist.h" #include "playback.h" #include "codec.h" #include "common.h" #include "messagepump.h" #include "messages.h" #include "streamer.h" extern GtkWidget *mainwin; static GdkPixmap *backbuf; static int rowheight = 17; static int scrollpos = 0; static int playlist_row = -1; static double playlist_clicktime = 0; static double ps_lastpos[2]; static int shift_sel_anchor = -1; static int nvisiblerows = 0; // array of lengths and widths // N = number of columns // M = number of visible rows, // cache[(ROW*ncolumns+COLUMN)*3+0] --- position to insert "...", or -1 if the whole line fits // cache[(ROW*ncolumns+COLUMN)*3+1] --- width extent in pixels // cache[(ROW*ncolumns+COLUMN)*3+2] --- 0 if needs recalc static int16_t *drawps_cache = NULL; #define ncolumns 5 #define colname_max 100 int refit_header[ncolumns] = { 1, 1, 1, 1, 1 }; const char *colnames[ncolumns] = { "Playing Status", "Artist / Album", "Track №", "Title / Track Artist", "Duration" }; char colnames_fitted[ncolumns][colname_max]; int colwidths[] = { 50, 200, 50, 200, 50 }; int fit_text (cairo_t *cr, char *out, int *dotpos, int len, const char *in, int width) { int l = strlen (in); len--; l = min (len, l); strncpy (out, in, l); out[l] = 0; int w = 0; char *p = &out[l]; p = g_utf8_find_prev_char (out, p); int processed = 0; if (dotpos) { *dotpos = -1; } for (;;) { cairo_text_extents_t e; cairo_text_extents (cr, out, &e); w = e.width;// + e.x_bearing + e.x_advance; if (e.width <= width && (processed == 0 || processed >= 3)) { break; } char *prev = g_utf8_find_prev_char (out, p); if (!prev) { break; } int i; for (i = 0; i < p-prev; i++) { prev[i] = '.'; } processed += p-prev; p = prev; if (processed >= 3) { if (dotpos) { *dotpos = p-out; } for (int i = 0; i < 3; i++) { p[i] = '.'; } p[3] = 0; } } return w; } static void text_draw (cairo_t *cr, int x, int y, const char *text) { cairo_move_to (cr, x, y+rowheight-3); cairo_show_text (cr, text); } void gtkps_setup_scrollbar (void) { GtkWidget *playlist = lookup_widget (mainwin, "playlist"); int h = playlist->allocation.height / rowheight; int size = ps_getcount (); if (h >= size) { size = 0; } GtkWidget *scroll = lookup_widget (mainwin, "playscroll"); if (size == 0) { gtk_widget_hide (scroll); } else { GtkAdjustment *adj = (GtkAdjustment*)gtk_adjustment_new (gtk_range_get_value (GTK_RANGE (scroll)), 0, size, 1, h, h); gtk_range_set_adjustment (GTK_RANGE (scroll), adj); gtk_widget_show (scroll); } } void redraw_ps_row_novis (GtkWidget *widget, int row) { cairo_t *cr; cr = gdk_cairo_create (backbuf); if (!cr) { return; } //cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); // cairo_select_font_face (cr, "fixed", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); playItem_t *it = ps_get_for_idx (row); // printf ("redraw row %d (selected = %d, cursor = %d)\n", row, it->selected, playlist_row == row ? 1 : 0); if (it) { draw_ps_row_back (backbuf, cr, row, it); draw_ps_row (backbuf, cr, row, it); } cairo_destroy (cr); } void redraw_ps_row (GtkWidget *widget, int row) { int x, y, w, h; x = 0; y = (row - scrollpos) * rowheight; w = widget->allocation.width; h = rowheight; redraw_ps_row_novis (widget, row); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, x, y, x, y, w, h); //gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, 0, 0, 0, 0, widget->allocation.width, widget->allocation.height); } void draw_ps_row_back (GdkDrawable *drawable, cairo_t *cr, int row, playItem_t *it) { // draw background float w; int start, end; int startx, endx; int width, height; gdk_drawable_get_size (drawable, &width, &height); w = width; if (it && it->selected) { if (row % 2) { cairo_set_source_rgb (cr, 0xa7/255.f, 0x9f/255.f, 0x96/255.f); } else { cairo_set_source_rgb (cr, 0xaf/255.f, 0xa7/255.f, 0x9e/255.f); } cairo_rectangle (cr, 0, row * rowheight - scrollpos * rowheight, width, rowheight); cairo_fill (cr); } else { if (row % 2) { cairo_set_source_rgb (cr, 0x1d/255.f, 0x1f/255.f, 0x1b/255.f); } else { cairo_set_source_rgb (cr, 0x21/255.f, 0x23/255.f, 0x1f/255.f); } cairo_rectangle (cr, 0, row * rowheight - scrollpos * rowheight, width, rowheight); cairo_fill (cr); } if (row == playlist_row) { cairo_set_source_rgb (cr, 0x7f/255.f, 0x7f/255.f, 0x7f/255.f); cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); cairo_rectangle (cr, 0, row * rowheight - scrollpos * rowheight, width, rowheight-1); cairo_set_line_width (cr, 1); cairo_stroke (cr); } } void draw_ps_row (GdkDrawable *drawable, cairo_t *cr, int row, playItem_t *it) { if (row-scrollpos >= nvisiblerows || row-scrollpos < 0) { fprintf (stderr, "WARNING: attempt to draw row outside of screen bounds (%d)\n", row-scrollpos); return; } int width, height; gdk_drawable_get_size (drawable, &width, &height); if (it == playlist_current_ptr) { cairo_set_source_rgb (cr, 1, 1, 1); cairo_rectangle (cr, 3, row * rowheight - scrollpos * rowheight + 3, rowheight-6, rowheight-6); cairo_fill (cr); } if (it && it->selected) { cairo_set_source_rgb (cr, 0, 0, 0); } else { cairo_set_source_rgb (cr, 0xf4/255.f, 0x7e/255.f, 0x46/255.f); } cairo_set_font_size (cr, rowheight-4); // draw as columns char dur[10]; int min = (int)it->duration/60; int sec = (int)(it->duration-min*60); snprintf (dur, 10, "%d:%02d", min, sec); const char *columns[ncolumns] = { "", ps_find_meta (it, "artist"), ps_find_meta (it, "track"), ps_find_meta (it, "title"), dur }; int x = 0; #if 1 for (int i = 0; i < ncolumns; i++) { char str[512]; if (i > 0) { int dotpos; int cidx = ((row-scrollpos) * ncolumns + i) * 3; if (!drawps_cache[cidx + 2]) { drawps_cache[cidx + 1] = fit_text (cr, str, &dotpos, 512, columns[i], colwidths[i]-10); drawps_cache[cidx + 0] = dotpos; drawps_cache[cidx + 2] = 1; } else { // reconstruct from cache dotpos = drawps_cache[cidx + 0]; strncpy (str, columns[i], 512); if (dotpos >= 0) { for (int k = 0; k < 3; k++) { str[k+dotpos] = '.'; } str[dotpos+3] = 0; } } int w = drawps_cache[cidx + 1]; // printf ("draw %s -> %s\n", columns[i], str); if (i == 2) { text_draw (cr, x + colwidths[i] - w - 5, row * rowheight - scrollpos * rowheight, str); } else { text_draw (cr, x + 5, row * rowheight - scrollpos * rowheight, str); } } x += colwidths[i]; } #endif #if 0 char dname[512]; ps_format_item_display_name (it, dname, 512); text_draw (cr, rowheight, row * rowheight - scrollpos * rowheight, dname); #endif } void draw_playlist (GtkWidget *widget, int x, int y, int w, int h) { if (!drawps_cache && nvisiblerows > 0 && ncolumns > 0) { drawps_cache = malloc (nvisiblerows * ncolumns * 3 * sizeof (int16_t)); memset (drawps_cache, 0, nvisiblerows * ncolumns * 3 * sizeof (int16_t)); } cairo_t *cr; cr = gdk_cairo_create (backbuf); if (!cr) { return; } //cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); // cairo_select_font_face (cr, "fixed", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); // cairo_set_font_size (cr, rowheight); int row; int row1; int row2; int row2_full; row1 = max (0, y / rowheight + scrollpos); row2 = min (ps_getcount (), (y+h) / rowheight + scrollpos + 1); row2_full = (y+h) / rowheight + scrollpos + 1; //printf ("drawing row %d (nvis=%d)\n", row2_full, nvisiblerows); // draw background playItem_t *it = ps_get_for_idx (scrollpos); playItem_t *it_copy = it; for (row = row1; row < row2_full; row++) { draw_ps_row_back (backbuf, cr, row, it); if (it) { it = it->next; } } it = it_copy; int idx = 0; for (row = row1; row < row2; row++, idx++) { draw_ps_row (backbuf, cr, row, it); it = it->next; } cairo_destroy (cr); } // change properties gboolean on_playlist_configure_event (GtkWidget *widget, GdkEventConfigure *event, gpointer user_data) { gtkps_setup_scrollbar (); if (backbuf) { g_object_unref (backbuf); backbuf = NULL; } if (drawps_cache) { free (drawps_cache); drawps_cache = NULL; } nvisiblerows = ceil (widget->allocation.height / (float)rowheight); backbuf = gdk_pixmap_new (widget->window, widget->allocation.width, widget->allocation.height, -1); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); return FALSE; } void gtkps_expose (GtkWidget *widget, int x, int y, int w, int h) { gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, x, y, x, y, w, h); } void gtkps_select_single (int sel) { int idx=0; GtkWidget *widget = lookup_widget (mainwin, "playlist"); for (playItem_t *it = playlist_head; it; it = it->next, idx++) { if (idx == sel) { if (!it->selected) { it->selected = 1; redraw_ps_row (widget, idx); } } else if (it->selected) { it->selected = 0; redraw_ps_row (widget, idx); } } } // {{{ expected behaviour for mouse1 without modifiers: // {{{ [+] if clicked unselected item: // unselect all // select clicked item // playlist_row = clicked // redraw // start 'area selection' mode // }}} // {{{ [+] if clicked selected item: // playlist_row = clicked // redraw // wait until next release or motion event, whichever is 1st // if release is 1st: // unselect all except clicked, redraw // else if motion is 1st: // enter drag-drop mode // }}} // }}} int areaselect = 0; int areaselect_x = -1; int areaselect_y = -1; int areaselect_dx = -1; int areaselect_dy = -1; int dragwait = 0; void gtkps_mouse1_pressed (int state, int ex, int ey, double time) { // cursor must be set here, but selection must be handled in keyrelease if (ps_getcount () == 0) { return; } GtkWidget *widget = lookup_widget (mainwin, "playlist"); // remember mouse coords for doubleclick detection ps_lastpos[0] = ex; ps_lastpos[1] = ey; // select item int y = ey/rowheight + scrollpos; if (y < 0 || y >= ps_getcount ()) { y = -1; } if (time - playlist_clicktime < 0.5 && fabs(ps_lastpos[0] - ex) < 3 && fabs(ps_lastpos[1] - ey) < 3) { // doubleclick - play this item if (playlist_row != -1) { messagepump_push (M_PLAYSONGNUM, 0, playlist_row, 0); } // prevent next click to trigger doubleclick playlist_clicktime = time-1; } else { playlist_clicktime = time; } int sel = y; if (y == -1) { y = ps_getcount () - 1; } int prev = playlist_row; playlist_row = y; shift_sel_anchor = playlist_row; // handle selection if (!(state & (GDK_CONTROL_MASK|GDK_SHIFT_MASK))) { playItem_t *it = ps_get_for_idx (sel); if (!it || !it->selected) { // reset selection, and set it to single item gtkps_select_single (sel); areaselect = 1; areaselect_x = ex; areaselect_y = ey; areaselect_dx = -1; areaselect_dy = -1; shift_sel_anchor = playlist_row; } else { dragwait = 1; redraw_ps_row (widget, prev); redraw_ps_row (widget, playlist_row); } } else if (state & GDK_CONTROL_MASK) { // toggle selection if (y != -1) { playItem_t *it = ps_get_for_idx (y); if (it) { it->selected = 1 - it->selected; redraw_ps_row (widget, y); } } } else if (state & GDK_SHIFT_MASK) { // select range int start = min (prev, playlist_row); int end = max (prev, playlist_row); int idx = 0; for (playItem_t *it = playlist_head; it; it = it->next, idx++) { if (idx >= start && idx <= end) { if (!it->selected) { it->selected = 1; redraw_ps_row (widget, idx); } } else { if (it->selected) { it->selected = 0; redraw_ps_row (widget, idx); } } } } if (prev != -1 && prev != playlist_row) { redraw_ps_row (widget, prev); } if (playlist_row != -1 && sel == -1) { redraw_ps_row (widget, playlist_row); } } void gtkps_mouse1_released (int state, int ex, int ey, double time) { if (dragwait) { dragwait = 0; int y = ey/rowheight + scrollpos; gtkps_select_single (y); } else if (areaselect) { areaselect = 0; } } void gtkps_draw_areasel (GtkWidget *widget, int x, int y) { // erase previous rect using 4 blits from backbuffer if (areaselect_dx != -1) { int sx = min (areaselect_x, areaselect_dx); int sy = min (areaselect_y, areaselect_dy); int dx = max (areaselect_x, areaselect_dx); int dy = max (areaselect_y, areaselect_dy); int w = dx - sx + 1; int h = dy - sy + 1; //gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, sx, sy, sx, sy, dx - sx + 1, dy - sy + 1); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, sx, sy, sx, sy, w, 1); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, sx, sy, sx, sy, 1, h); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, sx, sy + h - 1, sx, sy + h - 1, w, 1); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, sx + w - 1, sy, sx + w - 1, sy, 1, h); } areaselect_dx = x; areaselect_dy = y; cairo_t *cr; cr = gdk_cairo_create (widget->window); if (!cr) { return; } cairo_set_source_rgb (cr, 1.f, 1.f, 1.f); cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); cairo_set_line_width (cr, 1); int sx = min (areaselect_x, x); int sy = min (areaselect_y, y); int dx = max (areaselect_x, x); int dy = max (areaselect_y, y); cairo_rectangle (cr, sx, sy, dx-sx, dy-sy); cairo_stroke (cr); cairo_destroy (cr); } void gtkps_mousemove (GdkEventMotion *event) { if (dragwait) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); if (gtk_drag_check_threshold (widget, ps_lastpos[0], event->x, ps_lastpos[1], event->y)) { dragwait = 0; GtkTargetEntry entry = { .target = "STRING", .flags = GTK_TARGET_SAME_WIDGET, .info = TARGET_SAMEWIDGET }; GtkTargetList *lst = gtk_target_list_new (&entry, 1); gtk_drag_begin (widget, lst, GDK_ACTION_MOVE, TARGET_SAMEWIDGET, (GdkEvent *)event); } } else if (areaselect) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); int y = event->y/rowheight + scrollpos; if (y != shift_sel_anchor) { int start = min (y, shift_sel_anchor); int end = max (y, shift_sel_anchor); int idx=0; for (playItem_t *it = playlist_head; it; it = it->next, idx++) { if (idx >= start && idx <= end) { it->selected = 1; redraw_ps_row (widget, idx); } else if (it->selected) { it->selected = 0; redraw_ps_row (widget, idx); } } } // debug only // gtkps_draw_areasel (widget, event->x, event->y); } } void gtkps_handle_scroll_event (int direction) { GtkWidget *range = lookup_widget (mainwin, "playscroll"); GtkWidget *playlist = lookup_widget (mainwin, "playlist"); int h = playlist->allocation.height / rowheight; int size = ps_getcount (); if (h >= size) { size = 0; } if (size == 0) { return; } // pass event to scrollbar GtkAdjustment* adj = gtk_range_get_adjustment (GTK_RANGE (range)); int newscroll = gtk_range_get_value (GTK_RANGE (range)); if (direction == GDK_SCROLL_UP) { newscroll -= 10;//gtk_adjustment_get_page_increment (adj); } else if (direction == GDK_SCROLL_DOWN) { newscroll += 10;//gtk_adjustment_get_page_increment (adj); } gtk_range_set_value (GTK_RANGE (range), newscroll); } void gtkps_scroll (int newscroll) { if (newscroll != scrollpos) { int d = abs (newscroll - scrollpos); if (abs (newscroll - scrollpos) < nvisiblerows) { // move untouched cache part // and invalidate changed part if (newscroll < scrollpos) { //printf ("scroll up\n"); int r; for (r = nvisiblerows-1; r >= d; r--) { memcpy (&drawps_cache[r * ncolumns * 3], &drawps_cache[(r - d) * ncolumns * 3], sizeof (int16_t) * 3 * ncolumns); } for (r = 0; r < d; r++) { memset (&drawps_cache[r * ncolumns * 3], 0, sizeof (int16_t) * 3 * ncolumns); } } else { //printf ("scroll down\n"); int r; for (r = 0; r < nvisiblerows-d; r++) { memcpy (&drawps_cache[r * ncolumns * 3], &drawps_cache[(r + d) * ncolumns * 3], sizeof (int16_t) * 3 * ncolumns); } for (r = nvisiblerows-d; r < nvisiblerows; r++) { memset (&drawps_cache[r * ncolumns * 3], 0, sizeof (int16_t) * 3 * ncolumns); } } } else { // invalidate entire cache memset (drawps_cache, 0, sizeof (int16_t) * 3 * ncolumns * nvisiblerows); } scrollpos = newscroll; GtkWidget *widget = lookup_widget (mainwin, "playlist"); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, 0, 0, 0, 0, widget->allocation.width, widget->allocation.height); } } void gtkps_playsong (void) { if (p_ispaused ()) { printf ("unpause\n"); p_unpause (); } else if (playlist_current_ptr) { p_stop (); printf ("restart\n"); streamer_set_nextsong (ps_get_idx_of (playlist_current_ptr), 1); #if 0 ps_start_current (); GtkWidget *widget = lookup_widget (mainwin, "playlist"); redraw_ps_row (widget, ps_get_idx_of (playlist_current_ptr)); #endif } else if (playlist_row != -1) { printf ("start under cursor\n"); streamer_set_nextsong (playlist_row, 1); #if 0 playItem_t *it = ps_get_for_idx (playlist_row); if (it) { ps_set_current (it); } GtkWidget *widget = lookup_widget (mainwin, "playlist"); redraw_ps_row (widget, playlist_row); #endif } else { printf ("play 1st in list\n"); streamer_set_nextsong (0, 1); #if 0 ps_set_current (playlist_head); if (playlist_current_ptr) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); redraw_ps_row (widget, ps_get_idx_of (playlist_current_ptr)); } #endif } } #if 0 void gtkps_prevsong (void) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); playItem_t *prev = playlist_current_ptr; if (playlist_current_ptr) { printf ("gtkps_prevsong\n"); ps_set_current (playlist_current_ptr->prev); } if (!playlist_current_ptr) { printf ("gtkps_prevsong2\n"); ps_set_current (playlist_tail); } if (playlist_current_ptr != prev) { if (prev) { redraw_ps_row (widget, ps_get_idx_of (prev)); } if (playlist_current_ptr) { redraw_ps_row (widget, ps_get_idx_of (playlist_current_ptr)); } } } #endif void gtkps_randomsong (void) { p_stop (); ps_randomsong (); } void gtkps_stopsong (void) { p_stop (); } void gtkps_pausesong (void) { if (p_ispaused ()) { p_unpause (); } else { p_pause (); } } void gtkps_playsongnum (int idx) { p_stop (); streamer_set_nextsong (idx, 1); #if 0 playItem_t *it = ps_get_for_idx (playlist_row); if (it) { //if (it != playlist_current_ptr) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); int prev = -1; if (playlist_current_ptr) { prev = ps_get_idx_of (playlist_current_ptr); } ps_set_current (it); if (prev != -1) { redraw_ps_row (widget, prev); } redraw_ps_row (widget, idx); } } #endif } static int sb_context_id = -1; static char sb_text[512]; static int last_songpos = -1; void gtkps_update_songinfo (void) { if (!mainwin) { return; } char sbtext_new[512] = "-"; int songpos = 0; if (p_ispaused ()) { strcpy (sbtext_new, "Paused"); songpos = 0; } else if (playlist_current.codec) { codec_lock (); codec_t *c = playlist_current.codec; int minpos = c->info.position / 60; int secpos = c->info.position - minpos * 60; int mindur = playlist_current.duration / 60; int secdur = playlist_current.duration - mindur * 60; const char *mode = c->info.channels == 1 ? "Mono" : "Stereo"; int samplerate = c->info.samplesPerSecond; int bitspersample = c->info.bitsPerSample; float pos = c->info.position; int dur = playlist_current.duration; songpos = pos * 1000 / dur; codec_unlock (); snprintf (sbtext_new, 512, "[%s] %dHz | %d bit | %s | %d:%02d / %d:%02d | %d songs total", playlist_current.filetype ? playlist_current.filetype:"-", samplerate, bitspersample, mode, minpos, secpos, mindur, secdur, ps_getcount ()); } else { strcpy (sbtext_new, "Stopped"); } if (strcmp (sbtext_new, sb_text)) { strcpy (sb_text, sbtext_new); // form statusline GDK_THREADS_ENTER(); // FIXME: don't update if window is not visible GtkStatusbar *sb = GTK_STATUSBAR (lookup_widget (mainwin, "statusbar")); if (sb_context_id == -1) { sb_context_id = gtk_statusbar_get_context_id (sb, "msg"); } gtk_statusbar_pop (sb, sb_context_id); gtk_statusbar_push (sb, sb_context_id, sb_text); GDK_THREADS_LEAVE(); } if (songpos != last_songpos) { last_songpos = songpos; extern int g_disable_seekbar_handler; g_disable_seekbar_handler = 1; GtkRange *seekbar = GTK_RANGE (lookup_widget (mainwin, "playpos")); gtk_range_set_value (seekbar, songpos); g_disable_seekbar_handler = 0; } } void gtkps_songchanged (int from, int to) { if (from >= 0 || to >= 0) { GDK_THREADS_ENTER(); if (to >= 0) { playItem_t *it = ps_get_for_idx (to); char str[600]; char dname[512]; ps_format_item_display_name (it, dname, 512); snprintf (str, 600, "DeaDBeeF - %s", dname); gtk_window_set_title (GTK_WINDOW (mainwin), str); } else { gtk_window_set_title (GTK_WINDOW (mainwin), "DeaDBeeF"); } GtkWidget *widget = lookup_widget (mainwin, "playlist"); if (!widget) { return; } if (from >= 0) { redraw_ps_row (widget, from); } if (to >= 0) { redraw_ps_row (widget, to); } GDK_THREADS_LEAVE(); } } void gtkps_keypress (int keyval, int state) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); GtkWidget *range = lookup_widget (mainwin, "playscroll"); int prev = playlist_row; int newscroll = scrollpos; if ((keyval == GDK_A || keyval == GDK_a) && (state & GDK_CONTROL_MASK)) { // select all for (playItem_t *it = playlist_head; it; it = it->next) { it->selected = 1; } draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, 0, 0, 0, 0, widget->allocation.width, widget->allocation.height); return; } else if (keyval == GDK_Return && playlist_row != -1) { messagepump_push (M_PLAYSONGNUM, 0, playlist_row, 0); return; } else if (keyval == GDK_Delete) { ps_delete_selected (); if (playlist_row >= ps_getcount ()) { playlist_row = ps_getcount () - 1; } gtkps_setup_scrollbar (); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gtkps_expose (widget, 0, 0, widget->allocation.width, widget->allocation.height); return; } else if (keyval == GDK_Down && playlist_row < ps_getcount () - 1) { playlist_row++; if (playlist_row > scrollpos + widget->allocation.height / rowheight - 1) { newscroll = playlist_row - widget->allocation.height / rowheight + 1; } } else if (keyval == GDK_Up && playlist_row > 0) { playlist_row--; if (playlist_row < scrollpos) { newscroll = playlist_row; } } else if (keyval == GDK_Page_Down && playlist_row < ps_getcount () - 1) { playlist_row += 10; if (playlist_row >= ps_getcount ()) { playlist_row = ps_getcount () - 1; } if (playlist_row > scrollpos + widget->allocation.height / rowheight - 1) { newscroll = playlist_row - widget->allocation.height / rowheight + 1; } } else if (keyval == GDK_Page_Up && playlist_row > 0) { playlist_row -= 10; if (playlist_row < 0) { playlist_row = 0; } if (playlist_row < scrollpos) { newscroll = playlist_row; } } else if (keyval == GDK_End && playlist_row != ps_getcount () - 1) { playlist_row = ps_getcount () - 1; if (playlist_row > scrollpos + widget->allocation.height / rowheight - 1) { newscroll = playlist_row - widget->allocation.height / rowheight + 1; } } else if (keyval == GDK_Home && playlist_row != 0) { playlist_row = 0; if (playlist_row < scrollpos) { newscroll = playlist_row; } } if (state & GDK_SHIFT_MASK) { // select all between shift_sel_anchor and playlist_row if (prev != playlist_row) { int start = min (playlist_row, shift_sel_anchor); int end = max (playlist_row, shift_sel_anchor); int idx=0; for (playItem_t *it = playlist_head; it; it = it->next, idx++) { if (idx >= start && idx <= end) { // if (!it->selected) { it->selected = 1; if (newscroll == scrollpos) { redraw_ps_row (widget, idx); } else { redraw_ps_row_novis (widget, idx); } // } } else if (it->selected) { it->selected = 0; if (newscroll == scrollpos) { redraw_ps_row (widget, idx); } else { redraw_ps_row_novis (widget, idx); } } } } } else { // reset selection, set new single cursor/selection if (prev != playlist_row) { shift_sel_anchor = playlist_row; int idx=0; for (playItem_t *it = playlist_head; it; it = it->next, idx++) { if (idx == playlist_row) { if (!it->selected) { it->selected = 1; if (newscroll == scrollpos) { redraw_ps_row (widget, idx); } else { redraw_ps_row_novis (widget, idx); } } } else if (it->selected) { it->selected = 0; if (newscroll == scrollpos) { redraw_ps_row (widget, idx); } else { redraw_ps_row_novis (widget, idx); } } } } } if (newscroll != scrollpos) { gtk_range_set_value (GTK_RANGE (range), newscroll); } } static int drag_motion_y = -1; void gtkps_track_dragdrop (int y) { GtkWidget *widget = lookup_widget (mainwin, "playlist"); if (drag_motion_y != -1) { // erase previous track gdk_draw_drawable (widget->window, widget->style->black_gc, backbuf, 0, drag_motion_y * rowheight-3, 0, drag_motion_y * rowheight-3, widget->allocation.width, 7); } if (y == -1) { drag_motion_y = -1; return; } cairo_t *cr; cr = gdk_cairo_create (widget->window); if (!cr) { return; } drag_motion_y = y / rowheight; cairo_set_source_rgb (cr, 0xf4/255.f, 0x7e/255.f, 0x46/255.f); cairo_rectangle (cr, 0, drag_motion_y * rowheight-1, widget->allocation.width, 3); cairo_rectangle (cr, 0, drag_motion_y * rowheight-3, 3, 7); cairo_rectangle (cr, widget->allocation.width-3, drag_motion_y * rowheight-3, 3, 7); cairo_fill (cr); cairo_destroy (cr); } void gtkps_handle_drag_drop (int drop_y, uint32_t *d, int length) { int drop_row = drop_y / rowheight + scrollpos; playItem_t *drop_before = ps_get_for_idx (drop_row); while (drop_before && drop_before->selected) { drop_before = drop_before->next; } // unlink items from playlist, and link together playItem_t *head = NULL; playItem_t *tail = NULL; int processed = 0; int idx = 0; playItem_t *next = NULL; for (playItem_t *it = playlist_head; it && processed < length; it = next, idx++) { // printf ("idx: %d\n", d[i]); next = it->next; if (idx == d[processed]) { if (it->prev) { it->prev->next = it->next; } else { playlist_head = it->next; } if (it->next) { it->next->prev = it->prev; } else { playlist_tail = it->prev; } if (tail) { tail->next = it; it->prev = tail; tail = it; } else { head = tail = it; it->prev = it->next = NULL; } processed++; // extern int ps_count; // ps_count--; } } // find insertion point playItem_t *drop_after = NULL; if (drop_before) { drop_after = drop_before->prev; } else { drop_after = playlist_tail; } // insert in between head->prev = drop_after; if (drop_after) { drop_after->next = head; } else { playlist_head = head; } tail->next = drop_before; if (drop_before) { drop_before->prev = tail; } else { playlist_tail = tail; } } void on_playlist_drag_end (GtkWidget *widget, GdkDragContext *drag_context, gpointer user_data) { // invalidate entire cache - slow, but rare memset (drawps_cache, 0, sizeof (int16_t) * 3 * ncolumns * nvisiblerows); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gtkps_expose (widget, 0, 0, widget->allocation.width, widget->allocation.height); } void strcopy_special (char *dest, const char *src, int len) { while (len > 0) { if (len >= 3 && !strncmp (src, "\%20", 3)) { *dest = ' '; dest++; src += 3; len -= 3; } else { *dest++ = *src++; len--; } } *dest = 0; } int gtkps_add_file_info_cb (playItem_t *it, void *data) { GtkEntry *e = (GtkEntry *)data; GDK_THREADS_ENTER(); gtk_entry_set_text (GTK_ENTRY (e), it->fname); GDK_THREADS_LEAVE(); usleep (0); return 0; } void gtkps_add_fm_dropped_files (char *ptr, int length, int drop_y) { GDK_THREADS_ENTER(); gtk_widget_set_sensitive (mainwin, FALSE); GtkWidget *d = gtk_dialog_new (); GtkWidget *e = gtk_entry_new (); gtk_widget_set_size_request (e, 500, -1); gtk_widget_set_sensitive (GTK_WIDGET (e), FALSE); gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (d))), e); gtk_widget_show_all (d); GDK_THREADS_LEAVE(); int drop_row = drop_y / rowheight + scrollpos; playItem_t *drop_before = ps_get_for_idx (drop_row); playItem_t *after = NULL; if (drop_before) { after = drop_before->prev; } const gchar *p = ptr; while (*p) { const gchar *pe = p+1; while (*pe && *pe > ' ') { pe++; } if (pe - p < 4096 && pe - p > 7) { char fname[(int)(pe - p)]; strcopy_special (fname, p, pe-p); //strncpy (fname, p, pe - p); //fname[pe - p] = 0; playItem_t *inserted = ps_insert_dir (after, fname + 7, gtkps_add_file_info_cb, e); if (!inserted) { inserted = ps_insert_file (after, fname + 7, gtkps_add_file_info_cb, e); } if (inserted) { after = inserted; } } p = pe; // skip whitespace while (*p && *p <= ' ') { p++; } } free (ptr); ps_shuffle (); // invalidate entire cache - slow, but rare memset (drawps_cache, 0, sizeof (int16_t) * 3 * ncolumns * nvisiblerows); GDK_THREADS_ENTER(); gtk_widget_destroy (d); gtk_widget_set_sensitive (mainwin, TRUE); gtkps_setup_scrollbar (); GtkWidget *widget = lookup_widget (mainwin, "playlist"); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gtkps_expose (widget, 0, 0, widget->allocation.width, widget->allocation.height); GDK_THREADS_LEAVE(); } void gtkps_handle_fm_drag_drop (int drop_y, void *ptr, int length) { // this happens when dropped from file manager char *mem = malloc (length); memcpy (mem, ptr, length); messagepump_push (M_FMDRAGDROP, (uintptr_t)mem, length, drop_y); } void header_draw (GtkWidget *widget) { int x = 0; int w = 100; int h = widget->allocation.height; const char *detail = "toolbar"; for (int i = 0; i < ncolumns; i++) { if (x >= widget->allocation.width) { break; } w = colwidths[i]; gtk_paint_box (widget->style, widget->window, GTK_STATE_NORMAL, GTK_SHADOW_OUT, NULL, NULL, detail, x, 0, w - 2, h); gtk_paint_vline (widget->style, widget->window, GTK_STATE_NORMAL, NULL, NULL, NULL, 0, h, x+w - 2); x += w; } if (x < widget->allocation.width) { gtk_paint_box (widget->style, widget->window, GTK_STATE_INSENSITIVE, GTK_SHADOW_OUT, NULL, NULL, detail, x, 0, widget->allocation.width-x, h); } cairo_t *cr; cr = gdk_cairo_create (widget->window); if (!cr) { return; } x = 0; for (int i = 0; i < ncolumns; i++) { if (x >= widget->allocation.width) { break; } w = colwidths[i]; cairo_move_to (cr, x + 5, 15); if (refit_header[i]) { fit_text (cr, colnames_fitted[i], NULL, colname_max, colnames[i], colwidths[i]-10); refit_header[i] = 0; } cairo_show_text (cr, colnames_fitted[i]); x += w; } cairo_destroy (cr); } gboolean on_header_expose_event (GtkWidget *widget, GdkEventExpose *event, gpointer user_data) { header_draw (widget); return FALSE; } gboolean on_header_configure_event (GtkWidget *widget, GdkEventConfigure *event, gpointer user_data) { return FALSE; } GdkCursor* cursor_sz; GdkCursor* cursor_drag; int header_dragging = -1; int header_sizing = -1; int header_dragpt[2]; void on_header_realize (GtkWidget *widget, gpointer user_data) { // create cursor for sizing headers cursor_sz = gdk_cursor_new (GDK_SB_H_DOUBLE_ARROW); cursor_drag = gdk_cursor_new (GDK_FLEUR); } float last_header_motion_ev = -1; int prev_header_x = -1; gboolean on_header_motion_notify_event (GtkWidget *widget, GdkEventMotion *event, gpointer user_data) { if (header_dragging >= 0) { gdk_window_set_cursor (widget->window, cursor_drag); } else if (header_sizing >= 0) { // limit event rate if (event->time - last_header_motion_ev < 20 || prev_header_x == event->x) { return FALSE; } //printf ("%f\n", event->time - last_header_motion_ev); last_header_motion_ev = event->time; prev_header_x = event->x; gdk_window_set_cursor (widget->window, cursor_sz); // get column start pos int x = 0; for (int i = 0; i < header_sizing; i++) { int w = colwidths[i]; x += w; } int newx = event->x > x + 40 ? event->x : x + 40; colwidths[header_sizing] = newx - x; //printf ("ev->x = %d, w = %d\n", (int)event->x, newx - x - 2); refit_header[header_sizing] = 1; for (int k = 0; k < nvisiblerows; k++) { int cidx = (k * ncolumns + header_sizing) * 3; drawps_cache[cidx+2] = 0; } header_draw (widget); GtkWidget *ps = lookup_widget (mainwin, "playlist"); draw_playlist (ps, 0, 0, ps->allocation.width, ps->allocation.height); gtkps_expose (ps, 0, 0, ps->allocation.width, ps->allocation.height); } else { int x = 0; for (int i = 0; i < ncolumns; i++) { int w = colwidths[i]; if (event->x >= x + w - 2 && event->x <= x + w) { gdk_window_set_cursor (widget->window, cursor_sz); break; } else { gdk_window_set_cursor (widget->window, NULL); } x += w; } } return FALSE; } gboolean on_header_button_press_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data) { if (event->button == 1) { // start sizing/dragging header_dragging = -1; header_sizing = -1; header_dragpt[0] = event->x; header_dragpt[1] = event->y; int x = 0; for (int i = 0; i < ncolumns; i++) { int w = colwidths[i]; if (event->x >= x + w - 2 && event->x <= x + w) { header_sizing = i; header_dragging = -1; break; } else { header_dragging = i; } x += w; } } prev_header_x = -1; last_header_motion_ev = -1; return FALSE; } gboolean on_header_button_release_event (GtkWidget *widget, GdkEventButton *event, gpointer user_data) { header_dragging = -1; header_sizing = -1; int x = 0; for (int i = 0; i < ncolumns; i++) { int w = colwidths[i]; if (event->x >= x + w - 2 && event->x <= x + w) { gdk_window_set_cursor (widget->window, cursor_sz); break; } else { gdk_window_set_cursor (widget->window, NULL); } x += w; } return FALSE; } void gtkps_add_dir (char *folder) { // create window GDK_THREADS_ENTER(); gtk_widget_set_sensitive (mainwin, FALSE); GtkWidget *d = gtk_dialog_new (); GtkWidget *e = gtk_entry_new (); gtk_widget_set_size_request (e, 500, -1); gtk_widget_set_sensitive (GTK_WIDGET (e), FALSE); gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (d))), e); gtk_widget_show_all (d); GDK_THREADS_LEAVE(); ps_add_dir (folder, gtkps_add_file_info_cb, e); g_free (folder); ps_shuffle (); GDK_THREADS_ENTER(); gtk_widget_destroy (d); gtk_widget_set_sensitive (mainwin, TRUE); gtkps_setup_scrollbar (); GtkWidget *widget = lookup_widget (mainwin, "playlist"); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gtkps_expose (widget, 0, 0, widget->allocation.width, widget->allocation.height); GDK_THREADS_LEAVE(); } static void gtkps_addfile_cb (gpointer data, gpointer userdata) { ps_add_file (data, gtkps_add_file_info_cb, userdata); g_free (data); } void gtkps_add_files (GSList *lst) { // create window GDK_THREADS_ENTER(); gtk_widget_set_sensitive (mainwin, FALSE); GtkWidget *d = gtk_dialog_new (); GtkWidget *e = gtk_entry_new (); gtk_widget_set_size_request (e, 500, -1); gtk_widget_set_sensitive (GTK_WIDGET (e), FALSE); gtk_container_add (GTK_CONTAINER (gtk_dialog_get_content_area (GTK_DIALOG (d))), e); gtk_widget_show_all (d); GDK_THREADS_LEAVE(); g_slist_foreach(lst, gtkps_addfile_cb, e); g_slist_free (lst); ps_shuffle (); GDK_THREADS_ENTER(); gtk_widget_destroy (d); gtk_widget_set_sensitive (mainwin, TRUE); gtkps_setup_scrollbar (); GtkWidget *widget = lookup_widget (mainwin, "playlist"); draw_playlist (widget, 0, 0, widget->allocation.width, widget->allocation.height); gtkps_expose (widget, 0, 0, widget->allocation.width, widget->allocation.height); GDK_THREADS_LEAVE(); }