fix default pane divider cursor for vertical panes
[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 (2)
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_V_DOUBLE_ARROW);
44         }
45 }
46
47 void
48 Pane::set_child_minsize (Gtk::Widget const& w, int32_t minsize)
49 {
50         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
51                 if (c->w == &w) {
52                         c->minsize = minsize;
53                         break;
54                 }
55         }
56 }
57
58 void
59 Pane::set_drag_cursor (Gdk::Cursor c)
60 {
61         drag_cursor = c;
62 }
63
64 void
65 Pane::on_size_request (GtkRequisition* req)
66 {
67         GtkRequisition largest;
68
69         /* iterate over all children, get their size requests */
70
71         /* horizontal pane is as high as its tallest child, including the dividers.
72          * Its width is the sum of the children plus the dividers.
73          *
74          * vertical pane is as wide as its widest child, including the dividers.
75          * Its height is the sum of the children plus the dividers.
76          */
77
78         if (horizontal) {
79                 largest.width = (children.size()  - 1) * divider_width;
80                 largest.height = 0;
81         } else {
82                 largest.height = (children.size() - 1) * divider_width;
83                 largest.width = 0;
84         }
85
86         for (Children::iterator child = children.begin(); child != children.end(); ++child) {
87                 GtkRequisition r;
88
89                 child->w->size_request (r);
90
91                 if (horizontal) {
92                         largest.height = max (largest.height, r.height);
93                         largest.width += r.width;
94                 } else {
95                         largest.width = max (largest.width, r.width);
96                         largest.height += r.height;
97                 }
98         }
99
100         *req = largest;
101 }
102
103 GType
104 Pane::child_type_vfunc() const
105 {
106         /* We accept any number of any types of widgets */
107         return Gtk::Widget::get_type();
108 }
109
110 void
111 Pane::add_divider ()
112 {
113         Divider* d = new Divider;
114         d->signal_button_press_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_press_event), d), false);
115         d->signal_button_release_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_release_event), d), false);
116         d->signal_motion_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_motion_event), d), false);
117         d->signal_enter_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_enter_event), d), false);
118         d->signal_leave_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_leave_event), d), false);
119         d->set_parent (*this);
120         d->show ();
121         d->fract = 0.5;
122         dividers.push_back (d);
123 }
124
125 void
126 Pane::handle_child_visibility ()
127 {
128         reallocate (get_allocation());
129 }
130
131 void
132 Pane::on_add (Widget* w)
133 {
134         children.push_back (Child (w, 0));
135
136         w->set_parent (*this);
137
138         w->signal_show().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility));
139         w->signal_hide().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility));
140
141         while (dividers.size() < (children.size() - 1)) {
142                 add_divider ();
143         }
144 }
145
146 void
147 Pane::on_remove (Widget* w)
148 {
149         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
150                 if (c->w == w) {
151                         children.erase (c);
152                         break;
153                 }
154         }
155
156         w->unparent ();
157 }
158
159 void
160 Pane::on_size_allocate (Gtk::Allocation& alloc)
161 {
162         reallocate (alloc);
163         Container::on_size_allocate (alloc);
164 }
165
166 void
167 Pane::reallocate (Gtk::Allocation const & alloc)
168 {
169         int remaining;
170         int xpos = alloc.get_x();
171         int ypos = alloc.get_y();
172         float fract;
173
174         if (children.empty()) {
175                 return;
176         }
177
178         if (children.size() == 1) {
179                 /* only child gets the full allocation */
180                 children.front().w->size_allocate (alloc);
181                 return;
182         }
183
184         if (horizontal) {
185                 remaining = alloc.get_width ();
186         } else {
187                 remaining = alloc.get_height ();
188         }
189
190         Children::iterator child;
191         Children::iterator next;
192         Dividers::iterator div;
193
194         child = children.begin();
195
196         /* skip initial hidden children */
197
198         while (child != children.end()) {
199                 if (child->w->is_visible()) {
200                         break;
201                 }
202                 ++child;
203         }
204
205         for (div = dividers.begin(); child != children.end(); ) {
206
207                 Gtk::Allocation child_alloc;
208
209                 next = child;
210
211                 /* Move on to next *visible* child */
212
213                 while (++next != children.end()) {
214                         if (next->w->is_visible()) {
215                                 break;
216                         }
217                 }
218
219                 child_alloc.set_x (xpos);
220                 child_alloc.set_y (ypos);
221
222                 if (next == children.end()) {
223                         /* last child gets all the remaining space */
224                         fract = 1.0;
225                 } else {
226                         /* child gets the fraction of the remaining space given by the divider that follows it */
227                         fract = (*div)->fract;
228                 }
229
230                 Gtk::Requisition cr;
231                 child->w->size_request (cr);
232
233                 if (horizontal) {
234                         child_alloc.set_width ((gint) floor (remaining * fract));
235                         child_alloc.set_height (alloc.get_height());
236                         remaining = max (0, (remaining - child_alloc.get_width()));
237                         xpos += child_alloc.get_width();
238                 } else {
239                         child_alloc.set_width (alloc.get_width());
240                         child_alloc.set_height ((gint) floor (remaining * fract));
241                         remaining = max (0, (remaining - child_alloc.get_height()));
242                         ypos += child_alloc.get_height ();
243                 }
244
245                 if (child->minsize) {
246                         if (horizontal) {
247                                 child_alloc.set_width (max (child_alloc.get_width(), child->minsize));
248                         } else {
249                                 child_alloc.set_height (max (child_alloc.get_height(), child->minsize));
250                         }
251                 }
252
253                 child->w->size_allocate (child_alloc);
254
255                 if (next == children.end()) {
256                         /* done, no more children, no need for a divider */
257                         break;
258                 }
259
260                 child = next;
261
262                 /* add a divider between children */
263
264                 Gtk::Allocation divider_allocation;
265
266                 divider_allocation.set_x (xpos);
267                 divider_allocation.set_y (ypos);
268
269                 if (horizontal) {
270                         divider_allocation.set_width (divider_width);
271                         divider_allocation.set_height (alloc.get_height());
272                         remaining = max (0, remaining - divider_width);
273                         xpos += divider_width;
274                 } else {
275                         divider_allocation.set_width (alloc.get_width());
276                         divider_allocation.set_height (divider_width);
277                         remaining = max (0, remaining - divider_width);
278                         ypos += divider_width;
279                 }
280
281                 (*div)->size_allocate (divider_allocation);
282                 (*div)->show ();
283                 ++div;
284         }
285
286         /* hide all remaining dividers */
287
288         while (div != dividers.end()) {
289                 (*div)->hide ();
290                 ++div;
291         }
292 }
293
294 bool
295 Pane::on_expose_event (GdkEventExpose* ev)
296 {
297         Children::iterator child;
298         Dividers::iterator div;
299
300         for (child = children.begin(), div = dividers.begin(); child != children.end(); ++child, ++div) {
301
302                 if (child->w->is_visible()) {
303                         propagate_expose (*(child->w), ev);
304                 }
305
306                 if ((div != dividers.end()) && (*div)->is_visible()) {
307                         propagate_expose (**div, ev);
308                 }
309         }
310
311         return true;
312 }
313
314 bool
315 Pane::handle_press_event (GdkEventButton* ev, Divider* d)
316 {
317         d->dragging = true;
318         d->queue_draw ();
319
320         return false;
321 }
322
323 bool
324 Pane::handle_release_event (GdkEventButton* ev, Divider* d)
325 {
326         d->dragging = false;
327
328         if (did_move) {
329                 children.front().w->queue_resize ();
330                 did_move = false;
331         }
332
333         return false;
334 }
335
336 bool
337 Pane::handle_motion_event (GdkEventMotion* ev, Divider* d)
338 {
339         did_move = true;
340
341         if (!d->dragging) {
342                 return true;
343         }
344
345         /* determine new position for handle */
346
347         float new_fract;
348         int px, py;
349
350         d->translate_coordinates (*this, ev->x, ev->y, px, py);
351
352         Dividers::iterator prev = dividers.end();
353
354         for (Dividers::iterator di = dividers.begin(); di != dividers.end(); ++di) {
355                 if (*di == d) {
356                         break;
357                 }
358                 prev = di;
359         }
360
361         int space_remaining;
362         int prev_edge;
363
364         if (horizontal) {
365                 if (prev != dividers.end()) {
366                         prev_edge = (*prev)->get_allocation().get_x() + (*prev)->get_allocation().get_width();
367                 } else {
368                         prev_edge = 0;
369                 }
370                 space_remaining = get_allocation().get_width() - prev_edge;
371                 new_fract = (float) (px - prev_edge) / space_remaining;
372         } else {
373                 if (prev != dividers.end()) {
374                         prev_edge = (*prev)->get_allocation().get_y() + (*prev)->get_allocation().get_height();
375                 } else {
376                         prev_edge = 0;
377                 }
378                 space_remaining = get_allocation().get_height() - prev_edge;
379                 new_fract = (float) (py - prev_edge) / space_remaining;
380         }
381
382         new_fract = min (1.0f, max (0.0f, new_fract));
383
384         if (new_fract != d->fract) {
385                 d->fract = new_fract;
386                 reallocate (get_allocation ());
387                 queue_draw ();
388         }
389
390         return true;
391 }
392
393 void
394 Pane::set_divider (Dividers::size_type div, float fract)
395 {
396         bool redraw = false;
397
398         Dividers::iterator d = dividers.begin();
399
400         while (div--) {
401                 ++d;
402                 if (d == dividers.end()) {
403                         /* caller is trying to set divider that does not exist
404                          * yet.
405                          */
406                         return;
407                 }
408         }
409
410         if (fract != (*d)->fract) {
411                 (*d)->fract = fract;
412                 redraw = true;
413         }
414
415         if (redraw) {
416                 /* our size hasn't changed, but our internal allocations have */
417                 reallocate (get_allocation());
418                 queue_draw ();
419         }
420 }
421
422 float
423 Pane::get_divider (Dividers::size_type div)
424 {
425         Dividers::iterator d = dividers.begin();
426
427         while (div--) {
428                 ++d;
429                 if (d == dividers.end()) {
430                         /* caller is trying to set divider that does not exist
431                          * yet.
432                          */
433                         return -1.0f;
434                 }
435         }
436
437         return (*d)->fract;
438 }
439
440 void
441 Pane::forall_vfunc (gboolean include_internals, GtkCallback callback, gpointer callback_data)
442 {
443         /* since the callback could modify the child list(s), make sure we keep
444          * the iterators safe;
445          */
446
447         for (Children::iterator c = children.begin(); c != children.end(); ) {
448                 Children::iterator next = c;
449                 ++next;
450                 callback (c->w->gobj(), callback_data);
451                 c = next;
452         }
453
454         if (include_internals) {
455                 for (Dividers::iterator d = dividers.begin(); d != dividers.end(); ) {
456                         Dividers::iterator next = d;
457                         ++next;
458                         callback (GTK_WIDGET((*d)->gobj()), callback_data);
459                         d = next;
460                 }
461         }
462 }
463
464 Pane::Divider::Divider ()
465         : fract (0.0)
466         , dragging (false)
467 {
468         set_events (Gdk::EventMask (Gdk::BUTTON_PRESS|
469                                     Gdk::BUTTON_RELEASE|
470                                     Gdk::MOTION_NOTIFY|
471                                     Gdk::ENTER_NOTIFY|
472                                     Gdk::LEAVE_NOTIFY));
473 }
474
475 bool
476 Pane::Divider::on_expose_event (GdkEventExpose* ev)
477 {
478         Gdk::Color c = (dragging ? get_style()->get_fg (Gtk::STATE_ACTIVE) :
479                         get_style()->get_fg (get_state()));
480
481         Cairo::RefPtr<Cairo::Context> draw_context = get_window()->create_cairo_context ();
482         draw_context->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height);
483         draw_context->clip_preserve ();
484         draw_context->set_source_rgba (c.get_red_p(), c.get_green_p(), c.get_blue_p(), 1.0);
485         draw_context->fill ();
486
487         return true;
488 }
489
490 bool
491 Pane::handle_enter_event (GdkEventCrossing*, Divider* d)
492 {
493         d->get_window()->set_cursor (drag_cursor);
494         d->set_state (Gtk::STATE_SELECTED);
495         return true;
496 }
497
498 bool
499 Pane::handle_leave_event (GdkEventCrossing*, Divider* d)
500 {
501         d->get_window()->set_cursor ();
502         d->set_state (Gtk::STATE_NORMAL);
503         return true;
504 }