You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libqtxdg-packaging/xdgmenulayoutprocessor.cpp

431 lines
13 KiB

/* BEGIN_COMMON_COPYRIGHT_HEADER
* (c)LGPL2+
*
* Razor - a lightweight, Qt based, desktop toolset
* http://razor-qt.org
*
* Copyright: 2010-2011 Razor team
* Authors:
* Alexander Sokoloff <sokoloff.a@gmail.com>
*
* This program or library is free software; you can redistribute it
* and/or modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301 USA
*
* END_COMMON_COPYRIGHT_HEADER */
#include "xdgmenulayoutprocessor.h"
#include "xmlhelper.h"
#include <QDebug>
#include <QMap>
// Helper functions prototypes
QDomElement findLastElementByTag(const QDomElement element, const QString tagName);
int childsCount(const QDomElement& element);
/************************************************
************************************************/
QDomElement findLastElementByTag(const QDomElement element, const QString tagName)
{
QDomNodeList l = element.elementsByTagName(tagName);
if (l.isEmpty())
return QDomElement();
return l.at(l.length()-1).toElement();
}
/************************************************
If no default-layout has been specified then the layout as specified by
the following elements should be assumed:
<DefaultLayout
show_empty="false"
inline="false"
inline_limit="4"
inline_header="true"
inline_alias="false">
<Merge type="menus"/>
<Merge type="files"/>
</DefaultLayout>
************************************************/
XdgMenuLayoutProcessor::XdgMenuLayoutProcessor(QDomElement& element):
mElement(element)
{
mDefaultParams.mShowEmpty = false;
mDefaultParams.mInline = false;
mDefaultParams.mInlineLimit = 4;
mDefaultParams.mInlineHeader = true;
mDefaultParams.mInlineAlias = false;
mDefaultLayout = findLastElementByTag(element, "DefaultLayout");
if (mDefaultLayout.isNull())
{
// Create DefaultLayout node
QDomDocument doc = element.ownerDocument();
mDefaultLayout = doc.createElement("DefaultLayout");
QDomElement menus = doc.createElement("Merge");
menus.setAttribute("type", "menus");
mDefaultLayout.appendChild(menus);
QDomElement files = doc.createElement("Merge");
files.setAttribute("type", "files");
mDefaultLayout.appendChild(files);
mElement.appendChild(mDefaultLayout);
}
setParams(mDefaultLayout, &mDefaultParams);
// If a menu does not contain a <Layout> element or if it contains an empty <Layout> element
// then the default layout should be used.
mLayout = findLastElementByTag(element, "Layout");
if (mLayout.isNull() || !mLayout.hasChildNodes())
mLayout = mDefaultLayout;
}
/************************************************
************************************************/
XdgMenuLayoutProcessor::XdgMenuLayoutProcessor(QDomElement& element, XdgMenuLayoutProcessor *parent):
mElement(element)
{
mDefaultParams = parent->mDefaultParams;
// DefaultLayout ............................
QDomElement defaultLayout = findLastElementByTag(element, "DefaultLayout");
if (defaultLayout.isNull())
mDefaultLayout = parent->mDefaultLayout;
else
mDefaultLayout = defaultLayout;
setParams(mDefaultLayout, &mDefaultParams);
// If a menu does not contain a <Layout> element or if it contains an empty <Layout> element
// then the default layout should be used.
mLayout = findLastElementByTag(element, "Layout");
if (mLayout.isNull() || !mLayout.hasChildNodes())
mLayout = mDefaultLayout;
}
/************************************************
************************************************/
void XdgMenuLayoutProcessor::setParams(QDomElement defaultLayout, LayoutParams *result)
{
if (defaultLayout.hasAttribute("show_empty"))
result->mShowEmpty = defaultLayout.attribute("show_empty") == "true";
if (defaultLayout.hasAttribute("inline"))
result->mInline = defaultLayout.attribute("inline") == "true";
if (defaultLayout.hasAttribute("inline_limit"))
result->mInlineLimit = defaultLayout.attribute("inline_limit").toInt();
if (defaultLayout.hasAttribute("inline_header"))
result->mInlineHeader = defaultLayout.attribute("inline_header") == "true";
if (defaultLayout.hasAttribute("inline_alias"))
result->mInlineAlias = defaultLayout.attribute("inline_alias") == "true";
}
/************************************************
************************************************/
QDomElement XdgMenuLayoutProcessor::searchElement(const QString &tagName, const QString &attributeName, const QString &attributeValue) const
{
DomElementIterator it(mElement, tagName);
while (it.hasNext())
{
QDomElement e = it.next();
if (e.attribute(attributeName) == attributeValue)
{
return e;
}
}
return QDomElement();
}
/************************************************
************************************************/
int childsCount(const QDomElement& element)
{
int count = 0;
DomElementIterator it(element);
while (it.hasNext())
{
QString tag = it.next().tagName();
if (tag == "AppLink" || tag == "Menu" || tag == "Separator")
count ++;
}
return count;
}
/************************************************
************************************************/
void XdgMenuLayoutProcessor::run()
{
QDomDocument doc = mLayout.ownerDocument();
mResult = doc.createElement("Result");
mElement.appendChild(mResult);
// Process childs menus ...............................
{
DomElementIterator it(mElement, "Menu");
while (it.hasNext())
{
QDomElement e = it.next();
XdgMenuLayoutProcessor p(e, this);
p.run();
}
}
// Step 1 ...................................
DomElementIterator it(mLayout);
it.toFront();
while (it.hasNext())
{
QDomElement e = it.next();
if (e.tagName() == "Filename")
processFilenameTag(e);
else if (e.tagName() == "Menuname")
processMenunameTag(e);
else if (e.tagName() == "Separator")
processSeparatorTag(e);
else if (e.tagName() == "Merge")
{
QDomElement merge = mResult.ownerDocument().createElement("Merge");
merge.setAttribute("type", e.attribute("type"));
mResult.appendChild(merge);
}
}
// Step 2 ...................................
{
MutableDomElementIterator ri(mResult, "Merge");
while (ri.hasNext())
{
processMergeTag(ri.next());
}
}
// Move result cilds to element .............
MutableDomElementIterator ri(mResult);
while (ri.hasNext())
{
mElement.appendChild(ri.next());
}
// Final ....................................
mElement.removeChild(mResult);
if (mLayout.parentNode() == mElement)
mElement.removeChild(mLayout);
if (mDefaultLayout.parentNode() == mElement)
mElement.removeChild(mDefaultLayout);
}
/************************************************
The <Filename> element is the most basic matching rule.
It matches a desktop entry if the desktop entry has the given desktop-file id
************************************************/
void XdgMenuLayoutProcessor::processFilenameTag(const QDomElement &element)
{
QString id = element.text();
QDomElement appLink = searchElement("AppLink", "id", id);
if (!appLink.isNull())
mResult.appendChild(appLink);
}
/************************************************
Its contents references an immediate sub-menu of the current menu, as such it should never contain
a slash. If no such sub-menu exists the element should be ignored.
This element may have various attributes, the default values are taken from the DefaultLayout key.
show_empty [ bool ]
defines whether a menu that contains no desktop entries and no sub-menus
should be shown at all.
inline [ bool ]
If the inline attribute is "true" the menu that is referenced may be copied into the current
menu at the current point instead of being inserted as sub-menu of the current menu.
inline_limit [ int ]
defines the maximum number of entries that can be inlined. If the sub-menu has more entries
than inline_limit, the sub-menu will not be inlined. If the inline_limit is 0 (zero) there
is no limit.
inline_header [ bool ]
defines whether an inlined menu should be preceded with a header entry listing the caption of
the sub-menu.
inline_alias [ bool ]
defines whether a single inlined entry should adopt the caption of the inlined menu. In such
case no additional header entry will be added regardless of the value of the inline_header
attribute.
Example: if a menu has a sub-menu titled "WordProcessor" with a single entry "OpenOffice 4.2", and
both inline="true" and inline_alias="true" are specified then this would result in the
"OpenOffice 4.2" entry being inlined in the current menu but the "OpenOffice 4.2" caption of the
entry would be replaced with "WordProcessor".
************************************************/
void XdgMenuLayoutProcessor::processMenunameTag(const QDomElement &element)
{
QString id = element.text();
QDomElement menu = searchElement("Menu", "name", id);
if (menu.isNull())
return;
LayoutParams params = mDefaultParams;
setParams(element, &params);
int count = childsCount(menu);
if (count == 0)
{
if (params.mShowEmpty)
{
menu.setAttribute("keep", "true");
mResult.appendChild(menu);
}
return;
}
bool doInline = params.mInline &&
(!params.mInlineLimit || params.mInlineLimit > count);
bool doAlias = params.mInlineAlias &&
doInline && (count == 1);
bool doHeader = params.mInlineHeader &&
doInline && !doAlias;
if (!doInline)
{
mResult.appendChild(menu);
return;
}
// Header ....................................
if (doHeader)
{
QDomElement header = mLayout.ownerDocument().createElement("Header");
QDomNamedNodeMap attrs = menu.attributes();
for (int i=0; i < attrs.count(); ++i)
{
header.setAttributeNode(attrs.item(i).toAttr());
}
mResult.appendChild(header);
}
// Alias .....................................
if (doAlias)
{
menu.firstChild().toElement().setAttribute("title", menu.attribute("title"));
}
// Inline ....................................
MutableDomElementIterator it(menu);
while (it.hasNext())
{
mResult.appendChild(it.next());
}
}
/************************************************
It indicates a suggestion to draw a visual separator at this point in the menu.
<Separator> elements at the start of a menu, at the end of a menu or that directly
follow other <Separator> elements may be ignored.
************************************************/
void XdgMenuLayoutProcessor::processSeparatorTag(const QDomElement &element)
{
QDomElement separator = element.ownerDocument().createElement("Separator");
mResult.appendChild(separator);
}
/************************************************
It indicates the point where desktop entries and sub-menus that are not explicitly mentioned
within the <Layout> or <DefaultLayout> element are to be inserted.
It has a type attribute that indicates which elements should be inserted:
type="menus"
means that all sub-menus that are not explicitly mentioned should be inserted in alphabetical
order of their visual caption at this point.
type="files" means that all desktop entries contained in this menu that are not explicitly
mentioned should be inserted in alphabetical order of their visual caption at this point.
type="all" means that a mix of all sub-menus and all desktop entries that are not explicitly
mentioned should be inserted in alphabetical order of their visual caption at this point.
************************************************/
void XdgMenuLayoutProcessor::processMergeTag(const QDomElement &element)
{
QString type = element.attribute("type");
QMap<QString, QDomElement> map;
MutableDomElementIterator it(mElement);
while (it.hasNext())
{
QDomElement e = it.next();
if (
((type == "menus" || type == "all") && e.tagName() == "Menu" ) ||
((type == "files" || type == "all") && e.tagName() == "AppLink")
)
map.insert(e.attribute("title"), e);
}
QMapIterator<QString, QDomElement> mi(map);
while (mi.hasNext()) {
mi.next();
mResult.insertBefore(mi.value(), element);
}
mResult.removeChild(element);
}