From 4cbfec9c35ef7e9679e5df43efe9a386ffc6a77f Mon Sep 17 00:00:00 2001 From: Matthias Koefferlein Date: Sun, 22 Oct 2017 23:06:42 +0200 Subject: [PATCH] New feature: allow group generation in menu paths. --- src/lay/lay/doc/about/macro_in_menu.xml | 50 ++++- src/laybasic/laybasic/layAbstractMenu.cc | 229 +++++++++++++++------ src/laybasic/laybasic/layAbstractMenu.h | 18 +- src/laybasic/unit_tests/layAbstractMenu.cc | 128 ++++++++++++ src/laybasic/unit_tests/unit_tests.pro | 1 + 5 files changed, 355 insertions(+), 71 deletions(-) create mode 100644 src/laybasic/unit_tests/layAbstractMenu.cc diff --git a/src/lay/lay/doc/about/macro_in_menu.xml b/src/lay/lay/doc/about/macro_in_menu.xml index 701bf2277..ce78eda0b 100644 --- a/src/lay/lay/doc/about/macro_in_menu.xml +++ b/src/lay/lay/doc/about/macro_in_menu.xml @@ -19,15 +19,49 @@

The menu path specifies the position where the macro shall be put. A macro path is a - sequence of symbolic names separated by a dot. For example "edit_menu.end" is the end - of the "Edit" menu and "edit_menu.undo" is the "Undo" entry in the "Edit" menu. To obtain - a list of the paths available, have a look at the "Key Bindings" page in the "Application" - section of the setup dialog ("File/Setup"). The pseudo element "end" denotes the position - after the last entry. The same way "begin" indicates the first entry in the menu. - The macro will be inserted before the entry indicated by the path. Hence the if a - path of "edit_menu.end" is given, the macro will be inserted at the end of the - "Edit" menu and "edit_menu.undo" will insert the macro before the "Undo" entry. + sequence of symbolic names separated by a dot. For example:

+ +
  • + + +
  • + +

    To obtain + a list of the paths available, have a look at the "Key Bindings" page in the "Application" + section of the setup dialog ("File/Setup"). +

    + +

    + The pseudo element "end" denotes the position + after the last entry. The same way "begin" indicates the first entry in the menu. + The macro will be inserted before the entry indicated by the path. Hence: +

    + + + +

    + If a plus sign follows the macro path element, the new element is inserted after + this element. For example: +

    + + + +

    + A special form can be used to generate new groups: if the given element does not exist, + the menu generator can be instructed to create it by appending the insert point plus + the new text string to the element after a ">" character. For example: +

    + +

    If no macro path is specified, the macro is inserted in the "Macros" menu. diff --git a/src/laybasic/laybasic/layAbstractMenu.cc b/src/laybasic/laybasic/layAbstractMenu.cc index 8f7b9b838..c8a1d808e 100644 --- a/src/laybasic/laybasic/layAbstractMenu.cc +++ b/src/laybasic/laybasic/layAbstractMenu.cc @@ -101,13 +101,13 @@ parse_menu_title (const std::string &s, std::string &title, std::string &shortcu // AbstractMenuItem implementation AbstractMenuItem::AbstractMenuItem () - : mp_menu (0), m_has_submenu (false) + : mp_menu (0), m_has_submenu (false), m_remove_on_empty (false) { // ... nothing yet .. } AbstractMenuItem::AbstractMenuItem (const AbstractMenuItem &) - : mp_menu (0), m_has_submenu (false) + : mp_menu (0), m_has_submenu (false), m_remove_on_empty (false) { // ... nothing yet .. } @@ -173,7 +173,13 @@ AbstractMenuItem::set_has_submenu () m_has_submenu = true; } -void +void +AbstractMenuItem::set_remove_on_empty () +{ + m_remove_on_empty = true; +} + +void AbstractMenuItem::set_menu (QMenu *menu) { mp_menu = menu; @@ -708,7 +714,7 @@ AbstractMenu::create_action (const std::string &s) AbstractMenu::AbstractMenu (AbstractMenuProvider *provider) : mp_provider (provider) { - tl_assert (mp_provider != 0); + // .. nothing yet .. } AbstractMenu::~AbstractMenu () @@ -777,6 +783,8 @@ AbstractMenu::build_detached (const std::string &name, QMenuBar *mbar) void AbstractMenu::build (QMenuBar *mbar, QToolBar *tbar) { + tl_assert (mp_provider != 0); + m_helper_menu_items.clear (); mbar->clear (); tbar->clear (); @@ -942,23 +950,27 @@ AbstractMenu::items (const std::string &path) const } void -AbstractMenu::insert_item (const std::string &path, const std::string &name, const Action &action) +AbstractMenu::insert_item (const std::string &p, const std::string &name, const Action &action) { - std::pair::iterator > ins = find_item (path); - if (ins.first) { + typedef std::vector::iterator > > path_type; + path_type path = find_item (p); + if (! path.empty ()) { + + AbstractMenuItem *parent = path.back ().first; + std::list::iterator iter = path.back ().second; // insert the new item - ins.first->children.insert (ins.second, AbstractMenuItem ()); - --ins.second; + parent->children.insert (iter, AbstractMenuItem ()); + --iter; - ins.second->setup_item (ins.first->name (), name, action); + iter->setup_item (parent->name (), name, action); - // find an item with the same name and remove it - for (std::list::iterator existing = ins.first->children.begin (); existing != ins.first->children.end (); ) { + // find any items with the same name and remove them + for (std::list::iterator existing = parent->children.begin (); existing != parent->children.end (); ) { std::list::iterator existing_next = existing; ++existing_next; - if (existing->name () == ins.second->name () && &*existing != &*ins.second) { - ins.first->children.erase (existing); + if (existing->name () == iter->name () && existing != iter) { + parent->children.erase (existing); } existing = existing_next; } @@ -969,29 +981,55 @@ AbstractMenu::insert_item (const std::string &path, const std::string &name, con } void -AbstractMenu::insert_separator (const std::string &path, const std::string &name) +AbstractMenu::insert_separator (const std::string &p, const std::string &name) { - std::pair::iterator > ins = find_item (path); - if (ins.first) { - ins.first->children.insert (ins.second, AbstractMenuItem ()); - --ins.second; + tl_assert (mp_provider != 0); // required to get the parent for the new QAction (via ActionHandle) + + typedef std::vector::iterator > > path_type; + path_type path = find_item (p); + if (! path.empty ()) { + + AbstractMenuItem *parent = path.back ().first; + std::list::iterator iter = path.back ().second; + + parent->children.insert (iter, AbstractMenuItem ()); + --iter; Action action (new ActionHandle (mp_provider->menu_parent_widget ())); action.set_separator (true); - ins.second->setup_item (ins.first->name (), name, action); + iter->setup_item (parent->name (), name, action); + } + emit changed (); } void -AbstractMenu::insert_menu (const std::string &path, const std::string &name, const Action &action) +AbstractMenu::insert_menu (const std::string &p, const std::string &name, const Action &action) { - std::pair::iterator > ins = find_item (path); - if (ins.first) { - ins.first->children.insert (ins.second, AbstractMenuItem ()); - --ins.second; - ins.second->setup_item (ins.first->name (), name, action); - ins.second->set_has_submenu (); + typedef std::vector::iterator > > path_type; + path_type path = find_item (p); + if (! path.empty ()) { + + AbstractMenuItem *parent = path.back ().first; + std::list::iterator iter = path.back ().second; + + parent->children.insert (iter, AbstractMenuItem ()); + --iter; + iter->setup_item (parent->name (), name, action); + iter->set_has_submenu (); + + // find any items with the same name and remove them + for (std::list::iterator existing = parent->children.begin (); existing != parent->children.end (); ) { + std::list::iterator existing_next = existing; + ++existing_next; + if (existing->name () == iter->name () && existing != iter) { + parent->children.erase (existing); + } + existing = existing_next; + } + } + emit changed (); } @@ -1002,28 +1040,43 @@ AbstractMenu::insert_menu (const std::string &path, const std::string &name, con } void -AbstractMenu::delete_item (const std::string &path) +AbstractMenu::delete_item (const std::string &p) { - std::pair::iterator > ins = find_item (path); - if (ins.first) { - if (ins.second == ins.first->children.end ()) { - throw tl::Exception (tl::to_string (QObject::tr ("delete_item cannot delete past-the-end item"))); + typedef std::vector::iterator > > path_type; + path_type path = find_item (p); + if (! path.empty ()) { + + for (path_type::const_reverse_iterator p = path.rbegin (); p != path.rend (); ++p) { + + if (p->second == p->first->children.end ()) { + break; + } else if (p != path.rbegin () && (! p->second->remove_on_empty () || ! p->second->children.empty ())) { + // stop on non-empty parent menues + break; + } + + reset_menu_objects (*p->second); + p->first->children.erase (p->second); + } - reset_menu_objects (*ins.second); - ins.first->children.erase (ins.second); + } + emit changed (); } -static void do_delete_items (std::list &list, const Action &action) +static void do_delete_items (AbstractMenuItem &parent, const Action &action) { - for (std::list::iterator l = list.begin (); l != list.end (); ) { + for (std::list::iterator l = parent.children.begin (); l != parent.children.end (); ) { std::list::iterator ll = l; ++ll; if (l->action () == action) { - list.erase (l); + parent.children.erase (l); } else { - do_delete_items (l->children, action); + do_delete_items (*l, action); + if (l->remove_on_empty () && l->children.empty ()) { + parent.children.erase (l); + } } l = ll; } @@ -1032,7 +1085,7 @@ static void do_delete_items (std::list &list, const Action &ac void AbstractMenu::delete_items (const Action &action) { - do_delete_items (m_root.children, action); + do_delete_items (m_root, action); emit changed (); } @@ -1099,14 +1152,17 @@ AbstractMenu::find_item_exact (const std::string &path) return item; } -std::pair::iterator> -AbstractMenu::find_item (const std::string &path) +std::vector::iterator> > +AbstractMenu::find_item (const std::string &p) { - tl::Extractor extr (path.c_str ()); + typedef std::vector::iterator> > path_type; + path_type path; + + tl::Extractor extr (p.c_str ()); AbstractMenuItem *parent = &m_root; std::list::iterator iter = m_root.children.end (); - while (true) { + while (parent && ! extr.at_end ()) { if (extr.test ("#")) { @@ -1118,70 +1174,119 @@ AbstractMenu::find_item (const std::string &path) ++iter; } if (n > 0) { - return std::make_pair ((AbstractMenuItem *) 0, m_root.children.end ()); + return path_type (); } + } else if (extr.test ("begin")) { + + iter = parent->children.begin (); + + } else if (extr.test ("end")) { + + iter = parent->children.end (); + } else { - std::string n; - extr.read (n, ".+"); + std::string n, nn; - if (n == "begin") { - return std::make_pair (parent, parent->children.begin ()); - } else if (n == "end") { - return std::make_pair (parent, parent->children.end ()); + extr.read (n, ".+>("); + + if (extr.test (">")) { + extr.read (nn, ".+>("); } std::string name (parent->name ()); if (! name.empty ()) { name += "."; } + + std::string nname; + nname = name + nn; name += n; bool after = extr.test ("+"); + std::string ndesc; + if (! nn.empty () && extr.test ("(")) { + extr.read_word_or_quoted (ndesc, " _.$"); + extr.test (")"); + } + AbstractMenuItem *p = parent; parent = 0; + + // Look for the next path item for (std::list::iterator c = p->children.begin (); c != p->children.end (); ++c) { - if (c->name () == name) { - - if (after) { + if (after && nn.empty ()) { ++c; - if (c == p->children.end ()) { - return std::make_pair (parent, parent->children.end ()); - } } - parent = p; iter = c; break; + } + } + // If that's not found, check whether we are supposed to create one: + // identify the insert position and create a new entry there. + if (! parent && ! nn.empty ()) { + + if (nn == "begin") { + parent = p; + iter = parent->children.begin (); + } else if (nn == "end") { + parent = p; + iter = parent->children.end (); + } else { + for (std::list::iterator c = p->children.begin (); c != p->children.end (); ++c) { + if (c->name () == nname) { + if (after) { + ++c; + } + parent = p; + iter = c; + break; + } + } + } + + if (parent) { + parent->children.insert (iter, AbstractMenuItem ()); + --iter; + iter->setup_item (parent->name (), n, Action ()); + iter->set_has_submenu (); + iter->set_remove_on_empty (); + iter->set_action_title (ndesc.empty () ? n : ndesc); } } if (! parent) { - return std::make_pair ((AbstractMenuItem *) 0, m_root.children.end ()); + return path_type (); } } - if (extr.at_end ()) { - return std::make_pair (parent, iter); - } + path.push_back (std::make_pair (parent, iter)); extr.test ("."); - parent = &*iter; + if (iter == parent->children.end ()) { + parent = 0; + } else { + parent = iter.operator-> (); + } } + return path; } void AbstractMenu::transfer (const MenuLayoutEntry *layout, AbstractMenuItem &item) { + tl_assert (mp_provider != 0); + while (layout->name) { item.children.push_back (AbstractMenuItem ()); diff --git a/src/laybasic/laybasic/layAbstractMenu.h b/src/laybasic/laybasic/layAbstractMenu.h index 4b7e2dc60..171b70982 100644 --- a/src/laybasic/laybasic/layAbstractMenu.h +++ b/src/laybasic/laybasic/layAbstractMenu.h @@ -505,11 +505,19 @@ struct LAYBASIC_PUBLIC AbstractMenuItem return m_has_submenu; } + void set_remove_on_empty (); + + bool remove_on_empty () const + { + return m_remove_on_empty; + } + std::list children; private: QMenu *mp_menu; bool m_has_submenu; + bool m_remove_on_empty; Action m_action; std::string m_name; std::string m_basename; @@ -750,6 +758,14 @@ public: */ QActionGroup *make_exclusive_group (const std::string &name); + /** + * @brief Gets the root node of the menu + */ + const AbstractMenuItem &root () const + { + return m_root; + } + signals: /** * @brief this signal is emitted whenever something changes on the menu @@ -757,7 +773,7 @@ signals: void changed (); private: - std::pair::iterator> find_item (const std::string &path); + std::vector::iterator> > find_item (const std::string &path); const AbstractMenuItem *find_item_exact (const std::string &path) const; AbstractMenuItem *find_item_exact (const std::string &path); void transfer (const MenuLayoutEntry *layout, AbstractMenuItem &item); diff --git a/src/laybasic/unit_tests/layAbstractMenu.cc b/src/laybasic/unit_tests/layAbstractMenu.cc new file mode 100644 index 000000000..c5b6b2703 --- /dev/null +++ b/src/laybasic/unit_tests/layAbstractMenu.cc @@ -0,0 +1,128 @@ + +/* + + 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 "layAbstractMenu.h" + +#include "tlUnitTest.h" + +std::string node_to_string (const lay::AbstractMenuItem &item) +{ + std::string s = item.name (); + if (! item.children.empty ()) { + s += "("; + for (std::list::const_iterator c = item.children.begin (); c != item.children.end (); ++c) { + if (c != item.children.begin ()) { + s += ","; + } + s += node_to_string (*c); + } + s += ")"; + } + return s; +} + +std::string menu_to_string (const lay::AbstractMenu &menu) +{ + return node_to_string (menu.root ()); +} + +TEST(1) +{ + lay::AbstractMenu menu (0); + EXPECT_EQ (menu_to_string (menu), ""); + + try { + EXPECT_EQ (menu.action ("n1").get_title (), ""); + EXPECT_EQ (true, false); + } catch (...) { + } + + EXPECT_EQ (menu.is_valid ("n1"), false); + + menu.insert_menu ("end", "n1", lay::Action ("title:n1")); + EXPECT_EQ (menu_to_string (menu), "(n1)"); + EXPECT_EQ (tl::join (menu.items (""), ","), "n1"); + EXPECT_EQ (menu.is_menu ("n1"), true); + EXPECT_EQ (menu.action ("n1").get_title (), "title:n1"); + + EXPECT_EQ (menu.is_valid ("n1"), true); + EXPECT_EQ (menu.is_valid ("n2"), false); + + menu.insert_menu ("end", "n2", lay::Action ("title:n2")); + EXPECT_EQ (menu_to_string (menu), "(n1,n2)"); + EXPECT_EQ (tl::join (menu.items (""), ","), "n1,n2"); + EXPECT_EQ (menu.is_menu ("n2"), true); + EXPECT_EQ (menu.action ("n2").get_title (), "title:n2"); + + EXPECT_EQ (menu.is_valid ("n2"), true); + + menu.insert_menu ("end", "n1", lay::Action ("title:n1")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1)"); + EXPECT_EQ (menu.is_menu ("n1"), true); + EXPECT_EQ (menu.action ("n1").get_title (), "title:n1"); + + menu.insert_item ("n1.begin", "c1", lay::Action ("title:c1")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c1))"); + EXPECT_EQ (tl::join (menu.items ("n1"), ","), "n1.c1"); + EXPECT_EQ (menu.action ("n1.c1").get_title (), "title:c1"); + + menu.insert_item ("n1.end", "c2", lay::Action ("title:c2")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c1,n1.c2))"); + EXPECT_EQ (tl::join (menu.items ("n1"), ","), "n1.c1,n1.c2"); + EXPECT_EQ (menu.is_menu ("n1.c2"), false); + EXPECT_EQ (menu.action ("n1.c2").get_title (), "title:c2"); + + menu.insert_item ("n1.begin", "c1", lay::Action ("title:c1a")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c1,n1.c2))"); + EXPECT_EQ (tl::join (menu.items ("n1"), ","), "n1.c1,n1.c2"); + EXPECT_EQ (menu.action ("n1.c1").get_title (), "title:c1a"); + + menu.insert_item ("n1.c1", "c3", lay::Action ("title:c3")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c3,n1.c1,n1.c2))"); + + menu.insert_item ("n1.c1+", "c4", lay::Action ("title:c4")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c3,n1.c1,n1.c4,n1.c2))"); + EXPECT_EQ (menu.action ("n1.c4").get_title (), "title:c4"); + + menu.delete_item ("n1.c1"); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c3,n1.c4,n1.c2))"); + + menu.delete_item ("n1"); + EXPECT_EQ (menu_to_string (menu), "(n2)"); + + menu.insert_item ("n1>end(title).end", "c1", lay::Action ("title:c1")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c1))"); + EXPECT_EQ (menu.action ("n1.c1").get_title (), "title:c1"); + + menu.insert_item ("n1>end(title).end", "c2", lay::Action ("title:c2")); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c1,n1.c2))"); + + menu.delete_item ("n1.c1"); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c2))"); + + menu.delete_item ("n1.c1"); + EXPECT_EQ (menu_to_string (menu), "(n2,n1(n1.c2))"); + + menu.delete_item ("n1.c2"); + EXPECT_EQ (menu_to_string (menu), "(n2)"); +} + diff --git a/src/laybasic/unit_tests/unit_tests.pro b/src/laybasic/unit_tests/unit_tests.pro index ae6a63189..5fd39fc2f 100644 --- a/src/laybasic/unit_tests/unit_tests.pro +++ b/src/laybasic/unit_tests/unit_tests.pro @@ -14,6 +14,7 @@ SOURCES = \ layParsedLayerSource.cc \ layRenderer.cc \ laySnap.cc \ + layAbstractMenu.cc INCLUDEPATH += $$TL_INC $$LAYBASIC_INC $$DB_INC $$GSI_INC DEPENDPATH += $$TL_INC $$LAYBASIC_INC $$DB_INC $$GSI_INC