diff --git a/src/LyXRC.cpp b/src/LyXRC.cpp index 135ba86907..0b1aafae7c 100644 --- a/src/LyXRC.cpp +++ b/src/LyXRC.cpp @@ -93,6 +93,7 @@ keyword_item lyxrcTags[] = { { "\\fullscreen_tabbar", LyXRC::RC_FULL_SCREEN_TABBAR }, { "\\fullscreen_toolbars", LyXRC::RC_FULL_SCREEN_TOOLBARS }, { "\\fullscreen_width", LyXRC::RC_FULL_SCREEN_WIDTH }, + { "\\group_layouts", LyXRC::RC_GROUP_LAYOUTS }, { "\\index_command", LyXRC::RC_INDEX_COMMAND }, { "\\input", LyXRC::RC_INPUT }, { "\\kbmap", LyXRC::RC_KBMAP }, @@ -272,6 +273,7 @@ void LyXRC::setDefaults() { language_command_begin = "\\selectlanguage{$$lang}"; language_command_local = "\\foreignlanguage{$$lang}{"; sort_layouts = false; + group_layouts = true; default_language = "english"; show_banner = true; windows_style_tex_paths = false; @@ -1270,6 +1272,10 @@ int LyXRC::read(Lexer & lexrc) if (lexrc.next()) sort_layouts = lexrc.getBool(); break; + case RC_GROUP_LAYOUTS: + if (lexrc.next()) + group_layouts = lexrc.getBool(); + break; case RC_FULL_SCREEN_LIMIT: if (lexrc.next()) full_screen_limit = lexrc.getBool(); @@ -1461,6 +1467,14 @@ void LyXRC::write(ostream & os, bool ignore_system_lyxrc, string const & name) c } if (tag != RC_LAST) break; + case RC_GROUP_LAYOUTS: + if (ignore_system_lyxrc || + group_layouts != system_lyxrc.group_layouts) { + os << "# Group layouts by their category.\n" + << "\\group_layouts " << convert(group_layouts) << '\n'; + } + if (tag != RC_LAST) + break; case RC_VIEWDVI_PAPEROPTION: if (ignore_system_lyxrc || view_dvi_paper_option diff --git a/src/LyXRC.h b/src/LyXRC.h index d00ef2e2df..2b1148724e 100644 --- a/src/LyXRC.h +++ b/src/LyXRC.h @@ -79,6 +79,8 @@ public: RC_FULL_SCREEN_TABBAR, RC_FULL_SCREEN_TOOLBARS, RC_FULL_SCREEN_WIDTH, + RC_GEOMETRY_SESSION, + RC_GROUP_LAYOUTS, RC_INDEX_COMMAND, RC_INPUT, RC_KBMAP, @@ -93,8 +95,6 @@ public: RC_LANGUAGE_GLOBAL_OPTIONS, RC_LANGUAGE_PACKAGE, RC_LANGUAGE_USE_BABEL, - RC_SORT_LAYOUTS, - RC_USELASTFILEPOS, RC_LOADSESSION, RC_MACRO_EDIT_STYLE, RC_MAKE_BACKUP, @@ -134,17 +134,18 @@ public: RC_SCREEN_FONT_SIZES, RC_SCREEN_FONT_TYPEWRITER, RC_SCREEN_FONT_TYPEWRITER_FOUNDRY, - RC_GEOMETRY_SESSION, RC_SCREEN_ZOOM, RC_SERVERPIPE, RC_SET_COLOR, RC_SHOW_BANNER, + RC_SORT_LAYOUTS, RC_SPELL_COMMAND, RC_TEMPDIRPATH, RC_TEMPLATEPATH, RC_TEX_ALLOWS_SPACES, RC_TEX_EXPECTS_WINDOWS_PATHS, RC_UIFILE, + RC_USELASTFILEPOS, RC_USER_EMAIL, RC_USER_NAME, RC_USETEMPDIR, @@ -407,6 +408,8 @@ public: unsigned int converter_cache_maxage; /// Sort layouts alphabetically bool sort_layouts; + /// Group layout by their category + bool group_layouts; /// Toggle toolbars in fullscreen mode? bool full_screen_toolbars; /// Toggle scrollbar in fullscreen mode? diff --git a/src/frontends/qt4/GuiPrefs.cpp b/src/frontends/qt4/GuiPrefs.cpp index 6b50910fb2..855ac0b517 100644 --- a/src/frontends/qt4/GuiPrefs.cpp +++ b/src/frontends/qt4/GuiPrefs.cpp @@ -1797,6 +1797,8 @@ PrefUserInterface::PrefUserInterface(GuiPreferences * form, QWidget * parent) this, SIGNAL(changed())); connect(sortEnvironmentsCB, SIGNAL(clicked()), this, SIGNAL(changed())); + connect(groupEnvironmentsCB, SIGNAL(clicked()), + this, SIGNAL(changed())); connect(macroEditStyleCO, SIGNAL(activated(int)), this, SIGNAL(changed())); connect(autoSaveSB, SIGNAL(valueChanged(int)), @@ -1829,6 +1831,7 @@ void PrefUserInterface::apply(LyXRC & rc) const rc.allow_geometry_session = allowGeometrySessionCB->isChecked(); rc.cursor_follows_scrollbar = cursorFollowsCB->isChecked(); rc.sort_layouts = sortEnvironmentsCB->isChecked(); + rc.group_layouts = groupEnvironmentsCB->isChecked(); switch (macroEditStyleCO->currentIndex()) { case 0: rc.macro_edit_style = LyXRC::MACRO_EDIT_INLINE_BOX; break; case 1: rc.macro_edit_style = LyXRC::MACRO_EDIT_INLINE; break; @@ -1854,6 +1857,7 @@ void PrefUserInterface::update(LyXRC const & rc) allowGeometrySessionCB->setChecked(rc.allow_geometry_session); cursorFollowsCB->setChecked(rc.cursor_follows_scrollbar); sortEnvironmentsCB->setChecked(rc.sort_layouts); + groupEnvironmentsCB->setChecked(rc.group_layouts); macroEditStyleCO->setCurrentIndex(rc.macro_edit_style); // convert to minutes int mins(rc.autosave / 60); diff --git a/src/frontends/qt4/GuiToolbar.cpp b/src/frontends/qt4/GuiToolbar.cpp index 2da16848aa..32a71a1858 100644 --- a/src/frontends/qt4/GuiToolbar.cpp +++ b/src/frontends/qt4/GuiToolbar.cpp @@ -46,9 +46,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -61,6 +63,9 @@ #include +#include +#include + using namespace std; using namespace lyx::support; @@ -242,10 +247,10 @@ static QIcon getIcon(FuncRequest const & f, bool unknown) // ///////////////////////////////////////////////////////////////////// -class FilterItemDelegate : public QAbstractItemDelegate { +class LayoutItemDelegate : public QAbstractItemDelegate { public: /// - explicit FilterItemDelegate(QObject * parent = 0) + explicit LayoutItemDelegate(QObject * parent = 0) : QAbstractItemDelegate(parent) {} @@ -254,60 +259,38 @@ public: QModelIndex const & index) const { QComboBox * combo = static_cast(parent()); + QSortFilterProxyModel const * model + = static_cast(index.model()); QStyleOptionMenuItem opt = getStyleOption(option, index); + + painter->eraseRect(opt.rect); - // draw line with small text string for separator - if (opt.text.left(2) == "--") { - painter->save(); - - // set options for the separator, the first 8/18 of the vertical space - QStyleOptionMenuItem sopt = opt; - sopt.state = QStyle::State_Active | QStyle::State_Enabled; - sopt.checked = false; - sopt.text = QString(); - sopt.rect.setHeight(sopt.rect.height() * 8 / 18); - sopt.menuRect.setHeight(sopt.menuRect.height() * 8 / 18); - - // use the style with an empty text to paint the background - painter->eraseRect(sopt.rect); - combo->style()->drawControl(QStyle::CE_MenuItem, &sopt, painter, combo->view()); - - // draw the centered text, small and bold - QPen pen; - pen.setWidth(1); - pen.setColor(sopt.palette.text().color()); - painter->setPen(pen); - QFont font = sopt.font; - font.setBold(true); - font.setWeight(QFont::Black); - font.setPointSize(sopt.font.pointSize() * 8 / 10); - painter->setFont(font); - QRect brect; - painter->drawText(sopt.rect, Qt::AlignCenter, "Modules", &brect); + QString text = underlineFilter(opt.text); + opt.text = QString(); + + // category header? + if (lyxrc.group_layouts) { + QString stdCat = category(*model->sourceModel(), 0); + QString cat = category(*index.model(), index.row()); - // draw the horizontal line - QColor lcol = sopt.palette.text().color(); - lcol.setAlpha(127); - painter->setPen(lcol); - painter->drawLine(sopt.rect.x(), sopt.rect.y() + sopt.rect.height() / 2 , - brect.left() - 1, sopt.rect.y() + sopt.rect.height() / 2); - painter->drawLine(brect.right() + 1, sopt.rect.y() + sopt.rect.height() / 2, - sopt.rect.right(), sopt.rect.y() + sopt.rect.height() / 2); - - painter->restore(); - - // move rect down 8/20 of the original height - opt.rect.setTop(sopt.rect.y() + sopt.rect.height()); - opt.menuRect = opt.rect; + // not the standard layout and not the same as in the previous line? + if (stdCat != cat + && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { + // draw category header + paintBackground(painter, opt); + paintCategoryHeader(painter, opt, + category(*index.model(), index.row())); + + QFontMetrics fm(opt.font); + opt.rect.setTop(opt.rect.top() + headerHeight(opt)); + opt.menuRect = opt.rect; + } } // Draw using the menu item style (this is how QComboBox does it). // But for the rich text drawing below we will call it with an // empty string, and later then draw over it the real string. painter->save(); - QString text = underlineFilter(opt.text); - opt.text = QString(); - painter->eraseRect(opt.rect); combo->style()->drawControl(QStyle::CE_MenuItem, &opt, painter, combo->view()); painter->restore(); @@ -322,7 +305,7 @@ public: QTextDocument doc; doc.setDefaultFont(opt.font); doc.setHtml(text); - painter->translate(opt.rect.x() + 20, opt.rect.y()); + painter->translate(opt.rect.x() + 5, opt.rect.y()); doc.documentLayout()->draw(painter, context); painter->restore(); } @@ -331,17 +314,114 @@ public: QSize sizeHint(QStyleOptionViewItem const & option, QModelIndex const & index) const { - QComboBox * combo = static_cast(parent()); - + GuiLayoutBox * combo = static_cast(parent()); + QSortFilterProxyModel const * model + = static_cast(index.model()); QStyleOptionMenuItem opt = getStyleOption(option, index); QSize size = combo->style()->sizeFromContents( QStyle::CT_MenuItem, &opt, option.rect.size(), combo); - if (opt.text.left(2) == "--") - size.setHeight(size.height() * 18 / 10); + + /// QComboBox uses the first row height to estimate the + /// complete popup height during QComboBox::showPopup(). + /// To avoid scrolling we have to sneak in space for the headers. + /// So we tweak this value accordingly. It's not nice, but the + /// only possible way it seems. + if (lyxrc.group_layouts && index.row() == 0 && combo->inShowPopup_) { + int itemHeight = size.height(); + + // we have to show \c cats many headers: + unsigned cats = combo->visibleCategories_; + + // and we have \c n items to distribute the needed space over + unsigned n = combo->model()->rowCount(); + + // so the needed average height (rounded upwards) is: + size.setHeight((headerHeight(opt) * cats + itemHeight * n + n - 1) / n); + return size; + } + + // Add space for the category headers here? + // Not for the standard layout though. + QString stdCat = category(*model->sourceModel(), 0); + QString cat = category(*index.model(), index.row()); + if (lyxrc.group_layouts && stdCat != cat + && (index.row() == 0 || cat != category(*index.model(), index.row() - 1))) { + size.setHeight(size.height() + headerHeight(opt)); + } + return size; } private: + /// + QString category(QAbstractItemModel const & model, int row) const + { + return model.data(model.index(row, 2), Qt::DisplayRole).toString(); + } + + /// + void paintBackground(QPainter * painter, QStyleOptionMenuItem const & opt) const + { + QComboBox * combo = static_cast(parent()); + + // we only want to paint a background using the style, so + // disable every thing else + QStyleOptionMenuItem sopt = opt; + sopt.menuRect = sopt.rect; + sopt.state = QStyle::State_Active | QStyle::State_Enabled; + sopt.checked = false; + sopt.text = QString(); + + painter->save(); + combo->style()->drawControl(QStyle::CE_MenuItem, &sopt, + painter, combo->view()); + painter->restore(); + } + + /// + int headerHeight(QStyleOptionMenuItem const & opt) const + { + QFontMetrics fm(opt.font); + return fm.height() * 8 / 10; + } + /// + void paintCategoryHeader(QPainter * painter, QStyleOptionMenuItem const & opt, + QString const & category) const + { + painter->save(); + + // slightly blended color + QColor lcol = opt.palette.text().color(); + lcol.setAlpha(127); + painter->setPen(lcol); + + // set 80% scaled, bold font + QFont font = opt.font; + font.setBold(true); + font.setWeight(QFont::Black); + font.setPointSize(opt.font.pointSize() * 8 / 10); + painter->setFont(font); + + // draw the centered text + QFontMetrics fm(font); + int w = fm.width(category); + int x = opt.rect.x() + (opt.rect.width() - w) / 2; + int y = opt.rect.y() + fm.ascent(); + int left = x; + int right = x + w; + painter->drawText(x, y, category); + + // the vertical position of the line: middle of lower case chars + int ymid = y - 1 - fm.xHeight() / 2; // -1 for the baseline + + // draw the horizontal line + painter->drawLine(opt.rect.x(), ymid, left - 1, ymid); + painter->drawLine(right + 1, ymid, opt.rect.right(), ymid); + + painter->restore(); + } + + /// QString underlineFilter(QString const & s) const { @@ -381,7 +461,6 @@ private: // create the options for a menu item QStyleOptionMenuItem menuOption; menuOption.palette = QApplication::palette("QMenu"); - menuOption.checkType = QStyleOptionMenuItem::NonExclusive; menuOption.state = QStyle::State_Active | QStyle::State_Enabled; menuOption.menuRect = option.rect; menuOption.rect = option.rect; @@ -393,41 +472,32 @@ private: menuOption.menuItemType = QStyleOptionMenuItem::Normal; if (option.state & QStyle::State_Selected) menuOption.state |= QStyle::State_Selected; - menuOption.checked = combo->currentIndex() == index.row(); - + menuOption.checkType = QStyleOptionMenuItem::NotCheckable; + menuOption.checked = false; return menuOption; } }; -class GuiFilterProxyModel : public QSortFilterProxyModel -{ +class GuiLayoutFilterModel : public QSortFilterProxyModel { public: /// - GuiFilterProxyModel(QObject * parent) - : QSortFilterProxyModel(parent) {} - + GuiLayoutFilterModel(QObject * parent = 0) + : QSortFilterProxyModel(parent) + {} + /// - void setCharFilter(QString const & f) + void triggerLayoutChange() { - setFilterRegExp(charFilterRegExp(f)); - dataChanged(index(0, 0), index(rowCount() - 1, 1)); - } - -private: - /// - QString charFilterRegExp(QString const & filter) - { - QString re; - for (int i = 0; i < filter.length(); ++i) - re += ".*" + QRegExp::escape(filter[i]); - return re; + layoutAboutToBeChanged(); + layoutChanged(); } }; GuiLayoutBox::GuiLayoutBox(GuiView & owner) - : owner_(owner), filterItemDelegate_(new FilterItemDelegate(this)) + : owner_(owner), lastSel_(-1), layoutItemDelegate_(new LayoutItemDelegate(this)), + visibleCategories_(0), inShowPopup_(false) { setSizeAdjustPolicy(QComboBox::AdjustToContents); setFocusPolicy(Qt::ClickFocus); @@ -438,15 +508,13 @@ GuiLayoutBox::GuiLayoutBox(GuiView & owner) // 1st: translated layout names // 2nd: raw layout names model_ = new QStandardItemModel(0, 2, this); - filterModel_ = new GuiFilterProxyModel(this); + filterModel_ = new GuiLayoutFilterModel(this); filterModel_->setSourceModel(model_); - filterModel_->setDynamicSortFilter(true); - filterModel_->setFilterCaseSensitivity(Qt::CaseInsensitive); setModel(filterModel_); // for the filtering we have to intercept characters view()->installEventFilter(this); - view()->setItemDelegateForColumn(0, filterItemDelegate_); + view()->setItemDelegateForColumn(0, layoutItemDelegate_); QObject::connect(this, SIGNAL(activated(int)), this, SLOT(selected(int))); @@ -457,13 +525,23 @@ GuiLayoutBox::GuiLayoutBox(GuiView & owner) void GuiLayoutBox::setFilter(QString const & s) { + if (!s.isEmpty()) + owner_.message(_("Filtering layouts with \"" + fromqstr(s) + "\". " + "Press ESC to remove filter.")); + else + owner_.message(_("Enter characters to filter the layout list.")); + + bool enabled = view()->updatesEnabled(); + view()->setUpdatesEnabled(false); + // remember old selection int sel = currentIndex(); if (sel != -1) lastSel_ = filterModel_->mapToSource(filterModel_->index(sel, 0)).row(); filter_ = s; - filterModel_->setCharFilter(s); + filterModel_->setFilterRegExp(charFilterRegExp(filter_)); + countCategories(); // restore old selection if (lastSel_ != -1) { @@ -475,8 +553,56 @@ void GuiLayoutBox::setFilter(QString const & s) // Workaround to resize to content size // FIXME: There must be a better way. The QComboBox::AdjustToContents) // does not help. - if (view()->isVisible()) + if (view()->isVisible()) { + // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on + // the hack in the item delegate to make space for the headers. + // We do not call our implementation of showPopup because that + // would reset the filter again. This is only needed if the user clicks + // on the QComboBox. + BOOST_ASSERT(!inShowPopup_); + inShowPopup_ = true; QComboBox::showPopup(); + inShowPopup_ = false; + + // The item delegate hack is off again. So trigger a relayout of the popup. + filterModel_->triggerLayoutChange(); + } + + view()->setUpdatesEnabled(enabled); +} + + +void GuiLayoutBox::countCategories() +{ + int n = filterModel_->rowCount(); + visibleCategories_ = 0; + if (n == 0 || !lyxrc.group_layouts) + return; + + // skip the "Standard" category + QString prevCat = model_->index(0, 2).data().toString(); + + // count categories + for (int i = 0; i < n; ++i) { + QString cat = filterModel_->index(i, 2).data().toString(); + if (cat != prevCat) + ++visibleCategories_; + prevCat = cat; + } +} + + +QString GuiLayoutBox::charFilterRegExp(QString const & filter) +{ + QString re; + for (int i = 0; i < filter.length(); ++i) { + QChar c = filter[i]; + if (c.isLower()) + re += ".*[" + QRegExp::escape(c) + QRegExp::escape(c.toUpper()) + "]"; + else + re += ".*" + QRegExp::escape(c); + } + return re; } @@ -488,9 +614,24 @@ void GuiLayoutBox::resetFilter() void GuiLayoutBox::showPopup() { - resetFilter(); owner_.message(_("Enter characters to filter the layout list.")); + + bool enabled = view()->updatesEnabled(); + view()->setUpdatesEnabled(false); + + resetFilter(); + + // call QComboBox::showPopup. But set the inShowPopup_ flag to switch on + // the hack in the item delegate to make space for the headers. + BOOST_ASSERT(!inShowPopup_); + inShowPopup_ = true; QComboBox::showPopup(); + inShowPopup_ = false; + + // The item delegate hack is off again. So trigger a relayout of the popup. + filterModel_->triggerLayoutChange(); + + view()->setUpdatesEnabled(enabled); } @@ -565,40 +706,55 @@ void GuiLayoutBox::set(docstring const & layout) } -void GuiLayoutBox::addItemSort(docstring const & item, bool sorted) +void GuiLayoutBox::addItemSort(docstring const & item, docstring const & category, + bool sorted, bool sortedByCat) { QString qitem = toqstr(item); QString titem = toqstr(translateIfPossible(item)); + QString qcat = toqstr(translateIfPossible(category)); QList row; row.append(new QStandardItem(titem)); row.append(new QStandardItem(qitem)); + row.append(new QStandardItem(qcat)); - // the simple unsorted case + // the first entry is easy int const end = model_->rowCount(); - if (!sorted || end < 2 || qitem[0].category() != QChar::Letter_Uppercase) { + if (end == 0) { model_->appendRow(row); return; } - // find row to insert the item, after the separator if it exists - int i = 1; // skip the Standard layout - - QList sep = model_->findItems("--", Qt::MatchStartsWith); - if (!sep.isEmpty()) - i = sep.first()->index().row() + 1; - if (i < model_->rowCount()) { - // find alphabetic position - QString is = model_->item(i, 0)->text(); - while (is.compare(titem) < 0) { - // e.g. --Separator-- - if (is.at(0).category() != QChar::Letter_Uppercase) - break; + // find category + int i = 0; + if (sortedByCat) { + while (i < end && model_->item(i, 2)->text() != qcat) + ++i; + } + + // skip the Standard layout + if (i == 0) + ++i; + + // the simple unsorted case + if (!sorted) { + if (sortedByCat) { + // jump to the end of the category group + while (i < end && model_->item(i, 2)->text() == qcat) + ++i; + model_->insertRow(i, row); + } else + model_->appendRow(row); + return; + } + + // find row to insert the item, after the separator if it exists + if (i < end) { + // find alphabetic position + while (i != end + && model_->item(i, 0)->text().compare(titem) < 0 + && (!sortedByCat || model_->item(i, 2)->text() == qcat)) ++i; - if (i == end) - break; - is = model_->item(i, 0)->text(); - } } model_->insertRow(i, row); @@ -645,11 +801,12 @@ void GuiLayoutBox::updateContents(bool reset) if (name == text_class_->emptyLayoutName() && inset && !inset->forceEmptyLayout() && !inset->useEmptyLayout()) continue; - addItemSort(name, lyxrc.sort_layouts); + addItemSort(name, lit->category(), lyxrc.sort_layouts, lyxrc.group_layouts); } set(owner_.view()->cursor().innerParagraph().layout().name()); - + countCategories(); + // needed to recalculate size hint hide(); setMinimumWidth(sizeHint().width()); diff --git a/src/frontends/qt4/GuiToolbar.h b/src/frontends/qt4/GuiToolbar.h index e5b869caa0..f616410658 100644 --- a/src/frontends/qt4/GuiToolbar.h +++ b/src/frontends/qt4/GuiToolbar.h @@ -18,26 +18,27 @@ #include "Session.h" +#include +#include #include #include -#include +class QSortFilterProxyModel; class QStandardItemModel; namespace lyx { -class Inset; class DocumentClass; +class Inset; class ToolbarItem; namespace frontend { -class FilterItemDelegate; -class GuiCommandBuffer; -class GuiFilterProxyModel; -class GuiView; class Action; - +class GuiCommandBuffer; +class GuiLayoutFilterModel; +class GuiView; +class LayoutItemDelegate; class GuiLayoutBox : public QComboBox { @@ -50,7 +51,8 @@ public: /// Populate the layout combobox. void updateContents(bool reset); /// Add Item to Layout box according to sorting settings from preferences - void addItemSort(docstring const & item, bool sorted); + void addItemSort(docstring const & item, docstring const & category, + bool sorted, bool sortedByCat); /// void showPopup(); @@ -65,11 +67,17 @@ private Q_SLOTS: void selected(int index); private: + friend class LayoutItemDelegate; + /// void resetFilter(); /// void setFilter(QString const & s); - + /// + QString charFilterRegExp(QString const & filter); + /// + void countCategories(); + /// GuiView & owner_; /// @@ -80,13 +88,17 @@ private: /// the layout model: 1st column translated, 2nd column raw layout name QStandardItemModel * model_; /// the proxy model filtering \c model_ - GuiFilterProxyModel * filterModel_; + GuiLayoutFilterModel * filterModel_; /// the (model-) index of the last successful selection int lastSel_; /// the character filter QString filter_; /// - FilterItemDelegate * filterItemDelegate_; + LayoutItemDelegate * layoutItemDelegate_; + /// + unsigned visibleCategories_; + /// + bool inShowPopup_; }; diff --git a/src/frontends/qt4/ui/PrefUi.ui b/src/frontends/qt4/ui/PrefUi.ui index 4b9ba17e7e..5cd70b34e0 100644 --- a/src/frontends/qt4/ui/PrefUi.ui +++ b/src/frontends/qt4/ui/PrefUi.ui @@ -5,14 +5,12 @@ 0 0 - 430 - 582 + 459 + 596 - - 0 - 0 + 0 0 @@ -21,12 +19,6 @@ - - 9 - - - 6 - @@ -36,10 +28,10 @@ true - - 9 + + 4 - + 4 @@ -84,12 +76,6 @@ true - - 9 - - - 6 - @@ -99,20 +85,14 @@ true - - 9 - - - 6 - - - 10000 - 0 + + 10000 + 10 @@ -164,9 +144,7 @@ - - 0 - 0 + 0 0 @@ -191,10 +169,10 @@ true - - 9 + + 4 - + 4 @@ -207,11 +185,11 @@ - Sort &Environments alphabetically + Sort &environments alphabetically - + @@ -230,6 +208,13 @@ + + + + &Group environments by their category + + + @@ -242,20 +227,8 @@ true - - 9 - - - 6 - - - 0 - - - 6 - @@ -310,12 +283,12 @@ - - 300 - 1 + + 300 + @@ -353,10 +326,10 @@ true - - 9 + + 4 - + 4