diff --git a/demos/gtk-demo/listview_ucd.c b/demos/gtk-demo/listview_ucd.c index 2a71ef88ed..80749d4dc7 100644 --- a/demos/gtk-demo/listview_ucd.c +++ b/demos/gtk-demo/listview_ucd.c @@ -1,9 +1,11 @@ -/* Lists/Characters +/* Lists/Characters (scroll_to) * * This demo shows a multi-column representation of some parts * of the Unicode Character Database, or UCD. * * The dataset used here has 33 796 items. + * + * It includes some widgets to demo the scroll_to() API functionality. */ #include @@ -18,6 +20,7 @@ struct _UcdItem GObject parent_instance; gunichar codepoint; const char *name; + guint colnumber; }; struct _UcdItemClass @@ -39,7 +42,8 @@ ucd_item_class_init (UcdItemClass *class) static UcdItem * ucd_item_new (gunichar codepoint, - const char *name) + const char *name, + guint col) { UcdItem *item; @@ -47,6 +51,7 @@ ucd_item_new (gunichar codepoint, item->codepoint = codepoint; item->name = name; + item->colnumber = col; return item; } @@ -63,6 +68,12 @@ ucd_item_get_name (UcdItem *item) return item->name; } +static guint +ucd_item_get_colnumber (UcdItem *item) +{ + return item->colnumber; +} + static GListModel * ucd_model_new (void) { @@ -71,6 +82,7 @@ ucd_model_new (void) GVariantIter *iter; GListStore *store; guint u; + guint colnumber; char *name; bytes = g_resources_lookup_data ("/listview_ucd_data/ucdnames.data", 0, NULL); @@ -79,13 +91,15 @@ ucd_model_new (void) iter = g_variant_iter_new (v); store = g_list_store_new (G_TYPE_OBJECT); + colnumber = 1; while (g_variant_iter_next (iter, "(u&s)", &u, &name)) { if (u == 0) continue; - UcdItem *item = ucd_item_new (u, name); + UcdItem *item = ucd_item_new (u, name, colnumber); g_list_store_append (store, item); + colnumber++; g_object_unref (item); } @@ -127,6 +141,24 @@ setup_ellipsizing_label (GtkSignalListItemFactory *factory, gtk_list_item_set_child (GTK_LIST_ITEM (listitem), label); } +static void +bind_colnumber (GtkSignalListItemFactory *factory, + GObject *listitem, + GListModel *ucd_model) +{ + GtkWidget *label; + GObject *item; + uint colnumber; + char buffer[16] = { 0, }; + + label = gtk_list_item_get_child (GTK_LIST_ITEM (listitem)); + item = gtk_list_item_get_item (GTK_LIST_ITEM (listitem)); + colnumber = ucd_item_get_colnumber (UCD_ITEM (item)); + + g_snprintf (buffer, 10, "%u", colnumber); + gtk_label_set_label (GTK_LABEL (label), buffer); +} + static void bind_codepoint (GtkSignalListItemFactory *factory, GObject *listitem) @@ -278,6 +310,21 @@ create_ucd_view (GtkWidget *label) cv = gtk_column_view_new (GTK_SELECTION_MODEL (selection)); gtk_column_view_set_show_column_separators (GTK_COLUMN_VIEW (cv), TRUE); + + // HACK: This is a non-visible empty column we add here because later we use this model + // for the GtkDropDown of columns, and we need an empty item to mean "nothing is selected". + column = gtk_column_view_column_new (NULL, NULL); + gtk_column_view_column_set_visible (column, FALSE); + gtk_column_view_append_column (GTK_COLUMN_VIEW (cv), column); + g_object_unref (column); + + factory = gtk_signal_list_item_factory_new (); + g_signal_connect (factory, "setup", G_CALLBACK (setup_centered_label), NULL); + g_signal_connect (factory, "bind", G_CALLBACK (bind_colnumber), NULL); + column = gtk_column_view_column_new ("Row nº", factory); + gtk_column_view_append_column (GTK_COLUMN_VIEW (cv), column); + g_object_unref (column); + factory = gtk_signal_list_item_factory_new (); g_signal_connect (factory, "setup", G_CALLBACK (setup_centered_label), NULL); g_signal_connect (factory, "bind", G_CALLBACK (bind_codepoint), NULL); @@ -336,6 +383,7 @@ create_ucd_view (GtkWidget *label) } static GtkWidget *window; +static GtkWidget *spin, *check_h, *check_v, *check_ha, *check_va, *list_view, *dropdown_cols; static void remove_provider (gpointer data) @@ -346,13 +394,97 @@ remove_provider (gpointer data) g_object_unref (provider); } +static void +dropdown_cols_changed (GObject *object, + GParamSpec *pspec, + gpointer data) +{ + guint selected_pos; + gboolean any_col_selected; + + selected_pos = gtk_drop_down_get_selected (GTK_DROP_DOWN (object)); + any_col_selected = (selected_pos && selected_pos != GTK_INVALID_LIST_POSITION); + + if (any_col_selected) + { + gtk_widget_set_sensitive (check_h, TRUE); + gtk_widget_set_sensitive (check_ha, TRUE); + } + else + { + gtk_check_button_set_active (GTK_CHECK_BUTTON (check_h), FALSE); + gtk_check_button_set_active (GTK_CHECK_BUTTON (check_ha), FALSE); + gtk_widget_set_sensitive (check_h, FALSE); + gtk_widget_set_sensitive (check_ha, FALSE); + } +} + +static void +scroll_to_cb (GtkWidget *button, gpointer data) +{ + GtkColumnViewColumn *center_column; + GtkScrollInfo *scroll_info; + gboolean col, col_always, row, row_always; + guint pos; + GtkListScrollFlags flags = GTK_LIST_SCROLL_SELECT; + + row = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_v)); + row_always = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_va)); + col = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_h)); + col_always = gtk_check_button_get_active (GTK_CHECK_BUTTON (check_ha)); + + scroll_info = NULL; + if (row || row_always || col || col_always) + { + GtkScrollInfoCenter center_flags = GTK_SCROLL_INFO_CENTER_NONE; + + if (row) + center_flags |= GTK_SCROLL_INFO_CENTER_ROW; + if (row_always) + center_flags |= GTK_SCROLL_INFO_CENTER_ROW_ALWAYS; + if (col) + center_flags |= GTK_SCROLL_INFO_CENTER_COL; + if (col_always) + center_flags |= GTK_SCROLL_INFO_CENTER_COL_ALWAYS; + + scroll_info = gtk_scroll_info_new (); + gtk_scroll_info_set_center_flags (scroll_info, center_flags); + } + + center_column = gtk_drop_down_get_selected_item (GTK_DROP_DOWN (dropdown_cols)); + if (!gtk_column_view_column_get_visible (center_column)) + center_column = NULL; + + pos = (guint) gtk_spin_button_get_value_as_int (GTK_SPIN_BUTTON (spin)); + gtk_column_view_scroll_to (GTK_COLUMN_VIEW (list_view), pos - 1, center_column, flags, scroll_info); +} + +static GtkWidget * +pack_with_label (const char *str, GtkWidget *widget1, GtkWidget *widget2) +{ + GtkWidget *box, *label; + + box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0); + if (str) + { + label = gtk_label_new (str); + gtk_box_append (GTK_BOX (box), label); + } + + gtk_box_append (GTK_BOX (box), widget1); + if (widget2) + gtk_box_append (GTK_BOX (box), widget2); + + return box; +} + GtkWidget * do_listview_ucd (GtkWidget *do_widget) { if (window == NULL) { GtkWidget *listview, *sw; - GtkWidget *box, *label; + GtkWidget *box, *label, *box2, *button; GtkCssProvider *provider; window = gtk_window_new (); @@ -362,6 +494,36 @@ do_listview_ucd (GtkWidget *do_widget) gtk_widget_get_display (do_widget)); g_object_add_weak_pointer (G_OBJECT (window), (gpointer *) &window); + box2 = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 10); + gtk_widget_set_margin_start (GTK_WIDGET (box2), 20); + gtk_widget_set_margin_top (GTK_WIDGET (box2), 20); + gtk_widget_set_margin_bottom (GTK_WIDGET (box2), 15); + + spin = gtk_spin_button_new_with_range (1.0, 33796.0, 1.0); + gtk_spin_button_set_numeric (GTK_SPIN_BUTTON (spin), TRUE); + gtk_spin_button_set_wrap (GTK_SPIN_BUTTON (spin), TRUE); + gtk_widget_set_valign (spin, GTK_ALIGN_CENTER); + + check_v = gtk_check_button_new_with_label ("center row"); + check_va = gtk_check_button_new_with_label ("center row always"); + check_h = gtk_check_button_new_with_label ("center col"); + check_ha = gtk_check_button_new_with_label ("center col always"); + gtk_widget_set_sensitive (check_h, FALSE); + gtk_widget_set_sensitive (check_ha, FALSE); + + button = gtk_button_new_with_label ("Scroll to row / col"); + gtk_widget_set_valign (button, GTK_ALIGN_CENTER); + g_signal_connect (button, "clicked", G_CALLBACK (scroll_to_cb), NULL); + + dropdown_cols = gtk_drop_down_new (NULL, NULL); + gtk_drop_down_set_show_arrow (GTK_DROP_DOWN (dropdown_cols), FALSE); + + gtk_box_append (GTK_BOX (box2), pack_with_label ("Row nº to scroll to:", spin, NULL)); + gtk_box_append (GTK_BOX (box2), pack_with_label ("Col to scroll (optional):", dropdown_cols, NULL)); + gtk_box_append (GTK_BOX (box2), pack_with_label (NULL, pack_with_label (NULL, check_v, check_va), + pack_with_label (NULL, check_h, check_ha))); + gtk_box_append (GTK_BOX (box2), button); + box = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0); label = gtk_label_new (""); gtk_label_set_width_chars (GTK_LABEL (label), 2); @@ -374,10 +536,18 @@ do_listview_ucd (GtkWidget *do_widget) sw = gtk_scrolled_window_new (); gtk_scrolled_window_set_propagate_natural_width (GTK_SCROLLED_WINDOW (sw), TRUE); + gtk_scrolled_window_set_propagate_natural_height (GTK_SCROLLED_WINDOW (sw), TRUE); listview = create_ucd_view (label); + gtk_drop_down_set_model (GTK_DROP_DOWN (dropdown_cols), + gtk_column_view_get_columns (GTK_COLUMN_VIEW (listview))); + gtk_drop_down_set_expression (GTK_DROP_DOWN (dropdown_cols), + gtk_property_expression_new (GTK_TYPE_COLUMN_VIEW_COLUMN, + NULL, "title")); + g_signal_connect (dropdown_cols, "notify::selected", G_CALLBACK (dropdown_cols_changed), NULL); + list_view = listview; gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), listview); gtk_box_prepend (GTK_BOX (box), sw); - gtk_window_set_child (GTK_WINDOW (window), box); + gtk_window_set_child (GTK_WINDOW (window), pack_with_label (NULL, box2, box)); g_object_set_data_full (G_OBJECT (window), "provider", provider, remove_provider); } diff --git a/gtk/gtkcolumnview.c b/gtk/gtkcolumnview.c index f95738bfce..b90b6dc581 100644 --- a/gtk/gtkcolumnview.c +++ b/gtk/gtkcolumnview.c @@ -1786,16 +1786,36 @@ gtk_column_view_scroll_to_column (GtkColumnView *self, GtkColumnViewColumn *column, GtkScrollInfo *scroll_info) { - int col_x, col_width, new_value; + int col_x, col_width, new_value, viewport_size; + gboolean center, center_always, cell_is_visible; + GtkScrollInfoCenter center_flags; gtk_column_view_column_get_header_allocation (column, &col_x, &col_width); + viewport_size = gtk_adjustment_get_page_size (self->hadjustment); + new_value = gtk_scroll_info_compute_for_orientation (scroll_info, GTK_ORIENTATION_HORIZONTAL, col_x, col_width, gtk_adjustment_get_value (self->hadjustment), - gtk_adjustment_get_page_size (self->hadjustment)); + viewport_size); + + if (scroll_info) + { + center_flags = gtk_scroll_info_get_center_flags (scroll_info); + center = (center_flags & GTK_SCROLL_INFO_CENTER_COL); + center_always = (center_flags & GTK_SCROLL_INFO_CENTER_COL_ALWAYS); + + if (new_value == col_x || /* cell aligned at start of viewport */ + new_value + viewport_size == col_x + col_width) /* cell aligned at end of viewport */ + cell_is_visible = FALSE; + else + cell_is_visible = TRUE; + + if (center_always || (center && !cell_is_visible)) + new_value = (int) (col_x - ((viewport_size - col_width) / 2)); + } gtk_adjustment_set_value (self->hadjustment, new_value); diff --git a/gtk/gtklistbase.c b/gtk/gtklistbase.c index 10372c5754..24479ad498 100644 --- a/gtk/gtklistbase.c +++ b/gtk/gtklistbase.c @@ -87,6 +87,9 @@ struct _GtkListBasePrivate GtkGesture *drag_gesture; RubberbandData *rubberband; + /* Whether the user asked to vertically center target item for current scroll operation */ + gboolean center_vertically; + guint autoscroll_id; double autoscroll_delta_x; double autoscroll_delta_y; @@ -825,31 +828,58 @@ gtk_list_base_compute_scroll_align (int cell_start, int visible_size, double current_align, GtkPackType current_side, + GtkScrollInfoCenter center_flags, double *new_align, - GtkPackType *new_side) + GtkPackType *new_side, + gboolean *do_center) { int cell_end, visible_end; visible_end = visible_start + visible_size; cell_end = cell_start + cell_size; + gboolean center_always = (center_flags & GTK_SCROLL_INFO_CENTER_ROW_ALWAYS); + gboolean center = (center_flags & GTK_SCROLL_INFO_CENTER_ROW); + if (do_center) + *do_center = FALSE; if (cell_size <= visible_size) { if (cell_start < visible_start) { - *new_align = 0.0; *new_side = GTK_PACK_START; + if (center || center_always) + { + *new_align = 0.5; + if (do_center) + *do_center = TRUE; + } + else + *new_align = 0.0; } else if (cell_end > visible_end) { - *new_align = 1.0; *new_side = GTK_PACK_END; + if (center || center_always) + { + *new_align = 0.5; + if (do_center) + *do_center = TRUE; + } + else + *new_align = 1.0; } else { /* XXX: start or end here? */ *new_side = GTK_PACK_START; - *new_align = (double) (cell_start - visible_start) / visible_size; + if (center_always || (center && (visible_end == cell_end || visible_start == cell_start))) + { + *new_align = 0.5; + if (do_center) + *do_center = TRUE; + } + else + *new_align = (double) (cell_start - visible_start) / visible_size; } } else @@ -903,12 +933,14 @@ gtk_list_base_scroll_to_item (GtkListBase *self, gtk_list_base_compute_scroll_align (area.y, area.height, y, viewport.height, priv->anchor_align_along, priv->anchor_side_along, - &align_along, &side_along); + scroll ? gtk_scroll_info_get_center_flags (scroll) : 0, + &align_along, &side_along, &priv->center_vertically); gtk_list_base_compute_scroll_align (area.x, area.width, x, viewport.width, priv->anchor_align_across, priv->anchor_side_across, - &align_across, &side_across); + GTK_SCROLL_INFO_CENTER_NONE, + &align_across, &side_across, NULL); gtk_list_base_set_anchor (self, pos, @@ -924,14 +956,24 @@ gtk_list_base_scroll_to_item_action (GtkWidget *widget, GVariant *parameter) { GtkListBase *self = GTK_LIST_BASE (widget); + GtkScrollInfo *scroll_info = NULL; + GtkScrollInfoCenter center_flags = 0; guint pos; - if (!g_variant_check_format_string (parameter, "u", FALSE)) + if (g_variant_check_format_string (parameter, "(uu)", FALSE)) + g_variant_get (parameter, "(uu)", &pos, ¢er_flags); + else if (g_variant_check_format_string (parameter, "u", FALSE)) + g_variant_get (parameter, "u", &pos); + else return; - g_variant_get (parameter, "u", &pos); + if (center_flags) + { + scroll_info = gtk_scroll_info_new (); + gtk_scroll_info_set_center_flags (scroll_info, center_flags); + } - gtk_list_base_scroll_to_item (self, pos, NULL); + gtk_list_base_scroll_to_item (self, pos, scroll_info); } static void @@ -1255,13 +1297,15 @@ gtk_list_base_class_init (GtkListBaseClass *klass) /** * GtkListBase|list.scroll-to-item: * @position: position of item to scroll to + * @center_flags: a set of `GtkScrollInfoCenter` flags + * Since: 4.14 * - * Moves the visible area to the item given in @position with the minimum amount - * of scrolling required. If the item is already visible, nothing happens. + * Moves the visible area to the item given in @position and according + * to the @center_flags provided. */ gtk_widget_class_install_action (widget_class, "list.scroll-to-item", - "u", + "(uu)", gtk_list_base_scroll_to_item_action); /** @@ -2006,6 +2050,7 @@ gtk_list_base_init_real (GtkListBase *self, priv->anchor_side_across = GTK_PACK_START; priv->selected = gtk_list_item_tracker_new (priv->item_manager); priv->focus = gtk_list_item_tracker_new (priv->item_manager); + priv->center_vertically = FALSE; priv->adjustment[GTK_ORIENTATION_HORIZONTAL] = gtk_adjustment_new (0.0, 0.0, 0.0, 0.0, 0.0, 0.0); g_object_ref_sink (priv->adjustment[GTK_ORIENTATION_HORIZONTAL]); @@ -2084,10 +2129,14 @@ gtk_list_base_update_adjustments (GtkListBase *self) { value_across = area.x; value_along = area.y; + + if (priv->center_vertically && G_APPROX_VALUE (priv->anchor_align_along, 0.5, DBL_EPSILON)) + value_along += area.height / 2; + else if (priv->anchor_side_along == GTK_PACK_END) + value_along += area.height; + if (priv->anchor_side_across == GTK_PACK_END) value_across += area.width; - if (priv->anchor_side_along == GTK_PACK_END) - value_along += area.height; value_across -= priv->anchor_align_across * page_across; value_along -= priv->anchor_align_along * page_along; } @@ -2098,6 +2147,10 @@ gtk_list_base_update_adjustments (GtkListBase *self) } } + /* Can be reset now as we've already use it to calculate current scroll operation values */ + if (priv->center_vertically) + priv->center_vertically = FALSE; + gtk_list_base_set_adjustment_values (self, OPPOSITE_ORIENTATION (priv->orientation), value_across, diff --git a/gtk/gtkscrollinfo.c b/gtk/gtkscrollinfo.c index 513eccacc9..d9d4102292 100644 --- a/gtk/gtkscrollinfo.c +++ b/gtk/gtkscrollinfo.c @@ -38,6 +38,7 @@ struct _GtkScrollInfo guint ref_count; gboolean enabled[2]; /* directions */ + GtkScrollInfoCenter center_flags; }; static GtkScrollInfo default_scroll_info = { @@ -219,6 +220,47 @@ gtk_scroll_info_compute_for_orientation (GtkScrollInfo *self, return viewport_origin + delta; } +/** + * gtk_scroll_info_set_center_flags: + * @self: a `GtkScrollInfo` + * @flags: a set of `GtkScrollInfoCenter` flags + * + * Sets the flags that @self will use to center a target item (row or column item) + * when scrolling to it. + * + * Since: 4.14 + */ +void +gtk_scroll_info_set_center_flags (GtkScrollInfo *self, + GtkScrollInfoCenter flags) +{ + g_return_if_fail (self != NULL); + + if (self->center_flags == flags) + return; + + self->center_flags = flags; +} + +/** + * gtk_scroll_info_get_center_flags: + * @self: a `GtkScrollInfo` + * + * Gets the flags that @self will use to center a target item (row or column item) + * when scrolling to it. + * + * Returns: a set of `GtkScrollInfoCenter` flags + * + * Since: 4.14 + */ +GtkScrollInfoCenter +gtk_scroll_info_get_center_flags (GtkScrollInfo *self) +{ + g_return_val_if_fail (self != NULL, 0); + + return self->center_flags; +} + /* * gtk_scroll_info_compute_scroll: * @self: a `GtkScrollInfo` diff --git a/gtk/gtkscrollinfo.h b/gtk/gtkscrollinfo.h index 56cbf63d21..0520b3688b 100644 --- a/gtk/gtkscrollinfo.h +++ b/gtk/gtkscrollinfo.h @@ -30,6 +30,36 @@ G_BEGIN_DECLS +/** + * GtkScrollInfoCenter: + * @GTK_SCROLL_INFO_CENTER_NONE: Don't do anything + * @GTK_SCROLL_INFO_CENTER_ROW: When scrolling vertically + * to a row item, this will center the row item along the + * visible part of list. If row item was already in the + * visible part, this will do nothing. + * @GTK_SCROLL_INFO_CENTER_ROW_ALWAYS: Same as `GTK_SCROLL_INFO_CENTER_ROW` + * but will center the item even if it's already in the + * visible part of list. + * @GTK_SCROLL_INFO_CENTER_COL: When scrolling horizontally + * to a column, this will center the column item across the + * visible part of list. If col item was already in the + * visible part of list, this will do nothing. + * @GTK_SCROLL_INFO_CENTER_COL_ALWAYS: Same as `GTK_SCROLL_INFO_CENTER_COL` + * but will center the item even if it's already in the + * visible part of list. + * + * How we would like to center target item when scrolling to it. + * + * Since: 4.14 + */ +typedef enum { + GTK_SCROLL_INFO_CENTER_NONE = 0, + GTK_SCROLL_INFO_CENTER_ROW = 1 << 0, + GTK_SCROLL_INFO_CENTER_ROW_ALWAYS = 1 << 1, + GTK_SCROLL_INFO_CENTER_COL = 1 << 2, + GTK_SCROLL_INFO_CENTER_COL_ALWAYS = 1 << 3 +} GtkScrollInfoCenter; + #define GTK_TYPE_SCROLL_INFO (gtk_scroll_info_get_type ()) GDK_AVAILABLE_IN_4_12 @@ -54,6 +84,12 @@ void gtk_scroll_info_set_enable_vertical (GtkScrollInfo GDK_AVAILABLE_IN_4_12 gboolean gtk_scroll_info_get_enable_vertical (GtkScrollInfo *self); +GDK_AVAILABLE_IN_4_14 +void gtk_scroll_info_set_center_flags (GtkScrollInfo *self, + GtkScrollInfoCenter flags); +GDK_AVAILABLE_IN_4_14 +GtkScrollInfoCenter gtk_scroll_info_get_center_flags (GtkScrollInfo *self); + G_DEFINE_AUTOPTR_CLEANUP_FUNC(GtkScrollInfo, gtk_scroll_info_unref)