diff --git a/.gitmodules b/.gitmodules
index 07b5d8b55..e01b94be2 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -45,3 +45,6 @@
[submodule "third_party/harfbuzz"]
path = third_party/harfbuzz
url = https://github.com/aseprite/harfbuzz.git
+[submodule "third_party/json"]
+ path = third_party/json
+ url = https://github.com/aseprite/json.git
diff --git a/data/themes/default/LICENSE.txt b/data/extensions/aseprite-theme/LICENSE.txt
similarity index 100%
rename from data/themes/default/LICENSE.txt
rename to data/extensions/aseprite-theme/LICENSE.txt
diff --git a/data/extensions/aseprite-theme/package.json b/data/extensions/aseprite-theme/package.json
new file mode 100644
index 000000000..2f010494a
--- /dev/null
+++ b/data/extensions/aseprite-theme/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "aseprite-theme",
+ "displayName": "Aseprite Default Theme",
+ "description": "Default Aseprite Pixel-Art Theme",
+ "version": "1.0",
+ "author": { "name": "David Capello", "email": "davidcapello@gmail.com", "url": "http://davidcapello.com/" },
+ "contributors": [
+ { "name": "Ilija Melentijevic", "url": "http://ilkke.blogspot.com/" }
+ ],
+ "publisher": "aseprite",
+ "license": "CC-BY-4.0",
+ "categories": [
+ "Themes"
+ ],
+ "contributes": {
+ "themes": [
+ { "id": "default", "path": "." }
+ ]
+ }
+}
diff --git a/data/themes/default/sheet.aseprite-data b/data/extensions/aseprite-theme/sheet.aseprite-data
similarity index 100%
rename from data/themes/default/sheet.aseprite-data
rename to data/extensions/aseprite-theme/sheet.aseprite-data
diff --git a/data/themes/default/sheet.png b/data/extensions/aseprite-theme/sheet.png
similarity index 100%
rename from data/themes/default/sheet.png
rename to data/extensions/aseprite-theme/sheet.png
diff --git a/data/themes/default/theme.xml b/data/extensions/aseprite-theme/theme.xml
similarity index 100%
rename from data/themes/default/theme.xml
rename to data/extensions/aseprite-theme/theme.xml
diff --git a/data/strings/en.ini b/data/strings/en.ini
index 1cc838578..d805e7090 100644
--- a/data/strings/en.ini
+++ b/data/strings/en.ini
@@ -268,6 +268,7 @@ section_grid = Grid
section_guides_and_slices = Guides && Slices
section_undo = Undo
section_theme = Theme
+section_extensions = Extensions
section_experimental = Experimental
general = General
screen_scaling = Screen Scaling:
@@ -391,6 +392,10 @@ undo_allow_nonlinear_history = Allow non-linear history
available_themes = Available Themes
select_theme = &Select
open_theme_folder = Open &Folder
+new_extension = New
+disable_extension = Disable
+uninstall_extension = Uninstall
+open_extension_folder = Open &Folder
user_interface = User Interface
native_file_dialog = Use native file dialog
flash_selected_layer = Flash layer when it is selected
diff --git a/data/widgets/options.xml b/data/widgets/options.xml
index f1f35b070..90b7a3a5f 100644
--- a/data/widgets/options.xml
+++ b/data/widgets/options.xml
@@ -16,6 +16,7 @@
+
@@ -270,6 +271,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/LICENSES.md b/docs/LICENSES.md
index 92cf1728c..0a22ffce1 100644
--- a/docs/LICENSES.md
+++ b/docs/LICENSES.md
@@ -894,6 +894,32 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
+# [The Art of C++ / JSON](https://github.com/taocpp/json/)
+
+```
+The MIT License (MIT)
+
+Copyright (c) 2015-2017 Dr. Colin Hirsch and Daniel Frey
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+```
+
# [tinyxml](http://www.grinninglizard.com/tinyxml/)
```
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index 35293833f..f9c7ed3e9 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -363,6 +363,7 @@ add_library(app-lib
document_range.cpp
document_range_ops.cpp
document_undo.cpp
+ extensions.cpp
extra_cel.cpp
file/file.cpp
file/file_data.cpp
@@ -539,7 +540,8 @@ target_link_libraries(app-lib
${WEBP_LIBRARIES}
${ZLIB_LIBRARIES}
${FREETYPE_LIBRARIES}
- ${HARFBUZZ_LIBRARIES})
+ ${HARFBUZZ_LIBRARIES}
+ taocpp-json)
if(ENABLE_SCRIPTING)
target_link_libraries(app-lib script-lib)
diff --git a/src/app/app.cpp b/src/app/app.cpp
index 413c8d65a..978b027cc 100644
--- a/src/app/app.cpp
+++ b/src/app/app.cpp
@@ -19,6 +19,7 @@
#include "app/commands/commands.h"
#include "app/console.h"
#include "app/crash/data_recovery.h"
+#include "app/extensions.h"
#include "app/file/file.h"
#include "app/file/file_formats_manager.h"
#include "app/file_system.h"
@@ -96,6 +97,7 @@ public:
RecentFiles m_recent_files;
InputChain m_inputChain;
clipboard::ClipboardManager m_clipboardManager;
+ Extensions m_extensions;
// This is a raw pointer because we want to delete this explicitly.
app::crash::DataRecovery* m_recovery;
@@ -208,7 +210,7 @@ void App::initialize(const AppOptions& options)
ui::Manager::getDefault()->invalidate();
}
- // Procress options
+ // Process options
LOG("APP: Processing options...\n");
{
base::UniquePtr delegate;
@@ -413,6 +415,11 @@ Preferences& App::preferences() const
return m_coreModules->m_preferences;
}
+Extensions& App::extensions() const
+{
+ return m_modules->m_extensions;
+}
+
crash::DataRecovery* App::dataRecovery() const
{
return m_modules->recovery();
diff --git a/src/app/app.h b/src/app/app.h
index 55023339a..bf96679ad 100644
--- a/src/app/app.h
+++ b/src/app/app.h
@@ -1,5 +1,5 @@
// Aseprite
-// Copyright (C) 2001-2016 David Capello
+// Copyright (C) 2001-2017 David Capello
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
@@ -31,6 +31,7 @@ namespace app {
class BackupIndicator;
class ContextBar;
class Document;
+ class Extensions;
class INotificationDelegate;
class InputChain;
class LegacyModules;
@@ -81,6 +82,7 @@ namespace app {
ContextBar* contextBar() const;
Timeline* timeline() const;
Preferences& preferences() const;
+ Extensions& extensions() const;
crash::DataRecovery* dataRecovery() const;
AppBrushes& brushes() {
diff --git a/src/app/commands/cmd_options.cpp b/src/app/commands/cmd_options.cpp
index fe878eca7..1b4894796 100644
--- a/src/app/commands/cmd_options.cpp
+++ b/src/app/commands/cmd_options.cpp
@@ -11,6 +11,7 @@
#include "app/app.h"
#include "app/commands/command.h"
#include "app/context.h"
+#include "app/extensions.h"
#include "app/ini_file.h"
#include "app/launcher.h"
#include "app/pref/preferences.h"
@@ -34,6 +35,7 @@ namespace app {
static const char* kSectionBgId = "section_bg";
static const char* kSectionGridId = "section_grid";
static const char* kSectionThemeId = "section_theme";
+static const char* kSectionExtensionsId = "section_extensions";
using namespace ui;
@@ -64,6 +66,36 @@ class OptionsWindow : public app::gen::Options {
std::string m_path;
std::string m_name;
};
+
+ class ExtensionItem : public ListItem {
+ public:
+ ExtensionItem(Extension* extension)
+ : ListItem(extension->displayName())
+ , m_extension(extension) {
+ }
+
+ bool isEnabled() const { return m_extension->isEnabled(); }
+ bool isInstalled() const { return m_extension->isInstalled(); }
+ bool canBeDisabled() const { return m_extension->canBeDisabled(); }
+ bool canBeUninstalled() const { return m_extension->canBeUninstalled(); }
+
+ void enable(bool state) {
+ m_extension->enable(state);
+ }
+
+ void uninstall() {
+ ASSERT(canBeUninstalled());
+ m_extension->uninstall();
+ }
+
+ void openFolder() const {
+ app::launcher::open_folder(m_extension->path());
+ }
+
+ private:
+ Extension* m_extension;
+ };
+
public:
OptionsWindow(Context* context, int& curSection)
: m_pref(Preferences::instance())
@@ -254,6 +286,13 @@ public:
selectTheme()->Click.connect(base::Bind(&OptionsWindow::onSelectTheme, this));
openThemeFolder()->Click.connect(base::Bind(&OptionsWindow::onOpenThemeFolder, this));
+ // Extensions buttons
+ extensionsList()->Change.connect(base::Bind(&OptionsWindow::onExtensionChange, this));
+ newExtension()->Click.connect(base::Bind(&OptionsWindow::onNewExtension, this));
+ disableExtension()->Click.connect(base::Bind(&OptionsWindow::onDisableExtension, this));
+ uninstallExtension()->Click.connect(base::Bind(&OptionsWindow::onUninstallExtension, this));
+ openExtensionFolder()->Click.connect(base::Bind(&OptionsWindow::onOpenExtensionFolder, this));
+
// Apply button
buttonApply()->Click.connect(base::Bind(&OptionsWindow::saveConfig, this));
@@ -408,6 +447,9 @@ private:
// Load themes
else if (item->getValue() == kSectionThemeId)
loadThemes();
+ // Load extension
+ else if (item->getValue() == kSectionExtensionsId)
+ loadExtensions();
}
void onChangeBgScope() {
@@ -548,6 +590,20 @@ private:
themeList()->layout();
}
+ void loadExtensions() {
+ // Extensions already loaded
+ if (extensionsList()->getItemsCount() > 0)
+ return;
+
+ for (auto extension : App::instance()->extensions()) {
+ ExtensionItem* item = new ExtensionItem(extension);
+ extensionsList()->addChild(item);
+ }
+
+ onExtensionChange();
+ extensionsList()->layout();
+ }
+
void onThemeChange() {
ThemeItem* item = dynamic_cast(themeList()->getSelectedChild());
selectTheme()->setEnabled(item && item->canSelect());
@@ -571,6 +627,47 @@ private:
item->openFolder();
}
+ void onExtensionChange() {
+ ExtensionItem* item = dynamic_cast(extensionsList()->getSelectedChild());
+ if (item && item->isInstalled()) {
+ disableExtension()->setText(item->isEnabled() ? "Disable": "Enable");
+ disableExtension()->setEnabled(item->isEnabled() ? item->canBeDisabled(): true);
+ uninstallExtension()->setEnabled(item->canBeUninstalled());
+ openExtensionFolder()->setEnabled(true);
+ }
+ else {
+ disableExtension()->setEnabled(false);
+ uninstallExtension()->setEnabled(false);
+ openExtensionFolder()->setEnabled(false);
+ }
+ }
+
+ void onNewExtension() {
+ // TODO open dialog to select a .zip file with the extension and uncompress it in the user folder
+ }
+
+ void onDisableExtension() {
+ ExtensionItem* item = dynamic_cast(extensionsList()->getSelectedChild());
+ if (item) {
+ item->enable(!item->isEnabled());
+ onExtensionChange();
+ }
+ }
+
+ void onUninstallExtension() {
+ ExtensionItem* item = dynamic_cast(extensionsList()->getSelectedChild());
+ if (item) {
+ item->uninstall();
+ onExtensionChange();
+ }
+ }
+
+ void onOpenExtensionFolder() {
+ ExtensionItem* item = dynamic_cast(extensionsList()->getSelectedChild());
+ if (item)
+ item->openFolder();
+ }
+
void onCursorColorType() {
switch (cursorColorType()->getSelectedItemIndex()) {
case 0:
diff --git a/src/app/extensions.cpp b/src/app/extensions.cpp
new file mode 100644
index 000000000..1e3adc196
--- /dev/null
+++ b/src/app/extensions.cpp
@@ -0,0 +1,173 @@
+// Aseprite
+// Copyright (C) 2017 David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/extensions.h"
+
+#include "app/resource_finder.h"
+#include "base/fs.h"
+#include "base/unique_ptr.h"
+
+#include "tao/json.hpp"
+
+namespace app {
+
+Extension::Extension(const std::string& path,
+ const std::string& name,
+ const std::string& displayName,
+ const bool isEnabled,
+ const bool isBuiltinExtension)
+ : m_path(path)
+ , m_name(name)
+ , m_displayName(displayName)
+ , m_isEnabled(isEnabled)
+ , m_isInstalled(true)
+ , m_isBuiltinExtension(isBuiltinExtension)
+{
+}
+
+void Extension::enable(const bool state)
+{
+ // Do nothing
+ if (m_isEnabled == state)
+ return;
+
+ // TODO save the enable/disable state on configuration or other place
+
+ m_isEnabled = state;
+ Enable(this, state);
+}
+
+void Extension::uninstall()
+{
+ if (!m_isInstalled)
+ return;
+
+ // TODO remove files if the extension is not built-in
+
+ m_isEnabled = false;
+ m_isInstalled = false;
+ Uninstall(this);
+}
+
+Extensions::Extensions()
+{
+ ResourceFinder rf;
+ rf.includeDataDir("extensions");
+
+ // Load extensions from data/ directory on all possible locations
+ // (installed folder and user folder)
+ while (rf.next()) {
+ auto extensionsDir = rf.filename();
+
+ if (base::is_directory(extensionsDir)) {
+ for (auto fn : base::list_files(extensionsDir)) {
+ auto dir = base::join_path(extensionsDir, fn);
+ if (!base::is_directory(dir))
+ continue;
+
+ auto fullFn = base::join_path(dir, "package.json");
+ LOG("EXT: Loading extension '%s'...\n", fullFn.c_str());
+ if (!base::is_file(fullFn)) {
+ LOG("EXT: File '%s' not found\n", fullFn.c_str());
+ continue;
+ }
+
+ bool isBuiltinExtension = true; // TODO check if the extension is in Aseprite installation folder or the user folder
+
+ try {
+ Extension* extension = loadExtension(dir, fullFn, isBuiltinExtension);
+ m_extensions.push_back(extension);
+ }
+ catch (const std::exception& ex) {
+ LOG("EXT: Error loading JSON file: %s\n",
+ ex.what());
+ }
+ }
+ }
+ }
+}
+
+Extensions::~Extensions()
+{
+ for (auto ext : m_extensions)
+ delete ext;
+}
+
+std::string Extensions::themePath(const std::string& themeId)
+{
+ auto it = m_userThemes.find(themeId);
+ if (it != m_userThemes.end())
+ return it->second;
+
+ it = m_builtinThemes.find(themeId);
+ if (it != m_builtinThemes.end())
+ return it->second;
+
+ return std::string();
+}
+
+Extension* Extensions::loadExtension(const std::string& path,
+ const std::string& fullPackageFilename,
+ const bool isBuiltinExtension)
+{
+ auto json = tao::json::parse_file(fullPackageFilename);
+ auto name = json["name"].get_string();
+ auto displayName = json["displayName"].get_string();
+
+ LOG("EXT: Extension '%s' loaded\n", name.c_str());
+
+ base::UniquePtr extension(
+ new Extension(path,
+ name,
+ displayName,
+ true, // TODO check if the extension is enabled in the configuration
+ isBuiltinExtension));
+
+ auto contributes = json["contributes"];
+ if (contributes.is_object()) {
+ auto themes = contributes["themes"];
+ if (themes.is_array()) {
+ for (const auto& theme : themes.get_array()) {
+ auto jsonThemeId = theme.at("id");
+ auto jsonThemePath = theme.at("path");
+
+ if (!jsonThemeId.is_string()) {
+ LOG("EXT: A theme doesn't have 'id' property\n");
+ }
+ else if (!jsonThemePath.is_string()) {
+ LOG("EXT: Theme '%s' doesn't have 'path' property\n",
+ jsonThemeId.get_string().c_str());
+ }
+ else {
+ std::string themeId = jsonThemeId.get_string();
+ std::string themePath = jsonThemePath.get_string();
+
+ // The path must be always relative to the extension
+ themePath = base::join_path(path, themePath);
+
+ LOG("EXT: New theme '%s' in '%s'\n",
+ themeId.c_str(),
+ themePath.c_str());
+
+ if (isBuiltinExtension) {
+ m_builtinThemes[themeId] = themePath;
+ }
+ else {
+ m_userThemes[themeId] = themePath;
+ }
+ }
+ }
+ }
+ }
+
+ return extension.release();
+}
+
+} // namespace app
diff --git a/src/app/extensions.h b/src/app/extensions.h
new file mode 100644
index 000000000..7b29f27c9
--- /dev/null
+++ b/src/app/extensions.h
@@ -0,0 +1,85 @@
+// Aseprite
+// Copyright (C) 2017 David Capello
+//
+// This program is distributed under the terms of
+// the End-User License Agreement for Aseprite.
+
+#ifndef APP_EXTENSIONS_H_INCLUDED
+#define APP_EXTENSIONS_H_INCLUDED
+#pragma once
+
+#include "obs/signal.h"
+
+#include