diff options
author | Robin Gareus <robin@gareus.org> | 2017-07-17 04:55:52 +0200 |
---|---|---|
committer | Robin Gareus <robin@gareus.org> | 2017-07-17 21:06:04 +0200 |
commit | f9e5e4360e54f5ff5327b4384ee451d86f8dec91 (patch) | |
tree | 6ae86878b9d7db86c101df33559ffec913d9ed19 /libs/widgets/pane.cc | |
parent | b5e9451bc7be12acc5d81c55cdaa6545837f3181 (diff) |
Move more Gtkmm2ext widgets into libwidget
Diffstat (limited to 'libs/widgets/pane.cc')
-rw-r--r-- | libs/widgets/pane.cc | 671 |
1 files changed, 671 insertions, 0 deletions
diff --git a/libs/widgets/pane.cc b/libs/widgets/pane.cc new file mode 100644 index 0000000000..2968f7bfaa --- /dev/null +++ b/libs/widgets/pane.cc @@ -0,0 +1,671 @@ +/* + Copyright (C) 2016 Paul Davis + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. + +*/ + +#include <assert.h> +#include <gdkmm/cursor.h> + +#include "widgets/pane.h" + +#include "pbd/i18n.h" + +using namespace std; +using namespace Gtk; +using namespace PBD; +using namespace ArdourWidgets; + +Pane::Pane (bool h) + : horizontal (h) + , did_move (false) + , divider_width (2) + , check_fract (false) +{ + using namespace Gdk; + + set_name ("Pane"); + set_has_window (false); + + if (horizontal) { + drag_cursor = Cursor (SB_H_DOUBLE_ARROW); + } else { + drag_cursor = Cursor (SB_V_DOUBLE_ARROW); + } +} + +Pane::~Pane () +{ + for (Children::iterator c = children.begin(); c != children.end(); ++c) { + (*c)->show_con.disconnect (); + (*c)->hide_con.disconnect (); + if ((*c)->w) { + (*c)->w->remove_destroy_notify_callback ((*c).get()); + (*c)->w->unparent (); + } + } + children.clear (); +} + +void +Pane::set_child_minsize (Gtk::Widget const& w, int32_t minsize) +{ + for (Children::iterator c = children.begin(); c != children.end(); ++c) { + if ((*c)->w == &w) { + (*c)->minsize = minsize; + break; + } + } +} + +void +Pane::set_drag_cursor (Gdk::Cursor c) +{ + drag_cursor = c; +} + +void +Pane::on_size_request (GtkRequisition* req) +{ + GtkRequisition largest; + + /* iterate over all children, get their size requests */ + + /* horizontal pane is as high as its tallest child, including the dividers. + * Its width is the sum of the children plus the dividers. + * + * vertical pane is as wide as its widest child, including the dividers. + * Its height is the sum of the children plus the dividers. + */ + + if (horizontal) { + largest.width = (children.size() - 1) * divider_width; + largest.height = 0; + } else { + largest.height = (children.size() - 1) * divider_width; + largest.width = 0; + } + + for (Children::iterator c = children.begin(); c != children.end(); ++c) { + GtkRequisition r; + + if (!(*c)->w->is_visible ()) { + continue; + } + + (*c)->w->size_request (r); + + if (horizontal) { + largest.height = max (largest.height, r.height); + if ((*c)->minsize) { + largest.width += (*c)->minsize; + } else { + largest.width += r.width; + } + } else { + largest.width = max (largest.width, r.width); + if ((*c)->minsize) { + largest.height += (*c)->minsize; + } else { + largest.height += r.height; + } + } + } + + *req = largest; +} + +GType +Pane::child_type_vfunc() const +{ + /* We accept any number of any types of widgets */ + return Gtk::Widget::get_type(); +} + +void +Pane::add_divider () +{ + Divider* d = new Divider; + d->set_name (X_("Divider")); + d->signal_button_press_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_press_event), d), false); + d->signal_button_release_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_release_event), d), false); + d->signal_motion_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_motion_event), d), false); + d->signal_enter_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_enter_event), d), false); + d->signal_leave_notify_event().connect (sigc::bind (sigc::mem_fun (*this, &Pane::handle_leave_event), d), false); + d->set_parent (*this); + d->show (); + d->fract = 0.5; + dividers.push_back (d); +} + +void +Pane::handle_child_visibility () +{ + reallocate (get_allocation()); +} + +void +Pane::on_add (Widget* w) +{ + children.push_back (boost::shared_ptr<Child> (new Child (this, w, 0))); + Child* kid = children.back ().get(); + + w->set_parent (*this); + /* Gtkmm 2.4 does not correctly arrange for ::on_remove() to be called + for custom containers that derive from Gtk::Container. So ... we need + to ensure that we hear about child destruction ourselves. + */ + w->add_destroy_notify_callback (kid, &Pane::notify_child_destroyed); + + kid->show_con = w->signal_show().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility)); + kid->hide_con = w->signal_hide().connect (sigc::mem_fun (*this, &Pane::handle_child_visibility)); + + while (dividers.size() < (children.size() - 1)) { + add_divider (); + } +} + +void* +Pane::notify_child_destroyed (void* data) +{ + Child* child = reinterpret_cast<Child*> (data); + return child->pane->child_destroyed (child->w); +} + +void* +Pane::child_destroyed (Gtk::Widget* w) +{ + for (Children::iterator c = children.begin(); c != children.end(); ++c) { + if ((*c)->w == w) { + (*c)->show_con.disconnect (); + (*c)->hide_con.disconnect (); + (*c)->w = NULL; // mark invalid + children.erase (c); + break; + } + } + return 0; +} + +void +Pane::on_remove (Widget* w) +{ + for (Children::iterator c = children.begin(); c != children.end(); ++c) { + if ((*c)->w == w) { + (*c)->show_con.disconnect (); + (*c)->hide_con.disconnect (); + w->remove_destroy_notify_callback ((*c).get()); + w->unparent (); + (*c)->w = NULL; // mark invalid + children.erase (c); + break; + } + } +} + +void +Pane::on_size_allocate (Gtk::Allocation& alloc) +{ + reallocate (alloc); + Container::on_size_allocate (alloc); + + /* minumum pane size constraints */ + Dividers::size_type div = 0; + for (Dividers::const_iterator d = dividers.begin(); d != dividers.end(); ++d, ++div) { + // XXX skip dividers that were just hidden in reallocate() + Pane::set_divider (div, (*d)->fract); + } + // TODO this needs tweaking for panes with > 2 children + // if a child grows, re-check the ones before it. + assert (dividers.size () < 3); +} + +void +Pane::reallocate (Gtk::Allocation const & alloc) +{ + int remaining; + int xpos = alloc.get_x(); + int ypos = alloc.get_y(); + float fract; + + if (children.empty()) { + return; + } + + if (children.size() == 1) { + /* only child gets the full allocation */ + if (children.front()->w->is_visible ()) { + children.front()->w->size_allocate (alloc); + } + return; + } + + if (horizontal) { + remaining = alloc.get_width (); + } else { + remaining = alloc.get_height (); + } + + Children::iterator child; + Children::iterator next; + Dividers::iterator div; + + child = children.begin(); + + /* skip initial hidden children */ + + while (child != children.end()) { + if ((*child)->w->is_visible()) { + break; + } + ++child; + } + + for (div = dividers.begin(); child != children.end(); ) { + + Gtk::Allocation child_alloc; + + next = child; + + /* Move on to next *visible* child */ + + while (++next != children.end()) { + if ((*next)->w->is_visible()) { + break; + } + } + + child_alloc.set_x (xpos); + child_alloc.set_y (ypos); + + if (next == children.end()) { + /* last child gets all the remaining space */ + fract = 1.0; + } else { + /* child gets the fraction of the remaining space given by the divider that follows it */ + fract = (*div)->fract; + } + + Gtk::Requisition cr; + (*child)->w->size_request (cr); + + if (horizontal) { + child_alloc.set_width ((gint) floor (remaining * fract)); + child_alloc.set_height (alloc.get_height()); + remaining = max (0, (remaining - child_alloc.get_width())); + xpos += child_alloc.get_width(); + } else { + child_alloc.set_width (alloc.get_width()); + child_alloc.set_height ((gint) floor (remaining * fract)); + remaining = max (0, (remaining - child_alloc.get_height())); + ypos += child_alloc.get_height (); + } + + if ((*child)->minsize) { + if (horizontal) { + child_alloc.set_width (max (child_alloc.get_width(), (*child)->minsize)); + } else { + child_alloc.set_height (max (child_alloc.get_height(), (*child)->minsize)); + } + } + + if ((*child)->w->is_visible ()) { + (*child)->w->size_allocate (child_alloc); + } + + if (next == children.end()) { + /* done, no more children, no need for a divider */ + break; + } + + child = next; + + /* add a divider between children */ + + Gtk::Allocation divider_allocation; + + divider_allocation.set_x (xpos); + divider_allocation.set_y (ypos); + + if (horizontal) { + divider_allocation.set_width (divider_width); + divider_allocation.set_height (alloc.get_height()); + remaining = max (0, remaining - divider_width); + xpos += divider_width; + } else { + divider_allocation.set_width (alloc.get_width()); + divider_allocation.set_height (divider_width); + remaining = max (0, remaining - divider_width); + ypos += divider_width; + } + + (*div)->size_allocate (divider_allocation); + (*div)->show (); + ++div; + } + + /* hide all remaining dividers */ + + while (div != dividers.end()) { + (*div)->hide (); + ++div; + } +} + +bool +Pane::on_expose_event (GdkEventExpose* ev) +{ + Children::iterator child; + Dividers::iterator div; + + for (child = children.begin(), div = dividers.begin(); child != children.end(); ++child) { + + if ((*child)->w->is_visible()) { + propagate_expose (*((*child)->w), ev); + } + + if (div != dividers.end()) { + if ((*div)->is_visible()) { + propagate_expose (**div, ev); + } + ++div; + } + } + + return true; +} + +bool +Pane::handle_press_event (GdkEventButton* ev, Divider* d) +{ + d->dragging = true; + d->queue_draw (); + + return false; +} + +bool +Pane::handle_release_event (GdkEventButton* ev, Divider* d) +{ + d->dragging = false; + + if (did_move && !children.empty()) { + children.front()->w->queue_resize (); + did_move = false; + } + + return false; +} +void +Pane::set_check_divider_position (bool yn) +{ + check_fract = yn; +} + +float +Pane::constrain_fract (Dividers::size_type div, float fract) +{ + if (get_allocation().get_width() == 1 && get_allocation().get_height() == 1) { + /* space not * allocated - * divider being set from startup code. Let it pass, + * since our goal is mostly to catch drags to a position that will interfere with window + * resizing. + */ + return fract; + } + + if (children.size () <= div + 1) { return fract; } // XXX remove once hidden divs are skipped + assert(children.size () > div + 1); + + const float size = horizontal ? get_allocation().get_width() : get_allocation().get_height(); + + // TODO: optimize: cache in Pane::on_size_request + Gtk::Requisition prev_req(children.at (div)->w->size_request ()); + Gtk::Requisition next_req(children.at (div + 1)->w->size_request ()); + float prev = (horizontal ? prev_req.width : prev_req.height); + float next = (horizontal ? next_req.width : next_req.height); + + if (children.at (div)->minsize) { + prev = children.at (div)->minsize; + } + if (children.at (div + 1)->minsize) { + next = children.at (div + 1)->minsize; + } + + if (size * fract < prev) { + return prev / size; + } + if (size * (1.f - fract) < next) { + return 1.f - next / size; + } + + if (!check_fract) { + return fract; + } + +#ifdef __APPLE__ + + /* On Quartz, if the pane handle (divider) gets to + be adjacent to the window edge, you can no longer grab it: + any attempt to do so is interpreted by the Quartz window + manager ("Finder") as a resize drag on the window edge. + */ + + + if (horizontal) { + if (div == dividers.size() - 1) { + if (get_allocation().get_width() * (1.0 - fract) < (divider_width*2)) { + /* too close to right edge */ + return 1.f - (divider_width * 2.f) / (float) get_allocation().get_width(); + } + } + + if (div == 0) { + if (get_allocation().get_width() * fract < (divider_width*2)) { + /* too close to left edge */ + return (divider_width * 2.f) / (float)get_allocation().get_width(); + } + } + } else { + if (div == dividers.size() - 1) { + if (get_allocation().get_height() * (1.0 - fract) < (divider_width*2)) { + /* too close to bottom */ + return 1.f - (divider_width * 2.f) / (float) get_allocation().get_height(); + } + } + + if (div == 0) { + if (get_allocation().get_height() * fract < (divider_width*2)) { + /* too close to top */ + return (divider_width * 2.f) / (float) get_allocation().get_height(); + } + } + } +#endif + return fract; +} + +bool +Pane::handle_motion_event (GdkEventMotion* ev, Divider* d) +{ + did_move = true; + + if (!d->dragging) { + return true; + } + + /* determine new position for handle */ + + float new_fract; + int px, py; + + d->translate_coordinates (*this, ev->x, ev->y, px, py); + + Dividers::iterator prev = dividers.end(); + Dividers::size_type div = 0; + + for (Dividers::iterator di = dividers.begin(); di != dividers.end(); ++di, ++div) { + if (*di == d) { + break; + } + prev = di; + } + + int space_remaining; + int prev_edge; + + if (horizontal) { + if (prev != dividers.end()) { + prev_edge = (*prev)->get_allocation().get_x() + (*prev)->get_allocation().get_width(); + } else { + prev_edge = 0; + } + space_remaining = get_allocation().get_width() - prev_edge; + new_fract = (float) (px - prev_edge) / space_remaining; + } else { + if (prev != dividers.end()) { + prev_edge = (*prev)->get_allocation().get_y() + (*prev)->get_allocation().get_height(); + } else { + prev_edge = 0; + } + space_remaining = get_allocation().get_height() - prev_edge; + new_fract = (float) (py - prev_edge) / space_remaining; + } + + new_fract = min (1.0f, max (0.0f, new_fract)); + new_fract = constrain_fract (div, new_fract); + new_fract = min (1.0f, max (0.0f, new_fract)); + + if (new_fract != d->fract) { + d->fract = new_fract; + reallocate (get_allocation ()); + queue_draw (); + } + + return true; +} + +void +Pane::set_divider (Dividers::size_type div, float fract) +{ + Dividers::iterator d = dividers.begin(); + + for (d = dividers.begin(); d != dividers.end() && div != 0; ++d, --div) { + /* relax */ + } + + if (d == dividers.end()) { + /* caller is trying to set divider that does not exist + * yet. + */ + return; + } + + fract = max (0.0f, min (1.0f, fract)); + fract = constrain_fract (div, fract); + fract = max (0.0f, min (1.0f, fract)); + + if (fract != (*d)->fract) { + (*d)->fract = fract; + /* our size hasn't changed, but our internal allocations have */ + reallocate (get_allocation()); + queue_draw (); + } +} + +float +Pane::get_divider (Dividers::size_type div) +{ + Dividers::iterator d = dividers.begin(); + + for (d = dividers.begin(); d != dividers.end() && div != 0; ++d, --div) { + /* relax */ + } + + if (d == dividers.end()) { + /* caller is trying to set divider that does not exist + * yet. + */ + return -1.0f; + } + + return (*d)->fract; +} + +void +Pane::forall_vfunc (gboolean include_internals, GtkCallback callback, gpointer callback_data) +{ + /* since the callback could modify the child list(s), make sure we keep + * the iterators safe; + */ + Children kids (children); + for (Children::const_iterator c = kids.begin(); c != kids.end(); ++c) { + if ((*c)->w) { + callback ((*c)->w->gobj(), callback_data); + } + } + + if (include_internals) { + for (Dividers::iterator d = dividers.begin(); d != dividers.end(); ) { + Dividers::iterator next = d; + ++next; + callback (GTK_WIDGET((*d)->gobj()), callback_data); + d = next; + } + } +} + +Pane::Divider::Divider () + : fract (0.0) + , dragging (false) +{ + set_events (Gdk::EventMask (Gdk::BUTTON_PRESS| + Gdk::BUTTON_RELEASE| + Gdk::MOTION_NOTIFY| + Gdk::ENTER_NOTIFY| + Gdk::LEAVE_NOTIFY)); +} + +bool +Pane::Divider::on_expose_event (GdkEventExpose* ev) +{ + Gdk::Color c = (dragging ? get_style()->get_fg (Gtk::STATE_ACTIVE) : + get_style()->get_fg (get_state())); + + Cairo::RefPtr<Cairo::Context> draw_context = get_window()->create_cairo_context (); + draw_context->rectangle (ev->area.x, ev->area.y, ev->area.width, ev->area.height); + draw_context->clip_preserve (); + draw_context->set_source_rgba (c.get_red_p(), c.get_green_p(), c.get_blue_p(), 1.0); + draw_context->fill (); + + return true; +} + +bool +Pane::handle_enter_event (GdkEventCrossing*, Divider* d) +{ + d->get_window()->set_cursor (drag_cursor); + d->set_state (Gtk::STATE_SELECTED); + return true; +} + +bool +Pane::handle_leave_event (GdkEventCrossing*, Divider* d) +{ + d->get_window()->set_cursor (); + d->set_state (Gtk::STATE_NORMAL); + d->queue_draw (); + return true; +} |