Implement Add/Uninstall extension (#1403)

- Added libarchive to uncompress .zip extensions
- Moved ListItem painting code to styles because we needed a selected+disabled state.
This commit is contained in:
David Capello 2017-06-12 12:38:53 -03:00
parent 950955787f
commit a9e688989f
16 changed files with 421 additions and 86 deletions

3
.gitmodules vendored
View File

@ -48,3 +48,6 @@
[submodule "third_party/json"]
path = third_party/json
url = https://github.com/aseprite/json.git
[submodule "third_party/libarchive"]
path = third_party/libarchive
url = https://github.com/aseprite/libarchive.git

View File

@ -890,5 +890,14 @@
<border part="simple_color_border" />
<border part="simple_color_selected" state="selected" />
</style>
<style id="list_item" border="1">
<background color="listitem_normal_face" />
<background color="listitem_selected_face" state="selected" />
<background color="face" state="disabled" />
<background color="listitem_selected_face" state="selected disabled" />
<text color="listitem_normal_text" align="left middle" x="1" />
<text color="listitem_selected_text" align="left middle" x="1" state="selected" />
<text color="disabled" align="left middle" x="1" state="disabled" />
</style>
</styles>
</theme>

View File

@ -392,7 +392,7 @@ undo_allow_nonlinear_history = Allow non-linear history
available_themes = Available Themes
select_theme = &Select
open_theme_folder = Open &Folder
new_extension = New
add_extension = Add Extension
disable_extension = Disable
uninstall_extension = Uninstall
open_extension_folder = Open &Folder

View File

@ -277,7 +277,7 @@
<listbox id="extensions_list" />
</view>
<hbox>
<button id="new_extension" text="@.new_extension" minwidth="60" />
<button id="add_extension" text="@.add_extension" minwidth="60" />
<boxfiller />
<button id="disable_extension" text="@.disable_extension" minwidth="60" />
<button id="uninstall_extension" text="@.uninstall_extension" minwidth="60" />

View File

@ -527,6 +527,70 @@ ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO
PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
```
# [libarchive](http://www.libarchive.org/)
```
The libarchive distribution as a whole is Copyright by Tim Kientzle
and is subject to the copyright notice reproduced at the bottom of
this file.
Each individual file in this distribution should have a clear
copyright/licensing statement at the beginning of the file. If any do
not, please let me know and I will rectify it. The following is
intended to summarize the copyright status of the individual files;
the actual statements in the files are controlling.
* Except as listed below, all C sources (including .c and .h files)
and documentation files are subject to the copyright notice reproduced
at the bottom of this file.
* The following source files are also subject in whole or in part to
a 3-clause UC Regents copyright; please read the individual source
files for details:
libarchive/archive_entry.c
libarchive/archive_read_support_filter_compress.c
libarchive/archive_write_add_filter_compress.c
libarchive/mtree.5
* The following source files are in the public domain:
libarchive/archive_getdate.c
* The build files---including Makefiles, configure scripts,
and auxiliary scripts used as part of the compile process---have
widely varying licensing terms. Please check individual files before
distributing them to see if those restrictions apply to you.
I intend for all new source code to use the license below and hope over
time to replace code with other licenses with new implementations that
do use the license below. The varying licensing of the build scripts
seems to be an unavoidable mess.
Copyright (c) 2003-2009 <author(s)>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer
in this position and unchanged.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
# [libjpeg](http://www.ijg.org/)
```

View File

@ -541,7 +541,8 @@ target_link_libraries(app-lib
${ZLIB_LIBRARIES}
${FREETYPE_LIBRARIES}
${HARFBUZZ_LIBRARIES}
taocpp-json)
taocpp-json
archive_static)
if(ENABLE_SCRIPTING)
target_link_libraries(app-lib script-lib)

View File

@ -10,8 +10,10 @@
#include "app/app.h"
#include "app/commands/command.h"
#include "app/console.h"
#include "app/context.h"
#include "app/extensions.h"
#include "app/file_selector.h"
#include "app/ini_file.h"
#include "app/launcher.h"
#include "app/pref/preferences.h"
@ -72,23 +74,45 @@ class OptionsWindow : public app::gen::Options {
ExtensionItem(Extension* extension)
: ListItem(extension->displayName())
, m_extension(extension) {
setEnabled(extension->isEnabled());
}
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(); }
bool isEnabled() const {
ASSERT(m_extension);
return m_extension->isEnabled();
}
bool isInstalled() const {
ASSERT(m_extension);
return m_extension->isInstalled();
}
bool canBeDisabled() const {
ASSERT(m_extension);
return m_extension->canBeDisabled();
}
bool canBeUninstalled() const {
ASSERT(m_extension);
return m_extension->canBeUninstalled();
}
void enable(bool state) {
ASSERT(m_extension);
m_extension->enable(state);
setEnabled(m_extension->isEnabled());
}
void uninstall() {
ASSERT(m_extension);
ASSERT(canBeUninstalled());
m_extension->uninstall();
m_extension = nullptr;
}
void openFolder() const {
ASSERT(m_extension);
app::launcher::open_folder(m_extension->path());
}
@ -288,7 +312,7 @@ public:
// Extensions buttons
extensionsList()->Change.connect(base::Bind<void>(&OptionsWindow::onExtensionChange, this));
newExtension()->Click.connect(base::Bind<void>(&OptionsWindow::onNewExtension, this));
addExtension()->Click.connect(base::Bind<void>(&OptionsWindow::onAddExtension, 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));
@ -642,8 +666,28 @@ private:
}
}
void onNewExtension() {
// TODO open dialog to select a .zip file with the extension and uncompress it in the user folder
void onAddExtension() {
FileSelectorFiles filename;
if (!app::show_file_selector(
"Add Extension", "", "zip",
FileSelectorType::Open, filename))
return;
ASSERT(!filename.empty());
try {
Extension* extension =
App::instance()->extensions().installCompressedExtension(filename.front());
// Add the new extension in the listbox
ExtensionItem* item = new ExtensionItem(extension);
extensionsList()->addChild(item);
extensionsList()->selectChild(item);
extensionsList()->layout();
}
catch (std::exception& ex) {
Console::showException(ex);
}
}
void onDisableExtension() {
@ -656,9 +700,27 @@ private:
void onUninstallExtension() {
ExtensionItem* item = dynamic_cast<ExtensionItem*>(extensionsList()->getSelectedChild());
if (item) {
if (!item)
return;
if (ui::Alert::show(
"Warning"
"<<Do you really want to uninstall '%s' extension?"
"||&Yes||&No",
item->text().c_str()) != 1)
return;
try {
item->uninstall();
onExtensionChange();
// Remove the item from the list
extensionsList()->removeChild(item);
extensionsList()->layout();
item->deferDelete();
}
catch (std::exception& ex) {
Console::showException(ex);
}
}

View File

@ -10,14 +10,139 @@
#include "app/extensions.h"
#include "app/ini_file.h"
#include "app/pref/preferences.h"
#include "app/resource_finder.h"
#include "base/exception.h"
#include "base/file_handle.h"
#include "base/fs.h"
#include "base/unique_ptr.h"
#include "archive.h"
#include "archive_entry.h"
#include "tao/json.hpp"
#include <queue>
namespace app {
namespace {
class ReadArchive {
public:
ReadArchive(const std::string& filename)
: m_arch(nullptr), m_open(false) {
m_arch = archive_read_new();
archive_read_support_format_zip(m_arch);
m_file = base::open_file(filename, "rb");
if (!m_file)
throw base::Exception("Error loading file %s",
filename.c_str());
int err;
if ((err = archive_read_open_FILE(m_arch, m_file.get())))
throw base::Exception("Error uncompressing extension\n%s (%d)",
archive_error_string(m_arch), err);
m_open = true;
}
~ReadArchive() {
if (m_arch) {
if (m_open)
archive_read_close(m_arch);
archive_read_free(m_arch);
}
}
archive_entry* readEntry() {
archive_entry* entry;
int err = archive_read_next_header(m_arch, &entry);
if (err == ARCHIVE_EOF)
return nullptr;
if (err != ARCHIVE_OK)
throw base::Exception("Error uncompressing extension\n%s",
archive_error_string(m_arch));
return entry;
}
int copyDataTo(archive* out) {
const void* buf;
size_t size;
int64_t offset;
for (;;) {
int err = archive_read_data_block(m_arch, &buf, &size, &offset);
if (err == ARCHIVE_EOF)
break;
if (err != ARCHIVE_OK)
return err;
err = archive_write_data_block(out, buf, size, offset);
if (err != ARCHIVE_OK) {
throw base::Exception("Error writing data blocks\n%s (%d)",
archive_error_string(out), err);
return err;
}
}
return ARCHIVE_OK;
}
private:
base::FileHandle m_file;
archive* m_arch;
bool m_open;
};
class WriteArchive {
public:
WriteArchive(const std::string& outputDir)
: m_arch(nullptr)
, m_open(false)
, m_outputDir(outputDir) {
m_arch = archive_write_disk_new();
m_open = true;
}
~WriteArchive() {
if (m_arch) {
if (m_open)
archive_write_close(m_arch);
archive_write_free(m_arch);
}
}
void writeEntry(ReadArchive& in, archive_entry* entry) {
const std::string origFn = archive_entry_pathname(entry);
const std::string fullFn = base::join_path(m_outputDir, origFn);
archive_entry_set_pathname(entry, fullFn.c_str());
LOG("EXT: Uncompressing file <%s> to <%s>\n",
origFn.c_str(), fullFn.c_str());
int err = archive_write_header(m_arch, entry);
if (err != ARCHIVE_OK)
throw base::Exception("Error writing file into disk\n%s (%d)",
archive_error_string(m_arch), err);
in.copyDataTo(m_arch);
err = archive_write_finish_entry(m_arch);
if (err != ARCHIVE_OK)
throw base::Exception("Error saving the last part of a file entry in disk\n%s (%d)",
archive_error_string(m_arch), err);
}
private:
archive* m_arch;
bool m_open;
std::string m_outputDir;
};
} // anonymous namespace
Extension::Extension(const std::string& path,
const std::string& name,
const std::string& displayName,
@ -32,13 +157,34 @@ Extension::Extension(const std::string& path,
{
}
void Extension::addTheme(const std::string& id, const std::string& path)
{
m_themes[id] = path;
}
void Extension::addPalette(const std::string& id, const std::string& path)
{
m_palettes[id] = path;
}
bool Extension::canBeDisabled() const
{
return (m_isEnabled && !isCurrentTheme());
}
bool Extension::canBeUninstalled() const
{
return (!m_isBuiltinExtension && !isCurrentTheme());
}
void Extension::enable(const bool state)
{
// Do nothing
if (m_isEnabled == state)
return;
// TODO save the enable/disable state on configuration or other place
set_config_bool("extensions", m_name.c_str(), state);
flush_config_file();
m_isEnabled = state;
Enable(this, state);
@ -49,15 +195,57 @@ void Extension::uninstall()
if (!m_isInstalled)
return;
// TODO remove files if the extension is not built-in
ASSERT(canBeUninstalled());
if (!canBeUninstalled())
return;
TRACE("EXT: Uninstall extension '%s' from '%s'...\n",
m_name.c_str(), m_path.c_str());
// Remove all files inside the extension path
uninstallFiles(m_path);
ASSERT(!base::is_directory(m_path));
m_isEnabled = false;
m_isInstalled = false;
Uninstall(this);
}
void Extension::uninstallFiles(const std::string& path)
{
for (auto& item : base::list_files(path)) {
std::string fn = base::join_path(path, item);
if (base::is_file(fn)) {
TRACE("EXT: Deleting file '%s'\n", fn.c_str());
base::delete_file(fn);
}
else if (base::is_directory(fn)) {
uninstallFiles(fn);
}
}
TRACE("EXT: Deleting directory '%s'\n", path.c_str());
base::remove_directory(path);
}
bool Extension::isCurrentTheme() const
{
auto it = m_themes.find(Preferences::instance().theme.selected.defaultValue());
return (it != m_themes.end());
}
Extensions::Extensions()
{
// Create and get the user extensions directory
{
ResourceFinder rf2;
rf2.includeUserDir("data/extensions/.");
m_userExtensionsPath = rf2.getFirstOrCreateDefault();
m_userExtensionsPath = base::normalize_path(m_userExtensionsPath);
m_userExtensionsPath = base::get_file_path(m_userExtensionsPath);
LOG("EXT: User extensions path '%s'\n", m_userExtensionsPath.c_str());
}
ResourceFinder rf;
rf.includeDataDir("extensions");
@ -68,22 +256,24 @@ Extensions::Extensions()
if (base::is_directory(extensionsDir)) {
for (auto fn : base::list_files(extensionsDir)) {
auto dir = base::join_path(extensionsDir, fn);
const auto dir = base::join_path(extensionsDir, fn);
if (!base::is_directory(dir))
continue;
const bool isBuiltinExtension =
(m_userExtensionsPath != base::get_file_path(dir));
auto fullFn = base::join_path(dir, "package.json");
fullFn = base::normalize_path(fullFn);
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);
loadExtension(dir, fullFn, isBuiltinExtension);
}
catch (const std::exception& ex) {
LOG("EXT: Error loading JSON file: %s\n",
@ -102,20 +292,46 @@ Extensions::~Extensions()
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;
for (auto ext : m_extensions) {
auto it = ext->themes().find(themeId);
if (it != ext->themes().end())
return it->second;
}
return std::string();
}
const std::map<std::string, std::string>& Extensions::palettes() const
ExtensionItems Extensions::palettes() const
{
return m_palettes;
ExtensionItems palettes;
for (auto ext : m_extensions)
for (auto item : ext->palettes())
palettes[item.first] = item.second;
return palettes;
}
Extension* Extensions::installCompressedExtension(const std::string& zipFn)
{
std::string dstExtensionPath =
base::join_path(m_userExtensionsPath,
base::get_file_title(zipFn));
ReadArchive in(zipFn);
WriteArchive out(dstExtensionPath);
archive_entry* entry;
while ((entry = in.readEntry()) != nullptr)
out.writeEntry(in, entry);
Extension* extension = loadExtension(
dstExtensionPath,
base::join_path(dstExtensionPath, "package.json"),
false);
if (!extension)
throw base::Exception("Error adding the new extension");
// Generate signal
NewExtension(extension);
return extension;
}
Extension* Extensions::loadExtension(const std::string& path,
@ -132,7 +348,8 @@ Extension* Extensions::loadExtension(const std::string& path,
new Extension(path,
name,
displayName,
true, // TODO check if the extension is enabled in the configuration
// Extensions are enabled by default
get_config_bool("extensions", name.c_str(), true),
isBuiltinExtension));
auto contributes = json["contributes"];
@ -151,12 +368,7 @@ Extension* Extensions::loadExtension(const std::string& path,
themeId.c_str(),
themePath.c_str());
if (isBuiltinExtension) {
m_builtinThemes[themeId] = themePath;
}
else {
m_userThemes[themeId] = themePath;
}
extension->addTheme(themeId, themePath);
}
}
@ -174,11 +386,13 @@ Extension* Extensions::loadExtension(const std::string& path,
palId.c_str(),
palPath.c_str());
m_palettes[palId] = palPath;
extension->addPalette(palId, palPath);
}
}
}
if (extension)
m_extensions.push_back(extension.get());
return extension.release();
}

View File

@ -16,6 +16,10 @@
namespace app {
// Key=theme/palette/etc. id
// Value=theme/palette/etc. path
typedef std::map<std::string, std::string> ExtensionItems;
class Extension {
public:
Extension(const std::string& path,
@ -28,10 +32,16 @@ namespace app {
const std::string& name() const { return m_name; }
const std::string& displayName() const { return m_displayName; }
const ExtensionItems& themes() const { return m_themes; }
const ExtensionItems& palettes() const { return m_palettes; }
void addTheme(const std::string& id, const std::string& path);
void addPalette(const std::string& id, const std::string& path);
bool isEnabled() const { return m_isEnabled; }
bool isInstalled() const { return m_isInstalled; }
bool canBeDisabled() const { return m_isEnabled; }
bool canBeUninstalled() const { return !m_isBuiltinExtension; }
bool canBeDisabled() const;
bool canBeUninstalled() const;
void enable(const bool state);
void uninstall();
@ -41,6 +51,11 @@ namespace app {
obs::signal<void(Extension*)> Uninstall;
private:
void uninstallFiles(const std::string& path);
bool isCurrentTheme() const;
ExtensionItems m_themes;
ExtensionItems m_palettes;
std::string m_path;
std::string m_name;
std::string m_displayName;
@ -64,10 +79,12 @@ namespace app {
void disableExtension(Extension* extension);
void uninstallExtension(Extension* extension);
void installCompressedExtension(const std::string& zipFn);
Extension* installCompressedExtension(const std::string& zipFn);
std::string themePath(const std::string& themeId);
const std::map<std::string, std::string>& palettes() const;
ExtensionItems palettes() const;
obs::signal<void(Extension*)> NewExtension;
private:
Extension* loadExtension(const std::string& path,
@ -75,12 +92,7 @@ namespace app {
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;
std::map<std::string, std::string> m_palettes;
std::string m_userExtensionsPath;
};
} // namespace app

View File

@ -748,7 +748,7 @@ void SkinTheme::initWidget(Widget* widget)
break;
case kListItemWidget:
BORDER(1 * scale);
widget->setStyle(styles.listItem());
break;
case kComboBoxWidget: {
@ -1063,34 +1063,6 @@ void SkinTheme::paintListBox(PaintEvent& ev)
g->fillRect(colors.background(), g->getClipBounds());
}
void SkinTheme::paintListItem(ui::PaintEvent& ev)
{
Widget* widget = static_cast<Widget*>(ev.getSource());
gfx::Rect bounds = widget->clientBounds();
Graphics* g = ev.graphics();
gfx::Color fg, bg;
if (!widget->isEnabled()) {
bg = colors.face();
fg = colors.disabled();
}
else if (widget->isSelected()) {
fg = colors.listitemSelectedText();
bg = colors.listitemSelectedFace();
}
else {
fg = colors.listitemNormalText();
bg = colors.listitemNormalFace();
}
g->fillRect(bg, bounds);
if (widget->hasText()) {
bounds.shrink(widget->border());
drawText(g, nullptr, fg, bg, widget, bounds, 0, 0);
}
}
void SkinTheme::paintMenu(PaintEvent& ev)
{
Widget* widget = static_cast<Widget*>(ev.getSource());

View File

@ -59,7 +59,6 @@ namespace app {
void paintEntry(ui::PaintEvent& ev) override;
void paintListBox(ui::PaintEvent& ev) override;
void paintListItem(ui::PaintEvent& ev) override;
void paintMenu(ui::PaintEvent& ev) override;
void paintMenuItem(ui::PaintEvent& ev) override;
void paintSlider(ui::PaintEvent& ev) override;

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2001-2016 David Capello
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -43,11 +43,6 @@ bool ListItem::onProcessMessage(Message* msg)
return Widget::onProcessMessage(msg);
}
void ListItem::onPaint(PaintEvent& ev)
{
theme()->paintListItem(ev);
}
void ListItem::onResize(ResizeEvent& ev)
{
setBoundsQuietly(ev.bounds());

View File

@ -1,5 +1,5 @@
// Aseprite UI Library
// Copyright (C) 2001-2016 David Capello
// Copyright (C) 2001-2017 David Capello
//
// This file is released under the terms of the MIT license.
// Read LICENSE.txt for more information.
@ -24,7 +24,6 @@ namespace ui {
protected:
bool onProcessMessage(Message* msg) override;
void onPaint(PaintEvent& ev) override;
void onResize(ResizeEvent& ev) override;
void onSizeHint(SizeHintEvent& ev) override;

View File

@ -64,7 +64,6 @@ namespace ui {
virtual void paintEntry(PaintEvent& ev) = 0;
virtual void paintListBox(PaintEvent& ev) = 0;
virtual void paintListItem(PaintEvent& ev) = 0;
virtual void paintMenu(PaintEvent& ev) = 0;
virtual void paintMenuItem(PaintEvent& ev) = 0;
virtual void paintSlider(PaintEvent& ev) = 0;

View File

@ -111,3 +111,8 @@ endif()
# JSON
set(TAOCPP_JSON_BUILD_TESTS OFF CACHE BOOL "Build test programs")
add_subdirectory(json)
# libarchive
add_subdirectory(libarchive)
target_include_directories(archive_static INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/libarchive/libarchive>)

1
third_party/libarchive vendored Submodule

@ -0,0 +1 @@
Subproject commit 328453a041e2ead52bf2c64b778a29f99bd17f14