/* KLayout Layout Viewer Copyright (C) 2006-2017 Matthias Koefferlein 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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "layEditable.h" #include "dbClipboard.h" #include "tlAssert.h" #include "layPropertiesDialog.h" #include namespace lay { // ---------------------------------------------------------------- // A helper compare functor template struct first_of_pair_cmp_f { bool operator() (const std::pair &p1, const std::pair &p2) const { return p1.first < p2.first; } }; // ---------------------------------------------------------------- // Editable implementation Editable::Editable (lay::Editables *editables) : mp_editables (editables) { if (editables) { editables->m_editables.push_back (this); } } Editable::~Editable () { // erase the object from the table of enabled services if (mp_editables) { mp_editables->enable (this, false); } } // ---------------------------------------------------------------- // Editables implementation Editables::Editables (db::Manager *manager) : db::Object (manager), mp_properties_dialog (0), m_move_selection (false), m_any_move_operation (false) { // .. nothing yet .. } Editables::~Editables () { cancel_edits (); } void Editables::del () { if (selection_size () > 0) { cancel_edits (); // begin the transaction tl_assert (! manager ()->transacting ()); manager ()->transaction (tl::to_string (QObject::tr ("Delete"))); // this dummy operation will update the screen: manager ()->queue (this, new db::Op ()); for (iterator e = begin (); e != end (); ++e) { e->del (); } // end the transaction manager ()->commit (); } } void Editables::cut () { if (selection_size () > 0) { cancel_edits (); // this dummy operation will update the screen: manager ()->queue (this, new db::Op ()); db::Clipboard::instance ().clear (); for (iterator e = begin (); e != end (); ++e) { e->cut (); } } } void Editables::copy () { if (selection_size () > 0) { db::Clipboard::instance ().clear (); for (iterator e = begin (); e != end (); ++e) { e->copy (); } } } db::DBox Editables::selection_bbox () { db::DBox sel_bbox; for (iterator e = begin (); e != end (); ++e) { sel_bbox += e->selection_bbox (); } return sel_bbox; } void Editables::transform (const db::DCplxTrans &tr) { if (selection_size () > 0) { try { tl_assert (! manager ()->transacting ()); manager ()->transaction (tl::to_string (QObject::tr ("Transform"))); // this dummy operation will update the screen: manager ()->queue (this, new db::Op ()); for (iterator e = begin (); e != end (); ++e) { e->transform (tr); } // end the transaction manager ()->commit (); } catch (...) { manager ()->clear (); throw; } } } void Editables::paste () { if (! db::Clipboard::instance ().empty ()) { cancel_edits (); // this dummy operation will update the screen: if (manager ()->transacting ()) { manager ()->queue (this, new db::Op ()); } for (iterator e = begin (); e != end (); ++e) { e->paste (); } } } void Editables::enable (lay::Editable *obj, bool en) { if (en) { m_enabled.insert (obj); } else { cancel_edits (); obj->select (db::DBox (), lay::Editable::Reset); // clear selection m_enabled.erase (obj); } } void Editables::transient_select (const db::DPoint &pt) { bool same_point = (m_last_selected_point.is_point () && m_last_selected_point.center ().sq_double_distance (pt) < 1e-10); if (! same_point) { clear_previous_selection (); } m_last_selected_point = db::DBox (pt, pt); // in a first pass evaluate the point selection proximity value to select // those plugins that are active. This code is a copy of the code for the single-point selection below. std::vector< std::pair > plugins; for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { plugins.push_back (std::make_pair (e->click_proximity (pt, lay::Editable::Replace), e)); } } // sort the plugins found by the proximity std::sort (plugins.begin (), plugins.end (), first_of_pair_cmp_f ()); // and call select on those objects until the first one (with the least proximity) // has something selected std::vector< std::pair >::const_iterator pi = plugins.begin (); for ( ; pi != plugins.end (); ++pi) { if (pi->second->transient_select (pt)) { break; } } // if no plugin has selected anything, try a reset (clear previous selection) and select again: this is supposed // to implement the cycling protocol which allows the plugins to cycle through different // selections for repeated clicks on the same point. Let this happen for replace mode // only because otherwise clearing the selection does not make sense. if (same_point && pi == plugins.end ()) { clear_previous_selection (); plugins.clear (); for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { plugins.push_back (std::make_pair (e->click_proximity (pt, lay::Editable::Replace), e)); } } // sort the plugins found by the proximity std::sort (plugins.begin (), plugins.end (), first_of_pair_cmp_f ()); for (pi = plugins.begin (); pi != plugins.end (); ++pi) { if (pi->second->transient_select (pt)) { break; } } } // send a signal to the observers signal_transient_selection_changed (); } void Editables::clear_previous_selection () { m_last_selected_point = db::DBox (); for (iterator e = begin (); e != end (); ++e) { e->clear_previous_selection (); } } void Editables::clear_transient_selection () { for (iterator e = begin (); e != end (); ++e) { e->clear_transient_selection (); } // send a signal to the observers signal_transient_selection_changed (); } void Editables::clear_selection () { cancel_edits (); for (iterator e = begin (); e != end (); ++e) { e->select (db::DBox (), lay::Editable::Reset); // clear selection e->clear_transient_selection (); e->clear_previous_selection (); } // send a signal to the observers signal_selection_changed (); } void Editables::select () { cancel_edits (); clear_transient_selection (); clear_previous_selection (); for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { e->select (db::DBox (), lay::Editable::Replace); // select "all" } } // send a signal to the observers signal_selection_changed (); } void Editables::select (const db::DBox &box, lay::Editable::SelectionMode mode) { if (box.is_point ()) { select (box.center (), mode); } else { cancel_edits (); clear_transient_selection (); clear_previous_selection (); for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { e->select (box, mode); } } // send a signal to the observers signal_selection_changed (); } } void Editables::select (const db::DPoint &pt, lay::Editable::SelectionMode mode) { bool same_point = (m_last_selected_point.is_point () && m_last_selected_point.center ().sq_double_distance (pt) < 1e-10); if (! same_point) { clear_previous_selection (); } m_last_selected_point = db::DBox (pt, pt); cancel_edits (); clear_transient_selection (); // in a first pass evaluate the point selection proximity value to select // those plugins that are active. std::vector< std::pair > plugins; for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { plugins.push_back (std::make_pair (e->click_proximity (pt, mode), e)); } } // sort the plugins found by the proximity std::sort (plugins.begin (), plugins.end (), first_of_pair_cmp_f ()); // and call select on those objects until the first one (with the least proximity) // has something selected std::vector< std::pair >::const_iterator pi = plugins.begin (); for ( ; pi != plugins.end (); ++pi) { if (pi->second->select (db::DBox (pt, pt), mode)) { break; } } // if no plugin has selected anything, try a reset (clear_previous selection) and select again: this is supposed // to implement the cycling protocol which allows the plugins to cycle through different // selections for repeated clicks on the same point. Let this happen for replace mode // only because otherwise clearing the selection does not make sense. if (same_point && pi == plugins.end () && mode == lay::Editable::Replace) { clear_previous_selection (); plugins.clear (); for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { plugins.push_back (std::make_pair (e->click_proximity (pt, mode), e)); } } std::sort (plugins.begin (), plugins.end (), first_of_pair_cmp_f ()); for (pi = plugins.begin (); pi != plugins.end (); ++pi) { if (pi->second->select (db::DBox (pt, pt), mode)) { break; } } } // in replace mode clear the selections on the following plugins if (mode == lay::Editable::Replace && pi != plugins.end ()) { for (++pi ; pi != plugins.end (); ++pi) { pi->second->select (db::DBox (), lay::Editable::Reset); } } // send a signal to the observers signal_selection_changed (); } bool Editables::begin_move (const db::DPoint &p, lay::angle_constraint_type ac) { cancel_edits (); clear_previous_selection (); m_move_selection = false; m_any_move_operation = false; // In a first pass evaluate the point selection proximity value to select // those plugins that are close to the given point. This code is a copy of the code for the single-point selection below. std::vector< std::pair > plugins; for (iterator e = begin (); e != end (); ++e) { if (m_enabled.find (&*e) != m_enabled.end ()) { plugins.push_back (std::make_pair (e->click_proximity (p, lay::Editable::Replace), e)); } } // sort the plugins found by the proximity std::sort (plugins.begin (), plugins.end (), first_of_pair_cmp_f ()); if (selection_size () > 0 && selection_bbox ().contains (p)) { // if anything is selected and we are within the selection bbox, // issue a move operation on all editables: first try a Partial mode begin_move for (std::vector< std::pair >::const_iterator pi = plugins.begin (); pi != plugins.end (); ++pi) { if (pi->second->begin_move (Editable::Partial, p, ac)) { // clear the selection on all other plugins, because we focus on one plugin for (std::vector< std::pair >::const_iterator pii = plugins.begin (); pii != plugins.end (); ++pii) { if (pii->second != pi->second) { pii->second->select (db::DBox (), lay::Editable::Reset); } } return true; } } // if something is selected, issue a move operation on all editables for (iterator e = begin (); e != end (); ++e) { e->begin_move (Editable::Selected, p, ac); } return true; } else { // don't move the selection - clear the existing one first clear_selection (); // if nothing is selected, issue a move operation on all editables std::vector< std::pair >::const_iterator pi = plugins.begin (); // HACK: only the first plugin (measured through click_proximity) has a chance to intercept // the standard procedure which is select + move selected. The reason behind that is that // for example edtService does not implement Any but rather relies on select + Selected mode. // Hence allowing some "later" plugin to override it at this point would break the least // proximity priority rule. if (pi != plugins.end () && pi->second->begin_move (Editable::Any, p, ac)) { return true; } // nothing particular selected - select ourselves and start over select (p, Editable::Replace); // now we assume we have a selection - try to begin_move on this. if (selection_size () > 0) { m_move_selection = true; for (iterator e = begin (); e != end (); ++e) { e->begin_move (Editable::Selected, p, ac); } return true; } else { return false; } } } void Editables::move (const db::DPoint &p, lay::angle_constraint_type ac) { m_any_move_operation = true; for (iterator e = begin (); e != end (); ++e) { e->move (p, ac); } } void Editables::move_transform (const db::DPoint &p, db::DFTrans t, lay::angle_constraint_type ac) { m_any_move_operation = true; for (iterator e = begin (); e != end (); ++e) { e->move_transform (p, t, ac); } } void Editables::end_move (const db::DPoint &p, lay::angle_constraint_type ac) { if (m_any_move_operation) { // begin the transaction tl_assert (! manager ()->transacting ()); manager ()->transaction (tl::to_string (QObject::tr ("Move"))); // this dummy operation will update the screen: manager ()->queue (this, new db::Op ()); for (iterator e = begin (); e != end (); ++e) { e->end_move (p, ac); } // end the transaction manager ()->commit (); // clear the selection that was set previously if (m_move_selection) { clear_selection (); } } else { // if nothing was moved, treat the end_move as a select which makes the move sticky or // replaces a complex selection by a simple one edit_cancel (); select (p, Editable::Replace); } } size_t Editables::selection_size () { size_t c = 0; for (iterator e = begin (); e != end (); ++e) { c += e->selection_size (); } return c; } void Editables::edit_cancel () { clear_previous_selection (); for (iterator e = begin (); e != end (); ++e) { e->edit_cancel (); } } void Editables::cancel_edits () { // close the property dialog if (mp_properties_dialog) { delete mp_properties_dialog; } mp_properties_dialog = 0; // cancel any edit operations for (iterator e = begin (); e != end (); ++e) { e->edit_cancel (); } } void Editables::show_properties (QWidget *parent) { cancel_edits (); mp_properties_dialog = new lay::PropertiesDialog (parent, manager (), this); mp_properties_dialog->show (); } }