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