First draft-implementation of extensions (#1403)

At the moment only themes are supported and some functionality is not
available yet (disabling an extension, uninstalling it, etc.)
This commit is contained in:
David Capello 2017-06-10 15:02:39 -03:00
parent fc7f96149f
commit 27c4d832bf
19 changed files with 483 additions and 33 deletions

3
.gitmodules vendored
View File

@ -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

View File

@ -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": "." }
]
}
}

View File

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -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

View File

@ -16,6 +16,7 @@
<listitem text="@.section_guides_and_slices" value="section_guides_and_slices" />
<listitem text="@.section_undo" value="section_undo" />
<listitem text="@.section_theme" value="section_theme" />
<listitem text="@.section_extensions" value="section_extensions" />
<listitem text="@.section_experimental" value="section_experimental" />
</listbox>
</view>
@ -270,6 +271,20 @@
</hbox>
</vbox>
<!-- Extensions -->
<vbox id="section_extensions">
<view expansive="true" maxsize="true">
<listbox id="extensions_list" />
</view>
<hbox>
<button id="new_extension" text="@.new_extension" minwidth="60" />
<boxfiller />
<button id="disable_extension" text="@.disable_extension" minwidth="60" />
<button id="uninstall_extension" text="@.uninstall_extension" minwidth="60" />
<button id="open_extension_folder" text="@.open_extension_folder" minwidth="60" />
</hbox>
</vbox>
<!-- Experimental -->
<vbox id="section_experimental">
<separator text="@.user_interface" horizontal="true" />

View File

@ -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/)
```

View File

@ -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)

View File

@ -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<CliDelegate> 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();

View File

@ -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() {

View File

@ -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<void>(&OptionsWindow::onSelectTheme, this));
openThemeFolder()->Click.connect(base::Bind<void>(&OptionsWindow::onOpenThemeFolder, this));
// Extensions buttons
extensionsList()->Change.connect(base::Bind<void>(&OptionsWindow::onExtensionChange, this));
newExtension()->Click.connect(base::Bind<void>(&OptionsWindow::onNewExtension, this));
disableExtension()->Click.connect(base::Bind<void>(&OptionsWindow::onDisableExtension, this));
uninstallExtension()->Click.connect(base::Bind<void>(&OptionsWindow::onUninstallExtension, this));
openExtensionFolder()->Click.connect(base::Bind<void>(&OptionsWindow::onOpenExtensionFolder, this));
// Apply button
buttonApply()->Click.connect(base::Bind<void>(&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<ThemeItem*>(themeList()->getSelectedChild());
selectTheme()->setEnabled(item && item->canSelect());
@ -571,6 +627,47 @@ private:
item->openFolder();
}
void onExtensionChange() {
ExtensionItem* item = dynamic_cast<ExtensionItem*>(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<ExtensionItem*>(extensionsList()->getSelectedChild());
if (item) {
item->enable(!item->isEnabled());
onExtensionChange();
}
}
void onUninstallExtension() {
ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
if (item) {
item->uninstall();
onExtensionChange();
}
}
void onOpenExtensionFolder() {
ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
if (item)
item->openFolder();
}
void onCursorColorType() {
switch (cursorColorType()->getSelectedItemIndex()) {
case 0:

173
src/app/extensions.cpp Normal file
View File

@ -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> 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

85
src/app/extensions.h Normal file
View File

@ -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 <map>
#include <string>
#include <vector>
namespace app {
class Extension {
public:
Extension(const std::string& path,
const std::string& name,
const std::string& displayName,
const bool isEnabled,
const bool isBuiltinExtension);
const std::string& path() const { return m_path; }
const std::string& name() const { return m_name; }
const std::string& displayName() const { return m_displayName; }
bool isEnabled() const { return m_isEnabled; }
bool isInstalled() const { return m_isInstalled; }
bool canBeDisabled() const { return m_isEnabled; }
bool canBeUninstalled() const { return !m_isBuiltinExtension; }
void enable(const bool state);
void uninstall();
obs::signal<void(Extension*, bool)> Enable;
obs::signal<void(Extension*)> Disable;
obs::signal<void(Extension*)> Uninstall;
private:
std::string m_path;
std::string m_name;
std::string m_displayName;
bool m_isEnabled;
bool m_isInstalled;
bool m_isBuiltinExtension;
};
class Extensions {
public:
typedef std::vector<Extension*> List;
typedef List::iterator iterator;
Extensions();
~Extensions();
iterator begin() { return m_extensions.begin(); }
iterator end() { return m_extensions.end(); }
void enableExtension(Extension* extension);
void disableExtension(Extension* extension);
void uninstallExtension(Extension* extension);
void installCompressedExtension(const std::string& zipFn);
std::string themePath(const std::string& themeId);
private:
Extension* loadExtension(const std::string& path,
const std::string& fullPackageFilename,
const bool isBuiltinExtension);
List m_extensions;
// Key=theme id, Value=theme path
std::map<std::string, std::string> m_builtinThemes;
std::map<std::string, std::string> m_userThemes;
};
} // namespace app
#endif

View File

@ -8,7 +8,11 @@
#include "config.h"
#endif
#include "app/ui/skin/skin_theme.h"
#include "app/app.h"
#include "app/console.h"
#include "app/extensions.h"
#include "app/font_path.h"
#include "app/modules/gui.h"
#include "app/pref/preferences.h"
@ -18,7 +22,6 @@
#include "app/ui/skin/font_data.h"
#include "app/ui/skin/skin_property.h"
#include "app/ui/skin/skin_slider_property.h"
#include "app/ui/skin/skin_theme.h"
#include "app/xml_document.h"
#include "app/xml_exception.h"
#include "base/bind.h"
@ -48,6 +51,7 @@ namespace skin {
using namespace gfx;
using namespace ui;
// TODO For backward compatibility, in future versions we should remove this (extensions are preferred)
const char* SkinTheme::kThemesFolderName = "themes";
static const char* g_cursor_names[kCursorTypes] = {
@ -248,32 +252,31 @@ void SkinTheme::loadFontData()
}
}
void SkinTheme::loadAll(const std::string& skinId)
void SkinTheme::loadAll(const std::string& themeId)
{
LOG("THEME: Loading theme %s\n", skinId.c_str());
LOG("THEME: Loading theme %s\n", themeId.c_str());
if (m_fonts.empty())
loadFontData();
loadSheet(skinId);
loadXml(skinId);
m_path = themePath(themeId);
if (m_path.empty())
throw base::Exception("Theme %s not found", themeId.c_str());
loadSheet();
loadXml();
}
void SkinTheme::loadSheet(const std::string& skinId)
void SkinTheme::loadSheet()
{
// Load the skin sheet
std::string sheet_filename(themeFileName(skinId, "sheet.png"));
ResourceFinder rf;
rf.includeDataDir(sheet_filename.c_str());
if (!rf.findFirst())
throw base::Exception("File %s not found", sheet_filename.c_str());
std::string sheet_filename(base::join_path(m_path, "sheet.png"));
try {
if (m_sheet) {
m_sheet->dispose();
m_sheet = nullptr;
}
m_sheet = she::instance()->loadRgbaSurface(rf.filename().c_str());
m_sheet = she::instance()->loadRgbaSurface(sheet_filename.c_str());
if (m_sheet)
m_sheet->applyScale(guiscale());
}
@ -282,18 +285,14 @@ void SkinTheme::loadSheet(const std::string& skinId)
}
}
void SkinTheme::loadXml(const std::string& skinId)
void SkinTheme::loadXml()
{
const int scale = guiscale();
// Load the skin XML
std::string xml_filename(themeFileName(skinId, "theme.xml"));
ResourceFinder rf;
rf.includeDataDir(xml_filename.c_str());
if (!rf.findFirst())
return;
std::string xml_filename(base::join_path(m_path, "theme.xml"));
XmlDocumentRef doc = open_xml(rf.filename());
XmlDocumentRef doc = open_xml(xml_filename);
TiXmlHandle handle(doc.get());
// Load fonts
@ -304,7 +303,7 @@ void SkinTheme::loadXml(const std::string& skinId)
.FirstChild("font").ToElement();
while (xmlFont) {
const char* idStr = xmlFont->Attribute("id");
FontData* fontData = load_font(m_fonts, xmlFont, rf.filename());
FontData* fontData = load_font(m_fonts, xmlFont, xml_filename);
if (idStr && fontData) {
std::string id(idStr);
LOG(VERBOSE) << "THEME: Loading theme font '" << id << "\n";
@ -1607,11 +1606,22 @@ void SkinTheme::paintProgressBar(ui::Graphics* g, const gfx::Rect& rc0, double p
g->fillRect(colors.background(), gfx::Rect(rc.x+u, rc.y, rc.w-u, rc.h));
}
std::string SkinTheme::themeFileName(const std::string& skinId,
const std::string& fileName) const
std::string SkinTheme::themePath(const std::string& themeId) const
{
std::string path = base::join_path(SkinTheme::kThemesFolderName, skinId);
path = base::join_path(path, fileName);
// First we try to find the theme on an extensions
std::string path = App::instance()->extensions().themePath(themeId);
if (path.empty()) {
// Then we try a theme in the old themes/ folder
path = base::join_path(SkinTheme::kThemesFolderName, themeId);
path = base::join_path(path, "theme.xml");
ResourceFinder rf;
rf.includeDataDir(path.c_str());
if (!rf.findFirst())
return std::string();
path = base::get_file_path(rf.filename());
}
return path;
}

View File

@ -129,9 +129,9 @@ namespace app {
private:
void loadFontData();
void loadAll(const std::string& skinId);
void loadSheet(const std::string& skinId);
void loadXml(const std::string& skinId);
void loadAll(const std::string& themeId);
void loadSheet();
void loadXml();
she::Surface* sliceSheet(she::Surface* sur, const gfx::Rect& bounds);
gfx::Color getWidgetBgColor(ui::Widget* widget);
@ -140,9 +140,9 @@ namespace app {
int selected_offset, int mnemonic);
void drawEntryText(ui::Graphics* g, ui::Entry* widget);
std::string themeFileName(const std::string& skinId,
const std::string& fileName) const;
std::string themePath(const std::string& themeId) const;
std::string m_path;
she::Surface* m_sheet;
std::map<std::string, SkinPartPtr> m_parts_by_id;
std::map<std::string, gfx::Color> m_colors_by_id;

View File

@ -107,3 +107,7 @@ if(NOT USE_SHARED_CMARK)
add_custom_target(copy_cmark_headers DEPENDS ${copy_cmark_headers})
add_dependencies(libcmark_static copy_cmark_headers)
endif()
# JSON
set(TAOCPP_JSON_BUILD_TESTS OFF CACHE BOOL "Build test programs")
add_subdirectory(json)

1
third_party/json vendored Submodule

@ -0,0 +1 @@
Subproject commit 450705891c39dfc38de0ef04e239c51f1d98855d