various work on Pane, including cursors, more styling stuff, and making the forall_vf...
[ardour.git] / libs / gtkmm2ext / pane.cc
1 /*
2     Copyright (C) 2016 Paul Davis
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #include <gdkmm/cursor.h>
21 #include "gtkmm2ext/pane.h"
22
23 #include "i18n.h"
24
25 using namespace PBD;
26 using namespace Gtk;
27 using namespace Gtkmm2ext;
28 using namespace std;
29
30 Pane::Pane (bool h)
31         : horizontal (h)
32         , did_move (false)
33         , divider_width (5)
34 {
35         using namespace Gdk;
36
37         set_name ("Pane");
38         set_has_window (false);
39
40         if (horizontal) {
41                 drag_cursor = Cursor (SB_H_DOUBLE_ARROW);
42         } else {
43                 drag_cursor = Cursor (SB_H_DOUBLE_ARROW);
44         }
45 }
46
47 void
48 Pane::set_drag_cursor (Gdk::Cursor c)
49 {
50         drag_cursor = c;
51 }
52
53 void
54 Pane::on_size_request (GtkRequisition* req)
55 {
56         GtkRequisition largest;
57
58         /* iterate over all children, get their size requests */
59
60         /* horizontal pane is as high as its tallest child, including the dividers.
61          * Its width is the sum of the children plus the dividers.
62          *
63          * vertical pane is as wide as its widest child, including the dividers.
64          * Its height is the sum of the children plus the dividers.
65          */
66
67         if (horizontal) {
68                 largest.width = (children.size()  - 1) * divider_width;
69                 largest.height = 0;
70         } else {
71                 largest.height = (children.size() - 1) * divider_width;
72                 largest.width = 0;
73         }
74
75         for (Children::iterator child = children.begin(); child != children.end(); ++child) {
76                 GtkRequisition r;
77
78                 (*child)->size_request (r);
79
80                 if (horizontal) {
81                         largest.height = max (largest.height, r.height);
82                         largest.width += r.width;
83                 } else {
84                         largest.width = max (largest.width, r.width);
85                         largest.height += r.height;
86                 }
87         }
88
89         *req = largest;
90 }
91
92 GType
93 Pane::child_type_vfunc() const
94 {
95         /* We accept any number of any types of widgets */
96         return Gtk::Widget::get_type();
97 }
98
99 void
100 Pane::add_divider ()
101 {
102         Divider* d = new Divider;
103         d->signal_button_press_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_press_event), d), false);
104         d->signal_button_release_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_release_event), d), false);
105         d->signal_motion_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_motion_event), d), false);
106         d->signal_enter_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_enter_event), d), false);
107         d->signal_leave_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_leave_event), d), false);
108         d->set_parent (*this);
109         d->show ();
110         d->fract = 0.5;
111         dividers.push_back (d);
112 }
113
114 void
115 Pane::on_add (Widget* w)
116 {
117         children.push_back (w);
118
119         w->set_parent (*this);
120
121         while (dividers.size() < (children.size() - 1)) {
122                 add_divider ();
123         }
124 }
125
126 void
127 Pane::on_remove (Widget* w)
128 {
129         w->unparent ();
130         children.remove (w);
131 }
132
133 void
134 Pane::on_size_allocate (Gtk::Allocation& alloc)
135 {
136         reallocate (alloc);
137         Container::on_size_allocate (alloc);
138 }
139
140 void
141 Pane::reallocate (Gtk::Allocation const & alloc)
142 {
143         int remaining;
144         int xpos = alloc.get_x();
145         int ypos = alloc.get_y();
146         float fract;
147
148         if (children.empty()) {
149                 return;
150         }
151
152         if (children.size() == 1) {
153                 /* only child gets the full allocation */
154                 children.front()->size_allocate (alloc);
155                 return;
156         }
157
158         if (horizontal) {
159                 remaining = alloc.get_width ();
160         } else {
161                 remaining = alloc.get_height ();
162         }
163
164         Children::iterator child;
165         Children::iterator next;
166         Dividers::iterator div;
167
168         for (child = children.begin(), div = dividers.begin(); child != children.end(); ) {
169
170                 Gtk::Allocation child_alloc;
171                 next = child;
172                 ++next;
173
174                 child_alloc.set_x (xpos);
175                 child_alloc.set_y (ypos);
176
177                 if (next == children.end()) {
178                         /* last child gets all the remaining space */
179                         fract = 1.0;
180                 } else {
181                         /* child gets the fraction of the remaining space given by the divider that follows it */
182                         fract = (*div)->fract;
183                 }
184
185                 Gtk::Requisition cr;
186                 (*child)->size_request (cr);
187
188                 if (horizontal) {
189                         child_alloc.set_width ((gint) floor (remaining * fract));
190                         child_alloc.set_height (alloc.get_height());
191                         remaining = max (0, (remaining - child_alloc.get_width()));
192                         xpos += child_alloc.get_width();
193                 } else {
194                         child_alloc.set_width (alloc.get_width());
195                         child_alloc.set_height ((gint) floor (remaining * fract));
196                         remaining = max (0, (remaining - child_alloc.get_height()));
197                         ypos += child_alloc.get_height ();
198                 }
199
200                 (*child)->size_allocate (child_alloc);
201                 ++child;
202
203                 if (child == children.end()) {
204                         /* done, no more children, no need for a divider */
205                         break;
206                 }
207
208                 /* add a divider between children */
209
210                 Gtk::Allocation divider_allocation;
211
212                 divider_allocation.set_x (xpos);
213                 divider_allocation.set_y (ypos);
214
215                 if (horizontal) {
216                         divider_allocation.set_width (divider_width);
217                         divider_allocation.set_height (alloc.get_height());
218                         remaining = max (0, remaining - divider_width);
219                         xpos += divider_width;
220                 } else {
221                         divider_allocation.set_width (alloc.get_width());
222                         divider_allocation.set_height (divider_width);
223                         remaining = max (0, remaining - divider_width);
224                         ypos += divider_width;
225                 }
226
227                 (*div)->size_allocate (divider_allocation);
228                 ++div;
229         }
230 }
231
232 bool
233 Pane::on_expose_event (GdkEventExpose* ev)
234 {
235         Children::iterator child;
236         Dividers::iterator div;
237
238         for (child = children.begin(), div = dividers.begin(); child != children.end(); ++child, ++div) {
239
240                 propagate_expose (**child, ev);
241
242                 if (div != dividers.end()) {
243                         propagate_expose (**div, ev);
244                 }
245         }
246
247         return true;
248 }
249
250 bool
251 Pane::handle_press_event (GdkEventButton* ev, Divider* d)
252 {
253         d->dragging = true;
254         d->queue_draw ();
255
256         return false;
257 }
258
259 bool
260 Pane::handle_release_event (GdkEventButton* ev, Divider* d)
261 {
262         d->dragging = false;
263
264         if (did_move) {
265                 children.front()->queue_resize ();
266                 did_move = false;
267         }
268
269         return false;
270 }
271
272 bool
273 Pane::handle_motion_event (GdkEventMotion* ev, Divider* d)
274 {
275         did_move = true;
276
277         if (!d->dragging) {
278                 return true;
279         }
280
281         /* determine new position for handle */
282
283         float new_fract;
284         int px, py;
285
286         d->translate_coordinates (*this, ev->x, ev->y, px, py);
287
288         Dividers::iterator prev = dividers.end();
289
290         for (Dividers::iterator di = dividers.begin(); di != dividers.end(); ++di) {
291                 if (*di == d) {
292                         break;
293                 }
294                 prev = di;
295         }
296
297         int space_remaining;
298         int prev_edge;
299
300         if (horizontal) {
301                 if (prev != dividers.end()) {
302                         prev_edge = (*prev)->get_allocation().get_x() + (*prev)->get_allocation().get_width();
303                 } else {
304                         prev_edge = 0;
305                 }
306                 space_remaining = get_allocation().get_width() - prev_edge;
307                 new_fract = (float) (px - prev_edge) / space_remaining;
308         } else {
309                 if (prev != dividers.end()) {
310                         prev_edge = (*prev)->get_allocation().get_y() + (*prev)->get_allocation().get_height();
311                 } else {
312                         prev_edge = 0;
313                 }
314                 space_remaining = get_allocation().get_height() - prev_edge;
315                 new_fract = (float) (py - prev_edge) / space_remaining;
316         }
317
318         new_fract = min (1.0f, max (0.0f, new_fract));
319
320         if (new_fract != d->fract) {
321                 d->fract = new_fract;
322                 reallocate (get_allocation ());
323                 queue_draw ();
324         }
325
326         return true;
327 }
328
329 void
330 Pane::set_divider (Dividers::size_type div, float fract)
331 {
332         bool redraw = false;
333
334         Dividers::iterator d = dividers.begin();
335
336         while (div--) {
337                 ++d;
338                 if (d == dividers.end()) {
339                         /* caller is trying to set divider that does not exist
340                          * yet.
341                          */
342                         return;
343                 }
344         }
345
346         if (fract != (*d)->fract) {
347                 (*d)->fract = fract;
348                 redraw = true;
349         }
350
351         if (redraw) {
352                 /* our size hasn't changed, but our internal allocations have */
353                 reallocate (get_allocation());
354                 queue_draw ();
355         }
356 }
357
358 float
359 Pane::get_divider (Dividers::size_type div)
360 {
361         Dividers::iterator d = dividers.begin();
362
363         while (div--) {
364                 ++d;
365                 if (d == dividers.end()) {
366                         /* caller is trying to set divider that does not exist
367                          * yet.
368                          */
369                         return -1.0f;
370                 }
371         }
372
373         return (*d)->fract;
374 }
375
376 void
377 Pane::forall_vfunc (gboolean include_internals, GtkCallback callback, gpointer callback_data)
378 {
379         /* since the callback could modify the child list(s), make sure we keep
380          * the iterators safe;
381          */
382
383         for (Children::iterator w = children.begin(); w != children.end(); ) {
384                 Children::iterator next = w;
385                 ++next;
386                 callback ((*w)->gobj(), callback_data);
387                 w = next;
388         }
389
390         if (include_internals) {
391                 for (Dividers::iterator d = dividers.begin(); d != dividers.end(); ) {
392                         Dividers::iterator next = d;
393                         ++next;
394                         callback (GTK_WIDGET((*d)->gobj()), callback_data);
395                         d = next;
396                 }
397         }
398 }
399
400 Pane::Divider::Divider ()
401         : fract (0.0)
402         , dragging (false)
403 {
404         set_events (Gdk::EventMask (Gdk::BUTTON_PRESS|
405                                     Gdk::BUTTON_RELEASE|
406                                     Gdk::MOTION_NOTIFY|
407                                     Gdk::ENTER_NOTIFY|
408                                     Gdk::LEAVE_NOTIFY));
409 }
410
411 bool
412 Pane::Divider::on_expose_event (GdkEventExpose* ev)
413 {
414         Gdk::Color c = (dragging ? get_style()->get_fg (Gtk::STATE_ACTIVE) :
415                         get_style()->get_fg (get_state()));
416
417         Cairo::RefPtr<Cairo::Context> draw_context = get_window()->create_cairo_context ();
418         draw_context->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height);
419         draw_context->clip_preserve ();
420         draw_context->set_source_rgba (c.get_red_p(), c.get_green_p(), c.get_blue_p(), 1.0);
421         draw_context->fill ();
422
423         return true;
424 }
425
426 bool
427 Pane::handle_enter_event (GdkEventCrossing*, Divider* d)
428 {
429         d->get_window()->set_cursor (drag_cursor);
430         d->set_state (Gtk::STATE_SELECTED);
431         return true;
432 }
433
434 bool
435 Pane::handle_leave_event (GdkEventCrossing*, Divider* d)
436 {
437         d->get_window()->set_cursor ();
438         d->set_state (Gtk::STATE_NORMAL);
439         return true;
440 }