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.
431 lines
13 KiB
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, ¶ms);
|
|
|
|
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);
|
|
}
|
|
|