gtklevelbar.c 41.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* GTK - The GIMP Toolkit
 * Copyright © 2012 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library. If not, see <http://www.gnu.org/licenses/>.
 *
 * Author: Cosimo Cecchi <cosimoc@gnome.org>
 *
 */

/**
22
 * GtkLevelBar:
23
 *
Matthias Clasen's avatar
Matthias Clasen committed
24
25
26
27
 * `GtkLevelBar` is a widget that can be used as a level indicator.
 *
 * Typical use cases are displaying the strength of a password, or
 * showing the charge level of a battery.
28
 *
Matthias Clasen's avatar
Matthias Clasen committed
29
30
31
32
 * ![An example GtkLevelBar](levelbar.png)
 *
 * Use [method@Gtk.LevelBar.set_value] to set the current value, and
 * [method@Gtk.LevelBar.add_offset_value] to set the value offsets at which
33
 * the bar will be considered in a different state. GTK will add a few
Matthias Clasen's avatar
Matthias Clasen committed
34
35
 * offsets by default on the level bar: %GTK_LEVEL_BAR_OFFSET_LOW,
 * %GTK_LEVEL_BAR_OFFSET_HIGH and %GTK_LEVEL_BAR_OFFSET_FULL, with
36
 * values 0.25, 0.75 and 1.0 respectively.
37
 *
38
 * Note that it is your responsibility to update preexisting offsets
Matthias Clasen's avatar
Matthias Clasen committed
39
 * when changing the minimum or maximum value. GTK will simply clamp
40
41
 * them to the new range.
 *
42
43
 * ## Adding a custom offset on the bar
 *
Matthias Clasen's avatar
Matthias Clasen committed
44
 * ```c
45
46
47
 * static GtkWidget *
 * create_level_bar (void)
 * {
48
49
 *   GtkWidget *widget;
 *   GtkLevelBar *bar;
50
 *
51
52
 *   widget = gtk_level_bar_new ();
 *   bar = GTK_LEVEL_BAR (widget);
53
 *
54
 *   // This changes the value of the default low offset
55
56
57
58
 *
 *   gtk_level_bar_add_offset_value (bar,
 *                                   GTK_LEVEL_BAR_OFFSET_LOW,
 *                                   0.10);
59
 *
60
 *   // This adds a new offset to the bar; the application will
Matthias Clasen's avatar
Matthias Clasen committed
61
 *   // be able to change its color CSS like this:
62
 *   //
Matthias Clasen's avatar
Matthias Clasen committed
63
64
 *   // levelbar block.my-offset {
 *   //   background-color: magenta;
65
66
67
68
 *   //   border-style: solid;
 *   //   border-color: black;
 *   //   border-style: 1px;
 *   // }
69
 *
70
71
72
 *   gtk_level_bar_add_offset_value (bar, "my-offset", 0.60);
 *
 *   return widget;
73
 * }
Matthias Clasen's avatar
Matthias Clasen committed
74
 * ```
75
 *
Matthias Clasen's avatar
Matthias Clasen committed
76
77
78
79
80
 * The default interval of values is between zero and one, but it’s possible
 * to modify the interval using [method@Gtk.LevelBar.set_min_value] and
 * [method@Gtk.LevelBar.set_max_value]. The value will be always drawn in
 * proportion to the admissible interval, i.e. a value of 15 with a specified
 * interval between 10 and 20 is equivalent to a value of 0.5 with an interval
Matthias Clasen's avatar
Matthias Clasen committed
81
 * between 0 and 1. When %GTK_LEVEL_BAR_MODE_DISCRETE is used, the bar level
Matthias Clasen's avatar
Matthias Clasen committed
82
83
84
 * is rendered as a finite number of separated blocks instead of a single one.
 * The number of blocks that will be rendered is equal to the number of units
 * specified by the admissible interval.
85
 *
86
 * For instance, to build a bar rendered with five blocks, it’s sufficient to
Matthias Clasen's avatar
Matthias Clasen committed
87
88
 * set the minimum value to 0 and the maximum value to 5 after changing the
 * indicator mode to discrete.
89
 *
90
91
 * # GtkLevelBar as GtkBuildable
 *
Matthias Clasen's avatar
Matthias Clasen committed
92
 * The `GtkLevelBar` implementation of the `GtkBuildable` interface supports a
93
94
 * custom <offsets> element, which can contain any number of <offset> elements,
 * each of which must have name and value attributes.
95
96
97
 *
 * # CSS nodes
 *
Matthias Clasen's avatar
Matthias Clasen committed
98
 * ```
Matthias Clasen's avatar
Matthias Clasen committed
99
 * levelbar[.discrete]
100
 * ╰── trough
101
 *     ├── block.filled.level-name
Matthias Clasen's avatar
Matthias Clasen committed
102
103
104
 *     ┊
 *     ├── block.empty
 *     ┊
Matthias Clasen's avatar
Matthias Clasen committed
105
 * ```
106
 *
Matthias Clasen's avatar
Matthias Clasen committed
107
 * `GtkLevelBar` has a main CSS node with name levelbar and one of the style
Matthias Clasen's avatar
Matthias Clasen committed
108
109
110
111
 * classes .discrete or .continuous and a subnode with name trough. Below the
 * trough node are a number of nodes with name block and style class .filled
 * or .empty. In continuous mode, there is exactly one node of each, in discrete
 * mode, the number of filled and unfilled nodes corresponds to blocks that are
112
 * drawn. The block.filled nodes also get a style class .level-name corresponding
Matthias Clasen's avatar
Matthias Clasen committed
113
 * to the level for the current value.
114
115
116
 *
 * In horizontal orientation, the nodes are always arranged from left to right,
 * regardless of text direction.
117
118
119
 *
 * # Accessibility
 *
Matthias Clasen's avatar
Matthias Clasen committed
120
 * `GtkLevelBar` uses the %GTK_ACCESSIBLE_ROLE_METER role.
121
122
123
 */
#include "config.h"

Timm Bäder's avatar
Timm Bäder committed
124
#include "gtkbinlayout.h"
125
#include "gtkbuildable.h"
126
#include "gtkbuilderprivate.h"
127
#include "gtkgizmoprivate.h"
128
129
130
#include "gtkintl.h"
#include "gtklevelbar.h"
#include "gtkmarshalers.h"
131
#include "gtkorientable.h"
132
#include "gtkcssnodeprivate.h"
133
#include "gtktypebuiltins.h"
134
#include "gtkwidgetprivate.h"
135
136
137
138
139
140
141
142
143

#include <math.h>
#include <stdlib.h>

enum {
  PROP_VALUE = 1,
  PROP_MIN_VALUE,
  PROP_MAX_VALUE,
  PROP_MODE,
144
  PROP_INVERTED,
145
146
147
148
149
150
151
152
153
154
155
156
  LAST_PROPERTY,
  PROP_ORIENTATION /* overridden */
};

enum {
  SIGNAL_OFFSET_CHANGED,
  NUM_SIGNALS
};

static GParamSpec *properties[LAST_PROPERTY] = { NULL, };
static guint signals[NUM_SIGNALS] = { 0, };

157
158
typedef struct _GtkLevelBarClass   GtkLevelBarClass;

159
typedef struct {
Benjamin Otte's avatar
Benjamin Otte committed
160
  char *name;
161
  double value;
162
163
} GtkLevelBarOffset;

Matthias Clasen's avatar
Matthias Clasen committed
164
165
166
struct _GtkLevelBar {
  GtkWidget parent_instance;

167
168
  GtkOrientation orientation;

169
170
  GtkLevelBarMode bar_mode;

171
172
173
  double min_value;
  double max_value;
  double cur_value;
174
175
176

  GList *offsets;

177
178
  GtkWidget *trough_widget;
  GtkWidget **block_widget;
Matthias Clasen's avatar
Matthias Clasen committed
179
  guint n_blocks;
180

181
  guint inverted : 1;
182
183
};

184
185
186
187
struct _GtkLevelBarClass {
  GtkWidgetClass parent_class;

  void (* offset_changed) (GtkLevelBar *self,
Benjamin Otte's avatar
Benjamin Otte committed
188
                           const char *name);
189
190
};

191
static void gtk_level_bar_set_value_internal (GtkLevelBar *self,
192
                                              double       value);
193

194
195
196
197
198
199
200
static void gtk_level_bar_buildable_init (GtkBuildableIface *iface);

G_DEFINE_TYPE_WITH_CODE (GtkLevelBar, gtk_level_bar, GTK_TYPE_WIDGET,
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)
                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
                                                gtk_level_bar_buildable_init))

201
static GtkLevelBarOffset *
Benjamin Otte's avatar
Benjamin Otte committed
202
gtk_level_bar_offset_new (const char *name,
203
                          double       value)
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
{
  GtkLevelBarOffset *offset = g_slice_new0 (GtkLevelBarOffset);

  offset->name = g_strdup (name);
  offset->value = value;

  return offset;
}

static void
gtk_level_bar_offset_free (GtkLevelBarOffset *offset)
{
  g_free (offset->name);
  g_slice_free (GtkLevelBarOffset, offset);
}

Benjamin Otte's avatar
Benjamin Otte committed
220
static int
221
222
223
224
offset_find_func (gconstpointer data,
                  gconstpointer user_data)
{
  const GtkLevelBarOffset *offset = data;
Benjamin Otte's avatar
Benjamin Otte committed
225
  const char *name = user_data;
226
227
228
229

  return g_strcmp0 (name, offset->name);
}

Benjamin Otte's avatar
Benjamin Otte committed
230
static int
231
232
233
234
235
236
237
238
239
240
241
offset_sort_func (gconstpointer a,
                  gconstpointer b)
{
  const GtkLevelBarOffset *offset_a = a;
  const GtkLevelBarOffset *offset_b = b;

  return (offset_a->value > offset_b->value);
}

static gboolean
gtk_level_bar_ensure_offset (GtkLevelBar *self,
Benjamin Otte's avatar
Benjamin Otte committed
242
                             const char *name,
243
                             double       value)
244
245
246
{
  GList *existing;
  GtkLevelBarOffset *offset = NULL;
247
  GtkLevelBarOffset *new_offset;
248

249
  existing = g_list_find_custom (self->offsets, name, offset_find_func);
250
251
252
253
254
255
  if (existing)
    offset = existing->data;

  if (offset && (offset->value == value))
    return FALSE;

256
257
  new_offset = gtk_level_bar_offset_new (name, value);

258
259
260
  if (offset)
    {
      gtk_level_bar_offset_free (offset);
261
      self->offsets = g_list_delete_link (self->offsets, existing);
262
263
    }

264
  self->offsets = g_list_insert_sorted (self->offsets, new_offset, offset_sort_func);
265
266
267
268

  return TRUE;
}

269
#ifndef G_DISABLE_CHECKS
270
271
static gboolean
gtk_level_bar_value_in_interval (GtkLevelBar *self,
272
                                 double       value)
273
{
274
275
  return ((value >= self->min_value) &&
          (value <= self->max_value));
276
}
277
#endif
278

Benjamin Otte's avatar
Benjamin Otte committed
279
static int
280
281
gtk_level_bar_get_num_blocks (GtkLevelBar *self)
{
282
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
283
    return 1;
284
  else if (self->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE)
Benjamin Otte's avatar
Benjamin Otte committed
285
    return MAX (1, (int) (round (self->max_value) - round (self->min_value)));
286
287
288
289

  return 0;
}

Benjamin Otte's avatar
Benjamin Otte committed
290
static int
291
292
gtk_level_bar_get_num_block_nodes (GtkLevelBar *self)
{
293
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
294
295
296
297
298
    return 2;
  else
    return gtk_level_bar_get_num_blocks (self);
}

299
300
static void
gtk_level_bar_get_min_block_size (GtkLevelBar *self,
Benjamin Otte's avatar
Benjamin Otte committed
301
302
                                  int         *block_width,
                                  int         *block_height)
303
304
{
  guint i, n_blocks;
Benjamin Otte's avatar
Benjamin Otte committed
305
  int width, height;
306
307
308
309
310
311

  *block_width = *block_height = 0;
  n_blocks = gtk_level_bar_get_num_block_nodes (self);

  for (i = 0; i < n_blocks; i++)
    {
312
      gtk_widget_measure (self->block_widget[i],
313
314
315
316
                          GTK_ORIENTATION_HORIZONTAL,
                          -1,
                          &width, NULL,
                          NULL, NULL);
317
      gtk_widget_measure (self->block_widget[i],
318
319
320
321
                          GTK_ORIENTATION_VERTICAL,
                          -1,
                          &height, NULL,
                          NULL, NULL);
322
323
324
325
326
327

      *block_width = MAX (width, *block_width);
      *block_height = MAX (height, *block_height);
    }
}

Cosimo Cecchi's avatar
Cosimo Cecchi committed
328
329
330
331
static gboolean
gtk_level_bar_get_real_inverted (GtkLevelBar *self)
{
  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL &&
332
333
      self->orientation == GTK_ORIENTATION_HORIZONTAL)
    return !self->inverted;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
334

335
  return self->inverted;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
336
337
}

338
static void
339
340
gtk_level_bar_render_trough (GtkGizmo    *gizmo,
                             GtkSnapshot *snapshot)
341
{
342
343
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (GTK_WIDGET (gizmo)));
Matthias Clasen's avatar
Matthias Clasen committed
344

345
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
346
347
    {
      gboolean inverted;
348

349
      inverted = gtk_level_bar_get_real_inverted (self);
350

351
      /* render the empty (unfilled) part */
352
      gtk_widget_snapshot_child (widget, self->block_widget[inverted ? 0 : 1], snapshot);
353

354
      /* now render the filled part on top of it */
355
356
      if (self->cur_value != 0)
        gtk_widget_snapshot_child (widget, self->block_widget[inverted ? 1 : 0], snapshot);
357
358
359
    }
  else
    {
Benjamin Otte's avatar
Benjamin Otte committed
360
      int num_blocks, i;
361

362
      num_blocks = gtk_level_bar_get_num_blocks (self);
Cosimo Cecchi's avatar
Cosimo Cecchi committed
363

364
      for (i = 0; i < num_blocks; i++)
365
        gtk_widget_snapshot_child (widget, self->block_widget[i], snapshot);
366
    }
Cosimo Cecchi's avatar
Cosimo Cecchi committed
367
}
368
369

static void
370
gtk_level_bar_measure_trough (GtkGizmo       *gizmo,
Cosimo Cecchi's avatar
Cosimo Cecchi committed
371
372
373
374
375
                              GtkOrientation  orientation,
                              int             for_size,
                              int            *minimum,
                              int            *natural,
                              int            *minimum_baseline,
376
                              int            *natural_baseline)
377
{
378
379
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (widget));
Benjamin Otte's avatar
Benjamin Otte committed
380
381
  int num_blocks, size;
  int block_width, block_height;
382

Cosimo Cecchi's avatar
Cosimo Cecchi committed
383
  num_blocks = gtk_level_bar_get_num_blocks (self);
384
385
  gtk_level_bar_get_min_block_size (self, &block_width, &block_height);

Cosimo Cecchi's avatar
Cosimo Cecchi committed
386
  if (orientation == GTK_ORIENTATION_HORIZONTAL)
387
    {
388
      if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
Cosimo Cecchi's avatar
Cosimo Cecchi committed
389
390
391
        size = num_blocks * block_width;
      else
        size = block_width;
392
393
394
    }
  else
    {
395
      if (self->orientation == GTK_ORIENTATION_VERTICAL)
Cosimo Cecchi's avatar
Cosimo Cecchi committed
396
        size = num_blocks * block_height;
397
      else
Cosimo Cecchi's avatar
Cosimo Cecchi committed
398
        size = block_height;
399
    }
Cosimo Cecchi's avatar
Cosimo Cecchi committed
400
401
402

  *minimum = size;
  *natural = size;
403
404
}

Cosimo Cecchi's avatar
Cosimo Cecchi committed
405
static void
406
407
408
409
gtk_level_bar_allocate_trough_continuous (GtkLevelBar *self,
                                          int          width,
                                          int          height,
                                          int          baseline)
410
{
411
  GtkAllocation block_area;
412
  double fill_percentage;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
413
  gboolean inverted;
414
  int block_min;
415

Cosimo Cecchi's avatar
Cosimo Cecchi committed
416
  inverted = gtk_level_bar_get_real_inverted (self);
417

Cosimo Cecchi's avatar
Cosimo Cecchi committed
418
  /* allocate the empty (unfilled) part */
419
  gtk_widget_size_allocate (self->block_widget[inverted ? 0 : 1],
420
                            &(GtkAllocation) {0, 0, width, height},
421
                            baseline);
422

423
  if (self->cur_value == 0)
424
425
    return;

Cosimo Cecchi's avatar
Cosimo Cecchi committed
426
  /* now allocate the filled part */
427
  block_area = (GtkAllocation) {0, 0, width, height};
428
429
  fill_percentage = (self->cur_value - self->min_value) /
    (self->max_value - self->min_value);
430

431
432
  gtk_widget_measure (self->block_widget[inverted ? 1 : 0],
                      self->orientation, -1,
433
434
435
                      &block_min, NULL,
                      NULL, NULL);

436
  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
Cosimo Cecchi's avatar
Cosimo Cecchi committed
437
    {
Benjamin Otte's avatar
Benjamin Otte committed
438
      block_area.width = (int) floor (block_area.width * fill_percentage);
439
      block_area.width = MAX (block_area.width, block_min);
440

Cosimo Cecchi's avatar
Cosimo Cecchi committed
441
      if (inverted)
442
        block_area.x += width - block_area.width;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
443
444
445
    }
  else
    {
Benjamin Otte's avatar
Benjamin Otte committed
446
      block_area.height = (int) floor (block_area.height * fill_percentage);
447
      block_area.height = MAX (block_area.height, block_min);
448

Cosimo Cecchi's avatar
Cosimo Cecchi committed
449
      if (inverted)
450
        block_area.y += height - block_area.height;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
451
452
    }

453
  gtk_widget_size_allocate (self->block_widget[inverted ? 1 : 0],
454
                            &block_area,
455
                            baseline);
456
457
458
}

static void
Cosimo Cecchi's avatar
Cosimo Cecchi committed
459
gtk_level_bar_allocate_trough_discrete (GtkLevelBar *self,
460
461
462
                                        int          width,
                                        int          height,
                                        int          baseline)
463
{
464
  GtkAllocation block_area;
Benjamin Otte's avatar
Benjamin Otte committed
465
466
  int num_blocks, i;
  int block_width, block_height;
467

Cosimo Cecchi's avatar
Cosimo Cecchi committed
468
  gtk_level_bar_get_min_block_size (self, &block_width, &block_height);
469
470
  num_blocks = gtk_level_bar_get_num_blocks (self);

471
472
473
  if (num_blocks == 0)
    return;

474
  if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
475
    {
Benjamin Otte's avatar
Benjamin Otte committed
476
      block_width = MAX (block_width, (int) floor (width / num_blocks));
477
      block_height = height;
478
    }
479
  else
480
    {
481
      block_width = width;
Benjamin Otte's avatar
Benjamin Otte committed
482
      block_height = MAX (block_height, (int) floor (height / num_blocks));
483
    }
Cosimo Cecchi's avatar
Cosimo Cecchi committed
484

485
486
  block_area.x = 0;
  block_area.y = 0;
Cosimo Cecchi's avatar
Cosimo Cecchi committed
487
488
  block_area.width = block_width;
  block_area.height = block_height;
489

Cosimo Cecchi's avatar
Cosimo Cecchi committed
490
491
  for (i = 0; i < num_blocks; i++)
    {
492
      gtk_widget_size_allocate (self->block_widget[i],
493
                                &block_area,
494
                                baseline);
Cosimo Cecchi's avatar
Cosimo Cecchi committed
495

496
      if (self->orientation == GTK_ORIENTATION_HORIZONTAL)
Cosimo Cecchi's avatar
Cosimo Cecchi committed
497
498
499
500
        block_area.x += block_area.width;
      else
        block_area.y += block_area.height;
    }
501
502
503
}

static void
504
505
506
507
gtk_level_bar_allocate_trough (GtkGizmo *gizmo,
                               int       width,
                               int       height,
                               int       baseline)
508
{
509
510
  GtkWidget *widget = GTK_WIDGET (gizmo);
  GtkLevelBar *self = GTK_LEVEL_BAR (gtk_widget_get_parent (widget));
511

512
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
513
    gtk_level_bar_allocate_trough_continuous (self, width, height, baseline);
514
  else
515
    gtk_level_bar_allocate_trough_discrete (self, width, height, baseline);
516
517
}

Matthias Clasen's avatar
Matthias Clasen committed
518
519
520
521
522
523
static void
update_block_nodes (GtkLevelBar *self)
{
  guint n_blocks;
  guint i;

524
  n_blocks = gtk_level_bar_get_num_block_nodes (self);
Matthias Clasen's avatar
Matthias Clasen committed
525

526
  if (self->n_blocks == n_blocks)
Matthias Clasen's avatar
Matthias Clasen committed
527
    return;
528
  else if (n_blocks < self->n_blocks)
Matthias Clasen's avatar
Matthias Clasen committed
529
    {
530
      for (i = n_blocks; i < self->n_blocks; i++)
531
        {
532
          gtk_widget_unparent (self->block_widget[i]);
533
        }
534
535
      self->block_widget = g_renew (GtkWidget*, self->block_widget, n_blocks);
      self->n_blocks = n_blocks;
Matthias Clasen's avatar
Matthias Clasen committed
536
537
538
    }
  else
    {
539
540
      self->block_widget = g_renew (GtkWidget*, self->block_widget, n_blocks);
      for (i = self->n_blocks; i < n_blocks; i++)
Matthias Clasen's avatar
Matthias Clasen committed
541
        {
542
543
544
          self->block_widget[i] = gtk_gizmo_new_with_role ("block",
                                                           GTK_ACCESSIBLE_ROLE_NONE,
                                                           NULL, NULL, NULL, NULL, NULL, NULL);
545
          gtk_widget_insert_before (self->block_widget[i], GTK_WIDGET (self->trough_widget), NULL);
Matthias Clasen's avatar
Matthias Clasen committed
546
        }
547
      self->n_blocks = n_blocks;
Matthias Clasen's avatar
Matthias Clasen committed
548
549
550
551
552
    }
}

static void
update_mode_style_classes (GtkLevelBar *self)
553
{
Matthias Clasen's avatar
Matthias Clasen committed
554
  GtkCssNode *widget_node;
555

Matthias Clasen's avatar
Matthias Clasen committed
556
  widget_node = gtk_widget_get_css_node (GTK_WIDGET (self));
557
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
558
    {
Matthias Clasen's avatar
Matthias Clasen committed
559
560
      gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("discrete"));
      gtk_css_node_add_class (widget_node, g_quark_from_static_string ("continuous"));
561
    }
562
  else if (self->bar_mode == GTK_LEVEL_BAR_MODE_DISCRETE)
563
    {
Matthias Clasen's avatar
Matthias Clasen committed
564
565
      gtk_css_node_add_class (widget_node, g_quark_from_static_string ("discrete"));
      gtk_css_node_remove_class (widget_node, g_quark_from_static_string ("continuous"));
566
567
568
569
    }
}

static void
Matthias Clasen's avatar
Matthias Clasen committed
570
update_level_style_classes (GtkLevelBar *self)
571
{
572
  double value;
Benjamin Otte's avatar
Benjamin Otte committed
573
  const char *value_class = NULL;
574
575
  GtkLevelBarOffset *offset, *prev_offset;
  GList *l;
Benjamin Otte's avatar
Benjamin Otte committed
576
  int num_filled, num_blocks, i;
577
  gboolean inverted;
578

579
580
  value = gtk_level_bar_get_value (self);

581
  for (l = self->offsets; l != NULL; l = l->next)
582
583
584
585
    {
      offset = l->data;

      /* find the right offset for our style class */
586
      if (value <= offset->value)
587
        {
588
589
590
591
592
593
594
595
596
597
          if (l->prev == NULL)
            {
              value_class = offset->name;
            }
          else
            {
              prev_offset = l->prev->data;
              if (prev_offset->value < value)
                value_class = offset->name;
            }
Matthias Clasen's avatar
Matthias Clasen committed
598
599
600
601
602
603
        }

      if (value_class)
        break;
    }

604
605
  inverted = gtk_level_bar_get_real_inverted (self);
  num_blocks = gtk_level_bar_get_num_block_nodes (self);
Matthias Clasen's avatar
Matthias Clasen committed
606

607
  if (self->bar_mode == GTK_LEVEL_BAR_MODE_CONTINUOUS)
608
609
    num_filled = 1;
  else
Benjamin Otte's avatar
Benjamin Otte committed
610
    num_filled = MIN (num_blocks, (int) round (self->cur_value) - (int) round (self->min_value));
Cosimo Cecchi's avatar
Cosimo Cecchi committed
611

612
  for (i = 0; i < num_filled; i++)
613
    {
614
      GtkCssNode *node = gtk_widget_get_css_node (self->block_widget[inverted ? num_blocks - 1 - i : i]);
615

616
617
      gtk_css_node_set_classes (node, NULL);
      gtk_css_node_add_class (node, g_quark_from_static_string ("filled"));
618
619

      if (value_class)
620
        gtk_css_node_add_class (node, g_quark_from_string (value_class));
621
    }
Matthias Clasen's avatar
Matthias Clasen committed
622

623
  for (; i < num_blocks; i++)
624
    {
625
      GtkCssNode *node = gtk_widget_get_css_node (self->block_widget[inverted ? num_blocks - 1 - i : i]);
626

627
628
      gtk_css_node_set_classes (node, NULL);
      gtk_css_node_add_class (node, g_quark_from_static_string ("empty"));
629
    }
630
631
}

Matthias Clasen's avatar
Matthias Clasen committed
632
633
634
635
636
637
638
639
640
641
642
static void
gtk_level_bar_direction_changed (GtkWidget        *widget,
                                 GtkTextDirection  previous_dir)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (widget);

  update_level_style_classes (self);

  GTK_WIDGET_CLASS (gtk_level_bar_parent_class)->direction_changed (widget, previous_dir);
}

643
644
645
646
static void
gtk_level_bar_ensure_offsets_in_range (GtkLevelBar *self)
{
  GtkLevelBarOffset *offset;
647
  GList *l = self->offsets;
648
649
650
651
652
653

  while (l != NULL)
    {
      offset = l->data;
      l = l->next;

654
655
656
657
      if (offset->value < self->min_value)
        gtk_level_bar_ensure_offset (self, offset->name, self->min_value);
      else if (offset->value > self->max_value)
        gtk_level_bar_ensure_offset (self, offset->name, self->max_value);
658
659
660
661
662
    }
}

typedef struct {
  GtkLevelBar *self;
663
  GtkBuilder *builder;
664
665
666
667
  GList *offsets;
} OffsetsParserData;

static void
668
offset_start_element (GtkBuildableParseContext *context,
Benjamin Otte's avatar
Benjamin Otte committed
669
670
671
                      const char               *element_name,
                      const char              **names,
                      const char              **values,
672
673
                      gpointer                  user_data,
                      GError                  **error)
674
{
675
  OffsetsParserData *data = user_data;
676
677

  if (strcmp (element_name, "offsets") == 0)
678
679
680
681
682
683
684
685
686
    {
      if (!_gtk_builder_check_parent (data->builder, context, "object", error))
        return;

      if (!g_markup_collect_attributes (element_name, names, values, error,
                                        G_MARKUP_COLLECT_INVALID, NULL, NULL,
                                        G_MARKUP_COLLECT_INVALID))
        _gtk_builder_prefix_error (data->builder, context, error);
    }
687
688
  else if (strcmp (element_name, "offset") == 0)
    {
Benjamin Otte's avatar
Benjamin Otte committed
689
690
      const char *name;
      const char *value;
691
692
693
694
695
696
697
698
699
700
      GValue gvalue = G_VALUE_INIT;
      GtkLevelBarOffset *offset;

      if (!_gtk_builder_check_parent (data->builder, context, "offsets", error))
        return;

      if (!g_markup_collect_attributes (element_name, names, values, error,
                                        G_MARKUP_COLLECT_STRING, "name", &name,
                                        G_MARKUP_COLLECT_STRING, "value", &value,
                                        G_MARKUP_COLLECT_INVALID))
701
        {
702
703
          _gtk_builder_prefix_error (data->builder, context, error);
          return;
704
705
        }

706
      if (!gtk_builder_value_from_string_type (data->builder, G_TYPE_DOUBLE, value, &gvalue, error))
707
        {
708
709
          _gtk_builder_prefix_error (data->builder, context, error);
          return;
710
        }
711
712
713

      offset = gtk_level_bar_offset_new (name, g_value_get_double (&gvalue));
      data->offsets = g_list_prepend (data->offsets, offset);
714
715
716
    }
  else
    {
717
718
719
      _gtk_builder_error_unhandled_tag (data->builder, context,
                                        "GtkLevelBar", element_name,
                                        error);
720
721
722
    }
}

723
static const GtkBuildableParser offset_parser =
724
725
726
727
{
  offset_start_element
};

728
729
static GtkBuildableIface *parent_buildable_iface;

730
static gboolean
731
732
733
gtk_level_bar_buildable_custom_tag_start (GtkBuildable       *buildable,
                                          GtkBuilder         *builder,
                                          GObject            *child,
Benjamin Otte's avatar
Benjamin Otte committed
734
                                          const char         *tagname,
735
736
                                          GtkBuildableParser *parser,
                                          gpointer           *parser_data)
737
{
738
  OffsetsParserData *data;
739

740
741
742
743
  if (parent_buildable_iface->custom_tag_start (buildable, builder, child,
                                                tagname, parser, parser_data))
    return TRUE;

744
745
746
747
748
749
  if (child)
    return FALSE;

  if (strcmp (tagname, "offsets") != 0)
    return FALSE;

750
751
752
753
  data = g_slice_new0 (OffsetsParserData);
  data->self = GTK_LEVEL_BAR (buildable);
  data->builder = builder;
  data->offsets = NULL;
754
755

  *parser = offset_parser;
756
  *parser_data = data;
757
758
759
760
761
762
763
764

  return TRUE;
}

static void
gtk_level_bar_buildable_custom_finished (GtkBuildable *buildable,
                                         GtkBuilder   *builder,
                                         GObject      *child,
Benjamin Otte's avatar
Benjamin Otte committed
765
                                         const char   *tagname,
766
767
                                         gpointer      user_data)
{
768
  OffsetsParserData *data = user_data;
769
770
771
772
  GtkLevelBar *self;
  GtkLevelBarOffset *offset;
  GList *l;

773
  self = data->self;
774
775

  if (strcmp (tagname, "offsets") != 0)
776
777
778
779
780
    {
      parent_buildable_iface->custom_finished (buildable, builder, child,
                                               tagname, user_data);
      return;
    }
781

782
  for (l = data->offsets; l != NULL; l = l->next)
783
784
785
786
787
    {
      offset = l->data;
      gtk_level_bar_add_offset_value (self, offset->name, offset->value);
    }

788
789
  g_list_free_full (data->offsets, (GDestroyNotify) gtk_level_bar_offset_free);
  g_slice_free (OffsetsParserData, data);
790
791
792
793
794
}

static void
gtk_level_bar_buildable_init (GtkBuildableIface *iface)
{
795
796
  parent_buildable_iface = g_type_interface_peek_parent (iface);

797
798
799
800
801
  iface->custom_tag_start = gtk_level_bar_buildable_custom_tag_start;
  iface->custom_finished = gtk_level_bar_buildable_custom_finished;
}

static void
802
803
gtk_level_bar_set_orientation (GtkLevelBar    *self,
                               GtkOrientation  orientation)
804
{
805
  if (self->orientation != orientation)
806
    {
807
      self->orientation = orientation;
808
      gtk_widget_update_orientation (GTK_WIDGET (self), self->orientation);
809
      gtk_widget_queue_resize (GTK_WIDGET (self));
810
      g_object_notify (G_OBJECT (self), "orientation");
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
    }
}

static void
gtk_level_bar_get_property (GObject    *obj,
                            guint       property_id,
                            GValue     *value,
                            GParamSpec *pspec)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);

  switch (property_id)
    {
    case PROP_VALUE:
      g_value_set_double (value, gtk_level_bar_get_value (self));
      break;
    case PROP_MIN_VALUE:
      g_value_set_double (value, gtk_level_bar_get_min_value (self));
      break;
    case PROP_MAX_VALUE:
      g_value_set_double (value, gtk_level_bar_get_max_value (self));
      break;
    case PROP_MODE:
      g_value_set_enum (value, gtk_level_bar_get_mode (self));
      break;
836
837
838
    case PROP_INVERTED:
      g_value_set_boolean (value, gtk_level_bar_get_inverted (self));
      break;
839
    case PROP_ORIENTATION:
840
      g_value_set_enum (value, self->orientation);
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
gtk_level_bar_set_property (GObject      *obj,
                            guint         property_id,
                            const GValue *value,
                            GParamSpec   *pspec)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);

  switch (property_id)
    {
    case PROP_VALUE:
      gtk_level_bar_set_value (self, g_value_get_double (value));
      break;
    case PROP_MIN_VALUE:
      gtk_level_bar_set_min_value (self, g_value_get_double (value));
      break;
    case PROP_MAX_VALUE:
      gtk_level_bar_set_max_value (self, g_value_get_double (value));
      break;
    case PROP_MODE:
      gtk_level_bar_set_mode (self, g_value_get_enum (value));
      break;
870
871
872
    case PROP_INVERTED:
      gtk_level_bar_set_inverted (self, g_value_get_boolean (value));
      break;
873
874
875
876
877
878
879
880
881
882
883
884
885
    case PROP_ORIENTATION:
      gtk_level_bar_set_orientation (self, g_value_get_enum (value));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, property_id, pspec);
      break;
    }
}

static void
gtk_level_bar_finalize (GObject *obj)
{
  GtkLevelBar *self = GTK_LEVEL_BAR (obj);
Benjamin Otte's avatar
Benjamin Otte committed
886
  int i;
887

888
  g_list_free_full (self->offsets, (GDestroyNotify) gtk_level_bar_offset_free);
Cosimo Cecchi's avatar
Cosimo Cecchi committed
889

890
891
  for (i = 0; i < self->n_blocks; i++)
    gtk_widget_unparent (self->block_widget[i]);
892

893
  g_free (self->block_widget);
Cosimo Cecchi's avatar
Cosimo Cecchi committed
894

895
  gtk_widget_unparent (self->trough_widget);
896
897
898
899
900
901
902
903
904
905
906
907
908
909

  G_OBJECT_CLASS (gtk_level_bar_parent_class)->finalize (obj);
}

static void
gtk_level_bar_class_init (GtkLevelBarClass *klass)
{
  GObjectClass *oclass = G_OBJECT_CLASS (klass);
  GtkWidgetClass *wclass = GTK_WIDGET_CLASS (klass);

  oclass->get_property = gtk_level_bar_get_property;
  oclass->set_property = gtk_level_bar_set_property;
  oclass->finalize = gtk_level_bar_finalize;

Matthias Clasen's avatar
Matthias Clasen committed
910
  wclass->direction_changed = gtk_level_bar_direction_changed;
911
912
913
914
915

  g_object_class_override_property (oclass, PROP_ORIENTATION, "orientation");

  /**
   * GtkLevelBar::offset-changed:
Matthias Clasen's avatar
Matthias Clasen committed
916
   * @self: a `GtkLevelBar`
917
918
   * @name: the name of the offset that changed value
   *
Matthias Clasen's avatar
Matthias Clasen committed
919
920
921
922
   * Emitted when an offset specified on the bar changes value.
   *
   * This typically is the result of a [method@Gtk.LevelBar.add_offset_value]
   * call.
923
924
925
926
927
928
   *
   * The signal supports detailed connections; you can connect to the
   * detailed signal "changed::x" in order to only receive callbacks when
   * the value of offset "x" changes.
   */
  signals[SIGNAL_OFFSET_CHANGED] =
929
    g_signal_new (I_("offset-changed"),
930
931
932
933
                  GTK_TYPE_LEVEL_BAR,
                  G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED,
                  G_STRUCT_OFFSET (GtkLevelBarClass, offset_changed),
                  NULL, NULL,
934
                  NULL,
935
936
937
938
                  G_TYPE_NONE,
                  1, G_TYPE_STRING);

  /**
939
   * GtkLevelBar:value: (attributes org.gtk.Property.get=gtk_level_bar_get_value org.gtk.Property.set=gtk_level_bar_set_value)
940
   *
Matthias Clasen's avatar
Matthias Clasen committed
941
   * Determines the currently filled value of the level bar.
942
943
944
945
946
947
   */
  properties[PROP_VALUE] =
    g_param_spec_double ("value",
                         P_("Currently filled value level"),
                         P_("Currently filled value level of the level bar"),
                         0.0, G_MAXDOUBLE, 0.0,
948
949
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

950
  /**
951
   * GtkLevelBar:min-value: (attributes org.gtk.Property.get=gtk_level_bar_get_min_value org.gtk.Property.set=gtk_level_bar_set_min_value)
952
   *
Matthias Clasen's avatar
Matthias Clasen committed
953
   * Determines the minimum value of the interval that can be displayed by the bar.
954
955
956
957
958
959
   */
  properties[PROP_MIN_VALUE] =
    g_param_spec_double ("min-value",
                         P_("Minimum value level for the bar"),
                         P_("Minimum value level that can be displayed by the bar"),
                         0.0, G_MAXDOUBLE, 0.0,
960
961
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

962
  /**
963
   * GtkLevelBar:max-value: (attributes org.gtk.Property.get=gtk_level_bar_get_max_value org.gtk.Property.set=gtk_level_bar_set_max_value)
964
   *
Matthias Clasen's avatar
Matthias Clasen committed
965
   * Determines the maximum value of the interval that can be displayed by the bar.
966
967
968
969
970
971
   */
  properties[PROP_MAX_VALUE] =
    g_param_spec_double ("max-value",
                         P_("Maximum value level for the bar"),
                         P_("Maximum value level that can be displayed by the bar"),
                         0.0, G_MAXDOUBLE, 1.0,
972
973
                         G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);

974
  /**
975
   * GtkLevelBar:mode: (attributes org.gtk.Property.get=gtk_level_bar_get_mode org.gtk.Property.set=gtk_level_bar_set_mode)
976
   *
Matthias Clasen's avatar
Matthias Clasen committed
977
978
979
   * Determines the way `GtkLevelBar` interprets the value properties to draw the
   * level fill area.
   *
Matthias Clasen's avatar
Matthias Clasen committed
980
   * Specifically, when the value is %GTK_LEVEL_BAR_MODE_CONTINUOUS,
Matthias Clasen's avatar
Matthias Clasen committed
981
   * `GtkLevelBar` will draw a single block representing the current value in
Matthias Clasen's avatar
Matthias Clasen committed
982
   * that area; when the value is %GTK_LEVEL_BAR_MODE_DISCRETE,
983
984
   * the widget will draw a succession of separate blocks filling the
   * draw area, with the number of blocks being equal to the units separating
Matthias Clasen's avatar
Matthias Clasen committed
985
986
   * the integral roundings of [property@Gtk.LevelBar:min-value] and
   * [property@Gtk.LevelBar:max-value].
987
988
989
990
991
992
993
   */
  properties[PROP_MODE] =
    g_param_spec_enum ("mode",
                       P_("The mode of the value indicator"),
                       P_("The mode of the value indicator displayed by the bar"),
                       GTK_TYPE_LEVEL_BAR_MODE,
                       GTK_LEVEL_BAR_MODE_CONTINUOUS,
994
                       G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
995

996
  /**
997
   * GtkLevelBar:inverted: (attributes org.gtk.Property.get=gtk_level_bar_get_inverted org.gtk.Property.set=gtk_level_bar_set_inverted)
998
   *
Matthias Clasen's avatar
Matthias Clasen committed
999
1000
   * Whether the `GtkLeveBar` is inverted.
   *
1001
1002
1003
1004
1005
1006
1007
1008
   * Level bars normally grow from top to bottom or left to right.
   * Inverted level bars grow in the opposite direction.
   */
  properties[PROP_INVERTED] =
    g_param_spec_boolean ("inverted",
                          P_("Inverted"),
                          P_("Invert the direction in which the level bar grows"),
                          FALSE,
1009
                          G_PARAM_READWRITE|G_PARAM_STATIC_STRINGS|G_PARAM_EXPLICIT_NOTIFY);
1010

1011
  g_object_class_install_properties (oclass, LAST_PROPERTY, properties);
1012

Timm Bäder's avatar
Timm Bäder committed
1013
  gtk_widget_class_set_layout_manager_type (wclass, GTK_TYPE_BIN_LAYOUT);
Matthias Clasen's avatar
Matthias Clasen committed
1014
  gtk_widget_class_set_css_name (wclass, I_("levelbar"));
1015