Much improved console UI with global hotkeys and some column formatting

This commit is contained in:
casey 2016-05-18 02:29:11 -07:00
parent e8b9c873db
commit 1d2541e100
21 changed files with 293 additions and 85 deletions

View File

@ -49,6 +49,8 @@ static const std::string TAG = "LocalLibrary";
using namespace musik::core;
using namespace musik::core::library;
#define VERBOSE_LOGGING 0
LibraryPtr LocalLibrary::Create(std::string name, int id) {
LibraryPtr lib(new LocalLibrary(name, id));
return lib;
@ -140,7 +142,9 @@ int LocalLibrary::Enqueue(QueryPtr query, unsigned int options) {
queryQueue.push_back(query);
queueCondition.notify_all();
if (VERBOSE_LOGGING) {
musik::debug::info(TAG, "query '" + query->Name() + "' enqueued");
}
return query->GetId();
}
@ -185,13 +189,19 @@ void LocalLibrary::ThreadProc() {
}
if (query) {
if (VERBOSE_LOGGING) {
musik::debug::info(TAG, "query '" + query->Name() + "' running");
}
query->Run(this->db);
this->QueryCompleted(query);
if (VERBOSE_LOGGING) {
musik::debug::info(TAG, boost::str(boost::format(
"query '%1%' finished with status=%2%") % query->Name() % query->GetStatus()));
"query '%1%' finished with status=%2%")
% query->Name()
% query->GetStatus()));
}
query.reset();
}

View File

@ -46,8 +46,8 @@ namespace musik { namespace core { namespace library { namespace constants {
static const char* DURATION = "duration";
static const char* FILESIZE = "filesize";
static const char* YEAR = "year";
static const char* DISPLAY_GENRE_ID = "visual_genre_id";
static const char* DISPLAY_ARTIST_ID = "visual_artist_id";
static const char* GENRE_ID = "visual_genre_id";
static const char* ARTIST_ID = "visual_artist_id";
static const char* ALBUM_ID = "album_id";
static const char* PATH_ID = "path_id";
static const char* TITLE = "title";

View File

@ -41,7 +41,8 @@ using namespace musik::core::audio;
static std::string TAG = "Transport";
Transport::Transport()
: volume(1.0) {
: volume(1.0)
, state(StateStopped) {
}
Transport::~Transport() {
@ -49,6 +50,11 @@ Transport::~Transport() {
this->currentPlayer.reset();
}
Transport::PlaybackState Transport::GetPlaybackState() {
boost::mutex::scoped_lock lock(this->stateMutex);
return this->state;
}
void Transport::PrepareNextTrack(std::string trackUrl) {
PlayerPtr player = Player::Create(trackUrl);
@ -61,6 +67,12 @@ void Transport::PrepareNextTrack(std::string trackUrl) {
void Transport::Start(std::string url) {
musik::debug::info(TAG, "we were asked to start the track at " + url);
/* TODO FIXME: hack; the player is reference counted, we don't want the count
to reach zero within the critical section, because its background thread may
raise an event and cause a deadlock. do we really need shared_ptrs for these
Player instances? */
PlayerPtr current;
PlayerPtr player;
{
@ -77,6 +89,7 @@ void Transport::Start(std::string url) {
musik::debug::info(TAG, "Player created successfully");
}
current = this->currentPlayer; /* see hack note above. */
this->currentPlayer = newPlayer;
this->currentPlayer->PlaybackStarted.connect(this, &Transport::OnPlaybackStarted);
@ -85,7 +98,7 @@ void Transport::Start(std::string url) {
this->currentPlayer->PlaybackError.connect(this, &Transport::OnPlaybackError);
musik::debug::info(TAG, "play()");
this->currentPlayer->Play();
this->currentPlayer->Play(); /* non-blocking */
player = this->currentPlayer;
}
@ -272,6 +285,24 @@ void Transport::OnPlaybackError(Player *player) {
}
void Transport::RaisePlaybackEvent(int type, PlayerPtr player) {
/* TODO FIXME: should be either decoupled or merged with the playback
event enum. */
switch (type) {
case EventStarted:
case EventResumed:
this->state = StatePlaying;
break;
case EventFinished:
case EventError:
this->state = StateStopped;
break;
case EventPaused:
this->state = StatePaused;
break;
}
std::string uri = player ? player->GetUrl() : "";
this->PlaybackEvent(type, uri);
}

View File

@ -37,12 +37,31 @@
#include <boost/shared_ptr.hpp>
#include <boost/scoped_ptr.hpp>
#include <sigslot/sigslot.h>
#include <boost/thread/mutex.hpp>
#include <boost/thread/recursive_mutex.hpp>
namespace musik { namespace core { namespace audio {
class Transport : public sigslot::has_slots<> {
public:
sigslot::signal2<int, std::string> PlaybackEvent;
sigslot::signal0<> VolumeChanged;
typedef enum {
StateStopped,
StatePaused,
StatePlaying
} PlaybackState;
typedef enum {
EventScheduled = 0,
EventStarted = 1,
EventPaused = 2,
EventResumed = 3,
EventAlmostDone = 4,
EventFinished = 5,
EventError = -1
} PlaybackEventType;
Transport();
~Transport();
@ -58,19 +77,7 @@ namespace musik { namespace core { namespace audio {
double Volume();
void SetVolume(double volume);
public:
typedef enum {
EventScheduled = 0,
EventStarted = 1,
EventPaused = 2,
EventResumed = 3,
EventAlmostDone = 4,
EventFinished = 5,
EventError = -1
} PlaybackEventType;
sigslot::signal2<int, std::string> PlaybackEvent;
sigslot::signal0<> VolumeChanged;
PlaybackState GetPlaybackState();
private:
void RaisePlaybackEvent(int type, PlayerPtr player);
@ -82,6 +89,7 @@ namespace musik { namespace core { namespace audio {
private:
double volume;
PlaybackState state;
boost::mutex stateMutex;
PlayerPtr currentPlayer;

View File

@ -40,6 +40,7 @@
#include <app/layout/MainLayout.h>
#include <app/layout/LibraryLayout.h>
#include <app/util/GlobalHotkeys.h>
#include <boost/locale.hpp>
#include <boost/filesystem/path.hpp>
@ -163,6 +164,8 @@ int main(int argc, char* argv[])
using musik::core::LibraryFactory;
LibraryPtr library = LibraryFactory::Libraries().at(0);
GlobalHotkeys globalHotkeys(tp);
LibraryLayout libraryLayout(tp, library);
MainLayout mainLayout(tp, library);
@ -186,21 +189,16 @@ int main(int argc, char* argv[])
}
if (ch != -1) { /* -1 = idle timeout */
std::string kn = keyname(ch);
std::string kn = keyname((int) ch);
if (ch == '\t') { /* tab */
focusNextInLayout(state);
}
else if (kn == "^D") {
else if (kn == "^D") { /* ctrl+d quits */
quit = true;
}
else if (kn == "ALT_K") {
tp.SetVolume(tp.Volume() + 0.05); /* 5% */
}
else if (kn == "ALT_J") {
tp.SetVolume(tp.Volume() - 0.05);
}
else if (ch >= KEY_F(0) && ch <= KEY_F(12)) {
else if (!globalHotkeys.Handle(ch)) {
if (ch >= KEY_F(0) && ch <= KEY_F(12)) {
if (ch == KEY_F(1)) {
changeLayout(state, &mainLayout);
}
@ -215,6 +213,7 @@ int main(int argc, char* argv[])
state.keyHandler->KeyPress(ch);
}
}
}
Window::WriteToScreen();
WindowMessageQueue::Instance().Dispatch();

View File

@ -1,10 +1,17 @@
#include "stdafx.h"
#include <cursespp/Colors.h>
#include <cursespp/Screen.h>
#include <core/library/LocalLibraryConstants.h>
#include "LibraryLayout.h"
using namespace musik::core::library::constants;
#define CATEGORY_WIDTH 25
#define TRANSPORT_HEIGHT 3
#define DEFAULT_CATEGORY Track::ALBUM_ID
LibraryLayout::LibraryLayout(Transport& transport, LibraryPtr library)
: LayoutBase()
@ -21,9 +28,9 @@ void LibraryLayout::Layout() {
this->SetSize(Screen::GetWidth(), Screen::GetHeight());
this->SetPosition(0, 0);
this->albumList->SetPosition(0, 0);
this->albumList->SetSize(CATEGORY_WIDTH, this->GetHeight() - TRANSPORT_HEIGHT);
this->albumList->SetFocusOrder(0);
this->categoryList->SetPosition(0, 0);
this->categoryList->SetSize(CATEGORY_WIDTH, this->GetHeight() - TRANSPORT_HEIGHT);
this->categoryList->SetFocusOrder(0);
this->trackList->SetPosition(CATEGORY_WIDTH, 0);
this->trackList->SetSize(this->GetWidth() - CATEGORY_WIDTH, this->GetHeight() - TRANSPORT_HEIGHT);
@ -35,15 +42,15 @@ void LibraryLayout::Layout() {
}
void LibraryLayout::InitializeWindows() {
this->albumList.reset(new CategoryListView(this->library));
this->categoryList.reset(new CategoryListView(this->library, DEFAULT_CATEGORY));
this->trackList.reset(new TrackListView(this->transport, this->library));
this->transportView.reset(new TransportWindow(this->library, this->transport));
this->AddWindow(this->albumList);
this->AddWindow(this->categoryList);
this->AddWindow(this->trackList);
this->AddWindow(this->transportView);
this->albumList->SelectionChanged.connect(
this->categoryList->SelectionChanged.connect(
this, &LibraryLayout::OnCategoryViewSelectionChanged);
this->Layout();
@ -51,15 +58,15 @@ void LibraryLayout::InitializeWindows() {
void LibraryLayout::Show() {
LayoutBase::Show();
this->albumList->Requery();
this->categoryList->Requery();
}
void LibraryLayout::OnCategoryViewSelectionChanged(
ListWindow *view, size_t newIndex, size_t oldIndex) {
if (view == this->albumList.get()) {
DBID id = this->albumList->GetSelectedId();
if (view == this->categoryList.get()) {
DBID id = this->categoryList->GetSelectedId();
if (id != -1) {
this->trackList->Requery("album_id", id);
this->trackList->Requery(this->categoryList->GetFieldName(), id);
}
}
}

View File

@ -30,8 +30,7 @@ class LibraryLayout : public LayoutBase, public sigslot::has_slots<> {
Transport& transport;
LibraryPtr library;
std::shared_ptr<CategoryListView> albumList;
std::shared_ptr<CategoryListView> categoryList;
std::shared_ptr<TrackListView> trackList;
std::shared_ptr<TransportWindow> transportView;
};

View File

@ -3,15 +3,62 @@
#include "stdafx.h"
#include "CategoryListViewQuery.h"
#include <core/library/LocalLibraryConstants.h>
#include <core/db/Statement.h>
#include <boost/thread/mutex.hpp>
#include <map>
using musik::core::db::Statement;
using musik::core::db::Row;
using namespace musik::core::library::constants;
#define reset(x) x.reset(new std::vector<std::shared_ptr<Result>>);
CategoryListViewQuery::CategoryListViewQuery() {
static const std::string ALBUM_QUERY =
"SELECT DISTINCT albums.id, albums.name "
"FROM albums, tracks "
"WHERE albums.id = tracks.album_id "
"ORDER BY albums.sort_order;";
static const std::string ARTIST_QUERY =
"SELECT DISTINCT artists.id, artists.name "
"FROM artists, tracks "
"WHERE artists.id = tracks.visual_artist_id "
"ORDER BY artists.sort_order;";
static const std::string GENRE_QUERY =
"SELECT DISTINCT genres.id, genres.name "
"FROM genres, tracks "
"WHERE genres.id = tracks.visual_genre_id "
"ORDER BY genres.sort_order;";
static boost::mutex QUERY_MAP_MUTEX;
static std::map<std::string, std::string> FIELD_TO_QUERY_MAP;
static void initFieldToQueryMap() {
FIELD_TO_QUERY_MAP.emplace(Track::ALBUM_ID, ALBUM_QUERY);
FIELD_TO_QUERY_MAP.emplace(Track::ARTIST_ID, ARTIST_QUERY);
FIELD_TO_QUERY_MAP.emplace(Track::GENRE_ID, GENRE_QUERY);
}
CategoryListViewQuery::CategoryListViewQuery(const std::string& trackField) {
this->trackField = trackField;
reset(result);
{
boost::mutex::scoped_lock lock(QUERY_MAP_MUTEX);
if (!FIELD_TO_QUERY_MAP.size()) {
initFieldToQueryMap();
}
}
if (FIELD_TO_QUERY_MAP.find(trackField) == FIELD_TO_QUERY_MAP.end()) {
throw "invalid field for CategoryListView specified";
}
}
CategoryListViewQuery::~CategoryListViewQuery() {
@ -25,12 +72,7 @@ CategoryListViewQuery::ResultList CategoryListViewQuery::GetResult() {
bool CategoryListViewQuery::OnRun(Connection& db) {
reset(result);
std::string query =
"SELECT DISTINCT albums.id, albums.name "
"FROM albums, tracks "
"WHERE albums.id = tracks.album_id "
"ORDER BY albums.sort_order;";
std::string query = FIELD_TO_QUERY_MAP[this->trackField];
Statement stmt(query.c_str(), db);
while (stmt.Step() == Row) {

View File

@ -17,17 +17,16 @@ class CategoryListViewQuery : public QueryBase {
typedef std::shared_ptr<std::vector<
std::shared_ptr<Result>>> ResultList;
CategoryListViewQuery();
~CategoryListViewQuery();
CategoryListViewQuery(const std::string& trackField);
virtual ~CategoryListViewQuery();
std::string Name() {
return "CategoryListViewQuery";
}
std::string Name() { return "CategoryListViewQuery"; }
virtual ResultList GetResult();
protected:
virtual bool OnRun(Connection &db);
std::string trackField;
ResultList result;
};

View File

@ -13,7 +13,7 @@ using musik::core::LibraryPtr;
class SingleTrackQuery : public QueryBase {
public:
SingleTrackQuery(const std::string& path);
~SingleTrackQuery();
virtual ~SingleTrackQuery();
virtual std::string Name() { return "SingleTrackQuery"; }
virtual TrackPtr GetResult();

View File

@ -36,7 +36,8 @@ bool TrackListViewQuery::OnRun(Connection& db) {
std::string query = boost::str(boost::format(
"SELECT DISTINCT t.track, t.bpm, t.duration, t.filesize, t.year, t.title, t.filename, t.thumbnail_id, al.name AS album, gn.name AS genre, ar.name AS artist, t.filetime " \
"FROM tracks t, paths p, albums al, artists ar, genres gn " \
"WHERE t.%s=? AND t.album_id=al.id AND t.visual_genre_id=gn.id AND t.visual_artist_id=ar.id") % this->column);
"WHERE t.%s=? AND t.album_id=al.id AND t.visual_genre_id=gn.id AND t.visual_artist_id=ar.id "
"ORDER BY album, track, artist") % this->column);
Statement trackQuery(query.c_str(), db);
@ -53,8 +54,8 @@ bool TrackListViewQuery::OnRun(Connection& db) {
track->SetValue(Track::FILENAME, trackQuery.ColumnText(6));
track->SetValue(Track::THUMBNAIL_ID, trackQuery.ColumnText(7));
track->SetValue(Track::ALBUM_ID, trackQuery.ColumnText(8));
track->SetValue(Track::DISPLAY_GENRE_ID, trackQuery.ColumnText(9));
track->SetValue(Track::DISPLAY_ARTIST_ID, trackQuery.ColumnText(10));
track->SetValue(Track::GENRE_ID, trackQuery.ColumnText(9));
track->SetValue(Track::ARTIST_ID, trackQuery.ColumnText(10));
track->SetValue(Track::FILETIME, trackQuery.ColumnText(11));
result->push_back(track);

View File

@ -15,10 +15,9 @@ class TrackListViewQuery : public QueryBase {
typedef std::shared_ptr<std::vector<TrackPtr>> Result;
TrackListViewQuery(LibraryPtr library, const std::string& column, DBID id);
~TrackListViewQuery();
std::string Name() {
return "TrackListViewQuery";
}
virtual ~TrackListViewQuery();
std::string Name() { return "TrackListViewQuery"; }
virtual Result GetResult();

View File

@ -0,0 +1,35 @@
#include "stdafx.h"
#include "GlobalHotkeys.h"
GlobalHotkeys::GlobalHotkeys(Transport& transport)
: transport(transport) {
}
GlobalHotkeys::~GlobalHotkeys() {
}
bool GlobalHotkeys::Handle(int64 ch) {
std::string kn = keyname((int) ch);
if (kn == "ALT_K") {
int state = this->transport.GetPlaybackState();
if (state == Transport::StatePaused) {
this->transport.Resume();
}
else if (state == Transport::StatePlaying) {
this->transport.Pause();
}
}
if (kn == "ALT_L") {
this->transport.SetVolume(this->transport.Volume() + 0.05); /* 5% */
return true;
}
else if (kn == "ALT_J") {
this->transport.SetVolume(this->transport.Volume() - 0.05);
return true;
}
return false;
}

View File

@ -0,0 +1,18 @@
#pragma once
#include "stdafx.h"
#include <core/playback/Transport.h>
using musik::core::audio::Transport;
class GlobalHotkeys {
public:
GlobalHotkeys(Transport& transport);
~GlobalHotkeys(); /* non-virtual; do not use as a base class */
bool Handle(int64 ch);
private:
Transport& transport;
};

View File

@ -7,20 +7,24 @@
#include <cursespp/MultiLineEntry.h>
#include <cursespp/IWindowMessage.h>
#include <core/library/LocalLibraryConstants.h>
#include <app/query/CategoryListViewQuery.h>
#include "CategoryListView.h"
using musik::core::LibraryPtr;
using musik::core::IQuery;
using namespace musik::core::library::constants;
#define WINDOW_MESSAGE_QUERY_COMPLETED 1002
CategoryListView::CategoryListView(LibraryPtr library, IWindow *parent)
: ListWindow(parent) {
CategoryListView::CategoryListView(LibraryPtr library, const std::string& fieldName)
: ListWindow(NULL) {
this->SetContentColor(BOX_COLOR_WHITE_ON_BLACK);
this->library = library;
this->library->QueryCompleted.connect(this, &CategoryListView::OnQueryCompleted);
this->fieldName = fieldName;
this->adapter = new Adapter(*this);
}
@ -28,8 +32,25 @@ CategoryListView::~CategoryListView() {
delete adapter;
}
void CategoryListView::KeyPress(int64 ch) {
std::string kn = keyname((int) ch);
if (kn == "ALT_1") {
this->SetFieldName(Track::ARTIST_ID);
}
else if (kn == "ALT_2") {
this->SetFieldName(Track::ALBUM_ID);
}
else if (kn == "ALT_3") {
this->SetFieldName(Track::GENRE_ID);
}
else {
ListWindow::KeyPress(ch);
}
}
void CategoryListView::Requery() {
this->activeQuery.reset(new CategoryListViewQuery());
this->activeQuery.reset(new CategoryListViewQuery(this->fieldName));
this->library->Enqueue(activeQuery);
}
@ -41,6 +62,17 @@ DBID CategoryListView::GetSelectedId() {
return -1;
}
std::string CategoryListView::GetFieldName() {
return this->fieldName;
}
void CategoryListView::SetFieldName(const std::string& fieldName) {
if (this->fieldName != fieldName) {
this->fieldName = fieldName;
this->Requery();
}
}
void CategoryListView::OnQueryCompleted(QueryPtr query) {
if (query == this->activeQuery) {
Post(WINDOW_MESSAGE_QUERY_COMPLETED);

View File

@ -15,12 +15,17 @@ using musik::core::LibraryPtr;
class CategoryListView : public ListWindow, public sigslot::has_slots<> {
public:
CategoryListView(LibraryPtr library, IWindow *parent = NULL);
CategoryListView(LibraryPtr library, const std::string& fieldName);
virtual ~CategoryListView();
void Requery();
virtual void ProcessMessage(IWindowMessage &message);
virtual void KeyPress(int64 ch);
DBID GetSelectedId();
std::string GetFieldName();
void SetFieldName(const std::string& fieldName);
protected:
virtual IScrollAdapter& GetScrollAdapter();
@ -42,6 +47,7 @@ class CategoryListView : public ListWindow, public sigslot::has_slots<> {
LibraryPtr library;
Adapter *adapter;
std::string fieldName;
std::shared_ptr<CategoryListViewQuery> activeQuery;
CategoryListViewQuery::ResultList metadata;
};

View File

@ -82,16 +82,32 @@ size_t TrackListView::Adapter::GetEntryCount() {
return parent.metadata ? parent.metadata->size() : 0;
}
static inline void trunc(std::string& s, int max) {
if (s.size() > max) {
s = s.substr(0, max);
}
}
#define MAX_ARTIST 12
#define MAX_ALBUM 12
IScrollAdapter::EntryPtr TrackListView::Adapter::GetEntry(size_t index) {
int64 attrs = (index == parent.GetSelectedIndex()) ? COLOR_PAIR(BOX_COLOR_BLACK_ON_GREEN) : -1;
TrackPtr track = parent.metadata->at(index);
std::string trackNum = track->GetValue("track");
std::string title = track->GetValue("title");
std::string trackNum = track->GetValue(Track::TRACK_NUM);
std::string artist = track->GetValue(Track::ARTIST_ID);
std::string album = track->GetValue(Track::ALBUM_ID);
std::string title = track->GetValue(Track::TITLE);
trunc(artist, MAX_ARTIST);
trunc(album, MAX_ALBUM);
std::string text = boost::str(
boost::format("%s %s")
boost::format("%s %s %s %s")
% boost::io::group(std::setw(3), std::setfill(' '), trackNum)
% boost::io::group(std::setw(MAX_ARTIST), std::setiosflags(std::ios::left), std::setfill(' '), artist)
% boost::io::group(std::setw(MAX_ALBUM), std::setiosflags(std::ios::left), std::setfill(' '), album)
% title);
IScrollAdapter::EntryPtr entry(new SingleLineEntry(text));

View File

@ -117,8 +117,7 @@ inline static void breakIntoSubLines(
size_t wordLength = u8len(word);
size_t extra = (i != 0);
/* we have enough space for this new word. accumulate it. the
+1 here is to take the space into account */
/* we have enough space for this new word. accumulate it. */
if (accumLength + extra + wordLength < width) {
if (extra) {

View File

@ -213,9 +213,8 @@ void Window::Show() {
}
}
this->Repaint();
this->isVisible = true;
this->Repaint();
}
void Window::Hide() {

View File

@ -120,6 +120,7 @@
<ClCompile Include="app\query\CategoryListViewQuery.cpp" />
<ClCompile Include="app\query\SingleTrackQuery.cpp" />
<ClCompile Include="app\query\TrackListViewQuery.cpp" />
<ClCompile Include="app\util\GlobalHotkeys.cpp" />
<ClCompile Include="app\util\SystemInfo.cpp" />
<ClCompile Include="app\window\CategoryListView.cpp" />
<ClCompile Include="app\window\CommandWindow.cpp" />
@ -152,6 +153,7 @@
<ClInclude Include="app\query\CategoryListViewQuery.h" />
<ClInclude Include="app\query\SingleTrackQuery.h" />
<ClInclude Include="app\query\TrackListViewQuery.h" />
<ClInclude Include="app\util\GlobalHotkeys.h" />
<ClInclude Include="app\util\SystemInfo.h" />
<ClInclude Include="app\window\CategoryListView.h" />
<ClInclude Include="app\window\CommandWindow.h" />

View File

@ -78,6 +78,9 @@
<ClCompile Include="app\query\SingleTrackQuery.cpp">
<Filter>app\query</Filter>
</ClCompile>
<ClCompile Include="app\util\GlobalHotkeys.cpp">
<Filter>app\util</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="stdafx.h" />
@ -186,6 +189,9 @@
<ClInclude Include="app\query\SingleTrackQuery.h">
<Filter>app\query</Filter>
</ClInclude>
<ClInclude Include="app\util\GlobalHotkeys.h">
<Filter>app\util</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="cursespp">