diff --git a/ChangeLog b/ChangeLog index 75fd51961..1b006af92 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,7 @@ +2008-??-?? Version 0.7.0 + * Protocol change: packet length is now comes first in the header + * Layer support + 2008-10-20 Version 0.6.0 * Subpixel drawing * Added pen tool (draw without antialiasing) diff --git a/doc/dox/protocol.dox b/doc/dox/protocol.dox index 1a88453e3..e8a237fac 100644 --- a/doc/dox/protocol.dox +++ b/doc/dox/protocol.dox @@ -14,8 +14,8 @@ for passing the packets, and the packet format is described below. \section msg_structure Message structure -All packets are prefixed with an 8 bit packet identifier and 16 bit -packet payload length value. +All packets are prefixed with 16 bit packet length value and an 8 bit packet +identifier. The length is the length of the payload + 1 for the type field. At minimum, the packet consists only of the packet type and nothing else. The rest depends on the packet itself. diff --git a/src/client/CMakeLists.txt b/src/client/CMakeLists.txt index 1e5ba2ac4..4bac7368a 100644 --- a/src/client/CMakeLists.txt +++ b/src/client/CMakeLists.txt @@ -15,7 +15,8 @@ set ( localserver.h popupmessage.h userlistwidget.h userlistmodel.h chatwidget.h mandatoryfields.h palettewidget.h palettebox.h colorbox.h settingsdialog.h - navigator.h toolsettings.h brushslider.h viewstatus.h + navigator.h toolsettings.h brushslider.h viewstatus.h layerlistdelegate.h + layerlistwidget.h core/layerstack.h ) # unused headers because they do not create moc_* files () @@ -24,7 +25,8 @@ set ( version.h user.h interfaces.h preview.h boardeditor.h boarditem.h localpalette.h palette.h icons.h tools.h recentfiles.h sessioninfo.h annotationitem.h - core/tile.h core/layer.h core/brush.h core/point.h core/rasterop.h + core/tile.h core/layer.h core/brush.h core/point.h + core/rasterop.h ) set ( @@ -40,15 +42,16 @@ set ( sessioninfo.cpp icons.cpp mandatoryfields.cpp localpalette.cpp palettewidget.cpp palettebox.cpp colorbox.cpp settingsdialog.cpp palette.cpp navigator.cpp annotationitem.cpp - core/tile.cpp core/layer.cpp core/brush.cpp core/rasterop.cpp - brushslider.cpp viewstatus.cpp + core/tile.cpp core/layer.cpp core/layerstack.cpp core/brush.cpp + core/rasterop.cpp brushslider.cpp viewstatus.cpp layerlistdelegate.cpp + layerlistwidget.cpp ) set ( UIs ui/brushsettings.ui ui/simplesettings.ui ui/colordialog.ui ui/newdialog.ui ui/hostdialog.ui ui/joindialog.ui ui/logindialog.ui ui/chatbox.ui ui/palettebox.ui ui/colorbox.ui ui/settings.ui ui/navibox.ui ui/textsettings.ui ui/pensettings.ui - ui/erasersettings.ui ) + ui/erasersettings.ui ui/layerbox.ui ) # .UI diff --git a/src/client/annotationitem.cpp b/src/client/annotationitem.cpp index 083875c2b..d14bd3d1c 100644 --- a/src/client/annotationitem.cpp +++ b/src/client/annotationitem.cpp @@ -206,7 +206,7 @@ dpcore::Layer *AnnotationItem::toLayer(int *x, int *y) QPoint offset(int(pos().x() - xi*Tile::SIZE), int(pos().y() - yi*Tile::SIZE)); QImage img(offset.x() + int(size_.width()), - int(offset.y() + size_.height()), QImage::Format_ARGB32); + int(offset.y() + size_.height()), QImage::Format_ARGB32_Premultiplied); img.fill(0); QPainter painter(&img); render(&painter, QRectF(offset, size_)); @@ -214,7 +214,7 @@ dpcore::Layer *AnnotationItem::toLayer(int *x, int *y) *x = xi; if(y) *y = yi; - return new Layer(img); + return new Layer(0, -1, "", img); } } diff --git a/src/client/board.cpp b/src/client/board.cpp index 01ccef8fb..a228e2b5f 100644 --- a/src/client/board.cpp +++ b/src/client/board.cpp @@ -26,14 +26,14 @@ #include "boardeditor.h" #include "preview.h" #include "interfaces.h" -#include "core/layer.h" +#include "core/layerstack.h" #include "../shared/net/annotation.h" #include "../shared/net/message.h" namespace drawingboard { Board::Board(QObject *parent, interface::BrushSource *brush, interface::ColorSource *color) - : QGraphicsScene(parent), image_(0),localuser_(-1), toolpreview_(0), brushsrc_(brush), colorsrc_(color), hla_(false) + : QGraphicsScene(parent), image_(0),localuser_(-1), toolpreview_(0), brushsrc_(brush), colorsrc_(color), hla_(false), layerwidget_(0) { setItemIndexMethod(NoIndex); } @@ -69,7 +69,7 @@ void Board::initBoard(QImage image) image_ = new BoardItem(image.convertToFormat(QImage::Format_RGB32)); addItem(image_); foreach(User *u, users_) - u->setLayer(image_); + u->setBoard(image_); QList regions; regions.append(sceneRect()); emit changed(regions); @@ -156,8 +156,7 @@ void Board::addUser(int id) qDebug() << "Reusing board user" << id; delete users_.take(id); } - User *user = new User(id); - user->setLayer(image_); + User *user = new User(image_, id); users_[id] = user; } @@ -170,6 +169,20 @@ void Board::setLocalUser(int id) { Q_ASSERT(users_.contains(id)); localuser_ = id; + // Set the layer list if we know it already + if(layerwidget_) + users_[localuser_]->setLayerList(layerwidget_); +} + +/** + * @param llist layer list widget + */ +void Board::setLayerList(widgets::LayerList *llist) +{ + layerwidget_ = llist; + // Update local user if it exists + if(localuser_ != -1) + users_[localuser_]->setLayerList(layerwidget_); } /** @@ -178,7 +191,7 @@ void Board::setLocalUser(int id) QImage Board::image() const { if(image_) - return image_->image()->toImage(); + return image_->image()->toFlatImage(); else return QImage(); } @@ -219,6 +232,16 @@ int Board::height() const { return -1; } +/** + * @return layer stack + */ +dpcore::LayerStack *Board::layers() +{ + if(image_) + return image_->image(); + return 0; +} + /** * Returns a BoardEditor for modifying the drawing board either * directly or over the network. @@ -248,7 +271,7 @@ void Board::addPreview(const dpcore::Point& point) Preview *pre; if(previewcache_.isEmpty()) { - pre = new StrokePreview(user->layer()); + pre = new StrokePreview(user->board()); } else pre = previewcache_.dequeue(); if(previewstarted_) { @@ -278,10 +301,10 @@ void Board::commitPreviews() while(previews_.isEmpty()==false) { qreal distance; Preview *p = previews_.dequeue(); - if(p->from() != lastpoint) - image_->drawPoint(p->from(), p->brush()); - else - image_->drawLine(p->from(), p->to(), p->brush(), distance); + if(p->from() != lastpoint) // TODO + image_->drawPoint(0, p->from(), p->brush()); + else // TODO + image_->drawLine(0, p->from(), p->to(), p->brush(), distance); lastpoint = p->to(); delete p; } @@ -389,6 +412,37 @@ void Board::unannotate(int id) } } +void Board::addLayer(const QString& name) +{ + layers()->addLayer(name, layers()->size()); +} + +/** + * The layer is removed and all users active layers are changed to point + * to something else. + * @param layer id + */ +void Board::deleteLayer(int id) +{ + const int index = layers()->id2index(id); + if(index<0) { + qWarning() << "Tried to delete nonexistent layer"; + } + + // Fix user layers + foreach(User *u, users_) { + if(u->layer() == index) { + if(index==0) + u->setLayerId(1); + else + u->setLayerId(index-1); + } + } + + // Delete the layer + layers()->deleteLayer(id); + update(); +} } diff --git a/src/client/board.h b/src/client/board.h index 549296c48..7a2ef1a9f 100644 --- a/src/client/board.h +++ b/src/client/board.h @@ -41,6 +41,11 @@ namespace interface { namespace dpcore { class Brush; + class LayerStack; +} + +namespace widgets { + class LayerList; } //! Drawing board related classes @@ -73,12 +78,18 @@ class Board : public QGraphicsScene //! Initialize the board using an existing image as base void initBoard(QImage image); + //! Set the layer list widget to update when local user's layer changes + void setLayerList(widgets::LayerList *llist); + //! Get board width int width() const; //! Get board height int height() const; + //! Get the layers + dpcore::LayerStack *layers(); + //! Get board contents as an image QImage image() const; @@ -137,6 +148,12 @@ class Board : public QGraphicsScene //! Remove an annotation void unannotate(int id); + //! Create a new layer + void addLayer(const QString& name); + + //! Delete a layer + void deleteLayer(int id); + signals: //! The local user just created a new annotation void newLocalAnnotation(drawingboard::AnnotationItem *item); @@ -180,6 +197,8 @@ class Board : public QGraphicsScene interface::ColorSource *colorsrc_; bool hla_; + + widgets::LayerList *layerwidget_; }; } diff --git a/src/client/boardeditor.cpp b/src/client/boardeditor.cpp index 8b5a55b0b..36e195ee6 100644 --- a/src/client/boardeditor.cpp +++ b/src/client/boardeditor.cpp @@ -30,6 +30,7 @@ #include "preview.h" #include "annotationitem.h" #include "core/layer.h" +#include "core/layerstack.h" #include "../shared/net/annotation.h" namespace drawingboard { @@ -111,9 +112,9 @@ void BoardEditor::startPreview(tools::Type tool, const dpcore::Point& point, con Q_ASSERT(board_->toolpreview_ == 0); Q_ASSERT(tool == tools::LINE || tool == tools::RECTANGLE || tool == tools::ANNOTATION); if(tool == tools::LINE) - board_->toolpreview_ = new StrokePreview(user_->layer()); + board_->toolpreview_ = new StrokePreview(user_->board()); else - board_->toolpreview_ = new RectanglePreview(user_->layer()); + board_->toolpreview_ = new RectanglePreview(user_->board()); board_->toolpreview_->preview(point,point, brush); } @@ -138,7 +139,7 @@ void BoardEditor::endPreview() */ void BoardEditor::mergeLayer(int x, int y, const dpcore::Layer *layer) { - board_->image_->image()->merge(x, y, layer); + board_->image_->image()->getLayer(user_->layer())->merge(x, y, layer); } /** @@ -158,6 +159,31 @@ void LocalBoardEditor::setTool(const dpcore::Brush& brush) user_->setBrush(brush); } +/** + * @param id layer id + */ +void LocalBoardEditor::setLayer(int id) +{ + user_->setLayerId(id); +} + +/** + * A new empty layer is created on top of the layer stack + * @param name layer name + */ +void LocalBoardEditor::createLayer(const QString& name) +{ + board_->addLayer(name); +} + +/** + * @param id layer ID + */ +void LocalBoardEditor::deleteLayer(int id) +{ + board_->deleteLayer(id); +} + /** * @param point point to add */ @@ -223,6 +249,28 @@ void RemoteBoardEditor::setTool(const dpcore::Brush& brush) session_->sendToolSelect(brush); } +/** + * @param id layer id + */ +void RemoteBoardEditor::setLayer(int id) +{ + session_->sendLayerSelect(id); +} + +void RemoteBoardEditor::createLayer(const QString& name) +{ + // TODO +} + +/** + * Layer deletion is not supported while in a network session. + * @param id layer ID + */ +void RemoteBoardEditor::deleteLayer(int id) +{ + qWarning() << "BUG: Tried to delete layer ID" << id << "while in a network session!"; +} + /** * In atomic mode, all strokes until strokeEnd are sent in a single long * message. This is slightly more efficient and ensures no brush strokes diff --git a/src/client/boardeditor.h b/src/client/boardeditor.h index 497a261ed..928c0ffac 100644 --- a/src/client/boardeditor.h +++ b/src/client/boardeditor.h @@ -95,12 +95,21 @@ class BoardEditor { //! Remove the preview void endPreview(); - //! Merge a layer + //! Create an empty layer + virtual void createLayer(const QString& name) = 0; + + //! Delete a layer + virtual void deleteLayer(int id) = 0; + + //! Merge a layer with the currently selected layer void mergeLayer(int x, int y, const dpcore::Layer *layer); //! Set the tool used for drawing virtual void setTool(const dpcore::Brush& brush) = 0; + //! Select the layer to draw on + virtual void setLayer(int id) = 0; + //! Make strokes until endStroke atomic virtual void startAtomic() = 0; @@ -148,6 +157,9 @@ class LocalBoardEditor : public BoardEditor { : BoardEditor(board,user, brush, color) {} void setTool(const dpcore::Brush& brush); + void setLayer(int id); + void createLayer(const QString& name); + void deleteLayer(int id); // Atomic strokes are meaningles in local mode void startAtomic() { } void addStroke(const dpcore::Point& point); @@ -169,6 +181,9 @@ class RemoteBoardEditor : public BoardEditor { bool isCurrentBrush(const dpcore::Brush& brush) const; void setTool(const dpcore::Brush& brush); + void createLayer(const QString& name); + void deleteLayer(int id); + void setLayer(int id); void startAtomic(); void addStroke(const dpcore::Point& point); void endStroke(); diff --git a/src/client/boarditem.cpp b/src/client/boarditem.cpp index 953ba371e..f3a6e5c30 100644 --- a/src/client/boarditem.cpp +++ b/src/client/boarditem.cpp @@ -26,6 +26,7 @@ #include "boarditem.h" #include "core/point.h" #include "core/brush.h" +#include "core/layerstack.h" #include "core/layer.h" namespace drawingboard { @@ -62,7 +63,10 @@ void BoardItem::setImage(const QImage& image) Q_ASSERT(image.format() == QImage::Format_RGB32 || image.format() == QImage::Format_ARGB32); prepareGeometryChange(); delete image_; - image_ = new dpcore::Layer(image); + image_ = new dpcore::LayerStack(); + image_->addLayer(dpcore::LayerStack::tr("Background"), image); + // TODO testing... + image_->addLayer("Testing...", image.size()); } /** @@ -71,7 +75,7 @@ void BoardItem::setImage(const QImage& image) * First pixel is not drawn. This is done on purpose, as drawLine is usually * used to draw multiple joined lines. * - * If distance is not null, it is used to add spacing between dabs. + * @param layer ID of the layer on which to draw * @param point1 start coordinates * @param point2 end coordinates * @param brush brush to draw with @@ -79,22 +83,23 @@ void BoardItem::setImage(const QImage& image) * * @todo delta pressure(?) */ -void BoardItem::drawLine(const dpcore::Point& point1, const dpcore::Point& point2, const dpcore::Brush& brush,qreal &distance) +void BoardItem::drawLine(int layer, const dpcore::Point& point1, const dpcore::Point& point2, const dpcore::Brush& brush,qreal &distance) { - image_->drawLine(brush, point1, point2, distance); + image_->getLayer(layer)->drawLine(brush, point1, point2, distance); // Update screen int rad = brush.radius(point1.pressure()); update(QRect(point1, point2).normalized().adjusted(-rad-2,-rad-2,rad+2,rad+2)); } /** + * @param layer ID of the layer on which to draw * @param point coordinates * @param brush brush to use */ -void BoardItem::drawPoint(const dpcore::Point& point, const dpcore::Brush& brush) +void BoardItem::drawPoint(int layer, const dpcore::Point& point, const dpcore::Brush& brush) { int r = brush.radius(point.pressure()); - image_->dab(brush, point); + image_->getLayer(layer)->dab(brush, point); update(point.x()-r-2,point.y()-r-2,r*2+4,r*2+4); } diff --git a/src/client/boarditem.h b/src/client/boarditem.h index bfa8d3c8f..4e7fd92ba 100644 --- a/src/client/boarditem.h +++ b/src/client/boarditem.h @@ -24,17 +24,17 @@ namespace dpcore { class Layer; + class LayerStack; class Brush; class Point; } namespace drawingboard { -//! A drawing layer item item for QGraphicsScene +//! A drawing board item item for QGraphicsScene /** - * The layer item provides a modifiable image item for QGraphicsScene. + * The board item provides an interface to a LayerStack for QGraphicsScene. * Methods are provided for drawing lines and points with a Brush object. - * */ class BoardItem : public QGraphicsItem { @@ -51,14 +51,16 @@ class BoardItem : public QGraphicsItem void setImage(const QImage& image); //! Get the image - dpcore::Layer *image() const { return image_; } + dpcore::LayerStack *image() const { return image_; } //! Draw a line between two points with interpolated pressure values - void drawLine(const dpcore::Point& point1, const dpcore::Point& point2, - const dpcore::Brush& brush, qreal &distance); + void drawLine(int layer,const dpcore::Point& point1, + const dpcore::Point& point2, const dpcore::Brush& brush, + qreal &distance); //! Draw a single point - void drawPoint(const dpcore::Point& point, const dpcore::Brush& brush); + void drawPoint(int layer, const dpcore::Point& point, + const dpcore::Brush& brush); /** reimplematation */ QRectF boundingRect() const; @@ -70,8 +72,7 @@ class BoardItem : public QGraphicsItem private: - dpcore::Layer *image_; - + dpcore::LayerStack *image_; int plastx_, plasty_; }; diff --git a/src/client/brushpreview.cpp b/src/client/brushpreview.cpp index 82324f3c5..725a00cc5 100644 --- a/src/client/brushpreview.cpp +++ b/src/client/brushpreview.cpp @@ -23,6 +23,7 @@ #include #include "core/point.h" +#include "core/layerstack.h" #include "core/layer.h" #include "brushpreview.h" @@ -90,14 +91,17 @@ void BrushPreview::paintEvent(QPaintEvent *event) void BrushPreview::updatePreview() { if(preview_==0) { - preview_ = new dpcore::Layer(QColor(0,0,0), contentsRect().size()); + preview_ = new dpcore::LayerStack(); + preview_->addLayer("", QColor(0,0,0), contentsRect().size()); } else if(preview_->width() != contentsRect().width() || preview_->height() != contentsRect().height()) { // TODO resize more nicely delete preview_; - preview_ = new dpcore::Layer(QColor(0,0,0), contentsRect().size()); + preview_ = new dpcore::LayerStack(); + preview_->addLayer("", QColor(0,0,0), contentsRect().size()); } + dpcore::Layer *layer = preview_->getLayerByIndex(0); - preview_->fillChecker(palette().light().color(), palette().mid().color()); + layer->fillChecker(palette().light().color(), palette().mid().color()); const int strokew = preview_->width() - preview_->width()/4; const int strokeh = preview_->height() / 4; @@ -114,7 +118,7 @@ void BrushPreview::updatePreview() const qreal fx = x/qreal(strokew); const qreal pressure = qBound(0.0, ((fx*fx) - (fx*fx*fx))*6.756, 1.0); const int y = qRound(sin(phase) * strokeh); - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx+lastx,offy+lasty, lastp), dpcore::Point(offx+x, offy+y, pressure), distance); lastx = x; @@ -122,25 +126,25 @@ void BrushPreview::updatePreview() lastp = pressure; } } else if(shape_ == Line) { - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx, offy, 1), dpcore::Point(offx+strokew, offy, 1), distance ); } else { - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx, offy-strokeh, 1), dpcore::Point(offx+strokew, offy-strokeh, 1), distance); - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx+strokew, offy-strokeh, 1), dpcore::Point(offx+strokew, offy+strokeh, 1), distance); - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx+strokew, offy+strokeh, 1), dpcore::Point(offx, offy+strokeh, 1), distance); - preview_->drawLine(brush_, + layer->drawLine(brush_, dpcore::Point(offx, offy+strokeh, 1), dpcore::Point(offx, offy-strokeh, 1), distance); diff --git a/src/client/brushpreview.h b/src/client/brushpreview.h index 2d2fbfed2..42cdccb75 100644 --- a/src/client/brushpreview.h +++ b/src/client/brushpreview.h @@ -25,7 +25,7 @@ #include "core/brush.h" namespace dpcore { - class Layer; + class LayerStack; } #ifndef DESIGNER_PLUGIN @@ -116,7 +116,7 @@ class PLUGIN_EXPORT BrushPreview : public QFrame { dpcore::Brush brush_; //QImage preview_; - dpcore::Layer *preview_; + dpcore::LayerStack *preview_; QPixmap bg_; bool sizepressure_; bool opacitypressure_; diff --git a/src/client/controller.cpp b/src/client/controller.cpp index 5cd76e4f9..921f6ddf1 100644 --- a/src/client/controller.cpp +++ b/src/client/controller.cpp @@ -70,6 +70,8 @@ void Controller::setModel(drawingboard::Board *board) toolbox_.aeditor(), SLOT(setSelection(drawingboard::AnnotationItem*))); connect(board, SIGNAL(annotationDeleted(drawingboard::AnnotationItem*)), toolbox_.aeditor(), SLOT(unselect(drawingboard::AnnotationItem*))); + // TODO testing... + //toolbox_.editor()->setLayer(1); } /** @@ -445,6 +447,21 @@ void Controller::setTool(tools::Type tool) tool_ = toolbox_.get(tool); } +void Controller::selectLayer(int id) +{ + toolbox_.editor()->setLayer(id); +} + +void Controller::newLayer(const QString& name) +{ + toolbox_.editor()->createLayer(name); +} + +void Controller::deleteLayer(int id, bool mergedown) +{ + toolbox_.editor()->deleteLayer(id); +} + void Controller::penDown(const dpcore::Point& point) { if(lock_ == false || (lock_ && tool_->readonly())) { diff --git a/src/client/controller.h b/src/client/controller.h index c9efe4cbe..d5d4bf60f 100644 --- a/src/client/controller.h +++ b/src/client/controller.h @@ -107,6 +107,14 @@ class Controller : public QObject //! Send chat message void sendChat(const QString& msg); + //! Select a layer + void selectLayer(int id); + + //! Add a new blank layer + void newLayer(const QString& name); + //! Delete or merge a layer + void deleteLayer(int id, bool mergedown); + void penDown(const dpcore::Point& point); void penMove(const dpcore::Point& point); void penUp(); diff --git a/src/client/core/brush.cpp b/src/client/core/brush.cpp index a31e41d0c..bcdaf53ed 100644 --- a/src/client/core/brush.cpp +++ b/src/client/core/brush.cpp @@ -69,7 +69,7 @@ void Brush::setRadius(int radius) Q_ASSERT(radius>=0); radius1_ = radius; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -82,7 +82,7 @@ void Brush::setHardness(qreal hardness) Q_ASSERT(hardness>=0 && hardness<=1); hardness1_ = hardness; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -95,7 +95,7 @@ void Brush::setOpacity(qreal opacity) Q_ASSERT(opacity>=0 && opacity<=1); opacity1_ = opacity; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -118,7 +118,7 @@ void Brush::setRadius2(int radius) Q_ASSERT(radius>=0); radius2_ = radius; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -131,7 +131,7 @@ void Brush::setHardness2(qreal hardness) Q_ASSERT(hardness>=0 && hardness<=1); hardness2_ = hardness; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -144,7 +144,7 @@ void Brush::setOpacity2(qreal opacity) Q_ASSERT(opacity>=0 && opacity<=1); opacity2_ = opacity; checkSensitivity(); - cache_ = RenderedBrush(); + cache_ = BrushMask(); } /** @@ -286,7 +286,7 @@ int Brush::spacing() const * @param pressure brush pressue [0..1] * @return diameter^2 pixel values */ -RenderedBrush Brush::render(qreal pressure) const { +BrushMask Brush::render(qreal pressure) const { const int dia = diameter(pressure)+1; const qreal o = opacity(pressure); @@ -305,7 +305,7 @@ RenderedBrush Brush::render(qreal pressure) const { memset(lookup+i, int(255*o), int(ceil(rr)-i)); // Render the brush - cache_ = RenderedBrush(dia, pressure); + cache_ = BrushMask(dia, pressure); uchar *ptr = cache_.data(); for(int y=0;y= 0 && x<= 1); Q_ASSERT(y>= 0 && y<= 1); - const RenderedBrush rb = render(pressure); + const BrushMask rb = render(pressure); if(x==0 && y==0) return rb; const int dia = rb.diameter(); - RenderedBrush b(dia, pressure); + BrushMask b(dia, pressure); qreal kernel[] = {x*y, (1.0-x)*y, x*(1.0-y), (1.0-x)*(1.0-y)}; Q_ASSERT(fabs(kernel[0]+kernel[1]+kernel[2]+kernel[3]-1.0)<0.001); @@ -395,7 +395,7 @@ bool Brush::operator!=(const Brush& brush) const * Copy the data from an existing brush. A brush data is guaranteed * to contain a pixel buffer. */ -RenderedBrushData::RenderedBrushData(const RenderedBrushData& other) +BrushMaskData::BrushMaskData(const BrushMaskData& other) : dia(other.dia), pressure(other.pressure) { data = new uchar[dia*dia]; @@ -409,8 +409,8 @@ RenderedBrushData::RenderedBrushData(const RenderedBrushData& other) * @param dia diameter of the new brush * @param pressure the pressure value at which the brush was rendered */ -RenderedBrush::RenderedBrush(int dia, qreal pressure) - : d(new RenderedBrushData) +BrushMask::BrushMask(int dia, qreal pressure) + : d(new BrushMaskData) { Q_ASSERT(dia>0); d->data = new uchar[dia*dia]; @@ -424,7 +424,7 @@ RenderedBrush::RenderedBrush(int dia, qreal pressure) * 1. it contains data * 2. it's pressure value is the same as @parma pressure iff sensitive is true. */ -bool RenderedBrush::isFresh(qreal pressure, bool sensitive) const { +bool BrushMask::isFresh(qreal pressure, bool sensitive) const { if(sensitive) return d.constData() && int(pressure*PRESSURE_LEVELS)==d->pressure; else diff --git a/src/client/core/brush.h b/src/client/core/brush.h index e606548f5..35bd6432c 100644 --- a/src/client/core/brush.h +++ b/src/client/core/brush.h @@ -27,23 +27,23 @@ namespace dpcore { class Point; -struct RenderedBrushData : public QSharedData +struct BrushMaskData : public QSharedData { - RenderedBrushData() : data(0), dia(0), pressure(-1) { } - RenderedBrushData(const RenderedBrushData& other); - ~RenderedBrushData() { delete [] data; } + BrushMaskData() : data(0), dia(0), pressure(-1) { } + BrushMaskData(const BrushMaskData& other); + ~BrushMaskData() { delete [] data; } uchar *data; int dia; int pressure; }; -//! A rendered brush +//! A brush mask /** * This is an implicitly shared class that holds the alpha map of the * brush shape. */ -class RenderedBrush +class BrushMask { public: //! Number of pressure levels supported. @@ -54,10 +54,10 @@ class RenderedBrush static const int PRESSURE_LEVELS = 256; //! Create an empty brush - RenderedBrush() : d(0) { } + BrushMask() : d(0) { } //! Create a new rendered brush - RenderedBrush(int dia, qreal pressure); + BrushMask(int dia, qreal pressure); //! Is this brush still valid bool isFresh(qreal pressure, bool sensitive) const; @@ -72,7 +72,7 @@ class RenderedBrush int diameter() const { return d->dia; } private: - QSharedDataPointer d; + QSharedDataPointer d; }; //! A brush for drawing onto a layer @@ -135,10 +135,10 @@ class Brush int blendingMode() const { return blend_; } //! Render the brush - RenderedBrush render(qreal pressure) const; + BrushMask render(qreal pressure) const; //! Render the brush with an offset - RenderedBrush render_subsampled(qreal x, qreal y, qreal pressure) const; + BrushMask render_subsampled(qreal x, qreal y, qreal pressure) const; //! Equality test bool operator==(const Brush& brush) const; @@ -159,7 +159,7 @@ class Brush bool subpixel_; int blend_; - mutable RenderedBrush cache_; + mutable BrushMask cache_; }; } diff --git a/src/client/core/layer.cpp b/src/client/core/layer.cpp index 59c6b3463..3d59a144d 100644 --- a/src/client/core/layer.cpp +++ b/src/client/core/layer.cpp @@ -17,10 +17,12 @@ along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +#include #include #include #include +#include "layerstack.h" #include "layer.h" #include "tile.h" #include "brush.h" @@ -28,27 +30,57 @@ namespace dpcore { -Layer::Layer(const QImage& image) { - width_ = image.width(); - height_ = image.height(); +void Layer::init(LayerStack *owner, int id, const QString& name, const QSize& size) +{ + owner_ = owner; + id_ = id; + name_ = name; + width_ = size.width(); + height_ = size.height(); xtiles_ = width_ / Tile::SIZE + ((width_ % Tile::SIZE)>0); ytiles_ = height_ / Tile::SIZE + ((height_ % Tile::SIZE)>0); tiles_ = new Tile*[xtiles_ * ytiles_]; +} + +/** + * Construct a layer whose content is loaded from the provided image. + * @param owner the stack to which this layer belongs to + * @param id layer ID + * @param image image on which the layer is based + */ +Layer::Layer(LayerStack *owner, int id, const QString& name, const QImage& image) { + init(owner, id, name, image.size()); for(int y=0;y0); - ytiles_ = height_ / Tile::SIZE + ((height_ % Tile::SIZE)>0); - tiles_ = new Tile*[xtiles_ * ytiles_]; +/** + * Construct a layer initialized to a solid color + * @param owner the stack to which this layer belongs to + * @param id layer ID + * @param color layer color + * @parma size layer size + */ +Layer::Layer(LayerStack *owner, int id, const QString& name, const QColor& color, const QSize& size) { + init(owner, id, name, size); for(int y=0;ycopyToImage(image); - - return image; -} - -void Layer::paint(const QRectF& rect, QPainter *painter) { - const int top = qMax(int(rect.top()), 0); - const int left = qMax(int(rect.left()), 0); - const int right = Tile::roundTo(qMin(int(rect.right()), width_)); - const int bottom = Tile::roundTo(qMin(int(rect.bottom()), height_)); - - painter->save(); - painter->setClipRect(rect); - for(int y=top;ypaint(painter, - QPoint(xindex*Tile::SIZE, yindex*Tile::SIZE)); - } + QImage image(width_, height_, QImage::Format_ARGB32_Premultiplied); + for(int i=0;icopyToImage(image); } - painter->restore(); + return image; } /** @@ -89,7 +103,7 @@ void Layer::paint(const QRectF& rect, QPainter *painter) { * @param y * @return invalid color if x or y is outside image boundaries */ -QColor Layer::colorAt(int x, int y) +QColor Layer::colorAt(int x, int y) const { if(x<0 || y<0 || x>=width_ || y>=height_) return QColor(); @@ -118,9 +132,9 @@ void Layer::dab(const Brush& brush, const Point& point) return; // Render the brush - RenderedBrush rb = brush.subpixel()?brush.render_subsampled(point.xFrac(), point.yFrac(), point.pressure()):brush.render(point.pressure()); - const int realdia = rb.diameter(); - const uchar *values = rb.data(); + BrushMask bm = brush.subpixel()?brush.render_subsampled(point.xFrac(), point.yFrac(), point.pressure()):brush.render(point.pressure()); + const int realdia = bm.diameter(); + const uchar *values = bm.data(); QColor color = brush.color(point.pressure()); // A single dab can (and often does) span multiple tiles. @@ -138,8 +152,10 @@ void Layer::dab(const Brush& brush, const Point& point) const int xindex = x / Tile::SIZE; const int xt = x - xindex * Tile::SIZE; const int wb = xt+realdia-xb < Tile::SIZE ? realdia-xb : Tile::SIZE-xt; - - tiles_[xtiles_ * yindex + xindex]->composite( + const int i = xtiles_ * yindex + xindex; + if(tiles_[i]==0) + tiles_[i] = new Tile(xindex, yindex); + tiles_[i]->composite( brush.blendingMode(), values + yb * realdia + xb, color, @@ -148,6 +164,9 @@ void Layer::dab(const Brush& brush, const Point& point) realdia-wb ); + if(owner_) + owner_->markDirty(xindex, yindex); + x = (xindex+1) * Tile::SIZE; xb = xb + wb; } @@ -281,7 +300,12 @@ void Layer::merge(int x, int y, const Layer *layer) int myy = y; for(int i=0;iytiles_&&myyxtiles_;++j&&myxmerge(layer->tiles_[layer->xtiles_*i+j]); + if(owner_) + owner_->markDirty(myx, myy); } myx = x; } diff --git a/src/client/core/layer.h b/src/client/core/layer.h index 7b24208c8..cd187e214 100644 --- a/src/client/core/layer.h +++ b/src/client/core/layer.h @@ -20,16 +20,19 @@ #ifndef LAYER_H #define LAYER_H +#include + class QImage; -class QColor; class QPainter; class QRectF; +class QSize; namespace dpcore { class Brush; class Point; class Tile; +class LayerStack; //! A drawing layer/tile manager /** @@ -40,10 +43,13 @@ class Tile; class Layer { public: //! Construct a layer from an image - Layer(const QImage& image); + Layer(LayerStack *owner, int id, const QString& name, const QImage& image); + + //! Construct a blank layer + Layer(LayerStack *owner, int id, const QString& name, const QColor& color, const QSize& size); - //! Construct an empty layer - Layer(const QColor& color, const QSize& size); + //! Construct a blank layer + Layer(LayerStack *owner, int id, const QString& name, const QSize& size); ~Layer(); @@ -53,17 +59,20 @@ class Layer { //! Get the layer height in pixels int height() const { return height_; } + //! Get the layer ID + int id() const { return id_; } + + //! Get the layer name + const QString& name() const { return name_; } + //! Get the layer as an image QImage toImage() const; //! Resize this layer //void resize(const QSize& newsize); - //! Paint an area of this layer - void paint(const QRectF& rect, QPainter *painter); - //! Get the color at the specified coordinate - QColor colorAt(int x, int y); + QColor colorAt(int x, int y) const; //! Dab the layer with a brush void dab(const Brush& brush, const Point& point); @@ -83,11 +92,22 @@ class Layer { //! Fill the layer with a checker pattern void fillChecker(const QColor& dark, const QColor& light); + //! Get a tile + const Tile *tile(int x, int y) const { return tiles_[y*xtiles_+x]; } + + //! Get a tile + const Tile *tile(int index) const { return tiles_[index]; } + private: + LayerStack *owner_; + void init(LayerStack *owner, int id, const QString& name, const QSize& size); + int width_; int height_; int xtiles_; int ytiles_; + int id_; + QString name_; Tile **tiles_; }; diff --git a/src/client/core/layerstack.cpp b/src/client/core/layerstack.cpp new file mode 100644 index 000000000..9db9fa0a2 --- /dev/null +++ b/src/client/core/layerstack.cpp @@ -0,0 +1,251 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include +#include + +#include "layer.h" +#include "layerstack.h" +#include "tile.h" +#include "rasterop.h" + +namespace dpcore { + +LayerStack::LayerStack(QObject *parent) : + QAbstractListModel(parent), width_(-1), height_(-1), lastid_(0) +{ +} + +LayerStack::~LayerStack() +{ + foreach(Layer *l, layers_) + delete l; +} + +void LayerStack::init(const QSize& size) +{ + width_ = size.width(); + height_ = size.height(); + xtiles_ = width_ / Tile::SIZE + ((width_ % Tile::SIZE)>0); + ytiles_ = height_ / Tile::SIZE + ((height_ % Tile::SIZE)>0); + cache_ = new QPixmap[xtiles_*ytiles_]; +} + +Layer *LayerStack::addLayer(const QString& name, const QImage& image) +{ + if(width_ != -1) { + // TODO, check that image dimensions match + } else { + init(image.size()); + } + Layer *nl = new Layer(this, lastid_++, name, image); + beginInsertRows(QModelIndex(),1,1); + layers_.append(nl); + endInsertRows(); + return nl; +} + +Layer *LayerStack::addLayer(const QString& name, const QColor& color, const QSize& size) +{ + if(width_ != -1) { + // TODO, check that image dimensions match + } else { + init(size); + } + Layer *nl = new Layer(this, lastid_++, name, color, size); + beginInsertRows(QModelIndex(),1,1); + layers_.append(nl); + endInsertRows(); + return nl; +} + +Layer *LayerStack::addLayer(const QString& name, const QSize& size) +{ + if(width_ != -1) { + // TODO, check that image dimensions match + } else { + init(size); + } + Layer *nl = new Layer(this, lastid_++, name, size); + beginInsertRows(QModelIndex(),1,1); + layers_.append(nl); + endInsertRows(); + return nl; +} + +/** + * @param id layer ID + * @return true if layer was found and deleted + */ +bool LayerStack::deleteLayer(int id) +{ + for(int i=0;iid() == id) { + // Invalidate cache + Layer *l = layers_.at(i); + for(int j=0;jtile(j)) + cache_[j] = QPixmap(); + } + // Remove the layer + int row = layers() - i; + beginRemoveRows(QModelIndex(), row, row); + layers_.removeAt(i); + endRemoveRows(); + return true; + } + } + return false; +} + +Layer *LayerStack::getLayerByIndex(int index) +{ + return layers_.at(index); +} + +Layer *LayerStack::getLayer(int id) +{ + // Since the expected number of layers is always fairly low, + // we can get away with a simple linear search. (Note that IDs + // may appear in random order due to layers being moved around.) + foreach(Layer *l, layers_) + if(l->id() == id) + return l; + return 0; +} + +/** + * @param id layer id + * @return layer index. Returns a negative index if layer is not found + */ +int LayerStack::id2index(int id) const +{ + for(int i=0;iid() == id) + return i; + return -1; +} + +/** + * Paint a view of the layer stack. The layers are composited + * together according to their options. + * @param rect area of image to paint + * @param painter painter to use + */ +void LayerStack::paint(const QRectF& rect, QPainter *painter) +{ + const int top = qMax(int(rect.top()), 0); + const int left = qMax(int(rect.left()), 0); + const int right = Tile::roundTo(qMin(int(rect.right()), width_)); + const int bottom = Tile::roundTo(qMin(int(rect.bottom()), height_)); + + painter->save(); + painter->setClipRect(rect); + for(int y=top;ydrawPixmap(QPoint(xindex*Tile::SIZE, yindex*Tile::SIZE), + cache_[i]); + } + } + painter->restore(); + +} + +QColor LayerStack::colorAt(int x, int y) const +{ + // TODO merge + return layers_.at(0)->colorAt(x, y); +} + +QImage LayerStack::toFlatImage() const +{ + // TODO + return layers_.at(0)->toImage(); +} + +// Flatten a single tile +void LayerStack::flattenTile(quint32 *data, int xindex, int yindex) +{ + // Start out with the lowermost visible layer + memcpy(data, layers_.at(0)->tile(xindex, yindex)->data(), Tile::BYTES); + + // Composite remaining layers + for(int i=1;itile(xindex, yindex); + if(tile) { + compositePixels(0, data, tile->data(), Tile::SIZE*Tile::SIZE); + } + } +} + +// Update the paint cache. The layers are composited together +// according to their blend mode and opacity options. +void LayerStack::updateCache(int xindex, int yindex) +{ + quint32 data[Tile::SIZE*Tile::SIZE]; + flattenTile(data, xindex, yindex); + cache_[yindex*xtiles_+xindex] = QPixmap::fromImage( + QImage(reinterpret_cast(data), Tile::SIZE, Tile::SIZE, + QImage::Format_RGB32) + ); + +} + +void LayerStack::markDirty(int tilex, int tiley) +{ + cache_[tiley*xtiles_ + tilex] = QPixmap(); +} + +// Model related functions +QVariant LayerStack::data(const QModelIndex& index, int role) const +{ + // Always display one extra layer (for adding new layers) + if(index.row() >= 0 && index.row() <= layers() && role == Qt::DisplayRole) { + if(index.row()==0) + return "New"; + // Display the layers in reverse order (topmost layer first) + int row = layers() - index.row(); + return QVariant::fromValue(layers_.at(row)); + } + return QVariant(); + +} + +Qt::ItemFlags LayerStack::flags(const QModelIndex& index) const +{ + if(index.row()==0) + return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled; + else + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; +} + +int LayerStack::rowCount(const QModelIndex& parent) const +{ + if(parent.isValid()) + return 0; + return layers() + 1; +} + +} diff --git a/src/client/core/layerstack.h b/src/client/core/layerstack.h new file mode 100644 index 000000000..c2f1c03bc --- /dev/null +++ b/src/client/core/layerstack.h @@ -0,0 +1,110 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#ifndef LAYERSTACK_H +#define LAYERSTACK_H + +#include +#include +#include + +namespace dpcore { + +class Layer; + +/** + * A stack of layers. The stack also provides a list model so it can be + * easily manipulated using Qt's MVC framework. + * The model always includes one null layer which can be used as an + * anchor point for new layers. + */ +class LayerStack : public QAbstractListModel { + Q_OBJECT + public: + LayerStack(QObject *parent=0); + ~LayerStack(); + + //! Add a new image as a layer to the top of the stack + Layer *addLayer(const QString& name, const QImage& image); + + //! Add a new layer of solid color to the top of the stack + Layer *addLayer(const QString& name, const QColor& color, const QSize& size); + + //! Add a new empty layre to the top of the stack + Layer *addLayer(const QString& name, const QSize& size); + + //! Delete a layer + bool deleteLayer(int id); + + //! Get the number of layers in the stack + int layers() const { return layers_.count(); } + + //! Get a layer by its index + Layer *getLayerByIndex(int index); + + //! Get a layer by its ID + Layer *getLayer(int id); + + //! Get the index of the specified layer + int id2index(int id) const; + + //! Get the width of the layer stack + int width() const { return width_; } + + //! Get the height of the layer stack + int height() const { return height_; } + + //! Get the width and height of the layer stack + QSize size() const { return QSize(width_, height_); } + + //! Paint an area of this layer stack + void paint(const QRectF& rect, QPainter *painter); + + //! Get the merged color value at the point + QColor colorAt(int x, int y) const; + + //! Return a flattened image of the layer stack + QImage toFlatImage() const; + + //! Mark a tile whose content has changed + void markDirty(int tilex, int tiley); + + // List model functions + int rowCount(const QModelIndex& parent) const; + QVariant data(const QModelIndex& index, int role) const; + Qt::ItemFlags flags(const QModelIndex& index) const; + + private: + void init(const QSize& size); + void flattenTile(quint32 *data, int xindex, int yindex); + void updateCache(int xindex, int yindex); + + int width_, height_; + int xtiles_, ytiles_; + QList layers_; + QPixmap *cache_; + int lastid_; +}; + +} + +Q_DECLARE_METATYPE(dpcore::Layer*) + +#endif + diff --git a/src/client/core/rasterop.cpp b/src/client/core/rasterop.cpp index c766684de..2dc58c184 100644 --- a/src/client/core/rasterop.cpp +++ b/src/client/core/rasterop.cpp @@ -121,7 +121,7 @@ void doComposite(quint32 *base, quint32 color, const uchar *mask, *dest = UINT8_BLEND(BO(*dest, src[0]), *dest, *mask); ++dest; *dest = UINT8_BLEND(BO(*dest, src[1]), *dest, *mask); ++dest; *dest = UINT8_BLEND(BO(*dest, src[2]), *dest, *mask); ++dest; - ++dest; + *dest = *dest + UINT8_MULT(255-*dest, *mask); ++dest; ++mask; } dest += baseskip*4; @@ -169,10 +169,10 @@ void compositePixels(int mode, quint32 *base, const quint32 *over, int len) uchar *dest = reinterpret_cast(base); const uchar *src = reinterpret_cast(over); while(len--) { - *dest = UINT8_BLEND(src[0], *dest, src[3]); ++dest; - *dest = UINT8_BLEND(src[1], *dest, src[3]); ++dest; - *dest = UINT8_BLEND(src[2], *dest, src[3]); ++dest; - ++dest; + *dest = src[0] + UINT8_MULT(255-src[3], *dest); ++dest; + *dest = src[1] + UINT8_MULT(255-src[3], *dest); ++dest; + *dest = src[2] + UINT8_MULT(255-src[3], *dest); ++dest; + *dest = *dest + UINT8_MULT(255-*dest, src[3]); ++dest; src+=4; } diff --git a/src/client/core/tile.cpp b/src/client/core/tile.cpp index 516e2ced3..ae0e67f4e 100644 --- a/src/client/core/tile.cpp +++ b/src/client/core/tile.cpp @@ -36,6 +36,12 @@ Tile::Tile(const QColor& color, int x, int y) *(ptr++) = col; } +Tile::Tile(int x, int y) + : x_(x), y_(y), data_(new quint32[SIZE*SIZE]) +{ + memset(data_, 0, SIZE*SIZE*sizeof(quint32)); +} + /** * Copy all pixel data from (x*SIZE, y*SIZE, (x+1)*SIZE, (y+1)*SIZE). * Pixels outside the source image are set to zero @@ -97,18 +103,6 @@ quint32 Tile::pixel(int x,int y) const { return *(data_ + y * SIZE + x); } -/** - * @param painter painter to paint the tile with - * @param target where to paint the tile - */ -void Tile::paint(QPainter *painter, const QPoint& target) const { - if(cache_.isNull()) { - cache_ = QPixmap::fromImage(QImage(reinterpret_cast(data_), - SIZE, SIZE, QImage::Format_RGB32)); - } - painter->drawPixmap(target, cache_); -} - /** * @param values array of alpha values * @param color composite color @@ -124,13 +118,11 @@ void Tile::composite(int mode, const uchar *values, const QColor& color, int x, Q_ASSERT((x+w)<=SIZE && (y+h)<=SIZE); compositeMask(mode, data_ + y * SIZE + x, color.rgba(), values, w, h, skip, SIZE-w); - cache_ = QPixmap(); } void Tile::merge(const Tile *tile) { compositePixels(0, data_, tile->data_, SIZE*SIZE); - cache_ = QPixmap(); } } diff --git a/src/client/core/tile.h b/src/client/core/tile.h index 8c89d86c1..960bc3f17 100644 --- a/src/client/core/tile.h +++ b/src/client/core/tile.h @@ -36,6 +36,7 @@ namespace dpcore { class Tile { public: static const int SIZE = 64; + static const int BYTES = SIZE * SIZE * sizeof(quint32); //! Round i upwards to SIZE boundary static int roundTo(int i) { @@ -48,6 +49,9 @@ class Tile { //! Construct a tile from an image Tile(const QImage& image, int x, int y); + //! Construct an empty tile + Tile(int x, int y); + ~Tile(); //! Get tile X index @@ -65,19 +69,18 @@ class Tile { //! Composite another tile with this tile void merge(const Tile *tile); - //! Paint this tile //! Copy the contents of this tile onto the appropriate spot on an image void copyToImage(QImage& image) const; - void paint(QPainter *painter, const QPoint& target) const; - //! Fill this tile with a checker pattern void fillChecker(const QColor& dark, const QColor& light); + //! Get read access to the raw pixel data + const quint32 *data() const { return data_; } + private: int x_, y_; quint32 *data_; - mutable QPixmap cache_; }; } diff --git a/src/client/layerlistdelegate.cpp b/src/client/layerlistdelegate.cpp new file mode 100644 index 000000000..e3376eb35 --- /dev/null +++ b/src/client/layerlistdelegate.cpp @@ -0,0 +1,97 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include +#include +#include +#include "layerlistdelegate.h" +#include "core/layerstack.h" +#include "core/layer.h" +#include "icons.h" + +LayerListDelegate::LayerListDelegate(QObject *parent) + : QItemDelegate(parent) +{ +} + +LayerListDelegate::~LayerListDelegate() +{ +} + +void LayerListDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + QStyleOptionViewItemV2 opt = setOptions(index, option); + const QStyleOptionViewItemV2 *v2 = qstyleoption_cast(&option); + opt.features = v2 ? v2->features : QStyleOptionViewItemV2::ViewItemFeatures(QStyleOptionViewItemV2::None); + + // Background + drawBackground(painter, opt, index); + + QRect textrect = opt.rect; + + if(index.row()==0) { + // Draw add new layer button + opt.font.setStyle(QFont::StyleItalic); + opt.displayAlignment = Qt::AlignHCenter; + drawDisplay(painter, opt, textrect, "New layer..."); + } else { + const dpcore::Layer *layer = index.data().value(); + QString name = QString("%1 %2 %3%").arg(layer->name()).arg("Normal").arg(100); + + const int delwidth = icon::kick().actualSize(QSize(16,16)).width(); + // Draw layer name + textrect.setWidth(textrect.width() - delwidth); + drawDisplay(painter, opt, textrect, name); + + // Draw delete button (except when in a network session) + // TODO correct icon + painter->drawPixmap(opt.rect.topRight()-QPoint(delwidth,0), + icon::kick().pixmap(16)); + } +} + +#if 0 +QSize LayerListDelegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index ) const +{ + QSize size = QItemDelegate::sizeHint(option, index); + const QSize iconsize = icon::lock().actualSize(QSize(16,16)); + if(size.height() < iconsize.height()) + size.setHeight(iconsize.height()); + return size; +} +#endif + +bool LayerListDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, const QModelIndex &index) +{ + const int btnwidth = icon::lock().actualSize(QSize(16,16)).width(); + if(event->type() == QEvent::MouseButtonRelease) { + const QMouseEvent *me = static_cast(event); + if(index.row()==0) { + emit newLayer(); + } else { + const dpcore::Layer *layer = index.data().value(); + if(me->x() >= option.rect.width() - btnwidth) + emit deleteLayer(layer); + } + } + return QItemDelegate::editorEvent(event, model, option, index); +} + diff --git a/src/client/layerlistdelegate.h b/src/client/layerlistdelegate.h new file mode 100644 index 000000000..6910e33cb --- /dev/null +++ b/src/client/layerlistdelegate.h @@ -0,0 +1,49 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#ifndef LAYERLISTMODEL_H +#define LAYERLISTMODEL_H + +#include +#include + +namespace dpcore { + class Layer; +} + +class LayerListDelegate : public QItemDelegate { + Q_OBJECT + public: + LayerListDelegate(QObject *parent=0); + ~LayerListDelegate(); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const; + //QSize sizeHint(const QStyleOptionViewItem & option, + //const QModelIndex & index ) const; + bool editorEvent(QEvent *event, QAbstractItemModel *model, + const QStyleOptionViewItem &option, const QModelIndex &index); + + signals: + void newLayer(); + void deleteLayer(const dpcore::Layer *layer); +}; + +#endif + diff --git a/src/client/layerlistwidget.cpp b/src/client/layerlistwidget.cpp new file mode 100644 index 000000000..1a0355a33 --- /dev/null +++ b/src/client/layerlistwidget.cpp @@ -0,0 +1,115 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#include +#include +#include +#include +#include +#include + +#include "layerlistwidget.h" +#include "layerlistdelegate.h" +#include "board.h" +#include "ui_layerbox.h" +#include "core/layerstack.h" +#include "core/layer.h" + +namespace widgets { + +LayerList::LayerList(QWidget *parent) + : QDockWidget(tr("Layers"), parent) +{ + ui_ = new Ui_LayerBox; + QWidget *w = new QWidget(this); + setWidget(w); + ui_->setupUi(w); + + //model_ = new LayerListModel(this); + //ui_->layers->setModel(model_); + LayerListDelegate *del = new LayerListDelegate(this); + ui_->layers->setItemDelegate(del); + connect(del, SIGNAL(newLayer()), this, SLOT(newLayer())); + connect(del, SIGNAL(deleteLayer(const dpcore::Layer*)), this, + SLOT(deleteLayer(const dpcore::Layer*))); +} + +LayerList::~LayerList() +{ +} + +void LayerList::setBoard(drawingboard::Board *board) +{ + ui_->layers->setModel(board->layers()); + connect(ui_->layers->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(selected(const QItemSelection&, const QItemSelection&))); +} + +void LayerList::selectLayer(int id) +{ + dpcore::LayerStack *layers = static_cast(ui_->layers->model()); + int row = layers->layers() - layers->id2index(id); + QModelIndexList sel = ui_->layers->selectionModel()->selectedIndexes(); + if(sel.isEmpty() || sel.first().row() != row) + ui_->layers->selectionModel()->select(layers->index(row,0), + QItemSelectionModel::Select); +} + +void LayerList::selected(const QItemSelection& selection, const QItemSelection& prev) +{ + if(selection.indexes().isEmpty()) { + // A layer must always be selected + ui_->layers->selectionModel()->select(prev.indexes().first(), + QItemSelectionModel::Select); + } else { + dpcore::LayerStack *layers = static_cast(ui_->layers->model()); + emit selected(layers->layers() - selection.indexes().first().row()); + } +} + + +void LayerList::newLayer() +{ + bool ok; + QString name = QInputDialog::getText(this, tr("Add a new layer"), + tr("Layer name:"), QLineEdit::Normal, "", &ok); + if(ok) { + if(name.isEmpty()) + name = tr("Unnamed layer"); + emit newLayer(name); + } +} + +void LayerList::deleteLayer(const dpcore::Layer *layer) +{ + QMessageBox box(QMessageBox::Question, tr("Delete layer"), + tr("Really delete %1?").arg(layer->id()), + QMessageBox::NoButton, this); + box.setInformativeText(tr("Press merge down to merge the layer with the first visible layer below it instead of deleting.")); + box.addButton(tr("Delete"), QMessageBox::DestructiveRole); + QPushButton *merge = box.addButton(tr("Merge down"), QMessageBox::DestructiveRole); + QPushButton *cancel = box.addButton(tr("Cancel"), QMessageBox::RejectRole); + box.setDefaultButton(cancel); + box.exec(); + QAbstractButton *choice = box.clickedButton(); + if(choice != cancel) + emit deleteLayer(layer->id(), choice==merge); +} + +} + diff --git a/src/client/layerlistwidget.h b/src/client/layerlistwidget.h new file mode 100644 index 000000000..7505590ae --- /dev/null +++ b/src/client/layerlistwidget.h @@ -0,0 +1,71 @@ +/* + DrawPile - a collaborative drawing program. + + Copyright (C) 2008 Calle Laakkonen + + This program 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 2, or (at your option) + any later version. + + This program 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 this program; if not, write to the Free Software Foundation, + Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ +#ifndef LAYERLISTWIDGET_H +#define LAYERLISTWIDGET_H + +#include + +class LayerListModel; +class QListView; + +namespace drawingboard { + class Board; +} + +namespace dpcore { + class Layer; +} + +class Ui_LayerBox; +class QItemSelection; + +namespace widgets { + +class LayerList : public QDockWidget +{ + Q_OBJECT + public: + LayerList(QWidget *parent=0); + ~LayerList(); + + void setBoard(drawingboard::Board *board); + + public slots: + void selectLayer(int id); + + signals: + void newLayer(const QString& name); + void deleteLayer(int id, bool mergedown); + void selected(int id); + + private slots: + void newLayer(); + void deleteLayer(const dpcore::Layer* layer); + void selected(const QItemSelection& selection, const QItemSelection& prev); + + private: + Ui_LayerBox *ui_; + LayerListModel *model_; +}; + +} + +#endif + diff --git a/src/client/mainwindow.cpp b/src/client/mainwindow.cpp index 19fb16243..1a63681a8 100644 --- a/src/client/mainwindow.cpp +++ b/src/client/mainwindow.cpp @@ -47,6 +47,7 @@ #include "controller.h" #include "toolsettingswidget.h" #include "userlistwidget.h" +#include "layerlistwidget.h" #include "chatwidget.h" #include "dualcolorbutton.h" #include "localserver.h" @@ -129,8 +130,8 @@ MainWindow::MainWindow(const MainWindow *source) board_->setBackgroundBrush( palette().brush(QPalette::Active,QPalette::Window)); view_->setBoard(board_); - navigator_->setScene(board_); + // Navigator <-> View connect(navigator_, SIGNAL(focusMoved(const QPoint&)), view_, SLOT(scrollTo(const QPoint&))); @@ -219,6 +220,14 @@ MainWindow::MainWindow(const MainWindow *source) connect(netstatus_, SIGNAL(statusMessage(QString)), chatbox_, SLOT(systemMessage(QString))); + // Layer box -> controller + connect(layerlist_, SIGNAL(newLayer(const QString&)), + controller_, SLOT(newLayer(const QString&))); + connect(layerlist_, SIGNAL(deleteLayer(int, bool)), + controller_, SLOT(deleteLayer(int, bool))); + connect(layerlist_, SIGNAL(selected(int)), + controller_, SLOT(selectLayer(int))); + if(source) cloneSettings(source); else @@ -297,6 +306,8 @@ void MainWindow::postInitBoard(const QString& filename) setTitle(); save_->setEnabled(true); saveas_->setEnabled(true); + layerlist_->setBoard(board_); + board_->setLayerList(layerlist_); } /** @@ -1353,7 +1364,7 @@ void MainWindow::initActions() zoomin_ = makeAction("zoomin", "zoom-in.png",tr("Zoom &in"), QString(), QKeySequence::ZoomIn); zoomout_ = makeAction("zoomout", "zoom-out.png",tr("Zoom &out"), QString(), QKeySequence::ZoomOut); zoomorig_ = makeAction("zoomone", "zoom-original.png",tr("&Normal size"), QString(), QKeySequence(Qt::CTRL + Qt::Key_0)); - rotateorig_ = makeAction("rotatezero", "view-refresh.png",tr("&Reset rotation"), QString(), QKeySequence(Qt::CTRL + Qt::Key_R)); + rotateorig_ = makeAction("rotatezero", "view-refresh.png",tr("&Reset rotation"), tr("Drag the view while holding ctrl-space to rotate"), QKeySequence(Qt::CTRL + Qt::Key_R)); fullscreen_ = makeAction("fullscreen", 0, tr("&Full screen"), QString(), QKeySequence("F11")); fullscreen_->setCheckable(true); @@ -1510,9 +1521,11 @@ void MainWindow::createDocks() createColorBoxes(toggles); createPalette(toggles); createUserList(toggles); + createLayerList(toggles); createNavigator(toggles); tabifyDockWidget(hsv_, rgb_); tabifyDockWidget(hsv_, palette_); + tabifyDockWidget(userlist_, layerlist_); docktoggles_->setMenu(toggles); } @@ -1547,6 +1560,15 @@ void MainWindow::createUserList(QMenu *toggles) addDockWidget(Qt::RightDockWidgetArea, userlist_); } +void MainWindow::createLayerList(QMenu *toggles) +{ + layerlist_ = new widgets::LayerList(this); + layerlist_->setObjectName("layerlistdock"); + layerlist_->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); + toggles->addAction(layerlist_->toggleViewAction()); + addDockWidget(Qt::RightDockWidgetArea, layerlist_); +} + void MainWindow::createPalette(QMenu *toggles) { palette_ = new widgets::PaletteBox(tr("Palette"), this); diff --git a/src/client/mainwindow.h b/src/client/mainwindow.h index d1703093a..5d068ae89 100644 --- a/src/client/mainwindow.h +++ b/src/client/mainwindow.h @@ -39,6 +39,7 @@ namespace widgets { class DualColorButton; class ToolSettings; class UserList; + class LayerList; class ChatBox; class PaletteBox; class ColorBox; @@ -229,6 +230,8 @@ class MainWindow : public QMainWindow { void createToolSettings(QMenu *menu); //! Create user list dock void createUserList(QMenu *menu); + //! Create layer list dock + void createLayerList(QMenu *menu); //! Create palette dock void createPalette(QMenu *menu); //! Create color docks @@ -247,6 +250,7 @@ class MainWindow : public QMainWindow { QSplitter *splitter_; widgets::ToolSettings *toolsettings_; widgets::UserList *userlist_; + widgets::LayerList *layerlist_; widgets::ChatBox *chatbox_; widgets::DualColorButton *fgbgcolor_; diff --git a/src/client/sessionstate.cpp b/src/client/sessionstate.cpp index 579070c1a..0241dea13 100644 --- a/src/client/sessionstate.cpp +++ b/src/client/sessionstate.cpp @@ -225,6 +225,18 @@ void SessionState::sendToolSelect(const dpcore::Brush& brush) } +/** + * @param layer ID of the layer to select + */ +void SessionState::sendLayerSelect(int layer) +{ + host_->sendPacket( protocol::LayerSelect( + host_->localuser_, + layer + ) + ); +} + /** * @param point stroke coordinates to send */ @@ -431,6 +443,16 @@ bool SessionState::handleToolSelect(protocol::ToolSelect *ts) return false; } +bool SessionState::handleLayerSelect(protocol::LayerSelect *ts) +{ + if(bufferdrawing_) { + drawbuffer_.enqueue(ts); + return true; + } + emit layerSelectReceived(ts->user(), ts->layer()); + return false; +} + /** * @param msg StrokeInfo message * @retval true message was buffered, don't delete diff --git a/src/client/sessionstate.h b/src/client/sessionstate.h index b781327c6..90a509af0 100644 --- a/src/client/sessionstate.h +++ b/src/client/sessionstate.h @@ -38,6 +38,7 @@ namespace dpcore { namespace protocol { class Packet; class ToolSelect; + class LayerSelect; class StrokePoint; class StrokeEnd; class BinaryChunk; @@ -105,6 +106,9 @@ class SessionState : public QObject { //! Send a tool select message void sendToolSelect(const dpcore::Brush& brush); + //! Send a layer select message + void sendLayerSelect(int layerId); + //! Send a stroke info message void sendStrokePoint(const dpcore::Point& point); @@ -169,6 +173,9 @@ class SessionState : public QObject { //! Results of a ToolInfo message void toolReceived(int user, const dpcore::Brush& brush); + //! A layer selection + void layerSelectReceived(int user, int id); + //! Results of a StrokeInfo message void strokeReceived(int user, const dpcore::Point& point); @@ -195,6 +202,9 @@ class SessionState : public QObject { //! Handle a tool select bool handleToolSelect(protocol::ToolSelect *ts); + //! Handle layer select + bool handleLayerSelect(protocol::LayerSelect *ls); + //! Handle a stroke bool handleStroke(protocol::StrokePoint *s); diff --git a/src/client/ui/layerbox.ui b/src/client/ui/layerbox.ui new file mode 100644 index 000000000..0c91f2b1d --- /dev/null +++ b/src/client/ui/layerbox.ui @@ -0,0 +1,68 @@ + + LayerBox + + + + 0 + 0 + 166 + 131 + + + + Form + + + + 1 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 3 + + + + + Layer blending mode + + + + + + + Opacity + + + 0 + + + 100 + + + Qt::Horizontal + + + + + + + + + + diff --git a/src/client/user.cpp b/src/client/user.cpp index 0e827eef7..8e44adc96 100644 --- a/src/client/user.cpp +++ b/src/client/user.cpp @@ -1,7 +1,7 @@ /* DrawPile - a collaborative drawing program. - Copyright (C) 2006 Calle Laakkonen + Copyright (C) 2006-2008 Calle Laakkonen This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -20,14 +20,29 @@ #include "user.h" #include "boarditem.h" +#include "layerlistwidget.h" namespace drawingboard { -User::User(int id) - : id_(id), layer_(0), strokestarted_(false) +User::User(BoardItem *board, int id) + : id_(id), board_(board), layerlist_(0), layer_(0), strokestarted_(false) { } +void User::setLayerList(widgets::LayerList *ll) +{ + layerlist_ = ll; + // Synchronize widget with current layer selection + layerlist_->selectLayer(layer_); +} + +void User::setLayerId(int id) +{ + layer_ = id; + if(layerlist_) + layerlist_->selectLayer(id); +} + /** * Starts or continues a stroke. If continuing, a line is drawn from the * previous coordinates to \a point. @@ -35,10 +50,11 @@ User::User(int id) */ void User::addStroke(const dpcore::Point& point) { - if(layer_) { + if(board_) { if(strokestarted_) { // Continuing stroke - layer_->drawLine( + board_->drawLine( + layer_, lastpoint_, point, brush_, @@ -46,7 +62,7 @@ void User::addStroke(const dpcore::Point& point) ); } else { // First point - layer_->drawPoint(point, brush_); + board_->drawPoint(layer_, point, brush_); strokestarted_ = true; strokelen_ = 0; } diff --git a/src/client/user.h b/src/client/user.h index 84720254f..3953f37ac 100644 --- a/src/client/user.h +++ b/src/client/user.h @@ -23,6 +23,10 @@ #include "core/point.h" #include "core/brush.h" +namespace widgets { + class LayerList; +} + namespace drawingboard { class BoardItem; @@ -35,16 +39,25 @@ class BoardItem; class User { public: - User(int id); + User(BoardItem *board, int id); //! Get the user's ID number int id() const { return id_; } + //! Set the drawing board + void setBoard(BoardItem *board) { board_ = board; } + + //! Set the layer list widget to update (for local user) + void setLayerList(widgets::LayerList *ll); + //! Set the layer on which to draw - void setLayer(BoardItem *layer) { layer_ = layer; } + void setLayerId(int layer); + + //! Get the used drawing board + BoardItem *board() const { return board_; } - //! Get the used layer - BoardItem *layer() const { return layer_; } + //! Get the used layer ID + int layer() const { return layer_; } //! Set brush to use void setBrush(const dpcore::Brush& brush) { brush_ = brush; } @@ -62,7 +75,9 @@ class User int id_; dpcore::Brush brush_; - BoardItem *layer_; + BoardItem *board_; + widgets::LayerList *layerlist_; + int layer_; dpcore::Point lastpoint_; bool strokestarted_; qreal strokelen_; diff --git a/src/client/version.h b/src/client/version.h index 104c79481..0ac8ff1f3 100644 --- a/src/client/version.h +++ b/src/client/version.h @@ -18,7 +18,7 @@ namespace version { static const int level = 1; //! Version string -static const char string[] = "0.6.0"; +static const char string[] = "0.7.0"; } diff --git a/src/shared/net/constants.h b/src/shared/net/constants.h index 9d2399553..3996b1725 100644 --- a/src/shared/net/constants.h +++ b/src/shared/net/constants.h @@ -33,7 +33,7 @@ static const char MAGIC[] = {'D', 'r', 'P', 'l'}; * The protocol revision. Servers should drop connections from * clients with a different protocol revision. */ -static const int REVISION = 1; +static const int REVISION = 2; /** * The default port to use. diff --git a/src/shared/net/protocol.cpp b/src/shared/net/protocol.cpp index 910df08c6..1d06d392c 100644 --- a/src/shared/net/protocol.cpp +++ b/src/shared/net/protocol.cpp @@ -34,10 +34,13 @@ unsigned int Packet::sniffLength(const QByteArray& data) { QBuffer buf(const_cast(&data)); buf.open(QBuffer::ReadOnly); - char type = utils::read8(buf); + // Read the packet length (incl. the type field) quint16 len = utils::read16(buf); + if(len<1) + return 0; + --len; + char type = utils::read8(buf); - // Do some sanity checks switch(type) { case STROKE: if((len-1)%5) @@ -52,7 +55,7 @@ unsigned int Packet::sniffLength(const QByteArray& data) { return 0; break; case MESSAGE: - if(len>2048) // Any longer than this is suspicious + if(len>2048) // Any longer than this is suspicious (arbitrary) return 0; break; case BINARY_CHUNK: @@ -66,7 +69,8 @@ unsigned int Packet::sniffLength(const QByteArray& data) { // Unknown packet type return 0; } - // If the message passed the sanity checks, return the length + 3 header len + // If the message passed the sanity checks, return the full lenght of the + // packet (length and type fields included) return len+3; } @@ -74,8 +78,8 @@ Packet *Packet::deserialize(const QByteArray& data) { QBuffer buf(const_cast(&data)); buf.open(QBuffer::ReadOnly); + quint16 len = utils::read16(buf) - 1; char type = utils::read8(buf); - quint16 len = utils::read16(buf); // Deserialize correct message type switch(type) { @@ -93,8 +97,8 @@ QByteArray Packet::serialize() const { QBuffer buf; buf.open(QBuffer::WriteOnly); + utils::write16(buf, payloadLength() + 1); buf.putChar(_type); - utils::write16(buf, payloadLength()); serializeBody(buf); return buf.buffer(); diff --git a/src/shared/net/protocol.h b/src/shared/net/protocol.h index 06bd232a9..4a70f5962 100644 --- a/src/shared/net/protocol.h +++ b/src/shared/net/protocol.h @@ -35,7 +35,8 @@ enum PacketType { BINARY_CHUNK, TOOL_SELECT, STROKE, - STROKE_END + STROKE_END, + LAYER_SELECT }; /** diff --git a/src/shared/net/toolselect.cpp b/src/shared/net/toolselect.cpp index 354665d2b..d74dcb2a8 100644 --- a/src/shared/net/toolselect.cpp +++ b/src/shared/net/toolselect.cpp @@ -54,5 +54,17 @@ void ToolSelect::serializeBody(QIODevice& data) const { data.putChar(_space); } +LayerSelect *LayerSelect::deserialize(QIODevice& data, int len) { + Q_ASSERT(len == 2); + int user = utils::read8(data); + int layer = utils::read8(data); + return new LayerSelect(user, layer); +} + +void LayerSelect::serializeBody(QIODevice& data) const { + data.putChar(_user); + data.putChar(_layer); +} + } diff --git a/src/shared/net/toolselect.h b/src/shared/net/toolselect.h index 915034016..f6b67a758 100644 --- a/src/shared/net/toolselect.h +++ b/src/shared/net/toolselect.h @@ -121,6 +121,50 @@ class ToolSelect : public Packet { const int _s1, _s0, _h1, _h0, _space; }; +/** + * A layer select message selects which layer the tool operates on. The + * message is sent when a user selects a layer, or just before a StrokePoint + * message, like ToolSelect. + * If no layer select message has been sent, the bottom-most layer is assumed. + */ +class LayerSelect : public Packet { + public: + + /** + * Construct a layer select message + * @param user user who selects a layer + * @param layer ID of the layer + */ + LayerSelect(int user, int layer) : + Packet(LAYER_SELECT), _user(user), _layer(layer) { } + + /** + * Deserialize a tool select message + */ + static LayerSelect *deserialize(QIODevice& data, int len); + + /** + * Get the length of the tool select payload + */ + unsigned int payloadLength() const { return 1; } + + /** + * Get the ID of the user whose tool to change. + */ + int user() const { return _user; } + + /** + * Get the ID of the layer + */ + int layer() const { return _layer; } + + protected: + void serializeBody(QIODevice& data) const; + + private: + int _user, _layer; +}; + } #endif diff --git a/src/shared/server/client.cpp b/src/shared/server/client.cpp index a041c4b4f..e33e3d46c 100644 --- a/src/shared/server/client.cpp +++ b/src/shared/server/client.cpp @@ -26,6 +26,7 @@ #include "../net/messagequeue.h" #include "../net/login.h" #include "../net/message.h" +#include "../net/toolselect.h" #include "../net/binary.h" #include "../net/utils.h" @@ -51,7 +52,7 @@ QString randomSalt() { * @param locked does the client start out as locked? */ Client::Client(int id, Server *server, QTcpSocket *socket, bool locked) - : QObject(server), _id(id), _server(server), _socket(new protocol::MessageQueue(socket, this)), _state(CONNECT), _lock(locked), _syncready(false), _giveraster(false), _address(socket->peerAddress()) + : QObject(server), _id(id), _server(server), _socket(new protocol::MessageQueue(socket, this)), _state(CONNECT), _lock(locked), _syncready(false), _giveraster(false), _lastLayer(-1), _address(socket->peerAddress()) { _server->printDebug("New client connected from " + socket->peerAddress().toString() + " and was given ID " + QString::number(id)); connect(_socket, SIGNAL(messageAvailable()), this, SLOT(newData())); @@ -69,6 +70,7 @@ void Client::newData() { case protocol::STROKE: case protocol::STROKE_END: case protocol::TOOL_SELECT: + case protocol::LAYER_SELECT: handleDrawing(pkt); break; case protocol::LOGIN_ID: @@ -414,6 +416,8 @@ void Client::handleDrawing(const protocol::Packet *pkt) { _server->board().addDrawingCommand(msg); if(pkt->type()==protocol::TOOL_SELECT) _lastTool = msg; + else if(pkt->type()==protocol::LAYER_SELECT) + _lastLayer = static_cast(pkt)->layer(); _sentStroke = true; } diff --git a/src/shared/server/client.h b/src/shared/server/client.h index dbe314370..3247e735d 100644 --- a/src/shared/server/client.h +++ b/src/shared/server/client.h @@ -111,6 +111,9 @@ class Client : public QObject { //! Get the last tool select message received from this user const QByteArray& lastTool() const { return _lastTool; } + //! Get the last layer select received from this user + int lastLayer() const { return _lastLayer; } + //! Get an info message about this user. QString toMessage() const; @@ -158,6 +161,7 @@ class Client : public QObject { bool _giveraster; int _rasteroffset; QByteArray _lastTool; + int _lastLayer; QString _salt; QHostAddress _address; diff --git a/src/shared/server/server.cpp b/src/shared/server/server.cpp index 2189ffb68..6aca1fb6a 100644 --- a/src/shared/server/server.cpp +++ b/src/shared/server/server.cpp @@ -24,6 +24,7 @@ #include "server.h" #include "client.h" #include "../net/message.h" +#include "../net/toolselect.h" namespace server { @@ -248,6 +249,8 @@ void Server::briefClient(int id) { nc->sendRaw(protocol::Message(c->toMessage()).serialize()); if(c->lastTool().size()>0) nc->sendRaw(c->lastTool()); + if(c->lastLayer() >= 0) + nc->sendRaw(protocol::LayerSelect(c->id(), c->lastLayer()).serialize()); } } }