summaryrefslogtreecommitdiff
path: root/src/gui/elems/ge_actionChannel.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/elems/ge_actionChannel.cpp')
-rw-r--r--src/gui/elems/ge_actionChannel.cpp676
1 files changed, 676 insertions, 0 deletions
diff --git a/src/gui/elems/ge_actionChannel.cpp b/src/gui/elems/ge_actionChannel.cpp
new file mode 100644
index 0000000..b744ac3
--- /dev/null
+++ b/src/gui/elems/ge_actionChannel.cpp
@@ -0,0 +1,676 @@
+/* ---------------------------------------------------------------------
+ *
+ * Giada - Your Hardcore Loopmachine
+ *
+ * ge_actionChannel
+ *
+ * ---------------------------------------------------------------------
+ *
+ * Copyright (C) 2010-2015 Giovanni A. Zuliani | Monocasual
+ *
+ * This file is part of Giada - Your Hardcore Loopmachine.
+ *
+ * Giada - Your Hardcore Loopmachine is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU General
+ * Public License as published by the Free Software Foundation, either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * Giada - Your Hardcore Loopmachine is distributed in the hope that it
+ * will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Giada - Your Hardcore Loopmachine. If not, see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * ------------------------------------------------------------------ */
+
+
+#include <FL/fl_draw.H>
+#include "../../core/conf.h"
+#include "../../core/channel.h"
+#include "../../core/sampleChannel.h"
+#include "../../glue/glue.h"
+#include "../../utils/log.h"
+#include "../dialogs/gd_mainWindow.h"
+#include "../dialogs/gd_actionEditor.h"
+#include "ge_keyboard.h"
+#include "ge_actionChannel.h"
+
+
+extern gdMainWindow *mainWin;
+extern Mixer G_Mixer;
+extern Conf G_Conf;
+
+
+/* ------------------------------------------------------------------ */
+
+
+gActionChannel::gActionChannel(int x, int y, gdActionEditor *pParent, SampleChannel *ch)
+ : gActionWidget(x, y, 200, 40, pParent), ch(ch), selected(NULL)
+{
+ size(pParent->totalWidth, h());
+
+ /* add actions when the window opens. Their position is zoom-based;
+ * each frame is / 2 because we don't care about stereo infos. */
+
+ for (unsigned i=0; i<recorder::frames.size; i++) {
+ for (unsigned j=0; j<recorder::global.at(i).size; j++) {
+
+ recorder::action *ra = recorder::global.at(i).at(j);
+
+ if (ra->chan == pParent->chan->index) {
+
+ /* don't show actions > than the grey area */
+
+ if (recorder::frames.at(i) > G_Mixer.totalFrames)
+ continue;
+
+ /* skip the killchan actions in a singlepress channel. They cannot be recorded
+ * in such mode, but they can exist if you change from another mode to singlepress */
+
+ if (ra->type == ACTION_KILLCHAN && ch->mode == SINGLE_PRESS)
+ continue;
+
+ /* also filter out ACTION_KEYREL: it's up to gAction to find the other piece
+ * (namely frame_b) */
+
+ if (ra->type & (ACTION_KEYPRESS | ACTION_KILLCHAN)) {
+ int ax = x+(ra->frame/pParent->zoom);
+ gAction *a = new gAction(
+ ax, // x
+ y+4, // y
+ h()-8, // h
+ ra->frame, // frame_a
+ i, // n. of recordings
+ pParent, // pointer to the pParent window
+ ch, // pointer to SampleChannel
+ false, // record = false: don't record it, we are just displaying it!
+ ra->type); // type of action
+ add(a);
+ }
+ }
+ }
+ }
+ end(); // mandatory when you add widgets to a fl_group, otherwise mega malfunctions
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+gAction *gActionChannel::getSelectedAction() {
+ for (int i=0; i<children(); i++) {
+ int action_x = ((gAction*)child(i))->x();
+ int action_w = ((gAction*)child(i))->w();
+ if (Fl::event_x() >= action_x && Fl::event_x() <= action_x + action_w)
+ return (gAction*)child(i);
+ }
+ return NULL;
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gActionChannel::updateActions() {
+
+ /* when zooming, don't delete and re-add actions, just MOVE them. This
+ * function shifts the action by a zoom factor. Those singlepress are
+ * stretched, as well */
+
+ gAction *a;
+ for (int i=0; i<children(); i++) {
+
+ a = (gAction*)child(i);
+ int newX = x() + (a->frame_a / pParent->zoom);
+
+ if (ch->mode == SINGLE_PRESS) {
+ int newW = ((a->frame_b - a->frame_a) / pParent->zoom);
+ if (newW < gAction::MIN_WIDTH)
+ newW = gAction::MIN_WIDTH;
+ a->resize(newX, a->y(), newW, a->h());
+ }
+ else
+ a->resize(newX, a->y(), gAction::MIN_WIDTH, a->h());
+ }
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gActionChannel::draw() {
+
+ /* draw basic boundaries (+ beat bars) and hide the unused area. Then
+ * draw the children (the actions) */
+
+ baseDraw();
+
+ /* print label */
+
+ fl_color(COLOR_BG_1);
+ fl_font(FL_HELVETICA, 12);
+ if (active())
+ fl_draw("start/stop", x()+4, y(), w(), h(), (Fl_Align) (FL_ALIGN_LEFT | FL_ALIGN_CENTER)); /// FIXME h() is too much!
+ else
+ fl_draw("start/stop (disabled)", x()+4, y(), w(), h(), (Fl_Align) (FL_ALIGN_LEFT | FL_ALIGN_CENTER)); /// FIXME h() is too much!
+
+ draw_children();
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+int gActionChannel::handle(int e) {
+
+ int ret = Fl_Group::handle(e);
+
+ /* do nothing if the widget is deactivated. It could happen for loopmode
+ * channels */
+
+ if (!active())
+ return 1;
+
+ switch (e) {
+
+ case FL_DRAG: {
+ if (selected != NULL) { // if you don't drag an empty area
+
+ /* if onLeftEdge o onRightEdge are true it means that you're resizing
+ * an action. Otherwise move the widget. */
+
+ if (selected->onLeftEdge || selected->onRightEdge) {
+
+ /* some checks: a) cannot resize an action < N pixels, b) no beyond zero,
+ * c) no beyond bar maxwidth. Checks for overlap are done in FL_RELEASE */
+
+ if (selected->onRightEdge) {
+
+ int aw = Fl::event_x()-selected->x();
+ int ah = selected->h();
+
+ if (Fl::event_x() < selected->x()+gAction::MIN_WIDTH)
+ aw = gAction::MIN_WIDTH;
+ else
+ if (Fl::event_x() > pParent->coverX)
+ aw = pParent->coverX-selected->x();
+
+ selected->size(aw, ah);
+ }
+ else {
+
+ int ax = Fl::event_x();
+ int ay = selected->y();
+ int aw = selected->x()-Fl::event_x()+selected->w();
+ int ah = selected->h();
+
+ if (Fl::event_x() < x()) {
+ ax = x();
+ aw = selected->w()+selected->x()-x();
+ }
+ else
+ if (Fl::event_x() > selected->x()+selected->w()-gAction::MIN_WIDTH) {
+ ax = selected->x()+selected->w()-gAction::MIN_WIDTH;
+ aw = gAction::MIN_WIDTH;
+ }
+ selected->resize(ax, ay, aw, ah);
+ }
+ }
+
+ /* move the widget around */
+
+ else {
+ int real_x = Fl::event_x() - actionPickPoint;
+ if (real_x < x()) // don't go beyond the left border
+ selected->position(x(), selected->y());
+ else
+ if (real_x+selected->w() > pParent->coverX+x()) // don't go beyond the right border
+ selected->position(pParent->coverX+x()-selected->w(), selected->y());
+ else {
+ if (pParent->gridTool->isOn()) {
+ int snpx = pParent->gridTool->getSnapPoint(real_x-x()) + x() -1;
+ selected->position(snpx, selected->y());
+ }
+ else
+ selected->position(real_x, selected->y());
+ }
+ }
+ redraw();
+ }
+ ret = 1;
+ break;
+ }
+
+ case FL_PUSH: {
+
+ if (Fl::event_button1()) {
+
+ /* avoid at all costs two overlapping actions. We use 'selected' because
+ * the selected action can be reused in FL_DRAG, in case you want to move
+ * it. */
+
+ selected = getSelectedAction();
+
+ if (selected == NULL) {
+
+ /* avoid click on grey area */
+
+ if (Fl::event_x() >= pParent->coverX) {
+ ret = 1;
+ break;
+ }
+
+ /* snap function, if enabled */
+
+ int ax = Fl::event_x();
+ int fx = (ax - x()) * pParent->zoom;
+ if (pParent->gridTool->isOn()) {
+ ax = pParent->gridTool->getSnapPoint(ax-x()) + x() -1;
+ fx = pParent->gridTool->getSnapFrame(ax-x());
+
+ /* with snap=on an action can fall onto another */
+
+ if (actionCollides(fx)) {
+ ret = 1;
+ break;
+ }
+ }
+
+ gAction *a = new gAction(
+ ax, // x
+ y()+4, // y
+ h()-8, // h
+ fx, // frame_a
+ recorder::frames.size-1, // n. of actions recorded
+ pParent, // pParent window pointer
+ ch, // pointer to SampleChannel
+ true, // record = true: record it!
+ pParent->getActionType()); // type of action
+ add(a);
+ mainWin->keyboard->setChannelWithActions((gSampleChannel*)ch->guiChannel); // mainWindow update
+ redraw();
+ ret = 1;
+ }
+ else {
+ actionOriginalX = selected->x();
+ actionOriginalW = selected->w();
+ actionPickPoint = Fl::event_x() - selected->x();
+ ret = 1; // for dragging
+ }
+ }
+ else
+ if (Fl::event_button3()) {
+ gAction *a = getSelectedAction();
+ if (a != NULL) {
+ a->delAction();
+ remove(a);
+ delete a;
+ mainWin->keyboard->setChannelWithActions((gSampleChannel*)pParent->chan->guiChannel);
+ redraw();
+ ret = 1;
+ }
+ }
+ break;
+ }
+ case FL_RELEASE: {
+
+ if (selected == NULL) {
+ ret = 1;
+ break;
+ }
+
+ /* noChanges = true when you click on an action without doing anything
+ * (dragging or moving it). */
+
+ bool noChanges = false;
+ if (actionOriginalX == selected->x())
+ noChanges = true;
+ if (ch->mode == SINGLE_PRESS &&
+ actionOriginalX+actionOriginalW != selected->x()+selected->w())
+ noChanges = false;
+
+ if (noChanges) {
+ ret = 1;
+ selected = NULL;
+ break;
+ }
+
+ /* step 1: check if the action doesn't overlap with another one.
+ * In case of overlap the moved action returns to the original X
+ * value ("actionOriginalX"). */
+
+ bool overlap = false;
+ for (int i=0; i<children() && !overlap; i++) {
+
+ /* never check against itself. */
+
+ if ((gAction*)child(i) == selected)
+ continue;
+
+ int action_x = ((gAction*)child(i))->x();
+ int action_w = ((gAction*)child(i))->w();
+ if (ch->mode == SINGLE_PRESS) {
+
+ /* when 2 segments overlap?
+ * start = the highest value between the two starting points
+ * end = the lowest value between the two ending points
+ * if start < end then there's an overlap of end-start pixels. */
+
+ int start = action_x >= selected->x() ? action_x : selected->x();
+ int end = action_x+action_w < selected->x()+selected->w() ? action_x+action_w : selected->x()+selected->w();
+ if (start < end) {
+ selected->resize(actionOriginalX, selected->y(), actionOriginalW, selected->h());
+ redraw();
+ overlap = true; // one overlap: stop checking
+ }
+ }
+ else {
+ if (selected->x() == action_x) {
+ selected->position(actionOriginalX, selected->y());
+ redraw();
+ overlap = true; // one overlap: stop checking
+ }
+ }
+ }
+
+ /* step 2: no overlap? then update the coordinates of the action, ie
+ * delete the previous rec and create a new one.
+ * Anyway the selected action becomes NULL, because when you release
+ * the mouse button the dragging process ends. */
+
+ if (!overlap) {
+ if (pParent->gridTool->isOn()) {
+ int f = pParent->gridTool->getSnapFrame(selected->absx());
+ selected->moveAction(f);
+ }
+ else
+ selected->moveAction();
+ }
+ selected = NULL;
+ ret = 1;
+ break;
+ }
+ }
+
+ return ret;
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+bool gActionChannel::actionCollides(int frame) {
+
+ /* if SINGLE_PRESS we check that the tail (frame_b) of the action doesn't
+ * overlap the head (frame) of the new one. First the general case, yet. */
+
+ bool collision = false;
+
+ for (int i=0; i<children() && !collision; i++)
+ if ( ((gAction*)child(i))->frame_a == frame)
+ collision = true;
+
+ if (ch->mode == SINGLE_PRESS) {
+ for (int i=0; i<children() && !collision; i++) {
+ gAction *c = ((gAction*)child(i));
+ if (frame <= c->frame_b && frame >= c->frame_a)
+ collision = true;
+ }
+ }
+
+ return collision;
+}
+
+
+/* ------------------------------------------------------------------ */
+/* ------------------------------------------------------------------ */
+/* ------------------------------------------------------------------ */
+
+
+const int gAction::MIN_WIDTH = 8;
+
+
+/* ------------------------------------------------------------------ */
+
+
+/** TODO - index is useless?
+ * TODO - pass a record::action pointer and let gAction compute values */
+
+gAction::gAction(int X, int Y, int H, int frame_a, unsigned index, gdActionEditor *parent, SampleChannel *ch, bool record, char type)
+: Fl_Box (X, Y, MIN_WIDTH, H),
+ selected (false),
+ index (index),
+ parent (parent),
+ ch (ch),
+ type (type),
+ frame_a (frame_a),
+ onRightEdge(false),
+ onLeftEdge (false)
+{
+ /* bool 'record' defines how to understand the action.
+ * record = false: don't record it, just show it. It happens when you
+ * open the editor with some actions to be shown.
+ *
+ * record = true: record it AND show it. It happens when you click on
+ * an empty area in order to add a new actions. First you record it
+ * (addAction()) then you show it (FLTK::Draw()) */
+
+ if (record)
+ addAction();
+
+ /* in order to show a singlepress action we must compute the frame_b. We
+ * do that after the possible recording, otherwise we don't know which
+ * key_release is associated. */
+
+ if (ch->mode == SINGLE_PRESS && type == ACTION_KEYPRESS) {
+ recorder::action *a2 = NULL;
+ recorder::getNextAction(ch->index, ACTION_KEYREL, frame_a, &a2);
+ if (a2) {
+ frame_b = a2->frame;
+ w((frame_b - frame_a)/parent->zoom);
+ }
+ else
+ gLog("[gActionChannel] frame_b not found! [%d:???]\n", frame_a);
+
+ /* a singlepress action narrower than 8 pixel is useless. So check it.
+ * Warning: if an action is 8 px narrow, it has no body space to drag
+ * it. It's up to the user to zoom in and drag it. */
+
+ if (w() < MIN_WIDTH)
+ size(MIN_WIDTH, h());
+ }
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gAction::draw() {
+
+ int color;
+ if (selected) /// && gActionChannel !disabled
+ color = COLOR_BD_1;
+ else
+ color = COLOR_BG_2;
+
+ if (ch->mode == SINGLE_PRESS) {
+ fl_rectf(x(), y(), w(), h(), (Fl_Color) color);
+ }
+ else {
+ if (type == ACTION_KILLCHAN)
+ fl_rect(x(), y(), MIN_WIDTH, h(), (Fl_Color) color);
+ else {
+ fl_rectf(x(), y(), MIN_WIDTH, h(), (Fl_Color) color);
+ if (type == ACTION_KEYPRESS)
+ fl_rectf(x()+3, y()+h()-11, 2, 8, COLOR_BD_0);
+ else
+ if (type == ACTION_KEYREL)
+ fl_rectf(x()+3, y()+3, 2, 8, COLOR_BD_0);
+ }
+ }
+
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+int gAction::handle(int e) {
+
+ /* ret = 0 sends the event to the parent window. */
+
+ int ret = 0;
+
+ switch (e) {
+
+ case FL_ENTER: {
+ selected = true;
+ ret = 1;
+ redraw();
+ break;
+ }
+ case FL_LEAVE: {
+ fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+ selected = false;
+ ret = 1;
+ redraw();
+ break;
+ }
+ case FL_MOVE: {
+
+ /* handling of the two margins, left & right. 4 pixels are good enough */
+
+ if (ch->mode == SINGLE_PRESS) {
+ onLeftEdge = false;
+ onRightEdge = false;
+ if (Fl::event_x() >= x() && Fl::event_x() < x()+4) {
+ onLeftEdge = true;
+ fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+ }
+ else
+ if (Fl::event_x() >= x()+w()-4 && Fl::event_x() <= x()+w()) {
+ onRightEdge = true;
+ fl_cursor(FL_CURSOR_WE, FL_WHITE, FL_BLACK);
+ }
+ else
+ fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+ }
+ }
+ }
+
+ return ret;
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gAction::addAction() {
+
+ /* always check frame parity */
+
+ if (frame_a % 2 != 0)
+ frame_a++;
+
+ /* anatomy of an action
+ * ____[#######]_____ (a) is the left margin, ACTION_KEYPRESS. (b) is
+ * a b the right margin, the ACTION_KEYREL. This is the
+ * theory behind the singleshot.press actions; for any other kind the
+ * (b) is just a graphical and meaningless point. */
+
+ if (ch->mode == SINGLE_PRESS) {
+ recorder::rec(parent->chan->index, ACTION_KEYPRESS, frame_a);
+ recorder::rec(parent->chan->index, ACTION_KEYREL, frame_a+4096);
+ //gLog("action added, [%d, %d]\n", frame_a, frame_a+4096);
+ }
+ else {
+ recorder::rec(parent->chan->index, parent->getActionType(), frame_a);
+ //gLog("action added, [%d]\n", frame_a);
+ }
+
+ recorder::sortActions();
+
+ index++; // important!
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gAction::delAction() {
+
+ /* if SINGLE_PRESS you must delete both the keypress and the keyrelease
+ * actions. */
+
+ if (ch->mode == SINGLE_PRESS) {
+ recorder::deleteAction(parent->chan->index, frame_a, ACTION_KEYPRESS, false);
+ recorder::deleteAction(parent->chan->index, frame_b, ACTION_KEYREL, false);
+ }
+ else
+ recorder::deleteAction(parent->chan->index, frame_a, type, false);
+
+ /* restore the initial cursor shape, in case you delete an action and
+ * the double arrow (for resizing) is displayed */
+
+ fl_cursor(FL_CURSOR_DEFAULT, FL_WHITE, FL_BLACK);
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+void gAction::moveAction(int frame_a) {
+
+ /* easy one: delete previous action and record the new ones. As usual,
+ * SINGLE_PRESS requires two jobs. If frame_a is valid, use that frame
+ * value. */
+
+ delAction();
+
+ if (frame_a != -1)
+ this->frame_a = frame_a;
+ else
+ this->frame_a = xToFrame_a();
+
+
+ /* always check frame parity */
+
+ if (this->frame_a % 2 != 0)
+ this->frame_a++;
+
+ recorder::rec(parent->chan->index, type, this->frame_a);
+
+ if (ch->mode == SINGLE_PRESS) {
+ frame_b = xToFrame_b();
+ recorder::rec(parent->chan->index, ACTION_KEYREL, frame_b);
+ }
+
+ recorder::sortActions();
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+int gAction::absx() {
+ return x() - parent->ac->x();
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+int gAction::xToFrame_a() {
+ return (absx()) * parent->zoom;
+}
+
+
+/* ------------------------------------------------------------------ */
+
+
+int gAction::xToFrame_b() {
+ return (absx() + w()) * parent->zoom;
+}