diff --git a/data/skins/default/sheet.png b/data/skins/default/sheet.png
index 9ce7e635c..b5da4d71e 100644
Binary files a/data/skins/default/sheet.png and b/data/skins/default/sheet.png differ
diff --git a/data/skins/default/skin.xml b/data/skins/default/skin.xml
index 1e8fede72..63a55762d 100644
--- a/data/skins/default/skin.xml
+++ b/data/skins/default/skin.xml
@@ -407,7 +407,8 @@
-
+
+
diff --git a/data/widgets/keyboard_shortcuts.xml b/data/widgets/keyboard_shortcuts.xml
index 1471a8489..7dac927e4 100644
--- a/data/widgets/keyboard_shortcuts.xml
+++ b/data/widgets/keyboard_shortcuts.xml
@@ -5,6 +5,7 @@
+
@@ -14,6 +15,9 @@
+
+
+
diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt
index e6c23e2fc..a4d14f8d6 100644
--- a/src/app/CMakeLists.txt
+++ b/src/app/CMakeLists.txt
@@ -381,6 +381,7 @@ add_library(app-lib
ui/preview_editor.cpp
ui/recent_listbox.cpp
ui/resources_listbox.cpp
+ ui/search_entry.cpp
ui/select_accelerator.cpp
ui/skin/button_icon_impl.cpp
ui/skin/skin_part.cpp
diff --git a/src/app/commands/cmd_keyboard_shortcuts.cpp b/src/app/commands/cmd_keyboard_shortcuts.cpp
index a62c6fee0..3cc48b279 100644
--- a/src/app/commands/cmd_keyboard_shortcuts.cpp
+++ b/src/app/commands/cmd_keyboard_shortcuts.cpp
@@ -18,16 +18,21 @@
#include "app/tools/tool.h"
#include "app/ui/app_menuitem.h"
#include "app/ui/keyboard_shortcuts.h"
+#include "app/ui/search_entry.h"
#include "app/ui/select_accelerator.h"
#include "app/ui/skin/skin_theme.h"
#include "base/bind.h"
#include "base/fs.h"
#include "base/path.h"
+#include "base/scoped_value.h"
+#include "base/split_string.h"
+#include "base/string.h"
#include "ui/graphics.h"
#include "ui/listitem.h"
#include "ui/paint_event.h"
#include "ui/preferred_size_event.h"
#include "ui/resize_event.h"
+#include "ui/separator.h"
#include "keyboard_shortcuts.xml.h"
@@ -55,6 +60,8 @@ public:
setBorder(border);
}
+ Key* key() { return m_key; }
+
void restoreKeys() {
if (m_key && m_keyOrig)
*m_key = *m_keyOrig;
@@ -280,7 +287,7 @@ private:
class KeyboardShortcutsWindow : public app::gen::KeyboardShortcuts {
public:
- KeyboardShortcutsWindow() {
+ KeyboardShortcutsWindow() : m_searchChange(false) {
setAutoRemap(false);
section()->addChild(new ListItem("Menus"));
@@ -288,6 +295,7 @@ public:
section()->addChild(new ListItem("Tools"));
section()->addChild(new ListItem("Action Modifiers"));
+ search()->Change.connect(Bind(&KeyboardShortcutsWindow::onSearchChange, this));
section()->Change.connect(Bind(&KeyboardShortcutsWindow::onSectionChange, this));
importButton()->Click.connect(Bind(&KeyboardShortcutsWindow::onImport, this));
exportButton()->Click.connect(Bind(&KeyboardShortcutsWindow::onExport, this));
@@ -304,6 +312,8 @@ public:
private:
void deleteAllKeyItems() {
+ while (searchList()->getLastChild())
+ searchList()->removeChild(searchList()->getLastChild());
while (menus()->getLastChild())
menus()->removeChild(menus()->getLastChild());
while (commands()->getLastChild())
@@ -375,12 +385,78 @@ private:
this->actions()->sortItems();
section()->selectIndex(0);
- onSectionChange();
+ updateViews();
+ }
+
+ void fillSearchList(const std::string& search) {
+ while (searchList()->getLastChild())
+ searchList()->removeChild(searchList()->getLastChild());
+
+ std::vector parts;
+ base::split_string(base::string_to_lower(search), parts, " ");
+
+ ListBox* listBoxes[] = { commands(), tools(), actions() };
+ int sectionIdx = 1; // index 0 is menus, index 1 is commands
+ for (auto listBox : listBoxes) {
+ Separator* group = nullptr;
+
+ for (auto item : listBox->getChildren()) {
+ if (KeyItem* keyItem = dynamic_cast(item)) {
+ std::string itemText =
+ base::string_to_lower(keyItem->getText());
+ int matches = 0;
+
+ for (const auto& part : parts) {
+ if (itemText.find(part) != std::string::npos)
+ ++matches;
+ }
+
+ if (matches == int(parts.size())) {
+ if (!group) {
+ group = new Separator(
+ section()->getChildren()[sectionIdx]->getText(), HORIZONTAL);
+ group->setBgColor(SkinTheme::instance()->colors.background());
+
+ searchList()->addChild(group);
+ }
+
+ KeyItem* copyItem =
+ new KeyItem(keyItem->getText(),
+ keyItem->key(), nullptr, 0);
+ searchList()->addChild(copyItem);
+ }
+ }
+ }
+
+ ++sectionIdx;
+ }
+ }
+
+ void onSearchChange() {
+ base::ScopedValue flag(m_searchChange, true, false);
+ std::string searchText = search()->getText();
+
+ if (searchText.empty())
+ section()->selectIndex(0);
+ else {
+ fillSearchList(searchText);
+ section()->selectChild(nullptr);
+ }
+
+ updateViews();
}
void onSectionChange() {
- int section = this->section()->getSelectedIndex();
+ if (m_searchChange)
+ return;
+ search()->setText("");
+ updateViews();
+ }
+
+ void updateViews() {
+ int section = this->section()->getSelectedIndex();
+ searchView()->setVisible(section < 0);
menusView()->setVisible(section == 0);
commandsView()->setVisible(section == 1);
toolsView()->setVisible(section == 2);
@@ -438,6 +514,7 @@ private:
}
std::vector m_allKeyItems;
+ bool m_searchChange;
};
class KeyboardShortcutsCommand : public Command {
diff --git a/src/app/ui/search_entry.cpp b/src/app/ui/search_entry.cpp
new file mode 100644
index 000000000..a7bb56c86
--- /dev/null
+++ b/src/app/ui/search_entry.cpp
@@ -0,0 +1,107 @@
+// Aseprite
+// Copyright (C) 2001-2015 David Capello
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "app/ui/search_entry.h"
+
+#include "app/ui/skin/skin_theme.h"
+#include "she/surface.h"
+#include "ui/graphics.h"
+#include "ui/message.h"
+#include "ui/paint_event.h"
+#include "ui/preferred_size_event.h"
+
+namespace app {
+
+using namespace app::skin;
+using namespace gfx;
+using namespace ui;
+
+SearchEntry::SearchEntry()
+ : Entry(256, "")
+{
+}
+
+bool SearchEntry::onProcessMessage(ui::Message* msg)
+{
+ switch (msg->type()) {
+ case kMouseDownMessage: {
+ Rect closeBounds = getCloseIconBounds();
+ Point mousePos = static_cast(msg)->position()
+ - getBounds().getOrigin();
+
+ if (closeBounds.contains(mousePos)) {
+ setText("");
+ onChange();
+ return true;
+ }
+ break;
+ }
+ }
+ return Entry::onProcessMessage(msg);
+}
+
+void SearchEntry::onPaint(ui::PaintEvent& ev)
+{
+ SkinTheme* theme = static_cast(getTheme());
+ theme->paintEntry(ev);
+
+ auto icon = theme->parts.iconSearch()->getBitmap(0);
+ Rect bounds = getClientBounds();
+ ev.getGraphics()->drawColoredRgbaSurface(
+ icon, theme->colors.text(),
+ bounds.x + border().left(),
+ bounds.y + bounds.h/2 - icon->height()/2);
+
+ if (!getText().empty()) {
+ icon = theme->parts.iconClose()->getBitmap(0);
+ ev.getGraphics()->drawColoredRgbaSurface(
+ icon, theme->colors.text(),
+ bounds.x + bounds.w - border().right() - childSpacing() - icon->width(),
+ bounds.y + bounds.h/2 - icon->height()/2);
+ }
+}
+
+void SearchEntry::onPreferredSize(PreferredSizeEvent& ev)
+{
+ Entry::onPreferredSize(ev);
+ Size sz = ev.getPreferredSize();
+
+ SkinTheme* theme = static_cast(getTheme());
+ auto icon = theme->parts.iconSearch()->getBitmap(0);
+ sz.h = MAX(sz.h, icon->height()+border().height());
+
+ ev.setPreferredSize(sz);
+}
+
+Rect SearchEntry::onGetEntryTextBounds() const
+{
+ SkinTheme* theme = static_cast(getTheme());
+ Rect bounds = Entry::onGetEntryTextBounds();
+ auto icon1 = theme->parts.iconSearch()->getBitmap(0);
+ auto icon2 = theme->parts.iconClose()->getBitmap(0);
+ bounds.x += childSpacing() + icon1->width();
+ bounds.w -= 2*childSpacing() + icon1->width() + icon2->width();
+ return bounds;
+}
+
+Rect SearchEntry::getCloseIconBounds() const
+{
+ SkinTheme* theme = static_cast(getTheme());
+ Rect bounds = getClientBounds();
+ auto icon = theme->parts.iconClose()->getBitmap(0);
+ bounds.x += bounds.w - border().right() - childSpacing() - icon->width();
+ bounds.y += bounds.h/2 - icon->height()/2;
+ bounds.w = icon->width();
+ bounds.h = icon->height();
+ return bounds;
+}
+
+} // namespace app
diff --git a/src/app/ui/search_entry.h b/src/app/ui/search_entry.h
new file mode 100644
index 000000000..dcdccfce7
--- /dev/null
+++ b/src/app/ui/search_entry.h
@@ -0,0 +1,31 @@
+// Aseprite
+// Copyright (C) 2001-2015 David Capello
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License version 2 as
+// published by the Free Software Foundation.
+
+#ifndef APP_UI_SEARCH_ENTRY_H_INCLUDED
+#define APP_UI_SEARCH_ENTRY_H_INCLUDED
+#pragma once
+
+#include "ui/entry.h"
+
+namespace app {
+
+ class SearchEntry : public ui::Entry {
+ public:
+ SearchEntry();
+
+ private:
+ bool onProcessMessage(ui::Message* msg) override;
+ void onPaint(ui::PaintEvent& ev) override;
+ void onPreferredSize(ui::PreferredSizeEvent& ev) override;
+ gfx::Rect onGetEntryTextBounds() const override;
+
+ gfx::Rect getCloseIconBounds() const;
+ };
+
+} // namespace app
+
+#endif
diff --git a/src/app/ui/skin/skin_theme.cpp b/src/app/ui/skin/skin_theme.cpp
index 748171152..59fcda323 100644
--- a/src/app/ui/skin/skin_theme.cpp
+++ b/src/app/ui/skin/skin_theme.cpp
@@ -568,6 +568,7 @@ void SkinTheme::initWidget(Widget* widget)
parts.sunkenNormal()->getBitmapN()->height(),
parts.sunkenNormal()->getBitmapE()->width(),
parts.sunkenNormal()->getBitmapS()->height());
+ widget->setChildSpacing(3 * scale);
break;
case kGridWidget:
@@ -650,17 +651,6 @@ void SkinTheme::initWidget(Widget* widget)
else {
BORDER4(4 * scale, 2 * scale, 1 * scale, 2 * scale);
}
-
- if (widget->hasText()) {
- gfx::Border border = widget->border();
-
- if (widget->getAlign() & TOP)
- border.top(widget->getTextHeight());
- else if (widget->getAlign() & BOTTOM)
- border.bottom(widget->getTextHeight());
-
- widget->setBorder(border);
- }
break;
case kSliderWidget:
@@ -923,8 +913,9 @@ void SkinTheme::paintEntry(PaintEvent& ev)
bg);
// Draw the text
- x = bounds.x + widget->border().left();
- y = bounds.y + bounds.h/2 - widget->getTextHeight()/2;
+ bounds = widget->getEntryTextBounds();
+ x = bounds.x;
+ y = bounds.y;
base::utf8_const_iterator utf8_it = base::utf8_const_iterator(textString.begin());
int textlen = base::utf8_length(textString);
@@ -954,7 +945,7 @@ void SkinTheme::paintEntry(PaintEvent& ev)
}
w = g->measureChar(ch).w;
- if (x+w > bounds.x2()-3)
+ if (x+w > bounds.x2()-widget->childSpacing()*guiscale())
return;
caret_x = x;
@@ -969,7 +960,7 @@ void SkinTheme::paintEntry(PaintEvent& ev)
// Draw suffix if there is enough space
if (!widget->getSuffix().empty()) {
Rect sufBounds(x, y,
- bounds.x2()-3*guiscale()-x,
+ bounds.x2()-widget->childSpacing()*guiscale()-x,
widget->getTextHeight());
IntersectClip clip(g, sufBounds);
if (clip) {
@@ -1228,22 +1219,27 @@ void SkinTheme::paintSeparator(ui::PaintEvent& ev)
// background
g->fillRect(BGCOLOR, bounds);
- if (widget->getAlign() & HORIZONTAL)
- drawHline(g, bounds, parts.separatorHorz().get());
+ if (widget->getAlign() & HORIZONTAL) {
+ int h = parts.separatorHorz()->getBitmap(0)->height();
+ drawHline(g, gfx::Rect(bounds.x, bounds.y+bounds.h/2-h/2,
+ bounds.w, h),
+ parts.separatorHorz().get());
+ }
- if (widget->getAlign() & VERTICAL)
- drawVline(g, bounds, parts.separatorVert().get());
+ if (widget->getAlign() & VERTICAL) {
+ int w = parts.separatorVert()->getBitmap(0)->width();
+ drawVline(g, gfx::Rect(bounds.x+bounds.w/2-w/2, bounds.y,
+ w, bounds.h),
+ parts.separatorVert().get());
+ }
// text
if (widget->hasText()) {
int h = widget->getTextHeight();
Rect r(
- Point(
- bounds.x + widget->border().left()/2 + h/2,
- bounds.y + widget->border().top()/2 - h/2),
- Point(
- bounds.x2() - widget->border().right()/2 - h,
- bounds.y2() - widget->border().bottom()/2 + h));
+ bounds.x + widget->border().left()/2 + h/2,
+ bounds.y + bounds.h/2 - h/2,
+ widget->getTextWidth(), h);
drawTextString(g, NULL,
colors.separatorLabel(), BGCOLOR,
diff --git a/src/app/widget_loader.cpp b/src/app/widget_loader.cpp
index 836461b3e..a536c0731 100644
--- a/src/app/widget_loader.cpp
+++ b/src/app/widget_loader.cpp
@@ -17,6 +17,7 @@
#include "app/ui/button_set.h"
#include "app/ui/color_button.h"
#include "app/ui/drop_down_button.h"
+#include "app/ui/search_entry.h"
#include "app/ui/skin/skin_style_property.h"
#include "app/ui/skin/skin_theme.h"
#include "app/widget_not_found.h"
@@ -466,6 +467,10 @@ Widget* WidgetLoader::convertXmlElementToWidget(const TiXmlElement* elem, Widget
}
}
}
+ else if (elem_name == "search") {
+ if (!widget)
+ widget = new SearchEntry;
+ }
// Was the widget created?
if (widget)
diff --git a/src/gen/ui_class.cpp b/src/gen/ui_class.cpp
index 1f4f6f187..dcbeb7437 100644
--- a/src/gen/ui_class.cpp
+++ b/src/gen/ui_class.cpp
@@ -64,6 +64,7 @@ static std::string convert_type(const std::string& name)
if (name == "listbox") return "ui::ListBox";
if (name == "panel") return "ui::Panel";
if (name == "radio") return "ui::RadioButton";
+ if (name == "search") return "app::SearchEntry";
if (name == "slider") return "ui::Slider";
if (name == "splitter") return "ui::Splitter";
if (name == "vbox") return "ui::VBox";
diff --git a/src/ui/entry.cpp b/src/ui/entry.cpp
index 5b0af393d..4839551ed 100644
--- a/src/ui/entry.cpp
+++ b/src/ui/entry.cpp
@@ -178,6 +178,11 @@ void Entry::getEntryThemeInfo(int* scroll, int* caret, int* state,
}
}
+gfx::Rect Entry::getEntryTextBounds() const
+{
+ return onGetEntryTextBounds();
+}
+
bool Entry::onProcessMessage(Message* msg)
{
switch (msg->type()) {
@@ -454,27 +459,37 @@ void Entry::onChange()
Change();
}
+gfx::Rect Entry::onGetEntryTextBounds() const
+{
+ gfx::Rect bounds = getClientBounds();
+ bounds.x += border().left();
+ bounds.y += bounds.h/2 - getTextHeight()/2;
+ bounds.w -= border().width();
+ bounds.h = getTextHeight();
+ return bounds;
+}
+
int Entry::getCaretFromMouse(MouseMessage* mousemsg)
{
base::utf8_const_iterator utf8_begin = base::utf8_const_iterator(getText().begin());
base::utf8_const_iterator utf8_end = base::utf8_const_iterator(getText().end());
- int c, x, w, mx, caret = m_caret;
+ int caret = m_caret;
int textlen = base::utf8_length(getText());
+ gfx::Rect bounds = getEntryTextBounds().offset(getBounds().getOrigin());
- mx = mousemsg->position().x;
- mx = MID(getBounds().x+border().left(),
- mx,
- getBounds().x2()-border().right()-1);
+ int mx = mousemsg->position().x;
+ mx = MID(bounds.x, mx, bounds.x2()-1);
- x = getBounds().x + border().left();
+ int x = bounds.x;
base::utf8_const_iterator utf8_it =
(m_scroll < textlen ?
utf8_begin + m_scroll:
utf8_end);
- for (c=m_scroll; utf8_it != utf8_end; ++c, ++utf8_it) {
- w = getFont()->charWidth(*utf8_it);
+ int c = m_scroll;
+ for (; utf8_it != utf8_end; ++c, ++utf8_it) {
+ int w = getFont()->charWidth(*utf8_it);
if (x+w >= getBounds().x2()-border().right())
break;
if ((mx >= x) && (mx < x+w)) {
@@ -485,8 +500,7 @@ int Entry::getCaretFromMouse(MouseMessage* mousemsg)
}
if (utf8_it == utf8_end) {
- if ((mx >= x) &&
- (mx <= getBounds().x2()-border().right()-1)) {
+ if ((mx >= x) && (mx < bounds.x2())) {
caret = c;
}
}
diff --git a/src/ui/entry.h b/src/ui/entry.h
index 5cbd8e73b..8394fa171 100644
--- a/src/ui/entry.h
+++ b/src/ui/entry.h
@@ -40,6 +40,7 @@ namespace ui {
// for themes
void getEntryThemeInfo(int* scroll, int* caret, int* state,
int* selbeg, int* selend);
+ gfx::Rect getEntryTextBounds() const;
// Signals
Signal0 Change;
@@ -53,6 +54,7 @@ namespace ui {
// New Events
virtual void onChange();
+ virtual gfx::Rect onGetEntryTextBounds() const;
private:
enum class EntryCmd {
diff --git a/src/ui/separator.cpp b/src/ui/separator.cpp
index 476bd9c8d..3cfc09ccb 100644
--- a/src/ui/separator.cpp
+++ b/src/ui/separator.cpp
@@ -45,8 +45,10 @@ void Separator::onPreferredSize(PreferredSizeEvent& ev)
maxSize.h = MAX(maxSize.h, reqSize.h);
}
- if (hasText())
+ if (hasText()) {
maxSize.w = MAX(maxSize.w, getTextWidth());
+ maxSize.h = MAX(maxSize.h, getTextHeight());
+ }
int w = maxSize.w + border().width();
int h = maxSize.h + border().height();