ptformat: Update lib to upstream 3b60276
[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 <assert.h>
21 #include <gdkmm/cursor.h>
22 #include "gtkmm2ext/pane.h"
23
24 #include "pbd/i18n.h"
25
26 using namespace PBD;
27 using namespace Gtk;
28 using namespace Gtkmm2ext;
29 using namespace std;
30
31 Pane::Pane (bool h)
32         : horizontal (h)
33         , did_move (false)
34         , divider_width (2)
35         , check_fract (false)
36 {
37         using namespace Gdk;
38
39         set_name ("Pane");
40         set_has_window (false);
41
42         if (horizontal) {
43                 drag_cursor = Cursor (SB_H_DOUBLE_ARROW);
44         } else {
45                 drag_cursor = Cursor (SB_V_DOUBLE_ARROW);
46         }
47 }
48
49 Pane::~Pane ()
50 {
51         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
52                 (*c)->show_con.disconnect ();
53                 (*c)->hide_con.disconnect ();
54                 if ((*c)->w) {
55                         (*c)->w->remove_destroy_notify_callback ((*c).get());
56                         (*c)->w->unparent ();
57                 }
58         }
59         children.clear ();
60 }
61
62 void
63 Pane::set_child_minsize (Gtk::Widget const& w, int32_t minsize)
64 {
65         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
66                 if ((*c)->w == &w) {
67                         (*c)->minsize = minsize;
68                         break;
69                 }
70         }
71 }
72
73 void
74 Pane::set_drag_cursor (Gdk::Cursor c)
75 {
76         drag_cursor = c;
77 }
78
79 void
80 Pane::on_size_request (GtkRequisition* req)
81 {
82         GtkRequisition largest;
83
84         /* iterate over all children, get their size requests */
85
86         /* horizontal pane is as high as its tallest child, including the dividers.
87          * Its width is the sum of the children plus the dividers.
88          *
89          * vertical pane is as wide as its widest child, including the dividers.
90          * Its height is the sum of the children plus the dividers.
91          */
92
93         if (horizontal) {
94                 largest.width = (children.size()  - 1) * divider_width;
95                 largest.height = 0;
96         } else {
97                 largest.height = (children.size() - 1) * divider_width;
98                 largest.width = 0;
99         }
100
101         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
102                 GtkRequisition r;
103
104                 if (!(*c)->w->is_visible ()) {
105                         continue;
106                 }
107
108                 (*c)->w->size_request (r);
109
110                 if (horizontal) {
111                         largest.height = max (largest.height, r.height);
112                         if ((*c)->minsize) {
113                                 largest.width += (*c)->minsize;
114                         } else {
115                                 largest.width += r.width;
116                         }
117                 } else {
118                         largest.width = max (largest.width, r.width);
119                         if ((*c)->minsize) {
120                                 largest.height += (*c)->minsize;
121                         } else {
122                                 largest.height += r.height;
123                         }
124                 }
125         }
126
127         *req = largest;
128 }
129
130 GType
131 Pane::child_type_vfunc() const
132 {
133         /* We accept any number of any types of widgets */
134         return Gtk::Widget::get_type();
135 }
136
137 void
138 Pane::add_divider ()
139 {
140         Divider* d = new Divider;
141         d->set_name (X_("Divider"));
142         d->signal_button_press_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_press_event), d), false);
143         d->signal_button_release_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_release_event), d), false);
144         d->signal_motion_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_motion_event), d), false);
145         d->signal_enter_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_enter_event), d), false);
146         d->signal_leave_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_leave_event), d), false);
147         d->set_parent (*this);
148         d->show ();
149         d->fract = 0.5;
150         dividers.push_back (d);
151 }
152
153 void
154 Pane::handle_child_visibility ()
155 {
156         reallocate (get_allocation());
157 }
158
159 void
160 Pane::on_add (Widget* w)
161 {
162         children.push_back (boost::shared_ptr<Child> (new Child (this, w, 0)));
163         Child* kid = children.back ().get();
164
165         w->set_parent (*this);
166         /* Gtkmm 2.4 does not correctly arrange for ::on_remove() to be called
167            for custom containers that derive from Gtk::Container. So ... we need
168            to ensure that we hear about child destruction ourselves.
169         */
170         w->add_destroy_notify_callback (kid, &Pane::notify_child_destroyed);
171
172         kid->show_con = w->signal_show().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility));
173         kid->hide_con = w->signal_hide().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility));
174
175         while (dividers.size() < (children.size() - 1)) {
176                 add_divider ();
177         }
178 }
179
180 void*
181 Pane::notify_child_destroyed (void* data)
182 {
183         Child* child = reinterpret_cast<Child*> (data);
184         return child->pane->child_destroyed (child->w);
185 }
186
187 void*
188 Pane::child_destroyed (Gtk::Widget* w)
189 {
190         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
191                 if ((*c)->w == w) {
192                         (*c)->show_con.disconnect ();
193                         (*c)->hide_con.disconnect ();
194                         (*c)->w = NULL; // mark invalid
195                         children.erase (c);
196                         break;
197                 }
198         }
199         return 0;
200 }
201
202 void
203 Pane::on_remove (Widget* w)
204 {
205         for (Children::iterator c = children.begin(); c != children.end(); ++c) {
206                 if ((*c)->w == w) {
207                         (*c)->show_con.disconnect ();
208                         (*c)->hide_con.disconnect ();
209                         w->remove_destroy_notify_callback ((*c).get());
210                         w->unparent ();
211                         (*c)->w = NULL; // mark invalid
212                         children.erase (c);
213                         break;
214                 }
215         }
216 }
217
218 void
219 Pane::on_size_allocate (Gtk::Allocation& alloc)
220 {
221         reallocate (alloc);
222         Container::on_size_allocate (alloc);
223
224         /* minumum pane size constraints */
225         Dividers::size_type div = 0;
226         for (Dividers::const_iterator d = dividers.begin(); d != dividers.end(); ++d, ++div) {
227                 // XXX skip dividers that were just hidden in reallocate()
228                 Pane::set_divider (div, (*d)->fract);
229         }
230         // TODO this needs tweaking for panes with > 2 children
231         // if a child grows, re-check the ones before it.
232         assert (dividers.size () < 3);
233 }
234
235 void
236 Pane::reallocate (Gtk::Allocation const & alloc)
237 {
238         int remaining;
239         int xpos = alloc.get_x();
240         int ypos = alloc.get_y();
241         float fract;
242
243         if (children.empty()) {
244                 return;
245         }
246
247         if (children.size() == 1) {
248                 /* only child gets the full allocation */
249                 if (children.front()->w->is_visible ()) {
250                         children.front()->w->size_allocate (alloc);
251                 }
252                 return;
253         }
254
255         if (horizontal) {
256                 remaining = alloc.get_width ();
257         } else {
258                 remaining = alloc.get_height ();
259         }
260
261         Children::iterator child;
262         Children::iterator next;
263         Dividers::iterator div;
264
265         child = children.begin();
266
267         /* skip initial hidden children */
268
269         while (child != children.end()) {
270                 if ((*child)->w->is_visible()) {
271                         break;
272                 }
273                 ++child;
274         }
275
276         for (div = dividers.begin(); child != children.end(); ) {
277
278                 Gtk::Allocation child_alloc;
279
280                 next = child;
281
282                 /* Move on to next *visible* child */
283
284                 while (++next != children.end()) {
285                         if ((*next)->w->is_visible()) {
286                                 break;
287                         }
288                 }
289
290                 child_alloc.set_x (xpos);
291                 child_alloc.set_y (ypos);
292
293                 if (next == children.end()) {
294                         /* last child gets all the remaining space */
295                         fract = 1.0;
296                 } else {
297                         /* child gets the fraction of the remaining space given by the divider that follows it */
298                         fract = (*div)->fract;
299                 }
300
301                 Gtk::Requisition cr;
302                 (*child)->w->size_request (cr);
303
304                 if (horizontal) {
305                         child_alloc.set_width ((gint) floor (remaining * fract));
306                         child_alloc.set_height (alloc.get_height());
307                         remaining = max (0, (remaining - child_alloc.get_width()));
308                         xpos += child_alloc.get_width();
309                 } else {
310                         child_alloc.set_width (alloc.get_width());
311                         child_alloc.set_height ((gint) floor (remaining * fract));
312                         remaining = max (0, (remaining - child_alloc.get_height()));
313                         ypos += child_alloc.get_height ();
314                 }
315
316                 if ((*child)->minsize) {
317                         if (horizontal) {
318                                 child_alloc.set_width (max (child_alloc.get_width(), (*child)->minsize));
319                         } else {
320                                 child_alloc.set_height (max (child_alloc.get_height(), (*child)->minsize));
321                         }
322                 }
323
324                 if ((*child)->w->is_visible ()) {
325                         (*child)->w->size_allocate (child_alloc);
326                 }
327
328                 if (next == children.end()) {
329                         /* done, no more children, no need for a divider */
330                         break;
331                 }
332
333                 child = next;
334
335                 /* add a divider between children */
336
337                 Gtk::Allocation divider_allocation;
338
339                 divider_allocation.set_x (xpos);
340                 divider_allocation.set_y (ypos);
341
342                 if (horizontal) {
343                         divider_allocation.set_width (divider_width);
344                         divider_allocation.set_height (alloc.get_height());
345                         remaining = max (0, remaining - divider_width);
346                         xpos += divider_width;
347                 } else {
348                         divider_allocation.set_width (alloc.get_width());
349                         divider_allocation.set_height (divider_width);
350                         remaining = max (0, remaining - divider_width);
351                         ypos += divider_width;
352                 }
353
354                 (*div)->size_allocate (divider_allocation);
355                 (*div)->show ();
356                 ++div;
357         }
358
359         /* hide all remaining dividers */
360
361         while (div != dividers.end()) {
362                 (*div)->hide ();
363                 ++div;
364         }
365 }
366
367 bool
368 Pane::on_expose_event (GdkEventExpose* ev)
369 {
370         Children::iterator child;
371         Dividers::iterator div;
372
373         for (child = children.begin(), div = dividers.begin(); child != children.end(); ++child) {
374
375                 if ((*child)->w->is_visible()) {
376                         propagate_expose (*((*child)->w), ev);
377                 }
378
379                 if (div != dividers.end()) {
380                         if ((*div)->is_visible()) {
381                                 propagate_expose (**div, ev);
382                         }
383                         ++div;
384                 }
385         }
386
387         return true;
388 }
389
390 bool
391 Pane::handle_press_event (GdkEventButton* ev, Divider* d)
392 {
393         d->dragging = true;
394         d->queue_draw ();
395
396         return false;
397 }
398
399 bool
400 Pane::handle_release_event (GdkEventButton* ev, Divider* d)
401 {
402         d->dragging = false;
403
404         if (did_move && !children.empty()) {
405                 children.front()->w->queue_resize ();
406                 did_move = false;
407         }
408
409         return false;
410 }
411 void
412 Pane::set_check_divider_position (bool yn)
413 {
414         check_fract = yn;
415 }
416
417 float
418 Pane::constrain_fract (Dividers::size_type div, float fract)
419 {
420         if (get_allocation().get_width() == 1 && get_allocation().get_height() == 1) {
421                 /* space not * allocated - * divider being set from startup code. Let it pass,
422                  * since our goal is mostly to catch drags to a position that will interfere with window
423                  * resizing.
424                  */
425                 return fract;
426         }
427
428         if (children.size () <= div + 1) { return fract; } // XXX remove once hidden divs are skipped
429         assert(children.size () > div + 1);
430
431         const float size = horizontal ? get_allocation().get_width() : get_allocation().get_height();
432
433         // TODO: optimize: cache in Pane::on_size_request
434         Gtk::Requisition prev_req(children.at (div)->w->size_request ());
435         Gtk::Requisition next_req(children.at (div + 1)->w->size_request ());
436         float prev = (horizontal ? prev_req.width : prev_req.height);
437         float next = (horizontal ? next_req.width : next_req.height);
438
439         if (children.at (div)->minsize) {
440                 prev = children.at (div)->minsize;
441         }
442         if (children.at (div + 1)->minsize) {
443                 next = children.at (div + 1)->minsize;
444         }
445
446         if (size * fract < prev) {
447                 return prev / size;
448         }
449         if (size * (1.f - fract) < next) {
450                 return 1.f - next / size;
451         }
452
453         if (!check_fract) {
454                 return fract;
455         }
456
457 #ifdef __APPLE__
458
459         /* On Quartz, if the pane handle (divider) gets to
460            be adjacent to the window edge, you can no longer grab it:
461            any attempt to do so is interpreted by the Quartz window
462            manager ("Finder") as a resize drag on the window edge.
463         */
464
465
466         if (horizontal) {
467                 if (div == dividers.size() - 1) {
468                         if (get_allocation().get_width() * (1.0 - fract) < (divider_width*2)) {
469                                 /* too close to right edge */
470                                 return 1.f - (divider_width * 2.f) / (float) get_allocation().get_width();
471                         }
472                 }
473
474                 if (div == 0) {
475                         if (get_allocation().get_width() * fract < (divider_width*2)) {
476                                 /* too close to left edge */
477                                 return (divider_width * 2.f) / (float)get_allocation().get_width();
478                         }
479                 }
480         } else {
481                 if (div == dividers.size() - 1) {
482                         if (get_allocation().get_height() * (1.0 - fract) < (divider_width*2)) {
483                                 /* too close to bottom */
484                                 return 1.f - (divider_width * 2.f) / (float) get_allocation().get_height();
485                         }
486                 }
487
488                 if (div == 0) {
489                         if (get_allocation().get_height() * fract < (divider_width*2)) {
490                                 /* too close to top */
491                                 return (divider_width * 2.f) / (float) get_allocation().get_height();
492                         }
493                 }
494         }
495 #endif
496         return fract;
497 }
498
499 bool
500 Pane::handle_motion_event (GdkEventMotion* ev, Divider* d)
501 {
502         did_move = true;
503
504         if (!d->dragging) {
505                 return true;
506         }
507
508         /* determine new position for handle */
509
510         float new_fract;
511         int px, py;
512
513         d->translate_coordinates (*this, ev->x, ev->y, px, py);
514
515         Dividers::iterator prev = dividers.end();
516         Dividers::size_type div = 0;
517
518         for (Dividers::iterator di = dividers.begin(); di != dividers.end(); ++di, ++div) {
519                 if (*di == d) {
520                         break;
521                 }
522                 prev = di;
523         }
524
525         int space_remaining;
526         int prev_edge;
527
528         if (horizontal) {
529                 if (prev != dividers.end()) {
530                         prev_edge = (*prev)->get_allocation().get_x() + (*prev)->get_allocation().get_width();
531                 } else {
532                         prev_edge = 0;
533                 }
534                 space_remaining = get_allocation().get_width() - prev_edge;
535                 new_fract = (float) (px - prev_edge) / space_remaining;
536         } else {
537                 if (prev != dividers.end()) {
538                         prev_edge = (*prev)->get_allocation().get_y() + (*prev)->get_allocation().get_height();
539                 } else {
540                         prev_edge = 0;
541                 }
542                 space_remaining = get_allocation().get_height() - prev_edge;
543                 new_fract = (float) (py - prev_edge) / space_remaining;
544         }
545
546         new_fract = min (1.0f, max (0.0f, new_fract));
547         new_fract = constrain_fract (div, new_fract);
548         new_fract = min (1.0f, max (0.0f, new_fract));
549
550         if (new_fract != d->fract) {
551                 d->fract = new_fract;
552                 reallocate (get_allocation ());
553                 queue_draw ();
554         }
555
556         return true;
557 }
558
559 void
560 Pane::set_divider (Dividers::size_type div, float fract)
561 {
562         Dividers::iterator d = dividers.begin();
563
564         for (d = dividers.begin(); d != dividers.end() && div != 0; ++d, --div) {
565                 /* relax */
566         }
567
568         if (d == dividers.end()) {
569                 /* caller is trying to set divider that does not exist
570                  * yet.
571                  */
572                 return;
573         }
574
575         fract = max (0.0f, min (1.0f, fract));
576         fract = constrain_fract (div, fract);
577         fract = max (0.0f, min (1.0f, fract));
578
579         if (fract != (*d)->fract) {
580                 (*d)->fract = fract;
581                 /* our size hasn't changed, but our internal allocations have */
582                 reallocate (get_allocation());
583                 queue_draw ();
584         }
585 }
586
587 float
588 Pane::get_divider (Dividers::size_type div)
589 {
590         Dividers::iterator d = dividers.begin();
591
592         for (d = dividers.begin(); d != dividers.end() && div != 0; ++d, --div) {
593                 /* relax */
594         }
595
596         if (d == dividers.end()) {
597                 /* caller is trying to set divider that does not exist
598                  * yet.
599                  */
600                 return -1.0f;
601         }
602
603         return (*d)->fract;
604 }
605
606 void
607 Pane::forall_vfunc (gboolean include_internals, GtkCallback callback, gpointer callback_data)
608 {
609         /* since the callback could modify the child list(s), make sure we keep
610          * the iterators safe;
611          */
612         Children kids (children);
613         for (Children::const_iterator c = kids.begin(); c != kids.end(); ++c) {
614                 if ((*c)->w) {
615                         callback ((*c)->w->gobj(), callback_data);
616                 }
617         }
618
619         if (include_internals) {
620                 for (Dividers::iterator d = dividers.begin(); d != dividers.end(); ) {
621                         Dividers::iterator next = d;
622                         ++next;
623                         callback (GTK_WIDGET((*d)->gobj()), callback_data);
624                         d = next;
625                 }
626         }
627 }
628
629 Pane::Divider::Divider ()
630         : fract (0.0)
631         , dragging (false)
632 {
633         set_events (Gdk::EventMask (Gdk::BUTTON_PRESS|
634                                     Gdk::BUTTON_RELEASE|
635                                     Gdk::MOTION_NOTIFY|
636                                     Gdk::ENTER_NOTIFY|
637                                     Gdk::LEAVE_NOTIFY));
638 }
639
640 bool
641 Pane::Divider::on_expose_event (GdkEventExpose* ev)
642 {
643         Gdk::Color c = (dragging ? get_style()->get_fg (Gtk::STATE_ACTIVE) :
644                         get_style()->get_fg (get_state()));
645
646         Cairo::RefPtr<Cairo::Context> draw_context = get_window()->create_cairo_context ();
647         draw_context->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height);
648         draw_context->clip_preserve ();
649         draw_context->set_source_rgba (c.get_red_p(), c.get_green_p(), c.get_blue_p(), 1.0);
650         draw_context->fill ();
651
652         return true;
653 }
654
655 bool
656 Pane::handle_enter_event (GdkEventCrossing*, Divider* d)
657 {
658         d->get_window()->set_cursor (drag_cursor);
659         d->set_state (Gtk::STATE_SELECTED);
660         return true;
661 }
662
663 bool
664 Pane::handle_leave_event (GdkEventCrossing*, Divider* d)
665 {
666         d->get_window()->set_cursor ();
667         d->set_state (Gtk::STATE_NORMAL);
668         d->queue_draw ();
669         return true;
670 }