documentmodel.cpp 18.2 KB
Newer Older
hjk's avatar
hjk committed
1
/****************************************************************************
2
**
3 4
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
5
**
hjk's avatar
hjk committed
6
** This file is part of Qt Creator.
7
**
hjk's avatar
hjk committed
8 9 10 11
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
12 13 14
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
15
**
16 17 18 19 20 21 22
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
con's avatar
con committed
23
**
hjk's avatar
hjk committed
24
****************************************************************************/
25

26
#include "documentmodel.h"
27
#include "ieditor.h"
28
#include <coreplugin/documentmanager.h>
29
#include <coreplugin/idocument.h>
30
#include <coreplugin/coreicons.h>
31

32
#include <utils/algorithm.h>
33
#include <utils/dropsupport.h>
34
#include <utils/hostosinfo.h>
35 36
#include <utils/qtcassert.h>

37
#include <QAbstractItemModel>
38 39
#include <QDir>
#include <QIcon>
40
#include <QMimeData>
41
#include <QSet>
42
#include <QUrl>
43 44 45

namespace Core {

hjk's avatar
hjk committed
46
class DocumentModelPrivate : public QAbstractItemModel
hjk's avatar
hjk committed
47
{
hjk's avatar
hjk committed
48 49 50
    Q_OBJECT

public:
51 52
    DocumentModelPrivate();
    ~DocumentModelPrivate();
Friedemann Kleint's avatar
Friedemann Kleint committed
53

hjk's avatar
hjk committed
54 55
    int columnCount(const QModelIndex &parent = QModelIndex()) const;
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const;
56 57
    Qt::ItemFlags flags(const QModelIndex &index) const;
    QMimeData *mimeData(const QModelIndexList &indexes) const;
hjk's avatar
hjk committed
58 59 60 61
    QModelIndex parent(const QModelIndex &/*index*/) const { return QModelIndex(); }
    int rowCount(const QModelIndex &parent = QModelIndex()) const;
    QModelIndex index(int row, int column = 0, const QModelIndex &parent = QModelIndex()) const;

62 63 64
    Qt::DropActions supportedDragActions() const;
    QStringList mimeTypes() const;

hjk's avatar
hjk committed
65 66 67
    void addEntry(DocumentModel::Entry *entry);
    void removeDocument(int idx);

68
    int indexOfFilePath(const Utils::FileName &filePath) const;
hjk's avatar
hjk committed
69 70
    int indexOfDocument(IDocument *document) const;

71 72
    bool disambiguateDisplayNames(DocumentModel::Entry *entry);

Orgad Shaneh's avatar
Orgad Shaneh committed
73
private:
hjk's avatar
hjk committed
74 75 76
    friend class DocumentModel;
    void itemChanged();

77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
    class DynamicEntry
    {
    public:
        DocumentModel::Entry *entry;
        int pathComponents;

        DynamicEntry(DocumentModel::Entry *e) :
            entry(e),
            pathComponents(0)
        {
        }

        DocumentModel::Entry *operator->() const { return entry; }

        void disambiguate()
        {
            entry->document->setUniqueDisplayName(entry->fileName().fileName(++pathComponents));
        }

        void setNumberedName(int number)
        {
            entry->document->setUniqueDisplayName(QStringLiteral("%1 (%2)")
                                                  .arg(entry->document->displayName())
                                                  .arg(number));
        }
    };

Friedemann Kleint's avatar
Friedemann Kleint committed
104 105 106
    const QIcon m_lockedIcon;
    const QIcon m_unlockedIcon;

107
    QList<DocumentModel::Entry *> m_entries;
108
    QMap<IDocument *, QList<IEditor *> > m_editors;
109
    QHash<QString, DocumentModel::Entry *> m_entryByFixedPath;
Friedemann Kleint's avatar
Friedemann Kleint committed
110 111
};

112
DocumentModelPrivate::DocumentModelPrivate() :
113 114
    m_lockedIcon(Icons::LOCKED.icon()),
    m_unlockedIcon(Icons::UNLOCKED.icon())
115 116 117
{
}

118
DocumentModelPrivate::~DocumentModelPrivate()
Eike Ziller's avatar
Eike Ziller committed
119
{
120
    qDeleteAll(m_entries);
Eike Ziller's avatar
Eike Ziller committed
121 122
}

hjk's avatar
hjk committed
123 124
static DocumentModelPrivate *d;

125
DocumentModel::Entry::Entry() :
126
    document(0),
127
    isSuspended(false)
128 129 130 131
{
}

DocumentModel::Entry::~Entry()
Friedemann Kleint's avatar
Friedemann Kleint committed
132
{
133
    if (isSuspended)
134
        delete document;
Friedemann Kleint's avatar
Friedemann Kleint committed
135 136
}

hjk's avatar
hjk committed
137
DocumentModel::DocumentModel()
Friedemann Kleint's avatar
Friedemann Kleint committed
138 139 140
{
}

hjk's avatar
hjk committed
141 142 143 144 145 146
void DocumentModel::init()
{
    d = new DocumentModelPrivate;
}

void DocumentModel::destroy()
Friedemann Kleint's avatar
Friedemann Kleint committed
147
{
hjk's avatar
hjk committed
148
    delete d;
Friedemann Kleint's avatar
Friedemann Kleint committed
149 150
}

hjk's avatar
hjk committed
151
QIcon DocumentModel::lockedIcon()
152
{
Friedemann Kleint's avatar
Friedemann Kleint committed
153
    return d->m_lockedIcon;
154 155
}

hjk's avatar
hjk committed
156
QIcon DocumentModel::unlockedIcon()
157
{
Friedemann Kleint's avatar
Friedemann Kleint committed
158
    return d->m_unlockedIcon;
159 160
}

hjk's avatar
hjk committed
161 162 163 164 165
QAbstractItemModel *DocumentModel::model()
{
    return d;
}

166
Utils::FileName DocumentModel::Entry::fileName() const
hjk's avatar
hjk committed
167
{
168
    return document->filePath();
169
}
170

hjk's avatar
hjk committed
171 172
QString DocumentModel::Entry::displayName() const
{
173
    return document->displayName();
174
}
175

176 177
QString DocumentModel::Entry::plainDisplayName() const
{
178
    return document->plainDisplayName();
179 180
}

181
Id DocumentModel::Entry::id() const
182
{
183
    return document->id();
184 185
}

hjk's avatar
hjk committed
186
int DocumentModelPrivate::columnCount(const QModelIndex &parent) const
187
{
188 189 190
    if (!parent.isValid())
        return 2;
    return 0;
191 192
}

hjk's avatar
hjk committed
193
int DocumentModelPrivate::rowCount(const QModelIndex &parent) const
194 195
{
    if (!parent.isValid())
196
        return m_entries.count() + 1/*<no document>*/;
197 198 199
    return 0;
}

200
void DocumentModel::addEditor(IEditor *editor, bool *isNewDocument)
201
{
202 203 204
    if (!editor)
        return;

205
    QList<IEditor *> &editorList = d->m_editors[editor->document()];
Eike Ziller's avatar
Eike Ziller committed
206
    bool isNew = editorList.isEmpty();
207
    if (isNewDocument)
Eike Ziller's avatar
Eike Ziller committed
208
        *isNewDocument = isNew;
209
    editorList << editor;
Eike Ziller's avatar
Eike Ziller committed
210 211 212
    if (isNew) {
        Entry *entry = new Entry;
        entry->document = editor->document();
hjk's avatar
hjk committed
213
        d->addEntry(entry);
Eike Ziller's avatar
Eike Ziller committed
214
    }
215 216
}

217
void DocumentModel::addSuspendedDocument(const QString &fileName, const QString &displayName, Id id)
218
{
219
    Entry *entry = new Entry;
Orgad Shaneh's avatar
Orgad Shaneh committed
220
    entry->document = new IDocument;
221 222 223
    entry->document->setFilePath(Utils::FileName::fromString(fileName));
    entry->document->setPreferredDisplayName(displayName);
    entry->document->setId(id);
224
    entry->isSuspended = true;
hjk's avatar
hjk committed
225
    d->addEntry(entry);
226 227
}

228
DocumentModel::Entry *DocumentModel::firstSuspendedEntry()
229
{
230
    return Utils::findOrDefault(d->m_entries, [](Entry *entry) { return entry->isSuspended; });
231 232
}

hjk's avatar
hjk committed
233
void DocumentModelPrivate::addEntry(DocumentModel::Entry *entry)
234
{
235
    const Utils::FileName fileName = entry->fileName();
236 237 238
    QString fixedPath;
    if (!fileName.isEmpty())
        fixedPath = DocumentManager::fixFileName(fileName.toString(), DocumentManager::ResolveLinks);
239

240
    // replace a non-loaded entry (aka 'suspended') if possible
241
    int previousIndex = indexOfFilePath(fileName);
242
    if (previousIndex >= 0) {
243
        DocumentModel::Entry *previousEntry = m_entries.at(previousIndex);
244
        const bool replace = !entry->isSuspended && previousEntry->isSuspended;
245
        if (replace) {
Eike Ziller's avatar
Eike Ziller committed
246
            delete previousEntry;
247
            m_entries[previousIndex] = entry;
248 249
            if (!fixedPath.isEmpty())
                m_entryByFixedPath[fixedPath] = entry;
Eike Ziller's avatar
Eike Ziller committed
250 251
        } else {
            delete entry;
252
            entry = previousEntry;
253
        }
254 255 256
        previousEntry = 0;
        disambiguateDisplayNames(entry);
        if (replace)
Orgad Shaneh's avatar
Orgad Shaneh committed
257
            connect(entry->document, &IDocument::changed, this, &DocumentModelPrivate::itemChanged);
258 259 260 261
        return;
    }

    int index;
262
    const QString displayName = entry->plainDisplayName();
263
    for (index = 0; index < m_entries.count(); ++index) {
264 265 266 267
        int cmp = displayName.localeAwareCompare(m_entries.at(index)->plainDisplayName());
        if (cmp < 0)
            break;
        if (cmp == 0 && fileName < d->m_entries.at(index)->fileName())
268 269
            break;
    }
270 271
    int row = index + 1/*<no document>*/;
    beginInsertRows(QModelIndex(), row, row);
272
    m_entries.insert(index, entry);
273
    disambiguateDisplayNames(entry);
274 275
    if (!fixedPath.isEmpty())
        m_entryByFixedPath[fixedPath] = entry;
Orgad Shaneh's avatar
Orgad Shaneh committed
276
    connect(entry->document, &IDocument::changed, this, &DocumentModelPrivate::itemChanged);
277 278 279
    endInsertRows();
}

280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
bool DocumentModelPrivate::disambiguateDisplayNames(DocumentModel::Entry *entry)
{
    const QString displayName = entry->plainDisplayName();
    int minIdx = -1, maxIdx = -1;

    QList<DynamicEntry> dups;

    for (int i = 0, total = m_entries.count(); i < total; ++i) {
        DocumentModel::Entry *e = m_entries.at(i);
        if (e == entry || e->plainDisplayName() == displayName) {
            e->document->setUniqueDisplayName(QString());
            dups += DynamicEntry(e);
            maxIdx = i;
            if (minIdx < 0)
                minIdx = i;
        }
    }

    const int dupsCount = dups.count();
    if (dupsCount == 0)
        return false;

    if (dupsCount > 1) {
        int serial = 0;
        int count = 0;
        // increase uniqueness unless no dups are left
        forever {
            bool seenDups = false;
            for (int i = 0; i < dupsCount - 1; ++i) {
                DynamicEntry &e = dups[i];
                const Utils::FileName myFileName = e->document->filePath();
                if (e->document->isTemporary() || myFileName.isEmpty() || count > 10) {
                    // path-less entry, append number
                    e.setNumberedName(++serial);
                    continue;
                }
                for (int j = i + 1; j < dupsCount; ++j) {
                    DynamicEntry &e2 = dups[j];
318
                    if (e->displayName().compare(e2->displayName(), Utils::HostOsInfo::fileNameCaseSensitivity()) == 0) {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
                        const Utils::FileName otherFileName = e2->document->filePath();
                        if (otherFileName.isEmpty())
                            continue;
                        seenDups = true;
                        e2.disambiguate();
                        if (j > maxIdx)
                            maxIdx = j;
                    }
                }
                if (seenDups) {
                    e.disambiguate();
                    ++count;
                    break;
                }
            }
            if (!seenDups)
                break;
        }
    }

    emit dataChanged(index(minIdx + 1, 0), index(maxIdx + 1, 0));
    return true;
}

343
int DocumentModelPrivate::indexOfFilePath(const Utils::FileName &filePath) const
344
{
345
    if (filePath.isEmpty())
346
        return -1;
347 348
    const QString fixedPath = DocumentManager::fixFileName(filePath.toString(),
                                                           DocumentManager::ResolveLinks);
349
    return m_entries.indexOf(m_entryByFixedPath.value(fixedPath));
350 351
}

352
void DocumentModel::removeEntry(DocumentModel::Entry *entry)
353
{
354 355
    // For non suspended entries, we wouldn't know what to do with the associated editors
    QTC_ASSERT(entry->isSuspended, return);
356
    int index = d->m_entries.indexOf(entry);
hjk's avatar
hjk committed
357
    d->removeDocument(index);
358 359
}

360
void DocumentModel::removeEditor(IEditor *editor, bool *lastOneForDocument)
361
{
362 363 364 365 366 367 368 369 370 371
    if (lastOneForDocument)
        *lastOneForDocument = false;
    QTC_ASSERT(editor, return);
    IDocument *document = editor->document();
    QTC_ASSERT(d->m_editors.contains(document), return);
    d->m_editors[document].removeAll(editor);
    if (d->m_editors.value(document).isEmpty()) {
        if (lastOneForDocument)
            *lastOneForDocument = true;
        d->m_editors.remove(document);
hjk's avatar
hjk committed
372
        d->removeDocument(indexOfDocument(document));
373
    }
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
374 375
}

376
void DocumentModel::removeDocument(const QString &fileName)
377
{
378
    int index = d->indexOfFilePath(Utils::FileName::fromString(fileName));
379 380
    // For non suspended entries, we wouldn't know what to do with the associated editors
    QTC_ASSERT(d->m_entries.at(index)->isSuspended, return);
hjk's avatar
hjk committed
381
    d->removeDocument(index);
382 383
}

hjk's avatar
hjk committed
384
void DocumentModelPrivate::removeDocument(int idx)
Oswald Buddenhagen's avatar
Oswald Buddenhagen committed
385
{
386
    if (idx < 0)
387
        return;
388
    QTC_ASSERT(idx < d->m_entries.size(), return);
389
    int row = idx + 1/*<no document>*/;
390
    beginRemoveRows(QModelIndex(), row, row);
391
    DocumentModel::Entry *entry = d->m_entries.takeAt(idx);
392
    endRemoveRows();
393 394 395 396 397 398 399

    const QString fileName = entry->fileName().toString();
    if (!fileName.isEmpty()) {
        const QString fixedPath = DocumentManager::fixFileName(fileName,
                                                               DocumentManager::ResolveLinks);
        m_entryByFixedPath.remove(fixedPath);
    }
Orgad Shaneh's avatar
Orgad Shaneh committed
400
    disconnect(entry->document, &IDocument::changed, this, &DocumentModelPrivate::itemChanged);
401
    disambiguateDisplayNames(entry);
402
    delete entry;
403 404
}

405
void DocumentModel::removeAllSuspendedEntries()
406
{
407
    for (int i = d->m_entries.count()-1; i >= 0; --i) {
408
        if (d->m_entries.at(i)->isSuspended) {
409
            int row = i + 1/*<no document>*/;
hjk's avatar
hjk committed
410
            d->beginRemoveRows(QModelIndex(), row, row);
411
            delete d->m_entries.takeAt(i);
hjk's avatar
hjk committed
412
            d->endRemoveRows();
413 414
        }
    }
415 416 417 418 419 420 421 422
    QSet<QString> displayNames;
    foreach (DocumentModel::Entry *entry, d->m_entries) {
        const QString displayName = entry->plainDisplayName();
        if (displayNames.contains(displayName))
            continue;
        displayNames.insert(displayName);
        d->disambiguateDisplayNames(entry);
    }
423 424
}

hjk's avatar
hjk committed
425
QList<IEditor *> DocumentModel::editorsForDocument(IDocument *document)
426
{
427
    return d->m_editors.value(document);
428 429
}

hjk's avatar
hjk committed
430 431 432 433 434 435
QList<IEditor *> DocumentModel::editorsForOpenedDocuments()
{
    return editorsForDocuments(openedDocuments());
}

QList<IEditor *> DocumentModel::editorsForDocuments(const QList<IDocument *> &documents)
436 437
{
    QList<IEditor *> result;
438 439
    foreach (IDocument *document, documents)
        result += d->m_editors.value(document);
440 441 442
    return result;
}

hjk's avatar
hjk committed
443
int DocumentModel::indexOfDocument(IDocument *document)
444
{
hjk's avatar
hjk committed
445 446 447 448 449
    return d->indexOfDocument(document);
}

int DocumentModelPrivate::indexOfDocument(IDocument *document) const
{
Eike Ziller's avatar
Eike Ziller committed
450 451 452
    return Utils::indexOf(m_entries, [&document](DocumentModel::Entry *entry) {
        return entry->document == document;
    });
453 454
}

hjk's avatar
hjk committed
455
DocumentModel::Entry *DocumentModel::entryForDocument(IDocument *document)
456
{
457 458
    return Utils::findOrDefault(d->m_entries,
                                [&document](Entry *entry) { return entry->document == document; });
459 460
}

hjk's avatar
hjk committed
461
QList<IDocument *> DocumentModel::openedDocuments()
462 463 464 465
{
    return d->m_editors.keys();
}

hjk's avatar
hjk committed
466
IDocument *DocumentModel::documentForFilePath(const QString &filePath)
467
{
468
    const int index = d->indexOfFilePath(Utils::FileName::fromString(filePath));
469 470 471
    if (index < 0)
        return 0;
    return d->m_entries.at(index)->document;
472 473
}

hjk's avatar
hjk committed
474
QList<IEditor *> DocumentModel::editorsForFilePath(const QString &filePath)
475 476 477 478 479 480 481
{
    IDocument *document = documentForFilePath(filePath);
    if (document)
        return editorsForDocument(document);
    return QList<IEditor *>();
}

hjk's avatar
hjk committed
482
QModelIndex DocumentModelPrivate::index(int row, int column, const QModelIndex &parent) const
483
{
484
    Q_UNUSED(parent)
485
    if (column < 0 || column > 1 || row < 0 || row >= m_entries.count() + 1/*<no document>*/)
486 487
        return QModelIndex();
    return createIndex(row, column);
488 489
}

490 491 492 493 494 495 496
Qt::DropActions DocumentModelPrivate::supportedDragActions() const
{
    return Qt::MoveAction;
}

QStringList DocumentModelPrivate::mimeTypes() const
{
497
    return Utils::DropSupport::mimeTypesForFilePaths();
498 499
}

500
DocumentModel::Entry *DocumentModel::entryAtRow(int row)
501
{
502 503
    int entryIndex = row - 1/*<no document>*/;
    if (entryIndex < 0)
504
        return 0;
505
    return d->m_entries[entryIndex];
506 507
}

508
int DocumentModel::entryCount()
509
{
510
    return d->m_entries.count();
511 512
}

hjk's avatar
hjk committed
513
QVariant DocumentModelPrivate::data(const QModelIndex &index, int role) const
514 515 516
{
    if (!index.isValid() || (index.column() != 0 && role < Qt::UserRole))
        return QVariant();
517 518
    int entryIndex = index.row() - 1/*<no document>*/;
    if (entryIndex < 0) {
519 520 521 522 523 524 525 526 527 528
        // <no document> entry
        switch (role) {
        case Qt::DisplayRole:
            return tr("<no document>");
        case Qt::ToolTipRole:
            return tr("No document is selected.");
        default:
            return QVariant();
        }
    }
529
    const DocumentModel::Entry *e = m_entries.at(entryIndex);
530
    switch (role) {
531 532
    case Qt::DisplayRole: {
        QString name = e->displayName();
533
        if (e->document->isModified())
534 535 536
            name += QLatin1Char('*');
        return name;
    }
537
    case Qt::DecorationRole:
538
        return e->document->isFileReadOnly() ? m_lockedIcon : QIcon();
539
    case Qt::ToolTipRole:
540
        return e->fileName().isEmpty() ? e->displayName() : e->fileName().toUserOutput();
541 542 543 544 545 546
    default:
        return QVariant();
    }
    return QVariant();
}

547 548 549 550 551 552 553 554 555 556
Qt::ItemFlags DocumentModelPrivate::flags(const QModelIndex &index) const
{
    const DocumentModel::Entry *e = DocumentModel::entryAtRow(index.row());
    if (!e || e->fileName().isEmpty())
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    return Qt::ItemIsDragEnabled | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

QMimeData *DocumentModelPrivate::mimeData(const QModelIndexList &indexes) const
{
557
    auto data = new Utils::DropMimeData;
558 559 560 561
    foreach (const QModelIndex &index, indexes) {
        const DocumentModel::Entry *e = DocumentModel::entryAtRow(index.row());
        if (!e || e->fileName().isEmpty())
            continue;
562
        data->addFile(e->fileName().toString());
563
    }
564
    return data;
565 566
}

hjk's avatar
hjk committed
567
int DocumentModel::rowOfDocument(IDocument *document)
568
{
569
    if (!document)
570
        return 0 /*<no document>*/;
Eike Ziller's avatar
Eike Ziller committed
571
    return indexOfDocument(document) + 1/*<no document>*/;
572 573
}

hjk's avatar
hjk committed
574
void DocumentModelPrivate::itemChanged()
575
{
576 577
    IDocument *document = qobject_cast<IDocument *>(sender());

Eike Ziller's avatar
Eike Ziller committed
578
    int idx = indexOfDocument(document);
579 580
    if (idx < 0)
        return;
581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
    const QString fileName = document->filePath().toString();
    QString fixedPath;
    if (!fileName.isEmpty())
        fixedPath = DocumentManager::fixFileName(fileName, DocumentManager::ResolveLinks);
    DocumentModel::Entry *entry = d->m_entries.at(idx);
    bool found = false;
    // The entry's fileName might have changed, so find the previous fileName that was associated
    // with it and remove it, then add the new fileName.
    for (auto it = m_entryByFixedPath.begin(), end = m_entryByFixedPath.end(); it != end; ++it) {
        if (it.value() == entry) {
            found = true;
            if (it.key() != fixedPath) {
                m_entryByFixedPath.remove(it.key());
                if (!fixedPath.isEmpty())
                    m_entryByFixedPath[fixedPath] = entry;
            }
            break;
        }
    }
    if (!found && !fixedPath.isEmpty())
        m_entryByFixedPath[fixedPath] = entry;
602 603 604 605
    if (!disambiguateDisplayNames(d->m_entries.at(idx))) {
        QModelIndex mindex = index(idx + 1/*<no document>*/, 0);
        emit dataChanged(mindex, mindex);
    }
606 607
}

608
QList<DocumentModel::Entry *> DocumentModel::entries()
Friedemann Kleint's avatar
Friedemann Kleint committed
609
{
610
    return d->m_entries;
Friedemann Kleint's avatar
Friedemann Kleint committed
611 612
}

613
} // namespace Core
hjk's avatar
hjk committed
614 615

#include "documentmodel.moc"