KisMainWindow.cpp 79.9 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
/* This file is part of the KDE project
   Copyright (C) 1998, 1999 Torben Weis <weis@kde.org>
   Copyright (C) 2000-2006 David Faure <faure@kde.org>
   Copyright (C) 2007, 2009 Thomas zander <zander@kde.org>
   Copyright (C) 2010 Benjamin Port <port.benjamin@gmail.com>

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 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
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public License
   along with this library; see the file COPYING.LIB.  If not, write to
   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
*/

#include "KisMainWindow.h"

Halla Rempt's avatar
Halla Rempt committed
25 26
#include <KoConfig.h>

27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
// qt includes
#include <QApplication>
#include <QByteArray>
#include <QCloseEvent>
#include <QDesktopServices>
#include <QDesktopWidget>
#include <QDockWidget>
#include <QIcon>
#include <QLabel>
#include <QLayout>
#include <QMdiArea>
#include <QMdiSubWindow>
#include <QMutex>
#include <QMutexLocker>
#include <QPointer>
#include <QPrintDialog>
#include <QPrinter>
#include <QPrintPreviewDialog>
#include <QProgressBar>
46
#include <QToolButton>
47 48
#include <QSignalMapper>
#include <QTabBar>
49
#include <QMoveEvent>
Halla Rempt's avatar
Halla Rempt committed
50 51 52 53
#include <QUrl>
#include <QMessageBox>
#include <QTemporaryFile>
#include <QStatusBar>
Halla Rempt's avatar
Halla Rempt committed
54 55
#include <QMenu>
#include <QMenuBar>
56
#include <KisMimeDatabase.h>
57
#include <QMimeData>
58 59

#include <kactioncollection.h>
60
#include <QAction>
61
#include <kactionmenu.h>
Halla Rempt's avatar
Halla Rempt committed
62
#include <kis_debug.h>
63 64
#include <kedittoolbar.h>
#include <khelpmenu.h>
65
#include <klocalizedstring.h>
66
#include <kaboutdata.h>
Halla Rempt's avatar
Halla Rempt committed
67

Halla Rempt's avatar
Halla Rempt committed
68
#ifdef HAVE_KIO
69
#include <krecentdocument.h>
Halla Rempt's avatar
Halla Rempt committed
70
#endif
71
#include <krecentfilesaction.h>
72
#include <KoResourcePaths.h>
73 74
#include <ktoggleaction.h>
#include <ktoolbar.h>
75 76
#include <kmainwindow.h>
#include <kxmlguiwindow.h>
77
#include <kxmlguifactory.h>
78
#include <kxmlguiclient.h>
Sven Langkamp's avatar
Sven Langkamp committed
79
#include <kguiitem.h>
Halla Rempt's avatar
Halla Rempt committed
80
#include <kwindowconfig.h>
81 82 83 84 85 86

#include "KoDockFactoryBase.h"
#include "KoDockWidgetTitleBar.h"
#include "KoDocumentInfoDlg.h"
#include "KoDocumentInfo.h"
#include "KoFileDialog.h"
87
#include <kis_icon.h>
88 89 90 91
#include <KoPageLayoutDialog.h>
#include <KoPageLayoutWidget.h>
#include <KoToolManager.h>
#include <KoZoomController.h>
92
#include "KoToolDocker.h"
93 94 95
#include <KoToolBoxFactory.h>
#include <KoDockRegistry.h>
#include <KoPluginLoader.h>
96
#include <KoColorSpaceEngine.h>
97

98
#include <KisMimeDatabase.h>
99 100 101 102
#include <brushengine/kis_paintop_settings.h>
#include "dialogs/kis_about_application.h"
#include "dialogs/kis_delayed_save_dialog.h"
#include "dialogs/kis_dlg_preferences.h"
103
#include "kis_action.h"
104 105
#include "kis_action_manager.h"
#include "KisApplication.h"
106
#include "kis_canvas2.h"
107
#include "kis_canvas_controller.h"
108
#include "kis_canvas_resource_provider.h"
109
#include "kis_clipboard.h"
110 111
#include "kis_config.h"
#include "kis_config_notifier.h"
112 113 114 115 116 117 118 119 120 121 122
#include "kis_config_notifier.h"
#include "kis_custom_image_widget.h"
#include <KisDocument.h>
#include "KisDocument.h"
#include "KisDocument.h"
#include "kis_group_layer.h"
#include "kis_icon_utils.h"
#include "kis_image_from_clipboard_widget.h"
#include "kis_image.h"
#include <KisImportExportFilter.h>
#include "KisImportExportManager.h"
123
#include "kis_mainwindow_observer.h"
124 125 126 127 128
#include "kis_node.h"
#include "KisOpenPane.h"
#include "kis_paintop_box.h"
#include "KisPart.h"
#include "KisPrintJob.h"
129
#include "kis_resource_server_provider.h"
130 131 132 133
#include "kis_signal_compressor_with_param.h"
#include "KisView.h"
#include "KisViewManager.h"
#include "thememanager.h"
Jouni Pentikäinen's avatar
Jouni Pentikäinen committed
134
#include "kis_animation_importer.h"
135
#include "dialogs/kis_dlg_import_image_sequence.h"
136
#include "kis_animation_exporter.h"
137

138 139
#include <mutex>

140 141 142
#ifdef Q_OS_WIN
  #include <QtPlatformHeaders/QWindowsWindowFunctions>
#endif
143

144 145 146 147 148
class ToolDockerFactory : public KoDockFactoryBase
{
public:
    ToolDockerFactory() : KoDockFactoryBase() { }

149
    QString id() const override {
150 151 152
        return "sharedtooldocker";
    }

153
    QDockWidget* createDockWidget() override {
154 155 156 157 158
        KoToolDocker* dockWidget = new KoToolDocker();
        dockWidget->setTabEnabled(false);
        return dockWidget;
    }

159
    DockPosition defaultDockPosition() const override {
160 161 162 163
        return DockRight;
    }
};

164
class Q_DECL_HIDDEN KisMainWindow::Private
165 166
{
public:
Halla Rempt's avatar
Halla Rempt committed
167
    Private(KisMainWindow *parent)
168
        : q(parent)
169 170 171
        , dockWidgetMenu(new KActionMenu(i18nc("@action:inmenu", "&Dockers"), parent))
        , windowMenu(new KActionMenu(i18nc("@action:inmenu", "&Window"), parent))
        , documentMenu(new KActionMenu(i18nc("@action:inmenu", "New &View"), parent))
Halla Rempt's avatar
Halla Rempt committed
172
        , mdiArea(new QMdiArea(parent))
173 174
        , windowMapper(new QSignalMapper(parent))
        , documentMapper(new QSignalMapper(parent))
175 176 177
    {
    }

Halla Rempt's avatar
Halla Rempt committed
178
    ~Private() {
179 180 181
        qDeleteAll(toolbarList);
    }

Halla Rempt's avatar
Halla Rempt committed
182 183
    KisMainWindow *q {0};
    KisViewManager *viewManager {0};
184

185 186 187
    QPointer<KisView> activeView;

    QPointer<QProgressBar> progress;
188
    QPointer<QToolButton> progressCancel;
189 190 191 192
    QMutex progressMutex;

    QList<QAction *> toolbarList;

Halla Rempt's avatar
Halla Rempt committed
193 194 195 196 197 198
    bool firstTime {true};
    bool windowSizeDirty {false};
    bool readOnly {false};
    bool isImporting {false};
    bool isExporting {false};
    bool noCleanup {false};
199

Halla Rempt's avatar
Halla Rempt committed
200 201 202
    KisAction *showDocumentInfo {0};
    KisAction *saveAction {0};
    KisAction *saveActionAs {0};
203 204
//    KisAction *printAction;
//    KisAction *printActionPreview;
Halla Rempt's avatar
Halla Rempt committed
205 206 207
    KisAction *exportPdf {0};
    KisAction *importAnimation {0};
    KisAction *closeAll {0};
Halla Rempt's avatar
Halla Rempt committed
208
//    KisAction *reloadFile;
Halla Rempt's avatar
Halla Rempt committed
209 210 211 212 213 214 215 216 217 218 219 220
    KisAction *importFile {0};
    KisAction *exportFile {0};
    KisAction *undo {0};
    KisAction *redo {0};
    KisAction *newWindow {0};
    KisAction *close {0};
    KisAction *mdiCascade {0};
    KisAction *mdiTile {0};
    KisAction *mdiNextWindow {0};
    KisAction *mdiPreviousWindow {0};
    KisAction *toggleDockers {0};
    KisAction *toggleDockerTitleBars {0};
221

222 223
    KisAction *expandingSpacers[2];

Halla Rempt's avatar
Halla Rempt committed
224 225 226 227
    KActionMenu *dockWidgetMenu;
    KActionMenu *windowMenu;
    KActionMenu *documentMenu;

Halla Rempt's avatar
Halla Rempt committed
228
    KHelpMenu *helpMenu  {0};
Halla Rempt's avatar
Halla Rempt committed
229

Halla Rempt's avatar
Halla Rempt committed
230
    KRecentFilesAction *recentFiles {0};
231

Halla Rempt's avatar
Halla Rempt committed
232
    QUrl lastExportUrl;
233 234 235

    QMap<QString, QDockWidget *> dockWidgetsMap;
    QMap<QDockWidget *, bool> dockWidgetVisibilityMap;
Halla Rempt's avatar
Halla Rempt committed
236
    QByteArray dockerStateBeforeHiding;
Halla Rempt's avatar
Halla Rempt committed
237
    KoToolDocker *toolOptionsDocker {0};
238

Halla Rempt's avatar
Halla Rempt committed
239
    QCloseEvent *deferredClosingEvent {0};
240

Halla Rempt's avatar
Halla Rempt committed
241
    Digikam::ThemeManager *themeManager {0};
242

Halla Rempt's avatar
Halla Rempt committed
243
    QMdiArea *mdiArea;
Halla Rempt's avatar
Halla Rempt committed
244
    QMdiSubWindow *activeSubWindow  {0};
Halla Rempt's avatar
Halla Rempt committed
245 246
    QSignalMapper *windowMapper;
    QSignalMapper *documentMapper;
247

248
    QByteArray lastExportedFormat;
249
    QScopedPointer<KisSignalCompressorWithParam<int> > tabSwitchCompressor;
250
    QMutex savingEntryMutex;
251 252 253 254

    KisActionManager * actionManager() {
        return viewManager->actionManager();
    }
255 256 257 258 259 260 261 262 263 264 265

    QTabBar* findTabBarHACK() {
        QObjectList objects = mdiArea->children();
        Q_FOREACH (QObject *object, objects) {
            QTabBar *bar = qobject_cast<QTabBar*>(object);
            if (bar) {
                return bar;
            }
        }
        return 0;
    }
266 267
};

268
KisMainWindow::KisMainWindow()
269
    : KXmlGuiWindow()
Halla Rempt's avatar
Halla Rempt committed
270
    , d(new Private(this))
271
{
272 273
    KisConfig cfg;

274
    d->viewManager = new KisViewManager(this, actionCollection());
275 276
    KConfigGroup group( KSharedConfig::openConfig(), "theme");
    d->themeManager = new Digikam::ThemeManager(group.readEntry("Theme", "Krita dark"), this);
277

278
    setAcceptDrops(true);
279 280 281 282 283
    setStandardToolBarMenuEnabled(true);
    setTabPosition(Qt::AllDockWidgetAreas, QTabWidget::North);
    setDockNestingEnabled(true);

    qApp->setStartDragDistance(25);     // 25 px is a distance that works well for Tablet and Mouse events
284

Halla Rempt's avatar
Halla Rempt committed
285
#ifdef Q_OS_OSX
286 287 288 289
    setUnifiedTitleAndToolBarOnMac(true);
#endif

    connect(this, SIGNAL(restoringDone()), this, SLOT(forceDockTabFonts()));
290 291 292 293
    connect(this, SIGNAL(themeChanged()), d->viewManager, SLOT(updateIcons()));
    connect(KisPart::instance(), SIGNAL(documentClosed(QString)), SLOT(updateWindowMenu()));
    connect(KisPart::instance(), SIGNAL(documentOpened(QString)), SLOT(updateWindowMenu()));
    connect(KisConfigNotifier::instance(), SIGNAL(configChanged()), this, SLOT(configChanged()));
294 295 296

    actionCollection()->addAssociatedWidget(this);

297
    KoToolBoxFactory toolBoxFactory;
298 299
    QDockWidget *toolbox = createDockWidget(&toolBoxFactory);
    toolbox->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable);
300

301 302 303
    if (cfg.toolOptionsInDocker()) {
        ToolDockerFactory toolDockerFactory;
        d->toolOptionsDocker = qobject_cast<KoToolDocker*>(createDockWidget(&toolDockerFactory));
304
        d->toolOptionsDocker->toggleViewAction()->setEnabled(true);
305 306
    }

Halla Rempt's avatar
Halla Rempt committed
307
    QMap<QString, QAction*> dockwidgetActions;
308
    dockwidgetActions[toolbox->toggleViewAction()->text()] = toolbox->toggleViewAction();
309
    Q_FOREACH (const QString & docker, KoDockRegistry::instance()->keys()) {
310
        KoDockFactoryBase *factory = KoDockRegistry::instance()->value(docker);
Halla Rempt's avatar
Halla Rempt committed
311 312
        QDockWidget *dw = createDockWidget(factory);
        dockwidgetActions[dw->toggleViewAction()->text()] = dw->toggleViewAction();
313
    }
314 315 316 317

    if (d->toolOptionsDocker) {
        dockwidgetActions[d->toolOptionsDocker->toggleViewAction()->text()] = d->toolOptionsDocker->toggleViewAction();
    }
318
    connect(KoToolManager::instance(), SIGNAL(toolOptionWidgetsChanged(QList<QPointer<QWidget> >)), this, SLOT(newOptionWidgets(QList<QPointer<QWidget> >)));
319

320
    Q_FOREACH (QString title, dockwidgetActions.keys()) {
Halla Rempt's avatar
Halla Rempt committed
321 322 323
        d->dockWidgetMenu->addAction(dockwidgetActions[title]);
    }

324

325
    Q_FOREACH (QDockWidget *wdg, dockWidgets()) {
326 327
        if ((wdg->features() & QDockWidget::DockWidgetClosable) == 0) {
            wdg->setVisible(true);
328 329 330
        }
    }

331
    Q_FOREACH (KoCanvasObserverBase* observer, canvasObservers()) {
332 333 334 335 336 337
        observer->setObservedCanvas(0);
        KisMainwindowObserver* mainwindowObserver = dynamic_cast<KisMainwindowObserver*>(observer);
        if (mainwindowObserver) {
            mainwindowObserver->setMainWindow(d->viewManager);
        }
    }
338

Halla Rempt's avatar
Halla Rempt committed
339 340 341 342
    d->mdiArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    d->mdiArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    d->mdiArea->setTabPosition(QTabWidget::North);
    d->mdiArea->setTabsClosable(true);
343

Halla Rempt's avatar
Halla Rempt committed
344
    setCentralWidget(d->mdiArea);
345

Halla Rempt's avatar
Halla Rempt committed
346 347 348
    connect(d->mdiArea, SIGNAL(subWindowActivated(QMdiSubWindow*)), this, SLOT(subWindowActivated()));
    connect(d->windowMapper, SIGNAL(mapped(QWidget*)), this, SLOT(setActiveSubWindow(QWidget*)));
    connect(d->documentMapper, SIGNAL(mapped(QObject*)), this, SLOT(newView(QObject*)));
349

350
    createActions();
351

352
    setAutoSaveSettings("krita", false);
353

Michael Abrahams's avatar
Michael Abrahams committed
354 355 356
    KoPluginLoader::instance()->load("Krita/ViewPlugin",
                                     "Type == 'Service' and ([X-Krita-Version] == 28)",
                                     KoPluginLoader::PluginsConfig(),
357 358
                                     viewManager(),
                                     false);
359

360
    subWindowActivated();
361 362 363
    updateWindowMenu();


Halla Rempt's avatar
Halla Rempt committed
364
    if (isHelpMenuEnabled() && !d->helpMenu) {
365 366 367 368 369 370 371 372 373 374
        // workaround for KHelpMenu (or rather KAboutData::applicationData()) internally
        // not using the Q*Application metadata ATM, which results e.g. in the bugreport wizard
        // not having the app version preset
        // fixed hopefully in KF5 5.22.0, patch pending
        QGuiApplication *app = qApp;
        KAboutData aboutData(app->applicationName(), app->applicationDisplayName(), app->applicationVersion());
        aboutData.setOrganizationDomain(app->organizationDomain().toUtf8());
        d->helpMenu = new KHelpMenu(this, aboutData, false);
        // workaround-less version:
        // d->helpMenu = new KHelpMenu(this, QString()/*unused*/, false);
375

376 377 378
        // The difference between using KActionCollection->addAction() is that
        // these actions do not get tied to the MainWindow.  What does this all do?
        KActionCollection *actions = d->viewManager->actionCollection();
379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404
        QAction *helpContentsAction = d->helpMenu->action(KHelpMenu::menuHelpContents);
        QAction *whatsThisAction = d->helpMenu->action(KHelpMenu::menuWhatsThis);
        QAction *reportBugAction = d->helpMenu->action(KHelpMenu::menuReportBug);
        QAction *switchLanguageAction = d->helpMenu->action(KHelpMenu::menuSwitchLanguage);
        QAction *aboutAppAction = d->helpMenu->action(KHelpMenu::menuAboutApp);
        QAction *aboutKdeAction = d->helpMenu->action(KHelpMenu::menuAboutKDE);

        if (helpContentsAction) {
            actions->addAction(helpContentsAction->objectName(), helpContentsAction);
        }
        if (whatsThisAction) {
            actions->addAction(whatsThisAction->objectName(), whatsThisAction);
        }
        if (reportBugAction) {
            actions->addAction(reportBugAction->objectName(), reportBugAction);
        }
        if (switchLanguageAction) {
            actions->addAction(switchLanguageAction->objectName(), switchLanguageAction);
        }
        if (aboutAppAction) {
            actions->addAction(aboutAppAction->objectName(), aboutAppAction);
        }
        if (aboutKdeAction) {
            actions->addAction(aboutKdeAction->objectName(), aboutKdeAction);
        }

Halla Rempt's avatar
Halla Rempt committed
405
        connect(d->helpMenu, SIGNAL(showAboutApplication()), SLOT(showAboutApplication()));
406 407
    }

408
    // KDE' libs 4''s help contents action is broken outside kde, for some reason... We can handle it just as easily ourselves
409 410 411 412
    QAction *helpAction = actionCollection()->action("help_contents");
    helpAction->disconnect();
    connect(helpAction, SIGNAL(triggered()), this, SLOT(showManual()));

413 414 415 416

#if 0
    //check for colliding shortcuts
    QSet<QKeySequence> existingShortcuts;
417
    Q_FOREACH (QAction* action, actionCollection()->actions()) {
418 419 420
        if(action->shortcut() == QKeySequence(0)) {
            continue;
        }
Halla Rempt's avatar
Halla Rempt committed
421
        dbgKrita << "shortcut " << action->text() << " " << action->shortcut();
422 423 424 425 426 427 428
        Q_ASSERT(!existingShortcuts.contains(action->shortcut()));
        existingShortcuts.insert(action->shortcut());
    }
#endif

    configChanged();

429
    // If we have customized the toolbars, load that first
430
    setLocalXMLFile(KoResourcePaths::locateLocal("data", "krita.xmlgui"));
431
    setXMLFile(":/kxmlgui5/krita.xmlgui");
432

Halla Rempt's avatar
Halla Rempt committed
433
    guiFactory()->addClient(this);
434

435 436
    // Create and plug toolbar list for Settings menu
    QList<QAction *> toolbarList;
437
    Q_FOREACH (QWidget* it, guiFactory()->containers("ToolBar")) {
438 439 440 441
        KToolBar * toolBar = ::qobject_cast<KToolBar *>(it);

        if (toolBar) {
            if (toolBar->objectName() == "BrushesAndStuff") {
Halla Rempt's avatar
Halla Rempt committed
442
                toolBar->setEnabled(false);
443 444
            }

445
            KToggleAction* act = new KToggleAction(i18n("Show %1 Toolbar", toolBar->windowTitle()), this);
446 447 448 449 450 451
            actionCollection()->addAction(toolBar->objectName().toUtf8(), act);
            act->setCheckedState(KGuiItem(i18n("Hide %1 Toolbar", toolBar->windowTitle())));
            connect(act, SIGNAL(toggled(bool)), this, SLOT(slotToolbarToggled(bool)));
            act->setChecked(!toolBar->isHidden());
            toolbarList.append(act);
        } else
Halla Rempt's avatar
Halla Rempt committed
452
            warnUI << "Toolbar list contains a " << it->metaObject()->className() << " which is not a toolbar!";
453 454 455 456
    }
    plugActionList("toolbarlist", toolbarList);
    setToolbarList(toolbarList);

457
    applyToolBarLayout();
458

459 460 461
    d->viewManager->updateGUI();
    d->viewManager->updateIcons();

462 463 464 465 466
#ifdef Q_OS_WIN
    auto w = qApp->activeWindow();
    if (w) QWindowsWindowFunctions::setHasBorderInFullScreen(w->windowHandle(), true);
#endif

467
    QTimer::singleShot(1000, this, SLOT(checkSanity()));
468 469 470 471 472 473 474 475 476

    {
        using namespace std::placeholders; // For _1 placeholder
        std::function<void (int)> callback(
            std::bind(&KisMainWindow::switchTab, this, _1));

        d->tabSwitchCompressor.reset(
            new KisSignalCompressorWithParam<int>(500, callback, KisSignalCompressor::FIRST_INACTIVE));
    }
477 478 479



480 481 482 483 484 485 486 487 488
}

void KisMainWindow::setNoCleanup(bool noCleanup)
{
    d->noCleanup = noCleanup;
}

KisMainWindow::~KisMainWindow()
{
489
//    Q_FOREACH (QAction *ac, actionCollection()->actions()) {
490
//        QAction *action = qobject_cast<QAction*>(ac);
491
//        if (action) {
Halla Rempt's avatar
Halla Rempt committed
492
//        dbgKrita << "<Action"
493 494 495 496 497 498
//                 << "name=" << action->objectName()
//                 << "icon=" << action->icon().name()
//                 << "text="  << action->text().replace("&", "&amp;")
//                 << "whatsThis="  << action->whatsThis()
//                 << "toolTip="  << action->toolTip().replace("<html>", "").replace("</html>", "")
//                 << "iconText="  << action->iconText().replace("&", "&amp;")
499 500
//                 << "shortcut="  << action->shortcut(QAction::ActiveShortcut).toString()
//                 << "defaultShortcut="  << action->shortcut(QAction::DefaultShortcut).toString()
501 502 503 504 505
//                 << "isCheckable="  << QString((action->isChecked() ? "true" : "false"))
//                 << "statusTip=" << action->statusTip()
//                 << "/>"   ;
//        }
//        else {
Halla Rempt's avatar
Halla Rempt committed
506
//            dbgKrita << "Got a QAction:" << ac->objectName();
507 508 509 510
//        }

//    }

511
    // The doc and view might still exist (this is the case when closing the window)
512
    KisPart::instance()->removeMainWindow(this);
513 514 515 516

    if (d->noCleanup)
        return;

Halla Rempt's avatar
Halla Rempt committed
517
    delete d->viewManager;
518 519 520 521 522 523 524 525 526
    delete d;

}

void KisMainWindow::addView(KisView *view)
{
    if (d->activeView == view) return;

    if (d->activeView) {
527
        d->activeView->disconnect(this);
528 529 530 531 532 533
    }

    showView(view);
    updateCaption();
    emit restoringDone();

534 535
    if (d->activeView) {
        connect(d->activeView, SIGNAL(titleModified(QString,bool)), SLOT(slotDocumentTitleModified(QString,bool)));
536 537 538 539 540 541 542
    }
}

void KisMainWindow::showView(KisView *imageView)
{
    if (imageView && activeView() != imageView) {
        // XXX: find a better way to initialize this!
Halla Rempt's avatar
Halla Rempt committed
543
        imageView->setViewManager(d->viewManager);
544

Halla Rempt's avatar
Halla Rempt committed
545
        imageView->canvasBase()->setFavoriteResourceManager(d->viewManager->paintOpBox()->favoriteResourcesManager());
546
        imageView->slotLoadingFinished();
547

Halla Rempt's avatar
Halla Rempt committed
548
        QMdiSubWindow *subwin = d->mdiArea->addSubWindow(imageView);
549 550 551
        subwin->setAttribute(Qt::WA_DeleteOnClose, true);
        connect(subwin, SIGNAL(destroyed()), SLOT(updateWindowMenu()));

552 553 554
        KisConfig cfg;
        subwin->setOption(QMdiSubWindow::RubberBandMove, cfg.readEntry<int>("mdi_rubberband", cfg.useOpenGL()));
        subwin->setOption(QMdiSubWindow::RubberBandResize, cfg.readEntry<int>("mdi_rubberband", cfg.useOpenGL()));
555
        subwin->setWindowIcon(qApp->windowIcon());
556

557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
        /**
         * Hack alert!
         *
         * Here we explicitly request KoToolManager to emit all the tool
         * activation signals, to reinitialize the tool options docker.
         *
         * That is needed due to a design flaw we have in the
         * initialization procedure.  The tool in the KoToolManager is
         * initialized in KisView::setViewManager() calls, which
         * happens early enough. During this call the tool manager
         * requests KoCanvasControllerWidget to emit the signal to
         * update the widgets in the tool docker. *But* at that moment
         * of time the view is not yet connected to the main window,
         * because it happens in KisViewManager::setCurrentView a bit
         * later. This fact makes the widgets updating signals be lost
         * and never reach the tool docker.
         *
         * So here we just explicitly call the tool activation stub.
         */

        KoToolManager::instance()->initializeCurrentToolForCanvas();

Halla Rempt's avatar
Halla Rempt committed
579
        if (d->mdiArea->subWindowList().size() == 1) {
580 581 582 583 584 585
            imageView->showMaximized();
        }
        else {
            imageView->show();
        }

Halla Rempt's avatar
Halla Rempt committed
586 587 588 589 590
        // No, no, no: do not try to call this _before_ the show() has
        // been called on the view; only when that has happened is the
        // opengl context active, and very bad things happen if we tell
        // the dockers to update themselves with a view if the opengl
        // context is not active.
591 592
        setActiveView(imageView);

593
        updateWindowMenu();
594
        updateCaption();
595 596 597 598 599 600 601 602 603
    }
}

void KisMainWindow::slotPreferences()
{
    if (KisDlgPreferences::editPreferences()) {
        KisConfigNotifier::instance()->notifyConfigChanged();

        // XXX: should this be changed for the views in other windows as well?
604
        Q_FOREACH (QPointer<KisView> koview, KisPart::instance()->views()) {
605 606 607 608 609 610 611 612 613 614 615 616 617
            KisViewManager *view = qobject_cast<KisViewManager*>(koview);
            if (view) {
                // Update the settings for all nodes -- they don't query
                // KisConfig directly because they need the settings during
                // compositing, and they don't connect to the config notifier
                // because nodes are not QObjects (because only one base class
                // can be a QObject).
                KisNode* node = dynamic_cast<KisNode*>(view->image()->rootLayer().data());
                node->updateSettings();
            }

        }

Halla Rempt's avatar
Halla Rempt committed
618
        d->viewManager->showHideScrollbars();
619 620 621
    }
}

622 623 624
void KisMainWindow::slotThemeChanged()
{
    // save theme changes instantly
625
    KConfigGroup group( KSharedConfig::openConfig(), "theme");
626 627
    group.writeEntry("Theme", d->themeManager->currentThemeName());

628
    // reload action icons!
629
    Q_FOREACH (QAction *action, actionCollection()->actions()) {
630
        KisIconUtils::updateIcon(action);
631 632
    }

633 634 635
    emit themeChanged();
}

Halla Rempt's avatar
Halla Rempt committed
636
void KisMainWindow::updateReloadFileAction(KisDocument *doc)
637
{
Halla Rempt's avatar
Halla Rempt committed
638
    Q_UNUSED(doc);
Halla Rempt's avatar
Halla Rempt committed
639
//    d->reloadFile->setEnabled(doc && !doc->url().isEmpty());
640 641 642 643 644 645
}

void KisMainWindow::setReadWrite(bool readwrite)
{
    d->saveAction->setEnabled(readwrite);
    d->importFile->setEnabled(readwrite);
Halla Rempt's avatar
Halla Rempt committed
646
    d->readOnly = !readwrite;
647 648 649
    updateCaption();
}

Halla Rempt's avatar
Halla Rempt committed
650
void KisMainWindow::addRecentURL(const QUrl &url)
651
{
Halla Rempt's avatar
Halla Rempt committed
652
    dbgUI << "KisMainWindow::addRecentURL url=" << url.toDisplayString();
653 654 655 656 657
    // Add entry to recent documents list
    // (call coming from KisDocument because it must work with cmd line, template dlg, file/open, etc.)
    if (!url.isEmpty()) {
        bool ok = true;
        if (url.isLocalFile()) {
Halla Rempt's avatar
Halla Rempt committed
658
            QString path = url.adjusted(QUrl::StripTrailingSlash).toLocalFile();
659
            const QStringList tmpDirs = KoResourcePaths::resourceDirs("tmp");
660 661 662
            for (QStringList::ConstIterator it = tmpDirs.begin() ; ok && it != tmpDirs.end() ; ++it)
                if (path.contains(*it))
                    ok = false; // it's in the tmp resource
Halla Rempt's avatar
Halla Rempt committed
663
#ifdef HAVE_KIO
664
            if (ok) {
665
                KRecentDocument::add(QUrl::fromLocalFile(path));
666
            }
Halla Rempt's avatar
Halla Rempt committed
667 668 669 670
#endif
        }
#ifdef HAVE_KIO
        else {
671
            KRecentDocument::add(url.adjusted(QUrl::StripTrailingSlash));
672
        }
Halla Rempt's avatar
Halla Rempt committed
673
#endif
674
        if (ok) {
Halla Rempt's avatar
Halla Rempt committed
675
            d->recentFiles->addUrl(url);
676 677 678 679 680 681 682 683 684
        }
        saveRecentFiles();

    }
}

void KisMainWindow::saveRecentFiles()
{
    // Save list of recent files
685
    KSharedConfigPtr config =  KSharedConfig::openConfig();
Halla Rempt's avatar
Halla Rempt committed
686
    d->recentFiles->saveEntries(config->group("RecentFiles"));
687 688 689 690
    config->sync();

    // Tell all windows to reload their list, after saving
    // Doesn't work multi-process, but it's a start
691
    Q_FOREACH (KMainWindow* window, KMainWindow::memberList())
692 693 694 695 696
        static_cast<KisMainWindow *>(window)->reloadRecentFileList();
}

void KisMainWindow::reloadRecentFileList()
{
697
    d->recentFiles->loadEntries( KSharedConfig::openConfig()->group("RecentFiles"));
698 699 700 701
}

void KisMainWindow::updateCaption()
{
Halla Rempt's avatar
Halla Rempt committed
702
    if (!d->mdiArea->activeSubWindow()) {
703 704 705 706 707 708 709 710
        updateCaption(QString(), false);
    }
    else {
        QString caption( d->activeView->document()->caption() );
        if (d->readOnly) {
            caption += ' ' + i18n("(write protected)");
        }

711 712
        d->activeView->setWindowTitle(caption);

713
        updateCaption(caption, d->activeView->document()->isModified());
714

Halla Rempt's avatar
Halla Rempt committed
715 716
        if (!d->activeView->document()->url().fileName().isEmpty())
            d->saveAction->setToolTip(i18n("Save as %1", d->activeView->document()->url().fileName()));
717 718 719 720 721 722 723
        else
            d->saveAction->setToolTip(i18n("Save"));
    }
}

void KisMainWindow::updateCaption(const QString & caption, bool mod)
{
Halla Rempt's avatar
Halla Rempt committed
724
    dbgUI << "KisMainWindow::updateCaption(" << caption << "," << mod << ")";
725 726
#ifdef KRITA_ALPHA
    setCaption(QString("ALPHA %1: %2").arg(KRITA_ALPHA).arg(caption), mod);
727 728
    return;
#endif
729 730
#ifdef KRITA_BETA
    setCaption(QString("BETA %1: %2").arg(KRITA_BETA).arg(caption), mod);
731 732
    return;
#endif
733 734
#ifdef KRITA_RC
    setCaption(QString("RELEASE CANDIDATE %1: %2").arg(KRITA_RC).arg(caption), mod);
735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
    return;
#endif

    setCaption(caption, mod);
}


KisView *KisMainWindow::activeView() const
{
    if (d->activeView) {
        return d->activeView;
    }
    return 0;
}

Halla Rempt's avatar
Halla Rempt committed
750
bool KisMainWindow::openDocument(const QUrl &url)
751
{
752
    if (!QFile(url.toLocalFile()).exists()) {
Halla Rempt's avatar
Halla Rempt committed
753
        QMessageBox::critical(0, i18nc("@title:window", "Krita"), i18n("The file %1 does not exist.", url.url()));
Halla Rempt's avatar
Halla Rempt committed
754
        d->recentFiles->removeUrl(url); //remove the file from the recent-opened-file-list
755 756 757 758 759 760
        saveRecentFiles();
        return false;
    }
    return openDocumentInternal(url);
}

Halla Rempt's avatar
Halla Rempt committed
761
bool KisMainWindow::openDocumentInternal(const QUrl &url, KisDocument *newdoc)
762
{
Halla Rempt's avatar
Halla Rempt committed
763 764 765 766 767
    if (!url.isLocalFile()) {
        qDebug() << "KisMainWindow::openDocumentInternal. Not a local file:" << url;
        return false;
    }

768
    if (!newdoc) {
769
        newdoc = KisPart::instance()->createDocument();
770 771 772 773 774 775
    }

    d->firstTime = true;
    connect(newdoc, SIGNAL(sigProgress(int)), this, SLOT(slotProgress(int)));
    connect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted()));
    connect(newdoc, SIGNAL(canceled(const QString &)), this, SLOT(slotLoadCanceled(const QString &)));
776
    bool openRet = (!d->isImporting) ? newdoc->openUrl(url) : newdoc->importDocument(url);
777 778 779 780
    if (!openRet) {
        delete newdoc;
        return false;
    }
781 782

    KisPart::instance()->addDocument(newdoc);
783 784
    updateReloadFileAction(newdoc);

785
    if (!QFileInfo(url.toLocalFile()).isWritable()) {
786 787 788 789 790
        setReadWrite(false);
    }
    return true;
}

791 792 793 794 795 796 797 798
void KisMainWindow::addViewAndNotifyLoadingCompleted(KisDocument *document)
{
    KisView *view = KisPart::instance()->createView(document, resourceManager(), actionCollection(), this);
    addView(view);

    emit guiLoadingFinished();
}

799 800 801 802
QStringList KisMainWindow::showOpenFileDialog()
{
    KoFileDialog dialog(this, KoFileDialog::ImportFiles, "OpenDocument");
    dialog.setDefaultDir(QDesktopServices::storageLocation(QDesktopServices::PicturesLocation));
803
    dialog.setMimeTypeFilters(KisImportExportManager::mimeFilter(KisImportExportManager::Import));
804
    dialog.setCaption(d->isImporting ? i18n("Import Images") : i18n("Open Images"));
805 806 807 808

    return dialog.filenames();
}

809 810 811 812
// Separate from openDocument to handle async loading (remote URLs)
void KisMainWindow::slotLoadCompleted()
{
    KisDocument *newdoc = qobject_cast<KisDocument*>(sender());
813 814
    if (newdoc && newdoc->image()) {
        addViewAndNotifyLoadingCompleted(newdoc);
815

816 817 818
        disconnect(newdoc, SIGNAL(sigProgress(int)), this, SLOT(slotProgress(int)));
        disconnect(newdoc, SIGNAL(completed()), this, SLOT(slotLoadCompleted()));
        disconnect(newdoc, SIGNAL(canceled(const QString &)), this, SLOT(slotLoadCanceled(const QString &)));
819

820 821
        emit loadCompleted();
    }
822 823 824 825
}

void KisMainWindow::slotLoadCanceled(const QString & errMsg)
{
Halla Rempt's avatar
Halla Rempt committed
826
    dbgUI << "KisMainWindow::slotLoadCanceled";
827
    if (!errMsg.isEmpty())   // empty when canceled by user
Halla Rempt's avatar
Halla Rempt committed
828
        QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg);
829 830 831 832 833 834 835 836 837 838 839
    // ... can't delete the document, it's the one who emitted the signal...

    KisDocument* doc = qobject_cast<KisDocument*>(sender());
    Q_ASSERT(doc);
    disconnect(doc, SIGNAL(sigProgress(int)), this, SLOT(slotProgress(int)));
    disconnect(doc, SIGNAL(completed()), this, SLOT(slotLoadCompleted()));
    disconnect(doc, SIGNAL(canceled(const QString &)), this, SLOT(slotLoadCanceled(const QString &)));
}

void KisMainWindow::slotSaveCanceled(const QString &errMsg)
{
Halla Rempt's avatar
Halla Rempt committed
840
    dbgUI << "KisMainWindow::slotSaveCanceled";
841
    if (!errMsg.isEmpty())   // empty when canceled by user
Halla Rempt's avatar
Halla Rempt committed
842
        QMessageBox::critical(this, i18nc("@title:window", "Krita"), errMsg);
843 844 845 846 847
    slotSaveCompleted();
}

void KisMainWindow::slotSaveCompleted()
{
Halla Rempt's avatar
Halla Rempt committed
848
    dbgUI << "KisMainWindow::slotSaveCompleted";
849 850 851 852 853 854 855 856 857 858
    KisDocument* doc = qobject_cast<KisDocument*>(sender());
    Q_ASSERT(doc);
    disconnect(doc, SIGNAL(sigProgress(int)), this, SLOT(slotProgress(int)));
    disconnect(doc, SIGNAL(completed()), this, SLOT(slotSaveCompleted()));
    disconnect(doc, SIGNAL(canceled(const QString &)), this, SLOT(slotSaveCanceled(const QString &)));

    if (d->deferredClosingEvent) {
        KXmlGuiWindow::closeEvent(d->deferredClosingEvent);
    }
}
859

860 861 862 863 864 865 866
bool KisMainWindow::hackIsSaving() const
{
    StdLockableWrapper<QMutex> wrapper(&d->savingEntryMutex);
    std::unique_lock<StdLockableWrapper<QMutex>> l(wrapper, std::try_to_lock);
    return !l.owns_lock();
}

Halla Rempt's avatar
Halla Rempt committed
867
bool KisMainWindow::saveDocument(KisDocument *document, bool saveas)
868
{
869
    if (!document) {
870 871 872
        return true;
    }

873 874 875 876
    /**
     * Make sure that we cannot enter this method twice!
     *
     * The lower level functions may call processEvents() so
877
     * double-entry is quite possible to achieve. Here we try to lock
878 879 880 881 882 883
     * the mutex, and if it is failed, just cancel saving.
     */
    StdLockableWrapper<QMutex> wrapper(&d->savingEntryMutex);
    std::unique_lock<StdLockableWrapper<QMutex>> l(wrapper, std::try_to_lock);
    if (!l.owns_lock()) return false;

884 885 886
    KisDelayedSaveDialog dlg(document->image(), this);
    dlg.blockIfImageIsBusy();

887 888 889 890
    if (dlg.result() != QDialog::Accepted) {
        return false;
    }

891 892 893 894 895 896 897 898 899 900 901 902 903
    bool reset_url;

    if (document->url().isEmpty()) {
        reset_url = true;
        saveas = true;
    } else {
        reset_url = false;
    }

    connect(document, SIGNAL(sigProgress(int)), this, SLOT(slotProgress(int)));
    connect(document, SIGNAL(completed()), this, SLOT(slotSaveCompleted()));
    connect(document, SIGNAL(canceled(const QString &)), this, SLOT(slotSaveCanceled(const QString &)));

Halla Rempt's avatar
Halla Rempt committed
904
    QUrl oldURL = document->url();
905 906 907 908 909
    QString oldFile = document->localFilePath();

    QByteArray _native_format = document->nativeFormatMimeType();
    QByteArray oldOutputFormat = document->outputMimeType();

Halla Rempt's avatar
Halla Rempt committed
910
    QUrl suggestedURL = document->url();
911 912

    QStringList mimeFilter;
Halla Rempt's avatar
Halla Rempt committed
913

Halla Rempt's avatar
Halla Rempt committed
914
    mimeFilter = KisImportExportManager::mimeFilter(KisImportExportManager::Export);
915

916
    if (!mimeFilter.contains(oldOutputFormat) && !d->isExporting) {
Halla Rempt's avatar
Halla Rempt committed
917
        dbgUI << "KisMainWindow::saveDocument no export filter for" << oldOutputFormat;
918 919 920 921 922 923

        // --- don't setOutputMimeType in case the user cancels the Save As
        // dialog and then tries to just plain Save ---

        // suggest a different filename extension (yes, we fortunately don't all live in a world of magic :))
        QString suggestedFilename = suggestedURL.fileName();
924

925 926 927
        if (!suggestedFilename.isEmpty()) {  // ".kra" looks strange for a name
            int c = suggestedFilename.lastIndexOf('.');

928
            const QString ext = KisMimeDatabase::suffixesForMimeType(_native_format).first();
929 930
            if (!ext.isEmpty()) {
                if (c < 0)
931
                    suggestedFilename = suggestedFilename + "." + ext;
932
                else
933
                    suggestedFilename = suggestedFilename.left(c) + "." + ext;
934 935 936 937 938 939 940
            } else { // current filename extension wrong anyway
                if (c > 0) {
                    // this assumes that a . signifies an extension, not just a .
                    suggestedFilename = suggestedFilename.left(c);
                }
            }

Halla Rempt's avatar
Halla Rempt committed
941 942
            suggestedURL = suggestedURL.adjusted(QUrl::RemoveFilename);
            suggestedURL.setPath(suggestedURL.path() + suggestedFilename);
943 944 945 946 947 948 949 950 951
        }

        // force the user to choose outputMimeType
        saveas = true;
    }

    bool ret = false;

    if (document->url().isEmpty() || saveas) {
952 953 954 955
        // if you're just File/Save As'ing to change filter options you
        // don't want to be reminded about overwriting files etc.
        bool justChangingFilterOptions = false;

956 957
        KoFileDialog dialog(this, KoFileDialog::SaveFile, "SaveDocument");
        dialog.setCaption(i18n("untitled"));
958
        if (d->isExporting && !d->lastExportUrl.isEmpty()) {
959
            dialog.setDefaultDir(d->lastExportUrl.toLocalFile());
960 961
        }
        else {
962
            dialog.setDefaultDir(suggestedURL.toLocalFile());
963
        }
964
        // Default to all supported file types if user is exporting, otherwise use Krita default
965
        dialog.setMimeTypeFilters(mimeFilter, QString(_native_format));
Halla Rempt's avatar
Halla Rempt committed
966
        QUrl newURL = QUrl::fromUserInput(dialog.filename());
967 968 969 970

        if (newURL.isLocalFile()) {
            QString fn = newURL.toLocalFile();
            if (QFileInfo(fn).completeSuffix().isEmpty()) {
971
                fn.append(KisMimeDatabase::suffixesForMimeType(_native_format).first());
Halla Rempt's avatar
Halla Rempt committed
972
                newURL = QUrl::fromLocalFile(fn);
973 974 975
            }
        }

976 977 978 979 980 981
        if (document->documentInfo()->aboutInfo("title") == i18n("Unnamed")) {
            QString fn = newURL.toLocalFile();
            QFileInfo info(fn);
            document->documentInfo()->setAboutInfo("title", info.baseName());
        }

982 983
        QByteArray outputFormat = _native_format;

Halla Rempt's avatar
Halla Rempt committed
984 985
        QString outputFormatString = KisMimeDatabase::mimeTypeForFile(newURL.toLocalFile());
        outputFormat = outputFormatString.toLatin1();
986

987