Пример #1
0
class SpineDatapackageWidget(QMainWindow):
    """A widget to edit CSV files in a Data Connection and create a tabular datapackage.
    """

    msg = Signal(str)
    msg_error = Signal(str)

    def __init__(self, datapackage):
        """Initialize class.

        Args:
            datapackage (CustomPackage): Data package associated to this widget
        """
        from ..ui.spine_datapackage_form import Ui_MainWindow  # pylint: disable=import-outside-toplevel

        super().__init__(flags=Qt.Window)
        self.datapackage = datapackage
        self.selected_resource_index = None
        self.resources_model = DatapackageResourcesModel(self, self.datapackage)
        self.fields_model = DatapackageFieldsModel(self, self.datapackage)
        self.foreign_keys_model = DatapackageForeignKeysModel(self, self.datapackage)
        self.resource_data_model = DatapackageResourceDataModel(self, self.datapackage)
        self.default_row_height = QFontMetrics(QFont("", 0)).lineSpacing()
        max_screen_height = max([s.availableSize().height() for s in QGuiApplication.screens()])
        self.visible_rows = int(max_screen_height / self.default_row_height)
        self.err_msg = QErrorMessage(self)
        self.notification_stack = NotificationStack(self)
        self._foreign_keys_context_menu = QMenu(self)
        self._file_watcher = QFileSystemWatcher(self)
        self._file_watcher.addPath(self.datapackage.base_path)
        self._changed_source_indexes = set()
        self.undo_group = QUndoGroup(self)
        self.undo_stacks = {}
        self._save_resource_actions = []
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.takeCentralWidget()
        self._before_save_all = self.ui.menuFile.insertSeparator(self.ui.actionSave_All)
        self.setWindowIcon(QIcon(":/symbols/app.ico"))
        self.qsettings = QSettings("SpineProject", "Spine Toolbox")
        self.restore_ui()
        self.add_menu_actions()
        self.setStyleSheet(MAINWINDOW_SS)
        self.ui.tableView_resources.setModel(self.resources_model)
        self.ui.tableView_resources.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_resource_data.setModel(self.resource_data_model)
        self.ui.tableView_resource_data.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_resource_data.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.ui.tableView_fields.setModel(self.fields_model)
        self.ui.tableView_fields.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_fields.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.ui.tableView_foreign_keys.setModel(self.foreign_keys_model)
        self.ui.tableView_foreign_keys.verticalHeader().setDefaultSectionSize(self.default_row_height)
        self.ui.tableView_foreign_keys.horizontalHeader().setResizeContentsPrecision(self.visible_rows)
        self.connect_signals()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.setWindowTitle("{0}[*] - Spine datapackage manager".format(self.datapackage.base_path))
        self.load_datapackage()

    @property
    def undo_stack(self):
        return self.undo_group.activeStack()

    @property
    def datapackage_path(self):
        return os.path.join(self.datapackage.base_path, "datapackage.json")

    def load_datapackage(self):
        self._file_watcher.addPaths(self.datapackage.sources)
        self.append_save_resource_actions()
        self.resources_model.refresh_model()
        first_index = self.resources_model.index(0, 0)
        if not first_index.isValid():
            return
        self.ui.tableView_resources.selectionModel().setCurrentIndex(first_index, QItemSelectionModel.Select)

    def add_menu_actions(self):
        """Add extra menu actions."""
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_resources.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_data.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_fields.toggleViewAction())
        self.ui.menuDock_Widgets.addAction(self.ui.dockWidget_foreign_keys.toggleViewAction())
        undo_action = self.undo_group.createUndoAction(self)
        redo_action = self.undo_group.createRedoAction(self)
        undo_action.setShortcuts(QKeySequence.Undo)
        redo_action.setShortcuts(QKeySequence.Redo)
        undo_action.setIcon(QIcon(":/icons/menu_icons/undo.svg"))
        redo_action.setIcon(QIcon(":/icons/menu_icons/redo.svg"))
        before = self.ui.menuEdit.actions()[0]
        self.ui.menuEdit.insertAction(before, undo_action)
        self.ui.menuEdit.insertAction(before, redo_action)
        self.ui.menuEdit.insertSeparator(before)

    def connect_signals(self):
        """Connect signals to slots."""
        self.msg.connect(self.add_message)
        self.msg_error.connect(self.add_error_message)
        self._file_watcher.directoryChanged.connect(self._handle_source_dir_changed)
        self._file_watcher.fileChanged.connect(self._handle_source_file_changed)
        self.ui.actionCopy.triggered.connect(self.copy)
        self.ui.actionPaste.triggered.connect(self.paste)
        self.ui.actionClose.triggered.connect(self.close)
        self.ui.actionSave_All.triggered.connect(self.save_all)
        self.ui.actionSave_datapackage.triggered.connect(self.save_datapackage)
        self.ui.menuEdit.aboutToShow.connect(self.refresh_copy_paste_actions)
        self.fields_model.dataChanged.connect(self._handle_fields_data_changed)
        self.undo_group.cleanChanged.connect(self.update_window_modified)
        checkbox_delegate = CheckBoxDelegate(self)
        checkbox_delegate.data_committed.connect(self.fields_model.setData)
        self.ui.tableView_fields.setItemDelegateForColumn(2, checkbox_delegate)
        foreign_keys_delegate = ForeignKeysDelegate(self)
        foreign_keys_delegate.data_committed.connect(self.foreign_keys_model.setData)
        self.ui.tableView_foreign_keys.setItemDelegate(foreign_keys_delegate)
        self.ui.tableView_resources.selectionModel().currentChanged.connect(self._handle_current_resource_changed)
        self.ui.tableView_foreign_keys.customContextMenuRequested.connect(self.show_foreign_keys_context_menu)
        self._foreign_keys_context_menu.addAction("Remove foreign key", self._remove_foreign_key)

    @Slot(bool)
    def update_window_modified(self, _clean=None):
        """Updates window modified status and save actions depending on the state of the undo stack."""
        try:
            dirty_resource_indexes = {
                idx for idx in range(len(self.datapackage.resources)) if self.is_resource_dirty(idx)
            }
            dirty = bool(dirty_resource_indexes)
            self.setWindowModified(dirty)
        except RuntimeError:
            return
        self.ui.actionSave_datapackage.setEnabled(dirty)
        self.ui.actionSave_All.setEnabled(dirty)
        for idx, action in enumerate(self._save_resource_actions):
            dirty = idx in dirty_resource_indexes
            action.setEnabled(dirty)
            self.resources_model.update_resource_dirty(idx, dirty)

    def is_resource_dirty(self, resource_index):
        if resource_index in self._changed_source_indexes:
            return True
        try:
            return not self.undo_stacks[resource_index].isClean()
        except KeyError:
            return False

    def get_undo_stack(self, resource_index):
        if resource_index not in self.undo_stacks:
            self.undo_stacks[resource_index] = stack = QUndoStack(self.undo_group)
            stack.cleanChanged.connect(self.update_window_modified)
        return self.undo_stacks[resource_index]

    @Slot(str)
    def _handle_source_dir_changed(self, _path):
        if not self.datapackage.resources:
            self.load_datapackage()
            return
        self.datapackage.difference_infer(os.path.join(self.datapackage.base_path, '*.csv'))
        self._file_watcher.addPaths(self.datapackage.sources)
        self.append_save_resource_actions()
        self.resources_model.refresh_model()
        self.refresh_models()

    @Slot(str)
    def _handle_source_file_changed(self, path):
        for idx, source in enumerate(self.datapackage.sources):
            if os.path.normpath(source) == os.path.normpath(path):
                self._changed_source_indexes.add(idx)
                self.update_window_modified()
                break

    def append_save_resource_actions(self):
        new_actions = []
        for resource_index in range(len(self._save_resource_actions), len(self.datapackage.resources)):
            resource = self.datapackage.resources[resource_index]
            action = QAction(f"Save '{os.path.basename(resource.source)}'")
            action.setEnabled(False)
            action.triggered.connect(
                lambda checked=False, resource_index=resource_index: self.save_resource(resource_index)
            )
            new_actions.append(action)
        self.ui.menuFile.insertActions(self._before_save_all, new_actions)
        self._save_resource_actions += new_actions

    @Slot()
    def refresh_copy_paste_actions(self):
        """Adjusts copy and paste actions depending on which widget has the focus.
        """
        self.ui.actionCopy.setEnabled(focused_widget_has_callable(self, "copy"))
        self.ui.actionPaste.setEnabled(focused_widget_has_callable(self, "paste"))

    @Slot(str)
    def add_message(self, msg):
        """Prepend regular message to status bar.

        Args:
            msg (str): String to show in QStatusBar
        """
        self.notification_stack.push(msg)

    @Slot(str)
    def add_error_message(self, msg):
        """Show error message.

        Args:
            msg (str): String to show
        """
        self.err_msg.showMessage(msg)

    @Slot(bool)
    def save_all(self, _=False):
        resource_paths = {k: r.source for k, r in enumerate(self.datapackage.resources) if self.is_resource_dirty(k)}
        all_paths = list(resource_paths.values()) + [self.datapackage_path]
        if not self.get_permission(*all_paths):
            return
        for k, path in resource_paths.items():
            self._save_resource(k, path)
        self.save_datapackage()

    @Slot(bool)
    def save_datapackage(self, _=False):
        if self.datapackage.save(self.datapackage_path):
            self.msg.emit("'datapackage.json' succesfully saved")
            return
        self.msg_error.emit("Failed to save 'datapackage.json'")

    def save_resource(self, resource_index):
        resource = self.datapackage.resources[resource_index]
        filepath = resource.source
        if not self.get_permission(filepath, self.datapackage_path):
            return
        self._save_resource(resource_index, filepath)
        self.save_datapackage()

    def _save_resource(self, resource_index, filepath):
        headers = self.datapackage.resources[resource_index].schema.field_names
        self._file_watcher.removePath(filepath)
        with open(filepath, 'w', newline='') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(headers)
            for row in self.datapackage.resource_data(resource_index):
                writer.writerow(row)
        self.msg.emit(f"'{os.path.basename(filepath)}' succesfully saved")
        self._file_watcher.addPath(filepath)
        self._changed_source_indexes.discard(resource_index)
        stack = self.undo_stacks.get(resource_index)
        if not stack or stack.isClean():
            self.update_window_modified()
        elif stack:
            stack.setClean()

    def get_permission(self, *filepaths):
        start_dir = self.datapackage.base_path
        filepaths = [os.path.relpath(path, start_dir) for path in filepaths if os.path.isfile(path)]
        if not filepaths:
            return True
        pathlist = "".join([f"<li>{path}</li>" for path in filepaths])
        msg = f"The following file(s) in <b>{os.path.basename(start_dir)}</b> will be replaced: <ul>{pathlist}</ul>. Are you sure?"
        message_box = QMessageBox(
            QMessageBox.Question, "Replacing file(s)", msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self
        )
        message_box.button(QMessageBox.Ok).setText("Replace")
        return message_box.exec_() != QMessageBox.Cancel

    @Slot(bool)
    def copy(self, checked=False):
        """Copies data to clipboard."""
        call_on_focused_widget(self, "copy")

    @Slot(bool)
    def paste(self, checked=False):
        """Pastes data from clipboard."""
        call_on_focused_widget(self, "paste")

    @Slot("QModelIndex", "QModelIndex")
    def _handle_current_resource_changed(self, current, _previous):
        """Resets resource data and schema models whenever a new resource is selected."""
        self.refresh_models(current)

    def refresh_models(self, current=None):
        if current is None:
            current = self.ui.tableView_resources.selectionModel().currentIndex()
        if current.column() != 0 or current.row() == self.selected_resource_index:
            return
        self.selected_resource_index = current.row()
        self.get_undo_stack(self.selected_resource_index).setActive()
        self.resource_data_model.refresh_model(self.selected_resource_index)
        self.fields_model.refresh_model(self.selected_resource_index)
        self.foreign_keys_model.refresh_model(self.selected_resource_index)
        self.ui.tableView_resource_data.resizeColumnsToContents()
        self.ui.tableView_fields.resizeColumnsToContents()
        self.ui.tableView_foreign_keys.resizeColumnsToContents()

    @Slot("QModelIndex", "QModelIndex", "QVector<int>")
    def _handle_fields_data_changed(self, top_left, bottom_right, roles):
        top, left = top_left.row(), top_left.column()
        bottom, right = bottom_right.row(), bottom_right.column()
        if left <= 0 <= right and Qt.DisplayRole in roles:
            # Fields name changed
            self.resource_data_model.headerDataChanged.emit(Qt.Horizontal, top, bottom)
            self.ui.tableView_resource_data.resizeColumnsToContents()
            self.foreign_keys_model.emit_data_changed()

    @Slot("QPoint")
    def show_foreign_keys_context_menu(self, pos):
        index = self.ui.tableView_foreign_keys.indexAt(pos)
        if not index.isValid() or index.row() == index.model().rowCount() - 1:
            return
        global_pos = self.ui.tableView_foreign_keys.viewport().mapToGlobal(pos)
        self._foreign_keys_context_menu.popup(global_pos)

    @Slot(bool)
    def _remove_foreign_key(self, checked=False):
        index = self.ui.tableView_foreign_keys.currentIndex()
        if not index.isValid():
            return
        index.model().call_remove_foreign_key(index.row())

    def restore_ui(self):
        """Restore UI state from previous session."""
        window_size = self.qsettings.value("dataPackageWidget/windowSize")
        window_pos = self.qsettings.value("dataPackageWidget/windowPosition")
        window_maximized = self.qsettings.value("dataPackageWidget/windowMaximized", defaultValue='false')
        window_state = self.qsettings.value("dataPackageWidget/windowState")
        n_screens = self.qsettings.value("mainWindow/n_screens", defaultValue=1)
        original_size = self.size()
        if window_size:
            self.resize(window_size)
        if window_pos:
            self.move(window_pos)
        # noinspection PyArgumentList
        if len(QGuiApplication.screens()) < int(n_screens):
            # There are less screens available now than on previous application startup
            self.move(0, 0)  # Move this widget to primary screen position (0,0)
        ensure_window_is_on_screen(self, original_size)
        if window_maximized == 'true':
            self.setWindowState(Qt.WindowMaximized)
        if window_state:
            self.restoreState(window_state, version=1)  # Toolbar and dockWidget positions

    def closeEvent(self, event=None):
        """Handle close event.

        Args:
            event (QEvent): Closing event if 'X' is clicked.
        """
        # save qsettings
        self.qsettings.setValue("dataPackageWidget/windowSize", self.size())
        self.qsettings.setValue("dataPackageWidget/windowPosition", self.pos())
        self.qsettings.setValue("dataPackageWidget/windowState", self.saveState(version=1))
        self.qsettings.setValue("dataPackageWidget/windowMaximized", self.windowState() == Qt.WindowMaximized)
        self.qsettings.setValue("dataPackageWidget/n_screens", len(QGuiApplication.screens()))
        if event:
            event.accept()
Пример #2
0
class QmlInstantEngine(QQmlApplicationEngine):
    """
    QmlInstantEngine is an utility class helping developing QML applications.
    It reloads itself whenever one of the watched source files is modified.
    As it consumes resources, make sure to disable file watching in production mode.
    """
    def __init__(self,
                 sourceFile="",
                 watching=True,
                 verbose=False,
                 parent=None):
        """
        watching -- Defines whether the watcher is active (default: True)
        verbose -- if True, output log infos (default: False)
        """
        super(QmlInstantEngine, self).__init__(parent)

        self._fileWatcher = QFileSystemWatcher()  # Internal Qt File Watcher
        self._sourceFile = ""
        self._watchedFiles = []  # Internal watched files list
        self._verbose = verbose  # Verbose bool
        self._watching = False  #
        self._extensions = [
            "qml", "js"
        ]  # File extensions that defines files to watch when adding a folder

        self._rootItem = None

        def onObjectCreated(root, url):
            if not root:
                return
            # Restore root item geometry
            if self._rootItem:
                root.setGeometry(self._rootItem.geometry())
                self._rootItem.deleteLater()
            self._rootItem = root

        self.objectCreated.connect(onObjectCreated)

        # Update the watching status
        self.setWatching(watching)

        if sourceFile:
            self.load(sourceFile)

    def load(self, sourceFile):
        self._sourceFile = sourceFile
        super(QmlInstantEngine, self).load(sourceFile)

    def setWatching(self, watchValue):
        """
        Enable (True) or disable (False) the file watching.
        Tip: file watching should be enable only when developing.
        """
        if self._watching is watchValue:
            return

        self._watching = watchValue
        # Enable the watcher
        if self._watching:
            # 1. Add internal list of files to the internal Qt File Watcher
            self.addFiles(self._watchedFiles)
            # 2. Connect 'filechanged' signal
            self._fileWatcher.fileChanged.connect(self.onFileChanged)

        # Disabling the watcher
        else:
            # 1. Remove all files in the internal Qt File Watcher
            self._fileWatcher.removePaths(self._watchedFiles)
            # 2. Disconnect 'filechanged' signal
            self._fileWatcher.fileChanged.disconnect(self.onFileChanged)

    @property
    def watchedExtensions(self):
        """ Returns the list of extensions used when using addFilesFromDirectory. """
        return self._extensions

    @watchedExtensions.setter
    def watchedExtensions(self, extensions):
        """ Set the list of extensions to search for when using addFilesFromDirectory. """
        self._extensions = extensions

    def setVerbose(self, verboseValue):
        """ Activate (True) or desactivate (False) the verbose. """
        self._verbose = verboseValue

    def addFile(self, filename):
        """
        Add the given 'filename' to the watched files list.
        'filename' can be an absolute or relative path (str and QUrl accepted)
        """
        # Deal with QUrl type
        # NOTE: happens when using the source() method on a QQuickView
        if isinstance(filename, QUrl):
            filename = filename.path()

        # Make sure the file exists
        if not os.path.isfile(filename):
            raise ValueError("addFile: file %s doesn't exist." % filename)

        # Return if the file is already in our internal list
        if filename in self._watchedFiles:
            return

        # Add this file to the internal files list
        self._watchedFiles.append(filename)
        # And, if watching is active, add it to the internal watcher as well
        if self._watching:
            if self._verbose:
                print("instantcoding: addPath", filename)
            self._fileWatcher.addPath(filename)

    def addFiles(self, filenames):
        """
        Add the given 'filenames' to the watched files list.
        filenames -- a list of absolute or relative paths (str and QUrl accepted)
        """
        # Convert to list
        if not isinstance(filenames, list):
            filenames = [filenames]

        for filename in filenames:
            self.addFile(filename)

    def addFilesFromDirectory(self, dirname, recursive=False):
        """
        Add files from the given directory name 'dirname'.
        dirname -- an absolute or a relative path
        recursive -- if True, will search inside each subdirectories recursively.
        """
        if not os.path.isdir(dirname):
            raise RuntimeError(
                "addFilesFromDirectory : %s is not a valid directory." %
                dirname)

        if recursive:
            for dirpath, dirnames, filenames in os.walk(dirname):
                for filename in filenames:
                    # Removing the starting dot from extension
                    if os.path.splitext(filename)[1][1:] in self._extensions:
                        self.addFile(os.path.join(dirpath, filename))
        else:
            filenames = os.listdir(dirname)
            filenames = [
                os.path.join(dirname, filename) for filename in filenames
                if os.path.splitext(filename)[1][1:] in self._extensions
            ]
            self.addFiles(filenames)

    def removeFile(self, filename):
        """
        Remove the given 'filename' from the watched file list.
        Tip: make sure to use relative or absolute path according to how you add this file.
        """
        if filename in self._watchedFiles:
            self._watchedFiles.remove(filename)
        if self._watching:
            self._fileWatcher.removePath(filename)

    def getRegisteredFiles(self):
        """ Returns the list of watched files """
        return self._watchedFiles

    @Slot(str)
    def onFileChanged(self, filepath):
        """ Handle changes in a watched file. """
        if filepath not in self._watchedFiles:
            # could happen if a file has just been reloaded
            # and has not been re-added yet to the watched files
            return

        if self._verbose:
            print("Source file changed : ", filepath)
        # Clear the QQuickEngine cache
        self.clearComponentCache()
        # Remove the modified file from the watched list
        self.removeFile(filepath)
        cptTry = 0

        # Make sure file is available before doing anything
        # NOTE: useful to handle editors (Qt Creator) that deletes the source file and
        #       creates a new one when saving
        while not os.path.exists(filepath) and cptTry < 10:
            time.sleep(0.1)
            cptTry += 1

        self.reload()

        # Finally, re-add the modified file to the watch system
        # after a short cooldown to avoid multiple consecutive reloads
        QTimer.singleShot(200, lambda: self.addFile(filepath))

    def reload(self):
        print("Reloading {}".format(self._sourceFile))
        self.load(self._sourceFile)
Пример #3
0
class MainWindow(QMainWindow):

    def __init__(self, app, parent=None):
        super(MainWindow, self).__init__(parent)
        self.imagesDir = app.dir + '/images/'
        self.setWindowIcon(QIcon(self.imagesDir + 'icon.png'))
        self.path = ''

        self.settings = QSettings()
        self.lastDir = self.settings.value('lastDir', '')

        self.setMinimumWidth(540)

        self.supportedFormats = []
        for f in QImageReader.supportedImageFormats():
            self.supportedFormats.append(str(f.data(), encoding="utf-8"))

        self.fileWatcher = QFileSystemWatcher()
        self.fileWatcher.fileChanged.connect(self.fileChanged)

        # widgets
        self.showPixmapWidget = None

        self.tileWidthSpinBox = QSpinBox()
        self.tileWidthSpinBox.setValue(16)
        self.tileWidthSpinBox.setFixedWidth(50)
        self.tileWidthSpinBox.setMinimum(1)

        self.tileHeightSpinBox = QSpinBox()
        self.tileHeightSpinBox.setValue(16)
        self.tileHeightSpinBox.setFixedWidth(50)
        self.tileHeightSpinBox.setMinimum(1)

        self.paddingSpinBox = QSpinBox()
        self.paddingSpinBox.setFixedWidth(50)
        self.paddingSpinBox.setMinimum(1)

        self.transparentCheckbox = QCheckBox("Transparent")
        self.transparentCheckbox.setChecked(True)
        self.transparentCheckbox.stateChanged.connect(self.transparentChanged)

        self.backgroundColorEdit = ColorEdit()
        self.backgroundColorEdit.setEnabled(False)
        self.backgroundColorLabel = QLabel("Background color:")
        self.backgroundColorLabel.setEnabled(False)

        self.forcePotCheckBox = QCheckBox("Force PoT")
        self.forcePotCheckBox.setChecked(True)
        self.forcePotCheckBox.stateChanged.connect(self.forcePotChanged)

        self.reorderTilesCheckBox = QCheckBox("Reorder tiles")

        self.generateAndExportButton = QPushButton("Generate and export")
        self.generateAndExportButton.setFixedHeight(32)
        self.generateAndExportButton.clicked.connect(self.generateAndExportClicked)
        self.generateAndExportButton.setEnabled(False)

        self.pixmapWidget = PixmapWidget()
        self.pixmapWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.pixmapWidget.setPixmap(self.createDropTextPixmap())
        self.pixmapWidget.dropSignal.connect(self.fileDropped)
        self.pixmapWidget.setMinimumHeight(300)

        # load settings
        self.tileWidthSpinBox.setValue(int(self.settings.value('tileWidth', 16)))
        self.tileHeightSpinBox.setValue(int(self.settings.value('tileHeight', 16)))
        self.paddingSpinBox.setValue(int(self.settings.value('padding', 1)))
        self.forcePotCheckBox.setChecked(True if self.settings.value('forcePot', 'true') == 'true' else False)
        self.reorderTilesCheckBox.setChecked(True if self.settings.value('reorderTiles', 'false') == 'true' else False)
        self.transparentCheckbox.setChecked(True if self.settings.value('transparent', 'false') == 'true' else False)
        self.backgroundColorEdit.setColorText(str(self.settings.value('backgroundColor', '#FF00FF')))
        self.restoreGeometry(QByteArray(self.settings.value('MainWindow/geometry')))
        self.restoreState(QByteArray(self.settings.value('MainWindow/windowState')))

        # layout
        hl1 = QHBoxLayout()
        hl1.setContentsMargins(5, 5, 5, 5)
        hl1.addWidget(QLabel("Tile width:"))
        hl1.addSpacing(5)
        hl1.addWidget(self.tileWidthSpinBox)
        hl1.addSpacing(15)
        hl1.addWidget(QLabel("Tile height:"))
        hl1.addSpacing(5)
        hl1.addWidget(self.tileHeightSpinBox)
        hl1.addSpacing(15)
        hl1.addWidget(QLabel("Padding:"))
        hl1.addSpacing(5)
        hl1.addWidget(self.paddingSpinBox)
        hl1.addSpacing(15)
        hl1.addWidget(self.forcePotCheckBox)
        hl1.addSpacing(15)
        hl1.addWidget(self.reorderTilesCheckBox)
        hl1.addStretch()

        hl2 = QHBoxLayout()
        hl2.setContentsMargins(5, 5, 5, 5)
        hl2.addWidget(self.transparentCheckbox)
        hl2.addSpacing(15)
        hl2.addWidget(self.backgroundColorLabel)
        hl2.addSpacing(5)
        hl2.addWidget(self.backgroundColorEdit)
        hl2.addStretch()

        hl3 = QHBoxLayout()
        hl3.setContentsMargins(5, 5, 5, 5)
        hl3.addWidget(self.generateAndExportButton)

        vl = QVBoxLayout()
        vl.setContentsMargins(0, 0, 0, 0)
        vl.setSpacing(0)
        vl.addLayout(hl1)
        vl.addLayout(hl2)
        vl.addWidget(self.pixmapWidget)
        vl.addLayout(hl3)

        w = QWidget()
        w.setLayout(vl)
        self.setCentralWidget(w)

        self.setTitle()

    def setTitle(self):
        p = ' - ' + os.path.basename(self.path) if self.path else ''
        self.setWindowTitle(QCoreApplication.applicationName() + ' ' + QCoreApplication.applicationVersion() + p)

    def createDropTextPixmap(self):
        pixmap = QPixmap(481, 300)
        pixmap.fill(QColor("#333333"))
        painter = QPainter(pixmap)
        font = QFont("Arial")
        font.setPixelSize(28)
        font.setBold(True)
        fm = QFontMetrics(font)
        painter.setFont(font)
        painter.setPen(QPen(QColor("#888888"), 1))
        text = "Drop the tileset image here"
        x = (pixmap.width()-fm.width(text))/2
        y = (pixmap.height()+fm.height())/2
        painter.drawText(x, y, text)
        del painter
        return pixmap

    def fileDropped(self, path):
        path = str(path)
        name, ext = os.path.splitext(path)
        ext = ext[1:]
        if not ext in self.supportedFormats:
            QMessageBox.warning(self, "Warning", "The dropped file is not supported")
            return
        pixmap = QPixmap(path)
        if pixmap.isNull():
            QMessageBox.warning(self, "Warning", "Can't load the image")
            return
        if self.path:
            self.fileWatcher.removePath(self.path)
        self.path = path
        self.fileWatcher.addPath(self.path)
        self.pixmapWidget.setPixmap(pixmap)
        self.generateAndExportButton.setEnabled(True)
        self.setTitle()
        self.activateWindow()

    def fileChanged(self, path):
        #self.fileDropped(path)
        pass

    def transparentChanged(self):
        e = self.transparentCheckbox.isChecked()
        self.backgroundColorEdit.setEnabled(not e)
        self.backgroundColorLabel.setEnabled(not e)

    def forcePotChanged(self):
        e = self.forcePotCheckBox.isChecked()
        self.reorderTilesCheckBox.setEnabled(e)

    def generateAndExportClicked(self):

        g = Generator()
        g.tileWidth = self.tileWidthSpinBox.value()
        g.tileHeight = self.tileHeightSpinBox.value()
        g.forcePot = self.forcePotCheckBox.isChecked()
        g.isTransparent = self.transparentCheckbox.isChecked()
        g.bgColor = self.backgroundColorEdit.getColor()
        g.reorder = self.reorderTilesCheckBox.isChecked()
        g.padding = self.paddingSpinBox.value()

        target = g.create(self.pixmapWidget.pixmap);

        # export
        self.lastDir = os.path.dirname(self.path)
        targetPath = QFileDialog.getSaveFileName(self, 'Export', self.lastDir, 'PNG (*.png)')
        if targetPath:
            target.save(targetPath[0])
            showPixmap = QPixmap.fromImage(target)
            if self.showPixmapWidget:
                self.showPixmapWidget.deleteLater()
                del self.showPixmapWidget
            self.showPixmapWidget = PixmapWidget()
            self.showPixmapWidget.setWindowIcon(self.windowIcon())
            self.showPixmapWidget.setWindowTitle(os.path.basename(targetPath[0]))
            self.showPixmapWidget.resize(showPixmap.width(), showPixmap.height())
            self.showPixmapWidget.setPixmap(showPixmap)
            self.showPixmapWidget.show()

    def closeEvent(self, event):
        if self.showPixmapWidget:
            self.showPixmapWidget.close()

        # save settings
        self.settings.setValue('tileWidth', self.tileWidthSpinBox.value())
        self.settings.setValue('tileHeight', self.tileHeightSpinBox.value())
        self.settings.setValue('padding', self.paddingSpinBox.value())
        self.settings.setValue('forcePot', self.forcePotCheckBox.isChecked())
        self.settings.setValue('reorderTiles', self.reorderTilesCheckBox.isChecked())
        self.settings.setValue('transparent', self.transparentCheckbox.isChecked())
        self.settings.setValue('backgroundColor', self.backgroundColorEdit.getColor().name())
        self.settings.setValue('lastDir', self.lastDir)
        self.settings.setValue('MainWindow/geometry', self.saveGeometry())
        self.settings.setValue('MainWindow/windowState', self.saveState())

        super(MainWindow, self).closeEvent(event)